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/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/index.js +583 -0
- package/index.ts +3 -0
- package/package.json +55 -0
- package/src/lib/camel-case.ts +4 -0
- package/src/lib/class-index.ts +169 -0
- package/src/lib/css-comments.ts +55 -0
- package/src/lib/declaration-map.ts +40 -0
- package/src/lib/generate-dts.ts +157 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/path-filter.ts +37 -0
- package/src/lib/sourcemap.ts +73 -0
- package/src/lib/transform.ts +30 -0
- package/src/lib/types.ts +65 -0
- package/src/plugin.ts +287 -0
- package/src/tests/camel-case.spec.ts +36 -0
- package/src/tests/class-index.spec.ts +439 -0
- package/src/tests/css-comments.spec.ts +116 -0
- package/src/tests/declaration-map.spec.ts +102 -0
- package/src/tests/generate-dts.spec.ts +499 -0
- package/src/tests/integration.spec.ts +175 -0
- package/src/tests/path-filter.spec.ts +130 -0
- package/src/tests/transform.spec.ts +101 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { toCamelCase } from "./camel-case.ts";
|
|
2
|
+
import type { LineMapping } from "./sourcemap.ts";
|
|
3
|
+
import type { ClassEntry, CssCommentInfo } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds an O(1) lookup from hashed class names to their source line mappings
|
|
7
|
+
* by scanning generated CSS lines for `.className` selectors.
|
|
8
|
+
*/
|
|
9
|
+
export function buildClassNameIndex(lineMappings: LineMapping[]): Map<string, LineMapping> {
|
|
10
|
+
const index = new Map<string, LineMapping>();
|
|
11
|
+
const classRegex = /\.([a-zA-Z_][\w-]*)/g;
|
|
12
|
+
|
|
13
|
+
for (const mapping of lineMappings) {
|
|
14
|
+
if (!mapping.originalLine) continue;
|
|
15
|
+
|
|
16
|
+
for (const match of mapping.generatedContent.matchAll(classRegex)) {
|
|
17
|
+
const className = match[1];
|
|
18
|
+
if (className && !index.has(className)) {
|
|
19
|
+
index.set(className, mapping);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return index;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Scans original CSS source for class selectors and returns a name→line map.
|
|
29
|
+
* Also registers camelCase variants for `localsConvention: 'camelCase'`.
|
|
30
|
+
*/
|
|
31
|
+
export function buildOriginalClassLineMap(cssSource: string): Map<string, number> {
|
|
32
|
+
const map = new Map<string, number>();
|
|
33
|
+
const lines = cssSource.split("\n");
|
|
34
|
+
const classRegex = /\.([a-zA-Z_][\w-]*)/g;
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
const line = lines[i];
|
|
38
|
+
if (!line) continue;
|
|
39
|
+
|
|
40
|
+
for (const match of line.matchAll(classRegex)) {
|
|
41
|
+
const name = match[1];
|
|
42
|
+
if (name && !map.has(name)) {
|
|
43
|
+
const lineNum = i + 1;
|
|
44
|
+
map.set(name, lineNum);
|
|
45
|
+
|
|
46
|
+
const camelName = toCamelCase(name);
|
|
47
|
+
if (camelName !== name && !map.has(camelName)) {
|
|
48
|
+
map.set(camelName, lineNum);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return map;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extracts CSS rule blocks (selector + brace-delimited body) for each class.
|
|
59
|
+
* Handles nesting for `@media`/`@layer`. Registers camelCase variants.
|
|
60
|
+
*/
|
|
61
|
+
export function extractCssRuleBodies(cssSource: string): Map<string, string> {
|
|
62
|
+
const map = new Map<string, string>();
|
|
63
|
+
const lines = cssSource.split("\n");
|
|
64
|
+
const classRegex = /\.([a-zA-Z_][\w-]*)/g;
|
|
65
|
+
|
|
66
|
+
const lineOffsets: number[] = [0];
|
|
67
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
68
|
+
const prevOffset = lineOffsets[i] ?? 0;
|
|
69
|
+
const lineLen = lines[i]?.length ?? 0;
|
|
70
|
+
lineOffsets.push(prevOffset + lineLen + 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
const line = lines[i];
|
|
75
|
+
if (!line) continue;
|
|
76
|
+
|
|
77
|
+
for (const match of line.matchAll(classRegex)) {
|
|
78
|
+
const name = match[1];
|
|
79
|
+
if (!name || map.has(name)) continue;
|
|
80
|
+
|
|
81
|
+
const lineStartOffset = lineOffsets[i] ?? 0;
|
|
82
|
+
const braceIdx = cssSource.indexOf("{", lineStartOffset);
|
|
83
|
+
if (braceIdx === -1) continue;
|
|
84
|
+
|
|
85
|
+
let depth = 1;
|
|
86
|
+
let pos = braceIdx + 1;
|
|
87
|
+
while (pos < cssSource.length && depth > 0) {
|
|
88
|
+
const ch = cssSource[pos];
|
|
89
|
+
if (ch === "{") depth++;
|
|
90
|
+
else if (ch === "}") depth--;
|
|
91
|
+
pos++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (depth !== 0) continue;
|
|
95
|
+
|
|
96
|
+
const ruleBlock = cssSource.slice(lineStartOffset, pos).trim();
|
|
97
|
+
map.set(name, ruleBlock);
|
|
98
|
+
|
|
99
|
+
const camelName = toCamelCase(name);
|
|
100
|
+
if (camelName !== name && !map.has(camelName)) {
|
|
101
|
+
map.set(camelName, ruleBlock);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return map;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolves each exported class to its source location and nearby comment.
|
|
111
|
+
*
|
|
112
|
+
* Two-tier lookup: primary from original CSS scan, fallback via sourcemap index.
|
|
113
|
+
* Second pass propagates resolved lines to unresolved entries sharing the same hash
|
|
114
|
+
* (handles camelCase variant mismatches).
|
|
115
|
+
*/
|
|
116
|
+
export function resolveClassEntries(
|
|
117
|
+
classMapping: Record<string, string>,
|
|
118
|
+
classIndex: Map<string, LineMapping> | null,
|
|
119
|
+
comments: CssCommentInfo[] | null,
|
|
120
|
+
originalClassLines: Map<string, number> | null = null,
|
|
121
|
+
cssRuleBodies: Map<string, string> | null = null,
|
|
122
|
+
): ClassEntry[] {
|
|
123
|
+
const entries: ClassEntry[] = [];
|
|
124
|
+
|
|
125
|
+
for (const key in classMapping) {
|
|
126
|
+
const hashKey = classMapping[key] ?? "";
|
|
127
|
+
|
|
128
|
+
let line = originalClassLines?.get(key);
|
|
129
|
+
if (!line) {
|
|
130
|
+
line = classIndex?.get(hashKey)?.originalLine;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const comment = line != null ? (comments?.[line - 1]?.content ?? "") : "";
|
|
134
|
+
const cssBlock = cssRuleBodies?.get(key) ?? "";
|
|
135
|
+
|
|
136
|
+
entries.push({ rule: key, value: hashKey, line, comment, cssBlock });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Propagate resolved lines to unresolved entries with same hash value.
|
|
140
|
+
const lineByValue = new Map<string, number>();
|
|
141
|
+
const cssBlockByValue = new Map<string, string>();
|
|
142
|
+
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.line != null && !lineByValue.has(entry.value)) {
|
|
145
|
+
lineByValue.set(entry.value, entry.line);
|
|
146
|
+
}
|
|
147
|
+
if (entry.cssBlock && !cssBlockByValue.has(entry.value)) {
|
|
148
|
+
cssBlockByValue.set(entry.value, entry.cssBlock);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (entry.line == null) {
|
|
154
|
+
const propagatedLine = lineByValue.get(entry.value);
|
|
155
|
+
if (propagatedLine != null) {
|
|
156
|
+
entry.line = propagatedLine;
|
|
157
|
+
entry.comment = comments?.[propagatedLine - 1]?.content ?? "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!entry.cssBlock) {
|
|
161
|
+
const propagatedBlock = cssBlockByValue.get(entry.value);
|
|
162
|
+
if (propagatedBlock) {
|
|
163
|
+
entry.cssBlock = propagatedBlock;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return entries;
|
|
169
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CssCommentInfo } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** Builds a sorted array of newline offsets for O(log n) line lookups. */
|
|
4
|
+
export function buildNewlineIndex(content: string): number[] {
|
|
5
|
+
const offsets: number[] = [];
|
|
6
|
+
for (let i = 0; i < content.length; i++) {
|
|
7
|
+
if (content.charCodeAt(i) === 10) {
|
|
8
|
+
offsets.push(i);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return offsets;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Returns the 1-based line number for a byte offset via binary search. */
|
|
15
|
+
export function getLineAtOffset(newlineIndex: number[], offset: number): number {
|
|
16
|
+
let lo = 0;
|
|
17
|
+
let hi = newlineIndex.length;
|
|
18
|
+
while (lo < hi) {
|
|
19
|
+
const mid = (lo + hi) >> 1;
|
|
20
|
+
if ((newlineIndex[mid] ?? 0) < offset) {
|
|
21
|
+
lo = mid + 1;
|
|
22
|
+
} else {
|
|
23
|
+
hi = mid;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return lo + 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extracts `/** ... */` docblock comments from CSS source.
|
|
31
|
+
* Returns a sparse array keyed by end-line for O(1) lookup.
|
|
32
|
+
*/
|
|
33
|
+
export function extractCssComments(cssContent: string): CssCommentInfo[] {
|
|
34
|
+
const newlineIndex = buildNewlineIndex(cssContent);
|
|
35
|
+
const regex = /\/\*\*([\s\S]*?)\*\//g;
|
|
36
|
+
const comments: CssCommentInfo[] = [];
|
|
37
|
+
|
|
38
|
+
for (const match of cssContent.matchAll(regex)) {
|
|
39
|
+
const content = (match[1] ?? "").trim();
|
|
40
|
+
|
|
41
|
+
if (match.index === undefined) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const endOffset = match.index + match[0].length;
|
|
46
|
+
const endLine = getLineAtOffset(newlineIndex, endOffset);
|
|
47
|
+
|
|
48
|
+
comments[endLine] = {
|
|
49
|
+
content,
|
|
50
|
+
endLine,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return comments;
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { encode } from "@jridgewell/sourcemap-codec";
|
|
2
|
+
import type { DtsSourceMapping } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
type SourceMapSegment = [number, number, number, number];
|
|
5
|
+
|
|
6
|
+
/** Generates a V3 source map JSON for `.d.ts` → `.module.css` navigation. */
|
|
7
|
+
export function generateDeclarationMap(
|
|
8
|
+
dtsFileName: string,
|
|
9
|
+
cssRelativePath: string,
|
|
10
|
+
lineMappings: DtsSourceMapping[],
|
|
11
|
+
totalDtsLines: number,
|
|
12
|
+
): string {
|
|
13
|
+
const sorted = [...lineMappings].sort(
|
|
14
|
+
(a, b) => a.dtsLine - b.dtsLine || a.dtsColumn - b.dtsColumn,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const decoded: SourceMapSegment[][] = [];
|
|
18
|
+
let mappingIdx = 0;
|
|
19
|
+
|
|
20
|
+
for (let line = 1; line <= totalDtsLines; line++) {
|
|
21
|
+
const lineSegments: SourceMapSegment[] = [];
|
|
22
|
+
|
|
23
|
+
while (mappingIdx < sorted.length && sorted[mappingIdx]?.dtsLine === line) {
|
|
24
|
+
const m = sorted[mappingIdx];
|
|
25
|
+
if (!m) break;
|
|
26
|
+
lineSegments.push([m.dtsColumn, 0, m.cssLine - 1, m.cssColumn]);
|
|
27
|
+
mappingIdx++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
decoded.push(lineSegments);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return JSON.stringify({
|
|
34
|
+
version: 3 as const,
|
|
35
|
+
file: dtsFileName,
|
|
36
|
+
sourceRoot: "",
|
|
37
|
+
sources: [cssRelativePath],
|
|
38
|
+
mappings: encode(decoded),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClassEntry,
|
|
3
|
+
DtsGenerationOptions,
|
|
4
|
+
DtsSourceMapping,
|
|
5
|
+
GenerateDtsResult,
|
|
6
|
+
} from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const EMOJI_ARROW = "\u{2197}"; // ↗
|
|
9
|
+
const JS_IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ASTERISK OPERATOR (U+2217) — visually identical to `*` but doesn't
|
|
13
|
+
* close JSDoc or open CSS comment regions in syntax highlighters.
|
|
14
|
+
*/
|
|
15
|
+
const ASTERISK_OPERATOR = "\u{2217}"; // ∗
|
|
16
|
+
|
|
17
|
+
function escapeCssCommentDelimiters(text: string): string {
|
|
18
|
+
return text.replaceAll("/*", `/${ASTERISK_OPERATOR}`).replaceAll("*/", `${ASTERISK_OPERATOR}/`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Builds a JSDoc comment line with file link and optional docblock/CSS snippet. */
|
|
22
|
+
export function buildJsDocLine(item: ClassEntry, fileName: string, absolutePath: string): string {
|
|
23
|
+
const lineInfo = item.line ? `#L${item.line}` : "";
|
|
24
|
+
const lineLabel = item.line ? `:${item.line}` : "";
|
|
25
|
+
const commentBlock = item.comment ? `\`\`\`txt\n${item.comment.trim()}\n\`\`\`\n` : "";
|
|
26
|
+
const cssBlock = item.cssBlock
|
|
27
|
+
? `---\n\`\`\`css\n${escapeCssCommentDelimiters(item.cssBlock).trim()}\n\`\`\`\n`
|
|
28
|
+
: "";
|
|
29
|
+
const separator = commentBlock || cssBlock ? "---\n" : "";
|
|
30
|
+
const leadingBreak = commentBlock || cssBlock ? "\n" : "";
|
|
31
|
+
return ` /** ${leadingBreak}${commentBlock}${cssBlock}${separator} [${EMOJI_ARROW} ${fileName}${lineLabel}](file://${absolutePath}${lineInfo}) */`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates `.d.ts` content for a CSS module.
|
|
36
|
+
* Returns the declaration text and line mappings for declaration map generation.
|
|
37
|
+
*/
|
|
38
|
+
export function generateDtsContent(
|
|
39
|
+
entries: ClassEntry[],
|
|
40
|
+
fileName: string,
|
|
41
|
+
absolutePath: string,
|
|
42
|
+
options: DtsGenerationOptions = {},
|
|
43
|
+
): GenerateDtsResult {
|
|
44
|
+
if (entries.length === 0) {
|
|
45
|
+
return { content: "export {};\n", lineMappings: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
const lineMappings: DtsSourceMapping[] = [];
|
|
50
|
+
|
|
51
|
+
function pushContent(content: string): void {
|
|
52
|
+
for (const part of content.split("\n")) {
|
|
53
|
+
lines.push(part);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function trackMapping(cssLine: number | undefined, dtsColumn: number): void {
|
|
58
|
+
if (cssLine) {
|
|
59
|
+
lineMappings.push({
|
|
60
|
+
dtsLine: lines.length,
|
|
61
|
+
dtsColumn,
|
|
62
|
+
cssLine,
|
|
63
|
+
cssColumn: 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pushes JSDoc + declaration with source mappings and a guard line.
|
|
70
|
+
* The guard line absorbs TS's off-by-one in declaration-map resolution.
|
|
71
|
+
*/
|
|
72
|
+
function pushMappedPair(
|
|
73
|
+
jsDoc: string,
|
|
74
|
+
declaration: string,
|
|
75
|
+
cssLine: number | undefined,
|
|
76
|
+
declColumn: number,
|
|
77
|
+
): void {
|
|
78
|
+
const jsDocFirstLine = lines.length + 1;
|
|
79
|
+
pushContent(jsDoc);
|
|
80
|
+
const jsDocLastLine = lines.length;
|
|
81
|
+
|
|
82
|
+
if (cssLine) {
|
|
83
|
+
for (let ln = jsDocFirstLine; ln <= jsDocLastLine; ln++) {
|
|
84
|
+
lineMappings.push({ dtsLine: ln, dtsColumn: 0, cssLine, cssColumn: 0 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pushContent(declaration);
|
|
89
|
+
trackMapping(cssLine, declColumn);
|
|
90
|
+
|
|
91
|
+
lines.push("");
|
|
92
|
+
trackMapping(cssLine, 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (options.namedExports) {
|
|
96
|
+
const emittedNamedExports = new Set<string>();
|
|
97
|
+
|
|
98
|
+
for (const item of entries) {
|
|
99
|
+
if (!JS_IDENT_RE.test(item.rule)) continue;
|
|
100
|
+
if (emittedNamedExports.has(item.rule)) continue;
|
|
101
|
+
emittedNamedExports.add(item.rule);
|
|
102
|
+
|
|
103
|
+
const jsDoc = buildJsDocLine(item, fileName, absolutePath);
|
|
104
|
+
pushMappedPair(jsDoc, `export declare const ${item.rule}: string;`, item.line, 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
lines.push("");
|
|
108
|
+
pushContent(`/** [${EMOJI_ARROW} ${fileName}](file://${absolutePath}) */`);
|
|
109
|
+
lines.push("declare const styles: {");
|
|
110
|
+
|
|
111
|
+
const emittedProps = new Set<string>();
|
|
112
|
+
|
|
113
|
+
for (const item of entries) {
|
|
114
|
+
if (emittedProps.has(item.rule)) continue;
|
|
115
|
+
emittedProps.add(item.rule);
|
|
116
|
+
|
|
117
|
+
const jsDoc = buildJsDocLine(item, fileName, absolutePath);
|
|
118
|
+
pushMappedPair(
|
|
119
|
+
jsDoc,
|
|
120
|
+
` readonly ${JSON.stringify(item.rule)}: ${JSON.stringify(item.value)};`,
|
|
121
|
+
item.line,
|
|
122
|
+
2,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
lines.push("};");
|
|
127
|
+
lines.push("export default styles;");
|
|
128
|
+
lines.push("");
|
|
129
|
+
|
|
130
|
+
return { content: lines.join("\n"), lineMappings };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Default mode
|
|
134
|
+
pushContent(`/** [${EMOJI_ARROW} ${fileName}](file://${absolutePath}) */`);
|
|
135
|
+
lines.push("declare const styles: {");
|
|
136
|
+
|
|
137
|
+
const emittedProps = new Set<string>();
|
|
138
|
+
|
|
139
|
+
for (const item of entries) {
|
|
140
|
+
if (emittedProps.has(item.rule)) continue;
|
|
141
|
+
emittedProps.add(item.rule);
|
|
142
|
+
|
|
143
|
+
const jsDoc = buildJsDocLine(item, fileName, absolutePath);
|
|
144
|
+
pushMappedPair(
|
|
145
|
+
jsDoc,
|
|
146
|
+
` readonly ${JSON.stringify(item.rule)}: ${JSON.stringify(item.value)};`,
|
|
147
|
+
item.line,
|
|
148
|
+
2,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lines.push("};");
|
|
153
|
+
lines.push("export default styles;");
|
|
154
|
+
lines.push("");
|
|
155
|
+
|
|
156
|
+
return { content: lines.join("\n"), lineMappings };
|
|
157
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CssCommentInfo,
|
|
3
|
+
ClassEntry,
|
|
4
|
+
DtsGenerationOptions,
|
|
5
|
+
DtsSourceMapping,
|
|
6
|
+
GenerateDtsResult,
|
|
7
|
+
VitePluginCssModuleTypesOptions,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export { normalizeFolderPath, normalizeFolderList, isInFolders, shouldProcessFile } from "./path-filter.ts";
|
|
11
|
+
export { buildNewlineIndex, getLineAtOffset, extractCssComments } from "./css-comments.ts";
|
|
12
|
+
export { patchViteModuleCode, evaluateModuleExports } from "./transform.ts";
|
|
13
|
+
export { buildClassNameIndex, buildOriginalClassLineMap, extractCssRuleBodies, resolveClassEntries } from "./class-index.ts";
|
|
14
|
+
export { buildJsDocLine, generateDtsContent } from "./generate-dts.ts";
|
|
15
|
+
export { getInlineLineMappings, type LineMapping } from "./sourcemap.ts";
|
|
16
|
+
export { generateDeclarationMap } from "./declaration-map.ts";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Normalizes a path: slashes, strip leading `./` and trailing `/`. */
|
|
2
|
+
export function normalizeFolderPath(value: string): string {
|
|
3
|
+
return value
|
|
4
|
+
.replaceAll("\\", "/")
|
|
5
|
+
.replace(/^\.?\//, "")
|
|
6
|
+
.replace(/\/+$/, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Normalizes a string or array into cleaned folder paths. */
|
|
10
|
+
export function normalizeFolderList(folders?: string | string[]): string[] {
|
|
11
|
+
if (!folders) return [];
|
|
12
|
+
const list = Array.isArray(folders) ? folders : [folders];
|
|
13
|
+
return list.map(normalizeFolderPath).filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Checks if `relativeFilePath` starts with any of `folders`. */
|
|
17
|
+
export function isInFolders(relativeFilePath: string, folders: string[]): boolean {
|
|
18
|
+
if (folders.length === 0) return false;
|
|
19
|
+
const normalizedPath = normalizeFolderPath(relativeFilePath);
|
|
20
|
+
return folders.some((folder) => {
|
|
21
|
+
return normalizedPath === folder || normalizedPath.startsWith(`${folder}/`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns true if the file should be processed.
|
|
27
|
+
* Empty `include` means all files match. `exclude` always wins.
|
|
28
|
+
*/
|
|
29
|
+
export function shouldProcessFile(
|
|
30
|
+
relativeFilePath: string,
|
|
31
|
+
includeFolders: string[],
|
|
32
|
+
excludeFolders: string[],
|
|
33
|
+
): boolean {
|
|
34
|
+
const isIncluded = includeFolders.length === 0 || isInFolders(relativeFilePath, includeFolders);
|
|
35
|
+
if (!isIncluded) return false;
|
|
36
|
+
return !isInFolders(relativeFilePath, excludeFolders);
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
import { SourceMapConsumer, type RawSourceMap, type MappingItem } from "source-map";
|
|
3
|
+
|
|
4
|
+
export interface LineMapping {
|
|
5
|
+
generatedLine: number;
|
|
6
|
+
generatedContent: string;
|
|
7
|
+
source?: string;
|
|
8
|
+
originalLine?: number;
|
|
9
|
+
originalContent?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts per-line generated→original mappings from an inline CSS sourcemap.
|
|
14
|
+
* Returns empty array if no inline sourcemap is found.
|
|
15
|
+
*/
|
|
16
|
+
export async function getInlineLineMappings(css: string): Promise<LineMapping[]> {
|
|
17
|
+
const prefix = "/*# sourceMappingURL=data:application/json;base64,";
|
|
18
|
+
const suffix = "*/";
|
|
19
|
+
|
|
20
|
+
const start = css.lastIndexOf(prefix);
|
|
21
|
+
const end = css.indexOf(suffix, start);
|
|
22
|
+
if (start === -1 || end === -1) return [];
|
|
23
|
+
|
|
24
|
+
const base64 = css.slice(start + prefix.length, end).trim();
|
|
25
|
+
const rawMap = JSON.parse(Buffer.from(base64, "base64").toString("utf8")) as RawSourceMap;
|
|
26
|
+
const cssLines = css.split("\n");
|
|
27
|
+
|
|
28
|
+
const consumer = await new SourceMapConsumer(rawMap);
|
|
29
|
+
|
|
30
|
+
const sourceLineMap = new Map<string, string[]>();
|
|
31
|
+
if (rawMap.sources && rawMap.sourcesContent) {
|
|
32
|
+
for (let i = 0; i < rawMap.sources.length; i++) {
|
|
33
|
+
const src = rawMap.sources[i];
|
|
34
|
+
const content = rawMap.sourcesContent[i];
|
|
35
|
+
if (typeof src === "string" && typeof content === "string") {
|
|
36
|
+
sourceLineMap.set(src, content.split("\n"));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lineToMapping = new Map<number, MappingItem>();
|
|
42
|
+
consumer.eachMapping((m) => {
|
|
43
|
+
if (!lineToMapping.has(m.generatedLine)) {
|
|
44
|
+
lineToMapping.set(m.generatedLine, m);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const results: LineMapping[] = [];
|
|
49
|
+
|
|
50
|
+
for (let line = 1; line <= cssLines.length; line++) {
|
|
51
|
+
const generatedContent = cssLines[line - 1] ?? "";
|
|
52
|
+
const mapping = lineToMapping.get(line);
|
|
53
|
+
|
|
54
|
+
let source: string | undefined;
|
|
55
|
+
let originalLine: number | undefined;
|
|
56
|
+
let originalContent: string | undefined;
|
|
57
|
+
|
|
58
|
+
if (mapping?.source && mapping.originalLine) {
|
|
59
|
+
source = mapping.source;
|
|
60
|
+
originalLine = mapping.originalLine;
|
|
61
|
+
|
|
62
|
+
const srcLines = sourceLineMap.get(source);
|
|
63
|
+
if (srcLines && srcLines.length >= originalLine) {
|
|
64
|
+
originalContent = srcLines[originalLine - 1];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
results.push({ generatedLine: line, generatedContent, source, originalLine, originalContent });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
consumer.destroy();
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Patches Vite's CSS module JS output to be evaluable as a standalone module.
|
|
5
|
+
* Comments out HMR/client imports and re-exports `__vite__css`.
|
|
6
|
+
*/
|
|
7
|
+
export function patchViteModuleCode(code: string): string {
|
|
8
|
+
return code
|
|
9
|
+
.replaceAll("import {", "// import {")
|
|
10
|
+
.replaceAll("__vite__updateStyle", "// __vite__updateStyle")
|
|
11
|
+
.replace(/if\s*\(import\.meta\.hot\)\s*\{[\s\S]*?\n\}/g, "")
|
|
12
|
+
.replaceAll("import.meta.hot", "// import.meta.hot")
|
|
13
|
+
.replaceAll("const __vite__css", "export const __vite__css");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dynamically imports patched code via data: URI to extract the class mapping
|
|
18
|
+
* and raw CSS text.
|
|
19
|
+
*/
|
|
20
|
+
export async function evaluateModuleExports(patchedCode: string): Promise<{
|
|
21
|
+
classMapping: Record<string, string>;
|
|
22
|
+
cssText: string;
|
|
23
|
+
}> {
|
|
24
|
+
const base64 = Buffer.from(patchedCode, "utf-8").toString("base64");
|
|
25
|
+
const mod = await import(`data:text/javascript;base64,${base64}`);
|
|
26
|
+
return {
|
|
27
|
+
classMapping: mod.default ?? {},
|
|
28
|
+
cssText: mod.__vite__css ?? "",
|
|
29
|
+
};
|
|
30
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface CssCommentInfo {
|
|
2
|
+
content: string;
|
|
3
|
+
/** 1-based line of the closing comment marker. */
|
|
4
|
+
endLine: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface VitePluginCssModuleTypesOptions {
|
|
8
|
+
/** Output directory for `.d.ts` files, relative to Vite root. */
|
|
9
|
+
dtsOutputDir: string;
|
|
10
|
+
|
|
11
|
+
/** Folder(s) to include (prefix-matched). When omitted, all folders match. */
|
|
12
|
+
include?: string | string[];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Folder(s) to exclude. Takes precedence over `include`.
|
|
16
|
+
* @default ["node_modules", ".git", "dist", "build"]
|
|
17
|
+
*/
|
|
18
|
+
exclude?: string | string[];
|
|
19
|
+
|
|
20
|
+
/** Emit `export declare const` per class instead of a single `styles` block. */
|
|
21
|
+
namedExports?: boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Use TS 5.0+ arbitrary-extension naming: `*.d.css.ts` instead of `*.css.d.ts`.
|
|
25
|
+
* Requires `allowArbitraryExtensions: true` in tsconfig.
|
|
26
|
+
*/
|
|
27
|
+
allowArbitraryExtensions?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate `.d.ts.map` for "Go to Definition" navigation to CSS source.
|
|
31
|
+
* Requires `css.devSourcemap: true`.
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
declarationMap?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClassEntry {
|
|
38
|
+
/** Original class name (e.g., `my-class`). */
|
|
39
|
+
rule: string;
|
|
40
|
+
/** Hashed/scoped class name from Vite (e.g., `_my-class_1a2b3`). */
|
|
41
|
+
value: string;
|
|
42
|
+
/** 1-based line in the original CSS source. */
|
|
43
|
+
line?: number;
|
|
44
|
+
/** Preceding docblock comment content. */
|
|
45
|
+
comment: string;
|
|
46
|
+
/** CSS rule block text (selector + body). */
|
|
47
|
+
cssBlock: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Maps a `.d.ts` position to a `.module.css` position for declaration maps. */
|
|
51
|
+
export interface DtsSourceMapping {
|
|
52
|
+
dtsLine: number;
|
|
53
|
+
dtsColumn: number;
|
|
54
|
+
cssLine: number;
|
|
55
|
+
cssColumn: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GenerateDtsResult {
|
|
59
|
+
content: string;
|
|
60
|
+
lineMappings: DtsSourceMapping[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DtsGenerationOptions {
|
|
64
|
+
namedExports?: boolean;
|
|
65
|
+
}
|