Skip to content

Commit 8cbee81

Browse files
committed
refactor(config): flatten nested shared config layers
1 parent ca3c64a commit 8cbee81

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

‎packages/rstack/src/configLayers.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import type { ConfigParams as LibConfigParams, RslibConfig } from '@rslib/core';
66
import type { RslintConfig } from '@rslint/core';
77
import type { UserConfig as RspressConfig } from '@rspress/core';
88
import type { RstestConfig } from '@rstest/core';
9-
import type { Configs } from './config.ts';
9+
import {
10+
type Configs,
11+
type RstackConfig,
12+
normalizeRstackConfig,
13+
} from './config.ts';
1014
import type { FmtConfig } from './fmt/types.ts';
1115
import type { StagedConfig } from './staged.ts';
1216

@@ -26,6 +30,33 @@ type ConfigArgs<K extends keyof Configs> = K extends 'app'
2630
? [params: LibConfigParams]
2731
: [];
2832

33+
/** Expand inherited configs before their children, preserving every occurrence. */
34+
export const flattenConfigLayers = (
35+
configs: readonly RstackConfig[],
36+
): Configs[] => {
37+
const layers: Configs[] = [];
38+
const ancestors = new Map<RstackConfig, string>();
39+
40+
const visit = (config: RstackConfig, path: string): void => {
41+
const ancestorPath = ancestors.get(config);
42+
if (ancestorPath !== undefined) {
43+
throw new Error(
44+
`Circular config inheritance at ${path}: references ${ancestorPath}.`,
45+
);
46+
}
47+
48+
ancestors.set(config, path);
49+
config.extends?.forEach((inherited, index) => {
50+
visit(inherited, `${path}.extends[${index}]`);
51+
});
52+
layers.push(normalizeRstackConfig(config));
53+
ancestors.delete(config);
54+
};
55+
56+
configs.forEach((config, index) => visit(config, `extends[${index}]`));
57+
return layers;
58+
};
59+
2960
/**
3061
* Resolve one tool from ordered, normalized config layers. Lint factories are
3162
* already wrapped by define.lint. This function does not merge the results.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect, rs, test } from 'rstack/test';
2+
import type { RstackConfig } from '../../src/config.ts';
3+
import { flattenConfigLayers } from '../../src/configLayers.ts';
4+
5+
test('flattens nested configs in order and preserves repeated references without resolving factories', () => {
6+
const app = rs.fn(() => ({}));
7+
const base = { app, fmt: { printWidth: 80 } };
8+
const left = { extends: [base], fmt: { singleQuote: true } };
9+
const nested = { extends: [left], fmt: { semi: false } };
10+
const right = { extends: [base], fmt: { tabWidth: 4 } };
11+
const configs: readonly RstackConfig[] = [nested, right, base];
12+
13+
expect(flattenConfigLayers(configs)).toEqual([
14+
base,
15+
{ fmt: { singleQuote: true } },
16+
{ fmt: { semi: false } },
17+
base,
18+
{ fmt: { tabWidth: 4 } },
19+
base,
20+
]);
21+
expect(app).not.toHaveBeenCalled();
22+
expect(left.extends).toEqual([base]);
23+
expect(nested.extends).toEqual([left]);
24+
expect(right.extends).toEqual([base]);
25+
expect(configs).toEqual([nested, right, base]);
26+
});
27+
28+
test('reports the inheritance positions of a direct cycle', () => {
29+
const config: RstackConfig = {};
30+
config.extends = [config];
31+
32+
expect(() => flattenConfigLayers([config])).toThrow(
33+
'Circular config inheritance at extends[0].extends[0]: references extends[0].',
34+
);
35+
});
36+
37+
test('reports the inheritance positions of an indirect cycle', () => {
38+
const first: RstackConfig = {};
39+
const second: RstackConfig = { extends: [first] };
40+
first.extends = [second];
41+
42+
expect(() => flattenConfigLayers([first])).toThrow(
43+
'Circular config inheritance at extends[0].extends[0].extends[0]: references extends[0].',
44+
);
45+
});

0 commit comments

Comments
 (0)