diff --git a/packages/rstack/src/fmt/config.ts b/packages/rstack/src/fmt/config.ts index 949116a..86ba749 100644 --- a/packages/rstack/src/fmt/config.ts +++ b/packages/rstack/src/fmt/config.ts @@ -1,5 +1,6 @@ -import { dirname, relative } from 'node:path'; +import { dirname } from 'node:path'; import micromatch from 'micromatch'; +import { createRelativePathResolver } from './relativePath.ts'; import type { FmtConfig, FmtConfigDefinition, @@ -14,6 +15,7 @@ type ResolveFmtConfigOptions = { }; type PathMatcher = (filePath: string) => boolean; +type FmtOptionsResolver = (filePath: string) => ResolvedFmtOptions; const neverMatches: PathMatcher = () => false; @@ -87,26 +89,30 @@ const normalizeFmtConfig = (config: FmtConfig | undefined, rootPath: string): Re }; }; -/** Applies matching overrides to the shared formatter options. */ -const resolveFmtOptions = (filePath: string, config: ResolvedFmtConfig): ResolvedFmtOptions => { +/** Creates a reusable resolver for applying per-file formatter overrides. */ +const createFmtOptionsResolver = (config: ResolvedFmtConfig): FmtOptionsResolver => { if (config.overrides.length === 0) { - return config.baseOptions; + return () => config.baseOptions; } - let options = config.baseOptions; - const relativeFilePath = relative(config.rootPath, filePath); + const resolveRelativePath = createRelativePathResolver(config.rootPath); - for (const override of config.overrides) { - if (!override.options || !override.matches(relativeFilePath)) { - continue; - } - if (options === config.baseOptions) { - options = { ...options }; + return (filePath) => { + let options = config.baseOptions; + const relativeFilePath = resolveRelativePath(filePath); + + for (const override of config.overrides) { + if (!override.options || !override.matches(relativeFilePath)) { + continue; + } + if (options === config.baseOptions) { + options = { ...options }; + } + Object.assign(options, override.options); } - Object.assign(options, override.options); - } - return options; + return options; + }; }; /** Resolves a formatter config definition and its project root. */ @@ -121,4 +127,5 @@ const resolveFmtConfig = async ({ return normalizeFmtConfig(config, rootPath); }; -export { normalizeFmtConfig, resolveFmtConfig, resolveFmtOptions }; +export { createFmtOptionsResolver, normalizeFmtConfig, resolveFmtConfig }; +export type { FmtOptionsResolver }; diff --git a/packages/rstack/src/fmt/discovery.ts b/packages/rstack/src/fmt/discovery.ts index 532859b..bacd755 100644 --- a/packages/rstack/src/fmt/discovery.ts +++ b/packages/rstack/src/fmt/discovery.ts @@ -1,11 +1,14 @@ -import { resolveFmtOptions } from './config.ts'; +import { createFmtOptionsResolver, type FmtOptionsResolver } from './config.ts'; import { discoverFmtPaths } from './discoverPaths.ts'; import { createIgnoreMatcher } from './ignore.ts'; -import type { DiscoverFmtFilesOptions, FmtFileRequest, ResolvedFmtConfig } from './types.ts'; +import type { DiscoverFmtFilesOptions, FmtFileRequest } from './types.ts'; -const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFileRequest => ({ +const createFileRequest = ( + filePath: string, + resolveOptions: FmtOptionsResolver, +): FmtFileRequest => ({ path: filePath, - options: resolveFmtOptions(filePath, config), + options: resolveOptions(filePath), }); /** Discovers worker-ready files without automatically reading Prettier config or ignore files. */ @@ -28,7 +31,8 @@ const discoverFmtFiles = async ({ } const filePaths = candidates.filter((filePath) => !isIgnored(filePath)); - const files = filePaths.map((filePath) => createFileRequest(filePath, config)); + const resolveOptions = createFmtOptionsResolver(config); + const files = filePaths.map((filePath) => createFileRequest(filePath, resolveOptions)); if (!files.some((file) => file.options.plugins?.length)) { return files; } diff --git a/packages/rstack/src/fmt/stdin.ts b/packages/rstack/src/fmt/stdin.ts index ec69200..f6b6a5f 100644 --- a/packages/rstack/src/fmt/stdin.ts +++ b/packages/rstack/src/fmt/stdin.ts @@ -1,4 +1,5 @@ import { resolve } from 'node:path'; +import { createFmtOptionsResolver } from './config.ts'; import { createFileRequest } from './discovery.ts'; import { formatFmtSource } from './format.ts'; import { createIgnoreMatcher } from './ignore.ts'; @@ -78,7 +79,7 @@ const runFmtStdin = async ({ return; } - let file = createFileRequest(absolutePath, config); + let file = createFileRequest(absolutePath, createFmtOptionsResolver(config)); if (file.options.plugins?.length) { const { createFmtPluginResolver } = await import( /* rspackChunkName: 'fmtPlugins' */ diff --git a/packages/rstack/tests/fmt/config.test.ts b/packages/rstack/tests/fmt/config.test.ts index 605b8c5..e4d083b 100644 --- a/packages/rstack/tests/fmt/config.test.ts +++ b/packages/rstack/tests/fmt/config.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { expect, test } from 'rstack/test'; -import { normalizeFmtConfig, resolveFmtOptions } from '../../src/fmt/config.ts'; +import { createFmtOptionsResolver, normalizeFmtConfig } from '../../src/fmt/config.ts'; const rootPath = path.join(import.meta.dirname, 'project'); @@ -12,8 +12,9 @@ test('reuses base options when no override matches', () => { }, rootPath, ); + const resolveOptions = createFmtOptionsResolver(config); - expect(resolveFmtOptions(path.join(rootPath, 'index.js'), config)).toBe(config.baseOptions); + expect(resolveOptions(path.join(rootPath, 'index.js'))).toBe(config.baseOptions); }); test('applies basename and path overrides in declaration order', () => { @@ -38,12 +39,25 @@ test('applies basename and path overrides in declaration order', () => { }, rootPath, ); + const resolveOptions = createFmtOptionsResolver(config); - const options = resolveFmtOptions(path.join(rootPath, 'src/index.ts'), config); - const testOptions = resolveFmtOptions(path.join(rootPath, 'src/index.test.ts'), config); + const options = resolveOptions(path.join(rootPath, 'src/index.ts')); + const testOptions = resolveOptions(path.join(rootPath, 'src/index.test.ts')); expect(options).not.toBe(config.baseOptions); expect(options).toEqual({ semi: true, singleQuote: true, tabWidth: 4 }); expect(testOptions).toEqual({ singleQuote: true }); expect(config.baseOptions).toEqual({ singleQuote: false }); }); + +test('applies overrides outside the config root', () => { + const config = normalizeFmtConfig( + { + overrides: [{ files: '../shared/*.ts', options: { semi: false } }], + }, + rootPath, + ); + const resolveOptions = createFmtOptionsResolver(config); + + expect(resolveOptions(path.join(rootPath, '../shared/index.ts'))).toEqual({ semi: false }); +});