tw-migrate 0.1.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/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 };