From 78bfb611578a73161166aaf2bfd14351cdc9c8c5 Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 3 Oct 2026 09:09:36 +0800 Subject: [PATCH] refactor(config): merge staged config layers --- packages/rstack/src/configLayers.ts | 16 ++++++ packages/rstack/src/staged.ts | 5 +- .../rstack/tests/config/staged-merge.test.ts | 50 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 packages/rstack/tests/config/staged-merge.test.ts diff --git a/packages/rstack/src/configLayers.ts b/packages/rstack/src/configLayers.ts index dd83834c..e1375e13 100644 --- a/packages/rstack/src/configLayers.ts +++ b/packages/rstack/src/configLayers.ts @@ -63,3 +63,19 @@ export const resolveRslintConfig = async ( const configs = await resolveConfigLayers(layers, 'lint'); return configs.length > 1 ? configs.flat() : configs[0]; }; + +export const resolveStagedConfig = async ( + layers: readonly Configs[], +): Promise => { + const configs = await resolveConfigLayers(layers, 'staged'); + if (configs.length <= 1) { + return configs[0]; + } + + // Top-level task generators replace the whole config rather than glob keys. + return configs.reduce((merged, config) => + typeof merged === 'function' || typeof config === 'function' + ? config + : { ...merged, ...config }, + ); +}; diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index e364c490..ff45c2f7 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -2,6 +2,7 @@ import lintStaged from 'lint-staged'; import { parseArgs } from './cli/args.ts'; import { printCommandHelp } from './cli/help.ts'; import { loadRstackConfig } from './config.ts'; +import { resolveStagedConfig } from './configLayers.ts'; export type StagedSyncTaskGenerator = ( stagedFileNames: readonly string[], @@ -51,8 +52,8 @@ export async function runStagedCLI(args: string[]): Promise { } const { configs } = await loadRstackConfig(); - const stagedConfig = configs.staged; - if (!stagedConfig) { + const stagedConfig = await resolveStagedConfig([configs]); + if (stagedConfig === undefined) { throw new Error( 'No define.staged config found. Add define.staged({ "*": "your-command" }) to rstack config file', ); diff --git a/packages/rstack/tests/config/staged-merge.test.ts b/packages/rstack/tests/config/staged-merge.test.ts new file mode 100644 index 00000000..99c75c42 --- /dev/null +++ b/packages/rstack/tests/config/staged-merge.test.ts @@ -0,0 +1,50 @@ +import { expect, rs, test } from 'rstack/test'; +import { resolveStagedConfig } from '../../src/configLayers.ts'; + +test('shallowly merges staged globs and replaces task arrays', async () => { + const generate = rs.fn(() => 'rs fmt'); + const task = rs.fn(); + const config = await resolveStagedConfig([ + { + staged: { + '*.ts': ['rs lint', 'rs fmt'], + '*.md': generate, + '*.json': { title: 'Check JSON', task }, + }, + }, + { staged: { '*.ts': ['rs check'], '*.css': 'rs fmt' } }, + ]); + + expect(config).toEqual({ + '*.ts': ['rs check'], + '*.md': generate, + '*.json': { title: 'Check JSON', task }, + '*.css': 'rs fmt', + }); + expect(generate).not.toHaveBeenCalled(); + expect(task).not.toHaveBeenCalled(); +}); + +test('a top-level task generator replaces earlier staged config without running', async () => { + const generate = rs.fn(() => ['rs check']); + const config = await resolveStagedConfig([ + { staged: { '*.ts': 'rs lint' } }, + { staged: generate }, + ]); + + expect(config).toBe(generate); + expect(generate).not.toHaveBeenCalled(); +}); + +test('glob maps after a top-level task generator start a new staged config', async () => { + const generate = rs.fn(() => ['rs check']); + const config = await resolveStagedConfig([ + { staged: { '*.ts': 'rs lint' } }, + { staged: generate }, + { staged: { '*.md': 'rs fmt' } }, + { staged: { '*.css': 'rs fmt' } }, + ]); + + expect(config).toEqual({ '*.md': 'rs fmt', '*.css': 'rs fmt' }); + expect(generate).not.toHaveBeenCalled(); +});