Skip to content

Commit 4eeae90

Browse files
committed
refactor(config): simplify config layer resolution and tests
1 parent 10b9eeb commit 4eeae90

2 files changed

Lines changed: 33 additions & 160 deletions

File tree

‎packages/rstack/src/configLayers.ts‎

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ import type { Configs } from './config.ts';
1010
import type { FmtConfig } from './fmt/types.ts';
1111
import type { StagedConfig } from './staged.ts';
1212

13-
/** Internal definitions, including the deferred lint factory created by define.lint. */
14-
export type ConfigLayer = Readonly<Configs>;
15-
16-
/** Ordered from the lowest to the highest precedence; no tool merge rules here. */
17-
export type ConfigLayers = readonly ConfigLayer[];
18-
1913
type ConfigValues = {
2014
app: RsbuildConfig;
2115
lib: RslibConfig;
@@ -32,19 +26,12 @@ type ConfigArgs<K extends keyof Configs> = K extends 'app'
3226
? [params: LibConfigParams]
3327
: [];
3428

35-
/** Collect definitions without running factories or modifying the supplied layers. */
36-
export const createConfigLayers = (
37-
project: ConfigLayer,
38-
inherited: ConfigLayers = [],
39-
): ConfigLayers => [...inherited, project];
40-
4129
/**
42-
* Resolve only the requested tool, in layer order. Missing definitions contribute
43-
* nothing; defaults and merging belong to the tool adapter. Each call evaluates
44-
* its factories anew, without caching across native parameters or loads.
30+
* Resolve one tool from ordered, normalized config layers. Lint factories are
31+
* already wrapped by define.lint; merging belongs to the tool adapters.
4532
*/
4633
export const resolveConfigLayers = async <K extends keyof Configs>(
47-
layers: ConfigLayers,
34+
layers: readonly Configs[],
4835
kind: K,
4936
...args: ConfigArgs<K>
5037
): Promise<ConfigValues[K][]> => {
Lines changed: 30 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,56 @@
11
import type { ConfigParams } from 'rstack/app';
22
import { expect, rs, test } from 'rstack/test';
3-
import {
4-
createConfigLayers,
5-
resolveConfigLayers,
6-
type ConfigLayer,
7-
} from '../../src/configLayers.ts';
3+
import { resolveConfigLayers } from '../../src/configLayers.ts';
84

9-
test('collects inherited layers before the project without resolving definitions', () => {
10-
const factory = rs.fn(() => ({}));
11-
const base = Object.freeze({ app: factory });
12-
const team = Object.freeze({ test: factory });
13-
const project = Object.freeze({ app: factory });
14-
const inherited = Object.freeze([base, team]);
15-
16-
const layers = createConfigLayers(project, inherited);
17-
18-
expect(layers).toEqual([base, team, project]);
19-
expect(layers[0]).toBe(base);
20-
expect(layers[2]).toBe(project);
21-
expect(inherited).toEqual([base, team]);
22-
expect(createConfigLayers(project)).toEqual([project]);
23-
expect(factory).not.toHaveBeenCalled();
24-
});
25-
26-
test.each(['app', 'lib'] as const)(
27-
'passes native parameters unchanged to every %s factory',
28-
async (kind) => {
29-
const params: ConfigParams = {
5+
test('resolves only the selected tool sequentially with native parameters', async () => {
6+
const order: string[] = [];
7+
const base = async ({ env }: ConfigParams) => {
8+
await Promise.resolve();
9+
order.push('base');
10+
return { root: env };
11+
};
12+
const project = ({ command }: ConfigParams) => {
13+
order.push('project');
14+
return { root: command };
15+
};
16+
const unrelated = rs.fn(() => ({}));
17+
const layers = [{ app: base }, { app: project, fmt: unrelated }];
18+
19+
expect(
20+
await resolveConfigLayers(layers, 'app', {
3021
command: 'build',
3122
env: 'production',
32-
envMode: 'staging',
33-
meta: { caller: 'test' },
34-
};
35-
const baseConfig = { root: 'base' };
36-
const projectConfig = { root: 'project' };
37-
const base = rs.fn((_params: ConfigParams) => baseConfig);
38-
const project = rs.fn((_params: ConfigParams) =>
39-
Promise.resolve(projectConfig),
40-
);
41-
const layers = createConfigLayers({ [kind]: project }, [{ [kind]: base }]);
42-
43-
const configs = await resolveConfigLayers(layers, kind, params);
44-
45-
expect(configs).toEqual([baseConfig, projectConfig]);
46-
expect(configs[0]).toBe(baseConfig);
47-
expect(configs[1]).toBe(projectConfig);
48-
expect(base).toHaveBeenCalledExactlyOnceWith(params);
49-
expect(project).toHaveBeenCalledExactlyOnceWith(params);
50-
expect(base.mock.calls[0][0]).toBe(params);
51-
expect(project.mock.calls[0][0]).toBe(params);
52-
},
53-
);
54-
55-
test('awaits layers sequentially and only resolves the selected tool', async () => {
56-
const started = Promise.withResolvers<void>();
57-
const ready = Promise.withResolvers<void>();
58-
const baseConfig = Object.freeze({ retry: 2 });
59-
const projectConfig = Object.freeze({ retry: 1 });
60-
const unrelated = rs.fn(() => {
61-
throw new Error('Unselected tool must stay lazy');
62-
});
63-
const project = rs.fn(() => projectConfig);
64-
const layers = createConfigLayers({ test: project, app: unrelated }, [
65-
{
66-
async test() {
67-
started.resolve();
68-
await ready.promise;
69-
return baseConfig;
70-
},
71-
fmt: unrelated,
72-
},
73-
]);
74-
75-
const resolving = resolveConfigLayers(layers, 'test');
76-
await started.promise;
77-
try {
78-
expect(project).not.toHaveBeenCalled();
79-
} finally {
80-
ready.resolve();
81-
}
82-
const configs = await resolving;
83-
84-
expect(configs).toEqual([baseConfig, projectConfig]);
85-
expect(configs[0]).toBe(baseConfig);
86-
expect(configs[1]).toBe(projectConfig);
87-
expect(project).toHaveBeenCalledExactlyOnceWith();
23+
}),
24+
).toEqual([{ root: 'production' }, { root: 'build' }]);
25+
expect(order).toEqual(['base', 'project']);
8826
expect(unrelated).not.toHaveBeenCalled();
8927
});
9028

91-
test.each(['doc', 'test', 'lint', 'fmt'] as const)(
92-
'resolves %s values and factories without injecting arguments',
93-
async (kind) => {
94-
const values = {
95-
doc: { title: 'Docs' },
96-
test: { retry: 2 },
97-
lint: [],
98-
fmt: { singleQuote: true },
99-
};
100-
const factory = rs.fn(() => Promise.resolve(values[kind]));
101-
const layers = createConfigLayers({ [kind]: factory }, [
102-
{ [kind]: values[kind] },
103-
]);
104-
105-
const configs = await resolveConfigLayers(layers, kind);
106-
107-
expect(configs).toEqual([values[kind], values[kind]]);
108-
expect(configs[0]).toBe(values[kind]);
109-
expect(configs[1]).toBe(values[kind]);
110-
expect(factory).toHaveBeenCalledExactlyOnceWith();
111-
},
112-
);
113-
11429
test('skips missing definitions while preserving explicitly empty configs', async () => {
115-
const emptyConfig = {};
116-
const emptyLint: ConfigLayer = { lint: [] };
117-
const layers = createConfigLayers({ test: emptyConfig }, [{}, emptyLint]);
30+
const layers = [{}, { test: {}, lint: [] }, { test: () => ({ retry: 1 }) }];
11831

119-
expect(await resolveConfigLayers(layers, 'test')).toEqual([emptyConfig]);
32+
expect(await resolveConfigLayers(layers, 'test')).toEqual([{}, { retry: 1 }]);
12033
expect(await resolveConfigLayers(layers, 'lint')).toEqual([[]]);
12134
expect(await resolveConfigLayers(layers, 'fmt')).toEqual([]);
12235
});
12336

12437
test('preserves staged task generators without invoking them', async () => {
125-
const task = rs.fn((_files: readonly string[]) => 'rs lint');
126-
const generator = rs.fn((_files: readonly string[]) =>
127-
Promise.resolve(['rs fmt']),
128-
);
129-
const tasks = { '*.ts': task };
130-
const layers = createConfigLayers({ staged: generator }, [{ staged: tasks }]);
38+
const generator = rs.fn(() => ['rs fmt']);
39+
const tasks = { '*.ts': 'rs lint' };
40+
const layers = [{ staged: tasks }, { staged: generator }];
13141

132-
const configs = await resolveConfigLayers(layers, 'staged');
133-
134-
expect(configs[0]).toBe(tasks);
135-
expect(configs[1]).toBe(generator);
136-
expect(task).not.toHaveBeenCalled();
42+
expect(await resolveConfigLayers(layers, 'staged')).toEqual([
43+
tasks,
44+
generator,
45+
]);
13746
expect(generator).not.toHaveBeenCalled();
13847
});
13948

14049
test('stops at a failed factory and preserves its error', async () => {
14150
const error = new Error('Invalid shared config');
14251
const project = rs.fn(() => ({}));
143-
const layers = createConfigLayers({ fmt: project }, [
144-
{
145-
fmt: () => Promise.reject(error),
146-
},
147-
]);
52+
const layers = [{ fmt: () => Promise.reject(error) }, { fmt: project }];
14853

14954
await expect(resolveConfigLayers(layers, 'fmt')).rejects.toBe(error);
15055
expect(project).not.toHaveBeenCalled();
15156
});
152-
153-
test('keeps simultaneous resolutions with different parameters independent', async () => {
154-
const factory = rs.fn(async ({ env }: ConfigParams) => {
155-
await Promise.resolve();
156-
return { root: env };
157-
});
158-
const layers = createConfigLayers({ app: factory });
159-
160-
const configs = await Promise.all([
161-
resolveConfigLayers(layers, 'app', { command: 'build', env: 'production' }),
162-
resolveConfigLayers(layers, 'app', { command: 'dev', env: 'development' }),
163-
]);
164-
165-
expect(configs).toEqual([
166-
[{ root: 'production' }],
167-
[{ root: 'development' }],
168-
]);
169-
expect(factory).toHaveBeenCalledTimes(2);
170-
});

0 commit comments

Comments
 (0)