svelte-ag 1.1.17 → 1.2.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/bin/build-tailwind-manifest.d.ts +15 -0
- package/dist/bin/build-tailwind-manifest.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest.js +581 -0
- package/dist/bin/build-tailwind-manifest.unit.test.d.ts +2 -0
- package/dist/bin/build-tailwind-manifest.unit.test.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest.unit.test.js +208 -0
- package/dist/tailwind-sources.manifest.jsonc +352 -0
- package/dist/vite/index.d.ts +5 -7
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/tailwind-sources-manifest.d.ts +27 -0
- package/dist/vite/tailwind-sources-manifest.d.ts.map +1 -0
- package/dist/vite/tailwind-sources-manifest.js +84 -0
- package/dist/vite/vite-plugin-component-source-collector.d.ts +5 -7
- package/dist/vite/vite-plugin-component-source-collector.d.ts.map +1 -1
- package/dist/vite/vite-plugin-component-source-collector.js +253 -21
- package/dist/vite/vite-plugin-component-source-collector.unit.test.js +114 -1
- package/package.json +10 -3
- package/src/lib/bin/build-tailwind-manifest.ts +755 -0
- package/src/lib/bin/build-tailwind-manifest.unit.test.ts +307 -0
- package/src/lib/vite/tailwind-sources-manifest.ts +125 -0
- package/src/lib/vite/vite-plugin-component-source-collector.ts +312 -31
- package/src/lib/vite/vite-plugin-component-source-collector.unit.test.ts +149 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region dist/bin/build-tailwind-manifest.d.ts
|
|
2
|
+
type GeneratorOptions = {
|
|
3
|
+
exportFilters: string[];
|
|
4
|
+
};
|
|
5
|
+
/** Build a Tailwind source manifest for one package export map. */
|
|
6
|
+
declare function generateTailwindManifestForPackage(packageDir: string, options: GeneratorOptions): Promise<{
|
|
7
|
+
didWrite: boolean;
|
|
8
|
+
outputFile: string;
|
|
9
|
+
exportCount: number;
|
|
10
|
+
}>;
|
|
11
|
+
/** Run the manifest builder once or in watch mode from the current working directory. */
|
|
12
|
+
declare function main(argv?: string[]): Promise<void>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { generateTailwindManifestForPackage, main };
|
|
15
|
+
//# sourceMappingURL=build-tailwind-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-tailwind-manifest.d.ts","names":["GeneratorOptions","exportFilters","generateTailwindManifestForPackage","Promise","packageDir","options","didWrite","outputFile","exportCount","main","argv"],"sources":["../../home/runner/work/svelte-ag/svelte-ag/dist/bin/build-tailwind-manifest.d.ts"],"mappings":";KAAKA,gBAAAA;EACDC,aAAAA;AAAAA;;iBAGoBC,kCAAAA,CAAmCE,UAAAA,UAAoBC,OAAAA,EAASL,gBAAAA,GAAmBG,OAAAA;EACvGG,QAAAA;EACAC,UAAAA;EACAC,WAAAA;AAAAA;;iBAGoBC,IAAAA,CAAKC,IAAAA,cAAkBP,OAAAA"}
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { glob, readFile, readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { init, parse as parseEsm } from 'es-module-lexer';
|
|
6
|
+
import { parse as parseSvelte } from 'svelte/compiler';
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
import { readPackageJson, writeIfDifferent } from 'ts-ag';
|
|
9
|
+
import { ensureRelativeManifestSourcePath, getTailwindSourcesManifestPath, normalizeManifestExportFilter, serializeTailwindSourceManifest, shouldIncludeManifestExport } from '../vite/tailwind-sources-manifest.js';
|
|
10
|
+
const WATCH_POLL_INTERVAL_MS = 700;
|
|
11
|
+
const CLASS_COLLECTOR_CALLS = new Set(['cn', 'clsx', 'cva', 'tv']);
|
|
12
|
+
const IGNORED_DIRECTORIES = new Set(['.git', '.svelte-kit', 'node_modules']);
|
|
13
|
+
/** Build a Tailwind source manifest for one package export map. */
|
|
14
|
+
export async function generateTailwindManifestForPackage(packageDir, options) {
|
|
15
|
+
const packageJson = (await readPackageJson(path.join(packageDir, 'package.json')));
|
|
16
|
+
if (!packageJson) {
|
|
17
|
+
throw new Error(`No package.json found in ${packageDir}`);
|
|
18
|
+
}
|
|
19
|
+
const manifest = {
|
|
20
|
+
version: 1,
|
|
21
|
+
exports: {}
|
|
22
|
+
};
|
|
23
|
+
const packageExports = packageJson.exports ?? {};
|
|
24
|
+
const exportEntries = Object.entries(packageExports).filter(([exportKey]) => shouldIncludeManifestExport(exportKey, options.exportFilters));
|
|
25
|
+
for (const [exportKey, exportTarget] of exportEntries) {
|
|
26
|
+
if (exportKey.includes('*') || hasWildcardTarget(exportTarget)) {
|
|
27
|
+
console.warn(`[tailwind-manifest] Skipping wildcard export ${exportKey} in ${packageDir}`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const entryFiles = collectRuntimeTargets(exportTarget)
|
|
31
|
+
.map((target) => resolvePackageEntryFile(packageDir, target))
|
|
32
|
+
.filter((targetPath) => targetPath !== null);
|
|
33
|
+
if (entryFiles.length === 0) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const exportScan = await scanFileGraph(entryFiles, packageDir);
|
|
37
|
+
const manifestEntry = toManifestLeaf(exportScan);
|
|
38
|
+
const symbols = await collectExportSymbols(entryFiles, packageDir);
|
|
39
|
+
if (Object.keys(symbols).length > 0) {
|
|
40
|
+
manifest.exports[exportKey] = {
|
|
41
|
+
...manifestEntry,
|
|
42
|
+
symbols
|
|
43
|
+
};
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
manifest.exports[exportKey] = manifestEntry;
|
|
47
|
+
}
|
|
48
|
+
const outputFile = getTailwindSourcesManifestPath(packageDir, packageJson);
|
|
49
|
+
const didWrite = await writeIfDifferent(outputFile, serializeTailwindSourceManifest(manifest));
|
|
50
|
+
return { didWrite, outputFile, exportCount: Object.keys(manifest.exports).length };
|
|
51
|
+
}
|
|
52
|
+
/** Resolve exported symbols to their own Tailwind class and CSS source sets. */
|
|
53
|
+
async function collectExportSymbols(entryFiles, packageDir) {
|
|
54
|
+
const symbols = new Map();
|
|
55
|
+
for (const entryFile of entryFiles) {
|
|
56
|
+
const symbolTargets = await readEntrySymbolTargets(entryFile);
|
|
57
|
+
for (const [symbolName, targetFile] of symbolTargets) {
|
|
58
|
+
const scan = await scanFileGraph([targetFile], packageDir);
|
|
59
|
+
const existing = symbols.get(symbolName);
|
|
60
|
+
if (existing) {
|
|
61
|
+
for (const className of scan.classes)
|
|
62
|
+
existing.classes.add(className);
|
|
63
|
+
for (const sourcePath of scan.sources)
|
|
64
|
+
existing.sources.add(sourcePath);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
symbols.set(symbolName, {
|
|
68
|
+
classes: new Set(scan.classes),
|
|
69
|
+
sources: new Set(scan.sources)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Object.fromEntries([...symbols.entries()]
|
|
74
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
75
|
+
.map(([symbolName, scan]) => [symbolName, toManifestLeaf(scan)]));
|
|
76
|
+
}
|
|
77
|
+
/** Map re-exported symbol names from an entry file back to local source files. */
|
|
78
|
+
async function readEntrySymbolTargets(entryFile, visited = new Set()) {
|
|
79
|
+
if (visited.has(entryFile)) {
|
|
80
|
+
return new Map();
|
|
81
|
+
}
|
|
82
|
+
visited.add(entryFile);
|
|
83
|
+
const source = await readFile(entryFile, 'utf8');
|
|
84
|
+
const importBindings = new Map();
|
|
85
|
+
const symbolTargets = new Map();
|
|
86
|
+
for (const snippet of getModuleSnippets(entryFile, source)) {
|
|
87
|
+
await init;
|
|
88
|
+
const [imports, exports] = parseEsm(snippet);
|
|
89
|
+
for (const parsedImport of imports) {
|
|
90
|
+
const statement = snippet.slice(parsedImport.ss, parsedImport.se).trim();
|
|
91
|
+
if (statement === '' || /^import\s+type\b/.test(statement) || /^export\s+type\b/.test(statement)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (statement.startsWith('import')) {
|
|
95
|
+
const specifier = parsedImport.n;
|
|
96
|
+
if (!specifier || !isRelativeSpecifier(specifier))
|
|
97
|
+
continue;
|
|
98
|
+
for (const localName of readImportBindingNames(statement)) {
|
|
99
|
+
importBindings.set(localName, specifier);
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!statement.startsWith('export')) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const specifier = parsedImport.n;
|
|
107
|
+
if (!specifier || !isRelativeSpecifier(specifier))
|
|
108
|
+
continue;
|
|
109
|
+
const targetFile = resolveLocalImportPath(specifier, entryFile);
|
|
110
|
+
if (!targetFile)
|
|
111
|
+
continue;
|
|
112
|
+
if (/^export\s+\*\s+from\b/.test(statement)) {
|
|
113
|
+
const nestedTargets = await readEntrySymbolTargets(targetFile, visited);
|
|
114
|
+
for (const [symbolName, nestedTargetFile] of nestedTargets) {
|
|
115
|
+
if (!symbolTargets.has(symbolName)) {
|
|
116
|
+
symbolTargets.set(symbolName, nestedTargetFile);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const statementExports = exports.filter((exportRecord) => exportRecord.s >= parsedImport.ss && exportRecord.e <= parsedImport.se);
|
|
122
|
+
for (const exportRecord of statementExports) {
|
|
123
|
+
symbolTargets.set(exportRecord.n, targetFile);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const exportRecord of exports) {
|
|
127
|
+
const localName = exportRecord.ln ?? exportRecord.n;
|
|
128
|
+
const localSpecifier = importBindings.get(localName);
|
|
129
|
+
if (!localSpecifier)
|
|
130
|
+
continue;
|
|
131
|
+
const targetFile = resolveLocalImportPath(localSpecifier, entryFile);
|
|
132
|
+
if (!targetFile)
|
|
133
|
+
continue;
|
|
134
|
+
if (!symbolTargets.has(exportRecord.n)) {
|
|
135
|
+
symbolTargets.set(exportRecord.n, targetFile);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
visited.delete(entryFile);
|
|
140
|
+
return symbolTargets;
|
|
141
|
+
}
|
|
142
|
+
/** Walk local imports reachable from entry files and collect classes plus CSS sources. */
|
|
143
|
+
async function scanFileGraph(entryFiles, packageDir) {
|
|
144
|
+
const scan = {
|
|
145
|
+
classes: new Set(),
|
|
146
|
+
sources: new Set()
|
|
147
|
+
};
|
|
148
|
+
const visited = new Set();
|
|
149
|
+
const visit = async (filePath) => {
|
|
150
|
+
if (visited.has(filePath) || !isPathInside(packageDir, filePath)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
visited.add(filePath);
|
|
154
|
+
const source = await readFile(filePath, 'utf8');
|
|
155
|
+
collectClassNamesFromFile(filePath, source, scan.classes);
|
|
156
|
+
if (filePath.endsWith('.css')) {
|
|
157
|
+
scan.sources.add(ensureRelativeManifestSourcePath(toPosixPath(path.relative(packageDir, filePath))));
|
|
158
|
+
}
|
|
159
|
+
for (const specifier of await extractLocalSpecifiers(filePath, source)) {
|
|
160
|
+
const targetFile = resolveLocalImportPath(specifier, filePath);
|
|
161
|
+
if (!targetFile)
|
|
162
|
+
continue;
|
|
163
|
+
await visit(targetFile);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
for (const entryFile of entryFiles) {
|
|
167
|
+
await visit(entryFile);
|
|
168
|
+
}
|
|
169
|
+
return scan;
|
|
170
|
+
}
|
|
171
|
+
/** Extract likely Tailwind class tokens from markup and script expressions. */
|
|
172
|
+
function collectClassNamesFromFile(filePath, source, out) {
|
|
173
|
+
const markupSource = getClassAttributeSource(filePath, source);
|
|
174
|
+
for (const match of markupSource.matchAll(/(?:class|className)\s*=\s*(['"`])([\s\S]*?)\1/g)) {
|
|
175
|
+
addClassTokens(match[2] ?? '', out);
|
|
176
|
+
}
|
|
177
|
+
for (const match of markupSource.matchAll(/(?:class|className)\s*=\s*\{([\s\S]*?)\}/g)) {
|
|
178
|
+
const expression = match[1]?.trim();
|
|
179
|
+
if (!expression)
|
|
180
|
+
continue;
|
|
181
|
+
const sourceFile = createTypeScriptSourceFile(filePath, `(${expression})`);
|
|
182
|
+
const visit = (node) => {
|
|
183
|
+
if (ts.isCallExpression(node) && CLASS_COLLECTOR_CALLS.has(getExpressionName(node.expression))) {
|
|
184
|
+
collectStringLiterals(node, out);
|
|
185
|
+
}
|
|
186
|
+
if (ts.isPropertyAssignment(node) && isTailwindTokenPropertyName(node.name)) {
|
|
187
|
+
collectStringLiterals(node.initializer, out);
|
|
188
|
+
}
|
|
189
|
+
ts.forEachChild(node, visit);
|
|
190
|
+
};
|
|
191
|
+
visit(sourceFile);
|
|
192
|
+
}
|
|
193
|
+
for (const scriptBlock of getModuleSnippets(filePath, source)) {
|
|
194
|
+
const sourceFile = createTypeScriptSourceFile(filePath, scriptBlock);
|
|
195
|
+
const visit = (node) => {
|
|
196
|
+
if (ts.isCallExpression(node) && CLASS_COLLECTOR_CALLS.has(getExpressionName(node.expression))) {
|
|
197
|
+
collectStringLiterals(node, out);
|
|
198
|
+
}
|
|
199
|
+
if (ts.isPropertyAssignment(node) && isTailwindTokenPropertyName(node.name)) {
|
|
200
|
+
collectStringLiterals(node.initializer, out);
|
|
201
|
+
}
|
|
202
|
+
ts.forEachChild(node, visit);
|
|
203
|
+
};
|
|
204
|
+
visit(sourceFile);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function getClassAttributeSource(filePath, source) {
|
|
208
|
+
if (!filePath.endsWith('.svelte')) {
|
|
209
|
+
return source;
|
|
210
|
+
}
|
|
211
|
+
return source.replaceAll(/<!--[\s\S]*?-->/g, '');
|
|
212
|
+
}
|
|
213
|
+
function getModuleSnippets(filePath, source) {
|
|
214
|
+
if (!filePath.endsWith('.svelte')) {
|
|
215
|
+
return [source];
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const ast = parseSvelte(source, { filename: filePath, modern: true });
|
|
219
|
+
const scripts = [ast.module, ast.instance].filter(Boolean);
|
|
220
|
+
return scripts.map((script) => {
|
|
221
|
+
const content = script.content;
|
|
222
|
+
return source.slice(content.start, content.end);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return [source];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Recursively collect class tokens from string literal nodes. */
|
|
230
|
+
function collectStringLiterals(node, out) {
|
|
231
|
+
if (ts.isStringLiteralLike(node)) {
|
|
232
|
+
addClassTokens(node.text, out);
|
|
233
|
+
}
|
|
234
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
235
|
+
addClassTokens(node.text, out);
|
|
236
|
+
}
|
|
237
|
+
ts.forEachChild(node, (child) => collectStringLiterals(child, out));
|
|
238
|
+
}
|
|
239
|
+
/** Split whitespace-delimited class strings into manifest tokens. */
|
|
240
|
+
function addClassTokens(value, out) {
|
|
241
|
+
for (const token of value.split(/\s+/)) {
|
|
242
|
+
const trimmed = token.trim();
|
|
243
|
+
if (trimmed === '' || trimmed.includes('${'))
|
|
244
|
+
continue;
|
|
245
|
+
out.add(trimmed);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Parse source text with a script kind inferred from the file extension. */
|
|
249
|
+
function createTypeScriptSourceFile(filePath, source) {
|
|
250
|
+
const scriptKind = filePath.endsWith('.js')
|
|
251
|
+
? ts.ScriptKind.JS
|
|
252
|
+
: filePath.endsWith('.jsx')
|
|
253
|
+
? ts.ScriptKind.JSX
|
|
254
|
+
: filePath.endsWith('.tsx')
|
|
255
|
+
? ts.ScriptKind.TSX
|
|
256
|
+
: ts.ScriptKind.TS;
|
|
257
|
+
return ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, scriptKind);
|
|
258
|
+
}
|
|
259
|
+
/** Get a stable function name from simple call expressions. */
|
|
260
|
+
function getExpressionName(expression) {
|
|
261
|
+
if (ts.isIdentifier(expression)) {
|
|
262
|
+
return expression.text;
|
|
263
|
+
}
|
|
264
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
265
|
+
return expression.name.text;
|
|
266
|
+
}
|
|
267
|
+
return '';
|
|
268
|
+
}
|
|
269
|
+
/** Match object keys commonly used to hold Tailwind token strings. */
|
|
270
|
+
function isTailwindTokenPropertyName(name) {
|
|
271
|
+
return (ts.isIdentifier(name) || ts.isStringLiteral(name)) && /class/i.test(name.text);
|
|
272
|
+
}
|
|
273
|
+
/** Find relative imports/exports with module-lexer and CSS imports with a fallback regex. */
|
|
274
|
+
async function extractLocalSpecifiers(filePath, source) {
|
|
275
|
+
const matches = new Set();
|
|
276
|
+
await init;
|
|
277
|
+
const collectEsmSpecifiers = async (snippet) => {
|
|
278
|
+
const [imports] = parseEsm(snippet);
|
|
279
|
+
for (const parsedImport of imports) {
|
|
280
|
+
const specifier = parsedImport.n;
|
|
281
|
+
if (!specifier || !isRelativeSpecifier(specifier))
|
|
282
|
+
continue;
|
|
283
|
+
const statement = snippet.slice(parsedImport.ss, parsedImport.se).trim();
|
|
284
|
+
if (statement === '' || /^import\s+type\b/.test(statement) || /^export\s+type\b/.test(statement)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (!statement.startsWith('import') && !statement.startsWith('export'))
|
|
288
|
+
continue;
|
|
289
|
+
matches.add(specifier);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
if (filePath.endsWith('.svelte')) {
|
|
293
|
+
for (const snippet of getModuleSnippets(filePath, source)) {
|
|
294
|
+
await collectEsmSpecifiers(snippet);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
await collectEsmSpecifiers(source);
|
|
299
|
+
}
|
|
300
|
+
for (const match of source.matchAll(/@import\s+['"]([^'"]+)['"]/g)) {
|
|
301
|
+
const specifier = match[1];
|
|
302
|
+
if (specifier && isRelativeSpecifier(specifier)) {
|
|
303
|
+
matches.add(specifier);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return [...matches];
|
|
307
|
+
}
|
|
308
|
+
/** Check whether a module specifier resolves to a local file. */
|
|
309
|
+
function isRelativeSpecifier(specifier) {
|
|
310
|
+
return specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/');
|
|
311
|
+
}
|
|
312
|
+
/** Resolve a relative import from one file to an on-disk source file. */
|
|
313
|
+
function resolveLocalImportPath(specifier, importerPath) {
|
|
314
|
+
const cleanSpecifier = specifier.split('?')[0]?.split('#')[0] ?? specifier;
|
|
315
|
+
const targetPath = path.resolve(path.dirname(importerPath), cleanSpecifier);
|
|
316
|
+
return resolveFileCandidate(targetPath);
|
|
317
|
+
}
|
|
318
|
+
/** Resolve a package export target to its runtime source file. */
|
|
319
|
+
function resolvePackageEntryFile(packageDir, entryTarget) {
|
|
320
|
+
const normalizedTarget = entryTarget.startsWith('./') ? entryTarget : `./${entryTarget}`;
|
|
321
|
+
return resolveFileCandidate(path.resolve(packageDir, normalizedTarget));
|
|
322
|
+
}
|
|
323
|
+
/** Try supported source extensions and index files for a target path. */
|
|
324
|
+
function resolveFileCandidate(targetPath) {
|
|
325
|
+
const candidates = [...buildBaseCandidates(targetPath), ...buildBaseCandidates(path.join(targetPath, 'index'))];
|
|
326
|
+
for (const candidate of candidates) {
|
|
327
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
328
|
+
return candidate;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
function buildBaseCandidates(basePath) {
|
|
334
|
+
const candidates = new Set();
|
|
335
|
+
const addCandidatesForBase = (candidateBase) => {
|
|
336
|
+
candidates.add(candidateBase);
|
|
337
|
+
candidates.add(`${candidateBase}.js`);
|
|
338
|
+
candidates.add(`${candidateBase}.mjs`);
|
|
339
|
+
candidates.add(`${candidateBase}.cjs`);
|
|
340
|
+
candidates.add(`${candidateBase}.ts`);
|
|
341
|
+
candidates.add(`${candidateBase}.tsx`);
|
|
342
|
+
candidates.add(`${candidateBase}.jsx`);
|
|
343
|
+
candidates.add(`${candidateBase}.svelte`);
|
|
344
|
+
candidates.add(`${candidateBase}.svelte.ts`);
|
|
345
|
+
candidates.add(`${candidateBase}.css`);
|
|
346
|
+
};
|
|
347
|
+
addCandidatesForBase(basePath);
|
|
348
|
+
const emittedBasePath = stripEmittedModuleExtension(basePath);
|
|
349
|
+
if (emittedBasePath !== basePath) {
|
|
350
|
+
addCandidatesForBase(emittedBasePath);
|
|
351
|
+
}
|
|
352
|
+
return [...candidates];
|
|
353
|
+
}
|
|
354
|
+
function stripEmittedModuleExtension(targetPath) {
|
|
355
|
+
return targetPath.replace(/\.(?:mjs|cjs|js)$/i, '');
|
|
356
|
+
}
|
|
357
|
+
function readImportBindingNames(statement) {
|
|
358
|
+
const fromMatch = statement.match(/^import\s+([\s\S]*?)\s+from\s*['"][^'"]+['"]\s*;?$/);
|
|
359
|
+
if (!fromMatch) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
const importClause = fromMatch[1]?.trim();
|
|
363
|
+
if (!importClause || importClause.startsWith('type ')) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
const bindingNames = [];
|
|
367
|
+
const namedImportsMatch = importClause.match(/\{([\s\S]*?)\}/);
|
|
368
|
+
if (namedImportsMatch) {
|
|
369
|
+
for (const rawImport of namedImportsMatch[1].split(',')) {
|
|
370
|
+
const trimmedImport = rawImport.trim();
|
|
371
|
+
if (trimmedImport === '' || trimmedImport.startsWith('type '))
|
|
372
|
+
continue;
|
|
373
|
+
const aliasParts = trimmedImport.split(/\s+as\s+/i).map((part) => part.trim());
|
|
374
|
+
const localName = aliasParts.at(-1);
|
|
375
|
+
if (localName) {
|
|
376
|
+
bindingNames.push(localName);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const remainingClause = importClause
|
|
381
|
+
.replace(/\{[\s\S]*?\}/, '')
|
|
382
|
+
.trim()
|
|
383
|
+
.replace(/,$/, '')
|
|
384
|
+
.trim();
|
|
385
|
+
if (remainingClause !== '') {
|
|
386
|
+
for (const segment of remainingClause
|
|
387
|
+
.split(',')
|
|
388
|
+
.map((part) => part.trim())
|
|
389
|
+
.filter(Boolean)) {
|
|
390
|
+
if (segment.startsWith('* as ')) {
|
|
391
|
+
bindingNames.push(segment.slice(5).trim());
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (!segment.startsWith('type ')) {
|
|
395
|
+
bindingNames.push(segment);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return bindingNames;
|
|
400
|
+
}
|
|
401
|
+
/** Flatten runtime export targets while skipping type-only branches. */
|
|
402
|
+
function collectRuntimeTargets(target) {
|
|
403
|
+
if (typeof target === 'string') {
|
|
404
|
+
return target.endsWith('.d.ts') ? [] : [target];
|
|
405
|
+
}
|
|
406
|
+
if (Array.isArray(target)) {
|
|
407
|
+
return target.flatMap(collectRuntimeTargets);
|
|
408
|
+
}
|
|
409
|
+
return Object.entries(target).flatMap(([key, value]) => {
|
|
410
|
+
if (key === 'types') {
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
return collectRuntimeTargets(value);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/** Detect wildcard exports that cannot be resolved to fixed source files. */
|
|
417
|
+
function hasWildcardTarget(target) {
|
|
418
|
+
if (typeof target === 'string') {
|
|
419
|
+
return target.includes('*');
|
|
420
|
+
}
|
|
421
|
+
if (Array.isArray(target)) {
|
|
422
|
+
return target.some(hasWildcardTarget);
|
|
423
|
+
}
|
|
424
|
+
return Object.values(target).some(hasWildcardTarget);
|
|
425
|
+
}
|
|
426
|
+
/** Convert collected sets into the manifest's sorted JSON shape. */
|
|
427
|
+
function toManifestLeaf(scan) {
|
|
428
|
+
return {
|
|
429
|
+
classes: [...scan.classes].sort(),
|
|
430
|
+
sources: [...scan.sources].sort()
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/** Guard graph traversal so scans stay inside the current package. */
|
|
434
|
+
function isPathInside(parentPath, childPath) {
|
|
435
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
436
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
437
|
+
}
|
|
438
|
+
/** Normalize file paths for manifest stability across platforms. */
|
|
439
|
+
function toPosixPath(filePath) {
|
|
440
|
+
return filePath.replaceAll('\\', '/');
|
|
441
|
+
}
|
|
442
|
+
/** Resolve package directories from `package.json` globs relative to the working directory. */
|
|
443
|
+
async function discoverPackageDirectories(rootDir, packagePatterns) {
|
|
444
|
+
const rootPackageJson = path.join(rootDir, 'package.json');
|
|
445
|
+
if (packagePatterns.length === 0 && existsSync(rootPackageJson)) {
|
|
446
|
+
return [rootDir];
|
|
447
|
+
}
|
|
448
|
+
const packageJsonPaths = new Set();
|
|
449
|
+
for (const pattern of packagePatterns) {
|
|
450
|
+
for await (const matchedPath of glob(pattern, { cwd: rootDir })) {
|
|
451
|
+
const absolutePath = path.resolve(rootDir, matchedPath);
|
|
452
|
+
if (path.basename(absolutePath) !== 'package.json') {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
packageJsonPaths.add(absolutePath);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return [...packageJsonPaths].map((packageJsonPath) => path.dirname(packageJsonPath)).sort();
|
|
459
|
+
}
|
|
460
|
+
/** Generate manifests for every matching package and report what changed. */
|
|
461
|
+
async function runBuild(rootDir, options) {
|
|
462
|
+
const packageDirs = await discoverPackageDirectories(rootDir, options.packagePatterns);
|
|
463
|
+
if (packageDirs.length === 0) {
|
|
464
|
+
throw new Error('[tailwind-manifest] No matching package.json files found.');
|
|
465
|
+
}
|
|
466
|
+
for (const packageDir of packageDirs) {
|
|
467
|
+
const result = await generateTailwindManifestForPackage(packageDir, {
|
|
468
|
+
exportFilters: options.exportFilters
|
|
469
|
+
});
|
|
470
|
+
const relativeOutputPath = path.relative(rootDir, result.outputFile) || result.outputFile;
|
|
471
|
+
console.log(`[tailwind-manifest] ${result.didWrite ? 'wrote' : 'unchanged'} ${relativeOutputPath} (${result.exportCount} exports)`);
|
|
472
|
+
}
|
|
473
|
+
return packageDirs;
|
|
474
|
+
}
|
|
475
|
+
/** Rebuild manifests when any tracked package directory snapshot changes. */
|
|
476
|
+
async function runWatch(rootDir, options) {
|
|
477
|
+
const packageDirs = await runBuild(rootDir, options);
|
|
478
|
+
const snapshots = new Map();
|
|
479
|
+
for (const packageDir of packageDirs) {
|
|
480
|
+
snapshots.set(packageDir, await createDirectorySnapshot(packageDir));
|
|
481
|
+
}
|
|
482
|
+
console.log(`[tailwind-manifest] watching ${packageDirs.length} package${packageDirs.length === 1 ? '' : 's'}`);
|
|
483
|
+
const interval = setInterval(async () => {
|
|
484
|
+
for (const packageDir of packageDirs) {
|
|
485
|
+
const nextSnapshot = await createDirectorySnapshot(packageDir);
|
|
486
|
+
if (snapshots.get(packageDir) === nextSnapshot) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
snapshots.set(packageDir, nextSnapshot);
|
|
490
|
+
try {
|
|
491
|
+
const result = await generateTailwindManifestForPackage(packageDir, {
|
|
492
|
+
exportFilters: options.exportFilters
|
|
493
|
+
});
|
|
494
|
+
const relativeOutputPath = path.relative(rootDir, result.outputFile) || result.outputFile;
|
|
495
|
+
console.log(`[tailwind-manifest] ${result.didWrite ? 'wrote' : 'unchanged'} ${relativeOutputPath} (${result.exportCount} exports)`);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
console.error(error);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}, WATCH_POLL_INTERVAL_MS);
|
|
502
|
+
process.on('SIGINT', () => {
|
|
503
|
+
clearInterval(interval);
|
|
504
|
+
process.exit(0);
|
|
505
|
+
});
|
|
506
|
+
await new Promise(() => { });
|
|
507
|
+
}
|
|
508
|
+
/** Hash directory contents into a cheap polling snapshot for watch mode. */
|
|
509
|
+
async function createDirectorySnapshot(packageDir) {
|
|
510
|
+
const files = [];
|
|
511
|
+
const queue = [packageDir];
|
|
512
|
+
while (queue.length > 0) {
|
|
513
|
+
const currentDir = queue.shift();
|
|
514
|
+
if (!currentDir)
|
|
515
|
+
break;
|
|
516
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
517
|
+
for (const entry of entries) {
|
|
518
|
+
if (entry.isDirectory()) {
|
|
519
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
520
|
+
queue.push(path.join(currentDir, entry.name));
|
|
521
|
+
}
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
525
|
+
const fileStat = await stat(absolutePath);
|
|
526
|
+
files.push(`${toPosixPath(path.relative(packageDir, absolutePath))}:${fileStat.size}:${fileStat.mtimeMs}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return files.sort().join('|');
|
|
530
|
+
}
|
|
531
|
+
/** Parse manifest builder CLI flags into runtime options. */
|
|
532
|
+
function parseCliArgs(argv) {
|
|
533
|
+
const packagePatterns = [];
|
|
534
|
+
const exportFilters = [];
|
|
535
|
+
let watch = false;
|
|
536
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
537
|
+
const arg = argv[i];
|
|
538
|
+
if (arg === '--watch') {
|
|
539
|
+
watch = true;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (arg === '--packages') {
|
|
543
|
+
const value = argv[i + 1];
|
|
544
|
+
if (!value)
|
|
545
|
+
throw new Error('--packages requires a value');
|
|
546
|
+
packagePatterns.push(...value
|
|
547
|
+
.split(',')
|
|
548
|
+
.map((part) => part.trim())
|
|
549
|
+
.filter(Boolean));
|
|
550
|
+
i += 1;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (arg === '--exports') {
|
|
554
|
+
const value = argv[i + 1];
|
|
555
|
+
if (!value)
|
|
556
|
+
throw new Error('--exports requires a value');
|
|
557
|
+
exportFilters.push(...value
|
|
558
|
+
.split(',')
|
|
559
|
+
.map((part) => normalizeManifestExportFilter(part))
|
|
560
|
+
.filter(Boolean));
|
|
561
|
+
i += 1;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
565
|
+
}
|
|
566
|
+
return { exportFilters, packagePatterns, watch };
|
|
567
|
+
}
|
|
568
|
+
/** Run the manifest builder once or in watch mode from the current working directory. */
|
|
569
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
570
|
+
const options = parseCliArgs(argv);
|
|
571
|
+
const rootDir = process.cwd();
|
|
572
|
+
if (options.watch) {
|
|
573
|
+
await runWatch(rootDir, options);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
await runBuild(rootDir, options);
|
|
577
|
+
}
|
|
578
|
+
void main().catch((error) => {
|
|
579
|
+
console.error(error);
|
|
580
|
+
process.exitCode = 1;
|
|
581
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-tailwind-manifest.unit.test.d.ts","sourceRoot":"","sources":["../../src/lib/bin/build-tailwind-manifest.unit.test.ts"],"names":[],"mappings":""}
|