diff --git a/.changeset/cute-files-pump.md b/.changeset/cute-files-pump.md new file mode 100644 index 00000000..e7570cd0 --- /dev/null +++ b/.changeset/cute-files-pump.md @@ -0,0 +1,5 @@ +--- +'@css-modules-kit/core': minor +--- + +feat(core): support `${configDir}` in tsconfig options diff --git a/packages/core/src/config.test.ts b/packages/core/src/config.test.ts index 05deaa18..f8c7d0e4 100644 --- a/packages/core/src/config.test.ts +++ b/packages/core/src/config.test.ts @@ -268,6 +268,29 @@ describe('readConfigFile', () => { }), ); }); + // FIXME + test.fails('resolves relative paths against the defining tsconfig directory', async () => { + const iff = await createIFF({ + 'tsconfig.base.json': dedent` + { + "include": ["src"], + "exclude": ["dist"], + "cmkOptions": { + "dtsOutDir": "generated" + } + } + `, + 'app/tsconfig.json': dedent` + { + "extends": "../tsconfig.base.json" + } + `, + }); + const result = readConfigFile(iff.join('app')); + expect(result.includes).toEqual([iff.join('src')]); + expect(result.excludes).toEqual([iff.join('dist')]); + expect(result.dtsOutDir).toBe(iff.join('generated')); + }); }); describe('diagnostics', () => { test('returns diagnostics and a config object with error values excluded if config file has semantic errors', async () => { @@ -378,4 +401,54 @@ describe('readConfigFile', () => { ]); }); }); + describe('configDir template variable', () => { + // oxlint-disable-next-line no-template-curly-in-string + test('resolve ${configDir} with the entry tsconfig directory', async () => { + const iff = await createIFF({ + 'tsconfig.base.json': dedent` + { + "include": ["\${configDir}/src"], + "exclude": ["\${configDir}/dist"], + "cmkOptions": { + "dtsOutDir": "\${configDir}/generated" + } + } + `, + 'app/tsconfig.json': dedent` + { + "extends": "../tsconfig.base.json", + } + `, + }); + const result = readConfigFile(iff.join('app')); + expect(result.includes).toEqual([iff.join('app/src')]); + expect(result.excludes).toEqual([iff.join('app/dist')]); + expect(result.dtsOutDir).toBe(iff.join('app/generated')); + }); + // oxlint-disable-next-line no-template-curly-in-string + test('does not replace ${configDir} if it is not at the start of the path', async () => { + const iff = await createIFF({ + 'tsconfig.json': dedent` + { + "include": ["./\${configDir}/src"] + } + `, + }); + const result = readConfigFile(iff.rootDir); + // oxlint-disable-next-line no-template-curly-in-string + expect(result.includes).toEqual([iff.join('${configDir}/src')]); + }); + // oxlint-disable-next-line no-template-curly-in-string + test('replaces ${configDir} case-insensitively', async () => { + const iff = await createIFF({ + 'tsconfig.json': dedent` + { + "include": ["\${CONFIGDIR}/src"] + } + `, + }); + const result = readConfigFile(iff.rootDir); + expect(result.includes).toEqual([iff.join('src')]); + }); + }); }); diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 1e381852..0986497a 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -229,6 +229,11 @@ function parseTsConfigFile(fileName: string) { }; } +// https://github.com/microsoft/TypeScript/blob/55423abe4d029017f19b6e4c32097591994836b4/src/compiler/commandLineParser.ts#L3299-L3328 +function getSubstitutedPath(path: string, basePath: string) { + return join(basePath, path.replace(/^\$\{configDir}/i, './')); +} + /** * Reads the `tsconfig.json` file and returns the normalized config. * Even if the `tsconfig.json` file contains syntax or semantic errors, @@ -256,12 +261,15 @@ export function readConfigFile(project: string): CMKConfig { } const basePath = dirname(configFileName); + return { // If `include` is not specified, fallback to the default include spec。 // ref: https://github.com/microsoft/TypeScript/blob/caf1aee269d1660b4d2a8b555c2d602c97cb28d7/src/compiler/commandLineParser.ts#L3102 - includes: (parsedTsConfig.config.includes ?? [DEFAULT_INCLUDE_SPEC]).map((i) => join(basePath, i)), - excludes: (parsedTsConfig.config.excludes ?? []).map((e) => join(basePath, e)), - dtsOutDir: join(basePath, parsedTsConfig.config.dtsOutDir ?? 'generated'), + includes: (parsedTsConfig.config.includes ?? [DEFAULT_INCLUDE_SPEC]).map((path) => + getSubstitutedPath(path, basePath), + ), + excludes: (parsedTsConfig.config.excludes ?? []).map((path) => getSubstitutedPath(path, basePath)), + dtsOutDir: getSubstitutedPath(parsedTsConfig.config.dtsOutDir ?? 'generated', basePath), arbitraryExtensions: parsedTsConfig.config.arbitraryExtensions ?? false, namedExports: parsedTsConfig.config.namedExports ?? false, prioritizeNamedImports: parsedTsConfig.config.prioritizeNamedImports ?? false,