From 8cbee8128c65f436ef196ea2cc4382819ac36cd3 Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 3 Oct 2026 09:27:00 +0800 Subject: [PATCH 1/2] refactor(config): flatten nested shared config layers --- packages/rstack/src/configLayers.ts | 33 +++++++++++++- packages/rstack/tests/config/flatten.test.ts | 45 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/rstack/tests/config/flatten.test.ts diff --git a/packages/rstack/src/configLayers.ts b/packages/rstack/src/configLayers.ts index e1375e1..077dd48 100644 --- a/packages/rstack/src/configLayers.ts +++ b/packages/rstack/src/configLayers.ts @@ -6,7 +6,11 @@ import type { ConfigParams as LibConfigParams, RslibConfig } from '@rslib/core'; import type { RslintConfig } from '@rslint/core'; import type { UserConfig as RspressConfig } from '@rspress/core'; import type { RstestConfig } from '@rstest/core'; -import type { Configs } from './config.ts'; +import { + type Configs, + type RstackConfig, + normalizeRstackConfig, +} from './config.ts'; import type { FmtConfig } from './fmt/types.ts'; import type { StagedConfig } from './staged.ts'; @@ -26,6 +30,33 @@ type ConfigArgs = K extends 'app' ? [params: LibConfigParams] : []; +/** Expand inherited configs before their children, preserving every occurrence. */ +export const flattenConfigLayers = ( + configs: readonly RstackConfig[], +): Configs[] => { + const layers: Configs[] = []; + const ancestors = new Map(); + + const visit = (config: RstackConfig, path: string): void => { + const ancestorPath = ancestors.get(config); + if (ancestorPath !== undefined) { + throw new Error( + `Circular config inheritance at ${path}: references ${ancestorPath}.`, + ); + } + + ancestors.set(config, path); + config.extends?.forEach((inherited, index) => { + visit(inherited, `${path}.extends[${index}]`); + }); + layers.push(normalizeRstackConfig(config)); + ancestors.delete(config); + }; + + configs.forEach((config, index) => visit(config, `extends[${index}]`)); + return layers; +}; + /** * Resolve one tool from ordered, normalized config layers. Lint factories are * already wrapped by define.lint. This function does not merge the results. diff --git a/packages/rstack/tests/config/flatten.test.ts b/packages/rstack/tests/config/flatten.test.ts new file mode 100644 index 0000000..5e3c88b --- /dev/null +++ b/packages/rstack/tests/config/flatten.test.ts @@ -0,0 +1,45 @@ +import { expect, rs, test } from 'rstack/test'; +import type { RstackConfig } from '../../src/config.ts'; +import { flattenConfigLayers } from '../../src/configLayers.ts'; + +test('flattens nested configs in order and preserves repeated references without resolving factories', () => { + const app = rs.fn(() => ({})); + const base = { app, fmt: { printWidth: 80 } }; + const left = { extends: [base], fmt: { singleQuote: true } }; + const nested = { extends: [left], fmt: { semi: false } }; + const right = { extends: [base], fmt: { tabWidth: 4 } }; + const configs: readonly RstackConfig[] = [nested, right, base]; + + expect(flattenConfigLayers(configs)).toEqual([ + base, + { fmt: { singleQuote: true } }, + { fmt: { semi: false } }, + base, + { fmt: { tabWidth: 4 } }, + base, + ]); + expect(app).not.toHaveBeenCalled(); + expect(left.extends).toEqual([base]); + expect(nested.extends).toEqual([left]); + expect(right.extends).toEqual([base]); + expect(configs).toEqual([nested, right, base]); +}); + +test('reports the inheritance positions of a direct cycle', () => { + const config: RstackConfig = {}; + config.extends = [config]; + + expect(() => flattenConfigLayers([config])).toThrow( + 'Circular config inheritance at extends[0].extends[0]: references extends[0].', + ); +}); + +test('reports the inheritance positions of an indirect cycle', () => { + const first: RstackConfig = {}; + const second: RstackConfig = { extends: [first] }; + first.extends = [second]; + + expect(() => flattenConfigLayers([first])).toThrow( + 'Circular config inheritance at extends[0].extends[0].extends[0]: references extends[0].', + ); +}); From 300968fd2ac39e8345a7082fe289dae61e4e1ebe Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 3 Oct 2026 09:36:55 +0800 Subject: [PATCH 2/2] refactor(config): skip cycle tracking for leaf configs --- packages/rstack/src/configLayers.ts | 24 +++++++++++--------- packages/rstack/tests/config/flatten.test.ts | 12 ++++++---- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/rstack/src/configLayers.ts b/packages/rstack/src/configLayers.ts index 077dd48..8795a91 100644 --- a/packages/rstack/src/configLayers.ts +++ b/packages/rstack/src/configLayers.ts @@ -38,19 +38,21 @@ export const flattenConfigLayers = ( const ancestors = new Map(); const visit = (config: RstackConfig, path: string): void => { - const ancestorPath = ancestors.get(config); - if (ancestorPath !== undefined) { - throw new Error( - `Circular config inheritance at ${path}: references ${ancestorPath}.`, - ); - } + if (config.extends?.length) { + const ancestorPath = ancestors.get(config); + if (ancestorPath !== undefined) { + throw new Error( + `Circular config inheritance at ${path}: references ${ancestorPath}.`, + ); + } - ancestors.set(config, path); - config.extends?.forEach((inherited, index) => { - visit(inherited, `${path}.extends[${index}]`); - }); + ancestors.set(config, path); + config.extends.forEach((inherited, index) => { + visit(inherited, `${path}.extends[${index}]`); + }); + ancestors.delete(config); + } layers.push(normalizeRstackConfig(config)); - ancestors.delete(config); }; configs.forEach((config, index) => visit(config, `extends[${index}]`)); diff --git a/packages/rstack/tests/config/flatten.test.ts b/packages/rstack/tests/config/flatten.test.ts index 5e3c88b..583ff13 100644 --- a/packages/rstack/tests/config/flatten.test.ts +++ b/packages/rstack/tests/config/flatten.test.ts @@ -19,10 +19,14 @@ test('flattens nested configs in order and preserves repeated references without base, ]); expect(app).not.toHaveBeenCalled(); - expect(left.extends).toEqual([base]); - expect(nested.extends).toEqual([left]); - expect(right.extends).toEqual([base]); - expect(configs).toEqual([nested, right, base]); + expect(configs).toEqual([ + { + extends: [{ extends: [base], fmt: { singleQuote: true } }], + fmt: { semi: false }, + }, + { extends: [base], fmt: { tabWidth: 4 } }, + base, + ]); }); test('reports the inheritance positions of a direct cycle', () => {