vite-plugin-css-module-types 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,583 @@
1
+ // src/plugin.ts
2
+ import { mkdir, readFile, writeFile, readdir, stat } from "node:fs/promises";
3
+ import { mkdirSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ // src/lib/path-filter.ts
7
+ function normalizeFolderPath(value) {
8
+ return value.replaceAll("\\", "/").replace(/^\.?\//, "").replace(/\/+$/, "");
9
+ }
10
+ function normalizeFolderList(folders) {
11
+ if (!folders)
12
+ return [];
13
+ const list = Array.isArray(folders) ? folders : [folders];
14
+ return list.map(normalizeFolderPath).filter(Boolean);
15
+ }
16
+ function isInFolders(relativeFilePath, folders) {
17
+ if (folders.length === 0)
18
+ return false;
19
+ const normalizedPath = normalizeFolderPath(relativeFilePath);
20
+ return folders.some((folder) => {
21
+ return normalizedPath === folder || normalizedPath.startsWith(`${folder}/`);
22
+ });
23
+ }
24
+ function shouldProcessFile(relativeFilePath, includeFolders, excludeFolders) {
25
+ const isIncluded = includeFolders.length === 0 || isInFolders(relativeFilePath, includeFolders);
26
+ if (!isIncluded)
27
+ return false;
28
+ return !isInFolders(relativeFilePath, excludeFolders);
29
+ }
30
+ // src/lib/css-comments.ts
31
+ function buildNewlineIndex(content) {
32
+ const offsets = [];
33
+ for (let i = 0;i < content.length; i++) {
34
+ if (content.charCodeAt(i) === 10) {
35
+ offsets.push(i);
36
+ }
37
+ }
38
+ return offsets;
39
+ }
40
+ function getLineAtOffset(newlineIndex, offset) {
41
+ let lo = 0;
42
+ let hi = newlineIndex.length;
43
+ while (lo < hi) {
44
+ const mid = lo + hi >> 1;
45
+ if ((newlineIndex[mid] ?? 0) < offset) {
46
+ lo = mid + 1;
47
+ } else {
48
+ hi = mid;
49
+ }
50
+ }
51
+ return lo + 1;
52
+ }
53
+ function extractCssComments(cssContent) {
54
+ const newlineIndex = buildNewlineIndex(cssContent);
55
+ const regex = /\/\*\*([\s\S]*?)\*\//g;
56
+ const comments = [];
57
+ for (const match of cssContent.matchAll(regex)) {
58
+ const content = (match[1] ?? "").trim();
59
+ if (match.index === undefined) {
60
+ continue;
61
+ }
62
+ const endOffset = match.index + match[0].length;
63
+ const endLine = getLineAtOffset(newlineIndex, endOffset);
64
+ comments[endLine] = {
65
+ content,
66
+ endLine
67
+ };
68
+ }
69
+ return comments;
70
+ }
71
+ // src/lib/transform.ts
72
+ import { Buffer } from "buffer";
73
+ function patchViteModuleCode(code) {
74
+ return code.replaceAll("import {", "// import {").replaceAll("__vite__updateStyle", "// __vite__updateStyle").replace(/if\s*\(import\.meta\.hot\)\s*\{[\s\S]*?\n\}/g, "").replaceAll("import.meta.hot", "// import.meta.hot").replaceAll("const __vite__css", "export const __vite__css");
75
+ }
76
+ async function evaluateModuleExports(patchedCode) {
77
+ const base64 = Buffer.from(patchedCode, "utf-8").toString("base64");
78
+ const mod = await import(`data:text/javascript;base64,${base64}`);
79
+ return {
80
+ classMapping: mod.default ?? {},
81
+ cssText: mod.__vite__css ?? ""
82
+ };
83
+ }
84
+ // src/lib/camel-case.ts
85
+ function toCamelCase(name) {
86
+ return name.replace(/-([a-zA-Z\d])/g, (_, char) => char.toUpperCase());
87
+ }
88
+
89
+ // src/lib/class-index.ts
90
+ function buildClassNameIndex(lineMappings) {
91
+ const index = new Map;
92
+ const classRegex = /\.([a-zA-Z_][\w-]*)/g;
93
+ for (const mapping of lineMappings) {
94
+ if (!mapping.originalLine)
95
+ continue;
96
+ for (const match of mapping.generatedContent.matchAll(classRegex)) {
97
+ const className = match[1];
98
+ if (className && !index.has(className)) {
99
+ index.set(className, mapping);
100
+ }
101
+ }
102
+ }
103
+ return index;
104
+ }
105
+ function buildOriginalClassLineMap(cssSource) {
106
+ const map = new Map;
107
+ const lines = cssSource.split(`
108
+ `);
109
+ const classRegex = /\.([a-zA-Z_][\w-]*)/g;
110
+ for (let i = 0;i < lines.length; i++) {
111
+ const line = lines[i];
112
+ if (!line)
113
+ continue;
114
+ for (const match of line.matchAll(classRegex)) {
115
+ const name = match[1];
116
+ if (name && !map.has(name)) {
117
+ const lineNum = i + 1;
118
+ map.set(name, lineNum);
119
+ const camelName = toCamelCase(name);
120
+ if (camelName !== name && !map.has(camelName)) {
121
+ map.set(camelName, lineNum);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ return map;
127
+ }
128
+ function extractCssRuleBodies(cssSource) {
129
+ const map = new Map;
130
+ const lines = cssSource.split(`
131
+ `);
132
+ const classRegex = /\.([a-zA-Z_][\w-]*)/g;
133
+ const lineOffsets = [0];
134
+ for (let i = 0;i < lines.length - 1; i++) {
135
+ const prevOffset = lineOffsets[i] ?? 0;
136
+ const lineLen = lines[i]?.length ?? 0;
137
+ lineOffsets.push(prevOffset + lineLen + 1);
138
+ }
139
+ for (let i = 0;i < lines.length; i++) {
140
+ const line = lines[i];
141
+ if (!line)
142
+ continue;
143
+ for (const match of line.matchAll(classRegex)) {
144
+ const name = match[1];
145
+ if (!name || map.has(name))
146
+ continue;
147
+ const lineStartOffset = lineOffsets[i] ?? 0;
148
+ const braceIdx = cssSource.indexOf("{", lineStartOffset);
149
+ if (braceIdx === -1)
150
+ continue;
151
+ let depth = 1;
152
+ let pos = braceIdx + 1;
153
+ while (pos < cssSource.length && depth > 0) {
154
+ const ch = cssSource[pos];
155
+ if (ch === "{")
156
+ depth++;
157
+ else if (ch === "}")
158
+ depth--;
159
+ pos++;
160
+ }
161
+ if (depth !== 0)
162
+ continue;
163
+ const ruleBlock = cssSource.slice(lineStartOffset, pos).trim();
164
+ map.set(name, ruleBlock);
165
+ const camelName = toCamelCase(name);
166
+ if (camelName !== name && !map.has(camelName)) {
167
+ map.set(camelName, ruleBlock);
168
+ }
169
+ }
170
+ }
171
+ return map;
172
+ }
173
+ function resolveClassEntries(classMapping, classIndex, comments, originalClassLines = null, cssRuleBodies = null) {
174
+ const entries = [];
175
+ for (const key in classMapping) {
176
+ const hashKey = classMapping[key] ?? "";
177
+ let line = originalClassLines?.get(key);
178
+ if (!line) {
179
+ line = classIndex?.get(hashKey)?.originalLine;
180
+ }
181
+ const comment = line != null ? comments?.[line - 1]?.content ?? "" : "";
182
+ const cssBlock = cssRuleBodies?.get(key) ?? "";
183
+ entries.push({ rule: key, value: hashKey, line, comment, cssBlock });
184
+ }
185
+ const lineByValue = new Map;
186
+ const cssBlockByValue = new Map;
187
+ for (const entry of entries) {
188
+ if (entry.line != null && !lineByValue.has(entry.value)) {
189
+ lineByValue.set(entry.value, entry.line);
190
+ }
191
+ if (entry.cssBlock && !cssBlockByValue.has(entry.value)) {
192
+ cssBlockByValue.set(entry.value, entry.cssBlock);
193
+ }
194
+ }
195
+ for (const entry of entries) {
196
+ if (entry.line == null) {
197
+ const propagatedLine = lineByValue.get(entry.value);
198
+ if (propagatedLine != null) {
199
+ entry.line = propagatedLine;
200
+ entry.comment = comments?.[propagatedLine - 1]?.content ?? "";
201
+ }
202
+ }
203
+ if (!entry.cssBlock) {
204
+ const propagatedBlock = cssBlockByValue.get(entry.value);
205
+ if (propagatedBlock) {
206
+ entry.cssBlock = propagatedBlock;
207
+ }
208
+ }
209
+ }
210
+ return entries;
211
+ }
212
+ // src/lib/generate-dts.ts
213
+ var EMOJI_ARROW = "↗";
214
+ var JS_IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
215
+ var ASTERISK_OPERATOR = "∗";
216
+ function escapeCssCommentDelimiters(text) {
217
+ return text.replaceAll("/*", `/${ASTERISK_OPERATOR}`).replaceAll("*/", `${ASTERISK_OPERATOR}/`);
218
+ }
219
+ function buildJsDocLine(item, fileName, absolutePath) {
220
+ const lineInfo = item.line ? `#L${item.line}` : "";
221
+ const lineLabel = item.line ? `:${item.line}` : "";
222
+ const commentBlock = item.comment ? `\`\`\`txt
223
+ ${item.comment.trim()}
224
+ \`\`\`
225
+ ` : "";
226
+ const cssBlock = item.cssBlock ? `---
227
+ \`\`\`css
228
+ ${escapeCssCommentDelimiters(item.cssBlock).trim()}
229
+ \`\`\`
230
+ ` : "";
231
+ const separator = commentBlock || cssBlock ? `---
232
+ ` : "";
233
+ const leadingBreak = commentBlock || cssBlock ? `
234
+ ` : "";
235
+ return ` /** ${leadingBreak}${commentBlock}${cssBlock}${separator} [${EMOJI_ARROW} ${fileName}${lineLabel}](file://${absolutePath}${lineInfo}) */`;
236
+ }
237
+ function generateDtsContent(entries, fileName, absolutePath, options = {}) {
238
+ if (entries.length === 0) {
239
+ return { content: `export {};
240
+ `, lineMappings: [] };
241
+ }
242
+ const lines = [];
243
+ const lineMappings = [];
244
+ function pushContent(content) {
245
+ for (const part of content.split(`
246
+ `)) {
247
+ lines.push(part);
248
+ }
249
+ }
250
+ function trackMapping(cssLine, dtsColumn) {
251
+ if (cssLine) {
252
+ lineMappings.push({
253
+ dtsLine: lines.length,
254
+ dtsColumn,
255
+ cssLine,
256
+ cssColumn: 0
257
+ });
258
+ }
259
+ }
260
+ function pushMappedPair(jsDoc, declaration, cssLine, declColumn) {
261
+ const jsDocFirstLine = lines.length + 1;
262
+ pushContent(jsDoc);
263
+ const jsDocLastLine = lines.length;
264
+ if (cssLine) {
265
+ for (let ln = jsDocFirstLine;ln <= jsDocLastLine; ln++) {
266
+ lineMappings.push({ dtsLine: ln, dtsColumn: 0, cssLine, cssColumn: 0 });
267
+ }
268
+ }
269
+ pushContent(declaration);
270
+ trackMapping(cssLine, declColumn);
271
+ lines.push("");
272
+ trackMapping(cssLine, 0);
273
+ }
274
+ if (options.namedExports) {
275
+ const emittedNamedExports = new Set;
276
+ for (const item of entries) {
277
+ if (!JS_IDENT_RE.test(item.rule))
278
+ continue;
279
+ if (emittedNamedExports.has(item.rule))
280
+ continue;
281
+ emittedNamedExports.add(item.rule);
282
+ const jsDoc = buildJsDocLine(item, fileName, absolutePath);
283
+ pushMappedPair(jsDoc, `export declare const ${item.rule}: string;`, item.line, 0);
284
+ }
285
+ lines.push("");
286
+ pushContent(`/** [${EMOJI_ARROW} ${fileName}](file://${absolutePath}) */`);
287
+ lines.push("declare const styles: {");
288
+ const emittedProps2 = new Set;
289
+ for (const item of entries) {
290
+ if (emittedProps2.has(item.rule))
291
+ continue;
292
+ emittedProps2.add(item.rule);
293
+ const jsDoc = buildJsDocLine(item, fileName, absolutePath);
294
+ pushMappedPair(jsDoc, ` readonly ${JSON.stringify(item.rule)}: ${JSON.stringify(item.value)};`, item.line, 2);
295
+ }
296
+ lines.push("};");
297
+ lines.push("export default styles;");
298
+ lines.push("");
299
+ return { content: lines.join(`
300
+ `), lineMappings };
301
+ }
302
+ pushContent(`/** [${EMOJI_ARROW} ${fileName}](file://${absolutePath}) */`);
303
+ lines.push("declare const styles: {");
304
+ const emittedProps = new Set;
305
+ for (const item of entries) {
306
+ if (emittedProps.has(item.rule))
307
+ continue;
308
+ emittedProps.add(item.rule);
309
+ const jsDoc = buildJsDocLine(item, fileName, absolutePath);
310
+ pushMappedPair(jsDoc, ` readonly ${JSON.stringify(item.rule)}: ${JSON.stringify(item.value)};`, item.line, 2);
311
+ }
312
+ lines.push("};");
313
+ lines.push("export default styles;");
314
+ lines.push("");
315
+ return { content: lines.join(`
316
+ `), lineMappings };
317
+ }
318
+ // src/lib/sourcemap.ts
319
+ import { Buffer as Buffer2 } from "buffer";
320
+ import { SourceMapConsumer } from "source-map";
321
+ async function getInlineLineMappings(css) {
322
+ const prefix = "/*# sourceMappingURL=data:application/json;base64,";
323
+ const suffix = "*/";
324
+ const start = css.lastIndexOf(prefix);
325
+ const end = css.indexOf(suffix, start);
326
+ if (start === -1 || end === -1)
327
+ return [];
328
+ const base64 = css.slice(start + prefix.length, end).trim();
329
+ const rawMap = JSON.parse(Buffer2.from(base64, "base64").toString("utf8"));
330
+ const cssLines = css.split(`
331
+ `);
332
+ const consumer = await new SourceMapConsumer(rawMap);
333
+ const sourceLineMap = new Map;
334
+ if (rawMap.sources && rawMap.sourcesContent) {
335
+ for (let i = 0;i < rawMap.sources.length; i++) {
336
+ const src = rawMap.sources[i];
337
+ const content = rawMap.sourcesContent[i];
338
+ if (typeof src === "string" && typeof content === "string") {
339
+ sourceLineMap.set(src, content.split(`
340
+ `));
341
+ }
342
+ }
343
+ }
344
+ const lineToMapping = new Map;
345
+ consumer.eachMapping((m) => {
346
+ if (!lineToMapping.has(m.generatedLine)) {
347
+ lineToMapping.set(m.generatedLine, m);
348
+ }
349
+ });
350
+ const results = [];
351
+ for (let line = 1;line <= cssLines.length; line++) {
352
+ const generatedContent = cssLines[line - 1] ?? "";
353
+ const mapping = lineToMapping.get(line);
354
+ let source;
355
+ let originalLine;
356
+ let originalContent;
357
+ if (mapping?.source && mapping.originalLine) {
358
+ source = mapping.source;
359
+ originalLine = mapping.originalLine;
360
+ const srcLines = sourceLineMap.get(source);
361
+ if (srcLines && srcLines.length >= originalLine) {
362
+ originalContent = srcLines[originalLine - 1];
363
+ }
364
+ }
365
+ results.push({ generatedLine: line, generatedContent, source, originalLine, originalContent });
366
+ }
367
+ consumer.destroy();
368
+ return results;
369
+ }
370
+ // src/lib/declaration-map.ts
371
+ import { encode } from "@jridgewell/sourcemap-codec";
372
+ function generateDeclarationMap(dtsFileName, cssRelativePath, lineMappings, totalDtsLines) {
373
+ const sorted = [...lineMappings].sort((a, b) => a.dtsLine - b.dtsLine || a.dtsColumn - b.dtsColumn);
374
+ const decoded = [];
375
+ let mappingIdx = 0;
376
+ for (let line = 1;line <= totalDtsLines; line++) {
377
+ const lineSegments = [];
378
+ while (mappingIdx < sorted.length && sorted[mappingIdx]?.dtsLine === line) {
379
+ const m = sorted[mappingIdx];
380
+ if (!m)
381
+ break;
382
+ lineSegments.push([m.dtsColumn, 0, m.cssLine - 1, m.cssColumn]);
383
+ mappingIdx++;
384
+ }
385
+ decoded.push(lineSegments);
386
+ }
387
+ return JSON.stringify({
388
+ version: 3,
389
+ file: dtsFileName,
390
+ sourceRoot: "",
391
+ sources: [cssRelativePath],
392
+ mappings: encode(decoded)
393
+ });
394
+ }
395
+ // src/plugin.ts
396
+ async function findFiles(dir, suffix) {
397
+ const results = [];
398
+ let entries;
399
+ try {
400
+ entries = await readdir(dir, { withFileTypes: true });
401
+ } catch {
402
+ return results;
403
+ }
404
+ for (const entry of entries) {
405
+ const full = path.join(dir, entry.name);
406
+ if (entry.isDirectory()) {
407
+ results.push(...await findFiles(full, suffix));
408
+ } else if (entry.name.endsWith(suffix)) {
409
+ results.push(full);
410
+ }
411
+ }
412
+ return results;
413
+ }
414
+ function buildClassMappingFromSource(cssSource) {
415
+ const mapping = {};
416
+ const classRegex = /\.([a-zA-Z_][\w-]*)/g;
417
+ for (const match of cssSource.matchAll(classRegex)) {
418
+ const name = match[1];
419
+ if (!name || name in mapping)
420
+ continue;
421
+ mapping[name] = name;
422
+ const camel = toCamelCase(name);
423
+ if (camel !== name && !(camel in mapping)) {
424
+ mapping[camel] = name;
425
+ }
426
+ }
427
+ return mapping;
428
+ }
429
+ var DEFAULT_EXCLUDE_FOLDERS = ["node_modules", ".git", "dist", "build"];
430
+ function viteCssModuleTypesPlugin(options = {
431
+ dtsOutputDir: ".css-module-types"
432
+ }) {
433
+ let rootDir = "";
434
+ let cssSourceMapEnabled = false;
435
+ const includeFolders = normalizeFolderList(options.include);
436
+ const excludeFolders = normalizeFolderList(options.exclude ?? DEFAULT_EXCLUDE_FOLDERS);
437
+ let dtsGenOptions;
438
+ const createdDirs = new Set;
439
+ const contentCache = new Map;
440
+ async function ensureDir(dirPath) {
441
+ if (createdDirs.has(dirPath))
442
+ return;
443
+ await mkdir(dirPath, { recursive: true });
444
+ createdDirs.add(dirPath);
445
+ }
446
+ async function generateDtsFromSource(absolutePath, force = false) {
447
+ const relativePath = path.relative(rootDir, absolutePath);
448
+ const dtsFileName = options.allowArbitraryExtensions ? relativePath.replace(/\.css$/, ".d.css.ts") : `${relativePath}.d.ts`;
449
+ const dtsFilePath = path.join(rootDir, options.dtsOutputDir, dtsFileName);
450
+ if (!force) {
451
+ try {
452
+ await stat(dtsFilePath);
453
+ return;
454
+ } catch {}
455
+ }
456
+ const source = await readFile(absolutePath, "utf-8");
457
+ const classMapping = buildClassMappingFromSource(source);
458
+ const comments = extractCssComments(source);
459
+ const originalClassLines = buildOriginalClassLineMap(source);
460
+ const cssRuleBodies = extractCssRuleBodies(source);
461
+ const entries = resolveClassEntries(classMapping, null, comments, originalClassLines, cssRuleBodies);
462
+ const fileName = path.basename(absolutePath);
463
+ const { content: dtsBody } = generateDtsContent(entries, fileName, absolutePath, dtsGenOptions);
464
+ const outputDir = path.join(rootDir, options.dtsOutputDir, path.dirname(relativePath));
465
+ await ensureDir(outputDir);
466
+ await writeFile(dtsFilePath, dtsBody);
467
+ }
468
+ async function scanAllCssModules() {
469
+ const scanRoots = includeFolders.length > 0 ? includeFolders.map((f) => path.join(rootDir, f)) : [rootDir];
470
+ const allFiles = (await Promise.all(scanRoots.map((dir) => findFiles(dir, ".module.css")))).flat();
471
+ const eligible = allFiles.filter((f) => {
472
+ const rel = path.relative(rootDir, f);
473
+ return shouldProcessFile(rel, includeFolders, excludeFolders);
474
+ });
475
+ const results = await Promise.allSettled(eligible.map((f) => generateDtsFromSource(f)));
476
+ let generated = 0;
477
+ for (const r of results) {
478
+ if (r.status === "fulfilled")
479
+ generated++;
480
+ }
481
+ if (generated > 0) {
482
+ console.log(`[css-module-dts] Generated ${generated} declaration files on startup.`);
483
+ }
484
+ }
485
+ async function generateDtsFromTransform(code, id) {
486
+ const relativePath = path.relative(rootDir, id);
487
+ const patchedCode = patchViteModuleCode(code);
488
+ const { classMapping, cssText } = await evaluateModuleExports(patchedCode);
489
+ const lineMappings = cssSourceMapEnabled ? await getInlineLineMappings(cssText) : null;
490
+ const classIndex = lineMappings ? buildClassNameIndex(lineMappings) : null;
491
+ const source = await readFile(id, "utf-8");
492
+ const comments = extractCssComments(source);
493
+ const originalClassLines = buildOriginalClassLineMap(source);
494
+ const cssRuleBodies = extractCssRuleBodies(source);
495
+ const entries = resolveClassEntries(classMapping, classIndex, comments, originalClassLines, cssRuleBodies);
496
+ const fileName = path.basename(id);
497
+ const { content: dtsBody, lineMappings: dtsLineMappings } = generateDtsContent(entries, fileName, id, dtsGenOptions);
498
+ const outputDir = path.join(rootDir, options.dtsOutputDir, path.dirname(relativePath));
499
+ await ensureDir(outputDir);
500
+ const dtsFileName = options.allowArbitraryExtensions ? relativePath.replace(/\.css$/, ".d.css.ts") : `${relativePath}.d.ts`;
501
+ const dtsFilePath = path.join(rootDir, options.dtsOutputDir, dtsFileName);
502
+ const useDeclarationMap = (options.declarationMap ?? true) && cssSourceMapEnabled && dtsLineMappings.length > 0;
503
+ const dtsBaseName = path.basename(dtsFileName);
504
+ const mapFileName = `${dtsBaseName}.map`;
505
+ const dtsContent = useDeclarationMap ? `${dtsBody}//# sourceMappingURL=${mapFileName}
506
+ ` : dtsBody;
507
+ let mapContent;
508
+ if (useDeclarationMap) {
509
+ const cssRelativePath = path.relative(outputDir, id);
510
+ const totalDtsLines = dtsContent.split(`
511
+ `).length;
512
+ mapContent = generateDeclarationMap(dtsBaseName, cssRelativePath, dtsLineMappings, totalDtsLines);
513
+ }
514
+ const mapFilePath = useDeclarationMap ? path.join(outputDir, mapFileName) : undefined;
515
+ try {
516
+ const existingDts = await readFile(dtsFilePath, "utf-8");
517
+ if (existingDts === dtsContent) {
518
+ if (!mapFilePath || !mapContent)
519
+ return;
520
+ try {
521
+ const existingMap = await readFile(mapFilePath, "utf-8");
522
+ if (existingMap === mapContent)
523
+ return;
524
+ } catch {}
525
+ }
526
+ } catch {}
527
+ await writeFile(dtsFilePath, dtsContent);
528
+ if (mapFilePath && mapContent) {
529
+ await writeFile(mapFilePath, mapContent);
530
+ }
531
+ }
532
+ return {
533
+ name: "vite-plugin-css-module-types",
534
+ apply: "serve",
535
+ enforce: "post",
536
+ configResolved(config) {
537
+ rootDir = config.root;
538
+ cssSourceMapEnabled = config.css.devSourcemap;
539
+ mkdirSync(path.join(rootDir, options.dtsOutputDir), { recursive: true });
540
+ dtsGenOptions = { namedExports: options.namedExports };
541
+ },
542
+ configureServer() {
543
+ scanAllCssModules().catch((err) => {
544
+ console.warn("[css-module-dts] Initial scan failed:", err);
545
+ });
546
+ },
547
+ async transform(code, id) {
548
+ if (!id.endsWith(".module.css"))
549
+ return null;
550
+ const relativePath = path.relative(rootDir, id);
551
+ if (!shouldProcessFile(relativePath, includeFolders, excludeFolders))
552
+ return null;
553
+ if (contentCache.get(id) === code)
554
+ return null;
555
+ contentCache.set(id, code);
556
+ try {
557
+ await generateDtsFromTransform(code, id);
558
+ } catch (error) {
559
+ console.warn(`[css-module-dts] Failed to generate types for ${relativePath}:`, error instanceof Error ? error.message : error);
560
+ }
561
+ return null;
562
+ },
563
+ handleHotUpdate({ file }) {
564
+ if (!file.endsWith(".module.css"))
565
+ return;
566
+ const relativePath = path.relative(rootDir, file);
567
+ if (!shouldProcessFile(relativePath, includeFolders, excludeFolders))
568
+ return;
569
+ contentCache.delete(file);
570
+ generateDtsFromSource(file, true).catch((err) => {
571
+ console.warn(`[css-module-dts] HMR re-generation failed for ${relativePath}:`, err);
572
+ });
573
+ return;
574
+ }
575
+ };
576
+ }
577
+ var plugin_default = viteCssModuleTypesPlugin;
578
+
579
+ // index.ts
580
+ var vite_plugin_css_module_types_default = plugin_default;
581
+ export {
582
+ vite_plugin_css_module_types_default as default
583
+ };
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** Plugin entry point — exposes only the stable public API. */
2
+ import viteCssModuleTypesPlugin from './src/plugin.ts';
3
+ export default viteCssModuleTypesPlugin;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "vite-plugin-css-module-types",
3
+ "version": "1.0.0",
4
+ "description": "Vite dev-server plugin that generates .d.ts declarations for CSS Modules",
5
+ "keywords": [
6
+ "vite",
7
+ "plugin",
8
+ "css-modules",
9
+ "typescript",
10
+ "dts"
11
+ ],
12
+ "license": "MIT",
13
+ "sideEffects": false,
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./index.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "main": "./dist/index.js",
23
+ "files": [
24
+ "index.ts",
25
+ "src",
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "build": "bun build index.ts --bundle --outdir dist --format esm --target node --packages external",
32
+ "check-types": "tsc --noEmit",
33
+ "prepublishOnly": "npm run check-types && npm run test && npm run build",
34
+ "test": "vitest run --config vitest.config.ts",
35
+ "test:watch": "vitest --config vitest.config.ts"
36
+ },
37
+ "dependencies": {
38
+ "@jridgewell/sourcemap-codec": "^1.5.5",
39
+ "source-map": "^0.7.6"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^25.5.0",
43
+ "vitest": "^4.1.0"
44
+ },
45
+ "peerDependencies": {
46
+ "typescript": ">=5",
47
+ "vite": ">=7"
48
+ },
49
+ "engines": {
50
+ "node": ">=22.0.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }
@@ -0,0 +1,4 @@
1
+ /** Converts kebab-case to camelCase (hyphens only, not underscores). */
2
+ export function toCamelCase(name: string): string {
3
+ return name.replace(/-([a-zA-Z\d])/g, (_, char: string) => char.toUpperCase());
4
+ }