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/fiery-turkeys-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@css-modules-kit/core': minor
---

feat!: remove unused `isAbsolute`
5 changes: 5 additions & 0 deletions .changeset/orange-readers-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@css-modules-kit/core': patch
---

fix: report "Cannot import module" diagnostic for unresolvable bare specifiers
5 changes: 5 additions & 0 deletions .changeset/some-rules-stick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@css-modules-kit/core': patch
---

fix: return `undefined` for non-existent CSS module paths in resolver
5 changes: 4 additions & 1 deletion packages/codegen/e2e-test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('generates .d.ts', async () => {
const iff = await createIFF({
'src/a.module.css': dedent`
@import './b.module.css';
@import './external.css';
@import './unmatched.module.css';
/* @import '@/c.module.css'; */ /* TODO: Fix this */
.a1 { color: red; }
`,
Expand All @@ -26,8 +26,10 @@ test('generates .d.ts', async () => {
// @ts-expect-error
styles.a2;
`,
'src/unmatched.module.css': '',
'tsconfig.json': dedent`
{
"exclude": ["unmatched.module.css"],
"compilerOptions": {
"lib": ["ES2015"],
"noEmit": true,
Expand All @@ -47,6 +49,7 @@ test('generates .d.ts', async () => {
declare const styles = {
a1: '' as readonly string,
...(await import('./b.module.css')).default,
...(await import('./unmatched.module.css')).default,
};
export default styles;
"
Expand Down
21 changes: 20 additions & 1 deletion packages/core/src/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ describe('checkCSSModule', () => {
});
const check = prepareChecker();
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
// TODO: Report diagnostics for `package/c.module.css`
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`
[
{
Expand All @@ -236,6 +235,16 @@ describe('checkCSSModule', () => {
},
"text": "Cannot import module './b.module.css'",
},
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 20,
"start": {
"column": 10,
"line": 2,
},
"text": "Cannot import module 'package/c.module.css'",
},
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
Expand All @@ -246,6 +255,16 @@ describe('checkCSSModule', () => {
},
"text": "Cannot import module './b.module.css'",
},
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 20,
"start": {
"column": 18,
"line": 4,
},
"text": "Cannot import module 'package/c.module.css'",
},
]
`);
});
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
Resolver,
TokenImporter,
} from './type.js';
import { isValidAsJSIdentifier } from './util.js';
import { isURLSpecifier, isValidAsJSIdentifier } from './util.js';

export interface CheckerArgs {
config: CMKConfig;
Expand Down Expand Up @@ -39,13 +39,15 @@ export function checkCSSModule(cssModule: CSSModule, args: CheckerArgs): Diagnos
}

for (const tokenImporter of cssModule.tokenImporters) {
if (isURLSpecifier(tokenImporter.from)) continue;
const from = args.resolver(tokenImporter.from, { request: cssModule.fileName });
if (!from || !args.matchesPattern(from)) continue;
const imported = args.getCSSModule(from);
if (!imported) {
if (!from) {
diagnostics.push(createCannotImportModuleDiagnostic(cssModule, tokenImporter));
continue;
}
if (!args.matchesPattern(from)) continue;
const imported = args.getCSSModule(from);
if (!imported) throw new Error('unreachable: `imported` is undefined');

if (tokenImporter.type === 'value') {
const exportRecord = args.getExportRecord(imported);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ export {
} from './file.js';
export { checkCSSModule, type CheckerArgs } from './checker.js';
export { createExportBuilder } from './export-builder.js';
export { join, resolve, relative, dirname, basename, parse, isAbsolute } from './path.js';
export { join, resolve, relative, dirname, basename, parse } from './path.js';
export { findUsedTokenNames } from './util.js';
export { convertDiagnostic, convertDiagnosticWithLocation, convertSystemError } from './diagnostic.js';
3 changes: 0 additions & 3 deletions packages/core/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,3 @@ export function parse(path: string): ParsedPath {
const { root, dir, base, name, ext } = nodePath.parse(path);
return { root: slash(root), dir: slash(dir), base, name, ext };
}

// eslint-disable-next-line @typescript-eslint/unbound-method
export const isAbsolute = nodePath.isAbsolute;
3 changes: 1 addition & 2 deletions packages/core/src/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ describe('createResolver', async () => {
const resolve = createResolver(normalizeCompilerOptions({}, iff.rootDir), undefined);
expect(resolve('./a.module.css', { request })).toBe(iff.paths['a.module.css']);
expect(resolve('./dir/a.module.css', { request })).toBe(iff.paths['dir/a.module.css']);
// FIXME: It should return `undefined`.
expect(resolve('./non-existent.module.css', { request })).toBe(iff.join('non-existent.module.css'));
expect(resolve('./non-existent.module.css', { request })).toBe(undefined);
});
describe('resolve with `paths` option', () => {
test('basic', () => {
Expand Down
30 changes: 4 additions & 26 deletions packages/core/src/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { CompilerOptions } from 'typescript';
import ts from 'typescript';
import { isAbsolute, resolve } from './path.js';
import { resolve } from './path.js';
import type { Resolver, ResolverOptions } from './type.js';

export function createResolver(
compilerOptions: CompilerOptions,
moduleResolutionCache: ts.ModuleResolutionCache | undefined,
): Resolver {
return (_specifier: string, options: ResolverOptions) => {
let specifier = _specifier;

return (specifier: string, options: ResolverOptions) => {
const host: ts.ModuleResolutionHost = {
...ts.sys,
fileExists: (fileName) => {
Expand All @@ -29,27 +26,8 @@ export function createResolver(
);
if (resolvedModule) {
// TODO: Logging that the paths is used.
specifier = resolvedModule.resolvedFileName.replace(/\.module\.d\.css\.ts$/u, '.module.css');
}
if (isAbsolute(specifier)) {
return resolve(specifier);
} else if (isRelativeSpecifier(specifier)) {
// Convert the specifier to an absolute path
// NOTE: Node.js resolves relative specifier with standard relative URL resolution semantics. So we will follow that here as well.
// ref: https://nodejs.org/docs/latest-v23.x/api/esm.html#terminology
return resolve(fileURLToPath(new URL(specifier, pathToFileURL(options.request)).href));
} else {
// Do not support URL or bare specifiers
// TODO: Logging that the specifier could not resolve.
return undefined;
return resolve(resolvedModule.resolvedFileName.replace(/\.module\.d\.css\.ts$/u, '.module.css'));
}
return undefined;
};
}

/**
* Check if the specifier is a relative specifier.
* @see https://nodejs.org/docs/latest-v23.x/api/esm.html#terminology
*/
function isRelativeSpecifier(specifier: string): boolean {
return specifier.startsWith('./') || specifier.startsWith('../');
}
4 changes: 4 additions & 0 deletions packages/core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ export function findUsedTokenNames(componentText: string): Set<string> {
}
return usedClassNames;
}

export function isURLSpecifier(specifier: string): boolean {
return URL.canParse(specifier);
}