tw-migrate 0.4.0 → 0.5.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 CHANGED
@@ -3,8 +3,8 @@ import { promisify } from "node:util";
3
3
  import { chmod, lstat, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { execFile } from "node:child_process";
6
- import { parse } from "parse5";
7
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { parse } from "parse5";
8
8
  //#region src/util/diff.ts
9
9
  function unifiedDiff(path, before, after) {
10
10
  const oldLines = before.split("\n");
@@ -18,186 +18,10 @@ function unifiedDiff(path, before, after) {
18
18
  ].join("");
19
19
  }
20
20
  //#endregion
21
- //#region src/parser/html.ts
22
- const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
23
- function parseHtmlSource(path, source) {
24
- const errors = [];
25
- const document = parse(source, {
26
- sourceCodeLocationInfo: true,
27
- onParseError(error) {
28
- if (error.code !== "missing-doctype") errors.push(error);
29
- }
30
- });
31
- if (errors.length > 0) throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
32
- const links = [];
33
- const bases = [];
34
- const elements = [];
35
- const dynamicAttributes = [];
36
- const scriptTexts = [];
37
- const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
38
- function visit(node) {
39
- if (node.tagName === "script") {
40
- for (const child of node.childNodes ?? []) if (child.nodeName === "#text" && child.value) scriptTexts.push(child.value);
41
- }
42
- if (node.tagName) {
43
- const attributes = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
44
- const locations = node.sourceCodeLocation?.attrs;
45
- if (locations) {
46
- if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
47
- const href = locatedAttribute(source, locations.href, attributes.get("href"));
48
- const media = locatedAttribute(source, locations.media, attributes.get("media"));
49
- const tag = node.sourceCodeLocation?.startTag ?? node.sourceCodeLocation;
50
- if (href && tag) links.push({
51
- href: href.value,
52
- media: media?.value ?? "",
53
- start: href.start,
54
- end: href.end,
55
- tagStart: tag.startOffset,
56
- tagEnd: tag.endOffset
57
- });
58
- }
59
- if (node.tagName === "base" && bases.length === 0) {
60
- const href = locatedAttribute(source, locations.href, attributes.get("href"));
61
- if (href) bases.push({
62
- href: href.value,
63
- writable: href.writable,
64
- start: href.start,
65
- end: href.end
66
- });
67
- }
68
- let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
69
- const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
70
- const dynamic = (!classAttribute && idAttribute && locations.class ? {
71
- start: locations.class.startOffset,
72
- end: locations.class.endOffset
73
- } : void 0) ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
74
- if (dynamic) dynamicAttributes.push({
75
- start: dynamic.start,
76
- end: dynamic.end
77
- });
78
- else if (classAttribute || idAttribute) {
79
- if (!classAttribute && idAttribute) {
80
- const startTag = node.sourceCodeLocation?.startTag;
81
- if (startTag && source[startTag.endOffset - 1] === ">") {
82
- const insertion = classInsertionOffset(source, startTag.startOffset, startTag.endOffset - 1);
83
- classAttribute = {
84
- value: "",
85
- start: insertion,
86
- end: insertion,
87
- synthetic: true
88
- };
89
- }
90
- }
91
- elements.push({
92
- classAttribute,
93
- idAttribute
94
- });
95
- }
96
- }
97
- }
98
- for (const child of node.childNodes ?? []) visit(child);
99
- if (node.content) visit(node.content);
100
- }
101
- visit(document);
102
- return {
103
- ...toByteOffsets(source, {
104
- links,
105
- bases,
106
- elements,
107
- dynamicAttributes
108
- }),
109
- scriptText: scriptTexts.join("\n")
110
- };
111
- }
112
- function utf8OffsetMap(source, indices) {
113
- const sorted = [...new Set(indices)].sort((left, right) => left - right);
114
- const map = /* @__PURE__ */ new Map();
115
- let lastIndex = 0;
116
- let bytes = 0;
117
- for (const index of sorted) {
118
- bytes += Buffer.byteLength(source.slice(lastIndex, index));
119
- lastIndex = index;
120
- map.set(index, bytes);
121
- }
122
- return map;
123
- }
124
- function offsetLookup(offsets) {
125
- return (index) => {
126
- const byte = offsets.get(index);
127
- if (byte === void 0) throw new Error(`No byte offset was mapped for index ${index}`);
128
- return byte;
129
- };
130
- }
131
- function toByteOffsets(source, parsed) {
132
- const offset = offsetLookup(utf8OffsetMap(source, [
133
- ...parsed.links.flatMap((link) => [
134
- link.start,
135
- link.end,
136
- link.tagStart,
137
- link.tagEnd
138
- ]),
139
- ...parsed.bases.flatMap((base) => [base.start, base.end]),
140
- ...parsed.elements.flatMap((element) => [element.classAttribute, element.idAttribute].flatMap((value) => value ? [value.start, value.end] : [])),
141
- ...parsed.dynamicAttributes.flatMap((value) => [value.start, value.end])
142
- ]));
143
- const attribute = (value) => ({
144
- ...value,
145
- start: offset(value.start),
146
- end: offset(value.end)
147
- });
148
- return {
149
- links: parsed.links.map((link) => ({
150
- ...link,
151
- start: offset(link.start),
152
- end: offset(link.end),
153
- tagStart: offset(link.tagStart),
154
- tagEnd: offset(link.tagEnd)
155
- })),
156
- bases: parsed.bases.map((base) => attribute(base)),
157
- elements: parsed.elements.map((element) => ({
158
- classAttribute: element.classAttribute && attribute(element.classAttribute),
159
- idAttribute: element.idAttribute && attribute(element.idAttribute)
160
- })),
161
- dynamicAttributes: parsed.dynamicAttributes.map((value) => attribute(value))
162
- };
163
- }
164
- function classInsertionOffset(source, floor, tagEnd) {
165
- let offset = tagEnd;
166
- if (source[offset - 1] === "/") offset -= 1;
167
- while (offset > floor && /\s/.test(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
21
  //#region src/native.ts
22
+ function errorCode(error) {
23
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
24
+ }
201
25
  const require = createRequire(import.meta.url);
202
26
  const target = {
203
27
  "darwin-arm64": "darwin-arm64",
@@ -219,7 +43,75 @@ try {
219
43
  throw new Error(`No tw-migrate native addon was found for ${target}. Reinstall the package or build it locally.`);
220
44
  }
221
45
  }
222
- const { decodeSourceMap, planBatchMigration, staticImportBindings, staticImports, staticStringExpression, validateCss } = binding;
46
+ const { collectMediaConditions, mediaProbeKey, decodeSourceMap, planBatchMigration, validateCss } = binding;
47
+ function object(value) {
48
+ return value !== null && typeof value === "object";
49
+ }
50
+ function decode(label, json, valid) {
51
+ const value = JSON.parse(json);
52
+ if (!valid(value)) throw new Error(`Invalid native ${label} result`);
53
+ return value;
54
+ }
55
+ function cssDirectives$1(source) {
56
+ try {
57
+ const value = JSON.parse(binding.collectCssDirectives(source));
58
+ return Array.isArray(value) ? value : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ function sourceImport(value) {
64
+ return object(value) && typeof value.specifier === "string" && typeof value.typeOnly === "boolean" && typeof value.dynamic === "boolean";
65
+ }
66
+ function sourceAnalysisResult(value) {
67
+ return object(value) && Array.isArray(value.imports) && value.imports.every(sourceImport) && Array.isArray(value.staticImports) && value.staticImports.every((item) => typeof item === "string") && Array.isArray(value.defaultImports) && value.defaultImports.every((item) => object(item) && typeof item.source === "string" && typeof item.local === "string") && Array.isArray(value.vueGlobPatterns) && value.vueGlobPatterns.every((item) => typeof item === "string") && Array.isArray(value.useCssModuleLocals) && value.useCssModuleLocals.every((item) => typeof item === "string") && Array.isArray(value.unboundReferences) && value.unboundReferences.every((item) => typeof item === "string") && [
68
+ value.vueGlobUnverifiable,
69
+ value.hasDynamicImport,
70
+ value.hasVueFallthroughMacro,
71
+ value.usesCssModule,
72
+ value.hasUnboundUseCssModule,
73
+ value.definesRootUseCssModule
74
+ ].every((item) => typeof item === "boolean");
75
+ }
76
+ const ANALYSIS_CACHE_LIMIT = 4096;
77
+ const sourceAnalysisCache = /* @__PURE__ */ new Map();
78
+ const stylesheetAnalysisCache = /* @__PURE__ */ new Map();
79
+ function bound(cache) {
80
+ while (cache.size > ANALYSIS_CACHE_LIMIT) {
81
+ const oldest = cache.keys().next().value;
82
+ if (oldest === void 0) break;
83
+ cache.delete(oldest);
84
+ }
85
+ }
86
+ function expressionAnalysis(path, source) {
87
+ return decode("expression analysis", binding.expressionAnalysis(path, source), (value) => object(value) && (typeof value.staticString === "string" || value.staticString === null) && (typeof value.vueModuleMember === "string" || value.vueModuleMember === null) && typeof value.usesCssModule === "boolean" && typeof value.referencesUseCssModule === "boolean" && Array.isArray(value.references) && value.references.every((item) => typeof item === "string"));
88
+ }
89
+ function sourceAnalysis(path, source) {
90
+ const cached = sourceAnalysisCache.get(path);
91
+ if (cached?.source === source) return cached.analysis;
92
+ const analysis = decode("source analysis", binding.sourceAnalysis(path, source), sourceAnalysisResult);
93
+ sourceAnalysisCache.set(path, {
94
+ source,
95
+ analysis
96
+ });
97
+ bound(sourceAnalysisCache);
98
+ return analysis;
99
+ }
100
+ function stylesheetAnalysis(path, source) {
101
+ const cached = stylesheetAnalysisCache.get(path);
102
+ if (cached?.source === source) return cached.analysis;
103
+ const analysis = decode("stylesheet analysis", binding.stylesheetAnalysis(path, source), (value) => object(value) && Array.isArray(value.references) && value.references.every((item) => typeof item === "string") && Array.isArray(value.imports) && value.imports.every((item) => object(item) && typeof item.href === "string" && typeof item.media === "string" && typeof item.start === "number" && typeof item.end === "number") && [
104
+ value.unverifiable,
105
+ value.scopeEscapesUnverifiable,
106
+ value.selectorsUnverifiable
107
+ ].every((item) => typeof item === "boolean") && Array.isArray(value.scopeEscapes) && value.scopeEscapes.every((item) => typeof item === "string") && Array.isArray(value.scopeShadowCss) && value.scopeShadowCss.every((item) => typeof item === "string") && object(value.themeTokens) && Object.values(value.themeTokens).every((item) => typeof item === "string") && Array.isArray(value.globalAtRuleIdentities) && value.globalAtRuleIdentities.every((item) => typeof item === "string") && typeof value.globalAtRulesUnverifiable === "boolean");
108
+ stylesheetAnalysisCache.set(path, {
109
+ source,
110
+ analysis
111
+ });
112
+ bound(stylesheetAnalysisCache);
113
+ return analysis;
114
+ }
223
115
  //#endregion
224
116
  //#region src/parser/style-compiler.ts
225
117
  const SASS_EXTENSIONS = /* @__PURE__ */ new Set([".scss", ".sass"]);
@@ -355,9 +247,6 @@ const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
355
247
  "node_modules"
356
248
  ]);
357
249
  const RECOVERABLE_INPUT_ERROR = "TW_MIGRATE_RECOVERABLE_INPUT:";
358
- function errorCode(error) {
359
- return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
360
- }
361
250
  function errorMessage(error) {
362
251
  return error instanceof Error ? error.message : String(error);
363
252
  }
@@ -374,15 +263,6 @@ function isStylesheetModule(path) {
374
263
  const syntax = stylesheetSyntax(path);
375
264
  return syntax !== void 0 && path.endsWith(`.module.${syntax}`);
376
265
  }
377
- function sourceReferencesStyle(file, stylePath) {
378
- let importPath = normalizedRelativePath(dirname(file.path), stylePath);
379
- if (!importPath.startsWith(".")) importPath = `./${importPath}`;
380
- return [
381
- `'${importPath}'`,
382
- `"${importPath}"`,
383
- `\`${importPath}\``
384
- ].some((literal) => file.source.includes(literal));
385
- }
386
266
  function isProjectInput(workspaceRoot, path) {
387
267
  return isWithin(workspaceRoot, path) && !relative(workspaceRoot, path).split(/[\\/]/).includes("node_modules");
388
268
  }
@@ -427,24 +307,26 @@ function isIntegrityError(error) {
427
307
  function normalizedRelativePath(root, path) {
428
308
  return relative(root, path).split(sep).join("/");
429
309
  }
430
- function maskCssComments(source) {
431
- return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, " "));
432
- }
433
310
  function indexStylesheetDependents(styleSources) {
434
311
  const dependents = /* @__PURE__ */ new Map();
435
- for (const [path, rawSource] of styleSources) {
436
- const source = maskCssComments(rawSource);
437
- const references = [
438
- ...[...source.matchAll(/composes\s*:[^;{}]*?\bfrom\s+["']([^"']+)["']/g)].map((match) => match[1]),
439
- ...[...source.matchAll(/@(?:use|forward)\s+["']([^"']+)["']/g)].map((match) => match[1]),
440
- ...[...source.matchAll(/@import\s+(?:\([^)]*\)\s*)?(?:([^;{}]+);|([^;{}\r\n]+))/g)].flatMap((statement) => [...(statement[1] ?? statement[2]).matchAll(/["']([^"']+)["']/g)].map((match) => match[1])),
441
- ...[...source.matchAll(/@import\s+url\(\s*([^"'()\s]+)\s*\)/g)].map((match) => match[1])
442
- ];
443
- for (const reference of new Set(references)) for (const target of stylesheetReferenceTargets(path, reference, styleSources)) {
444
- if (target === path || !isStylesheetModule(target) && !isPreprocessorPath(target)) continue;
445
- const paths = dependents.get(target) ?? [];
446
- paths.push(path);
447
- dependents.set(target, paths);
312
+ const allPossibleTargets = [...styleSources.keys()].filter((path) => isStylesheetModule(path) || isPreprocessorPath(path));
313
+ for (const [path, source] of styleSources) {
314
+ let references;
315
+ try {
316
+ const analysis = stylesheetAnalysis(path, source);
317
+ references = analysis.references;
318
+ if (analysis.unverifiable) references = [...references, ...allPossibleTargets];
319
+ } catch {
320
+ references = allPossibleTargets;
321
+ }
322
+ for (const reference of new Set(references)) {
323
+ const targets = styleSources.has(reference) ? [reference] : stylesheetReferenceTargets(path, reference, styleSources);
324
+ for (const target of targets) {
325
+ if (target === path || !isStylesheetModule(target) && !isPreprocessorPath(target)) continue;
326
+ const paths = dependents.get(target) ?? [];
327
+ paths.push(path);
328
+ dependents.set(target, paths);
329
+ }
448
330
  }
449
331
  }
450
332
  for (const [target, paths] of dependents) dependents.set(target, [...new Set(paths)].sort());
@@ -466,63 +348,8 @@ function stylesheetReferenceTargets(importer, reference, styleSources) {
466
348
  join(target, `index${syntax}`)
467
349
  ])).filter((path) => styleSources.has(path));
468
350
  }
469
- function cssImports(source) {
470
- const imports = [];
471
- const masked = maskCssComments(source);
472
- let depth = 0;
473
- let quote;
474
- let importsAllowed = true;
475
- for (let index = 0; index < masked.length; index += 1) {
476
- const character = masked[index];
477
- if (quote) {
478
- if (character === "\\") index += 1;
479
- else if (character === quote) quote = void 0;
480
- continue;
481
- }
482
- if (character === "\"" || character === "'") {
483
- quote = character;
484
- continue;
485
- }
486
- if (character === "{") {
487
- depth += 1;
488
- importsAllowed = false;
489
- continue;
490
- }
491
- if (character === "}") {
492
- depth = Math.max(0, depth - 1);
493
- continue;
494
- }
495
- if (!importsAllowed || depth !== 0 || masked.slice(index, index + 7).toLowerCase() !== "@import" || /[-\w]/.test(masked[index + 7] ?? "")) continue;
496
- let end = index + 7;
497
- let importQuote;
498
- let parentheses = 0;
499
- for (; end < masked.length; end += 1) {
500
- const next = masked[end];
501
- if (importQuote) {
502
- if (next === "\\") end += 1;
503
- else if (next === importQuote) importQuote = void 0;
504
- } else if (next === "\"" || next === "'") importQuote = next;
505
- else if (next === "(") parentheses += 1;
506
- else if (next === ")") parentheses = Math.max(0, parentheses - 1);
507
- else if (next === ";" && parentheses === 0) break;
508
- }
509
- if (end >= masked.length) continue;
510
- const statement = masked.slice(index, end + 1);
511
- const match = /^@import\s+(?:url\(\s*)?(?:["']([^"']+)["']|([^"'()\s;]+))\s*\)?\s*([^;]*);$/i.exec(statement);
512
- if (match) imports.push({
513
- href: match[1] ?? match[2],
514
- media: match[3].trim(),
515
- start: index,
516
- end: end + 1
517
- });
518
- index = end;
519
- }
520
- const offsets = utf8OffsetMap(source, imports.flatMap((imported) => [imported.start, imported.end]));
521
- return imports.map((imported) => ({
522
- ...imported,
523
- start: offsets.get(imported.start) ?? imported.start,
524
- end: offsets.get(imported.end) ?? imported.end
525
- }));
351
+ function cssImports(path, source) {
352
+ return stylesheetAnalysis(path, source).imports;
526
353
  }
527
354
  //#endregion
528
355
  //#region src/discovery.ts
@@ -593,6 +420,28 @@ async function findGitRoot(cwd) {
593
420
  return;
594
421
  }
595
422
  }
423
+ async function scannerIgnoredPaths(workspaceRoot, paths) {
424
+ if (paths.length === 0 || await findGitRoot(workspaceRoot) !== workspaceRoot) return /* @__PURE__ */ new Set();
425
+ return new Promise((resolvePromise) => {
426
+ execFile("git", [
427
+ "check-ignore",
428
+ "--no-index",
429
+ "-z",
430
+ "--stdin"
431
+ ], {
432
+ cwd: workspaceRoot,
433
+ maxBuffer: 67108864
434
+ }, (error, stdout) => {
435
+ if (error !== null && error.code !== 1) {
436
+ resolvePromise(new Set(paths));
437
+ return;
438
+ }
439
+ const ignored = /* @__PURE__ */ new Set();
440
+ for (const entry of stdout.split("\0")) if (entry) ignored.add(resolve(workspaceRoot, entry));
441
+ resolvePromise(ignored);
442
+ }).stdin?.end(paths.map((path) => `${relative(workspaceRoot, path)}\0`).join(""));
443
+ });
444
+ }
596
445
  async function isIgnoredByGit(gitRoot, path) {
597
446
  if (gitRoot === path) return false;
598
447
  try {
@@ -617,7 +466,7 @@ async function discoverGitFiles(root) {
617
466
  "."
618
467
  ], {
619
468
  cwd: root,
620
- maxBuffer: 64 * 1024 * 1024
469
+ maxBuffer: 67108864
621
470
  });
622
471
  const paths = stdout.split("\0").filter(Boolean).map((path) => resolve(root, path)).filter((path) => !hasIgnoredDirectory(root, path) && isRelevantDiscoveredFile(path));
623
472
  return (await Promise.all(paths.map(async (path) => {
@@ -626,37 +475,509 @@ async function discoverGitFiles(root) {
626
475
  } catch {
627
476
  return;
628
477
  }
629
- }))).flatMap((path) => path ? [path] : []).sort();
478
+ }))).flatMap((path) => path ? [path] : []).sort();
479
+ }
480
+ async function discoverPackageRoots(workspaceRoot, paths) {
481
+ const roots = paths.filter((path) => basename(path) === "package.json").map(dirname);
482
+ try {
483
+ await readFile(join(workspaceRoot, "package.json"), "utf8");
484
+ roots.push(workspaceRoot);
485
+ } catch (error) {
486
+ if (errorCode(error) !== "ENOENT") throw error;
487
+ }
488
+ return [...new Set(roots)].sort();
489
+ }
490
+ function hasIgnoredDirectory(root, path) {
491
+ return relative(root, path).split(/[\\/]/).some((part) => IGNORED_DIRECTORIES.has(part));
492
+ }
493
+ function isRelevantDiscoveredFile(path) {
494
+ return basename(path) === "package.json" || isStylesheetPath(path) || SOURCE_EXTENSIONS.has(extname(path));
495
+ }
496
+ async function collectFiles(root, include) {
497
+ const files = [];
498
+ async function visit(directory) {
499
+ const entries = await readdir(directory, { withFileTypes: true });
500
+ for (const entry of entries) {
501
+ const path = join(directory, entry.name);
502
+ if (entry.isDirectory()) {
503
+ if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(path);
504
+ } else if (entry.isFile() && include(path)) files.push(path);
505
+ }
506
+ }
507
+ await visit(root);
508
+ return files;
509
+ }
510
+ //#endregion
511
+ //#region src/parser/html.ts
512
+ const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
513
+ function parseHtmlSource(path, source) {
514
+ const errors = [];
515
+ const document = parse(source, {
516
+ sourceCodeLocationInfo: true,
517
+ scriptingEnabled: false,
518
+ onParseError(error) {
519
+ if (error.code !== "missing-doctype") errors.push(error);
520
+ }
521
+ });
522
+ if (errors.length > 0) throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
523
+ const links = [];
524
+ const bases = [];
525
+ const elements = [];
526
+ const dynamicAttributes = [];
527
+ const scriptTexts = [];
528
+ let hasStyle = false;
529
+ const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
530
+ function visit(node) {
531
+ if (node.tagName === "style") hasStyle = true;
532
+ if (node.tagName === "script") {
533
+ for (const child of node.childNodes ?? []) if (child.nodeName === "#text" && child.value) scriptTexts.push(child.value);
534
+ }
535
+ if (node.tagName) {
536
+ const attributes = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
537
+ const locations = node.sourceCodeLocation?.attrs;
538
+ if (locations) {
539
+ if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
540
+ const href = locatedAttribute(source, locations.href, attributes.get("href"));
541
+ const media = locatedAttribute(source, locations.media, attributes.get("media"));
542
+ const tag = node.sourceCodeLocation?.startTag ?? node.sourceCodeLocation;
543
+ if (href && tag) links.push({
544
+ href: href.value,
545
+ media: media?.value ?? "",
546
+ start: href.start,
547
+ end: href.end,
548
+ tagStart: tag.startOffset,
549
+ tagEnd: tag.endOffset
550
+ });
551
+ }
552
+ if (node.tagName === "base" && bases.length === 0) {
553
+ const href = locatedAttribute(source, locations.href, attributes.get("href"));
554
+ if (href) bases.push({
555
+ href: href.value,
556
+ writable: href.writable,
557
+ start: href.start,
558
+ end: href.end
559
+ });
560
+ }
561
+ let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
562
+ const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
563
+ const dynamic = (!classAttribute && idAttribute && locations.class ? {
564
+ start: locations.class.startOffset,
565
+ end: locations.class.endOffset
566
+ } : void 0) ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
567
+ if (dynamic) dynamicAttributes.push({
568
+ start: dynamic.start,
569
+ end: dynamic.end
570
+ });
571
+ else if (classAttribute || idAttribute) {
572
+ if (!classAttribute && idAttribute) {
573
+ const startTag = node.sourceCodeLocation?.startTag;
574
+ if (startTag && source[startTag.endOffset - 1] === ">") {
575
+ const insertion = classInsertionOffset(source, startTag.startOffset, startTag.endOffset - 1);
576
+ classAttribute = {
577
+ value: "",
578
+ start: insertion,
579
+ end: insertion,
580
+ synthetic: true
581
+ };
582
+ }
583
+ }
584
+ elements.push({
585
+ classAttribute,
586
+ idAttribute
587
+ });
588
+ }
589
+ }
590
+ }
591
+ for (const child of node.childNodes ?? []) visit(child);
592
+ if (node.content) visit(node.content);
593
+ }
594
+ visit(document);
595
+ return {
596
+ ...toByteOffsets(source, {
597
+ links,
598
+ bases,
599
+ elements,
600
+ dynamicAttributes
601
+ }),
602
+ scriptText: scriptTexts.join("\n"),
603
+ hasStyle
604
+ };
605
+ }
606
+ function utf8OffsetMap(source, indices) {
607
+ const sorted = [...new Set(indices)].sort((left, right) => left - right);
608
+ const map = /* @__PURE__ */ new Map();
609
+ let lastIndex = 0;
610
+ let bytes = 0;
611
+ for (const index of sorted) {
612
+ bytes += Buffer.byteLength(source.slice(lastIndex, index));
613
+ lastIndex = index;
614
+ map.set(index, bytes);
615
+ }
616
+ return map;
617
+ }
618
+ function offsetLookup(offsets) {
619
+ return (index) => {
620
+ const byte = offsets.get(index);
621
+ if (byte === void 0) throw new Error(`No byte offset was mapped for index ${index}`);
622
+ return byte;
623
+ };
624
+ }
625
+ function toByteOffsets(source, parsed) {
626
+ const offset = offsetLookup(utf8OffsetMap(source, [
627
+ ...parsed.links.flatMap((link) => [
628
+ link.start,
629
+ link.end,
630
+ link.tagStart,
631
+ link.tagEnd
632
+ ]),
633
+ ...parsed.bases.flatMap((base) => [base.start, base.end]),
634
+ ...parsed.elements.flatMap((element) => [element.classAttribute, element.idAttribute].flatMap((value) => value ? [value.start, value.end] : [])),
635
+ ...parsed.dynamicAttributes.flatMap((value) => [value.start, value.end])
636
+ ]));
637
+ const attribute = (value) => ({
638
+ ...value,
639
+ start: offset(value.start),
640
+ end: offset(value.end)
641
+ });
642
+ return {
643
+ links: parsed.links.map((link) => ({
644
+ ...link,
645
+ start: offset(link.start),
646
+ end: offset(link.end),
647
+ tagStart: offset(link.tagStart),
648
+ tagEnd: offset(link.tagEnd)
649
+ })),
650
+ bases: parsed.bases.map((base) => attribute(base)),
651
+ elements: parsed.elements.map((element) => ({
652
+ classAttribute: element.classAttribute && attribute(element.classAttribute),
653
+ idAttribute: element.idAttribute && attribute(element.idAttribute)
654
+ })),
655
+ dynamicAttributes: parsed.dynamicAttributes.map((value) => attribute(value))
656
+ };
657
+ }
658
+ function classInsertionOffset(source, floor, tagEnd) {
659
+ let offset = tagEnd;
660
+ if (source[offset - 1] === "/") offset -= 1;
661
+ while (offset > floor && /\s/.test(source[offset - 1] ?? "")) offset -= 1;
662
+ return offset;
663
+ }
664
+ function stylesheetRel(value = "") {
665
+ const tokens = value.toLowerCase().split(/\s+/);
666
+ return tokens.includes("stylesheet") && !tokens.includes("alternate");
667
+ }
668
+ function locatedAttribute(source, location, parsedValue) {
669
+ if (!location || parsedValue === void 0) return void 0;
670
+ const raw = source.slice(location.startOffset, location.endOffset);
671
+ const equals = raw.indexOf("=");
672
+ if (equals < 0) return void 0;
673
+ let start = equals + 1;
674
+ while (/\s/.test(raw[start] ?? "")) start += 1;
675
+ const quote = raw[start];
676
+ let end;
677
+ if (quote === "\"" || quote === "'") {
678
+ start += 1;
679
+ end = raw.lastIndexOf(quote);
680
+ if (end < start) return void 0;
681
+ } else {
682
+ end = start;
683
+ while (end < raw.length && !/\s/.test(raw[end] ?? "")) end += 1;
684
+ }
685
+ const value = raw.slice(start, end);
686
+ return {
687
+ value: value.includes("&") ? parsedValue : value,
688
+ writable: !value.includes("&"),
689
+ start: location.startOffset + start,
690
+ end: location.startOffset + end
691
+ };
692
+ }
693
+ //#endregion
694
+ //#region src/plan/entry.ts
695
+ function emitsUtilities(specifier) {
696
+ return specifier === "tailwindcss" || specifier === "tailwindcss/utilities" || specifier === "tailwindcss/utilities.css";
697
+ }
698
+ function hasTailwindImport(source) {
699
+ return cssDirectives(source)?.some((directive) => directive.kind === "import" && directive.specifier !== null && emitsUtilities(directive.specifier)) ?? false;
700
+ }
701
+ function tailwindEntryCatalog(styleSources, pathOwners) {
702
+ const catalog = /* @__PURE__ */ new Map();
703
+ for (const [path, source] of styleSources) {
704
+ if (extname(path) !== ".css") continue;
705
+ if (!hasTailwindImport(source)) continue;
706
+ const owner = pathOwners.get(path);
707
+ if (owner === void 0) continue;
708
+ const entries = catalog.get(owner) ?? [];
709
+ entries.push(path);
710
+ catalog.set(owner, entries);
711
+ }
712
+ for (const entries of catalog.values()) entries.sort();
713
+ return catalog;
714
+ }
715
+ function cssDirectives(source) {
716
+ return cssDirectives$1(source)?.filter((directive) => directive !== null && typeof directive === "object" && "kind" in directive && typeof directive.kind === "string") ?? null;
717
+ }
718
+ function entryGraphSheets(entry, styleSources) {
719
+ const sheets = [];
720
+ const seen = /* @__PURE__ */ new Set();
721
+ const pending = [entry];
722
+ while (pending.length > 0) {
723
+ const path = pending.pop();
724
+ if (path === void 0 || seen.has(path)) continue;
725
+ seen.add(path);
726
+ const raw = styleSources.get(path);
727
+ if (raw === void 0) return null;
728
+ const directives = cssDirectives(raw);
729
+ if (directives === null) return null;
730
+ sheets.push({
731
+ path,
732
+ directives
733
+ });
734
+ for (const directive of directives) {
735
+ if (directive.kind !== "import") continue;
736
+ if (directive.specifier === null) return null;
737
+ const spec = directive.specifier;
738
+ if (directive.tailwind) continue;
739
+ if (!spec.startsWith(".") && !spec.startsWith("/")) {
740
+ let resolved;
741
+ try {
742
+ resolved = createRequire(join(dirname(path), "package.json")).resolve(spec);
743
+ } catch {
744
+ return null;
745
+ }
746
+ if (/[\\/]node_modules[\\/]/.test(resolved)) continue;
747
+ pending.push(resolved);
748
+ continue;
749
+ }
750
+ const resolved = resolve(dirname(path), spec);
751
+ pending.push(styleSources.has(resolved) ? resolved : `${resolved}.css`);
752
+ }
753
+ }
754
+ return sheets;
755
+ }
756
+ function scanProof(options) {
757
+ const sheets = entryGraphSheets(options.entry, new Map([...options.styleSources ?? [], [options.entry, options.entrySource]]));
758
+ if (sheets === null) return null;
759
+ let literal = false;
760
+ let automaticDisabled = false;
761
+ for (const { path, directives } of sheets) {
762
+ const base = dirname(path);
763
+ const coversChild = (scope) => {
764
+ if (scope.includes("*")) return false;
765
+ return isWithin(resolve(base, scope), options.packageRoot);
766
+ };
767
+ const excludesChild = (scope) => {
768
+ const stripped = resolve(base, scope.replace(/\*.*$/, ""));
769
+ if (scope.includes("*")) return options.packageRoot.startsWith(stripped) || stripped.startsWith(options.packageRoot);
770
+ return isWithin(stripped, options.packageRoot) || isWithin(options.packageRoot, stripped);
771
+ };
772
+ for (const directive of directives) if (directive.kind === "source") {
773
+ if (directive.inline) continue;
774
+ if (directive.unreadable || directive.scope === null) return null;
775
+ if (directive.not) {
776
+ if (excludesChild(directive.scope)) return null;
777
+ } else if (coversChild(directive.scope)) literal = true;
778
+ } else if (directive.kind === "import" && directive.tailwind) {
779
+ if (directive.sourceUnreadable) return null;
780
+ if (directive.source === "none") automaticDisabled = true;
781
+ else if (directive.source !== null) if (coversChild(directive.source)) literal = true;
782
+ else automaticDisabled = true;
783
+ }
784
+ }
785
+ if (literal) return "literal";
786
+ if (automaticDisabled) return null;
787
+ return isWithin(dirname(options.entry), options.packageRoot) ? "automatic" : null;
788
+ }
789
+ const OPAQUE_SOURCE_IMPORT = {
790
+ specifier: "#unverifiable-source",
791
+ typeOnly: false,
792
+ dynamic: true
793
+ };
794
+ function sourceImports(file) {
795
+ const extension = extname(file.path);
796
+ if (extension === ".html") return [];
797
+ if (extension === ".vue") return file.sourceImports && !file.sourceImportsUnverifiable ? file.sourceImports : [OPAQUE_SOURCE_IMPORT];
798
+ try {
799
+ return sourceAnalysis(file.path, file.source).imports;
800
+ } catch {
801
+ return [OPAQUE_SOURCE_IMPORT];
802
+ }
803
+ }
804
+ const RESOLVABLE_EXTENSIONS = [
805
+ ".ts",
806
+ ".tsx",
807
+ ".mts",
808
+ ".cts",
809
+ ".js",
810
+ ".jsx",
811
+ ".mjs",
812
+ ".cjs",
813
+ ".vue"
814
+ ];
815
+ function resolveImport(fromDir, spec, known) {
816
+ if (!spec.startsWith(".")) return void 0;
817
+ const base = resolve(fromDir, spec);
818
+ const emitted = base.match(/^(.*)\.(?:mjs|cjs|js|jsx)$/);
819
+ return [
820
+ base,
821
+ ...emitted ? [
822
+ ".ts",
823
+ ".tsx",
824
+ ".mts",
825
+ ".cts"
826
+ ].map((extension) => `${emitted[1]}${extension}`) : [],
827
+ ...RESOLVABLE_EXTENSIONS.map((extension) => `${base}${extension}`),
828
+ ...RESOLVABLE_EXTENSIONS.map((extension) => join(base, `index${extension}`))
829
+ ].find((candidate) => known.has(candidate));
830
+ }
831
+ function scriptLikeSpecifier(spec) {
832
+ if (!spec.startsWith(".")) return false;
833
+ const trimmed = spec.split(/[?#]/, 1)[0];
834
+ return /\.(?:m?[jt]sx?|c[jt]s|vue)$/.test(trimmed) || !/\.[^/.]+$/.test(trimmed);
835
+ }
836
+ function externalPackageSpecifier(spec) {
837
+ return /^(?:node:[\w/.-]+|(?:@[a-z0-9~][\w.~-]*\/)?[a-z0-9~][\w.~-]*(?:\/.*)?)$/.test(spec);
838
+ }
839
+ function importEdges(files) {
840
+ const known = new Set(files.map((file) => file.path));
841
+ const edges = /* @__PURE__ */ new Map();
842
+ for (const file of files) {
843
+ if (extname(file.path) === ".html") continue;
844
+ const targets = [];
845
+ for (const record of sourceImports(file)) {
846
+ if (record.typeOnly) continue;
847
+ const resolved = resolveImport(dirname(file.path), record.specifier, known);
848
+ if (resolved !== void 0) targets.push(resolved);
849
+ }
850
+ edges.set(file.path, targets);
851
+ }
852
+ return edges;
853
+ }
854
+ function reachableFrom(starts, edges) {
855
+ const reached = new Set(starts);
856
+ const pending = [...reached];
857
+ while (pending.length > 0) {
858
+ const current = pending.pop();
859
+ if (current === void 0) break;
860
+ for (const target of edges.get(current) ?? []) if (!reached.has(target)) {
861
+ reached.add(target);
862
+ pending.push(target);
863
+ }
864
+ }
865
+ return reached;
866
+ }
867
+ function exposedFiles(packageRoot, packageJson, files, inboundSeeds) {
868
+ const known = new Set(files.map((file) => file.path));
869
+ const specs = [];
870
+ const collect = (value) => {
871
+ if (typeof value === "string") {
872
+ if (value.includes("*")) return "all";
873
+ specs.push(value);
874
+ return;
875
+ }
876
+ if (value !== null && typeof value === "object") for (const [key, child] of Object.entries(value)) {
877
+ if (key.includes("*")) return "all";
878
+ if (collect(child) === "all") return "all";
879
+ }
880
+ };
881
+ for (const field of [
882
+ "main",
883
+ "module",
884
+ "exports",
885
+ "browser"
886
+ ]) if (collect(packageJson[field]) === "all") return "all";
887
+ if (packageJson.exports === void 0 && packageJson.private !== true) return "all";
888
+ const declared = specs.length > 0;
889
+ const entryFileSeeds = [...inboundSeeds];
890
+ if (!declared) specs.push("./index");
891
+ const entryFiles = [...entryFileSeeds];
892
+ for (const spec of specs) {
893
+ const resolved = resolveImport(packageRoot, spec.startsWith(".") ? spec : `./${spec}`, known);
894
+ if (resolved === void 0) {
895
+ if (declared) return "all";
896
+ continue;
897
+ }
898
+ entryFiles.push(resolved);
899
+ }
900
+ const byPath = new Map(files.map((file) => [file.path, file]));
901
+ const exposed = new Set(entryFiles);
902
+ const pending = [...entryFiles];
903
+ while (pending.length > 0) {
904
+ const current = pending.pop();
905
+ if (current === void 0) break;
906
+ const file = byPath.get(current);
907
+ if (file === void 0 || extname(file.path) === ".html") continue;
908
+ for (const record of sourceImports(file)) {
909
+ if (record.typeOnly) continue;
910
+ const resolved = resolveImport(dirname(file.path), record.specifier, known);
911
+ if (resolved === void 0) {
912
+ if (scriptLikeSpecifier(record.specifier) || !record.specifier.startsWith(".") && !externalPackageSpecifier(record.specifier)) return "all";
913
+ continue;
914
+ }
915
+ if (!exposed.has(resolved)) {
916
+ exposed.add(resolved);
917
+ pending.push(resolved);
918
+ }
919
+ }
920
+ }
921
+ return exposed;
922
+ }
923
+ function proveSharedEntry(options) {
924
+ const scan = scanProof(options);
925
+ if (scan === null) return null;
926
+ const ownedSources = options.packageSources.filter((file) => options.owned(file.path));
927
+ const loaders = ownedSources.filter((file) => options.writable(file.path)).filter((file) => (file.htmlStylesheets ?? []).some((context) => context.cssPath === options.entry) || htmlLinksEntry(file, options.entry) || extname(file.path) !== ".html" && importsEntry(file, options.entry)).map((file) => file.path);
928
+ if (loaders.length === 0) return null;
929
+ const reachable = reachableFrom(loaders, importEdges(ownedSources));
930
+ const ownedPaths = new Set(ownedSources.map((file) => file.path));
931
+ const packageName = typeof options.packageJson.name === "string" ? options.packageJson.name : void 0;
932
+ const resolveInbound = (fromDir, spec) => {
933
+ const relative = resolveImport(fromDir, spec, ownedPaths);
934
+ if (relative !== void 0) return relative;
935
+ if (packageName !== void 0 && spec.startsWith(`${packageName}/`)) return resolveImport(options.packageRoot, `./${spec.slice(packageName.length + 1)}`, ownedPaths);
936
+ };
937
+ const inboundSeeds = [];
938
+ let foreignUnverifiable = false;
939
+ for (const file of options.packageSources) {
940
+ if (options.owned(file.path) || extname(file.path) === ".html") continue;
941
+ if (importsEntry(file, options.entry)) continue;
942
+ const records = sourceImports(file);
943
+ if (records.includes(OPAQUE_SOURCE_IMPORT)) {
944
+ foreignUnverifiable = true;
945
+ continue;
946
+ }
947
+ for (const record of records) {
948
+ if (record.typeOnly) continue;
949
+ const resolved = resolveInbound(dirname(file.path), record.specifier);
950
+ if (resolved !== void 0) inboundSeeds.push(resolved);
951
+ }
952
+ }
953
+ const exposed = foreignUnverifiable ? "all" : exposedFiles(options.packageRoot, options.packageJson, ownedSources, inboundSeeds);
954
+ const provenConsumer = (consumer) => options.owned(consumer.path) && !sourceImports(consumer).includes(OPAQUE_SOURCE_IMPORT) && reachable.has(consumer.path) && (exposed === "all" ? false : !exposed.has(consumer.path)) && (scan === "literal" || !options.ignoredPaths.has(consumer.path));
955
+ return {
956
+ provenConsumer,
957
+ provenStyle: (consumers) => consumers.length > 0 && consumers.every(provenConsumer)
958
+ };
630
959
  }
631
- async function discoverPackageRoots(workspaceRoot, paths) {
632
- const roots = paths.filter((path) => basename(path) === "package.json").map(dirname);
960
+ function htmlLinksEntry(file, entry) {
961
+ if (extname(file.path) !== ".html") return false;
633
962
  try {
634
- await readFile(join(workspaceRoot, "package.json"), "utf8");
635
- roots.push(workspaceRoot);
636
- } catch (error) {
637
- if (errorCode(error) !== "ENOENT") throw error;
963
+ const parsed = parseHtmlSource(file.path, file.source);
964
+ if (parsed.bases.length > 0) return false;
965
+ return parsed.links.some((link) => {
966
+ const media = link.media.trim().toLowerCase();
967
+ if (media !== "" && media !== "all") return false;
968
+ const href = link.href.split(/[?#]/, 1)[0];
969
+ if (!href || /^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return false;
970
+ return resolve(dirname(file.path), href) === entry;
971
+ });
972
+ } catch {
973
+ return false;
638
974
  }
639
- return [...new Set(roots)].sort();
640
- }
641
- function hasIgnoredDirectory(root, path) {
642
- return relative(root, path).split(/[\\/]/).some((part) => IGNORED_DIRECTORIES.has(part));
643
975
  }
644
- function isRelevantDiscoveredFile(path) {
645
- return basename(path) === "package.json" || isStylesheetPath(path) || SOURCE_EXTENSIONS.has(extname(path));
976
+ function importsStylesheet(file, stylePath) {
977
+ return sourceImports(file).some((record) => record === OPAQUE_SOURCE_IMPORT && file.source.includes(basename(stylePath)) || !record.typeOnly && record.specifier.startsWith(".") && resolve(dirname(file.path), record.specifier) === stylePath);
646
978
  }
647
- async function collectFiles(root, include) {
648
- const files = [];
649
- async function visit(directory) {
650
- const entries = await readdir(directory, { withFileTypes: true });
651
- for (const entry of entries) {
652
- const path = join(directory, entry.name);
653
- if (entry.isDirectory()) {
654
- if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(path);
655
- } else if (entry.isFile() && include(path)) files.push(path);
656
- }
657
- }
658
- await visit(root);
659
- return files;
979
+ function importsEntry(file, entry) {
980
+ return sourceImports(file).some((record) => !record.typeOnly && !record.dynamic && record.specifier.startsWith(".") && resolve(dirname(file.path), record.specifier) === entry);
660
981
  }
661
982
  //#endregion
662
983
  //#region src/plan/html.ts
@@ -783,7 +1104,7 @@ async function collectHtmlStyleContexts(state) {
783
1104
  analyzable: true
784
1105
  });
785
1106
  if (extname(state.path) !== ".css") return;
786
- for (const imported of cssImports(source)) {
1107
+ for (const imported of cssImports(state.path, source)) {
787
1108
  const variants = mediaVariants(imported.media);
788
1109
  if (variants === void 0) {
789
1110
  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.`));
@@ -806,7 +1127,7 @@ function inferredPreprocessorPath(state) {
806
1127
  return matches.length === 1 ? matches[0] : void 0;
807
1128
  }
808
1129
  function addInferredPreprocessorContext(state) {
809
- if (state.styleSources.has(state.path) && state.sourceFiles.some((file) => extname(file.path) !== ".html" && sourceReferencesStyle(file, state.path))) return false;
1130
+ if (state.styleSources.has(state.path) && state.sourceFiles.some((file) => extname(file.path) !== ".html" && importsStylesheet(file, state.path))) return false;
810
1131
  const path = inferredPreprocessorPath(state);
811
1132
  if (!path) return false;
812
1133
  state.generatedPaths.add(state.path);
@@ -857,22 +1178,14 @@ function htmlWarning(code, file, start, end, message) {
857
1178
  }
858
1179
  //#endregion
859
1180
  //#region src/tailwind.ts
860
- function resolveTailwindEntry(stylePaths, styleSources, configuredPath) {
861
- const entries = stylePaths.filter((path) => {
862
- if (extname(path) !== ".css") return false;
863
- const source = maskCssComments(styleSources.get(path) ?? "");
864
- return /@import\s+["']tailwindcss(?:\/[^"']*)?["']/.test(source);
865
- });
866
- if (configuredPath) return {
867
- path: configuredPath,
868
- entries
869
- };
1181
+ function findTailwindEntries(stylePaths, styleSources) {
1182
+ return stylePaths.filter((path) => extname(path) === ".css" && importSpecifiers(styleSources.get(path) ?? "").some((specifier) => specifier === "tailwindcss" || specifier.startsWith("tailwindcss/")));
1183
+ }
1184
+ function selectTailwindEntry(entries, configuredPath) {
1185
+ if (configuredPath) return configuredPath;
870
1186
  if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
871
1187
  if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
872
- return {
873
- path: entries[0],
874
- entries
875
- };
1188
+ return entries[0];
876
1189
  }
877
1190
  async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
878
1191
  const projectRequire = createRequire(join(packageRoot, "package.json"));
@@ -911,45 +1224,265 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
911
1224
  base: dirname(path)
912
1225
  };
913
1226
  };
1227
+ const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
1228
+ const graphSources = [];
914
1229
  const themeTokens = {
915
- ...extractThemeTokens(await readFile(join(dirname(packagePath), "theme.css"), "utf8")),
916
- ...await extractThemeTokensFromGraph(css, base, loadStylesheet)
1230
+ ...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
1231
+ ...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources)
917
1232
  };
1233
+ graphSources.push({
1234
+ path: tailwindCss,
1235
+ source: css
1236
+ });
1237
+ const designSystem = await loadDesignSystem(css, {
1238
+ base,
1239
+ loadModule,
1240
+ loadStylesheet
1241
+ });
1242
+ const loadWith = (replacement) => loadDesignSystem(replacement, {
1243
+ base,
1244
+ loadModule,
1245
+ loadStylesheet
1246
+ });
918
1247
  return {
919
- designSystem: await loadDesignSystem(css, {
920
- base,
921
- loadModule,
922
- loadStylesheet
923
- }),
1248
+ designSystem,
924
1249
  css,
925
1250
  path: tailwindCss,
926
- themeTokens
1251
+ themeTokens,
1252
+ graphSources,
1253
+ loadWith
927
1254
  };
928
1255
  }
929
- function extractThemeTokens(css) {
930
- const tokens = {};
931
- for (const block of css.matchAll(/@theme[^{]*\{([^}]*)\}/gs)) for (const match of block[1].matchAll(/--([\w-]+):\s*([^;{}]+);/g)) tokens[match[1]] = match[2].trim();
932
- return tokens;
1256
+ function extractThemeTokens(key, css) {
1257
+ return stylesheetAnalysis(key, css).themeTokens;
933
1258
  }
934
- async function extractThemeTokensFromGraph(css, base, loadStylesheet, seen = /* @__PURE__ */ new Set()) {
1259
+ function importSpecifiers(css) {
1260
+ return (cssDirectives$1(css) ?? []).flatMap((directive) => directive !== null && typeof directive === "object" && "kind" in directive && directive.kind === "import" && "specifier" in directive && typeof directive.specifier === "string" ? [directive.specifier] : []);
1261
+ }
1262
+ async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, seen = /* @__PURE__ */ new Set()) {
935
1263
  const tokens = {};
936
- for (const match of css.matchAll(/@import\s+["']([^"']+)["']/g)) {
937
- const key = `${base}\0${match[1]}`;
1264
+ for (const spec of importSpecifiers(css)) {
1265
+ const key = `${base}\0${spec}`;
938
1266
  if (seen.has(key)) continue;
939
1267
  seen.add(key);
940
- const loaded = await loadStylesheet(match[1], base);
941
- Object.assign(tokens, await extractThemeTokensFromGraph(loaded.content, loaded.base, loadStylesheet, seen));
1268
+ const loaded = await loadStylesheet(spec, base);
1269
+ if (spec !== "tailwindcss" && !spec.startsWith("tailwindcss/")) graphSources.push({
1270
+ path: key,
1271
+ source: loaded.content
1272
+ });
1273
+ Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, seen));
942
1274
  }
943
- return Object.assign(tokens, extractThemeTokens(css));
1275
+ return Object.assign(tokens, extractThemeTokens(`${identity}.theme.css`, css));
944
1276
  }
945
- function invalidCandidates(tailwind, candidates) {
946
- const generated = tailwind.designSystem.candidatesToCss(candidates);
1277
+ function invalidCandidates(system, candidates) {
1278
+ const generated = system.candidatesToCss(candidates);
947
1279
  return candidates.filter((_, index) => generated[index] === null);
948
1280
  }
949
1281
  //#endregion
1282
+ //#region src/plan/media.ts
1283
+ const GENERATED_NAMESPACE = "twm-media";
1284
+ /**
1285
+ * Resolve one fixed name per key across a complete entry group.
1286
+ *
1287
+ * Resolution follows the RFC order: a built-in candidate with a verified
1288
+ * effective expansion, an existing breakpoint matched by exact value and
1289
+ * verified the same way, an adopted definition whose name, normalized
1290
+ * meaning, and effective expansion equal what the migration would emit,
1291
+ * the readable generated name, the digest name, and finally the
1292
+ * arbitrary-variant fallback. Reservation is ownership-blind:
1293
+ * authored names with any different meaning, active theme names, explicit
1294
+ * reserved names such as inert scanned candidates, and any name the design
1295
+ * system resolves all block a generated name.
1296
+ */
1297
+ function resolveMediaNames(collections, themeTokens, probes, reservedNames = []) {
1298
+ const components = mergeComponents(collections);
1299
+ const authoredKeysByName = mergeAuthoredVariants(collections);
1300
+ const reserved = new Set(reservedNames);
1301
+ for (const token of Object.keys(themeTokens)) if (token.startsWith("breakpoint-")) reserved.add(token.slice(11));
1302
+ const claimed = /* @__PURE__ */ new Set();
1303
+ const adoptableKey = (name) => {
1304
+ const keys = authoredKeysByName.get(name);
1305
+ if (keys === void 0 || keys.size !== 1) return null;
1306
+ const [key] = keys;
1307
+ return key ?? null;
1308
+ };
1309
+ const taken = (name) => claimed.has(name) || reserved.has(name) || authoredKeysByName.has(name) || probes.resolves(name);
1310
+ const names = /* @__PURE__ */ new Map();
1311
+ const fallbacks = /* @__PURE__ */ new Set();
1312
+ for (const component of components) {
1313
+ const resolved = (name, kind) => {
1314
+ claimed.add(name);
1315
+ names.set(component.key, {
1316
+ name,
1317
+ kind
1318
+ });
1319
+ };
1320
+ if (component.builtin !== null && probes.expansionMatches(component.builtin, component.key)) {
1321
+ resolved(component.builtin, "builtin");
1322
+ continue;
1323
+ }
1324
+ if (component.breakpoint !== null && probes.expansionMatches(component.breakpoint, component.key)) {
1325
+ resolved(component.breakpoint, "breakpoint");
1326
+ continue;
1327
+ }
1328
+ const digestName = `${GENERATED_NAMESPACE}-${component.digest}`;
1329
+ const candidates = component.readableName === null ? [digestName] : [component.readableName, digestName];
1330
+ let done = false;
1331
+ for (const candidate of candidates) {
1332
+ if (adoptableKey(candidate) === component.key && !claimed.has(candidate) && probes.expansionMatches(candidate, component.key)) {
1333
+ resolved(candidate, "adopted");
1334
+ done = true;
1335
+ break;
1336
+ }
1337
+ if (!taken(candidate)) {
1338
+ resolved(candidate, "generated");
1339
+ done = true;
1340
+ break;
1341
+ }
1342
+ }
1343
+ if (!done) fallbacks.add(component.key);
1344
+ }
1345
+ return {
1346
+ names,
1347
+ fallbacks
1348
+ };
1349
+ }
1350
+ function mergeComponents(collections) {
1351
+ const byKey = /* @__PURE__ */ new Map();
1352
+ for (const collection of collections) for (const component of collection.components) if (!byKey.has(component.key)) byKey.set(component.key, component);
1353
+ return [...byKey.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
1354
+ }
1355
+ function mergeAuthoredVariants(collections) {
1356
+ const byName = /* @__PURE__ */ new Map();
1357
+ for (const collection of collections) for (const variant of collection.authoredVariants) {
1358
+ const keys = byName.get(variant.name) ?? /* @__PURE__ */ new Set();
1359
+ keys.add(variant.mediaQueryKey);
1360
+ byName.set(variant.name, keys);
1361
+ }
1362
+ return byName;
1363
+ }
1364
+ const PROBE_UTILITY = "[--tw-probe:1]";
1365
+ function probeCandidate(prefix, name) {
1366
+ return `${prefix === null || prefix === "" ? "" : `${prefix}:`}${name}:${PROBE_UTILITY}`;
1367
+ }
1368
+ /**
1369
+ * Build the effective-expansion probes against the unaugmented loaded
1370
+ * design system. `resolves` proves only existence; `expansionMatches`
1371
+ * proves that compiling the name yields exactly one `@media` wrapper whose
1372
+ * normalized query equals the key.
1373
+ */
1374
+ function buildMediaProbes(tailwind) {
1375
+ const compiled = /* @__PURE__ */ new Map();
1376
+ const prefix = tailwind.designSystem.theme.prefix;
1377
+ const compile = (name) => {
1378
+ let css = compiled.get(name);
1379
+ if (css === void 0) {
1380
+ [css = null] = tailwind.designSystem.candidatesToCss([probeCandidate(prefix, name)]);
1381
+ compiled.set(name, css);
1382
+ }
1383
+ return css;
1384
+ };
1385
+ return {
1386
+ resolves: (name) => compile(name) !== null,
1387
+ expansionMatches: (name, key) => {
1388
+ const css = compile(name);
1389
+ if (css === null) return false;
1390
+ return JSON.parse(mediaProbeKey(css)) === key;
1391
+ }
1392
+ };
1393
+ }
1394
+ /**
1395
+ * Conservative scanned-corpus reservation: every colon-chained token in the
1396
+ * package's scanned sources reserves its variant segments, so an inert
1397
+ * candidate such as `width-lte-768px:hidden` is never activated with a new
1398
+ * meaning. Over-reservation only pushes a generated name to its digest
1399
+ * form, and content-identity adoption bypasses these reservations, so
1400
+ * reruns stay idempotent.
1401
+ */
1402
+ function scannedVariantReservations(sources) {
1403
+ const reserved = /* @__PURE__ */ new Set();
1404
+ for (const source of sources) for (const match of source.matchAll(/[A-Za-z][\w-]*(?::[^\s"'`{}]+)+/g)) for (const segment of match[0].split(":").slice(0, -1)) reserved.add(segment);
1405
+ return reserved;
1406
+ }
1407
+ /**
1408
+ * Run native condition collection for one member package of an entry
1409
+ * group. Kept separate from resolution so a recoverable collection
1410
+ * failure is attributable to its member.
1411
+ */
1412
+ function collectMemberMediaConditions(stylesheets, tailwind) {
1413
+ return JSON.parse(collectMediaConditions(JSON.stringify({
1414
+ stylesheets,
1415
+ themeTokens: tailwind.themeTokens,
1416
+ tailwindSources: tailwind.graphSources
1417
+ })));
1418
+ }
1419
+ /**
1420
+ * Resolve the fixed map the planner consumes plus the definitions
1421
+ * extraction may emit from the group's member collections. Name
1422
+ * allocation runs once for the complete group, so different keys from
1423
+ * different packages can never independently claim one name.
1424
+ */
1425
+ function planMediaExtraction(options) {
1426
+ const resolution = resolveMediaNames(options.collections, options.tailwind.themeTokens, buildMediaProbes(options.tailwind), scannedVariantReservations([...options.scannedSources, ...options.tailwind.graphSources.map((graphSource) => graphSource.source)]));
1427
+ const names = {};
1428
+ const generated = [];
1429
+ for (const [key, resolved] of resolution.names) {
1430
+ names[key] = resolved.name;
1431
+ if (resolved.kind === "generated") generated.push({
1432
+ key,
1433
+ name: resolved.name
1434
+ });
1435
+ }
1436
+ return {
1437
+ names,
1438
+ generated
1439
+ };
1440
+ }
1441
+ /**
1442
+ * The generated definitions the final plan actually uses, ordered by the
1443
+ * first applied rule that uses each name. A rule counts as applied only
1444
+ * when every one of its candidates survived into `plan.candidates`: a
1445
+ * retained global rule still applies its utilities to proven consumers,
1446
+ * while a blocked rule always contains a dropped candidate, so its
1447
+ * occurrences must not fix registration order for a candidate another rule
1448
+ * still applies. Registration order controls conflicting utility
1449
+ * precedence, so the order follows the applied rules rather than every
1450
+ * encountered condition.
1451
+ */
1452
+ function usedGeneratedDefinitions(extraction, plan) {
1453
+ const applied = new Set(plan.candidates);
1454
+ const byName = new Map(extraction.generated.map((definition) => [definition.name, definition]));
1455
+ const ordered = [];
1456
+ const seen = /* @__PURE__ */ new Set();
1457
+ for (const rule of plan.rules) {
1458
+ if (!rule.candidates.every((candidate) => applied.has(candidate))) continue;
1459
+ for (const candidate of rule.candidates) for (const segment of candidate.split(":").slice(0, -1)) {
1460
+ const definition = byName.get(segment);
1461
+ if (definition !== void 0 && !seen.has(segment)) {
1462
+ seen.add(segment);
1463
+ ordered.push(definition);
1464
+ }
1465
+ }
1466
+ }
1467
+ return ordered;
1468
+ }
1469
+ /**
1470
+ * Append the used definitions to the entry source, each as its own block,
1471
+ * matching the planner's moved-block formatting. Definitions already
1472
+ * present are recognized through adoption before this runs, so appending
1473
+ * never duplicates one.
1474
+ */
1475
+ function appendMediaDefinitions(source, definitions) {
1476
+ let output = source;
1477
+ for (const { key, name } of definitions) {
1478
+ if (!output.endsWith("\n")) output += "\n";
1479
+ if (!output.endsWith("\n\n")) output += "\n";
1480
+ output += `@custom-variant ${name} {\n @media ${key} {\n @slot;\n }\n}\n`;
1481
+ }
1482
+ return output;
1483
+ }
1484
+ //#endregion
950
1485
  //#region src/parser/vue.ts
951
- const ESCAPE_SELECTOR = /(?:::v-|:)(?:deep|global|slotted)\(([^)]*)\)/g;
952
- const ESCAPE_RESIDUE = /(?:>>>|\/deep\/|::v-deep|:deep|::v-slotted|:slotted|::v-global|:global)/;
953
1486
  const SUPPORTED_STYLE_ATTRIBUTES = /* @__PURE__ */ new Set([
954
1487
  "lang",
955
1488
  "module",
@@ -1053,6 +1586,7 @@ function analyzeVueSource(compiler, path, source) {
1053
1586
  const shadowPreprocessorTexts = [];
1054
1587
  const unscopedShadowPreprocessorTexts = [];
1055
1588
  let escapeUnverifiable = false;
1589
+ let opaqueStyleBlocks = false;
1056
1590
  let moduleSiblingUnsupported = false;
1057
1591
  for (const style of descriptor.styles) {
1058
1592
  const start = style.loc.start.offset;
@@ -1063,6 +1597,7 @@ function analyzeVueSource(compiler, path, source) {
1063
1597
  warn("unsupported-sfc-block", start, end, `Unsupported <style> attributes: ${unsupportedAttributes.join(", ")}.`);
1064
1598
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1065
1599
  escapeUnverifiable = true;
1600
+ opaqueStyleBlocks = true;
1066
1601
  continue;
1067
1602
  }
1068
1603
  if (style.src !== void 0) {
@@ -1070,6 +1605,7 @@ function analyzeVueSource(compiler, path, source) {
1070
1605
  warn("unsupported-sfc-block", start, end, "A <style module src> block is not supported.");
1071
1606
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1072
1607
  escapeUnverifiable = true;
1608
+ opaqueStyleBlocks = true;
1073
1609
  } else styleBlockImports.push({
1074
1610
  reference: style.src,
1075
1611
  start,
@@ -1081,12 +1617,19 @@ function analyzeVueSource(compiler, path, source) {
1081
1617
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1082
1618
  warn("unsupported-sfc-block", start, end, "Named <style module> blocks are not supported.");
1083
1619
  shadowModuleCssTexts.push(style.content);
1620
+ opaqueStyleBlocks = true;
1084
1621
  continue;
1085
1622
  }
1086
1623
  if (!SUPPORTED_STYLE_LANGUAGES.has(style.lang)) {
1087
1624
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1088
1625
  warn("preprocessor-style-block", start, end, `The <style lang="${style.lang}"> language is not supported.`);
1089
1626
  shadowPreprocessorTexts.push(style.content);
1627
+ opaqueStyleBlocks = true;
1628
+ try {
1629
+ if (stylesheetAnalysis(`${path}.css`, style.content).selectorsUnverifiable) escapeUnverifiable = true;
1630
+ } catch {
1631
+ escapeUnverifiable = true;
1632
+ }
1090
1633
  continue;
1091
1634
  }
1092
1635
  const outerStart = source.lastIndexOf("<style", start);
@@ -1094,6 +1637,7 @@ function analyzeVueSource(compiler, path, source) {
1094
1637
  if (outerStart < 0 || !closing) {
1095
1638
  warn("unsupported-sfc-block", start, end, "The style block tags could not be located.");
1096
1639
  escapeUnverifiable = true;
1640
+ opaqueStyleBlocks = true;
1097
1641
  continue;
1098
1642
  }
1099
1643
  const block = {
@@ -1110,24 +1654,37 @@ function analyzeVueSource(compiler, path, source) {
1110
1654
  }
1111
1655
  if (!style.scoped) {
1112
1656
  unscopedBlocks.push(block);
1113
- if (style.lang && style.lang !== "css") unscopedShadowPreprocessorTexts.push(style.content);
1114
- else unscopedShadowCssTexts.push(style.content);
1657
+ if (style.lang && style.lang !== "css") {
1658
+ unscopedShadowPreprocessorTexts.push(style.content);
1659
+ try {
1660
+ if (stylesheetAnalysis(`${path}.${style.lang}`, style.content).selectorsUnverifiable) escapeUnverifiable = true;
1661
+ } catch {
1662
+ escapeUnverifiable = true;
1663
+ }
1664
+ } else unscopedShadowCssTexts.push(style.content);
1115
1665
  continue;
1116
1666
  }
1117
- for (const [, inner] of style.content.matchAll(ESCAPE_SELECTOR)) {
1118
- shadowCssTexts.push(`${inner} {}`);
1119
- if (inner.includes("(")) escapeUnverifiable = true;
1667
+ try {
1668
+ const escapes = stylesheetAnalysis(`${path}.${style.lang ?? "css"}`, style.content);
1669
+ shadowCssTexts.push(...escapes.scopeEscapes);
1670
+ if (escapes.scopeEscapes.length > 0) block.shadowSource = escapes.scopeShadowCss.join("\n") || "/* scope escapes extracted */";
1671
+ if (escapes.scopeEscapesUnverifiable || escapes.selectorsUnverifiable) escapeUnverifiable = true;
1672
+ } catch {
1673
+ escapeUnverifiable = true;
1120
1674
  }
1121
- if (ESCAPE_RESIDUE.test(style.content.replace(ESCAPE_SELECTOR, " "))) escapeUnverifiable = true;
1122
1675
  blocks.push(block);
1123
1676
  }
1677
+ const scriptLang = descriptor.scriptSetup?.lang ?? descriptor.script?.lang;
1124
1678
  const state = {
1125
1679
  elements: [],
1126
1680
  components: [],
1127
1681
  dynamic: false,
1128
1682
  vHtml: false,
1129
1683
  hasSlot: false,
1130
- expressionTexts: []
1684
+ moduleClosureBroken: false,
1685
+ referencesUseCssModule: false,
1686
+ expressionReferences: /* @__PURE__ */ new Set(),
1687
+ expressionPath: scriptLang === "ts" || scriptLang === "tsx" ? "Component.ts" : "Component.js"
1131
1688
  };
1132
1689
  visitTemplateNode(source, template.ast, state);
1133
1690
  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 && [
@@ -1137,8 +1694,14 @@ function analyzeVueSource(compiler, path, source) {
1137
1694
  "for"
1138
1695
  ].includes(prop.name))).length;
1139
1696
  const scriptText = [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean).join("\n");
1140
- const fallthroughUnverifiable = Boolean(descriptor.script) || descriptor.scriptSetup?.src !== void 0 || /\b(?:defineOptions|defineProps|inheritAttrs)\b/.test(descriptor.scriptSetup?.content ?? "");
1141
1697
  let scriptImportsUnverifiable = false;
1698
+ const scriptImports = [];
1699
+ let setupAnalysis;
1700
+ let scriptUsesCssModule = false;
1701
+ let scriptHasDynamicImport = false;
1702
+ const scriptVueReferences = [];
1703
+ const scriptVueGlobPatterns = [];
1704
+ let scriptVueGlobUnverifiable = false;
1142
1705
  const styleImports = [descriptor.script, descriptor.scriptSetup].flatMap((script) => {
1143
1706
  if (!script) return [];
1144
1707
  if (script.src || !SUPPORTED_SCRIPT_LANGUAGES.has(script.lang)) {
@@ -1146,18 +1709,22 @@ function analyzeVueSource(compiler, path, source) {
1146
1709
  return [];
1147
1710
  }
1148
1711
  try {
1149
- return staticImports(`${path}.${script.lang ?? "js"}`, script.content);
1712
+ const analysis = sourceAnalysis(`${path}.${script.lang ?? "js"}`, script.content);
1713
+ if (script === descriptor.scriptSetup) setupAnalysis = analysis;
1714
+ if (analysis.usesCssModule || analysis.hasUnboundUseCssModule) scriptUsesCssModule = true;
1715
+ if (analysis.hasDynamicImport) scriptHasDynamicImport = true;
1716
+ scriptImports.push(...analysis.imports);
1717
+ scriptVueReferences.push(...analysis.imports.filter((record) => !record.typeOnly).map((record) => record.specifier));
1718
+ scriptVueGlobPatterns.push(...analysis.vueGlobPatterns);
1719
+ if (analysis.vueGlobUnverifiable) scriptVueGlobUnverifiable = true;
1720
+ return analysis.staticImports;
1150
1721
  } catch {
1151
1722
  scriptImportsUnverifiable = true;
1152
1723
  return [];
1153
1724
  }
1154
1725
  });
1155
- let componentImports = [];
1156
- if (descriptor.scriptSetup && !descriptor.scriptSetup.src && SUPPORTED_SCRIPT_LANGUAGES.has(descriptor.scriptSetup.lang)) try {
1157
- componentImports = staticImportBindings(`${path}.${descriptor.scriptSetup.lang ?? "js"}`, descriptor.scriptSetup.content);
1158
- } catch {
1159
- componentImports = [];
1160
- }
1726
+ const fallthroughUnverifiable = Boolean(descriptor.script) || descriptor.scriptSetup?.src !== void 0 || Boolean(descriptor.scriptSetup) && !setupAnalysis || setupAnalysis?.hasVueFallthroughMacro === true;
1727
+ const componentImports = setupAnalysis?.defaultImports ?? [];
1161
1728
  const offsets = utf8OffsetMap(source, [
1162
1729
  ...warnings.flatMap((warning) => [warning.start, warning.end]),
1163
1730
  ...styleRanges.flatMap((range) => [range.start, range.end]),
@@ -1209,7 +1776,7 @@ function analyzeVueSource(compiler, path, source) {
1209
1776
  blocks: blocks.map(toByteBlock),
1210
1777
  unscopedBlocks: unscopedBlocks.map(toByteBlock),
1211
1778
  moduleBlocks: moduleBlocks.map(toByteBlock),
1212
- moduleClosureBroken: moduleSiblingUnsupported || scriptImportsUnverifiable || /\$style|useCssModule/.test([...state.expressionTexts, scriptText].join("\n")),
1779
+ moduleClosureBroken: moduleSiblingUnsupported || scriptImportsUnverifiable || scriptUsesCssModule || state.moduleClosureBroken || state.referencesUseCssModule && setupAnalysis?.definesRootUseCssModule !== true || (setupAnalysis?.useCssModuleLocals ?? []).some((name) => state.expressionReferences.has(name)),
1213
1780
  htmlElements: state.elements.map(toByteSite),
1214
1781
  componentSites: state.components.map(toByteSite),
1215
1782
  rootStarts: template.ast.children.filter((node) => node.type === NODE_ELEMENT).map((node) => offsets.get(node.loc.start.offset)),
@@ -1217,7 +1784,13 @@ function analyzeVueSource(compiler, path, source) {
1217
1784
  rootFragment: template.ast.children.filter((node) => node.type === NODE_ELEMENT || node.type === NODE_INTERPOLATION || node.type === NODE_TEXT && node.content?.trim?.() !== "").length !== 1,
1218
1785
  scriptText,
1219
1786
  scriptStyleImports: [...new Set(styleImports)],
1787
+ scriptImports,
1220
1788
  scriptImportsUnverifiable,
1789
+ scriptHasDynamicImport,
1790
+ scriptVueReferences,
1791
+ scriptVueGlobPatterns,
1792
+ scriptVueGlobUnverifiable,
1793
+ hasOpaqueStyleBlocks: opaqueStyleBlocks,
1221
1794
  styleBlockImports: styleBlockImports.map((entry) => ({
1222
1795
  ...entry,
1223
1796
  start: offset(entry.start),
@@ -1254,27 +1827,36 @@ function visitTemplateNode(source, node, state) {
1254
1827
  let moduleBinding;
1255
1828
  for (const prop of node.props ?? []) {
1256
1829
  if (prop.type !== PROP_DIRECTIVE) continue;
1830
+ const expression = prop.exp?.content ? prop.name === "on" ? templateHandler(prop.exp.content) : templateExpression(state.expressionPath, prop.exp.content) : void 0;
1831
+ if (prop.exp?.content && !expression) state.moduleClosureBroken = true;
1257
1832
  let provenModuleExpression = false;
1258
1833
  if (prop.name === "bind") {
1259
1834
  if (!prop.arg || !prop.arg.isStatic) classOpaque = true;
1260
1835
  else if (prop.arg.content === "class") {
1261
- 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;
1836
+ const member = node.tagType === TAG_ELEMENT ? expression?.vueModuleMember : void 0;
1262
1837
  if (member && !moduleBinding) {
1263
1838
  moduleBinding = {
1264
- name: member[1],
1839
+ name: member,
1265
1840
  start: attributeRemovalStart(source, node, prop),
1266
1841
  end: prop.loc.end.offset
1267
1842
  };
1268
1843
  provenModuleExpression = true;
1269
1844
  } else {
1270
- const value = staticClassBinding(prop);
1845
+ const value = expression?.staticString ?? void 0;
1271
1846
  if (value === void 0) classOpaque = true;
1272
1847
  else bindingClasses.push(...value.split(/[\t\n\f\r ]+/).filter(Boolean));
1273
1848
  }
1274
1849
  } else if (prop.arg.content === "id") classOpaque = true;
1275
1850
  }
1276
- if (!provenModuleExpression && prop.exp?.content) state.expressionTexts.push(prop.exp.content);
1277
- if (prop.arg && !prop.arg.isStatic && prop.arg.content) state.expressionTexts.push(prop.arg.content);
1851
+ if (!provenModuleExpression && expression?.usesCssModule) state.moduleClosureBroken = true;
1852
+ if (expression?.referencesUseCssModule) state.referencesUseCssModule = true;
1853
+ for (const name of expression?.references ?? []) state.expressionReferences.add(name);
1854
+ if (prop.arg && !prop.arg.isStatic && prop.arg.content) {
1855
+ const argExpression = templateExpression(state.expressionPath, prop.arg.content);
1856
+ state.moduleClosureBroken ||= argExpression?.usesCssModule ?? true;
1857
+ state.referencesUseCssModule ||= argExpression?.referencesUseCssModule ?? false;
1858
+ for (const name of argExpression?.references ?? []) state.expressionReferences.add(name);
1859
+ }
1278
1860
  if (prop.name === "html") state.vHtml = true;
1279
1861
  }
1280
1862
  if (classOpaque) state.dynamic = true;
@@ -1288,7 +1870,12 @@ function visitTemplateNode(source, node, state) {
1288
1870
  if (node.tagType === TAG_COMPONENT) state.components.push(element);
1289
1871
  else if (node.tagType === TAG_ELEMENT) state.elements.push(element);
1290
1872
  }
1291
- if (node.type === NODE_INTERPOLATION && node.content?.content) state.expressionTexts.push(node.content.content);
1873
+ if (node.type === NODE_INTERPOLATION && node.content?.content) {
1874
+ const interpolation = templateExpression(state.expressionPath, node.content.content);
1875
+ state.moduleClosureBroken ||= interpolation?.usesCssModule ?? true;
1876
+ state.referencesUseCssModule ||= interpolation?.referencesUseCssModule ?? false;
1877
+ for (const name of interpolation?.references ?? []) state.expressionReferences.add(name);
1878
+ }
1292
1879
  for (const child of node.children ?? []) visitTemplateNode(source, child, state);
1293
1880
  }
1294
1881
  function attributeRemovalStart(source, node, prop) {
@@ -1297,10 +1884,23 @@ function attributeRemovalStart(source, node, prop) {
1297
1884
  while (start > node.loc.start.offset + 1 && /\s/.test(source[start - 1])) start -= 1;
1298
1885
  return /[\r\n]/.test(source.slice(start, attributeStart)) ? attributeStart : start;
1299
1886
  }
1300
- function staticClassBinding(prop) {
1301
- if (!prop.exp) return void 0;
1887
+ function templateExpression(path, source) {
1302
1888
  try {
1303
- return staticStringExpression("Component.js", prop.exp.content) ?? void 0;
1889
+ return expressionAnalysis(path, source);
1890
+ } catch {
1891
+ return;
1892
+ }
1893
+ }
1894
+ function templateHandler(source) {
1895
+ try {
1896
+ const analysis = sourceAnalysis("Component.handler.ts", `async function handler($event) { ${source} }`);
1897
+ return {
1898
+ staticString: null,
1899
+ vueModuleMember: null,
1900
+ usesCssModule: analysis.usesCssModule,
1901
+ referencesUseCssModule: analysis.hasUnboundUseCssModule,
1902
+ references: analysis.unboundReferences
1903
+ };
1304
1904
  } catch {
1305
1905
  return;
1306
1906
  }
@@ -1371,12 +1971,12 @@ function vueReferenceTarget(importer, reference, vuePaths) {
1371
1971
  join(target, "index.vue")
1372
1972
  ].find((path) => vuePaths.has(path));
1373
1973
  }
1374
- function vueLiteralTargets(file, vuePaths) {
1375
- return new Set([...file.source.matchAll(/(["'`])([^"'`\r\n]+)\1/g)].flatMap((match) => {
1376
- if (/[?*[\]{}]/.test(match[2]) && (match[2].includes("vue") || isLocalVueReference(match[2]))) return [...vuePaths];
1377
- const target = vueReferenceTarget(file.path, match[2], vuePaths);
1974
+ function vueGlobTargets(importer, patterns, vuePaths) {
1975
+ return new Set(patterns.flatMap((pattern) => {
1976
+ if (/[?*[\]{}]/.test(pattern)) return pattern.includes("vue") || isLocalVueReference(pattern) ? [...vuePaths] : [];
1977
+ const target = vueReferenceTarget(importer, pattern, vuePaths);
1378
1978
  if (target) return [target];
1379
- return match[2].includes(".vue") ? [...vuePaths] : [];
1979
+ return pattern.includes(".vue") ? [...vuePaths] : [];
1380
1980
  }));
1381
1981
  }
1382
1982
  function normalizedVueTag(value) {
@@ -1423,28 +2023,48 @@ function buildVueComponentGraph(ownedVue, sourceFiles, analyses, { styleSources,
1423
2023
  if (analysis.componentsOpen) for (const target of vuePaths) callerOpen.add(target);
1424
2024
  }
1425
2025
  for (const file of sourceFiles) {
2026
+ if (extname(file.path) === ".html") continue;
1426
2027
  let references;
2028
+ let vueReferences;
1427
2029
  let blockReferences = [];
2030
+ let hasDynamicImport = false;
2031
+ let vueGlobPatterns = [];
2032
+ let vueGlobUnverifiable = false;
1428
2033
  if (extname(file.path) === ".vue") {
1429
2034
  const fileAnalysis = analyses.get(file.path);
1430
2035
  if (fileAnalysis && !fileAnalysis.retained) {
1431
2036
  references = fileAnalysis.scriptStyleImports;
2037
+ vueReferences = fileAnalysis.scriptVueReferences;
1432
2038
  blockReferences = fileAnalysis.styleBlockImports.map((entry) => entry.reference);
1433
- } else references = [];
2039
+ hasDynamicImport = fileAnalysis.scriptHasDynamicImport || fileAnalysis.scriptImportsUnverifiable;
2040
+ vueGlobPatterns = fileAnalysis.scriptVueGlobPatterns;
2041
+ vueGlobUnverifiable = fileAnalysis.scriptVueGlobUnverifiable;
2042
+ } else {
2043
+ references = [];
2044
+ vueReferences = [];
2045
+ hasDynamicImport = true;
2046
+ }
1434
2047
  } else try {
1435
- references = staticImports(file.path, file.source);
2048
+ const analysis = sourceAnalysis(file.path, file.source);
2049
+ references = analysis.staticImports;
2050
+ vueReferences = analysis.imports.filter((record) => !record.typeOnly).map((record) => record.specifier);
2051
+ hasDynamicImport = analysis.hasDynamicImport;
2052
+ vueGlobPatterns = analysis.vueGlobPatterns;
2053
+ vueGlobUnverifiable = analysis.vueGlobUnverifiable;
1436
2054
  } catch {
1437
2055
  references = [];
2056
+ vueReferences = [];
2057
+ hasDynamicImport = true;
1438
2058
  }
1439
- 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);
2059
+ if (hasDynamicImport || vueGlobUnverifiable || vueReferences.some((reference) => !stylesheetReference.test(reference) && isLocalVueReference(reference) && couldResolveToVue(reference) && !vueReferenceTarget(file.path, reference, vuePaths))) for (const target of vuePaths) callerOpen.add(target);
1440
2060
  const resolvesOwned = (reference) => stylesheetReferenceTargets(file.path, reference, styleSources).some((path) => pathOwners.get(path) === packageRoot);
1441
2061
  if (pathOwners.get(file.path) === packageRoot && (references.some((reference) => stylesheetReference.test(reference) && (!reference.startsWith(".") || !resolvesOwned(reference))) || blockReferences.some((reference) => !resolvesOwned(reference)))) unresolvedStyleImport = true;
1442
- const imported = new Set(references.flatMap((reference) => {
2062
+ const imported = new Set(vueReferences.flatMap((reference) => {
1443
2063
  const target = vueReferenceTarget(file.path, reference, vuePaths);
1444
2064
  return target ? [target] : [];
1445
2065
  }));
1446
2066
  for (const target of imported) if (extname(file.path) !== ".vue" || !analyses.get(file.path)?.setupImports?.has(target)) callerOpen.add(target);
1447
- for (const target of vueLiteralTargets(file, vuePaths)) if (!imported.has(target)) callerOpen.add(target);
2067
+ for (const target of vueGlobTargets(file.path, vueGlobPatterns, vuePaths)) if (!imported.has(target)) callerOpen.add(target);
1448
2068
  }
1449
2069
  return {
1450
2070
  callers,
@@ -1452,15 +2072,16 @@ function buildVueComponentGraph(ownedVue, sourceFiles, analyses, { styleSources,
1452
2072
  unresolvedStyleImport
1453
2073
  };
1454
2074
  }
1455
- function vueWarningsOnlyResult(preparedVue) {
1456
- if (preparedVue.warnings.length === 0) return {};
2075
+ function vueWarningsOnlyResult(preparedVue, extraWarnings = []) {
2076
+ const warnings = [...preparedVue.warnings, ...extraWarnings];
2077
+ if (warnings.length === 0) return {};
1457
2078
  return { plan: {
1458
2079
  files: [],
1459
2080
  deletedFiles: [],
1460
2081
  unlinkedFiles: [],
1461
2082
  candidates: [],
1462
2083
  rules: [],
1463
- warnings: preparedVue.warnings,
2084
+ warnings,
1464
2085
  convertedRules: 0,
1465
2086
  retainedRules: 0
1466
2087
  } };
@@ -1542,13 +2163,17 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1542
2163
  pathOwners,
1543
2164
  packageRoot
1544
2165
  });
1545
- const generatesSelectors = (text) => /#\{|@\{|&[\w-]/.test(text);
1546
2166
  const vueShadowCss = [];
1547
2167
  const vueShadowModuleCss = [];
1548
2168
  let vueShadowUnverifiable = vueGraph.unresolvedStyleImport;
1549
2169
  for (const [path, source] of styleSources) {
1550
2170
  if (pathOwners.get(path) !== packageRoot) continue;
1551
- if (isPreprocessorPath(path) && generatesSelectors(source)) {
2171
+ try {
2172
+ if (stylesheetAnalysis(path, source).selectorsUnverifiable) {
2173
+ vueShadowUnverifiable = true;
2174
+ continue;
2175
+ }
2176
+ } catch {
1552
2177
  vueShadowUnverifiable = true;
1553
2178
  continue;
1554
2179
  }
@@ -1558,21 +2183,23 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1558
2183
  source
1559
2184
  });
1560
2185
  }
1561
- for (const file of sourceFiles) if (extname(file.path) === ".html" && pathOwners.get(file.path) === packageRoot && /<style/i.test(file.source)) vueShadowUnverifiable = true;
2186
+ for (const file of sourceFiles) if (extname(file.path) === ".html" && pathOwners.get(file.path) === packageRoot) try {
2187
+ if (parseHtmlSource(file.path, file.source).hasStyle) vueShadowUnverifiable = true;
2188
+ } catch {
2189
+ vueShadowUnverifiable = true;
2190
+ }
1562
2191
  for (const file of ownedVue) {
1563
2192
  const analysis = analysisOf(file.path);
1564
2193
  if (analysis.retained) {
1565
- if (/<style/i.test(file.source)) vueShadowUnverifiable = true;
2194
+ if (analysis.styleRanges.length > 0) vueShadowUnverifiable = true;
1566
2195
  continue;
1567
2196
  }
1568
2197
  if (analysis.escapeUnverifiable || analysis.scriptImportsUnverifiable) vueShadowUnverifiable = true;
1569
- for (const text of analysis.shadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
1570
- else vueShadowCss.push({
2198
+ for (const text of analysis.shadowPreprocessorTexts) vueShadowCss.push({
1571
2199
  path: file.path,
1572
2200
  source: text
1573
2201
  });
1574
- for (const text of analysis.unscopedShadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
1575
- else vueShadowCss.push({
2202
+ for (const text of analysis.unscopedShadowPreprocessorTexts) vueShadowCss.push({
1576
2203
  path: file.path,
1577
2204
  source: text,
1578
2205
  migratingUnscoped: true
@@ -1586,7 +2213,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1586
2213
  migratingUnscoped: true
1587
2214
  })), ...analysis.blocks.map((block) => ({
1588
2215
  path: file.path,
1589
- source: block.analysisSource ?? block.content,
2216
+ source: block.shadowSource ?? block.analysisSource ?? block.content,
1590
2217
  scoped: true
1591
2218
  })));
1592
2219
  vueShadowModuleCss.push(...analysis.shadowModuleCssTexts, ...analysis.moduleBlocks.map((block) => block.analysisSource ?? block.content));
@@ -1627,7 +2254,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1627
2254
  for (const path of importedStyles) {
1628
2255
  stylePaths.add(path);
1629
2256
  if (extname(path) !== ".css") continue;
1630
- for (const imported of cssImports(styleSources.get(path) ?? "")) {
2257
+ for (const imported of cssImports(path, styleSources.get(path) ?? "")) {
1631
2258
  if (imported.media) continue;
1632
2259
  for (const target of stylesheetReferenceTargets(path, imported.href, styleSources)) if (pathOwners.get(target) === packageRoot) importedStyles.add(target);
1633
2260
  }
@@ -1712,6 +2339,19 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1712
2339
  message
1713
2340
  });
1714
2341
  }
2342
+ const allStyleBlocks = [
2343
+ ...analysis.blocks,
2344
+ ...analysis.moduleBlocks,
2345
+ ...analysis.unscopedBlocks
2346
+ ];
2347
+ let vueAtRuleIdentities;
2348
+ if (analysis.hasOpaqueStyleBlocks) vueAtRuleIdentities = null;
2349
+ else try {
2350
+ const registrations = allStyleBlocks.map((block) => stylesheetAnalysis(`${file.path}.${block.analysisSource ? "css" : block.syntax}`, block.analysisSource ?? block.content));
2351
+ vueAtRuleIdentities = registrations.some((entry) => entry.globalAtRulesUnverifiable) ? null : registrations.flatMap((entry) => entry.globalAtRuleIdentities);
2352
+ } catch {
2353
+ vueAtRuleIdentities = null;
2354
+ }
1715
2355
  const vueBlocks = migrateUnscoped ? analysis.unscopedBlocks : analysis.blocks;
1716
2356
  if (vueBlocks.length === 0 && !migrateModule) continue;
1717
2357
  const vueRetention = migrateUnscoped ? void 0 : retention;
@@ -1719,19 +2359,20 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1719
2359
  if (migrateModule) stylesheets.push({
1720
2360
  cssPath: file.path,
1721
2361
  cssSource: file.source,
1722
- cssModuleId: normalizedRelativePath(packageRoot, file.path),
2362
+ cssModuleId: normalizedRelativePath(workspaceRoot, file.path),
1723
2363
  syntax: "css",
1724
2364
  isModule: true,
1725
2365
  vueBlocks: analysis.moduleBlocks,
1726
2366
  vueModule: true,
1727
2367
  vueShadowCss: shadowCss,
1728
2368
  vueShadowModuleCss,
1729
- vueShadowUnverifiable
2369
+ vueShadowUnverifiable,
2370
+ vueAtRuleIdentities
1730
2371
  });
1731
2372
  if (vueBlocks.length > 0) stylesheets.push({
1732
2373
  cssPath: file.path,
1733
2374
  cssSource: file.source,
1734
- cssModuleId: normalizedRelativePath(packageRoot, file.path),
2375
+ cssModuleId: normalizedRelativePath(workspaceRoot, file.path),
1735
2376
  syntax: "css",
1736
2377
  isModule: !vueRetention,
1737
2378
  vueBlocks,
@@ -1739,7 +2380,8 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1739
2380
  vueUnscoped: migrateUnscoped,
1740
2381
  vueShadowCss: vueRetention ? void 0 : shadowCss,
1741
2382
  vueShadowModuleCss: vueRetention ? void 0 : vueShadowModuleCss,
1742
- vueShadowUnverifiable: vueRetention ? void 0 : vueShadowUnverifiable
2383
+ vueShadowUnverifiable: vueRetention ? void 0 : vueShadowUnverifiable,
2384
+ vueAtRuleIdentities
1743
2385
  });
1744
2386
  }
1745
2387
  for (const file of ownedVue) {
@@ -1747,19 +2389,22 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1747
2389
  if (analysis.retained) continue;
1748
2390
  const elements = elementsByFile.get(file.path) ?? [];
1749
2391
  const contextPaths = [...new Set(elements.flatMap((element) => element.cssPaths))];
1750
- if (contextPaths.length === 0) continue;
1751
- await rejectSymlinkTarget(file.path, packageRoot);
2392
+ if (contextPaths.length > 0) await rejectSymlinkTarget(file.path, packageRoot);
1752
2393
  files.set(file.path, {
1753
2394
  ...file,
1754
- htmlElements: elements,
1755
- htmlStylesheets: contextPaths.map((cssPath) => ({
1756
- cssPath,
1757
- variants: [],
1758
- direct: cssPath === file.path,
1759
- analyzable: true
1760
- })),
1761
- htmlReferencesSafe: !analysis.dynamic,
1762
- htmlScriptText: analysis.scriptText
2395
+ ...contextPaths.length > 0 ? {
2396
+ htmlElements: elements,
2397
+ htmlStylesheets: contextPaths.map((cssPath) => ({
2398
+ cssPath,
2399
+ variants: [],
2400
+ direct: cssPath === file.path,
2401
+ analyzable: true
2402
+ })),
2403
+ htmlReferencesSafe: !analysis.dynamic,
2404
+ htmlScriptText: analysis.scriptText
2405
+ } : {},
2406
+ sourceImports: analysis.scriptImports,
2407
+ sourceImportsUnverifiable: analysis.scriptImportsUnverifiable
1763
2408
  });
1764
2409
  }
1765
2410
  return {
@@ -1865,7 +2510,13 @@ async function migrate(options = {}) {
1865
2510
  Promise.all(stylePaths.map(async (path) => [path, await snapshotFile(snapshots, path)])).then((entries) => new Map(entries)),
1866
2511
  Promise.all(sourcePaths.map(async (path) => {
1867
2512
  const source = await readFile(path, "utf8");
1868
- const mayReferenceModule = extname(path) !== ".html" || /<link\b/i.test(source);
2513
+ const mayReferenceModule = extname(path) !== ".html" || (() => {
2514
+ try {
2515
+ return parseHtmlSource(path, source).links.length > 0;
2516
+ } catch {
2517
+ return /<link\b/i.test(source);
2518
+ }
2519
+ })();
1869
2520
  if (!scope.targetable.has(path) && !mayReferenceModule) return void 0;
1870
2521
  return {
1871
2522
  path,
@@ -1885,15 +2536,27 @@ async function migrate(options = {}) {
1885
2536
  styleSources,
1886
2537
  sourceFiles,
1887
2538
  styleDependents: indexStylesheetDependents(styleSources),
1888
- vueStyleRanges
2539
+ vueStyleRanges,
2540
+ entryCatalog: tailwindEntryCatalog(styleSources, scope.pathOwners),
2541
+ ignoredPaths: options.workspaces ? await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)) : /* @__PURE__ */ new Set()
1889
2542
  };
1890
2543
  const failures = [];
1891
2544
  const plans = [];
2545
+ const prepared = [];
1892
2546
  for (const packageRoot of selectedPackages) {
1893
- const result = await planPackage(context, packageRoot);
1894
- if (result.failure) failures.push(result.failure);
1895
- else if (result.plan) plans.push(result.plan);
1896
- }
2547
+ const preparation = await preparePackage(context, packageRoot);
2548
+ if (preparation.failure) failures.push(preparation.failure);
2549
+ else if (preparation.plan) plans.push(preparation.plan);
2550
+ if (preparation.prepared) prepared.push(preparation.prepared);
2551
+ }
2552
+ const groups = /* @__PURE__ */ new Map();
2553
+ for (const preparation of prepared) {
2554
+ const members = groups.get(preparation.tailwind.path) ?? [];
2555
+ members.push(preparation);
2556
+ groups.set(preparation.tailwind.path, members);
2557
+ }
2558
+ for (const [, members] of [...groups].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) for (const result of await planPreparedGroup(context, members)) if (result.failure) failures.push(result.failure);
2559
+ else if (result.plan) plans.push(result.plan);
1897
2560
  const originals = new Map([...styleSources, ...sourceFiles.map((file) => [file.path, file.source])]);
1898
2561
  const { filesByPath, deletedPaths, candidates, rules, warnings, convertedRules, retainedRules } = mergePlans(plans, originals);
1899
2562
  const changed = [...filesByPath.values()].map((file) => ({
@@ -1980,7 +2643,7 @@ async function migrate(options = {}) {
1980
2643
  failures
1981
2644
  };
1982
2645
  }
1983
- async function planPackage(context, packageRoot) {
2646
+ async function preparePackage(context, packageRoot) {
1984
2647
  const { options, snapshots, workspaceRoot, explicitStyle, configuredEntry, styleSources, sourceFiles, styleDependents, pathOwners, targetable, selectedPackages, scannedPaths, vueStyleRanges } = context;
1985
2648
  const recover = (error, fatal = false) => {
1986
2649
  if (!options.force || fatal) throw error;
@@ -2030,19 +2693,84 @@ async function planPackage(context, packageRoot) {
2030
2693
  if (ownedStyles.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
2031
2694
  let tailwindPath;
2032
2695
  let tailwindEntries;
2033
- try {
2034
- ({path: tailwindPath, entries: tailwindEntries} = resolveTailwindEntry(ownedStyles, styleSources, configuredEntry));
2035
- } catch (error) {
2036
- return recover(error);
2696
+ let entryOwner = packageRoot;
2697
+ let sharedProofs;
2698
+ const ownEntries = findTailwindEntries(ownedStyles, styleSources);
2699
+ if (ownEntries.length === 0 && configuredEntry === void 0 && options.workspaces && options.extractMediaQueries !== false) {
2700
+ let packageJson = {};
2701
+ try {
2702
+ const rawPackageJson = snapshots.get(join(packageRoot, "package.json"));
2703
+ if (rawPackageJson !== void 0) {
2704
+ const parsed = JSON.parse(rawPackageJson);
2705
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) packageJson = { ...parsed };
2706
+ }
2707
+ } catch (error) {
2708
+ return recover(error);
2709
+ }
2710
+ const ancestors = selectedPackages.filter((root) => root !== packageRoot && isWithin(root, packageRoot)).sort((left, right) => right.length - left.length);
2711
+ for (const ancestor of ancestors) {
2712
+ const proven = [];
2713
+ for (const candidate of context.entryCatalog.get(ancestor) ?? []) {
2714
+ const proofs = proveSharedEntry({
2715
+ packageRoot,
2716
+ entry: candidate,
2717
+ entrySource: styleSources.get(candidate) ?? "",
2718
+ styleSources,
2719
+ packageSources,
2720
+ owned: (path) => pathOwners.get(path) === packageRoot,
2721
+ writable: (path) => targetable.has(path) && pathOwners.get(path) === packageRoot,
2722
+ packageJson,
2723
+ ignoredPaths: context.ignoredPaths
2724
+ });
2725
+ if (proofs) proven.push({
2726
+ entry: candidate,
2727
+ proofs
2728
+ });
2729
+ }
2730
+ if (proven.length > 1) return recover(/* @__PURE__ */ new Error("Multiple proven ancestor Tailwind entries were found. Pass --tailwind-css."));
2731
+ if (proven.length === 1) {
2732
+ [{entry: tailwindPath, proofs: sharedProofs}] = proven;
2733
+ entryOwner = ancestor;
2734
+ break;
2735
+ }
2736
+ }
2737
+ }
2738
+ if (sharedProofs && tailwindPath !== void 0) tailwindEntries = [];
2739
+ else {
2740
+ tailwindEntries = ownEntries;
2741
+ try {
2742
+ tailwindPath = selectTailwindEntry(ownEntries, configuredEntry);
2743
+ } catch (error) {
2744
+ return recover(error);
2745
+ }
2037
2746
  }
2038
2747
  const excludedEntries = /* @__PURE__ */ new Set([...tailwindEntries, tailwindPath]);
2039
2748
  const explicitCss = explicitStyle && extname(explicitStyle) !== ".vue" ? explicitStyle : void 0;
2040
- 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))));
2749
+ 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) || preparedVue.stylePaths.has(path) || packageSources.some((file) => importsStylesheet(file, path))));
2041
2750
  if (targets.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
2042
2751
  if (targets.some((path) => excludedEntries.has(path))) throw new Error("The Tailwind CSS entry cannot be migrated.");
2752
+ const proofWarnings = [];
2753
+ const unprovenFlow = (path) => ({
2754
+ code: "unproven-shared-entry-flow",
2755
+ file: path,
2756
+ start: 0,
2757
+ end: 0,
2758
+ message: "A consumer flow could not be proven to load the shared Tailwind entry, so the stylesheet is retained."
2759
+ });
2760
+ const provenTargets = sharedProofs ? targets.filter((path) => {
2761
+ const proven = sharedProofs.provenStyle(packageSources.filter((file) => importsStylesheet(file, path) || (file.htmlStylesheets ?? []).some((linked) => linked.cssPath === path)));
2762
+ if (!proven) proofWarnings.push(unprovenFlow(path));
2763
+ return proven;
2764
+ }) : targets;
2765
+ const provenVueStylesheets = sharedProofs ? preparedVue.stylesheets.filter((stylesheet) => {
2766
+ const proven = sharedProofs.provenStyle(packageSources.filter((file) => file.path === stylesheet.cssPath));
2767
+ if (!proven && !proofWarnings.some((warning) => warning.file === stylesheet.cssPath)) proofWarnings.push(unprovenFlow(stylesheet.cssPath));
2768
+ return proven;
2769
+ }) : preparedVue.stylesheets;
2770
+ if (provenTargets.length === 0 && provenVueStylesheets.length === 0) return vueWarningsOnlyResult(preparedVue, proofWarnings);
2043
2771
  let tailwind;
2044
2772
  try {
2045
- tailwind = await loadTailwind(packageRoot, tailwindPath, snapshots, workspaceRoot);
2773
+ tailwind = await loadTailwind(entryOwner, tailwindPath, snapshots, workspaceRoot);
2046
2774
  } catch (error) {
2047
2775
  return recover(error, isIntegrityError(error));
2048
2776
  }
@@ -2057,13 +2785,13 @@ async function planPackage(context, packageRoot) {
2057
2785
  try {
2058
2786
  stylesheets = [];
2059
2787
  const compilerDependents = /* @__PURE__ */ new Map();
2060
- for (const stylePath of targets.sort()) {
2788
+ for (const stylePath of provenTargets.sort()) {
2061
2789
  await rejectSymlinkTarget(stylePath, packageRoot);
2062
2790
  const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith("_");
2063
2791
  const stylesheet = {
2064
2792
  cssPath: stylePath,
2065
2793
  cssSource: styleSources.get(stylePath) ?? "",
2066
- cssModuleId: normalizedRelativePath(packageRoot, stylePath),
2794
+ cssModuleId: normalizedRelativePath(workspaceRoot, stylePath),
2067
2795
  cssDependents: styleDependents.get(stylePath) ?? [],
2068
2796
  syntax: stylesheetSyntax(stylePath),
2069
2797
  isModule: isStylesheetModule(stylePath),
@@ -2097,125 +2825,405 @@ async function planPackage(context, packageRoot) {
2097
2825
  } catch (error) {
2098
2826
  return recover(error, isIntegrityError(error));
2099
2827
  }
2100
- stylesheets.push(...preparedVue.stylesheets);
2101
- const request = {
2828
+ stylesheets.push(...provenVueStylesheets);
2829
+ return { prepared: {
2830
+ packageRoot,
2831
+ preparedHtml,
2832
+ preparedVue,
2833
+ styleCompilers,
2834
+ files,
2102
2835
  stylesheets,
2103
- tailwindPath: tailwind.path,
2104
- tailwindSource: tailwind.css,
2105
- utilityPrefix: tailwind.designSystem.theme.prefix,
2106
- themeTokens: tailwind.themeTokens,
2107
- files
2836
+ tailwind,
2837
+ sharedProofs,
2838
+ warnings: proofWarnings
2839
+ } };
2840
+ }
2841
+ async function planPreparedGroup(context, members) {
2842
+ const { options, workspaceRoot, targetable } = context;
2843
+ const results = [];
2844
+ let active = [...members].sort((left, right) => {
2845
+ const leftPath = normalizedRelativePath(workspaceRoot, left.packageRoot);
2846
+ const rightPath = normalizedRelativePath(workspaceRoot, right.packageRoot);
2847
+ return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
2848
+ });
2849
+ const entry = active[0].tailwind;
2850
+ const recoverGroup = (error, fatal = false) => {
2851
+ if (!options.force || fatal) throw error;
2852
+ for (const member of active) results.push({ failure: packageFailure(workspaceRoot, member.packageRoot, error) });
2853
+ return results;
2108
2854
  };
2109
- let plan;
2110
- try {
2111
- plan = JSON.parse(planBatchMigration(JSON.stringify(request)));
2855
+ const membersByRoot = new Map(active.map((member) => [member.packageRoot, member]));
2856
+ for (const member of options.extractMediaQueries !== false ? active : []) member.stylesheets = member.stylesheets.filter((stylesheet) => {
2857
+ const proven = member.files.filter((file) => context.pathOwners.get(file.path) !== member.packageRoot && (importsStylesheet(file, stylesheet.cssPath) || (file.htmlStylesheets ?? []).some((linked) => linked.cssPath === stylesheet.cssPath))).every((consumer) => {
2858
+ const consumerRoot = context.pathOwners.get(consumer.path);
2859
+ const consumerMember = consumerRoot === void 0 ? void 0 : membersByRoot.get(consumerRoot);
2860
+ if (consumerMember === void 0) return false;
2861
+ if (consumerMember.sharedProofs === void 0) return true;
2862
+ return consumerMember.sharedProofs.provenConsumer(consumer);
2863
+ });
2864
+ if (!proven) member.warnings.push({
2865
+ code: "unproven-shared-entry-flow",
2866
+ file: stylesheet.cssPath,
2867
+ start: 0,
2868
+ end: 0,
2869
+ message: "A consumer flow could not be proven to load the shared Tailwind entry, so the stylesheet is retained."
2870
+ });
2871
+ return proven;
2872
+ });
2873
+ for (const member of active.filter((remaining) => remaining.stylesheets.length === 0)) if (member.warnings.length > 0) results.push({ plan: {
2874
+ files: [],
2875
+ deletedFiles: [],
2876
+ unlinkedFiles: [],
2877
+ candidates: [],
2878
+ rules: [],
2879
+ warnings: [
2880
+ ...member.warnings,
2881
+ ...member.preparedHtml.warnings,
2882
+ ...member.preparedVue.warnings
2883
+ ],
2884
+ convertedRules: 0,
2885
+ retainedRules: 0
2886
+ } });
2887
+ active = active.filter((remaining) => remaining.stylesheets.length > 0);
2888
+ if (active.length === 0) return results;
2889
+ const groupWritable = !await entryUnsafe(entry.path, workspaceRoot, targetable);
2890
+ const mediaWarnings = [];
2891
+ let extraction;
2892
+ const collections = [];
2893
+ for (const member of active) try {
2894
+ collections.push(collectMemberMediaConditions(member.stylesheets, entry));
2112
2895
  } catch (error) {
2113
- return recover(error, !isRecoverablePlanningError(error));
2896
+ if (!options.force || !isRecoverablePlanningError(error)) throw error;
2897
+ results.push({ failure: packageFailure(workspaceRoot, member.packageRoot, error) });
2898
+ active = active.filter((remaining) => remaining !== member);
2899
+ }
2900
+ if (active.length === 0) return results;
2901
+ let names = extraction?.names;
2902
+ let generated = extraction?.generated ?? [];
2903
+ if (options.extractMediaQueries !== false) if (!groupWritable) {
2904
+ names = {};
2905
+ generated = [];
2906
+ if (collections.some((collection) => collection.components.length > 0)) mediaWarnings.push({
2907
+ code: "media-query-definition-fallback",
2908
+ file: entry.path,
2909
+ start: 0,
2910
+ end: 0,
2911
+ message: "The Tailwind entry could not be edited safely, so media behavior was preserved with arbitrary variants."
2912
+ });
2913
+ } else {
2914
+ try {
2915
+ extraction = planMediaExtraction({
2916
+ collections,
2917
+ tailwind: entry,
2918
+ scannedSources: context.sourceFiles.map((file) => file.source)
2919
+ });
2920
+ } catch (error) {
2921
+ return recoverGroup(error, !isRecoverablePlanningError(error));
2922
+ }
2923
+ names = extraction.names;
2924
+ generated = extraction.generated;
2114
2925
  }
2115
- try {
2116
- plan = replanCompileFailures(tailwind, request, plan);
2117
- } catch (error) {
2118
- return recover(error);
2926
+ let plan = {
2927
+ files: [],
2928
+ deletedFiles: [],
2929
+ unlinkedFiles: [],
2930
+ candidates: [],
2931
+ rules: [],
2932
+ warnings: [],
2933
+ convertedRules: 0,
2934
+ retainedRules: 0
2935
+ };
2936
+ let augmented = entry.css;
2937
+ const blocked = /* @__PURE__ */ new Map();
2938
+ finalization: for (;;) {
2939
+ const baseIdentities = new Set(atRuleIdentities(`${entry.path}.group-base.css`, [entry.css, ...entry.graphSources.map((graphSource) => graphSource.source)].join("\n")));
2940
+ const sheets = active.flatMap((member) => member.stylesheets.map((stylesheet) => ({
2941
+ member,
2942
+ stylesheet
2943
+ })));
2944
+ const movable = ({ member, stylesheet }) => member.sharedProofs === void 0 && stylesheet.isModule === true && (stylesheet.syntax === void 0 || stylesheet.syntax === "css") && (stylesheet.vueBlocks === void 0 || stylesheet.vueBlocks.length === 0);
2945
+ for (const sheet of sheets) {
2946
+ if (movable(sheet)) continue;
2947
+ for (const identity of stylesheetAtRuleIdentities(sheet.stylesheet)) baseIdentities.add(identity);
2948
+ }
2949
+ const sheetIdentities = sheets.map((sheet) => movable(sheet) ? new Set(stylesheetAtRuleIdentities(sheet.stylesheet)) : /* @__PURE__ */ new Set());
2950
+ const opaqueVueRegistrations = sheets.some(({ stylesheet }) => stylesheet.vueAtRuleIdentities === null);
2951
+ const movesDisabled = new Set(opaqueVueRegistrations ? sheets.flatMap((sheet, index) => movable(sheet) ? [index] : []) : []);
2952
+ for (const [index, identities] of sheetIdentities.entries()) for (const identity of identities) {
2953
+ if (baseIdentities.has(identity)) movesDisabled.add(index);
2954
+ for (const [other, otherIdentities] of sheetIdentities.entries()) if (other !== index && otherIdentities.has(identity)) {
2955
+ movesDisabled.add(index);
2956
+ movesDisabled.add(other);
2957
+ }
2958
+ }
2959
+ const filesByPath = /* @__PURE__ */ new Map();
2960
+ const contextsByPath = /* @__PURE__ */ new Map();
2961
+ for (const member of active) for (const file of member.files) {
2962
+ if (file.htmlStylesheets !== void 0 && file.htmlStylesheets.length > 0) {
2963
+ const contexts = contextsByPath.get(file.path) ?? [];
2964
+ contexts.push(...file.htmlStylesheets);
2965
+ contextsByPath.set(file.path, contexts);
2966
+ }
2967
+ if (!filesByPath.has(file.path) || context.pathOwners.get(file.path) === member.packageRoot) filesByPath.set(file.path, file);
2968
+ }
2969
+ const files = [...filesByPath.values()].map((file) => {
2970
+ const contexts = contextsByPath.get(file.path);
2971
+ if (contexts === void 0) return file;
2972
+ const seen = /* @__PURE__ */ new Set();
2973
+ const merged = contexts.filter((linked) => {
2974
+ const key = `${linked.cssPath}\0${linked.variants.join(",")}\0${linked.direct}\0${linked.analyzable}`;
2975
+ if (seen.has(key)) return false;
2976
+ seen.add(key);
2977
+ return true;
2978
+ });
2979
+ return {
2980
+ ...file,
2981
+ htmlStylesheets: merged
2982
+ };
2983
+ }).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
2984
+ planning: for (;;) {
2985
+ const request = {
2986
+ stylesheets: sheets.map(({ member, stylesheet }, index) => {
2987
+ const rules = blocked.get(index);
2988
+ const movesOff = member.sharedProofs !== void 0 || movesDisabled.has(index);
2989
+ return {
2990
+ ...stylesheet,
2991
+ ...rules ? { blockedRules: [...rules.values()].map((blockedRule) => blockedRule.ruleId) } : {},
2992
+ ...movesOff ? { globalAtRuleMoves: false } : {}
2993
+ };
2994
+ }),
2995
+ tailwindPath: entry.path,
2996
+ tailwindSource: entry.css,
2997
+ utilityPrefix: entry.designSystem.theme.prefix,
2998
+ themeTokens: entry.themeTokens,
2999
+ ...names ? { mediaNames: names } : {},
3000
+ ...groupWritable ? {} : { entryWritable: false },
3001
+ files
3002
+ };
3003
+ try {
3004
+ plan = JSON.parse(planBatchMigration(JSON.stringify(request)));
3005
+ } catch (error) {
3006
+ return recoverGroup(error, !isRecoverablePlanningError(error));
3007
+ }
3008
+ let composed = entry.css;
3009
+ const plannedEntry = plan.files.findIndex((file) => file.path === entry.path);
3010
+ if (plannedEntry >= 0) {
3011
+ if (!groupWritable) throw new Error(`The planner claimed an unwritable Tailwind entry: ${entry.path}`);
3012
+ composed = plan.files[plannedEntry].source;
3013
+ plan.files.splice(plannedEntry, 1);
3014
+ if (!composed.startsWith(entry.css)) throw new Error(`The planner rewrote the Tailwind entry instead of appending: ${entry.path}`);
3015
+ }
3016
+ const currentExtraction = names !== void 0 ? {
3017
+ names,
3018
+ generated
3019
+ } : void 0;
3020
+ const used = currentExtraction ? usedGeneratedDefinitions(currentExtraction, plan) : [];
3021
+ augmented = currentExtraction ? appendMediaDefinitions(composed, used) : composed;
3022
+ let system;
3023
+ try {
3024
+ system = augmented === entry.css ? entry.designSystem : await entry.loadWith(augmented);
3025
+ } catch (error) {
3026
+ if (isIntegrityError(error)) throw error;
3027
+ if (names !== void 0 && generated.length > 0) {
3028
+ const generatedKeys = new Set(generated.map((definition) => definition.key));
3029
+ names = Object.fromEntries(Object.entries(names).filter(([key]) => !generatedKeys.has(key)));
3030
+ generated = [];
3031
+ continue planning;
3032
+ }
3033
+ return recoverGroup(error);
3034
+ }
3035
+ if (currentExtraction && names !== void 0) {
3036
+ const prefix = entry.designSystem.theme.prefix;
3037
+ const rejected = new Set(used.filter(({ name }) => system.candidatesToCss([probeCandidate(prefix, name)])[0] === null).map(({ key }) => key));
3038
+ if (rejected.size > 0) {
3039
+ names = Object.fromEntries(Object.entries(names).filter(([key]) => !rejected.has(key)));
3040
+ generated = generated.filter(({ key }) => !rejected.has(key));
3041
+ continue planning;
3042
+ }
3043
+ }
3044
+ const failing = invalidCandidates(currentExtraction ? system : entry.designSystem, plan.candidates);
3045
+ if (failing.length > 0) {
3046
+ if (!accumulateBlockedRules(blocked, plan, failing)) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
3047
+ continue planning;
3048
+ }
3049
+ break;
3050
+ }
3051
+ for (const [index, rules] of blocked) {
3052
+ const cssPath = sheets[index].stylesheet.cssPath;
3053
+ for (const { authoredSpan, candidates } of rules.values()) {
3054
+ const failed = [...candidates].sort().map((candidate) => `\`${candidate}\``).join(", ");
3055
+ plan.warnings.push({
3056
+ code: "candidate-compilation-failure",
3057
+ file: cssPath,
3058
+ start: authoredSpan.start,
3059
+ end: authoredSpan.end,
3060
+ message: `Tailwind did not generate CSS for ${failed}, so the rule is retained.`
3061
+ });
3062
+ }
3063
+ }
3064
+ plan.warnings.push(...mediaWarnings);
3065
+ for (const member of active) plan.warnings.push(...member.warnings);
3066
+ removeGroupHtmlLinks(plan, active);
3067
+ const failure = await finishGroupPlan(context, active, plan);
3068
+ if (failure === void 0) break;
3069
+ if (!options.force || failure.fatal) throw failure.error;
3070
+ const failed = failure.member ?? active[0];
3071
+ results.push({ failure: packageFailure(workspaceRoot, failed.packageRoot, failure.error) });
3072
+ active = active.filter((member) => member !== failed);
3073
+ if (active.length === 0) return results;
3074
+ blocked.clear();
3075
+ plan.warnings = [];
3076
+ continue finalization;
3077
+ }
3078
+ const seenWarnings = /* @__PURE__ */ new Set();
3079
+ plan.warnings = plan.warnings.filter((warning) => {
3080
+ const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}\0${warning.message}`;
3081
+ if (seenWarnings.has(key)) return false;
3082
+ seenWarnings.add(key);
3083
+ return true;
3084
+ });
3085
+ results.push({ plan });
3086
+ if (augmented !== entry.css) results.push({ plan: {
3087
+ files: [{
3088
+ path: entry.path,
3089
+ source: augmented
3090
+ }],
3091
+ deletedFiles: [],
3092
+ unlinkedFiles: [],
3093
+ candidates: [],
3094
+ rules: [],
3095
+ warnings: [],
3096
+ convertedRules: 0,
3097
+ retainedRules: 0
3098
+ } });
3099
+ return results;
3100
+ }
3101
+ function accumulateBlockedRules(blocked, plan, failing) {
3102
+ let progressed = false;
3103
+ for (const rule of plan.rules) {
3104
+ const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
3105
+ if (failed.length === 0) continue;
3106
+ let rules = blocked.get(rule.stylesheet);
3107
+ if (!rules) blocked.set(rule.stylesheet, rules = /* @__PURE__ */ new Map());
3108
+ const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
3109
+ let blockedRule = rules.get(key);
3110
+ if (!blockedRule) {
3111
+ rules.set(key, blockedRule = {
3112
+ ruleId: rule.ruleId,
3113
+ authoredSpan: rule.authoredSpan,
3114
+ candidates: /* @__PURE__ */ new Set()
3115
+ });
3116
+ progressed = true;
3117
+ }
3118
+ for (const candidate of failed) blockedRule.candidates.add(candidate);
3119
+ }
3120
+ return progressed;
3121
+ }
3122
+ async function entryUnsafe(entryPath, workspaceRoot, targetable) {
3123
+ if (!targetable.has(entryPath) || !isProjectInput(workspaceRoot, entryPath)) return true;
3124
+ return (await lstat(entryPath)).isSymbolicLink();
3125
+ }
3126
+ function atRuleIdentities(key, css) {
3127
+ return css.trim() === "" ? [] : stylesheetAnalysis(key, css).globalAtRuleIdentities;
3128
+ }
3129
+ function stylesheetAtRuleIdentities(stylesheet) {
3130
+ if (stylesheet.vueAtRuleIdentities) return stylesheet.vueAtRuleIdentities;
3131
+ if (stylesheet.vueBlocks) return stylesheet.vueBlocks.flatMap((block, index) => atRuleIdentities(`${stylesheet.cssPath}.${index}.at-rules.${block.analysisSource ? "css" : block.syntax}`, block.analysisSource ?? block.content));
3132
+ return stylesheet.analysisSource === void 0 ? atRuleIdentities(`${stylesheet.cssPath}.at-rules.${stylesheet.syntax ?? "css"}`, stylesheet.cssSource) : atRuleIdentities(`${stylesheet.cssPath}.at-rules.css`, stylesheet.analysisSource);
3133
+ }
3134
+ async function finishGroupPlan(context, members, plan) {
3135
+ const { snapshots } = context;
3136
+ const membersByRoot = new Map(members.map((member) => [member.packageRoot, member]));
3137
+ const ownerOf = (path) => {
3138
+ const root = context.pathOwners.get(path);
3139
+ return root === void 0 ? void 0 : membersByRoot.get(root);
3140
+ };
3141
+ for (const member of members) {
3142
+ plan.warnings.push(...member.preparedHtml.warnings);
3143
+ plan.warnings.push(...member.preparedVue.warnings);
2119
3144
  }
2120
- removeMigratedHtmlLinks(plan, preparedHtml);
2121
- plan.warnings.push(...preparedHtml.warnings);
2122
- plan.warnings.push(...preparedVue.warnings);
2123
3145
  try {
2124
- for (const file of plan.files.filter((file) => extname(file.path) === ".html")) parseHtmlSource(file.path, file.source);
3146
+ for (const file of plan.files.filter((file) => extname(file.path) === ".html")) try {
3147
+ parseHtmlSource(file.path, file.source);
3148
+ } catch (error) {
3149
+ return {
3150
+ error,
3151
+ member: ownerOf(file.path),
3152
+ fatal: true
3153
+ };
3154
+ }
2125
3155
  for (const file of plan.files.filter((file) => extname(file.path) === ".vue")) {
2126
- const vueCompiler = preparedVue.compiler;
3156
+ const owner = ownerOf(file.path);
3157
+ const preparedVue = owner?.preparedVue ?? members[0].preparedVue;
3158
+ const vueCompiler = preparedVue.compiler ?? members[0].preparedVue.compiler;
2127
3159
  if (!vueCompiler) throw new Error(`No Vue compiler is available to verify ${file.path}`);
2128
- const includeUnscoped = preparedVue.unscopedPaths.has(file.path);
2129
- const blocks = verifyVueSource(vueCompiler, file.path, file.source, includeUnscoped);
2130
- const untouched = /* @__PURE__ */ new Map();
2131
- for (const block of verifyVueSource(vueCompiler, file.path, snapshots.get(file.path) ?? "", includeUnscoped)) {
2132
- const key = `${block.syntax}\0${block.content}`;
2133
- untouched.set(key, (untouched.get(key) ?? 0) + 1);
2134
- }
2135
- for (const [index, block] of blocks.entries()) {
2136
- const key = `${block.syntax}\0${block.content}`;
2137
- const remaining = untouched.get(key) ?? 0;
2138
- if (remaining > 0) {
2139
- untouched.set(key, remaining - 1);
2140
- continue;
3160
+ try {
3161
+ const includeUnscoped = preparedVue.unscopedPaths.has(file.path);
3162
+ const blocks = verifyVueSource(vueCompiler, file.path, file.source, includeUnscoped);
3163
+ const untouched = /* @__PURE__ */ new Map();
3164
+ for (const block of verifyVueSource(vueCompiler, file.path, snapshots.get(file.path) ?? "", includeUnscoped)) {
3165
+ const key = `${block.syntax}\0${block.content}`;
3166
+ untouched.set(key, (untouched.get(key) ?? 0) + 1);
2141
3167
  }
2142
- if (block.syntax === "css") validateCss(block.content);
2143
- else validateCss((await compileStyleEntry(styleCompilers, packageRoot, `${file.path}.${index}.${block.syntax}`, block.content, { virtualEntry: true })).css);
3168
+ for (const [index, block] of blocks.entries()) {
3169
+ const key = `${block.syntax}\0${block.content}`;
3170
+ const remaining = untouched.get(key) ?? 0;
3171
+ if (remaining > 0) {
3172
+ untouched.set(key, remaining - 1);
3173
+ continue;
3174
+ }
3175
+ if (block.syntax === "css") validateCss(block.content);
3176
+ else {
3177
+ const compilerOwner = owner ?? members[0];
3178
+ validateCss((await compileStyleEntry(compilerOwner.styleCompilers, compilerOwner.packageRoot, `${file.path}.${index}.${block.syntax}`, block.content, { virtualEntry: true })).css);
3179
+ }
3180
+ }
3181
+ } catch (error) {
3182
+ return {
3183
+ error,
3184
+ member: owner,
3185
+ fatal: !isMissingStyleCompilerError(error)
3186
+ };
2144
3187
  }
2145
3188
  }
2146
- for (const stylesheet of stylesheets.filter((stylesheet) => isPreprocessorPath(stylesheet.cssPath))) {
3189
+ for (const member of members) for (const stylesheet of member.stylesheets.filter((stylesheet) => isPreprocessorPath(stylesheet.cssPath))) {
2147
3190
  const changed = plan.files.find((file) => file.path === stylesheet.cssPath);
2148
3191
  if (!changed && !plan.deletedFiles.includes(stylesheet.cssPath)) continue;
2149
3192
  const source = changed?.source ?? "";
2150
- validateCss((await compileStyleEntry(styleCompilers, packageRoot, stylesheet.cssPath, source)).css);
2151
- }
2152
- } catch (error) {
2153
- return recover(error, !isMissingStyleCompilerError(error));
2154
- }
2155
- return { plan };
2156
- }
2157
- function replanCompileFailures(tailwind, request, initialPlan) {
2158
- let plan = initialPlan;
2159
- const blockedByStylesheet = /* @__PURE__ */ new Map();
2160
- while (true) {
2161
- const failing = invalidCandidates(tailwind, plan.candidates);
2162
- if (failing.length === 0) break;
2163
- let progressed = false;
2164
- for (const rule of plan.rules) {
2165
- const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
2166
- if (failed.length === 0) continue;
2167
- let blocked = blockedByStylesheet.get(rule.stylesheet);
2168
- if (!blocked) blockedByStylesheet.set(rule.stylesheet, blocked = /* @__PURE__ */ new Map());
2169
- const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
2170
- let entry = blocked.get(key);
2171
- if (!entry) {
2172
- blocked.set(key, entry = {
2173
- ruleId: rule.ruleId,
2174
- authoredSpan: rule.authoredSpan,
2175
- candidates: /* @__PURE__ */ new Set()
2176
- });
2177
- progressed = true;
3193
+ try {
3194
+ validateCss((await compileStyleEntry(member.styleCompilers, member.packageRoot, stylesheet.cssPath, source)).css);
3195
+ } catch (error) {
3196
+ return {
3197
+ error,
3198
+ member,
3199
+ fatal: !isMissingStyleCompilerError(error)
3200
+ };
2178
3201
  }
2179
- for (const candidate of failed) entry.candidates.add(candidate);
2180
- }
2181
- if (!progressed) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
2182
- plan = JSON.parse(planBatchMigration(JSON.stringify({
2183
- ...request,
2184
- stylesheets: request.stylesheets.map((stylesheet, index) => ({
2185
- ...stylesheet,
2186
- blockedRules: [...blockedByStylesheet.get(index)?.values() ?? []].map((entry) => entry.ruleId)
2187
- }))
2188
- })));
2189
- }
2190
- for (const [index, blocked] of blockedByStylesheet) {
2191
- const cssPath = request.stylesheets[index].cssPath;
2192
- for (const { authoredSpan, candidates } of blocked.values()) {
2193
- const failed = [...candidates].sort().map((candidate) => `\`${candidate}\``).join(", ");
2194
- plan.warnings.push({
2195
- code: "candidate-compilation-failure",
2196
- file: cssPath,
2197
- start: authoredSpan.start,
2198
- end: authoredSpan.end,
2199
- message: `Tailwind did not generate CSS for ${failed}, so the rule is retained.`
2200
- });
2201
3202
  }
3203
+ } catch (error) {
3204
+ return {
3205
+ error,
3206
+ fatal: !isMissingStyleCompilerError(error)
3207
+ };
2202
3208
  }
2203
- return plan;
2204
3209
  }
2205
- function removeMigratedHtmlLinks(plan, preparedHtml) {
3210
+ function removeGroupHtmlLinks(plan, members) {
2206
3211
  const unlinked = new Set(plan.unlinkedFiles);
2207
3212
  const linksByFile = /* @__PURE__ */ new Map();
2208
- for (const link of preparedHtml.removableLinks) {
2209
- if (!unlinked.has(link.cssPath)) continue;
2210
- const links = linksByFile.get(link.filePath) ?? /* @__PURE__ */ new Set();
2211
- links.add(`${link.href}\0${link.media}`);
2212
- linksByFile.set(link.filePath, links);
3213
+ const originals = /* @__PURE__ */ new Map();
3214
+ for (const member of members) {
3215
+ for (const link of member.preparedHtml.removableLinks) {
3216
+ if (!unlinked.has(link.cssPath)) continue;
3217
+ const links = linksByFile.get(link.filePath) ?? /* @__PURE__ */ new Set();
3218
+ links.add(`${link.href}\0${link.media}`);
3219
+ linksByFile.set(link.filePath, links);
3220
+ }
3221
+ for (const file of member.preparedHtml.files) if (!originals.has(file.path)) originals.set(file.path, file.source);
2213
3222
  }
2214
3223
  for (const [filePath, removable] of linksByFile) {
2215
3224
  const planned = plan.files.find((file) => file.path === filePath);
2216
- const original = preparedHtml.files.find((file) => file.path === filePath);
2217
- if (!original) continue;
2218
- const source = planned?.source ?? original.source;
3225
+ const source = planned?.source ?? originals.get(filePath);
3226
+ if (source === void 0) continue;
2219
3227
  const links = parseHtmlSource(filePath, source).links.filter((link) => removable.has(`${link.href}\0${link.media}`)).sort((left, right) => right.tagStart - left.tagStart);
2220
3228
  let bytes = Buffer.from(source);
2221
3229
  for (const link of links) bytes = Buffer.concat([bytes.subarray(0, link.tagStart), bytes.subarray(link.tagEnd)]);