tw-migrate 0.2.0 → 0.3.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/README.md +10 -10
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +146 -0
- package/{index.d.ts → dist/index.d.ts} +17 -13
- package/dist/index.js +2187 -0
- package/package.json +20 -27
- package/bin/tw-migrate.js +0 -51
- package/html.js +0 -191
- package/index.js +0 -2225
- package/native.js +0 -41
- package/style-compiler.js +0 -140
- package/vue.js +0 -541
package/dist/index.js
ADDED
|
@@ -0,0 +1,2187 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { chmod, lstat, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { parse } from "parse5";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
//#region src/util/diff.ts
|
|
9
|
+
function unifiedDiff(path, before, after) {
|
|
10
|
+
const oldLines = before.split("\n");
|
|
11
|
+
const newLines = after.split("\n");
|
|
12
|
+
return [
|
|
13
|
+
`--- a/${path}\n`,
|
|
14
|
+
`+++ b/${path}\n`,
|
|
15
|
+
`@@ -1,${oldLines.length} +1,${newLines.length} @@\n`,
|
|
16
|
+
...oldLines.map((line, index) => `${index === oldLines.length - 1 && line === "" ? " " : "-"}${line}\n`),
|
|
17
|
+
...newLines.map((line, index) => `${index === newLines.length - 1 && line === "" ? " " : "+"}${line}\n`)
|
|
18
|
+
].join("");
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/parser/html.ts
|
|
22
|
+
const RECOVERABLE_PARSE_ERRORS = /* @__PURE__ */ new Set(["missing-doctype"]);
|
|
23
|
+
const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
|
|
24
|
+
function parseHtmlSource(path, source) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
const document = parse(source, {
|
|
27
|
+
sourceCodeLocationInfo: true,
|
|
28
|
+
onParseError(error) {
|
|
29
|
+
if (!RECOVERABLE_PARSE_ERRORS.has(error.code)) errors.push(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (errors.length > 0) throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
|
|
33
|
+
const links = [];
|
|
34
|
+
const bases = [];
|
|
35
|
+
const elements = [];
|
|
36
|
+
const dynamicAttributes = [];
|
|
37
|
+
const scriptTexts = [];
|
|
38
|
+
const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
|
|
39
|
+
function visit(node) {
|
|
40
|
+
if (node.tagName === "script") {
|
|
41
|
+
for (const child of node.childNodes ?? []) if (child.nodeName === "#text" && child.value) scriptTexts.push(child.value);
|
|
42
|
+
}
|
|
43
|
+
if (node.tagName) {
|
|
44
|
+
const attributes = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
|
|
45
|
+
const locations = node.sourceCodeLocation?.attrs;
|
|
46
|
+
if (locations) {
|
|
47
|
+
if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
|
|
48
|
+
const href = locatedAttribute(source, locations.href, attributes.get("href"));
|
|
49
|
+
const media = locatedAttribute(source, locations.media, attributes.get("media"));
|
|
50
|
+
const tag = node.sourceCodeLocation?.startTag ?? node.sourceCodeLocation;
|
|
51
|
+
if (href && tag) links.push({
|
|
52
|
+
href: href.value,
|
|
53
|
+
media: media?.value ?? "",
|
|
54
|
+
start: href.start,
|
|
55
|
+
end: href.end,
|
|
56
|
+
tagStart: tag.startOffset,
|
|
57
|
+
tagEnd: tag.endOffset
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (node.tagName === "base" && bases.length === 0) {
|
|
61
|
+
const href = locatedAttribute(source, locations.href, attributes.get("href"));
|
|
62
|
+
if (href) bases.push({
|
|
63
|
+
href: href.value,
|
|
64
|
+
writable: href.writable,
|
|
65
|
+
start: href.start,
|
|
66
|
+
end: href.end
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
|
|
70
|
+
const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
|
|
71
|
+
const dynamic = (!classAttribute && idAttribute && locations.class ? {
|
|
72
|
+
start: locations.class.startOffset,
|
|
73
|
+
end: locations.class.endOffset
|
|
74
|
+
} : void 0) ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
|
|
75
|
+
if (dynamic) dynamicAttributes.push({
|
|
76
|
+
start: dynamic.start,
|
|
77
|
+
end: dynamic.end
|
|
78
|
+
});
|
|
79
|
+
else if (classAttribute || idAttribute) {
|
|
80
|
+
if (!classAttribute && idAttribute) {
|
|
81
|
+
const insertion = classInsertionOffset$1(source, node.sourceCodeLocation?.startTag);
|
|
82
|
+
if (insertion !== void 0) classAttribute = {
|
|
83
|
+
value: "",
|
|
84
|
+
start: insertion,
|
|
85
|
+
end: insertion,
|
|
86
|
+
synthetic: true
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
elements.push({
|
|
90
|
+
classAttribute,
|
|
91
|
+
idAttribute
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const child of node.childNodes ?? []) visit(child);
|
|
97
|
+
if (node.content) visit(node.content);
|
|
98
|
+
}
|
|
99
|
+
visit(document);
|
|
100
|
+
return {
|
|
101
|
+
...toByteOffsets(source, {
|
|
102
|
+
links,
|
|
103
|
+
bases,
|
|
104
|
+
elements,
|
|
105
|
+
dynamicAttributes
|
|
106
|
+
}),
|
|
107
|
+
scriptText: scriptTexts.join("\n")
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function utf8OffsetMap(source, indices) {
|
|
111
|
+
const sorted = [...new Set(indices)].sort((left, right) => left - right);
|
|
112
|
+
const map = /* @__PURE__ */ new Map();
|
|
113
|
+
let lastIndex = 0;
|
|
114
|
+
let bytes = 0;
|
|
115
|
+
for (const index of sorted) {
|
|
116
|
+
bytes += Buffer.byteLength(source.slice(lastIndex, index));
|
|
117
|
+
lastIndex = index;
|
|
118
|
+
map.set(index, bytes);
|
|
119
|
+
}
|
|
120
|
+
return map;
|
|
121
|
+
}
|
|
122
|
+
function offsetLookup(offsets) {
|
|
123
|
+
return (index) => {
|
|
124
|
+
const byte = offsets.get(index);
|
|
125
|
+
if (byte === void 0) throw new Error(`No byte offset was mapped for index ${index}`);
|
|
126
|
+
return byte;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function toByteOffsets(source, parsed) {
|
|
130
|
+
const offset = offsetLookup(utf8OffsetMap(source, [
|
|
131
|
+
...parsed.links.flatMap((link) => [
|
|
132
|
+
link.start,
|
|
133
|
+
link.end,
|
|
134
|
+
link.tagStart,
|
|
135
|
+
link.tagEnd
|
|
136
|
+
]),
|
|
137
|
+
...parsed.bases.flatMap((base) => [base.start, base.end]),
|
|
138
|
+
...parsed.elements.flatMap((element) => [element.classAttribute, element.idAttribute].flatMap((value) => value ? [value.start, value.end] : [])),
|
|
139
|
+
...parsed.dynamicAttributes.flatMap((value) => [value.start, value.end])
|
|
140
|
+
]));
|
|
141
|
+
const attribute = (value) => ({
|
|
142
|
+
...value,
|
|
143
|
+
start: offset(value.start),
|
|
144
|
+
end: offset(value.end)
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
links: parsed.links.map((link) => ({
|
|
148
|
+
...link,
|
|
149
|
+
start: offset(link.start),
|
|
150
|
+
end: offset(link.end),
|
|
151
|
+
tagStart: offset(link.tagStart),
|
|
152
|
+
tagEnd: offset(link.tagEnd)
|
|
153
|
+
})),
|
|
154
|
+
bases: parsed.bases.map((base) => attribute(base)),
|
|
155
|
+
elements: parsed.elements.map((element) => ({
|
|
156
|
+
classAttribute: element.classAttribute && attribute(element.classAttribute),
|
|
157
|
+
idAttribute: element.idAttribute && attribute(element.idAttribute)
|
|
158
|
+
})),
|
|
159
|
+
dynamicAttributes: parsed.dynamicAttributes.map((value) => attribute(value))
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function classInsertionOffset$1(source, startTag) {
|
|
163
|
+
if (!startTag) return void 0;
|
|
164
|
+
let offset = startTag.endOffset - 1;
|
|
165
|
+
if (source[offset] !== ">") return void 0;
|
|
166
|
+
while (offset > startTag.startOffset && /\s/.test(source[offset - 1] ?? "")) offset -= 1;
|
|
167
|
+
if (source[offset - 1] === "/") offset -= 1;
|
|
168
|
+
return offset;
|
|
169
|
+
}
|
|
170
|
+
function stylesheetRel(value = "") {
|
|
171
|
+
const tokens = value.toLowerCase().split(/\s+/);
|
|
172
|
+
return tokens.includes("stylesheet") && !tokens.includes("alternate");
|
|
173
|
+
}
|
|
174
|
+
function locatedAttribute(source, location, parsedValue) {
|
|
175
|
+
if (!location || parsedValue === void 0) return void 0;
|
|
176
|
+
const raw = source.slice(location.startOffset, location.endOffset);
|
|
177
|
+
const equals = raw.indexOf("=");
|
|
178
|
+
if (equals < 0) return void 0;
|
|
179
|
+
let start = equals + 1;
|
|
180
|
+
while (/\s/.test(raw[start] ?? "")) start += 1;
|
|
181
|
+
const quote = raw[start];
|
|
182
|
+
let end;
|
|
183
|
+
if (quote === "\"" || quote === "'") {
|
|
184
|
+
start += 1;
|
|
185
|
+
end = raw.lastIndexOf(quote);
|
|
186
|
+
if (end < start) return void 0;
|
|
187
|
+
} else {
|
|
188
|
+
end = start;
|
|
189
|
+
while (end < raw.length && !/\s/.test(raw[end] ?? "")) end += 1;
|
|
190
|
+
}
|
|
191
|
+
const value = raw.slice(start, end);
|
|
192
|
+
return {
|
|
193
|
+
value: value.includes("&") ? parsedValue : value,
|
|
194
|
+
writable: !value.includes("&"),
|
|
195
|
+
start: location.startOffset + start,
|
|
196
|
+
end: location.startOffset + end
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/native.ts
|
|
201
|
+
const require = createRequire(import.meta.url);
|
|
202
|
+
const target = {
|
|
203
|
+
"darwin-arm64": "darwin-arm64",
|
|
204
|
+
"darwin-x64": "darwin-x64",
|
|
205
|
+
"linux-arm64": "linux-arm64-gnu",
|
|
206
|
+
"linux-x64": "linux-x64-gnu",
|
|
207
|
+
"win32-x64": "win32-x64-msvc"
|
|
208
|
+
}[`${process.platform}-${process.arch}`];
|
|
209
|
+
if (!target) throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
210
|
+
let binding;
|
|
211
|
+
try {
|
|
212
|
+
binding = require(`../tw-migrate.${target}.node`);
|
|
213
|
+
} catch (localError) {
|
|
214
|
+
if (errorCode(localError) !== "MODULE_NOT_FOUND") throw localError;
|
|
215
|
+
try {
|
|
216
|
+
binding = require(`tw-migrate-${target}`);
|
|
217
|
+
} catch (packageError) {
|
|
218
|
+
if (errorCode(packageError) !== "MODULE_NOT_FOUND") throw packageError;
|
|
219
|
+
throw new Error(`No tw-migrate native addon was found for ${target}. Reinstall the package or build it locally.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const { decodeSourceMap, planBatchMigration, staticImportBindings, staticImports, staticStringExpression, validateCss } = binding;
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/parser/style-compiler.ts
|
|
225
|
+
const SASS_EXTENSIONS = /* @__PURE__ */ new Set([".scss", ".sass"]);
|
|
226
|
+
function isSassPath(path) {
|
|
227
|
+
return SASS_EXTENSIONS.has(extname(path));
|
|
228
|
+
}
|
|
229
|
+
function isPreprocessorPath(path) {
|
|
230
|
+
return isSassPath(path) || extname(path) === ".less";
|
|
231
|
+
}
|
|
232
|
+
async function loadProjectModule(packageRoot, name, errorMessage) {
|
|
233
|
+
const projectRequire = createRequire(join(packageRoot, "package.json"));
|
|
234
|
+
let modulePath;
|
|
235
|
+
try {
|
|
236
|
+
modulePath = projectRequire.resolve(name);
|
|
237
|
+
} catch {
|
|
238
|
+
throw new Error(errorMessage);
|
|
239
|
+
}
|
|
240
|
+
const imported = await import(pathToFileURL(modulePath).href);
|
|
241
|
+
return imported.default ?? imported;
|
|
242
|
+
}
|
|
243
|
+
function loadProjectSass(packageRoot) {
|
|
244
|
+
return loadProjectModule(packageRoot, "sass", "Sass must be installed in the target project.");
|
|
245
|
+
}
|
|
246
|
+
function loadProjectLess(packageRoot) {
|
|
247
|
+
return loadProjectModule(packageRoot, "less", "Less must be installed in the target project.");
|
|
248
|
+
}
|
|
249
|
+
async function compileStyleEntry(compilers, packageRoot, entryPath, source, options = {}) {
|
|
250
|
+
if (isSassPath(entryPath)) return compileSassEntry(compilers.sass ??= await loadProjectSass(packageRoot), entryPath, source, options);
|
|
251
|
+
return compileLessEntry(compilers.less ??= await loadProjectLess(packageRoot), entryPath, source);
|
|
252
|
+
}
|
|
253
|
+
async function compileSassEntry(sass, entryPath, source, { virtualEntry = false } = {}) {
|
|
254
|
+
const result = await sass.compileStringAsync(source, {
|
|
255
|
+
sourceMap: true,
|
|
256
|
+
sourceMapIncludeSources: true,
|
|
257
|
+
url: pathToFileURL(entryPath),
|
|
258
|
+
syntax: extname(entryPath) === ".sass" ? "indented" : "scss"
|
|
259
|
+
});
|
|
260
|
+
if (!result.sourceMap) throw new Error(`Sass did not produce a source map for ${entryPath}`);
|
|
261
|
+
const loadedPaths = result.loadedUrls.filter((url) => url.protocol === "file:").map((url) => fileURLToPath(url));
|
|
262
|
+
const mappings = sourceMappings(result.sourceMap);
|
|
263
|
+
const normalizedPaths = await normalizeEntryPaths([...loadedPaths, ...mappings.map((mapping) => mapping.sourcePath)], entryPath, virtualEntry);
|
|
264
|
+
return {
|
|
265
|
+
css: result.css,
|
|
266
|
+
loadedPaths: loadedPaths.map((path) => normalizedPaths.get(path) ?? path),
|
|
267
|
+
sourceMappings: mappings.map((mapping) => ({
|
|
268
|
+
...mapping,
|
|
269
|
+
sourcePath: normalizedPaths.get(mapping.sourcePath) ?? mapping.sourcePath
|
|
270
|
+
}))
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
async function normalizeEntryPaths(paths, entryPath, virtualEntry) {
|
|
274
|
+
const canonicalEntryPath = virtualEntry ? entryPath : await realpath(entryPath);
|
|
275
|
+
return new Map(await Promise.all([...new Set(paths)].map(async (path) => {
|
|
276
|
+
if (path === entryPath) return [path, path];
|
|
277
|
+
try {
|
|
278
|
+
return [path, await realpath(path) === canonicalEntryPath ? entryPath : path];
|
|
279
|
+
} catch {
|
|
280
|
+
return [path, path];
|
|
281
|
+
}
|
|
282
|
+
})));
|
|
283
|
+
}
|
|
284
|
+
async function compileLessEntry(less, entryPath, source) {
|
|
285
|
+
const result = await less.render(source, {
|
|
286
|
+
filename: entryPath,
|
|
287
|
+
sourceMap: {
|
|
288
|
+
outputSourceFiles: true,
|
|
289
|
+
sourceMapBasepath: dirname(entryPath),
|
|
290
|
+
sourceMapOutputFilename: `${basename(entryPath, ".less")}.css`
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
css: result.css,
|
|
295
|
+
loadedPaths: result.imports.map((path) => isAbsolute(path) ? path : resolve(dirname(entryPath), path)),
|
|
296
|
+
sourceMappings: result.map ? sourceMappings(JSON.parse(result.map), dirname(entryPath)) : []
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function sourceMappings(sourceMap, sourceBase) {
|
|
300
|
+
return JSON.parse(decodeSourceMap(JSON.stringify(sourceMap))).flatMap(({ source, ...mapping }) => {
|
|
301
|
+
const sourcePath = sourcePathFromMap(sourceReferenceFromMap(source, sourceMap.sourceRoot), sourceBase);
|
|
302
|
+
return sourcePath ? [{
|
|
303
|
+
...mapping,
|
|
304
|
+
sourcePath
|
|
305
|
+
}] : [];
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function sourceReferenceFromMap(source, sourceRoot) {
|
|
309
|
+
if (!sourceRoot) return source;
|
|
310
|
+
if (isAbsolute(sourceRoot)) return resolve(sourceRoot, source);
|
|
311
|
+
try {
|
|
312
|
+
const root = new URL(sourceRoot);
|
|
313
|
+
root.pathname = `${root.pathname.replace(/\/+$/, "")}/`;
|
|
314
|
+
return new URL(source, root).href;
|
|
315
|
+
} catch {
|
|
316
|
+
return join(sourceRoot, source);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function sourcePathFromMap(source, sourceBase) {
|
|
320
|
+
if (isAbsolute(source)) return resolve(source);
|
|
321
|
+
try {
|
|
322
|
+
const url = new URL(source);
|
|
323
|
+
return url.protocol === "file:" ? resolve(fileURLToPath(url)) : void 0;
|
|
324
|
+
} catch {
|
|
325
|
+
return sourceBase ? resolve(sourceBase, source) : void 0;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/util/shared.ts
|
|
330
|
+
const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
331
|
+
".html",
|
|
332
|
+
".vue",
|
|
333
|
+
".js",
|
|
334
|
+
".jsx",
|
|
335
|
+
".mjs",
|
|
336
|
+
".cjs",
|
|
337
|
+
".ts",
|
|
338
|
+
".tsx",
|
|
339
|
+
".mts",
|
|
340
|
+
".cts"
|
|
341
|
+
]);
|
|
342
|
+
const STYLESHEET_SYNTAX = /* @__PURE__ */ new Map([
|
|
343
|
+
[".css", "css"],
|
|
344
|
+
[".scss", "scss"],
|
|
345
|
+
[".sass", "sass"],
|
|
346
|
+
[".less", "less"]
|
|
347
|
+
]);
|
|
348
|
+
const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
349
|
+
".git",
|
|
350
|
+
".next",
|
|
351
|
+
"build",
|
|
352
|
+
"dist",
|
|
353
|
+
"node_modules"
|
|
354
|
+
]);
|
|
355
|
+
const RECOVERABLE_INPUT_ERROR = "TW_MIGRATE_RECOVERABLE_INPUT:";
|
|
356
|
+
function errorCode(error) {
|
|
357
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
358
|
+
}
|
|
359
|
+
function isWithin(root, path) {
|
|
360
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
361
|
+
}
|
|
362
|
+
function isStylesheetPath(path) {
|
|
363
|
+
return STYLESHEET_SYNTAX.has(extname(path));
|
|
364
|
+
}
|
|
365
|
+
function stylesheetSyntax(path) {
|
|
366
|
+
return STYLESHEET_SYNTAX.get(extname(path));
|
|
367
|
+
}
|
|
368
|
+
function isStylesheetModule(path) {
|
|
369
|
+
const syntax = stylesheetSyntax(path);
|
|
370
|
+
return syntax !== void 0 && path.endsWith(`.module.${syntax}`);
|
|
371
|
+
}
|
|
372
|
+
function sourceReferencesStyle(file, stylePath) {
|
|
373
|
+
let importPath = normalizedRelativePath(dirname(file.path), stylePath);
|
|
374
|
+
if (!importPath.startsWith(".")) importPath = `./${importPath}`;
|
|
375
|
+
return [
|
|
376
|
+
`'${importPath}'`,
|
|
377
|
+
`"${importPath}"`,
|
|
378
|
+
`\`${importPath}\``
|
|
379
|
+
].some((literal) => file.source.includes(literal));
|
|
380
|
+
}
|
|
381
|
+
function isProjectInput(workspaceRoot, path) {
|
|
382
|
+
return isWithin(workspaceRoot, path) && !relative(workspaceRoot, path).split(/[\\/]/).includes("node_modules");
|
|
383
|
+
}
|
|
384
|
+
async function rejectSymlinkTarget(path, root) {
|
|
385
|
+
for (let current = path; isWithin(root, current); current = dirname(current)) {
|
|
386
|
+
if ((await lstat(current)).isSymbolicLink()) throw new Error(`Refusing to migrate a symbolic-link target: ${path}`);
|
|
387
|
+
if (current === root) break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function snapshotFile(snapshots, path) {
|
|
391
|
+
return recordSnapshot(snapshots, path, await readFile(path, "utf8"));
|
|
392
|
+
}
|
|
393
|
+
async function snapshotLoadedSource(snapshots, path) {
|
|
394
|
+
try {
|
|
395
|
+
return await snapshotFile(snapshots, path);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (errorCode(error) === "ENOENT") throw new Error(`Source changed during planning: ${path}`);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function recordSnapshot(snapshots, path, source) {
|
|
402
|
+
if (snapshots.has(path) && snapshots.get(path) !== source) throw new Error(`Source changed during planning: ${path}`);
|
|
403
|
+
snapshots.set(path, source);
|
|
404
|
+
return source;
|
|
405
|
+
}
|
|
406
|
+
function packageFailure(workspaceRoot, packageRoot, error) {
|
|
407
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
408
|
+
return {
|
|
409
|
+
package: normalizedRelativePath(workspaceRoot, packageRoot) || ".",
|
|
410
|
+
message: message.startsWith("TW_MIGRATE_RECOVERABLE_INPUT:") ? message.slice(29) : message
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function isRecoverablePlanningError(error) {
|
|
414
|
+
return (error instanceof Error ? error.message : String(error)).startsWith(RECOVERABLE_INPUT_ERROR);
|
|
415
|
+
}
|
|
416
|
+
function isMissingStyleCompilerError(error) {
|
|
417
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
+
return /^(?:Sass|Less) must be installed in the target project\.$/.test(message);
|
|
419
|
+
}
|
|
420
|
+
function isIntegrityError(error) {
|
|
421
|
+
return (error instanceof Error ? error.message : String(error)).startsWith("Source changed during planning:");
|
|
422
|
+
}
|
|
423
|
+
function normalizedRelativePath(root, path) {
|
|
424
|
+
return relative(root, path).split(sep).join("/");
|
|
425
|
+
}
|
|
426
|
+
function maskCssComments(source) {
|
|
427
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, " "));
|
|
428
|
+
}
|
|
429
|
+
function indexStylesheetDependents(styleSources) {
|
|
430
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const [path, rawSource] of styleSources) {
|
|
432
|
+
const source = maskCssComments(rawSource);
|
|
433
|
+
const references = [
|
|
434
|
+
...[...source.matchAll(/composes\s*:[^;{}]*?\bfrom\s+["']([^"']+)["']/g)].map((match) => match[1]),
|
|
435
|
+
...[...source.matchAll(/@(?:use|forward)\s+["']([^"']+)["']/g)].map((match) => match[1]),
|
|
436
|
+
...[...source.matchAll(/@import\s+(?:\([^)]*\)\s*)?(?:([^;{}]+);|([^;{}\r\n]+))/g)].flatMap((statement) => [...(statement[1] ?? statement[2]).matchAll(/["']([^"']+)["']/g)].map((match) => match[1])),
|
|
437
|
+
...[...source.matchAll(/@import\s+url\(\s*([^"'()\s]+)\s*\)/g)].map((match) => match[1])
|
|
438
|
+
];
|
|
439
|
+
for (const reference of new Set(references)) for (const target of stylesheetReferenceTargets(path, reference, styleSources)) {
|
|
440
|
+
if (target === path || !isStylesheetModule(target) && !isPreprocessorPath(target)) continue;
|
|
441
|
+
const paths = dependents.get(target) ?? [];
|
|
442
|
+
paths.push(path);
|
|
443
|
+
dependents.set(target, paths);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const [target, paths] of dependents) dependents.set(target, [...new Set(paths)].sort());
|
|
447
|
+
return dependents;
|
|
448
|
+
}
|
|
449
|
+
function addStyleDependent(styleDependents, target, importer) {
|
|
450
|
+
const paths = styleDependents.get(target) ?? [];
|
|
451
|
+
if (paths.includes(importer)) return;
|
|
452
|
+
paths.push(importer);
|
|
453
|
+
paths.sort();
|
|
454
|
+
styleDependents.set(target, paths);
|
|
455
|
+
}
|
|
456
|
+
function stylesheetReferenceTargets(importer, reference, styleSources) {
|
|
457
|
+
const target = resolve(dirname(importer), reference);
|
|
458
|
+
return (STYLESHEET_SYNTAX.has(extname(target)) ? [target] : [...STYLESHEET_SYNTAX.keys()].flatMap((syntax) => [
|
|
459
|
+
`${target}${syntax}`,
|
|
460
|
+
join(dirname(target), `_${basename(target)}${syntax}`),
|
|
461
|
+
join(target, `_index${syntax}`),
|
|
462
|
+
join(target, `index${syntax}`)
|
|
463
|
+
])).filter((path) => styleSources.has(path));
|
|
464
|
+
}
|
|
465
|
+
function cssImports(source) {
|
|
466
|
+
const imports = [];
|
|
467
|
+
const masked = maskCssComments(source);
|
|
468
|
+
let depth = 0;
|
|
469
|
+
let quote;
|
|
470
|
+
let importsAllowed = true;
|
|
471
|
+
for (let index = 0; index < masked.length; index += 1) {
|
|
472
|
+
const character = masked[index];
|
|
473
|
+
if (quote) {
|
|
474
|
+
if (character === "\\") index += 1;
|
|
475
|
+
else if (character === quote) quote = void 0;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (character === "\"" || character === "'") {
|
|
479
|
+
quote = character;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (character === "{") {
|
|
483
|
+
depth += 1;
|
|
484
|
+
importsAllowed = false;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (character === "}") {
|
|
488
|
+
depth = Math.max(0, depth - 1);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (!importsAllowed || depth !== 0 || masked.slice(index, index + 7).toLowerCase() !== "@import" || /[-\w]/.test(masked[index + 7] ?? "")) continue;
|
|
492
|
+
let end = index + 7;
|
|
493
|
+
let importQuote;
|
|
494
|
+
let parentheses = 0;
|
|
495
|
+
for (; end < masked.length; end += 1) {
|
|
496
|
+
const next = masked[end];
|
|
497
|
+
if (importQuote) {
|
|
498
|
+
if (next === "\\") end += 1;
|
|
499
|
+
else if (next === importQuote) importQuote = void 0;
|
|
500
|
+
} else if (next === "\"" || next === "'") importQuote = next;
|
|
501
|
+
else if (next === "(") parentheses += 1;
|
|
502
|
+
else if (next === ")") parentheses = Math.max(0, parentheses - 1);
|
|
503
|
+
else if (next === ";" && parentheses === 0) break;
|
|
504
|
+
}
|
|
505
|
+
if (end >= masked.length) continue;
|
|
506
|
+
const statement = masked.slice(index, end + 1);
|
|
507
|
+
const match = /^@import\s+(?:url\(\s*)?(?:["']([^"']+)["']|([^"'()\s;]+))\s*\)?\s*([^;]*);$/i.exec(statement);
|
|
508
|
+
if (match) imports.push({
|
|
509
|
+
href: match[1] ?? match[2],
|
|
510
|
+
media: match[3].trim(),
|
|
511
|
+
start: index,
|
|
512
|
+
end: end + 1
|
|
513
|
+
});
|
|
514
|
+
index = end;
|
|
515
|
+
}
|
|
516
|
+
const offsets = utf8OffsetMap(source, imports.flatMap((imported) => [imported.start, imported.end]));
|
|
517
|
+
return imports.map((imported) => ({
|
|
518
|
+
...imported,
|
|
519
|
+
start: offsets.get(imported.start) ?? imported.start,
|
|
520
|
+
end: offsets.get(imported.end) ?? imported.end
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/discovery.ts
|
|
525
|
+
const run = promisify(execFile);
|
|
526
|
+
async function resolveScope(options) {
|
|
527
|
+
const cwd = await realpath(resolve(options.cwd ?? process.cwd()));
|
|
528
|
+
const currentPackage = await findPackageRoot(cwd);
|
|
529
|
+
const gitRoot = await findGitRoot(currentPackage);
|
|
530
|
+
const workspaceRoot = gitRoot && !await isIgnoredByGit(gitRoot, currentPackage) ? gitRoot : currentPackage;
|
|
531
|
+
const allPaths = await discoverFiles(workspaceRoot, workspaceRoot === gitRoot);
|
|
532
|
+
const scannedPaths = workspaceRoot === gitRoot ? [.../* @__PURE__ */ new Set([...allPaths, ...await collectFiles(workspaceRoot, isRelevantDiscoveredFile)])] : [...allPaths];
|
|
533
|
+
const explicitStyle = options.styleFile ? resolve(cwd, options.styleFile) : void 0;
|
|
534
|
+
const configuredEntry = options.tailwindCss ? resolve(cwd, options.tailwindCss) : void 0;
|
|
535
|
+
if (explicitStyle && !isStylesheetPath(explicitStyle) && extname(explicitStyle) !== ".vue") throw new TypeError("Only .css, .scss, .sass, .less, and .vue files can be migrated");
|
|
536
|
+
if (configuredEntry && extname(configuredEntry) !== ".css") throw new TypeError("The Tailwind CSS entry must be a .css file");
|
|
537
|
+
if (configuredEntry && !(await stat(configuredEntry)).isFile()) throw new TypeError("The Tailwind CSS entry must be a file");
|
|
538
|
+
for (const path of [explicitStyle, configuredEntry]) if (path) await rejectSymlinkTarget(path, currentPackage);
|
|
539
|
+
if (explicitStyle && !isWithin(currentPackage, explicitStyle)) throw new TypeError("The selected stylesheet must belong to the current package");
|
|
540
|
+
for (const path of [explicitStyle, configuredEntry]) {
|
|
541
|
+
if (path && !allPaths.includes(path)) allPaths.push(path);
|
|
542
|
+
if (path && !scannedPaths.includes(path)) scannedPaths.push(path);
|
|
543
|
+
}
|
|
544
|
+
allPaths.sort();
|
|
545
|
+
scannedPaths.sort();
|
|
546
|
+
const targetable = new Set(allPaths);
|
|
547
|
+
const allPackageRoots = await discoverPackageRoots(workspaceRoot, allPaths);
|
|
548
|
+
if (!allPackageRoots.includes(currentPackage)) allPackageRoots.push(currentPackage);
|
|
549
|
+
for (const path of [explicitStyle, configuredEntry]) {
|
|
550
|
+
if (!path) continue;
|
|
551
|
+
const owner = await findPackageRoot(dirname(path));
|
|
552
|
+
if (!allPackageRoots.includes(owner)) allPackageRoots.push(owner);
|
|
553
|
+
}
|
|
554
|
+
allPackageRoots.sort();
|
|
555
|
+
const rootsByDepth = [...allPackageRoots].sort((left, right) => right.length - left.length);
|
|
556
|
+
const pathOwners = new Map(scannedPaths.map((path) => [path, rootsByDepth.find((root) => isWithin(root, path))]));
|
|
557
|
+
if (explicitStyle && pathOwners.get(explicitStyle) !== currentPackage) throw new TypeError("The selected stylesheet must belong to the current package");
|
|
558
|
+
if (configuredEntry && pathOwners.get(configuredEntry) !== currentPackage) throw new TypeError("The Tailwind CSS entry must belong to the current package");
|
|
559
|
+
return {
|
|
560
|
+
cwd,
|
|
561
|
+
workspaceRoot,
|
|
562
|
+
scannedPaths,
|
|
563
|
+
targetable,
|
|
564
|
+
explicitStyle,
|
|
565
|
+
configuredEntry,
|
|
566
|
+
pathOwners,
|
|
567
|
+
selectedPackages: options.workspaces ? allPackageRoots : [currentPackage]
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async function findPackageRoot(start) {
|
|
571
|
+
let directory = start;
|
|
572
|
+
while (true) {
|
|
573
|
+
try {
|
|
574
|
+
await readFile(join(directory, "package.json"), "utf8");
|
|
575
|
+
return directory;
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
578
|
+
}
|
|
579
|
+
const parent = dirname(directory);
|
|
580
|
+
if (parent === directory) throw new Error(`No package.json was found from ${start}`);
|
|
581
|
+
directory = parent;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function findGitRoot(cwd) {
|
|
585
|
+
try {
|
|
586
|
+
const { stdout } = await run("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
587
|
+
return resolve(stdout.trim());
|
|
588
|
+
} catch {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function isIgnoredByGit(gitRoot, path) {
|
|
593
|
+
if (gitRoot === path) return false;
|
|
594
|
+
try {
|
|
595
|
+
await run("git", [
|
|
596
|
+
"check-ignore",
|
|
597
|
+
"-q",
|
|
598
|
+
"--",
|
|
599
|
+
relative(gitRoot, path)
|
|
600
|
+
], { cwd: gitRoot });
|
|
601
|
+
return true;
|
|
602
|
+
} catch {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function discoverFiles(root, useGit) {
|
|
607
|
+
if (!useGit) return collectFiles(root, isRelevantDiscoveredFile);
|
|
608
|
+
const { stdout } = await run("git", [
|
|
609
|
+
"ls-files",
|
|
610
|
+
"-co",
|
|
611
|
+
"--exclude-standard",
|
|
612
|
+
"-z",
|
|
613
|
+
"--",
|
|
614
|
+
"."
|
|
615
|
+
], {
|
|
616
|
+
cwd: root,
|
|
617
|
+
maxBuffer: 64 * 1024 * 1024
|
|
618
|
+
});
|
|
619
|
+
const paths = stdout.split("\0").filter(Boolean).map((path) => resolve(root, path)).filter((path) => !hasIgnoredDirectory(root, path) && isRelevantDiscoveredFile(path));
|
|
620
|
+
return (await Promise.all(paths.map(async (path) => {
|
|
621
|
+
try {
|
|
622
|
+
return (await stat(path)).isFile() ? path : void 0;
|
|
623
|
+
} catch {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}))).flatMap((path) => path ? [path] : []).sort();
|
|
627
|
+
}
|
|
628
|
+
async function discoverPackageRoots(workspaceRoot, paths) {
|
|
629
|
+
const roots = paths.filter((path) => basename(path) === "package.json").map(dirname);
|
|
630
|
+
try {
|
|
631
|
+
await readFile(join(workspaceRoot, "package.json"), "utf8");
|
|
632
|
+
roots.push(workspaceRoot);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
635
|
+
}
|
|
636
|
+
return [...new Set(roots)].sort();
|
|
637
|
+
}
|
|
638
|
+
function hasIgnoredDirectory(root, path) {
|
|
639
|
+
return relative(root, path).split(/[\\/]/).some((part) => IGNORED_DIRECTORIES.has(part));
|
|
640
|
+
}
|
|
641
|
+
function isRelevantDiscoveredFile(path) {
|
|
642
|
+
return basename(path) === "package.json" || isStylesheetPath(path) || SOURCE_EXTENSIONS.has(extname(path));
|
|
643
|
+
}
|
|
644
|
+
async function collectFiles(root, include) {
|
|
645
|
+
const files = [];
|
|
646
|
+
async function visit(directory) {
|
|
647
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
648
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
649
|
+
for (const entry of entries) {
|
|
650
|
+
const path = join(directory, entry.name);
|
|
651
|
+
if (entry.isDirectory()) {
|
|
652
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(path);
|
|
653
|
+
} else if (entry.isFile() && include(path)) files.push(path);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
await visit(root);
|
|
657
|
+
return files;
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/plan/html.ts
|
|
661
|
+
async function preparePackageHtml({ packageRoot, sourceFiles, styleSources, snapshots, pathOwners, styleDependents }) {
|
|
662
|
+
const files = [];
|
|
663
|
+
const stylePaths = /* @__PURE__ */ new Set();
|
|
664
|
+
const generatedPaths = /* @__PURE__ */ new Set();
|
|
665
|
+
const removableLinks = [];
|
|
666
|
+
const warnings = [];
|
|
667
|
+
const htmlFiles = sourceFiles.filter((file) => extname(file.path) === ".html");
|
|
668
|
+
const orderedFiles = [...htmlFiles.filter((file) => pathOwners.get(file.path) === packageRoot), ...htmlFiles.filter((file) => pathOwners.get(file.path) !== packageRoot)];
|
|
669
|
+
for (const file of orderedFiles) {
|
|
670
|
+
const owner = pathOwners.get(file.path);
|
|
671
|
+
const foreign = owner !== packageRoot;
|
|
672
|
+
if (foreign && !owner) continue;
|
|
673
|
+
const referenceRoot = foreign ? owner : packageRoot;
|
|
674
|
+
const parsed = parseHtmlSource(file.path, file.source);
|
|
675
|
+
const contexts = [];
|
|
676
|
+
let linkBase = dirname(file.path);
|
|
677
|
+
const base = parsed.bases[0];
|
|
678
|
+
if (base) {
|
|
679
|
+
const baseReference = base.href.split(/[?#]/, 1)[0];
|
|
680
|
+
const basePath = base.writable && (baseReference === "" ? file.path : localHtmlReference(referenceRoot, dirname(file.path), base.href));
|
|
681
|
+
if (!basePath || !isWithin(referenceRoot, basePath)) {
|
|
682
|
+
if (!foreign) warnings.push(htmlWarning("unsupported-html-base", file.path, base.start, base.end, "A remote or unrepresentable base URL prevents safe stylesheet link resolution."));
|
|
683
|
+
linkBase = void 0;
|
|
684
|
+
} else linkBase = base.href.split(/[?#]/, 1)[0].endsWith("/") ? basePath : dirname(basePath);
|
|
685
|
+
}
|
|
686
|
+
for (const link of parsed.links) {
|
|
687
|
+
const linkedPath = linkBase && localHtmlReference(referenceRoot, linkBase, link.href);
|
|
688
|
+
if (!linkedPath || !isWithin(packageRoot, linkedPath) || foreign && pathOwners.get(linkedPath) !== packageRoot) {
|
|
689
|
+
if (!foreign) warnings.push(htmlWarning("unsupported-html-stylesheet-link", file.path, link.start, link.end, "Only local package stylesheet links are analyzed."));
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const variants = mediaVariants(link.media);
|
|
693
|
+
if (variants === void 0) {
|
|
694
|
+
const cssPath = (foreign ? void 0 : inferredPreprocessorPath({
|
|
695
|
+
path: linkedPath,
|
|
696
|
+
packageRoot,
|
|
697
|
+
styleSources,
|
|
698
|
+
pathOwners,
|
|
699
|
+
styleDependents
|
|
700
|
+
})) ?? linkedPath;
|
|
701
|
+
contexts.push({
|
|
702
|
+
cssPath,
|
|
703
|
+
variants: [],
|
|
704
|
+
direct: true,
|
|
705
|
+
analyzable: false
|
|
706
|
+
});
|
|
707
|
+
if (!foreign) warnings.push(htmlWarning("unsupported-link-media", file.path, link.start, link.end, `The stylesheet link media condition ${JSON.stringify(link.media)} cannot be represented safely.`));
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const contextStart = contexts.length;
|
|
711
|
+
await collectHtmlStyleContexts({
|
|
712
|
+
path: linkedPath,
|
|
713
|
+
variants,
|
|
714
|
+
direct: true,
|
|
715
|
+
packageRoot,
|
|
716
|
+
sourceFiles,
|
|
717
|
+
styleSources,
|
|
718
|
+
snapshots,
|
|
719
|
+
pathOwners,
|
|
720
|
+
styleDependents,
|
|
721
|
+
stylePaths,
|
|
722
|
+
generatedPaths,
|
|
723
|
+
contexts,
|
|
724
|
+
warnings,
|
|
725
|
+
visited: /* @__PURE__ */ new Set()
|
|
726
|
+
});
|
|
727
|
+
const directContext = contexts[contextStart];
|
|
728
|
+
if (directContext?.direct) removableLinks.push({
|
|
729
|
+
filePath: file.path,
|
|
730
|
+
cssPath: directContext.cssPath,
|
|
731
|
+
href: link.href,
|
|
732
|
+
media: link.media
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (foreign && contexts.length === 0) continue;
|
|
736
|
+
if (!foreign && contexts.length > 0) for (const attribute of parsed.dynamicAttributes) warnings.push(htmlWarning("dynamic-html-attribute", file.path, attribute.start, attribute.end, "This HTML attribute is not a safely writable quoted literal."));
|
|
737
|
+
files.push({
|
|
738
|
+
...file,
|
|
739
|
+
htmlElements: parsed.elements,
|
|
740
|
+
htmlStylesheets: deduplicateHtmlContexts(contexts),
|
|
741
|
+
htmlReferencesSafe: parsed.dynamicAttributes.length === 0,
|
|
742
|
+
htmlScriptText: parsed.scriptText
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
files,
|
|
747
|
+
stylePaths,
|
|
748
|
+
generatedPaths,
|
|
749
|
+
removableLinks,
|
|
750
|
+
warnings
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
async function collectHtmlStyleContexts(state) {
|
|
754
|
+
const key = `${state.path}\0${state.variants.join(":")}`;
|
|
755
|
+
if (state.visited.has(key)) return;
|
|
756
|
+
state.visited.add(key);
|
|
757
|
+
if (!isStylesheetPath(state.path)) return;
|
|
758
|
+
const owner = state.pathOwners.get(state.path);
|
|
759
|
+
if (owner && owner !== state.packageRoot) {
|
|
760
|
+
state.warnings.push(htmlWarning("cross-package-stylesheet-link", state.path, 0, 0, "A stylesheet owned by another package is not analyzed outside workspace mode."));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
let source;
|
|
764
|
+
try {
|
|
765
|
+
source = state.styleSources.get(state.path) ?? await snapshotFile(state.snapshots, state.path);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (errorCode(error) === "ENOENT") {
|
|
768
|
+
if (extname(state.path) === ".css") addInferredPreprocessorContext(state);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
if (!state.styleSources.has(state.path)) state.styleSources.set(state.path, source);
|
|
774
|
+
if (!owner) state.pathOwners.set(state.path, state.packageRoot);
|
|
775
|
+
if (extname(state.path) === ".css" && addInferredPreprocessorContext(state)) return;
|
|
776
|
+
state.stylePaths.add(state.path);
|
|
777
|
+
state.contexts.push({
|
|
778
|
+
cssPath: state.path,
|
|
779
|
+
variants: state.variants,
|
|
780
|
+
direct: state.direct,
|
|
781
|
+
analyzable: true
|
|
782
|
+
});
|
|
783
|
+
if (extname(state.path) !== ".css") return;
|
|
784
|
+
for (const imported of cssImports(source)) {
|
|
785
|
+
const variants = mediaVariants(imported.media);
|
|
786
|
+
if (variants === void 0) {
|
|
787
|
+
state.warnings.push(htmlWarning("unsupported-link-media", state.path, imported.start, imported.end, `The stylesheet import media condition ${JSON.stringify(imported.media)} cannot be represented safely.`));
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
const importedPath = localHtmlReference(state.packageRoot, dirname(state.path), imported.href);
|
|
791
|
+
if (!importedPath || !isWithin(state.packageRoot, importedPath)) continue;
|
|
792
|
+
if (importedPath !== state.path && (isStylesheetModule(importedPath) || isPreprocessorPath(importedPath))) addStyleDependent(state.styleDependents, importedPath, state.path);
|
|
793
|
+
await collectHtmlStyleContexts({
|
|
794
|
+
...state,
|
|
795
|
+
path: importedPath,
|
|
796
|
+
variants: [.../* @__PURE__ */ new Set([...state.variants, ...variants])],
|
|
797
|
+
direct: false
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function inferredPreprocessorPath(state) {
|
|
802
|
+
const stem = basename(state.path, ".css");
|
|
803
|
+
const matches = [...state.styleSources.keys()].filter((path) => isPreprocessorPath(path) && state.pathOwners.get(path) === state.packageRoot && !basename(path).startsWith("_") && !state.styleDependents.has(path) && basename(path, extname(path)) === stem);
|
|
804
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
805
|
+
}
|
|
806
|
+
function addInferredPreprocessorContext(state) {
|
|
807
|
+
if (state.styleSources.has(state.path) && state.sourceFiles.some((file) => extname(file.path) !== ".html" && sourceReferencesStyle(file, state.path))) return false;
|
|
808
|
+
const path = inferredPreprocessorPath(state);
|
|
809
|
+
if (!path) return false;
|
|
810
|
+
state.generatedPaths.add(state.path);
|
|
811
|
+
state.stylePaths.add(path);
|
|
812
|
+
state.contexts.push({
|
|
813
|
+
cssPath: path,
|
|
814
|
+
variants: state.variants,
|
|
815
|
+
direct: state.direct,
|
|
816
|
+
analyzable: true
|
|
817
|
+
});
|
|
818
|
+
state.warnings.push(htmlWarning("inferred-preprocessor-source", state.path, 0, 0, `The linked CSS was matched to the unique preprocessor filename ${basename(path)}.`));
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
function localHtmlReference(packageRoot, base, reference) {
|
|
822
|
+
const path = reference.split(/[?#]/, 1)[0];
|
|
823
|
+
if (!path || path.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(path)) return void 0;
|
|
824
|
+
let decoded;
|
|
825
|
+
try {
|
|
826
|
+
decoded = decodeURIComponent(path);
|
|
827
|
+
} catch {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
return decoded.startsWith("/") ? resolve(packageRoot, `.${decoded}`) : resolve(base, decoded);
|
|
831
|
+
}
|
|
832
|
+
function mediaVariants(media) {
|
|
833
|
+
const normalized = media.trim().toLowerCase();
|
|
834
|
+
if (!normalized || normalized === "all") return [];
|
|
835
|
+
if (normalized === "print") return ["print"];
|
|
836
|
+
}
|
|
837
|
+
function deduplicateHtmlContexts(contexts) {
|
|
838
|
+
const unique = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const context of contexts) {
|
|
840
|
+
const key = `${context.cssPath}\0${context.variants.join(":")}\0${context.analyzable}`;
|
|
841
|
+
const existing = unique.get(key);
|
|
842
|
+
if (existing) existing.direct ||= context.direct;
|
|
843
|
+
else unique.set(key, { ...context });
|
|
844
|
+
}
|
|
845
|
+
return [...unique.values()];
|
|
846
|
+
}
|
|
847
|
+
function htmlWarning(code, file, start, end, message) {
|
|
848
|
+
return {
|
|
849
|
+
code,
|
|
850
|
+
file,
|
|
851
|
+
start,
|
|
852
|
+
end,
|
|
853
|
+
message
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/tailwind.ts
|
|
858
|
+
function resolveTailwindEntry(stylePaths, styleSources, configuredPath) {
|
|
859
|
+
const entries = stylePaths.filter((path) => {
|
|
860
|
+
if (extname(path) !== ".css") return false;
|
|
861
|
+
const source = maskCssComments(styleSources.get(path) ?? "");
|
|
862
|
+
return /@import\s+["']tailwindcss(?:\/[^"']*)?["']/.test(source);
|
|
863
|
+
});
|
|
864
|
+
if (configuredPath) return {
|
|
865
|
+
path: configuredPath,
|
|
866
|
+
entries
|
|
867
|
+
};
|
|
868
|
+
if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
|
|
869
|
+
if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
|
|
870
|
+
return {
|
|
871
|
+
path: entries[0],
|
|
872
|
+
entries
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
|
|
876
|
+
const projectRequire = createRequire(join(packageRoot, "package.json"));
|
|
877
|
+
let packagePath;
|
|
878
|
+
try {
|
|
879
|
+
packagePath = projectRequire.resolve("tailwindcss/package.json");
|
|
880
|
+
} catch {
|
|
881
|
+
throw new Error("Tailwind v4 must be installed in the target project.");
|
|
882
|
+
}
|
|
883
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
884
|
+
if (!String(packageJson.version).startsWith("4.")) throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
|
|
885
|
+
const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
|
|
886
|
+
const css = await snapshotFile(snapshots, tailwindCss);
|
|
887
|
+
const base = dirname(tailwindCss);
|
|
888
|
+
const loadModule = createModuleLoader(snapshots, workspaceRoot);
|
|
889
|
+
const loadStylesheet = createStylesheetLoader(projectRequire, packagePath, snapshots, workspaceRoot);
|
|
890
|
+
const themeTokens = {
|
|
891
|
+
...extractThemeTokens(await readFile(join(dirname(packagePath), "theme.css"), "utf8")),
|
|
892
|
+
...await extractThemeTokensFromGraph(css, base, loadStylesheet)
|
|
893
|
+
};
|
|
894
|
+
return {
|
|
895
|
+
designSystem: await loadDesignSystem(css, {
|
|
896
|
+
base,
|
|
897
|
+
loadModule,
|
|
898
|
+
loadStylesheet
|
|
899
|
+
}),
|
|
900
|
+
css,
|
|
901
|
+
path: tailwindCss,
|
|
902
|
+
themeTokens
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function extractThemeTokens(css) {
|
|
906
|
+
const tokens = {};
|
|
907
|
+
for (const block of css.matchAll(/@theme[^{]*\{([^}]*)\}/gs)) for (const match of block[1].matchAll(/--([\w-]+):\s*([^;{}]+);/g)) tokens[match[1]] = match[2].trim();
|
|
908
|
+
return tokens;
|
|
909
|
+
}
|
|
910
|
+
async function extractThemeTokensFromGraph(css, base, loadStylesheet, seen = /* @__PURE__ */ new Set()) {
|
|
911
|
+
const tokens = {};
|
|
912
|
+
for (const match of css.matchAll(/@import\s+["']([^"']+)["']/g)) {
|
|
913
|
+
const key = `${base}\0${match[1]}`;
|
|
914
|
+
if (seen.has(key)) continue;
|
|
915
|
+
seen.add(key);
|
|
916
|
+
const loaded = await loadStylesheet(match[1], base);
|
|
917
|
+
Object.assign(tokens, await extractThemeTokensFromGraph(loaded.content, loaded.base, loadStylesheet, seen));
|
|
918
|
+
}
|
|
919
|
+
return Object.assign(tokens, extractThemeTokens(css));
|
|
920
|
+
}
|
|
921
|
+
function invalidCandidates(tailwind, candidates) {
|
|
922
|
+
const generated = tailwind.designSystem.candidatesToCss(candidates);
|
|
923
|
+
return candidates.filter((_, index) => generated[index] === null);
|
|
924
|
+
}
|
|
925
|
+
function createModuleLoader(snapshots, workspaceRoot) {
|
|
926
|
+
return async (id, base) => {
|
|
927
|
+
const path = createRequire(join(base, "package.json")).resolve(id);
|
|
928
|
+
if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
|
|
929
|
+
const imported = await import(pathToFileURL(path).href);
|
|
930
|
+
return {
|
|
931
|
+
path,
|
|
932
|
+
base: dirname(path),
|
|
933
|
+
module: imported.default ?? imported
|
|
934
|
+
};
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function createStylesheetLoader(projectRequire, tailwindPackagePath, snapshots, workspaceRoot) {
|
|
938
|
+
const tailwindRoot = dirname(tailwindPackagePath);
|
|
939
|
+
return async (id, base) => {
|
|
940
|
+
let path;
|
|
941
|
+
if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
|
|
942
|
+
else if (id.startsWith("tailwindcss/")) {
|
|
943
|
+
const subpath = id.slice(12);
|
|
944
|
+
path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
|
|
945
|
+
} else if (id.startsWith(".") || isAbsolute(id)) path = resolve(base, id);
|
|
946
|
+
else path = projectRequire.resolve(id);
|
|
947
|
+
return {
|
|
948
|
+
content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
|
|
949
|
+
base: dirname(path)
|
|
950
|
+
};
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region src/parser/vue.ts
|
|
955
|
+
const ESCAPE_SELECTOR = /(?:::v-|:)(?:deep|global|slotted)\(([^)]*)\)/g;
|
|
956
|
+
const ESCAPE_RESIDUE = /(?:>>>|\/deep\/|::v-deep|:deep|::v-slotted|:slotted|::v-global|:global)/;
|
|
957
|
+
const SUPPORTED_STYLE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
958
|
+
"lang",
|
|
959
|
+
"module",
|
|
960
|
+
"scoped",
|
|
961
|
+
"src"
|
|
962
|
+
]);
|
|
963
|
+
const SUPPORTED_STYLE_LANGUAGES = /* @__PURE__ */ new Set([
|
|
964
|
+
void 0,
|
|
965
|
+
"css",
|
|
966
|
+
"scss",
|
|
967
|
+
"sass",
|
|
968
|
+
"less"
|
|
969
|
+
]);
|
|
970
|
+
const NODE_ELEMENT = 1;
|
|
971
|
+
const NODE_TEXT = 2;
|
|
972
|
+
const NODE_INTERPOLATION = 5;
|
|
973
|
+
const PROP_ATTRIBUTE = 6;
|
|
974
|
+
const PROP_DIRECTIVE = 7;
|
|
975
|
+
const TAG_ELEMENT = 0;
|
|
976
|
+
const TAG_COMPONENT = 1;
|
|
977
|
+
const TAG_SLOT = 2;
|
|
978
|
+
async function loadProjectVueCompiler(packageRoot) {
|
|
979
|
+
const projectRequire = createRequire(join(packageRoot, "package.json"));
|
|
980
|
+
let packagePath;
|
|
981
|
+
try {
|
|
982
|
+
packagePath = projectRequire.resolve("vue/package.json");
|
|
983
|
+
} catch {
|
|
984
|
+
throw new Error("Vue 3 with compiler-sfc must be installed in the target project.");
|
|
985
|
+
}
|
|
986
|
+
const { version } = JSON.parse(await readFile(packagePath, "utf8"));
|
|
987
|
+
if (!String(version).startsWith("3.")) return { unsupportedVersion: String(version) };
|
|
988
|
+
return { compiler: await loadProjectModule(packageRoot, "vue/compiler-sfc", "Vue 3 with compiler-sfc must be installed in the target project.") };
|
|
989
|
+
}
|
|
990
|
+
function analyzeVueSource(compiler, path, source) {
|
|
991
|
+
const warnings = [];
|
|
992
|
+
const warn = (code, start, end, message) => warnings.push({
|
|
993
|
+
code,
|
|
994
|
+
file: path,
|
|
995
|
+
start,
|
|
996
|
+
end,
|
|
997
|
+
message
|
|
998
|
+
});
|
|
999
|
+
const { descriptor, errors } = compiler.parse(source, { filename: path });
|
|
1000
|
+
if (errors.length > 0) {
|
|
1001
|
+
const offset = errors[0]?.loc?.start?.offset ?? 0;
|
|
1002
|
+
warn("unsupported-sfc-block", offset, offset, `The SFC could not be parsed: ${errors[0].message}`);
|
|
1003
|
+
return {
|
|
1004
|
+
warnings: toByteWarnings(source, warnings),
|
|
1005
|
+
retained: true
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
const template = descriptor.template;
|
|
1009
|
+
if (!template || template.lang || template.src) {
|
|
1010
|
+
warn("unsupported-sfc-block", 0, 0, !template ? "The SFC has no template block, so its class usage cannot be analyzed." : "Only the default template language with inline content is analyzed.");
|
|
1011
|
+
return {
|
|
1012
|
+
warnings: toByteWarnings(source, warnings),
|
|
1013
|
+
retained: true
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
if (descriptor.customBlocks.length > 0) {
|
|
1017
|
+
const block = descriptor.customBlocks[0];
|
|
1018
|
+
warn("unsupported-sfc-block", block.loc.start.offset, block.loc.end.offset, `The custom <${block.type}> block is not analyzed.`);
|
|
1019
|
+
return {
|
|
1020
|
+
warnings: toByteWarnings(source, warnings),
|
|
1021
|
+
retained: true
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const blocks = [];
|
|
1025
|
+
const unscopedBlocks = [];
|
|
1026
|
+
const moduleBlocks = [];
|
|
1027
|
+
const styleBlockImports = [];
|
|
1028
|
+
const shadowCssTexts = [];
|
|
1029
|
+
const unscopedShadowCssTexts = [];
|
|
1030
|
+
const shadowModuleCssTexts = [];
|
|
1031
|
+
const shadowPreprocessorTexts = [];
|
|
1032
|
+
const unscopedShadowPreprocessorTexts = [];
|
|
1033
|
+
let escapeUnverifiable = false;
|
|
1034
|
+
let moduleSiblingUnsupported = false;
|
|
1035
|
+
for (const style of descriptor.styles) {
|
|
1036
|
+
const start = style.loc.start.offset;
|
|
1037
|
+
const end = style.loc.end.offset;
|
|
1038
|
+
const usesDefaultModuleBinding = style.module === true || style.module === "$style";
|
|
1039
|
+
const unsupportedAttributes = Object.keys(style.attrs).filter((attribute) => !SUPPORTED_STYLE_ATTRIBUTES.has(attribute));
|
|
1040
|
+
if (unsupportedAttributes.length > 0) {
|
|
1041
|
+
warn("unsupported-sfc-block", start, end, `Unsupported <style> attributes: ${unsupportedAttributes.join(", ")}.`);
|
|
1042
|
+
if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
|
|
1043
|
+
escapeUnverifiable = true;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (style.src !== void 0) {
|
|
1047
|
+
if (style.module !== void 0) {
|
|
1048
|
+
warn("unsupported-sfc-block", start, end, "A <style module src> block is not supported.");
|
|
1049
|
+
if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
|
|
1050
|
+
escapeUnverifiable = true;
|
|
1051
|
+
} else styleBlockImports.push({
|
|
1052
|
+
reference: style.src,
|
|
1053
|
+
start,
|
|
1054
|
+
end
|
|
1055
|
+
});
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (style.module !== void 0 && style.module !== true) {
|
|
1059
|
+
if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
|
|
1060
|
+
warn("unsupported-sfc-block", start, end, "Named <style module> blocks are not supported.");
|
|
1061
|
+
shadowModuleCssTexts.push(style.content);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (!SUPPORTED_STYLE_LANGUAGES.has(style.lang)) {
|
|
1065
|
+
if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
|
|
1066
|
+
warn("preprocessor-style-block", start, end, `The <style lang="${style.lang}"> language is not supported.`);
|
|
1067
|
+
shadowPreprocessorTexts.push(style.content);
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
const outerStart = source.lastIndexOf("<style", start);
|
|
1071
|
+
const closing = source.slice(end).match(/^<\/style\s*>/)?.[0];
|
|
1072
|
+
if (outerStart < 0 || !closing) {
|
|
1073
|
+
warn("unsupported-sfc-block", start, end, "The style block tags could not be located.");
|
|
1074
|
+
escapeUnverifiable = true;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const block = {
|
|
1078
|
+
outerStart,
|
|
1079
|
+
outerEnd: end + closing.length,
|
|
1080
|
+
contentStart: start,
|
|
1081
|
+
contentEnd: end,
|
|
1082
|
+
syntax: style.lang ?? "css",
|
|
1083
|
+
content: style.content
|
|
1084
|
+
};
|
|
1085
|
+
if (style.module === true) {
|
|
1086
|
+
moduleBlocks.push(block);
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (!style.scoped) {
|
|
1090
|
+
unscopedBlocks.push(block);
|
|
1091
|
+
if (style.lang && style.lang !== "css") unscopedShadowPreprocessorTexts.push(style.content);
|
|
1092
|
+
else unscopedShadowCssTexts.push(style.content);
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
for (const [, inner] of style.content.matchAll(ESCAPE_SELECTOR)) {
|
|
1096
|
+
shadowCssTexts.push(`${inner} {}`);
|
|
1097
|
+
if (inner.includes("(")) escapeUnverifiable = true;
|
|
1098
|
+
}
|
|
1099
|
+
if (ESCAPE_RESIDUE.test(style.content.replace(ESCAPE_SELECTOR, " "))) escapeUnverifiable = true;
|
|
1100
|
+
blocks.push(block);
|
|
1101
|
+
}
|
|
1102
|
+
const state = {
|
|
1103
|
+
elements: [],
|
|
1104
|
+
components: [],
|
|
1105
|
+
dynamic: false,
|
|
1106
|
+
vHtml: false,
|
|
1107
|
+
hasSlot: false,
|
|
1108
|
+
expressionTexts: []
|
|
1109
|
+
};
|
|
1110
|
+
visitTemplateNode(source, template.ast, state);
|
|
1111
|
+
const alwaysRenderedRoots = template.ast.children.filter((node) => node.type === NODE_ELEMENT && (node.tagType === TAG_ELEMENT || node.tagType === TAG_COMPONENT) && !node.props?.some((prop) => prop.type === PROP_DIRECTIVE && [
|
|
1112
|
+
"if",
|
|
1113
|
+
"else-if",
|
|
1114
|
+
"else",
|
|
1115
|
+
"for"
|
|
1116
|
+
].includes(prop.name))).length;
|
|
1117
|
+
const scriptText = [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean).join("\n");
|
|
1118
|
+
const fallthroughUnverifiable = Boolean(descriptor.script) || descriptor.scriptSetup?.src !== void 0 || /\b(?:defineOptions|defineProps|inheritAttrs)\b/.test(descriptor.scriptSetup?.content ?? "");
|
|
1119
|
+
let scriptImportsUnverifiable = false;
|
|
1120
|
+
const styleImports = [descriptor.script, descriptor.scriptSetup].flatMap((script) => {
|
|
1121
|
+
if (!script) return [];
|
|
1122
|
+
if (script.src || ![
|
|
1123
|
+
void 0,
|
|
1124
|
+
"js",
|
|
1125
|
+
"jsx",
|
|
1126
|
+
"ts",
|
|
1127
|
+
"tsx"
|
|
1128
|
+
].includes(script.lang)) {
|
|
1129
|
+
scriptImportsUnverifiable = true;
|
|
1130
|
+
return [];
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
return staticImports(`${path}.${script.lang ?? "js"}`, script.content);
|
|
1134
|
+
} catch {
|
|
1135
|
+
scriptImportsUnverifiable = true;
|
|
1136
|
+
return [];
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
let componentImports = [];
|
|
1140
|
+
if (descriptor.scriptSetup && !descriptor.scriptSetup.src && [
|
|
1141
|
+
void 0,
|
|
1142
|
+
"js",
|
|
1143
|
+
"jsx",
|
|
1144
|
+
"ts",
|
|
1145
|
+
"tsx"
|
|
1146
|
+
].includes(descriptor.scriptSetup.lang)) try {
|
|
1147
|
+
componentImports = staticImportBindings(`${path}.${descriptor.scriptSetup.lang ?? "js"}`, descriptor.scriptSetup.content);
|
|
1148
|
+
} catch {
|
|
1149
|
+
componentImports = [];
|
|
1150
|
+
}
|
|
1151
|
+
const offsets = utf8OffsetMap(source, [
|
|
1152
|
+
...warnings.flatMap((warning) => [warning.start, warning.end]),
|
|
1153
|
+
...[
|
|
1154
|
+
...blocks,
|
|
1155
|
+
...unscopedBlocks,
|
|
1156
|
+
...moduleBlocks
|
|
1157
|
+
].flatMap((block) => [
|
|
1158
|
+
block.outerStart,
|
|
1159
|
+
block.outerEnd,
|
|
1160
|
+
block.contentStart,
|
|
1161
|
+
block.contentEnd
|
|
1162
|
+
]),
|
|
1163
|
+
...styleBlockImports.flatMap((entry) => [entry.start, entry.end]),
|
|
1164
|
+
...[...state.elements, ...state.components].flatMap((element) => [
|
|
1165
|
+
element.nodeStart,
|
|
1166
|
+
element.classAttribute,
|
|
1167
|
+
element.idAttribute,
|
|
1168
|
+
element.moduleBinding
|
|
1169
|
+
].flatMap((value) => value === void 0 ? [] : typeof value === "number" ? [value] : [value.start, value.end]))
|
|
1170
|
+
]);
|
|
1171
|
+
const offset = offsetLookup(offsets);
|
|
1172
|
+
const attribute = (value) => value && {
|
|
1173
|
+
...value,
|
|
1174
|
+
start: offset(value.start),
|
|
1175
|
+
end: offset(value.end)
|
|
1176
|
+
};
|
|
1177
|
+
const toByteBlock = (block) => ({
|
|
1178
|
+
...block,
|
|
1179
|
+
outerStart: offset(block.outerStart),
|
|
1180
|
+
outerEnd: offset(block.outerEnd),
|
|
1181
|
+
contentStart: offset(block.contentStart),
|
|
1182
|
+
contentEnd: offset(block.contentEnd)
|
|
1183
|
+
});
|
|
1184
|
+
const toByteSite = (element) => ({
|
|
1185
|
+
tag: element.tag,
|
|
1186
|
+
nodeStart: offset(element.nodeStart),
|
|
1187
|
+
classAttribute: attribute(element.classAttribute),
|
|
1188
|
+
idAttribute: attribute(element.idAttribute),
|
|
1189
|
+
matchClasses: element.matchClasses,
|
|
1190
|
+
moduleBinding: element.moduleBinding && {
|
|
1191
|
+
name: element.moduleBinding.name,
|
|
1192
|
+
start: offset(element.moduleBinding.start),
|
|
1193
|
+
end: offset(element.moduleBinding.end)
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
return {
|
|
1197
|
+
warnings: warnings.map((warning) => ({
|
|
1198
|
+
...warning,
|
|
1199
|
+
start: offset(warning.start),
|
|
1200
|
+
end: offset(warning.end)
|
|
1201
|
+
})),
|
|
1202
|
+
blocks: blocks.map(toByteBlock),
|
|
1203
|
+
unscopedBlocks: unscopedBlocks.map(toByteBlock),
|
|
1204
|
+
moduleBlocks: moduleBlocks.map(toByteBlock),
|
|
1205
|
+
moduleClosureBroken: moduleSiblingUnsupported || scriptImportsUnverifiable || /\$style|useCssModule/.test([...state.expressionTexts, scriptText].join("\n")),
|
|
1206
|
+
htmlElements: state.elements.map(toByteSite),
|
|
1207
|
+
componentSites: state.components.map(toByteSite),
|
|
1208
|
+
rootStarts: template.ast.children.filter((node) => node.type === NODE_ELEMENT).map((node) => offsets.get(node.loc.start.offset)),
|
|
1209
|
+
rootVFor: template.ast.children.some((node) => node.type === NODE_ELEMENT && node.props?.some((prop) => prop.type === PROP_DIRECTIVE && prop.name === "for")),
|
|
1210
|
+
rootFragment: template.ast.children.filter((node) => node.type === NODE_ELEMENT || node.type === NODE_INTERPOLATION || node.type === NODE_TEXT && node.content?.trim?.() !== "").length !== 1,
|
|
1211
|
+
scriptText,
|
|
1212
|
+
scriptStyleImports: [...new Set(styleImports)],
|
|
1213
|
+
scriptImportsUnverifiable,
|
|
1214
|
+
styleBlockImports: styleBlockImports.map((entry) => ({
|
|
1215
|
+
...entry,
|
|
1216
|
+
start: offset(entry.start),
|
|
1217
|
+
end: offset(entry.end)
|
|
1218
|
+
})),
|
|
1219
|
+
componentImports,
|
|
1220
|
+
fallthroughUnverifiable,
|
|
1221
|
+
dynamic: state.dynamic,
|
|
1222
|
+
vHtml: state.vHtml,
|
|
1223
|
+
hasSlot: state.hasSlot,
|
|
1224
|
+
alwaysRenderedRoots,
|
|
1225
|
+
shadowCssTexts,
|
|
1226
|
+
unscopedShadowCssTexts,
|
|
1227
|
+
shadowModuleCssTexts,
|
|
1228
|
+
shadowPreprocessorTexts,
|
|
1229
|
+
unscopedShadowPreprocessorTexts,
|
|
1230
|
+
escapeUnverifiable,
|
|
1231
|
+
retained: false
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function verifyVueSource(compiler, path, source, includeUnscoped = false) {
|
|
1235
|
+
const { descriptor, errors } = compiler.parse(source, { filename: path });
|
|
1236
|
+
if (errors.length > 0) throw new Error(`Edited SFC no longer parses: ${path}: ${errors[0].message}`);
|
|
1237
|
+
return descriptor.styles.filter((style) => style.src === void 0 && (style.module === void 0 || style.module === true) && (style.scoped || style.module === true || includeUnscoped) && SUPPORTED_STYLE_LANGUAGES.has(style.lang)).map((style) => ({
|
|
1238
|
+
content: style.content,
|
|
1239
|
+
syntax: style.lang ?? "css"
|
|
1240
|
+
}));
|
|
1241
|
+
}
|
|
1242
|
+
function visitTemplateNode(source, node, state) {
|
|
1243
|
+
if (node.type === NODE_ELEMENT) {
|
|
1244
|
+
if (node.tagType === TAG_SLOT) state.hasSlot = true;
|
|
1245
|
+
const bindingClasses = [];
|
|
1246
|
+
let classOpaque = false;
|
|
1247
|
+
let moduleBinding;
|
|
1248
|
+
for (const prop of node.props ?? []) {
|
|
1249
|
+
if (prop.type !== PROP_DIRECTIVE) continue;
|
|
1250
|
+
let provenModuleExpression = false;
|
|
1251
|
+
if (prop.name === "bind") {
|
|
1252
|
+
if (!prop.arg || !prop.arg.isStatic) classOpaque = true;
|
|
1253
|
+
else if (prop.arg.content === "class") {
|
|
1254
|
+
const member = node.tagType === TAG_ELEMENT && prop.exp ? /^\s*\$style\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(prop.exp.content) : void 0;
|
|
1255
|
+
if (member && !moduleBinding) {
|
|
1256
|
+
moduleBinding = {
|
|
1257
|
+
name: member[1],
|
|
1258
|
+
start: attributeRemovalStart(source, node, prop),
|
|
1259
|
+
end: prop.loc.end.offset
|
|
1260
|
+
};
|
|
1261
|
+
provenModuleExpression = true;
|
|
1262
|
+
} else {
|
|
1263
|
+
const value = staticClassBinding(prop);
|
|
1264
|
+
if (value === void 0) classOpaque = true;
|
|
1265
|
+
else bindingClasses.push(...value.split(/[\t\n\f\r ]+/).filter(Boolean));
|
|
1266
|
+
}
|
|
1267
|
+
} else if (prop.arg.content === "id") classOpaque = true;
|
|
1268
|
+
}
|
|
1269
|
+
if (!provenModuleExpression && prop.exp?.content) state.expressionTexts.push(prop.exp.content);
|
|
1270
|
+
if (prop.arg && !prop.arg.isStatic && prop.arg.content) state.expressionTexts.push(prop.arg.content);
|
|
1271
|
+
if (prop.name === "html") state.vHtml = true;
|
|
1272
|
+
}
|
|
1273
|
+
if (classOpaque) state.dynamic = true;
|
|
1274
|
+
const site = templateSite(source, node, bindingClasses, classOpaque, state);
|
|
1275
|
+
const element = {
|
|
1276
|
+
tag: node.tag,
|
|
1277
|
+
nodeStart: node.loc.start.offset,
|
|
1278
|
+
moduleBinding,
|
|
1279
|
+
...site
|
|
1280
|
+
};
|
|
1281
|
+
if (node.tagType === TAG_COMPONENT) state.components.push(element);
|
|
1282
|
+
else if (node.tagType === TAG_ELEMENT) state.elements.push(element);
|
|
1283
|
+
}
|
|
1284
|
+
if (node.type === NODE_INTERPOLATION && node.content?.content) state.expressionTexts.push(node.content.content);
|
|
1285
|
+
for (const child of node.children ?? []) visitTemplateNode(source, child, state);
|
|
1286
|
+
}
|
|
1287
|
+
function attributeRemovalStart(source, node, prop) {
|
|
1288
|
+
const attributeStart = prop.loc.start.offset;
|
|
1289
|
+
let start = attributeStart;
|
|
1290
|
+
while (start > node.loc.start.offset + 1 && /\s/.test(source[start - 1])) start -= 1;
|
|
1291
|
+
return /[\r\n]/.test(source.slice(start, attributeStart)) ? attributeStart : start;
|
|
1292
|
+
}
|
|
1293
|
+
function staticClassBinding(prop) {
|
|
1294
|
+
if (!prop.exp) return void 0;
|
|
1295
|
+
try {
|
|
1296
|
+
return staticStringExpression("Component.js", prop.exp.content) ?? void 0;
|
|
1297
|
+
} catch {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function templateSite(source, node, bindingClasses, classOpaque, state) {
|
|
1302
|
+
let classAttribute = literalAttribute(source, node, "class");
|
|
1303
|
+
const idAttribute = literalAttribute(source, node, "id");
|
|
1304
|
+
const hasClassAttr = node.props?.some((prop) => prop.type === PROP_ATTRIBUTE && prop.name === "class");
|
|
1305
|
+
const hasIdAttr = node.props?.some((prop) => prop.type === PROP_ATTRIBUTE && prop.name === "id");
|
|
1306
|
+
if (hasClassAttr && !classAttribute || hasIdAttr && !idAttribute) {
|
|
1307
|
+
state.dynamic = true;
|
|
1308
|
+
return { idAttribute };
|
|
1309
|
+
}
|
|
1310
|
+
const matchClasses = bindingClasses.length ? [...classAttribute?.value.split(/[\t\n\f\r ]+/).filter(Boolean) ?? [], ...bindingClasses] : void 0;
|
|
1311
|
+
if (!classAttribute && (bindingClasses.length > 0 || !classOpaque && (node.tagType === TAG_COMPONENT || idAttribute))) {
|
|
1312
|
+
const insertion = classInsertionOffset(source, node);
|
|
1313
|
+
if (insertion === void 0) state.dynamic = true;
|
|
1314
|
+
else classAttribute = {
|
|
1315
|
+
value: "",
|
|
1316
|
+
start: insertion,
|
|
1317
|
+
end: insertion,
|
|
1318
|
+
synthetic: true
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
return {
|
|
1322
|
+
classAttribute,
|
|
1323
|
+
idAttribute,
|
|
1324
|
+
matchClasses
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function literalAttribute(source, node, name) {
|
|
1328
|
+
const prop = node.props?.find((prop) => prop.type === PROP_ATTRIBUTE && prop.name === name);
|
|
1329
|
+
if (!prop?.value) return void 0;
|
|
1330
|
+
const start = prop.value.loc.start.offset;
|
|
1331
|
+
const end = prop.value.loc.end.offset;
|
|
1332
|
+
const raw = source.slice(start, end);
|
|
1333
|
+
const quote = raw[0];
|
|
1334
|
+
if (quote !== "\"" && quote !== "'" || raw[raw.length - 1] !== quote) return void 0;
|
|
1335
|
+
const value = raw.slice(1, -1);
|
|
1336
|
+
if (value !== prop.value.content || value.includes("&")) return void 0;
|
|
1337
|
+
return {
|
|
1338
|
+
value,
|
|
1339
|
+
start: start + 1,
|
|
1340
|
+
end: end - 1
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function classInsertionOffset(source, node) {
|
|
1344
|
+
const propsEnd = Math.max(node.loc.start.offset + 1 + node.tag.length, ...(node.props ?? []).map((prop) => prop.loc.end.offset));
|
|
1345
|
+
const tagEnd = source.indexOf(">", propsEnd);
|
|
1346
|
+
if (tagEnd < 0) return void 0;
|
|
1347
|
+
let offset = tagEnd;
|
|
1348
|
+
if (source[offset - 1] === "/") offset -= 1;
|
|
1349
|
+
while (offset > node.loc.start.offset && /\s/.test(source[offset - 1])) offset -= 1;
|
|
1350
|
+
return offset;
|
|
1351
|
+
}
|
|
1352
|
+
function toByteWarnings(source, warnings) {
|
|
1353
|
+
const offsets = utf8OffsetMap(source, warnings.flatMap((warning) => [warning.start, warning.end]));
|
|
1354
|
+
return warnings.map((warning) => ({
|
|
1355
|
+
...warning,
|
|
1356
|
+
start: offsets.get(warning.start) ?? warning.start,
|
|
1357
|
+
end: offsets.get(warning.end) ?? warning.end
|
|
1358
|
+
}));
|
|
1359
|
+
}
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/plan/vue.ts
|
|
1362
|
+
function isLocalVueReference(reference) {
|
|
1363
|
+
return reference.startsWith(".") || reference.startsWith("#") || reference.includes("/") || reference.includes(".vue");
|
|
1364
|
+
}
|
|
1365
|
+
function couldResolveToVue(reference) {
|
|
1366
|
+
const path = reference.split(/[?#]/, 1)[0];
|
|
1367
|
+
return path.endsWith(".vue") || extname(path) === "";
|
|
1368
|
+
}
|
|
1369
|
+
function vueReferenceTarget(importer, reference, vuePaths) {
|
|
1370
|
+
if (!reference.startsWith(".")) return void 0;
|
|
1371
|
+
const target = resolve(dirname(importer), reference);
|
|
1372
|
+
return [
|
|
1373
|
+
target,
|
|
1374
|
+
`${target}.vue`,
|
|
1375
|
+
join(target, "index.vue")
|
|
1376
|
+
].find((path) => vuePaths.has(path));
|
|
1377
|
+
}
|
|
1378
|
+
function vueLiteralTargets(file, vuePaths) {
|
|
1379
|
+
return new Set([...file.source.matchAll(/(["'`])([^"'`\r\n]+)\1/g)].flatMap((match) => {
|
|
1380
|
+
if (/[?*[\]{}]/.test(match[2]) && (match[2].includes("vue") || isLocalVueReference(match[2]))) return [...vuePaths];
|
|
1381
|
+
const target = vueReferenceTarget(file.path, match[2], vuePaths);
|
|
1382
|
+
if (target) return [target];
|
|
1383
|
+
return match[2].includes(".vue") ? [...vuePaths] : [];
|
|
1384
|
+
}));
|
|
1385
|
+
}
|
|
1386
|
+
function normalizedVueTag(value) {
|
|
1387
|
+
return value.replaceAll("-", "").toLowerCase();
|
|
1388
|
+
}
|
|
1389
|
+
function buildVueComponentGraph(ownedVue, sourceFiles, analyses, { styleSources, pathOwners, packageRoot }) {
|
|
1390
|
+
const vuePaths = new Set(ownedVue.map((file) => file.path));
|
|
1391
|
+
const callers = new Map([...vuePaths].map((path) => [path, []]));
|
|
1392
|
+
const callerOpen = /* @__PURE__ */ new Set();
|
|
1393
|
+
let unresolvedStyleImport = false;
|
|
1394
|
+
const stylesheetReference = /\.(?:css|scss|sass|less)(?:$|[?#])/;
|
|
1395
|
+
for (const file of ownedVue) {
|
|
1396
|
+
const analysis = analyses.get(file.path);
|
|
1397
|
+
if (!analysis || analysis.retained) {
|
|
1398
|
+
for (const target of vuePaths) callerOpen.add(target);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const bindings = analysis.componentImports.flatMap((entry) => {
|
|
1402
|
+
const target = vueReferenceTarget(file.path, entry.source, vuePaths);
|
|
1403
|
+
return target ? [{
|
|
1404
|
+
...entry,
|
|
1405
|
+
target
|
|
1406
|
+
}] : [];
|
|
1407
|
+
});
|
|
1408
|
+
analysis.resolvedComponents = analysis.componentSites.flatMap((site) => {
|
|
1409
|
+
const matches = bindings.filter((entry) => normalizedVueTag(entry.local) === normalizedVueTag(site.tag));
|
|
1410
|
+
if (matches.length !== 1) return [];
|
|
1411
|
+
const edge = {
|
|
1412
|
+
parent: file.path,
|
|
1413
|
+
child: matches[0].target,
|
|
1414
|
+
site
|
|
1415
|
+
};
|
|
1416
|
+
callers.get(edge.child)?.push(edge);
|
|
1417
|
+
if (site.idAttribute || analysis.dynamic) {
|
|
1418
|
+
callerOpen.add(edge.child);
|
|
1419
|
+
const childAnalysis = analyses.get(edge.child);
|
|
1420
|
+
if (childAnalysis) childAnalysis.rootIdsOverridden = true;
|
|
1421
|
+
}
|
|
1422
|
+
if (analysis.alwaysRenderedRoots < 2 && analysis.rootStarts.includes(site.nodeStart)) callerOpen.add(edge.child);
|
|
1423
|
+
return [edge];
|
|
1424
|
+
});
|
|
1425
|
+
analysis.componentsOpen = analysis.resolvedComponents.length !== analysis.componentSites.length;
|
|
1426
|
+
analysis.setupImports = new Set(bindings.map((entry) => entry.target));
|
|
1427
|
+
if (analysis.componentsOpen) for (const target of vuePaths) callerOpen.add(target);
|
|
1428
|
+
}
|
|
1429
|
+
for (const file of sourceFiles) {
|
|
1430
|
+
let references;
|
|
1431
|
+
let blockReferences = [];
|
|
1432
|
+
if (extname(file.path) === ".vue") {
|
|
1433
|
+
const fileAnalysis = analyses.get(file.path);
|
|
1434
|
+
if (fileAnalysis && !fileAnalysis.retained) {
|
|
1435
|
+
references = fileAnalysis.scriptStyleImports;
|
|
1436
|
+
blockReferences = fileAnalysis.styleBlockImports.map((entry) => entry.reference);
|
|
1437
|
+
} else references = [];
|
|
1438
|
+
} else try {
|
|
1439
|
+
references = staticImports(file.path, file.source);
|
|
1440
|
+
} catch {
|
|
1441
|
+
references = [];
|
|
1442
|
+
}
|
|
1443
|
+
if (/\bimport\s*\(/.test(file.source) || references.some((reference) => !stylesheetReference.test(reference) && isLocalVueReference(reference) && couldResolveToVue(reference) && !vueReferenceTarget(file.path, reference, vuePaths))) for (const target of vuePaths) callerOpen.add(target);
|
|
1444
|
+
const resolvesOwned = (reference) => stylesheetReferenceTargets(file.path, reference, styleSources).some((path) => pathOwners.get(path) === packageRoot);
|
|
1445
|
+
if (pathOwners.get(file.path) === packageRoot && (references.some((reference) => stylesheetReference.test(reference) && (!reference.startsWith(".") || !resolvesOwned(reference))) || blockReferences.some((reference) => !resolvesOwned(reference)))) unresolvedStyleImport = true;
|
|
1446
|
+
const imported = new Set(references.flatMap((reference) => {
|
|
1447
|
+
const target = vueReferenceTarget(file.path, reference, vuePaths);
|
|
1448
|
+
return target ? [target] : [];
|
|
1449
|
+
}));
|
|
1450
|
+
for (const target of imported) if (extname(file.path) !== ".vue" || !analyses.get(file.path)?.setupImports?.has(target)) callerOpen.add(target);
|
|
1451
|
+
for (const target of vueLiteralTargets(file, vuePaths)) if (!imported.has(target)) callerOpen.add(target);
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
callers,
|
|
1455
|
+
callerOpen,
|
|
1456
|
+
unresolvedStyleImport
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
function vueWarningsOnlyResult(preparedVue) {
|
|
1460
|
+
if (preparedVue.warnings.length === 0) return {};
|
|
1461
|
+
return { plan: {
|
|
1462
|
+
files: [],
|
|
1463
|
+
deletedFiles: [],
|
|
1464
|
+
unlinkedFiles: [],
|
|
1465
|
+
candidates: [],
|
|
1466
|
+
rules: [],
|
|
1467
|
+
warnings: preparedVue.warnings,
|
|
1468
|
+
convertedRules: 0,
|
|
1469
|
+
retainedRules: 0
|
|
1470
|
+
} };
|
|
1471
|
+
}
|
|
1472
|
+
async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, styleSources, pathOwners, targetable, explicitStyle, snapshots, workspaceRoot, workspaces, scannedPaths }) {
|
|
1473
|
+
const none = {
|
|
1474
|
+
files: /* @__PURE__ */ new Map(),
|
|
1475
|
+
stylesheets: [],
|
|
1476
|
+
stylePaths: /* @__PURE__ */ new Set(),
|
|
1477
|
+
unscopedPaths: /* @__PURE__ */ new Set(),
|
|
1478
|
+
warnings: [],
|
|
1479
|
+
compiler: void 0
|
|
1480
|
+
};
|
|
1481
|
+
if (explicitStyle && extname(explicitStyle) !== ".vue") return none;
|
|
1482
|
+
const ownedVue = sourceFiles.filter((file) => extname(file.path) === ".vue" && pathOwners.get(file.path) === packageRoot);
|
|
1483
|
+
const selected = ownedVue.filter((file) => targetable.has(file.path) && (!explicitStyle || file.path === explicitStyle));
|
|
1484
|
+
if (selected.length === 0) return none;
|
|
1485
|
+
const loaded = await loadProjectVueCompiler(packageRoot);
|
|
1486
|
+
const warnings = [];
|
|
1487
|
+
if (loaded.unsupportedVersion || !loaded.compiler) {
|
|
1488
|
+
for (const file of selected) warnings.push({
|
|
1489
|
+
code: "unsupported-vue-version",
|
|
1490
|
+
file: file.path,
|
|
1491
|
+
start: 0,
|
|
1492
|
+
end: 0,
|
|
1493
|
+
message: `Vue ${loaded.unsupportedVersion} is not supported; only Vue 3 SFCs are analyzed.`
|
|
1494
|
+
});
|
|
1495
|
+
return {
|
|
1496
|
+
...none,
|
|
1497
|
+
warnings
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
const compiler = loaded.compiler;
|
|
1501
|
+
const analyses = new Map(ownedVue.map((file) => [file.path, analyzeVueSource(compiler, file.path, file.source)]));
|
|
1502
|
+
const analysisOf = (path) => {
|
|
1503
|
+
const analysis = analyses.get(path);
|
|
1504
|
+
if (!analysis) throw new Error(`No Vue analysis was recorded for ${path}`);
|
|
1505
|
+
return analysis;
|
|
1506
|
+
};
|
|
1507
|
+
const selectedPaths = new Set(selected.map((file) => file.path));
|
|
1508
|
+
for (const file of ownedVue) {
|
|
1509
|
+
const analysis = analysisOf(file.path);
|
|
1510
|
+
if (analysis.retained) continue;
|
|
1511
|
+
for (const edge of analysis.styleBlockImports) {
|
|
1512
|
+
if (stylesheetReferenceTargets(file.path, edge.reference, styleSources).some((path) => pathOwners.get(path) === packageRoot)) continue;
|
|
1513
|
+
analysis.escapeUnverifiable = true;
|
|
1514
|
+
if (selectedPaths.has(file.path)) warnings.push({
|
|
1515
|
+
code: "unsupported-sfc-block",
|
|
1516
|
+
file: file.path,
|
|
1517
|
+
start: edge.start,
|
|
1518
|
+
end: edge.end,
|
|
1519
|
+
message: `The external style ${JSON.stringify(edge.reference)} could not be resolved inside the package.`
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const compileBlocks = async (file, blocks) => {
|
|
1524
|
+
for (const block of blocks.filter((block) => block.syntax !== "css")) {
|
|
1525
|
+
const virtualPath = `${file.path}.${block.syntax}`;
|
|
1526
|
+
const compiled = await compileStyleEntry(styleCompilers, packageRoot, virtualPath, block.content, { virtualEntry: true });
|
|
1527
|
+
validateCss(compiled.css);
|
|
1528
|
+
for (const loadedPath of compiled.loadedPaths) {
|
|
1529
|
+
if (loadedPath === virtualPath || !isProjectInput(workspaceRoot, loadedPath)) continue;
|
|
1530
|
+
const source = await snapshotLoadedSource(snapshots, loadedPath);
|
|
1531
|
+
if (!styleSources.has(loadedPath)) styleSources.set(loadedPath, source);
|
|
1532
|
+
if (!pathOwners.has(loadedPath)) pathOwners.set(loadedPath, packageRoot);
|
|
1533
|
+
}
|
|
1534
|
+
block.analysisSource = compiled.css;
|
|
1535
|
+
block.sourcePath = virtualPath;
|
|
1536
|
+
block.sourceMappings = compiled.sourceMappings;
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
for (const file of selected) {
|
|
1540
|
+
const analysis = analysisOf(file.path);
|
|
1541
|
+
if (!analysis.retained) await compileBlocks(file, [...analysis.blocks, ...analysis.moduleBlocks]);
|
|
1542
|
+
}
|
|
1543
|
+
const vueGraph = buildVueComponentGraph(ownedVue, sourceFiles, analyses, {
|
|
1544
|
+
styleSources,
|
|
1545
|
+
pathOwners,
|
|
1546
|
+
packageRoot
|
|
1547
|
+
});
|
|
1548
|
+
const generatesSelectors = (text) => /#\{|@\{|&[\w-]/.test(text);
|
|
1549
|
+
const vueShadowCss = [];
|
|
1550
|
+
const vueShadowModuleCss = [];
|
|
1551
|
+
let vueShadowUnverifiable = vueGraph.unresolvedStyleImport;
|
|
1552
|
+
for (const [path, source] of styleSources) {
|
|
1553
|
+
if (pathOwners.get(path) !== packageRoot) continue;
|
|
1554
|
+
if (isPreprocessorPath(path) && generatesSelectors(source)) {
|
|
1555
|
+
vueShadowUnverifiable = true;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
if (isStylesheetModule(path)) vueShadowModuleCss.push(source);
|
|
1559
|
+
else vueShadowCss.push({
|
|
1560
|
+
path,
|
|
1561
|
+
source
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
for (const file of sourceFiles) if (extname(file.path) === ".html" && pathOwners.get(file.path) === packageRoot && /<style/i.test(file.source)) vueShadowUnverifiable = true;
|
|
1565
|
+
for (const file of ownedVue) {
|
|
1566
|
+
const analysis = analysisOf(file.path);
|
|
1567
|
+
if (analysis.retained) {
|
|
1568
|
+
if (/<style/i.test(file.source)) vueShadowUnverifiable = true;
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (analysis.escapeUnverifiable || analysis.scriptImportsUnverifiable) vueShadowUnverifiable = true;
|
|
1572
|
+
for (const text of analysis.shadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
|
|
1573
|
+
else vueShadowCss.push({
|
|
1574
|
+
path: file.path,
|
|
1575
|
+
source: text
|
|
1576
|
+
});
|
|
1577
|
+
for (const text of analysis.unscopedShadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
|
|
1578
|
+
else vueShadowCss.push({
|
|
1579
|
+
path: file.path,
|
|
1580
|
+
source: text,
|
|
1581
|
+
migratingUnscoped: true
|
|
1582
|
+
});
|
|
1583
|
+
vueShadowCss.push(...analysis.shadowCssTexts.map((source) => ({
|
|
1584
|
+
path: file.path,
|
|
1585
|
+
source
|
|
1586
|
+
})), ...analysis.unscopedShadowCssTexts.map((source) => ({
|
|
1587
|
+
path: file.path,
|
|
1588
|
+
source,
|
|
1589
|
+
migratingUnscoped: true
|
|
1590
|
+
})), ...analysis.blocks.map((block) => ({
|
|
1591
|
+
path: file.path,
|
|
1592
|
+
source: block.analysisSource ?? block.content,
|
|
1593
|
+
scoped: true
|
|
1594
|
+
})));
|
|
1595
|
+
vueShadowModuleCss.push(...analysis.shadowModuleCssTexts, ...analysis.moduleBlocks.map((block) => block.analysisSource ?? block.content));
|
|
1596
|
+
}
|
|
1597
|
+
const packageIsPrivate = JSON.parse(await snapshotFile(snapshots, join(packageRoot, "package.json"))).private === true;
|
|
1598
|
+
const files = /* @__PURE__ */ new Map();
|
|
1599
|
+
const stylesheets = [];
|
|
1600
|
+
const stylePaths = /* @__PURE__ */ new Set();
|
|
1601
|
+
const unscopedPaths = /* @__PURE__ */ new Set();
|
|
1602
|
+
const ownedSources = sourceFiles.filter((file) => pathOwners.get(file.path) === packageRoot);
|
|
1603
|
+
const sourcePathSet = new Set(sourceFiles.map((file) => file.path));
|
|
1604
|
+
const hiddenHtml = scannedPaths.some((path) => extname(path) === ".html" && pathOwners.get(path) === packageRoot && !sourcePathSet.has(path));
|
|
1605
|
+
const projectWideUsageProven = packageIsPrivate && workspaceRoot === packageRoot && !workspaces && !explicitStyle && !hiddenHtml && ownedSources.every((file) => targetable.has(file.path)) && ownedVue.every((file) => !analysisOf(file.path).retained);
|
|
1606
|
+
const elementsByFile = /* @__PURE__ */ new Map();
|
|
1607
|
+
const addElement = (path, element, cssPaths) => {
|
|
1608
|
+
if (!element.classAttribute && !element.moduleBinding || cssPaths.length === 0) return;
|
|
1609
|
+
const elements = elementsByFile.get(path) ?? [];
|
|
1610
|
+
elements.push({
|
|
1611
|
+
...element,
|
|
1612
|
+
cssPaths: [...new Set(cssPaths)]
|
|
1613
|
+
});
|
|
1614
|
+
elementsByFile.set(path, elements);
|
|
1615
|
+
};
|
|
1616
|
+
const elementClasses = (element) => element.matchClasses ?? element.classAttribute?.value.split(/\s+/).filter(Boolean) ?? [];
|
|
1617
|
+
const componentRootSite = (site, root) => {
|
|
1618
|
+
const matchId = site.idAttribute?.value ?? root.idAttribute?.value;
|
|
1619
|
+
return {
|
|
1620
|
+
...site,
|
|
1621
|
+
matchClasses: [...elementClasses(site), ...elementClasses(root)],
|
|
1622
|
+
matchIds: matchId ? [matchId] : [],
|
|
1623
|
+
matchTag: root.tag
|
|
1624
|
+
};
|
|
1625
|
+
};
|
|
1626
|
+
for (const file of selected) {
|
|
1627
|
+
const analysis = analysisOf(file.path);
|
|
1628
|
+
if (analysis.retained) continue;
|
|
1629
|
+
const importedStyles = new Set([...analysis.scriptStyleImports.filter((reference) => reference.startsWith(".") && STYLESHEET_SYNTAX.has(extname(reference.split(/[?#]/, 1)[0]))).flatMap((reference) => stylesheetReferenceTargets(file.path, reference, styleSources)), ...analysis.styleBlockImports.flatMap((entry) => stylesheetReferenceTargets(file.path, entry.reference, styleSources))].filter((path) => pathOwners.get(path) === packageRoot));
|
|
1630
|
+
for (const path of importedStyles) {
|
|
1631
|
+
stylePaths.add(path);
|
|
1632
|
+
if (extname(path) !== ".css") continue;
|
|
1633
|
+
for (const imported of cssImports(styleSources.get(path) ?? "")) {
|
|
1634
|
+
if (imported.media) continue;
|
|
1635
|
+
for (const target of stylesheetReferenceTargets(path, imported.href, styleSources)) if (pathOwners.get(target) === packageRoot) importedStyles.add(target);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const ownContexts = [...analysis.blocks.length > 0 || analysis.unscopedBlocks.length > 0 || analysis.moduleBlocks.length > 0 && !analysis.moduleClosureBroken ? [file.path] : [], ...[...importedStyles].filter((path) => !isStylesheetModule(path))];
|
|
1639
|
+
for (const element of analysis.htmlElements) {
|
|
1640
|
+
const ownElement = analysis.rootIdsOverridden && analysis.rootStarts.includes(element.nodeStart) ? {
|
|
1641
|
+
...element,
|
|
1642
|
+
matchIds: []
|
|
1643
|
+
} : element;
|
|
1644
|
+
addElement(file.path, ownElement, ownContexts);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
for (const parent of ownedVue) {
|
|
1648
|
+
const analysis = analysisOf(parent.path);
|
|
1649
|
+
if (analysis.retained) continue;
|
|
1650
|
+
for (const edge of analysis.resolvedComponents ?? []) {
|
|
1651
|
+
const childAnalysis = analyses.get(edge.child);
|
|
1652
|
+
const childDetails = childAnalysis && !childAnalysis.retained ? childAnalysis : void 0;
|
|
1653
|
+
const singleRoot = childDetails?.rootStarts.length === 1 && !childDetails.rootVFor && !childDetails.rootFragment;
|
|
1654
|
+
const childRoots = childDetails?.htmlElements.filter((element) => childDetails.rootStarts.includes(element.nodeStart));
|
|
1655
|
+
if (selectedPaths.has(parent.path) && singleRoot && childRoots?.length === 1 && !childDetails?.fallthroughUnverifiable) addElement(parent.path, componentRootSite(edge.site, childRoots[0]), [parent.path]);
|
|
1656
|
+
if (selectedPaths.has(edge.child) && singleRoot && edge.parent !== edge.child && !childDetails?.fallthroughUnverifiable) {
|
|
1657
|
+
addElement(parent.path, edge.site, [edge.child]);
|
|
1658
|
+
if (childRoots?.length === 1) {
|
|
1659
|
+
const shadowSite = {
|
|
1660
|
+
...componentRootSite(edge.site, childRoots[0]),
|
|
1661
|
+
moduleBinding: childRoots[0].moduleBinding
|
|
1662
|
+
};
|
|
1663
|
+
if (shadowSite.classAttribute) {
|
|
1664
|
+
shadowSite.classAttribute = {
|
|
1665
|
+
...shadowSite.classAttribute,
|
|
1666
|
+
writable: false
|
|
1667
|
+
};
|
|
1668
|
+
addElement(parent.path, shadowSite, [edge.child]);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
for (const file of selected) {
|
|
1675
|
+
await rejectSymlinkTarget(file.path, packageRoot);
|
|
1676
|
+
const analysis = analysisOf(file.path);
|
|
1677
|
+
warnings.push(...analysis.warnings);
|
|
1678
|
+
if (analysis.retained) continue;
|
|
1679
|
+
const componentOpen = analysis.componentsOpen || (analysis.resolvedComponents ?? []).some((edge) => {
|
|
1680
|
+
const child = analyses.get(edge.child);
|
|
1681
|
+
return !child || child.retained || child.dynamic || child.fallthroughUnverifiable || child.rootVFor || child.rootFragment || child.rootStarts.length !== 1 || !child.htmlElements.some((element) => element.nodeStart === child.rootStarts[0]);
|
|
1682
|
+
});
|
|
1683
|
+
const callers = vueGraph.callers.get(file.path) ?? [];
|
|
1684
|
+
const callerOpen = !packageIsPrivate || callers.length === 0 || vueGraph.callerOpen.has(file.path) || callers.some((edge) => {
|
|
1685
|
+
const parent = analyses.get(edge.parent);
|
|
1686
|
+
return parent ? parent.retained || parent.dynamic : false;
|
|
1687
|
+
});
|
|
1688
|
+
const retentionReason = (open) => {
|
|
1689
|
+
if (analysis.dynamic) return "dynamic-template-class";
|
|
1690
|
+
if (open) return "component-class-target";
|
|
1691
|
+
if (analysis.alwaysRenderedRoots < 2 && callerOpen) return "open-root-fallthrough";
|
|
1692
|
+
};
|
|
1693
|
+
const retention = retentionReason(componentOpen);
|
|
1694
|
+
const migrateUnscoped = analysis.blocks.length === 0 && analysis.unscopedBlocks.length > 0 && projectWideUsageProven && !analysis.dynamic && !analysis.vHtml && !analysis.hasSlot && !analysis.escapeUnverifiable && !componentOpen && ownedSources.length === 1 && ownedSources[0].path === file.path;
|
|
1695
|
+
if (migrateUnscoped) {
|
|
1696
|
+
await compileBlocks(file, analysis.unscopedBlocks);
|
|
1697
|
+
unscopedPaths.add(file.path);
|
|
1698
|
+
}
|
|
1699
|
+
if (analysis.unscopedBlocks.length > 0 && !migrateUnscoped) for (const block of analysis.unscopedBlocks) warnings.push({
|
|
1700
|
+
code: "unscoped-style-block",
|
|
1701
|
+
file: file.path,
|
|
1702
|
+
start: block.contentStart,
|
|
1703
|
+
end: block.contentEnd,
|
|
1704
|
+
message: "The global reach of this unscoped style block could not be proven."
|
|
1705
|
+
});
|
|
1706
|
+
const moduleRetention = retentionReason(false);
|
|
1707
|
+
const migrateModule = analysis.moduleBlocks.length > 0 && !analysis.moduleClosureBroken && !moduleRetention;
|
|
1708
|
+
if (analysis.moduleBlocks.length > 0 && !migrateModule) {
|
|
1709
|
+
const [code, message] = analysis.moduleClosureBroken ? ["unsupported-css-module-reference", "`$style` is referenced outside provable direct member accesses, so the module is retained."] : moduleRetention === "dynamic-template-class" ? ["dynamic-template-class", "A dynamic class binding makes the template's class set unprovable, so the module is retained."] : ["open-root-fallthrough", "A parent component can merge classes onto the single root element, so the module is retained."];
|
|
1710
|
+
for (const block of analysis.moduleBlocks) warnings.push({
|
|
1711
|
+
code,
|
|
1712
|
+
file: file.path,
|
|
1713
|
+
start: block.contentStart,
|
|
1714
|
+
end: block.contentEnd,
|
|
1715
|
+
message
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
const vueBlocks = migrateUnscoped ? analysis.unscopedBlocks : analysis.blocks;
|
|
1719
|
+
if (vueBlocks.length === 0 && !migrateModule) continue;
|
|
1720
|
+
const vueRetention = migrateUnscoped ? void 0 : retention;
|
|
1721
|
+
const shadowCss = vueShadowCss.filter((entry) => entry.path !== file.path || !entry.scoped && !(migrateUnscoped && entry.migratingUnscoped)).map((entry) => entry.source);
|
|
1722
|
+
if (migrateModule) stylesheets.push({
|
|
1723
|
+
cssPath: file.path,
|
|
1724
|
+
cssSource: file.source,
|
|
1725
|
+
cssModuleId: normalizedRelativePath(packageRoot, file.path),
|
|
1726
|
+
syntax: "css",
|
|
1727
|
+
isModule: true,
|
|
1728
|
+
vueBlocks: analysis.moduleBlocks,
|
|
1729
|
+
vueModule: true,
|
|
1730
|
+
vueShadowCss: shadowCss,
|
|
1731
|
+
vueShadowModuleCss,
|
|
1732
|
+
vueShadowUnverifiable
|
|
1733
|
+
});
|
|
1734
|
+
if (vueBlocks.length > 0) stylesheets.push({
|
|
1735
|
+
cssPath: file.path,
|
|
1736
|
+
cssSource: file.source,
|
|
1737
|
+
cssModuleId: normalizedRelativePath(packageRoot, file.path),
|
|
1738
|
+
syntax: "css",
|
|
1739
|
+
isModule: !vueRetention,
|
|
1740
|
+
vueBlocks,
|
|
1741
|
+
vueRetention,
|
|
1742
|
+
vueUnscoped: migrateUnscoped,
|
|
1743
|
+
vueShadowCss: vueRetention ? void 0 : shadowCss,
|
|
1744
|
+
vueShadowModuleCss: vueRetention ? void 0 : vueShadowModuleCss,
|
|
1745
|
+
vueShadowUnverifiable: vueRetention ? void 0 : vueShadowUnverifiable
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
for (const file of ownedVue) {
|
|
1749
|
+
const analysis = analysisOf(file.path);
|
|
1750
|
+
if (analysis.retained) continue;
|
|
1751
|
+
const elements = elementsByFile.get(file.path) ?? [];
|
|
1752
|
+
const contextPaths = [...new Set(elements.flatMap((element) => element.cssPaths))];
|
|
1753
|
+
if (contextPaths.length === 0) continue;
|
|
1754
|
+
await rejectSymlinkTarget(file.path, packageRoot);
|
|
1755
|
+
files.set(file.path, {
|
|
1756
|
+
...file,
|
|
1757
|
+
htmlElements: elements,
|
|
1758
|
+
htmlStylesheets: contextPaths.map((cssPath) => ({
|
|
1759
|
+
cssPath,
|
|
1760
|
+
variants: [],
|
|
1761
|
+
direct: cssPath === file.path,
|
|
1762
|
+
analyzable: true
|
|
1763
|
+
})),
|
|
1764
|
+
htmlReferencesSafe: !analysis.dynamic,
|
|
1765
|
+
htmlScriptText: analysis.scriptText
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
return {
|
|
1769
|
+
files,
|
|
1770
|
+
stylesheets,
|
|
1771
|
+
stylePaths,
|
|
1772
|
+
unscopedPaths,
|
|
1773
|
+
warnings,
|
|
1774
|
+
compiler
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region src/util/write.ts
|
|
1779
|
+
async function verifySnapshots(snapshots) {
|
|
1780
|
+
const entries = [...snapshots].sort(([left], [right]) => left.localeCompare(right));
|
|
1781
|
+
const reads = await Promise.allSettled(entries.map(([path]) => readFile(path, "utf8")));
|
|
1782
|
+
for (const [index, [path, before]] of entries.entries()) {
|
|
1783
|
+
const read = reads[index];
|
|
1784
|
+
if (read.status === "rejected") {
|
|
1785
|
+
const error = read.reason;
|
|
1786
|
+
const detail = errorCode(error) ?? (error instanceof Error ? error.message : String(error));
|
|
1787
|
+
throw new Error(`Source changed after planning: ${path} (${detail})`);
|
|
1788
|
+
}
|
|
1789
|
+
if (read.value !== before) throw new Error(`Source changed after planning: ${path}`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
async function writeChanges(changes, deletions) {
|
|
1793
|
+
const token = `${process.pid}-${Date.now()}`;
|
|
1794
|
+
const staged = changes.map((change, index) => [join(dirname(change.path), `.${basename(change.path)}.tw-migrate-${token}-${index}`), change]);
|
|
1795
|
+
const backups = [...changes, ...deletions].map((change, index) => [join(dirname(change.path), `.${basename(change.path)}.tw-migrate-backup-${token}-${index}`), change.path]);
|
|
1796
|
+
const backedUp = [];
|
|
1797
|
+
let succeeded = false;
|
|
1798
|
+
try {
|
|
1799
|
+
const stagingResults = await Promise.allSettled(staged.map(async ([temporaryPath, change]) => {
|
|
1800
|
+
const { mode } = await stat(change.path);
|
|
1801
|
+
await writeFile(temporaryPath, change.source);
|
|
1802
|
+
await chmod(temporaryPath, mode & 511);
|
|
1803
|
+
}));
|
|
1804
|
+
for (const result of stagingResults) if (result.status === "rejected") throw result.reason;
|
|
1805
|
+
for (const [backupPath, originalPath] of backups) {
|
|
1806
|
+
await rename(originalPath, backupPath);
|
|
1807
|
+
backedUp.push([backupPath, originalPath]);
|
|
1808
|
+
}
|
|
1809
|
+
for (const [temporaryPath, change] of staged) await rename(temporaryPath, change.path);
|
|
1810
|
+
succeeded = true;
|
|
1811
|
+
} finally {
|
|
1812
|
+
if (succeeded) await Promise.all(backups.map(([backupPath]) => rm(backupPath, { force: true })));
|
|
1813
|
+
else for (const [backupPath, originalPath] of backedUp.reverse()) try {
|
|
1814
|
+
await rename(backupPath, originalPath);
|
|
1815
|
+
} catch {}
|
|
1816
|
+
await Promise.all(staged.map(([temporaryPath]) => rm(temporaryPath, { force: true })));
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
//#endregion
|
|
1820
|
+
//#region src/index.ts
|
|
1821
|
+
async function migrate(options = {}) {
|
|
1822
|
+
if (options.styleFile && options.workspaces) throw new TypeError("styleFile cannot be combined with workspaces");
|
|
1823
|
+
if (options.tailwindCss && options.workspaces) throw new TypeError("tailwindCss cannot be combined with workspaces");
|
|
1824
|
+
const scope = await resolveScope(options);
|
|
1825
|
+
const { cwd, workspaceRoot, selectedPackages, explicitStyle, configuredEntry } = scope;
|
|
1826
|
+
const leftovers = await collectFiles(workspaceRoot, (path) => basename(path).includes(".tw-migrate-"));
|
|
1827
|
+
if (leftovers.length > 0) {
|
|
1828
|
+
const listed = leftovers.sort().map((path) => ` ${normalizedRelativePath(cwd, path)}`).join("\n");
|
|
1829
|
+
throw new Error(`Found leftover tw-migrate files from an interrupted run:\n${listed}\nRestore each ".<name>.tw-migrate-backup-*" file by renaming it back to "<name>", delete any remaining ".<name>.tw-migrate-*" staging files, then re-run.`);
|
|
1830
|
+
}
|
|
1831
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
1832
|
+
const stylePaths = scope.scannedPaths.filter(isStylesheetPath);
|
|
1833
|
+
const sourcePaths = scope.scannedPaths.filter((path) => SOURCE_EXTENSIONS.has(extname(path)));
|
|
1834
|
+
const [styleSources, sourceCandidates] = await Promise.all([
|
|
1835
|
+
Promise.all(stylePaths.map(async (path) => [path, await snapshotFile(snapshots, path)])).then((entries) => new Map(entries)),
|
|
1836
|
+
Promise.all(sourcePaths.map(async (path) => {
|
|
1837
|
+
const source = await readFile(path, "utf8");
|
|
1838
|
+
const mayReferenceModule = extname(path) !== ".html" || /<link\b/i.test(source);
|
|
1839
|
+
if (!scope.targetable.has(path) && !mayReferenceModule) return void 0;
|
|
1840
|
+
return {
|
|
1841
|
+
path,
|
|
1842
|
+
source: recordSnapshot(snapshots, path, source)
|
|
1843
|
+
};
|
|
1844
|
+
})),
|
|
1845
|
+
...selectedPackages.map((packageRoot) => snapshotFile(snapshots, join(packageRoot, "package.json")))
|
|
1846
|
+
]);
|
|
1847
|
+
const sourceFiles = sourceCandidates.flatMap((file) => file ? [file] : []);
|
|
1848
|
+
if (explicitStyle && isStylesheetPath(explicitStyle) && !styleSources.has(explicitStyle)) styleSources.set(explicitStyle, await snapshotFile(snapshots, explicitStyle));
|
|
1849
|
+
if (configuredEntry && !styleSources.has(configuredEntry)) styleSources.set(configuredEntry, await snapshotFile(snapshots, configuredEntry));
|
|
1850
|
+
const context = {
|
|
1851
|
+
...scope,
|
|
1852
|
+
options,
|
|
1853
|
+
snapshots,
|
|
1854
|
+
styleSources,
|
|
1855
|
+
sourceFiles,
|
|
1856
|
+
styleDependents: indexStylesheetDependents(styleSources)
|
|
1857
|
+
};
|
|
1858
|
+
const failures = [];
|
|
1859
|
+
const plans = [];
|
|
1860
|
+
for (const packageRoot of selectedPackages) {
|
|
1861
|
+
const result = await planPackage(context, packageRoot);
|
|
1862
|
+
if (result.failure) failures.push(result.failure);
|
|
1863
|
+
else if (result.plan) plans.push(result.plan);
|
|
1864
|
+
}
|
|
1865
|
+
const originals = new Map([...styleSources, ...sourceFiles.map((file) => [file.path, file.source])]);
|
|
1866
|
+
const { filesByPath, deletedPaths, candidates, rules, warnings, convertedRules, retainedRules } = mergePlans(plans, originals);
|
|
1867
|
+
const changed = [...filesByPath.values()].map((file) => ({
|
|
1868
|
+
...file,
|
|
1869
|
+
before: originals.get(file.path) ?? ""
|
|
1870
|
+
})).filter((file) => file.before !== file.source);
|
|
1871
|
+
const deleted = [...deletedPaths].map((path) => ({
|
|
1872
|
+
path,
|
|
1873
|
+
before: originals.get(path) ?? ""
|
|
1874
|
+
}));
|
|
1875
|
+
const operations = [...changed, ...deleted].sort((left, right) => left.path.localeCompare(right.path));
|
|
1876
|
+
const changedFiles = operations.map((file) => normalizedRelativePath(cwd, file.path));
|
|
1877
|
+
const diff = operations.map((file) => unifiedDiff(normalizedRelativePath(cwd, file.path), file.before, file.source ?? "")).join("");
|
|
1878
|
+
if (options.write && operations.length > 0) {
|
|
1879
|
+
await verifySnapshots(snapshots);
|
|
1880
|
+
await writeChanges(changed, deleted);
|
|
1881
|
+
}
|
|
1882
|
+
warnings.sort((left, right) => left.file.localeCompare(right.file) || left.start - right.start || left.end - right.end || left.code.localeCompare(right.code));
|
|
1883
|
+
failures.sort((left, right) => left.package.localeCompare(right.package));
|
|
1884
|
+
return {
|
|
1885
|
+
changedFiles,
|
|
1886
|
+
diff,
|
|
1887
|
+
convertedRules,
|
|
1888
|
+
retainedRules,
|
|
1889
|
+
rules: rules.map(({ stylesheet: _, ...rule }) => ({
|
|
1890
|
+
...rule,
|
|
1891
|
+
file: normalizedRelativePath(cwd, rule.file)
|
|
1892
|
+
})),
|
|
1893
|
+
candidates: [...candidates].sort(),
|
|
1894
|
+
warnings: warnings.map((warning) => ({
|
|
1895
|
+
...warning,
|
|
1896
|
+
file: normalizedRelativePath(cwd, warning.file)
|
|
1897
|
+
})),
|
|
1898
|
+
failures
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
async function planPackage(context, packageRoot) {
|
|
1902
|
+
const { options, snapshots, workspaceRoot, explicitStyle, configuredEntry, styleSources, sourceFiles, styleDependents, pathOwners, targetable, selectedPackages, scannedPaths } = context;
|
|
1903
|
+
const recover = (error, fatal = false) => {
|
|
1904
|
+
if (!options.force || fatal) throw error;
|
|
1905
|
+
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
1906
|
+
};
|
|
1907
|
+
let preparedHtml;
|
|
1908
|
+
try {
|
|
1909
|
+
preparedHtml = await preparePackageHtml({
|
|
1910
|
+
packageRoot,
|
|
1911
|
+
sourceFiles,
|
|
1912
|
+
styleSources,
|
|
1913
|
+
snapshots,
|
|
1914
|
+
pathOwners,
|
|
1915
|
+
styleDependents
|
|
1916
|
+
});
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
return recover(error, isIntegrityError(error));
|
|
1919
|
+
}
|
|
1920
|
+
const styleCompilers = {};
|
|
1921
|
+
let preparedVue;
|
|
1922
|
+
try {
|
|
1923
|
+
preparedVue = await preparePackageVue({
|
|
1924
|
+
packageRoot,
|
|
1925
|
+
styleCompilers,
|
|
1926
|
+
sourceFiles,
|
|
1927
|
+
styleSources,
|
|
1928
|
+
pathOwners,
|
|
1929
|
+
targetable,
|
|
1930
|
+
explicitStyle,
|
|
1931
|
+
snapshots,
|
|
1932
|
+
workspaceRoot,
|
|
1933
|
+
workspaces: options.workspaces,
|
|
1934
|
+
scannedPaths
|
|
1935
|
+
});
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
return recover(error, isIntegrityError(error));
|
|
1938
|
+
}
|
|
1939
|
+
if (explicitStyle && extname(explicitStyle) === ".vue" && preparedVue.stylesheets.length === 0 && ![...preparedVue.stylePaths].some((path) => !isStylesheetModule(path))) return vueWarningsOnlyResult(preparedVue);
|
|
1940
|
+
const packageSources = [...sourceFiles.filter((file) => extname(file.path) !== ".html").map((file) => preparedVue.files.get(file.path) ?? file), ...preparedHtml.files];
|
|
1941
|
+
const ownedStyles = [...styleSources.keys()].filter((path) => pathOwners.get(path) === packageRoot && (targetable.has(path) || preparedHtml.stylePaths.has(path) || preparedVue.stylePaths.has(path)));
|
|
1942
|
+
if (ownedStyles.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
|
|
1943
|
+
let tailwindPath;
|
|
1944
|
+
let tailwindEntries;
|
|
1945
|
+
try {
|
|
1946
|
+
({path: tailwindPath, entries: tailwindEntries} = resolveTailwindEntry(ownedStyles, styleSources, configuredEntry));
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
return recover(error);
|
|
1949
|
+
}
|
|
1950
|
+
const excludedEntries = /* @__PURE__ */ new Set([...tailwindEntries, tailwindPath]);
|
|
1951
|
+
const explicitCss = explicitStyle && extname(explicitStyle) !== ".vue" ? explicitStyle : void 0;
|
|
1952
|
+
const targets = explicitCss ? [explicitCss] : explicitStyle ? [...preparedVue.stylePaths].filter((path) => !isStylesheetModule(path) && !excludedEntries.has(path)) : ownedStyles.filter((path) => !excludedEntries.has(path) && !preparedHtml.generatedPaths.has(path) && (!isPreprocessorPath(path) || preparedHtml.stylePaths.has(path) || packageSources.some((file) => sourceReferencesStyle(file, path))));
|
|
1953
|
+
if (targets.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
|
|
1954
|
+
if (targets.some((path) => excludedEntries.has(path))) throw new Error("The Tailwind CSS entry cannot be migrated.");
|
|
1955
|
+
let tailwind;
|
|
1956
|
+
try {
|
|
1957
|
+
tailwind = await loadTailwind(packageRoot, tailwindPath, snapshots, workspaceRoot);
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
return recover(error, isIntegrityError(error));
|
|
1960
|
+
}
|
|
1961
|
+
const files = packageSources.map((file) => {
|
|
1962
|
+
const owner = pathOwners.get(file.path);
|
|
1963
|
+
return {
|
|
1964
|
+
...file,
|
|
1965
|
+
writable: targetable.has(file.path) && (options.workspaces ? owner !== void 0 && selectedPackages.includes(owner) : owner === packageRoot)
|
|
1966
|
+
};
|
|
1967
|
+
});
|
|
1968
|
+
let stylesheets;
|
|
1969
|
+
try {
|
|
1970
|
+
stylesheets = [];
|
|
1971
|
+
const compilerDependents = /* @__PURE__ */ new Map();
|
|
1972
|
+
for (const stylePath of targets.sort()) {
|
|
1973
|
+
await rejectSymlinkTarget(stylePath, packageRoot);
|
|
1974
|
+
const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith("_");
|
|
1975
|
+
const stylesheet = {
|
|
1976
|
+
cssPath: stylePath,
|
|
1977
|
+
cssSource: styleSources.get(stylePath) ?? "",
|
|
1978
|
+
cssModuleId: normalizedRelativePath(packageRoot, stylePath),
|
|
1979
|
+
cssDependents: styleDependents.get(stylePath) ?? [],
|
|
1980
|
+
syntax: stylesheetSyntax(stylePath),
|
|
1981
|
+
isModule: isStylesheetModule(stylePath),
|
|
1982
|
+
isPartial
|
|
1983
|
+
};
|
|
1984
|
+
let compiled;
|
|
1985
|
+
if (isSassPath(stylePath) && !isPartial || extname(stylePath) === ".less") compiled = await compileStyleEntry(styleCompilers, packageRoot, stylePath, stylesheet.cssSource);
|
|
1986
|
+
if (compiled) {
|
|
1987
|
+
validateCss(compiled.css);
|
|
1988
|
+
for (const loadedPath of compiled.loadedPaths) {
|
|
1989
|
+
if (!isProjectInput(workspaceRoot, loadedPath)) continue;
|
|
1990
|
+
const source = await snapshotLoadedSource(snapshots, loadedPath);
|
|
1991
|
+
if (!styleSources.has(loadedPath)) styleSources.set(loadedPath, source);
|
|
1992
|
+
if (loadedPath !== stylePath) {
|
|
1993
|
+
const dependents = compilerDependents.get(loadedPath) ?? [];
|
|
1994
|
+
dependents.push(stylePath);
|
|
1995
|
+
compilerDependents.set(loadedPath, dependents);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
stylesheet.analysisSource = compiled.css;
|
|
1999
|
+
stylesheet.sourceMappings = compiled.sourceMappings;
|
|
2000
|
+
}
|
|
2001
|
+
stylesheets.push(stylesheet);
|
|
2002
|
+
}
|
|
2003
|
+
for (const stylesheet of stylesheets) {
|
|
2004
|
+
const dependents = compilerDependents.get(stylesheet.cssPath) ?? [];
|
|
2005
|
+
if (dependents.length === 0) continue;
|
|
2006
|
+
stylesheet.isPartial = true;
|
|
2007
|
+
stylesheet.cssDependents = [.../* @__PURE__ */ new Set([...stylesheet.cssDependents ?? [], ...dependents])].sort();
|
|
2008
|
+
}
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
return recover(error, isIntegrityError(error));
|
|
2011
|
+
}
|
|
2012
|
+
stylesheets.push(...preparedVue.stylesheets);
|
|
2013
|
+
const request = {
|
|
2014
|
+
stylesheets,
|
|
2015
|
+
tailwindPath: tailwind.path,
|
|
2016
|
+
tailwindSource: tailwind.css,
|
|
2017
|
+
utilityPrefix: tailwind.designSystem.theme.prefix,
|
|
2018
|
+
themeTokens: tailwind.themeTokens,
|
|
2019
|
+
files
|
|
2020
|
+
};
|
|
2021
|
+
let plan;
|
|
2022
|
+
try {
|
|
2023
|
+
plan = JSON.parse(planBatchMigration(JSON.stringify(request)));
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
return recover(error, !isRecoverablePlanningError(error));
|
|
2026
|
+
}
|
|
2027
|
+
try {
|
|
2028
|
+
plan = replanCompileFailures(tailwind, request, plan);
|
|
2029
|
+
} catch (error) {
|
|
2030
|
+
return recover(error);
|
|
2031
|
+
}
|
|
2032
|
+
removeMigratedHtmlLinks(plan, preparedHtml);
|
|
2033
|
+
plan.warnings.push(...preparedHtml.warnings);
|
|
2034
|
+
plan.warnings.push(...preparedVue.warnings);
|
|
2035
|
+
try {
|
|
2036
|
+
for (const file of plan.files.filter((file) => extname(file.path) === ".html")) parseHtmlSource(file.path, file.source);
|
|
2037
|
+
for (const file of plan.files.filter((file) => extname(file.path) === ".vue")) {
|
|
2038
|
+
const vueCompiler = preparedVue.compiler;
|
|
2039
|
+
if (!vueCompiler) throw new Error(`No Vue compiler is available to verify ${file.path}`);
|
|
2040
|
+
const includeUnscoped = preparedVue.unscopedPaths.has(file.path);
|
|
2041
|
+
const blocks = verifyVueSource(vueCompiler, file.path, file.source, includeUnscoped);
|
|
2042
|
+
const untouched = /* @__PURE__ */ new Map();
|
|
2043
|
+
for (const block of verifyVueSource(vueCompiler, file.path, snapshots.get(file.path) ?? "", includeUnscoped)) {
|
|
2044
|
+
const key = `${block.syntax}\0${block.content}`;
|
|
2045
|
+
untouched.set(key, (untouched.get(key) ?? 0) + 1);
|
|
2046
|
+
}
|
|
2047
|
+
for (const [index, block] of blocks.entries()) {
|
|
2048
|
+
const key = `${block.syntax}\0${block.content}`;
|
|
2049
|
+
const remaining = untouched.get(key) ?? 0;
|
|
2050
|
+
if (remaining > 0) {
|
|
2051
|
+
untouched.set(key, remaining - 1);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (block.syntax === "css") validateCss(block.content);
|
|
2055
|
+
else validateCss((await compileStyleEntry(styleCompilers, packageRoot, `${file.path}.${index}.${block.syntax}`, block.content, { virtualEntry: true })).css);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
for (const stylesheet of stylesheets.filter((stylesheet) => isPreprocessorPath(stylesheet.cssPath))) {
|
|
2059
|
+
const changed = plan.files.find((file) => file.path === stylesheet.cssPath);
|
|
2060
|
+
if (!changed && !plan.deletedFiles.includes(stylesheet.cssPath)) continue;
|
|
2061
|
+
const source = changed?.source ?? "";
|
|
2062
|
+
validateCss((await compileStyleEntry(styleCompilers, packageRoot, stylesheet.cssPath, source)).css);
|
|
2063
|
+
}
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
return recover(error, !isMissingStyleCompilerError(error));
|
|
2066
|
+
}
|
|
2067
|
+
return { plan };
|
|
2068
|
+
}
|
|
2069
|
+
function replanCompileFailures(tailwind, request, initialPlan) {
|
|
2070
|
+
let plan = initialPlan;
|
|
2071
|
+
const blockedByStylesheet = /* @__PURE__ */ new Map();
|
|
2072
|
+
while (true) {
|
|
2073
|
+
const failing = invalidCandidates(tailwind, plan.candidates);
|
|
2074
|
+
if (failing.length === 0) break;
|
|
2075
|
+
let progressed = false;
|
|
2076
|
+
for (const rule of plan.rules) {
|
|
2077
|
+
const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
|
|
2078
|
+
if (failed.length === 0) continue;
|
|
2079
|
+
let blocked = blockedByStylesheet.get(rule.stylesheet);
|
|
2080
|
+
if (!blocked) blockedByStylesheet.set(rule.stylesheet, blocked = /* @__PURE__ */ new Map());
|
|
2081
|
+
const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
|
|
2082
|
+
let entry = blocked.get(key);
|
|
2083
|
+
if (!entry) {
|
|
2084
|
+
blocked.set(key, entry = {
|
|
2085
|
+
ruleId: rule.ruleId,
|
|
2086
|
+
authoredSpan: rule.authoredSpan,
|
|
2087
|
+
candidates: /* @__PURE__ */ new Set()
|
|
2088
|
+
});
|
|
2089
|
+
progressed = true;
|
|
2090
|
+
}
|
|
2091
|
+
for (const candidate of failed) entry.candidates.add(candidate);
|
|
2092
|
+
}
|
|
2093
|
+
if (!progressed) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
|
|
2094
|
+
plan = JSON.parse(planBatchMigration(JSON.stringify({
|
|
2095
|
+
...request,
|
|
2096
|
+
stylesheets: request.stylesheets.map((stylesheet, index) => ({
|
|
2097
|
+
...stylesheet,
|
|
2098
|
+
blockedRules: [...blockedByStylesheet.get(index)?.values() ?? []].map((entry) => entry.ruleId)
|
|
2099
|
+
}))
|
|
2100
|
+
})));
|
|
2101
|
+
}
|
|
2102
|
+
for (const [index, blocked] of blockedByStylesheet) {
|
|
2103
|
+
const cssPath = request.stylesheets[index].cssPath;
|
|
2104
|
+
for (const { authoredSpan, candidates } of blocked.values()) {
|
|
2105
|
+
const failed = [...candidates].sort().map((candidate) => `\`${candidate}\``).join(", ");
|
|
2106
|
+
plan.warnings.push({
|
|
2107
|
+
code: "candidate-compilation-failure",
|
|
2108
|
+
file: cssPath,
|
|
2109
|
+
start: authoredSpan.start,
|
|
2110
|
+
end: authoredSpan.end,
|
|
2111
|
+
message: `Tailwind did not generate CSS for ${failed}, so the rule is retained.`
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return plan;
|
|
2116
|
+
}
|
|
2117
|
+
function removeMigratedHtmlLinks(plan, preparedHtml) {
|
|
2118
|
+
const unlinked = new Set(plan.unlinkedFiles);
|
|
2119
|
+
const linksByFile = /* @__PURE__ */ new Map();
|
|
2120
|
+
for (const link of preparedHtml.removableLinks) {
|
|
2121
|
+
if (!unlinked.has(link.cssPath)) continue;
|
|
2122
|
+
const links = linksByFile.get(link.filePath) ?? /* @__PURE__ */ new Set();
|
|
2123
|
+
links.add(`${link.href}\0${link.media}`);
|
|
2124
|
+
linksByFile.set(link.filePath, links);
|
|
2125
|
+
}
|
|
2126
|
+
for (const [filePath, removable] of linksByFile) {
|
|
2127
|
+
const planned = plan.files.find((file) => file.path === filePath);
|
|
2128
|
+
const original = preparedHtml.files.find((file) => file.path === filePath);
|
|
2129
|
+
if (!original) continue;
|
|
2130
|
+
const source = planned?.source ?? original.source;
|
|
2131
|
+
const links = parseHtmlSource(filePath, source).links.filter((link) => removable.has(`${link.href}\0${link.media}`)).sort((left, right) => right.tagStart - left.tagStart);
|
|
2132
|
+
let bytes = Buffer.from(source);
|
|
2133
|
+
for (const link of links) bytes = Buffer.concat([bytes.subarray(0, link.tagStart), bytes.subarray(link.tagEnd)]);
|
|
2134
|
+
const updated = bytes.toString();
|
|
2135
|
+
if (updated === source) continue;
|
|
2136
|
+
if (planned) planned.source = updated;
|
|
2137
|
+
else plan.files.push({
|
|
2138
|
+
path: filePath,
|
|
2139
|
+
source: updated
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
function mergePlans(plans, originals) {
|
|
2144
|
+
const filesByPath = /* @__PURE__ */ new Map();
|
|
2145
|
+
const deletedPaths = /* @__PURE__ */ new Set();
|
|
2146
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
2147
|
+
const rules = [];
|
|
2148
|
+
const warnings = [];
|
|
2149
|
+
const seenWarnings = /* @__PURE__ */ new Set();
|
|
2150
|
+
let convertedRules = 0;
|
|
2151
|
+
let retainedRules = 0;
|
|
2152
|
+
const claimPath = (path, kind) => {
|
|
2153
|
+
if (!originals.has(path)) throw new Error(`Planned ${kind} is outside the source snapshot: ${path}`);
|
|
2154
|
+
if (filesByPath.has(path) || deletedPaths.has(path)) throw new Error(`Multiple package groups planned changes for ${path}`);
|
|
2155
|
+
};
|
|
2156
|
+
for (const plan of plans) {
|
|
2157
|
+
for (const file of plan.files) {
|
|
2158
|
+
claimPath(file.path, "file");
|
|
2159
|
+
filesByPath.set(file.path, file);
|
|
2160
|
+
}
|
|
2161
|
+
for (const path of plan.deletedFiles) {
|
|
2162
|
+
claimPath(path, "deletion");
|
|
2163
|
+
deletedPaths.add(path);
|
|
2164
|
+
}
|
|
2165
|
+
for (const candidate of plan.candidates) candidates.add(candidate);
|
|
2166
|
+
convertedRules += plan.convertedRules;
|
|
2167
|
+
retainedRules += plan.retainedRules;
|
|
2168
|
+
rules.push(...plan.rules);
|
|
2169
|
+
for (const warning of plan.warnings) {
|
|
2170
|
+
const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}`;
|
|
2171
|
+
if (seenWarnings.has(key)) continue;
|
|
2172
|
+
seenWarnings.add(key);
|
|
2173
|
+
warnings.push(warning);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return {
|
|
2177
|
+
filesByPath,
|
|
2178
|
+
deletedPaths,
|
|
2179
|
+
candidates,
|
|
2180
|
+
rules,
|
|
2181
|
+
warnings,
|
|
2182
|
+
convertedRules,
|
|
2183
|
+
retainedRules
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
//#endregion
|
|
2187
|
+
export { migrate };
|