-
-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathcssModulesDtsLoader.js
More file actions
75 lines (61 loc) · 2.64 KB
/
Copy pathcssModulesDtsLoader.js
File metadata and controls
75 lines (61 loc) · 2.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { readFile, writeFile } from 'node:fs/promises';
// Emits a precise TypeScript declaration next to each CSS module so
// `import classes from './x.module.css'` is typed with the actual class names
// instead of the loose `Record<string, string>` ambient fallback.
//
// It must run directly after `css-loader` (placed above it in the `use` array)
// because it parses css-loader's JS output — specifically the
// `___CSS_LOADER_EXPORT___.locals = { ... }` object emitted when
// `modules.namedExport` is false.
//
// The declaration is named `*.module.d.css.ts` (the `.d.<ext>.ts` form), which
// is what TypeScript's `nodenext` resolution finds for a `.css` import when
// `allowArbitraryExtensions` is enabled. The legacy `*.css.d.ts` sidecar name
// is silently ignored under `nodenext`.
//
// Why a hand-rolled loader instead of an off-the-shelf one (e.g.
// `css-modules-dts-loader`): every published generator emits the legacy
// `*.css.d.ts` name, which `nodenext` ignores, and none expose an option to
// emit the `.d.css.ts` form. The only ways to avoid a custom loader are worse:
// switch the whole repo to `moduleResolution: node` (huge blast radius), or
// use an editor-only TS plugin (no build-time / CI type checking, since
// `tsc` don't load language-service plugins). So this ~20-line loader
// riding on css-loader's existing output is the cheapest way to get precise,
// build-checked CSS-module types. See doc/build-and-deploy.md.
const LOCALS_BLOCK = /\.locals\s*=\s*\{([\s\S]*?)\}/;
const KEY = /(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][\w$]*))\s*:/g;
const BANNER =
'// This file is automatically generated by cssModulesDtsLoader.\n// Do not edit.';
function extractKeys(source) {
const block = LOCALS_BLOCK.exec(source);
if (!block) {
return [];
}
const keys = [];
for (const m of block[1].matchAll(KEY)) {
keys.push(m[1] ?? m[2] ?? m[3]);
}
return keys;
}
function buildDts(keys) {
const props = keys.map((k) => ` ${JSON.stringify(k)}: string;`).join('\n');
return `${BANNER}
interface CssExports {
${props}
}
declare const cssExports: CssExports;
export default cssExports;
`;
}
export default function cssModulesDtsLoader(source) {
const callback = this.async();
const dts = buildDts(extractKeys(source));
const dtsPath = this.resourcePath.replace(/\.css$/, '.d.css.ts');
// Only write when the content actually changes, so unchanged builds don't
// bump the file's mtime and retrigger the type checker / watch.
readFile(dtsPath, 'utf8')
.catch(() => null)
.then((existing) => (existing === dts ? null : writeFile(dtsPath, dts)))
.then(() => callback(null, source))
.catch((err) => callback(err));
}