From b779ae10f046401bac1c70b2d61763ef704b5786 Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 13 Aug 2026 10:31:03 +0800 Subject: [PATCH] perf(fmt): reuse resolved options --- packages/rstack/src/fmt/config.ts | 29 +++++++++++++++++++---- packages/rstack/src/fmt/plugins.ts | 17 +++++++++---- packages/rstack/tests/fmt/config.test.ts | 23 ++++++++++++++++++ packages/rstack/tests/fmt/plugins.test.ts | 4 +++- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/packages/rstack/src/fmt/config.ts b/packages/rstack/src/fmt/config.ts index 362a4527..ee0a5123 100644 --- a/packages/rstack/src/fmt/config.ts +++ b/packages/rstack/src/fmt/config.ts @@ -17,6 +17,20 @@ type ResolveFmtConfigOptions = { type PathMatcher = (filePath: string) => boolean; type FmtOptionsResolver = (filePath: string) => ResolvedFmtOptions; +/** + * Each path from the root represents an ordered sequence of matching overrides. + * A node stores the options merged along that path. + */ +type OptionsCacheNode = { + children: WeakMap; + options: ResolvedFmtOptions; +}; + +const createOptionsCacheNode = (options: ResolvedFmtOptions): OptionsCacheNode => ({ + children: new WeakMap(), + options, +}); + const neverMatches: PathMatcher = () => false; const compileMatchers = ( @@ -96,22 +110,27 @@ const createOptionsResolver = (config: ResolvedFmtConfig): FmtOptionsResolver => } const resolveRelativePath = createRelativePathResolver(config.rootPath); + const rootCacheNode = createOptionsCacheNode(config.baseOptions); return (filePath) => { - let options = config.baseOptions; + let cacheNode = rootCacheNode; const relativeFilePath = resolveRelativePath(filePath); for (const override of config.overrides) { if (!override.options || !override.matches(relativeFilePath)) { continue; } - if (options === config.baseOptions) { - options = { ...options }; + + // Reuse the merged result for this override after the current matched sequence. + let nextCacheNode = cacheNode.children.get(override.options); + if (!nextCacheNode) { + nextCacheNode = createOptionsCacheNode({ ...cacheNode.options, ...override.options }); + cacheNode.children.set(override.options, nextCacheNode); } - Object.assign(options, override.options); + cacheNode = nextCacheNode; } - return options; + return cacheNode.options; }; }; diff --git a/packages/rstack/src/fmt/plugins.ts b/packages/rstack/src/fmt/plugins.ts index 312dfbb1..e7a33e16 100644 --- a/packages/rstack/src/fmt/plugins.ts +++ b/packages/rstack/src/fmt/plugins.ts @@ -94,11 +94,12 @@ const createFingerprintResolver = (): FingerprintResolver => { /** Creates a project-root resolver for plugins in final per-file options. */ const createPluginResolver = (rootPath: string): FmtPluginResolver => { const parentUrl = pathToFileURL(join(rootPath, 'index.js')); - const cache = new Map(); + const pluginCache = new Map(); + const optionsCache = new WeakMap(); const resolvePlugin = (plugin: FmtPluginSpecifier): string => { const specifier = plugin instanceof URL ? plugin.href : plugin; - const cached = cache.get(specifier); + const cached = pluginCache.get(specifier); if (cached !== undefined) { return cached; } @@ -119,11 +120,16 @@ const createPluginResolver = (rootPath: string): FmtPluginResolver => { } } - cache.set(specifier, resolved); + pluginCache.set(specifier, resolved); return resolved; }; return (options) => { + const cached = optionsCache.get(options); + if (cached !== undefined) { + return cached; + } + const { plugins } = options; if (!plugins?.length) { return options; @@ -136,10 +142,11 @@ const createPluginResolver = (rootPath: string): FmtPluginResolver => { } const resolvedPlugins = plugins.map(resolvePlugin); - - return resolvedPlugins.every((plugin, index) => plugin === plugins[index]) + const resolvedOptions = resolvedPlugins.every((plugin, index) => plugin === plugins[index]) ? options : { ...options, plugins: resolvedPlugins }; + optionsCache.set(options, resolvedOptions); + return resolvedOptions; }; }; diff --git a/packages/rstack/tests/fmt/config.test.ts b/packages/rstack/tests/fmt/config.test.ts index c2f32844..fe85d292 100644 --- a/packages/rstack/tests/fmt/config.test.ts +++ b/packages/rstack/tests/fmt/config.test.ts @@ -50,6 +50,29 @@ test('applies basename and path overrides in declaration order', () => { expect(config.baseOptions).toEqual({ singleQuote: false }); }); +test('reuses options for the same override combination', () => { + const config = normalizeFmtConfig( + { + singleQuote: false, + overrides: [ + { files: '*.ts', options: { semi: false } }, + { files: 'src/**/*.ts', options: { singleQuote: true } }, + ], + }, + rootPath, + ); + const resolveOptions = createOptionsResolver(config); + + const first = resolveOptions(path.join(rootPath, 'src/first.ts')); + const second = resolveOptions(path.join(rootPath, 'src/second.ts')); + const outside = resolveOptions(path.join(rootPath, 'outside.ts')); + + expect(first).toBe(second); + expect(first).not.toBe(outside); + expect(first).toEqual({ semi: false, singleQuote: true }); + expect(outside).toEqual({ semi: false, singleQuote: false }); +}); + test('applies overrides outside the config root', () => { const config = normalizeFmtConfig( { diff --git a/packages/rstack/tests/fmt/plugins.test.ts b/packages/rstack/tests/fmt/plugins.test.ts index 80f998e8..162b3e69 100644 --- a/packages/rstack/tests/fmt/plugins.test.ts +++ b/packages/rstack/tests/fmt/plugins.test.ts @@ -40,7 +40,8 @@ test('resolves plugin specifiers from the config root', async () => { ], }; - const resolved = createPluginResolver(rootPath)(options); + const resolvePlugins = createPluginResolver(rootPath); + const resolved = resolvePlugins(options); expect(resolved.plugins).toEqual([ pathToFileURL(packageEntry).href, @@ -50,6 +51,7 @@ test('resolves plugin specifiers from the config root', async () => { 'data:text/javascript,export default {}', ]); expect(options.plugins[0]).toBe('prettier-plugin-packagejson'); + expect(resolvePlugins(options)).toBe(resolved); }); });