Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-extends-array-precedence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@css-modules-kit/core': patch
'@css-modules-kit/ts-plugin': patch
'@css-modules-kit/codegen': patch
---

fix(core, ts-plugin, codegen): let a later entry of an `extends` array take precedence over an earlier one for `cmkOptions`, as TypeScript does for `compilerOptions`
61 changes: 61 additions & 0 deletions packages/core-strada/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,67 @@ describe('readConfigFile', () => {
});
expect(readConfigFile(iff.rootDir).dtsOutDir).toBe(iff.join('generated2'));
});
test('prefers a later entry of an `extends` array over an earlier one', async () => {
const iff = await createIFF({
'tsconfig.base1.json': dedent`
{
"compilerOptions": { "module": "esnext" },
"cmkOptions": { "dtsOutDir": "generated1" }
}
`,
'tsconfig.base2.json': dedent`
{
"compilerOptions": { "module": "es2015" },
"cmkOptions": { "dtsOutDir": "generated2" }
}
`,
'tsconfig.json': dedent`
{
"extends": ["./tsconfig.base1.json", "./tsconfig.base2.json"]
}
`,
});
expect(readConfigFile(iff.rootDir)).toStrictEqual<CMKConfig>(
expect.objectContaining({
dtsOutDir: iff.join('generated2'),
compilerOptions: expect.objectContaining({
module: ts.ModuleKind.ES2015,
}),
}),
);
});
test('prefers the tsconfig extended by a later entry of an `extends` array over an earlier entry', async () => {
const iff = await createIFF({
'tsconfig.base1.json': dedent`
{
"cmkOptions": { "dtsOutDir": "generated1", "namedExports": true }
}
`,
'tsconfig.base2-parent.json': dedent`
{
"cmkOptions": { "dtsOutDir": "generated2-parent", "arbitraryExtensions": true }
}
`,
'tsconfig.base2.json': dedent`
{
"extends": "./tsconfig.base2-parent.json",
"cmkOptions": { "arbitraryExtensions": false }
}
`,
'tsconfig.json': dedent`
{
"extends": ["./tsconfig.base1.json", "./tsconfig.base2.json"]
}
`,
});
expect(readConfigFile(iff.rootDir)).toStrictEqual<CMKConfig>(
expect.objectContaining({
dtsOutDir: iff.join('generated2-parent'),
namedExports: true,
arbitraryExtensions: false,
}),
);
});
test('inherits options from a tsconfig in a package', async () => {
const iff = await createIFF({
'node_modules/some-pkg/tsconfig.json': dedent`
Expand Down
52 changes: 44 additions & 8 deletions packages/core-strada/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,45 @@ function parseTsConfigFile(fileName: string, basePath: string) {
};
}

type ParsedTsConfig = ReturnType<typeof parseTsConfigFile>;

function readExtendedTsConfigFile(
fileName: string,
basePath: string,
cache: Map<string, ParsedTsConfig | undefined>,
): ParsedTsConfig | undefined {
if (!cache.has(fileName)) {
cache.set(fileName, isTsConfigFileExists(fileName) ? parseTsConfigFile(fileName, basePath) : undefined);
}
return cache.get(fileName);
}

/**
* Merges the options of `tsConfig` with the options it inherits through `extends`, with the same precedence as
* TypeScript: the options of the file itself win, and a later entry of an `extends` array wins over an earlier one
* (including the files that the earlier entry extends).
*/
function resolveInheritedConfig(
tsConfig: ParsedTsConfig,
basePath: string,
cache: Map<string, ParsedTsConfig | undefined>,
): UnnormalizedRawConfig {
// `extendedSourceFiles` is flattened: each entry of `extends` is followed by the files that it extends in turn.
// Those transitively extended files are skipped here, since they are merged when their entry is resolved.
const extendedSourceFiles = tsConfig.extendedSourceFiles ?? [];
let inheritedConfig: UnnormalizedRawConfig = {};
let i = 0;
while (i < extendedSourceFiles.length) {
const base = readExtendedTsConfigFile(extendedSourceFiles[i]!, basePath, cache);
i++;
if (!base) continue;
const transitivelyExtendedSourceFiles = new Set(base.extendedSourceFiles);
while (i < extendedSourceFiles.length && transitivelyExtendedSourceFiles.has(extendedSourceFiles[i]!)) i++;
inheritedConfig = { ...inheritedConfig, ...resolveInheritedConfig(base, basePath, cache) };
}
return { ...inheritedConfig, ...tsConfig.config };
}

const configDirTemplate = /^\$\{configDir\}/iu;
// https://github.com/microsoft/TypeScript/blob/7b8cb3bdf82f400642b73173f941335775d6f730/src/compiler/commandLineParser.ts#L3300
function startsWithConfigDirTemplate(path: string) {
Expand Down Expand Up @@ -298,15 +337,12 @@ export function readConfigFile(project: string): CMKConfig {

// The options read from `parsedCommandLine.raw` do not inherit values from the file specified in `extends`.
// So here we read the options from those files and merge them into `parsedRawData`.
if (parsedTsConfig.extendedSourceFiles) {
for (const extendedSourceFile of parsedTsConfig.extendedSourceFiles) {
if (isTsConfigFileExists(extendedSourceFile)) {
const base = parseTsConfigFile(extendedSourceFile, basePath);
parsedTsConfig.config = { ...base.config, ...parsedTsConfig.config };
parsedTsConfig.diagnostics = [...base.diagnostics, ...parsedTsConfig.diagnostics];
}
}
const extendedTsConfigs = new Map<string, ParsedTsConfig | undefined>();
for (const extendedSourceFile of parsedTsConfig.extendedSourceFiles ?? []) {
const base = readExtendedTsConfigFile(extendedSourceFile, basePath, extendedTsConfigs);
if (base) parsedTsConfig.diagnostics = [...base.diagnostics, ...parsedTsConfig.diagnostics];
}
parsedTsConfig.config = resolveInheritedConfig(parsedTsConfig, basePath, extendedTsConfigs);

if (parsedTsConfig.config.animation !== undefined && parsedTsConfig.config.keyframes !== undefined) {
parsedTsConfig.diagnostics.push({
Expand Down