Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/cute-files-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@css-modules-kit/core': minor
---

feat(core): support `${configDir}` in tsconfig options
73 changes: 73 additions & 0 deletions packages/core/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, this test fails. There seems to be a bug. I’ll fix it later, so I’ll leave it as is in this pull request.

 FAIL   unit  packages/core/src/config.test.ts > readConfigFile > inheritance > resolves relative paths against the defining tsconfig directory
AssertionError: expected '/var/folders/1b/v674402d0n1d90hz22pbz…' to be '/var/folders/1b/v674402d0n1d90hz22pbz…' // Object.is equality

Expected: "/var/folders/1b/v674402d0n1d90hz22pbz_tr0000gn/T/css-modules-kit/12/da8a9946-a88c-4612-920f-a8e84e4a23eb/generated"
Received: "/var/folders/1b/v674402d0n1d90hz22pbz_tr0000gn/T/css-modules-kit/12/da8a9946-a88c-4612-920f-a8e84e4a23eb/app/generated"

 ❯ packages/core/src/config.test.ts:292:32
    290|       expect(result.includes).toEqual([iff.join('src')]);
    291|       expect(result.excludes).toEqual([iff.join('dist')]);
    292|       expect(result.dtsOutDir).toBe(iff.join('generated'));
       |                                ^
    293|     });
    294|   });
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.16_@types+node@25.5.2_esbuild@0.27.7_tsx@4.21.0_typesc_d9d6de7afd15cc08c9d54a997cfe644c/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:1893:22

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON content is passed through convertToJson(), which reads from getTsconfigRootOptionsMap() via parseOwnConfigOfJsonSourceFile(). Only options that are declared to be paths (look for isFilePath: true and .isFilePath in commandLineParser.ts) get the special treatment.

I tried this hack, to no avail:

const tsConfigSourceFile = ts.readJsonConfigFile(fileName, path => {
  const originalContent = ts.sys.readFile(path);
  let parsed: unknown;

  try {
    parsed = JSON.parse(originalContent ?? '');
  } catch {
    return originalContent;
  }

  if (typeof parsed === 'object' && parsed !== null && 'cmkOptions' in parsed) {
    let compilerOptions: { cmkOptions?: any };
    if ('compilerOptions' in parsed) {
      if (typeof parsed.compilerOptions !== 'object' || parsed.compilerOptions === null) {
        console.log('compo2')
        return originalContent;
      }
      compilerOptions = parsed.compilerOptions;
    } else {
      compilerOptions = {};
      Object.assign(parsed, { compilerOptions });
    }
    compilerOptions!.cmkOptions = parsed.cmkOptions;
  }

  return JSON.stringify(parsed, null, '  ');
});

if ('optionsDeclaration' in ts && ts.optionsDeclaration instanceof Array) {
  ts.optionsDeclaration.push({
    name: 'cmkOptions',
    type: 'object',
    elementOptions: new Map(Object.entries({
      'dtsOutDir': {
        name: 'dtsOutDir',
        type: 'string',
        isFilePath: true
      }
    }))
  });
} 

All subcomponents of _tsconfigRootOptions, compilerOptions included, are built at import time, so it was too late.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bug is resolved in #373.

});
});
describe('diagnostics', () => {
test('returns diagnostics and a config object with error values excluded if config file has semantic errors', async () => {
Expand Down Expand Up @@ -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')]);
});
});
});
14 changes: 11 additions & 3 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down