tw-migrate 0.3.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 RECOVERABLE_PARSE_ERRORS = /* @__PURE__ */ new Set(["missing-doctype"]);
23
- const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
24
- function parseHtmlSource(path, source) {
25
- const errors = [];
26
- const document = parse(source, {
27
- sourceCodeLocationInfo: true,
28
- onParseError(error) {
29
- if (!RECOVERABLE_PARSE_ERRORS.has(error.code)) errors.push(error);
30
- }
31
- });
32
- if (errors.length > 0) throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
33
- const links = [];
34
- const bases = [];
35
- const elements = [];
36
- const dynamicAttributes = [];
37
- const scriptTexts = [];
38
- const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
39
- function visit(node) {
40
- if (node.tagName === "script") {
41
- for (const child of node.childNodes ?? []) if (child.nodeName === "#text" && child.value) scriptTexts.push(child.value);
42
- }
43
- if (node.tagName) {
44
- const attributes = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
45
- const locations = node.sourceCodeLocation?.attrs;
46
- if (locations) {
47
- if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
48
- const href = locatedAttribute(source, locations.href, attributes.get("href"));
49
- const media = locatedAttribute(source, locations.media, attributes.get("media"));
50
- const tag = node.sourceCodeLocation?.startTag ?? node.sourceCodeLocation;
51
- if (href && tag) links.push({
52
- href: href.value,
53
- media: media?.value ?? "",
54
- start: href.start,
55
- end: href.end,
56
- tagStart: tag.startOffset,
57
- tagEnd: tag.endOffset
58
- });
59
- }
60
- if (node.tagName === "base" && bases.length === 0) {
61
- const href = locatedAttribute(source, locations.href, attributes.get("href"));
62
- if (href) bases.push({
63
- href: href.value,
64
- writable: href.writable,
65
- start: href.start,
66
- end: href.end
67
- });
68
- }
69
- let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
70
- const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
71
- const dynamic = (!classAttribute && idAttribute && locations.class ? {
72
- start: locations.class.startOffset,
73
- end: locations.class.endOffset
74
- } : void 0) ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
75
- if (dynamic) dynamicAttributes.push({
76
- start: dynamic.start,
77
- end: dynamic.end
78
- });
79
- else if (classAttribute || idAttribute) {
80
- if (!classAttribute && idAttribute) {
81
- const insertion = classInsertionOffset$1(source, node.sourceCodeLocation?.startTag);
82
- if (insertion !== void 0) classAttribute = {
83
- value: "",
84
- start: insertion,
85
- end: insertion,
86
- synthetic: true
87
- };
88
- }
89
- elements.push({
90
- classAttribute,
91
- idAttribute
92
- });
93
- }
94
- }
95
- }
96
- for (const child of node.childNodes ?? []) visit(child);
97
- if (node.content) visit(node.content);
98
- }
99
- visit(document);
100
- return {
101
- ...toByteOffsets(source, {
102
- links,
103
- bases,
104
- elements,
105
- dynamicAttributes
106
- }),
107
- scriptText: scriptTexts.join("\n")
108
- };
109
- }
110
- function utf8OffsetMap(source, indices) {
111
- const sorted = [...new Set(indices)].sort((left, right) => left - right);
112
- const map = /* @__PURE__ */ new Map();
113
- let lastIndex = 0;
114
- let bytes = 0;
115
- for (const index of sorted) {
116
- bytes += Buffer.byteLength(source.slice(lastIndex, index));
117
- lastIndex = index;
118
- map.set(index, bytes);
119
- }
120
- return map;
121
- }
122
- function offsetLookup(offsets) {
123
- return (index) => {
124
- const byte = offsets.get(index);
125
- if (byte === void 0) throw new Error(`No byte offset was mapped for index ${index}`);
126
- return byte;
127
- };
128
- }
129
- function toByteOffsets(source, parsed) {
130
- const offset = offsetLookup(utf8OffsetMap(source, [
131
- ...parsed.links.flatMap((link) => [
132
- link.start,
133
- link.end,
134
- link.tagStart,
135
- link.tagEnd
136
- ]),
137
- ...parsed.bases.flatMap((base) => [base.start, base.end]),
138
- ...parsed.elements.flatMap((element) => [element.classAttribute, element.idAttribute].flatMap((value) => value ? [value.start, value.end] : [])),
139
- ...parsed.dynamicAttributes.flatMap((value) => [value.start, value.end])
140
- ]));
141
- const attribute = (value) => ({
142
- ...value,
143
- start: offset(value.start),
144
- end: offset(value.end)
145
- });
146
- return {
147
- links: parsed.links.map((link) => ({
148
- ...link,
149
- start: offset(link.start),
150
- end: offset(link.end),
151
- tagStart: offset(link.tagStart),
152
- tagEnd: offset(link.tagEnd)
153
- })),
154
- bases: parsed.bases.map((base) => attribute(base)),
155
- elements: parsed.elements.map((element) => ({
156
- classAttribute: element.classAttribute && attribute(element.classAttribute),
157
- idAttribute: element.idAttribute && attribute(element.idAttribute)
158
- })),
159
- dynamicAttributes: parsed.dynamicAttributes.map((value) => attribute(value))
160
- };
161
- }
162
- function classInsertionOffset$1(source, startTag) {
163
- if (!startTag) return void 0;
164
- let offset = startTag.endOffset - 1;
165
- if (source[offset] !== ">") return void 0;
166
- while (offset > startTag.startOffset && /\s/.test(source[offset - 1] ?? "")) offset -= 1;
167
- if (source[offset - 1] === "/") offset -= 1;
168
- return offset;
169
- }
170
- function stylesheetRel(value = "") {
171
- const tokens = value.toLowerCase().split(/\s+/);
172
- return tokens.includes("stylesheet") && !tokens.includes("alternate");
173
- }
174
- function locatedAttribute(source, location, parsedValue) {
175
- if (!location || parsedValue === void 0) return void 0;
176
- const raw = source.slice(location.startOffset, location.endOffset);
177
- const equals = raw.indexOf("=");
178
- if (equals < 0) return void 0;
179
- let start = equals + 1;
180
- while (/\s/.test(raw[start] ?? "")) start += 1;
181
- const quote = raw[start];
182
- let end;
183
- if (quote === "\"" || quote === "'") {
184
- start += 1;
185
- end = raw.lastIndexOf(quote);
186
- if (end < start) return void 0;
187
- } else {
188
- end = start;
189
- while (end < raw.length && !/\s/.test(raw[end] ?? "")) end += 1;
190
- }
191
- const value = raw.slice(start, end);
192
- return {
193
- value: value.includes("&") ? parsedValue : value,
194
- writable: !value.includes("&"),
195
- start: location.startOffset + start,
196
- end: location.startOffset + end
197
- };
198
- }
199
- //#endregion
200
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"]);
@@ -240,11 +132,13 @@ async function loadProjectModule(packageRoot, name, errorMessage) {
240
132
  const imported = await import(pathToFileURL(modulePath).href);
241
133
  return imported.default ?? imported;
242
134
  }
135
+ const missingCompilerMessage = (name) => `${name} must be installed in the target project.`;
136
+ const MISSING_STYLE_COMPILER_MESSAGES = /* @__PURE__ */ new Set([missingCompilerMessage("Sass"), missingCompilerMessage("Less")]);
243
137
  function loadProjectSass(packageRoot) {
244
- return loadProjectModule(packageRoot, "sass", "Sass must be installed in the target project.");
138
+ return loadProjectModule(packageRoot, "sass", missingCompilerMessage("Sass"));
245
139
  }
246
140
  function loadProjectLess(packageRoot) {
247
- return loadProjectModule(packageRoot, "less", "Less must be installed in the target project.");
141
+ return loadProjectModule(packageRoot, "less", missingCompilerMessage("Less"));
248
142
  }
249
143
  async function compileStyleEntry(compilers, packageRoot, entryPath, source, options = {}) {
250
144
  if (isSassPath(entryPath)) return compileSassEntry(compilers.sass ??= await loadProjectSass(packageRoot), entryPath, source, options);
@@ -353,8 +247,8 @@ const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
353
247
  "node_modules"
354
248
  ]);
355
249
  const RECOVERABLE_INPUT_ERROR = "TW_MIGRATE_RECOVERABLE_INPUT:";
356
- function errorCode(error) {
357
- return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
250
+ function errorMessage(error) {
251
+ return error instanceof Error ? error.message : String(error);
358
252
  }
359
253
  function isWithin(root, path) {
360
254
  return path === root || path.startsWith(`${root}${sep}`);
@@ -369,15 +263,6 @@ function isStylesheetModule(path) {
369
263
  const syntax = stylesheetSyntax(path);
370
264
  return syntax !== void 0 && path.endsWith(`.module.${syntax}`);
371
265
  }
372
- function sourceReferencesStyle(file, stylePath) {
373
- let importPath = normalizedRelativePath(dirname(file.path), stylePath);
374
- if (!importPath.startsWith(".")) importPath = `./${importPath}`;
375
- return [
376
- `'${importPath}'`,
377
- `"${importPath}"`,
378
- `\`${importPath}\``
379
- ].some((literal) => file.source.includes(literal));
380
- }
381
266
  function isProjectInput(workspaceRoot, path) {
382
267
  return isWithin(workspaceRoot, path) && !relative(workspaceRoot, path).split(/[\\/]/).includes("node_modules");
383
268
  }
@@ -404,43 +289,44 @@ function recordSnapshot(snapshots, path, source) {
404
289
  return source;
405
290
  }
406
291
  function packageFailure(workspaceRoot, packageRoot, error) {
407
- const message = error instanceof Error ? error.message : String(error);
292
+ const message = errorMessage(error);
408
293
  return {
409
294
  package: normalizedRelativePath(workspaceRoot, packageRoot) || ".",
410
295
  message: message.startsWith("TW_MIGRATE_RECOVERABLE_INPUT:") ? message.slice(29) : message
411
296
  };
412
297
  }
413
298
  function isRecoverablePlanningError(error) {
414
- return (error instanceof Error ? error.message : String(error)).startsWith(RECOVERABLE_INPUT_ERROR);
299
+ return errorMessage(error).startsWith(RECOVERABLE_INPUT_ERROR);
415
300
  }
416
301
  function isMissingStyleCompilerError(error) {
417
- const message = error instanceof Error ? error.message : String(error);
418
- return /^(?:Sass|Less) must be installed in the target project\.$/.test(message);
302
+ return MISSING_STYLE_COMPILER_MESSAGES.has(errorMessage(error));
419
303
  }
420
304
  function isIntegrityError(error) {
421
- return (error instanceof Error ? error.message : String(error)).startsWith("Source changed during planning:");
305
+ return errorMessage(error).startsWith("Source changed during planning:");
422
306
  }
423
307
  function normalizedRelativePath(root, path) {
424
308
  return relative(root, path).split(sep).join("/");
425
309
  }
426
- function maskCssComments(source) {
427
- return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, " "));
428
- }
429
310
  function indexStylesheetDependents(styleSources) {
430
311
  const dependents = /* @__PURE__ */ new Map();
431
- for (const [path, rawSource] of styleSources) {
432
- const source = maskCssComments(rawSource);
433
- const references = [
434
- ...[...source.matchAll(/composes\s*:[^;{}]*?\bfrom\s+["']([^"']+)["']/g)].map((match) => match[1]),
435
- ...[...source.matchAll(/@(?:use|forward)\s+["']([^"']+)["']/g)].map((match) => match[1]),
436
- ...[...source.matchAll(/@import\s+(?:\([^)]*\)\s*)?(?:([^;{}]+);|([^;{}\r\n]+))/g)].flatMap((statement) => [...(statement[1] ?? statement[2]).matchAll(/["']([^"']+)["']/g)].map((match) => match[1])),
437
- ...[...source.matchAll(/@import\s+url\(\s*([^"'()\s]+)\s*\)/g)].map((match) => match[1])
438
- ];
439
- for (const reference of new Set(references)) for (const target of stylesheetReferenceTargets(path, reference, styleSources)) {
440
- if (target === path || !isStylesheetModule(target) && !isPreprocessorPath(target)) continue;
441
- const paths = dependents.get(target) ?? [];
442
- paths.push(path);
443
- dependents.set(target, paths);
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
+ }
444
330
  }
445
331
  }
446
332
  for (const [target, paths] of dependents) dependents.set(target, [...new Set(paths)].sort());
@@ -462,63 +348,8 @@ function stylesheetReferenceTargets(importer, reference, styleSources) {
462
348
  join(target, `index${syntax}`)
463
349
  ])).filter((path) => styleSources.has(path));
464
350
  }
465
- function cssImports(source) {
466
- const imports = [];
467
- const masked = maskCssComments(source);
468
- let depth = 0;
469
- let quote;
470
- let importsAllowed = true;
471
- for (let index = 0; index < masked.length; index += 1) {
472
- const character = masked[index];
473
- if (quote) {
474
- if (character === "\\") index += 1;
475
- else if (character === quote) quote = void 0;
476
- continue;
477
- }
478
- if (character === "\"" || character === "'") {
479
- quote = character;
480
- continue;
481
- }
482
- if (character === "{") {
483
- depth += 1;
484
- importsAllowed = false;
485
- continue;
486
- }
487
- if (character === "}") {
488
- depth = Math.max(0, depth - 1);
489
- continue;
490
- }
491
- if (!importsAllowed || depth !== 0 || masked.slice(index, index + 7).toLowerCase() !== "@import" || /[-\w]/.test(masked[index + 7] ?? "")) continue;
492
- let end = index + 7;
493
- let importQuote;
494
- let parentheses = 0;
495
- for (; end < masked.length; end += 1) {
496
- const next = masked[end];
497
- if (importQuote) {
498
- if (next === "\\") end += 1;
499
- else if (next === importQuote) importQuote = void 0;
500
- } else if (next === "\"" || next === "'") importQuote = next;
501
- else if (next === "(") parentheses += 1;
502
- else if (next === ")") parentheses = Math.max(0, parentheses - 1);
503
- else if (next === ";" && parentheses === 0) break;
504
- }
505
- if (end >= masked.length) continue;
506
- const statement = masked.slice(index, end + 1);
507
- const match = /^@import\s+(?:url\(\s*)?(?:["']([^"']+)["']|([^"'()\s;]+))\s*\)?\s*([^;]*);$/i.exec(statement);
508
- if (match) imports.push({
509
- href: match[1] ?? match[2],
510
- media: match[3].trim(),
511
- start: index,
512
- end: end + 1
513
- });
514
- index = end;
515
- }
516
- const offsets = utf8OffsetMap(source, imports.flatMap((imported) => [imported.start, imported.end]));
517
- return imports.map((imported) => ({
518
- ...imported,
519
- start: offsets.get(imported.start) ?? imported.start,
520
- end: offsets.get(imported.end) ?? imported.end
521
- }));
351
+ function cssImports(path, source) {
352
+ return stylesheetAnalysis(path, source).imports;
522
353
  }
523
354
  //#endregion
524
355
  //#region src/discovery.ts
@@ -528,7 +359,7 @@ async function resolveScope(options) {
528
359
  const currentPackage = await findPackageRoot(cwd);
529
360
  const gitRoot = await findGitRoot(currentPackage);
530
361
  const workspaceRoot = gitRoot && !await isIgnoredByGit(gitRoot, currentPackage) ? gitRoot : currentPackage;
531
- const allPaths = await discoverFiles(workspaceRoot, workspaceRoot === gitRoot);
362
+ const allPaths = workspaceRoot === gitRoot ? await discoverGitFiles(workspaceRoot) : await collectFiles(workspaceRoot, isRelevantDiscoveredFile);
532
363
  const scannedPaths = workspaceRoot === gitRoot ? [.../* @__PURE__ */ new Set([...allPaths, ...await collectFiles(workspaceRoot, isRelevantDiscoveredFile)])] : [...allPaths];
533
364
  const explicitStyle = options.styleFile ? resolve(cwd, options.styleFile) : void 0;
534
365
  const configuredEntry = options.tailwindCss ? resolve(cwd, options.tailwindCss) : void 0;
@@ -589,6 +420,28 @@ async function findGitRoot(cwd) {
589
420
  return;
590
421
  }
591
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
+ }
592
445
  async function isIgnoredByGit(gitRoot, path) {
593
446
  if (gitRoot === path) return false;
594
447
  try {
@@ -603,8 +456,7 @@ async function isIgnoredByGit(gitRoot, path) {
603
456
  return false;
604
457
  }
605
458
  }
606
- async function discoverFiles(root, useGit) {
607
- if (!useGit) return collectFiles(root, isRelevantDiscoveredFile);
459
+ async function discoverGitFiles(root) {
608
460
  const { stdout } = await run("git", [
609
461
  "ls-files",
610
462
  "-co",
@@ -614,7 +466,7 @@ async function discoverFiles(root, useGit) {
614
466
  "."
615
467
  ], {
616
468
  cwd: root,
617
- maxBuffer: 64 * 1024 * 1024
469
+ maxBuffer: 67108864
618
470
  });
619
471
  const paths = stdout.split("\0").filter(Boolean).map((path) => resolve(root, path)).filter((path) => !hasIgnoredDirectory(root, path) && isRelevantDiscoveredFile(path));
620
472
  return (await Promise.all(paths.map(async (path) => {
@@ -645,7 +497,6 @@ async function collectFiles(root, include) {
645
497
  const files = [];
646
498
  async function visit(directory) {
647
499
  const entries = await readdir(directory, { withFileTypes: true });
648
- entries.sort((left, right) => left.name.localeCompare(right.name));
649
500
  for (const entry of entries) {
650
501
  const path = join(directory, entry.name);
651
502
  if (entry.isDirectory()) {
@@ -657,6 +508,478 @@ async function collectFiles(root, include) {
657
508
  return files;
658
509
  }
659
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
+ };
959
+ }
960
+ function htmlLinksEntry(file, entry) {
961
+ if (extname(file.path) !== ".html") return false;
962
+ try {
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;
974
+ }
975
+ }
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);
978
+ }
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);
981
+ }
982
+ //#endregion
660
983
  //#region src/plan/html.ts
661
984
  async function preparePackageHtml({ packageRoot, sourceFiles, styleSources, snapshots, pathOwners, styleDependents }) {
662
985
  const files = [];
@@ -781,7 +1104,7 @@ async function collectHtmlStyleContexts(state) {
781
1104
  analyzable: true
782
1105
  });
783
1106
  if (extname(state.path) !== ".css") return;
784
- for (const imported of cssImports(source)) {
1107
+ for (const imported of cssImports(state.path, source)) {
785
1108
  const variants = mediaVariants(imported.media);
786
1109
  if (variants === void 0) {
787
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.`));
@@ -804,7 +1127,7 @@ function inferredPreprocessorPath(state) {
804
1127
  return matches.length === 1 ? matches[0] : void 0;
805
1128
  }
806
1129
  function addInferredPreprocessorContext(state) {
807
- 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;
808
1131
  const path = inferredPreprocessorPath(state);
809
1132
  if (!path) return false;
810
1133
  state.generatedPaths.add(state.path);
@@ -855,22 +1178,14 @@ function htmlWarning(code, file, start, end, message) {
855
1178
  }
856
1179
  //#endregion
857
1180
  //#region src/tailwind.ts
858
- function resolveTailwindEntry(stylePaths, styleSources, configuredPath) {
859
- const entries = stylePaths.filter((path) => {
860
- if (extname(path) !== ".css") return false;
861
- const source = maskCssComments(styleSources.get(path) ?? "");
862
- return /@import\s+["']tailwindcss(?:\/[^"']*)?["']/.test(source);
863
- });
864
- if (configuredPath) return {
865
- path: configuredPath,
866
- entries
867
- };
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;
868
1186
  if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
869
1187
  if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
870
- return {
871
- path: entries[0],
872
- entries
873
- };
1188
+ return entries[0];
874
1189
  }
875
1190
  async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
876
1191
  const projectRequire = createRequire(join(packageRoot, "package.json"));
@@ -884,47 +1199,10 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
884
1199
  if (!String(packageJson.version).startsWith("4.")) throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
885
1200
  const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
886
1201
  const css = await snapshotFile(snapshots, tailwindCss);
887
- const base = dirname(tailwindCss);
888
- const loadModule = createModuleLoader(snapshots, workspaceRoot);
889
- const loadStylesheet = createStylesheetLoader(projectRequire, packagePath, snapshots, workspaceRoot);
890
- const themeTokens = {
891
- ...extractThemeTokens(await readFile(join(dirname(packagePath), "theme.css"), "utf8")),
892
- ...await extractThemeTokensFromGraph(css, base, loadStylesheet)
893
- };
894
- return {
895
- designSystem: await loadDesignSystem(css, {
896
- base,
897
- loadModule,
898
- loadStylesheet
899
- }),
900
- css,
901
- path: tailwindCss,
902
- themeTokens
903
- };
904
- }
905
- function extractThemeTokens(css) {
906
- const tokens = {};
907
- for (const block of css.matchAll(/@theme[^{]*\{([^}]*)\}/gs)) for (const match of block[1].matchAll(/--([\w-]+):\s*([^;{}]+);/g)) tokens[match[1]] = match[2].trim();
908
- return tokens;
909
- }
910
- async function extractThemeTokensFromGraph(css, base, loadStylesheet, seen = /* @__PURE__ */ new Set()) {
911
- const tokens = {};
912
- for (const match of css.matchAll(/@import\s+["']([^"']+)["']/g)) {
913
- const key = `${base}\0${match[1]}`;
914
- if (seen.has(key)) continue;
915
- seen.add(key);
916
- const loaded = await loadStylesheet(match[1], base);
917
- Object.assign(tokens, await extractThemeTokensFromGraph(loaded.content, loaded.base, loadStylesheet, seen));
918
- }
919
- return Object.assign(tokens, extractThemeTokens(css));
920
- }
921
- function invalidCandidates(tailwind, candidates) {
922
- const generated = tailwind.designSystem.candidatesToCss(candidates);
923
- return candidates.filter((_, index) => generated[index] === null);
924
- }
925
- function createModuleLoader(snapshots, workspaceRoot) {
926
- return async (id, base) => {
927
- const path = createRequire(join(base, "package.json")).resolve(id);
1202
+ const base = dirname(tailwindCss);
1203
+ const tailwindRoot = dirname(packagePath);
1204
+ const loadModule = async (id, moduleBase) => {
1205
+ const path = createRequire(join(moduleBase, "package.json")).resolve(id);
928
1206
  if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
929
1207
  const imported = await import(pathToFileURL(path).href);
930
1208
  return {
@@ -933,27 +1211,278 @@ function createModuleLoader(snapshots, workspaceRoot) {
933
1211
  module: imported.default ?? imported
934
1212
  };
935
1213
  };
936
- }
937
- function createStylesheetLoader(projectRequire, tailwindPackagePath, snapshots, workspaceRoot) {
938
- const tailwindRoot = dirname(tailwindPackagePath);
939
- return async (id, base) => {
1214
+ const loadStylesheet = async (id, sheetBase) => {
940
1215
  let path;
941
1216
  if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
942
1217
  else if (id.startsWith("tailwindcss/")) {
943
1218
  const subpath = id.slice(12);
944
1219
  path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
945
- } else if (id.startsWith(".") || isAbsolute(id)) path = resolve(base, id);
1220
+ } else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
946
1221
  else path = projectRequire.resolve(id);
947
1222
  return {
948
1223
  content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
949
1224
  base: dirname(path)
950
1225
  };
951
1226
  };
1227
+ const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
1228
+ const graphSources = [];
1229
+ const themeTokens = {
1230
+ ...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
1231
+ ...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources)
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
+ });
1247
+ return {
1248
+ designSystem,
1249
+ css,
1250
+ path: tailwindCss,
1251
+ themeTokens,
1252
+ graphSources,
1253
+ loadWith
1254
+ };
1255
+ }
1256
+ function extractThemeTokens(key, css) {
1257
+ return stylesheetAnalysis(key, css).themeTokens;
1258
+ }
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()) {
1263
+ const tokens = {};
1264
+ for (const spec of importSpecifiers(css)) {
1265
+ const key = `${base}\0${spec}`;
1266
+ if (seen.has(key)) continue;
1267
+ seen.add(key);
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));
1274
+ }
1275
+ return Object.assign(tokens, extractThemeTokens(`${identity}.theme.css`, css));
1276
+ }
1277
+ function invalidCandidates(system, candidates) {
1278
+ const generated = system.candidatesToCss(candidates);
1279
+ return candidates.filter((_, index) => generated[index] === null);
1280
+ }
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;
952
1483
  }
953
1484
  //#endregion
954
1485
  //#region src/parser/vue.ts
955
- const ESCAPE_SELECTOR = /(?:::v-|:)(?:deep|global|slotted)\(([^)]*)\)/g;
956
- const ESCAPE_RESIDUE = /(?:>>>|\/deep\/|::v-deep|:deep|::v-slotted|:slotted|::v-global|:global)/;
957
1486
  const SUPPORTED_STYLE_ATTRIBUTES = /* @__PURE__ */ new Set([
958
1487
  "lang",
959
1488
  "module",
@@ -967,6 +1496,13 @@ const SUPPORTED_STYLE_LANGUAGES = /* @__PURE__ */ new Set([
967
1496
  "sass",
968
1497
  "less"
969
1498
  ]);
1499
+ const SUPPORTED_SCRIPT_LANGUAGES = /* @__PURE__ */ new Set([
1500
+ void 0,
1501
+ "js",
1502
+ "jsx",
1503
+ "ts",
1504
+ "tsx"
1505
+ ]);
970
1506
  const NODE_ELEMENT = 1;
971
1507
  const NODE_TEXT = 2;
972
1508
  const NODE_INTERPOLATION = 5;
@@ -997,29 +1533,48 @@ function analyzeVueSource(compiler, path, source) {
997
1533
  message
998
1534
  });
999
1535
  const { descriptor, errors } = compiler.parse(source, { filename: path });
1536
+ const styleRanges = descriptor.styles.map((style) => ({
1537
+ start: style.loc.start.offset,
1538
+ end: style.loc.end.offset
1539
+ }));
1540
+ const byteRanges = (offset) => ({
1541
+ warnings: warnings.map((warning) => ({
1542
+ ...warning,
1543
+ start: offset(warning.start),
1544
+ end: offset(warning.end)
1545
+ })),
1546
+ styleRanges: styleRanges.map((range) => ({
1547
+ start: offset(range.start),
1548
+ end: offset(range.end)
1549
+ }))
1550
+ });
1551
+ const retained = () => {
1552
+ const offsets = utf8OffsetMap(source, [...warnings, ...styleRanges].flatMap((range) => [range.start, range.end]));
1553
+ return {
1554
+ retained: true,
1555
+ ...byteRanges(offsetLookup(offsets))
1556
+ };
1557
+ };
1000
1558
  if (errors.length > 0) {
1001
1559
  const offset = errors[0]?.loc?.start?.offset ?? 0;
1002
1560
  warn("unsupported-sfc-block", offset, offset, `The SFC could not be parsed: ${errors[0].message}`);
1003
- return {
1004
- warnings: toByteWarnings(source, warnings),
1005
- retained: true
1006
- };
1561
+ if (errors[0]?.loc?.start?.offset === 0) Object.assign(warnings[0], {
1562
+ line: 1,
1563
+ column: 1,
1564
+ endLine: 1,
1565
+ endColumn: 1
1566
+ });
1567
+ return retained();
1007
1568
  }
1008
1569
  const template = descriptor.template;
1009
1570
  if (!template || template.lang || template.src) {
1010
1571
  warn("unsupported-sfc-block", 0, 0, !template ? "The SFC has no template block, so its class usage cannot be analyzed." : "Only the default template language with inline content is analyzed.");
1011
- return {
1012
- warnings: toByteWarnings(source, warnings),
1013
- retained: true
1014
- };
1572
+ return retained();
1015
1573
  }
1016
1574
  if (descriptor.customBlocks.length > 0) {
1017
1575
  const block = descriptor.customBlocks[0];
1018
1576
  warn("unsupported-sfc-block", block.loc.start.offset, block.loc.end.offset, `The custom <${block.type}> block is not analyzed.`);
1019
- return {
1020
- warnings: toByteWarnings(source, warnings),
1021
- retained: true
1022
- };
1577
+ return retained();
1023
1578
  }
1024
1579
  const blocks = [];
1025
1580
  const unscopedBlocks = [];
@@ -1031,6 +1586,7 @@ function analyzeVueSource(compiler, path, source) {
1031
1586
  const shadowPreprocessorTexts = [];
1032
1587
  const unscopedShadowPreprocessorTexts = [];
1033
1588
  let escapeUnverifiable = false;
1589
+ let opaqueStyleBlocks = false;
1034
1590
  let moduleSiblingUnsupported = false;
1035
1591
  for (const style of descriptor.styles) {
1036
1592
  const start = style.loc.start.offset;
@@ -1041,6 +1597,7 @@ function analyzeVueSource(compiler, path, source) {
1041
1597
  warn("unsupported-sfc-block", start, end, `Unsupported <style> attributes: ${unsupportedAttributes.join(", ")}.`);
1042
1598
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1043
1599
  escapeUnverifiable = true;
1600
+ opaqueStyleBlocks = true;
1044
1601
  continue;
1045
1602
  }
1046
1603
  if (style.src !== void 0) {
@@ -1048,6 +1605,7 @@ function analyzeVueSource(compiler, path, source) {
1048
1605
  warn("unsupported-sfc-block", start, end, "A <style module src> block is not supported.");
1049
1606
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1050
1607
  escapeUnverifiable = true;
1608
+ opaqueStyleBlocks = true;
1051
1609
  } else styleBlockImports.push({
1052
1610
  reference: style.src,
1053
1611
  start,
@@ -1059,12 +1617,19 @@ function analyzeVueSource(compiler, path, source) {
1059
1617
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1060
1618
  warn("unsupported-sfc-block", start, end, "Named <style module> blocks are not supported.");
1061
1619
  shadowModuleCssTexts.push(style.content);
1620
+ opaqueStyleBlocks = true;
1062
1621
  continue;
1063
1622
  }
1064
1623
  if (!SUPPORTED_STYLE_LANGUAGES.has(style.lang)) {
1065
1624
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1066
1625
  warn("preprocessor-style-block", start, end, `The <style lang="${style.lang}"> language is not supported.`);
1067
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
+ }
1068
1633
  continue;
1069
1634
  }
1070
1635
  const outerStart = source.lastIndexOf("<style", start);
@@ -1072,6 +1637,7 @@ function analyzeVueSource(compiler, path, source) {
1072
1637
  if (outerStart < 0 || !closing) {
1073
1638
  warn("unsupported-sfc-block", start, end, "The style block tags could not be located.");
1074
1639
  escapeUnverifiable = true;
1640
+ opaqueStyleBlocks = true;
1075
1641
  continue;
1076
1642
  }
1077
1643
  const block = {
@@ -1088,24 +1654,37 @@ function analyzeVueSource(compiler, path, source) {
1088
1654
  }
1089
1655
  if (!style.scoped) {
1090
1656
  unscopedBlocks.push(block);
1091
- if (style.lang && style.lang !== "css") unscopedShadowPreprocessorTexts.push(style.content);
1092
- 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);
1093
1665
  continue;
1094
1666
  }
1095
- for (const [, inner] of style.content.matchAll(ESCAPE_SELECTOR)) {
1096
- shadowCssTexts.push(`${inner} {}`);
1097
- 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;
1098
1674
  }
1099
- if (ESCAPE_RESIDUE.test(style.content.replace(ESCAPE_SELECTOR, " "))) escapeUnverifiable = true;
1100
1675
  blocks.push(block);
1101
1676
  }
1677
+ const scriptLang = descriptor.scriptSetup?.lang ?? descriptor.script?.lang;
1102
1678
  const state = {
1103
1679
  elements: [],
1104
1680
  components: [],
1105
1681
  dynamic: false,
1106
1682
  vHtml: false,
1107
1683
  hasSlot: false,
1108
- expressionTexts: []
1684
+ moduleClosureBroken: false,
1685
+ referencesUseCssModule: false,
1686
+ expressionReferences: /* @__PURE__ */ new Set(),
1687
+ expressionPath: scriptLang === "ts" || scriptLang === "tsx" ? "Component.ts" : "Component.js"
1109
1688
  };
1110
1689
  visitTemplateNode(source, template.ast, state);
1111
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 && [
@@ -1115,41 +1694,40 @@ function analyzeVueSource(compiler, path, source) {
1115
1694
  "for"
1116
1695
  ].includes(prop.name))).length;
1117
1696
  const scriptText = [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean).join("\n");
1118
- const fallthroughUnverifiable = Boolean(descriptor.script) || descriptor.scriptSetup?.src !== void 0 || /\b(?:defineOptions|defineProps|inheritAttrs)\b/.test(descriptor.scriptSetup?.content ?? "");
1119
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;
1120
1705
  const styleImports = [descriptor.script, descriptor.scriptSetup].flatMap((script) => {
1121
1706
  if (!script) return [];
1122
- if (script.src || ![
1123
- void 0,
1124
- "js",
1125
- "jsx",
1126
- "ts",
1127
- "tsx"
1128
- ].includes(script.lang)) {
1707
+ if (script.src || !SUPPORTED_SCRIPT_LANGUAGES.has(script.lang)) {
1129
1708
  scriptImportsUnverifiable = true;
1130
1709
  return [];
1131
1710
  }
1132
1711
  try {
1133
- 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;
1134
1721
  } catch {
1135
1722
  scriptImportsUnverifiable = true;
1136
1723
  return [];
1137
1724
  }
1138
1725
  });
1139
- let componentImports = [];
1140
- if (descriptor.scriptSetup && !descriptor.scriptSetup.src && [
1141
- void 0,
1142
- "js",
1143
- "jsx",
1144
- "ts",
1145
- "tsx"
1146
- ].includes(descriptor.scriptSetup.lang)) try {
1147
- componentImports = staticImportBindings(`${path}.${descriptor.scriptSetup.lang ?? "js"}`, descriptor.scriptSetup.content);
1148
- } catch {
1149
- componentImports = [];
1150
- }
1726
+ const fallthroughUnverifiable = Boolean(descriptor.script) || descriptor.scriptSetup?.src !== void 0 || Boolean(descriptor.scriptSetup) && !setupAnalysis || setupAnalysis?.hasVueFallthroughMacro === true;
1727
+ const componentImports = setupAnalysis?.defaultImports ?? [];
1151
1728
  const offsets = utf8OffsetMap(source, [
1152
1729
  ...warnings.flatMap((warning) => [warning.start, warning.end]),
1730
+ ...styleRanges.flatMap((range) => [range.start, range.end]),
1153
1731
  ...[
1154
1732
  ...blocks,
1155
1733
  ...unscopedBlocks,
@@ -1194,15 +1772,11 @@ function analyzeVueSource(compiler, path, source) {
1194
1772
  }
1195
1773
  });
1196
1774
  return {
1197
- warnings: warnings.map((warning) => ({
1198
- ...warning,
1199
- start: offset(warning.start),
1200
- end: offset(warning.end)
1201
- })),
1775
+ ...byteRanges(offset),
1202
1776
  blocks: blocks.map(toByteBlock),
1203
1777
  unscopedBlocks: unscopedBlocks.map(toByteBlock),
1204
1778
  moduleBlocks: moduleBlocks.map(toByteBlock),
1205
- 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)),
1206
1780
  htmlElements: state.elements.map(toByteSite),
1207
1781
  componentSites: state.components.map(toByteSite),
1208
1782
  rootStarts: template.ast.children.filter((node) => node.type === NODE_ELEMENT).map((node) => offsets.get(node.loc.start.offset)),
@@ -1210,7 +1784,13 @@ function analyzeVueSource(compiler, path, source) {
1210
1784
  rootFragment: template.ast.children.filter((node) => node.type === NODE_ELEMENT || node.type === NODE_INTERPOLATION || node.type === NODE_TEXT && node.content?.trim?.() !== "").length !== 1,
1211
1785
  scriptText,
1212
1786
  scriptStyleImports: [...new Set(styleImports)],
1787
+ scriptImports,
1213
1788
  scriptImportsUnverifiable,
1789
+ scriptHasDynamicImport,
1790
+ scriptVueReferences,
1791
+ scriptVueGlobPatterns,
1792
+ scriptVueGlobUnverifiable,
1793
+ hasOpaqueStyleBlocks: opaqueStyleBlocks,
1214
1794
  styleBlockImports: styleBlockImports.map((entry) => ({
1215
1795
  ...entry,
1216
1796
  start: offset(entry.start),
@@ -1247,27 +1827,36 @@ function visitTemplateNode(source, node, state) {
1247
1827
  let moduleBinding;
1248
1828
  for (const prop of node.props ?? []) {
1249
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;
1250
1832
  let provenModuleExpression = false;
1251
1833
  if (prop.name === "bind") {
1252
1834
  if (!prop.arg || !prop.arg.isStatic) classOpaque = true;
1253
1835
  else if (prop.arg.content === "class") {
1254
- const member = node.tagType === TAG_ELEMENT && prop.exp ? /^\s*\$style\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(prop.exp.content) : void 0;
1836
+ const member = node.tagType === TAG_ELEMENT ? expression?.vueModuleMember : void 0;
1255
1837
  if (member && !moduleBinding) {
1256
1838
  moduleBinding = {
1257
- name: member[1],
1839
+ name: member,
1258
1840
  start: attributeRemovalStart(source, node, prop),
1259
1841
  end: prop.loc.end.offset
1260
1842
  };
1261
1843
  provenModuleExpression = true;
1262
1844
  } else {
1263
- const value = staticClassBinding(prop);
1845
+ const value = expression?.staticString ?? void 0;
1264
1846
  if (value === void 0) classOpaque = true;
1265
1847
  else bindingClasses.push(...value.split(/[\t\n\f\r ]+/).filter(Boolean));
1266
1848
  }
1267
1849
  } else if (prop.arg.content === "id") classOpaque = true;
1268
1850
  }
1269
- if (!provenModuleExpression && prop.exp?.content) state.expressionTexts.push(prop.exp.content);
1270
- if (prop.arg && !prop.arg.isStatic && prop.arg.content) state.expressionTexts.push(prop.arg.content);
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
+ }
1271
1860
  if (prop.name === "html") state.vHtml = true;
1272
1861
  }
1273
1862
  if (classOpaque) state.dynamic = true;
@@ -1281,7 +1870,12 @@ function visitTemplateNode(source, node, state) {
1281
1870
  if (node.tagType === TAG_COMPONENT) state.components.push(element);
1282
1871
  else if (node.tagType === TAG_ELEMENT) state.elements.push(element);
1283
1872
  }
1284
- 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
+ }
1285
1879
  for (const child of node.children ?? []) visitTemplateNode(source, child, state);
1286
1880
  }
1287
1881
  function attributeRemovalStart(source, node, prop) {
@@ -1290,10 +1884,23 @@ function attributeRemovalStart(source, node, prop) {
1290
1884
  while (start > node.loc.start.offset + 1 && /\s/.test(source[start - 1])) start -= 1;
1291
1885
  return /[\r\n]/.test(source.slice(start, attributeStart)) ? attributeStart : start;
1292
1886
  }
1293
- function staticClassBinding(prop) {
1294
- if (!prop.exp) return void 0;
1887
+ function templateExpression(path, source) {
1888
+ try {
1889
+ return expressionAnalysis(path, source);
1890
+ } catch {
1891
+ return;
1892
+ }
1893
+ }
1894
+ function templateHandler(source) {
1295
1895
  try {
1296
- return staticStringExpression("Component.js", prop.exp.content) ?? void 0;
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
+ };
1297
1904
  } catch {
1298
1905
  return;
1299
1906
  }
@@ -1309,7 +1916,7 @@ function templateSite(source, node, bindingClasses, classOpaque, state) {
1309
1916
  }
1310
1917
  const matchClasses = bindingClasses.length ? [...classAttribute?.value.split(/[\t\n\f\r ]+/).filter(Boolean) ?? [], ...bindingClasses] : void 0;
1311
1918
  if (!classAttribute && (bindingClasses.length > 0 || !classOpaque && (node.tagType === TAG_COMPONENT || idAttribute))) {
1312
- const insertion = classInsertionOffset(source, node);
1919
+ const insertion = nodeClassInsertionOffset(source, node);
1313
1920
  if (insertion === void 0) state.dynamic = true;
1314
1921
  else classAttribute = {
1315
1922
  value: "",
@@ -1340,22 +1947,11 @@ function literalAttribute(source, node, name) {
1340
1947
  end: end - 1
1341
1948
  };
1342
1949
  }
1343
- function classInsertionOffset(source, node) {
1950
+ function nodeClassInsertionOffset(source, node) {
1344
1951
  const propsEnd = Math.max(node.loc.start.offset + 1 + node.tag.length, ...(node.props ?? []).map((prop) => prop.loc.end.offset));
1345
1952
  const tagEnd = source.indexOf(">", propsEnd);
1346
1953
  if (tagEnd < 0) return void 0;
1347
- let offset = tagEnd;
1348
- if (source[offset - 1] === "/") offset -= 1;
1349
- while (offset > node.loc.start.offset && /\s/.test(source[offset - 1])) offset -= 1;
1350
- return offset;
1351
- }
1352
- function toByteWarnings(source, warnings) {
1353
- const offsets = utf8OffsetMap(source, warnings.flatMap((warning) => [warning.start, warning.end]));
1354
- return warnings.map((warning) => ({
1355
- ...warning,
1356
- start: offsets.get(warning.start) ?? warning.start,
1357
- end: offsets.get(warning.end) ?? warning.end
1358
- }));
1954
+ return classInsertionOffset(source, node.loc.start.offset, tagEnd);
1359
1955
  }
1360
1956
  //#endregion
1361
1957
  //#region src/plan/vue.ts
@@ -1375,12 +1971,12 @@ function vueReferenceTarget(importer, reference, vuePaths) {
1375
1971
  join(target, "index.vue")
1376
1972
  ].find((path) => vuePaths.has(path));
1377
1973
  }
1378
- function vueLiteralTargets(file, vuePaths) {
1379
- return new Set([...file.source.matchAll(/(["'`])([^"'`\r\n]+)\1/g)].flatMap((match) => {
1380
- if (/[?*[\]{}]/.test(match[2]) && (match[2].includes("vue") || isLocalVueReference(match[2]))) return [...vuePaths];
1381
- const target = vueReferenceTarget(file.path, match[2], vuePaths);
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);
1382
1978
  if (target) return [target];
1383
- return match[2].includes(".vue") ? [...vuePaths] : [];
1979
+ return pattern.includes(".vue") ? [...vuePaths] : [];
1384
1980
  }));
1385
1981
  }
1386
1982
  function normalizedVueTag(value) {
@@ -1427,28 +2023,48 @@ function buildVueComponentGraph(ownedVue, sourceFiles, analyses, { styleSources,
1427
2023
  if (analysis.componentsOpen) for (const target of vuePaths) callerOpen.add(target);
1428
2024
  }
1429
2025
  for (const file of sourceFiles) {
2026
+ if (extname(file.path) === ".html") continue;
1430
2027
  let references;
2028
+ let vueReferences;
1431
2029
  let blockReferences = [];
2030
+ let hasDynamicImport = false;
2031
+ let vueGlobPatterns = [];
2032
+ let vueGlobUnverifiable = false;
1432
2033
  if (extname(file.path) === ".vue") {
1433
2034
  const fileAnalysis = analyses.get(file.path);
1434
2035
  if (fileAnalysis && !fileAnalysis.retained) {
1435
2036
  references = fileAnalysis.scriptStyleImports;
2037
+ vueReferences = fileAnalysis.scriptVueReferences;
1436
2038
  blockReferences = fileAnalysis.styleBlockImports.map((entry) => entry.reference);
1437
- } 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
+ }
1438
2047
  } else try {
1439
- 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;
1440
2054
  } catch {
1441
2055
  references = [];
2056
+ vueReferences = [];
2057
+ hasDynamicImport = true;
1442
2058
  }
1443
- if (/\bimport\s*\(/.test(file.source) || references.some((reference) => !stylesheetReference.test(reference) && isLocalVueReference(reference) && couldResolveToVue(reference) && !vueReferenceTarget(file.path, reference, vuePaths))) for (const target of vuePaths) callerOpen.add(target);
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);
1444
2060
  const resolvesOwned = (reference) => stylesheetReferenceTargets(file.path, reference, styleSources).some((path) => pathOwners.get(path) === packageRoot);
1445
2061
  if (pathOwners.get(file.path) === packageRoot && (references.some((reference) => stylesheetReference.test(reference) && (!reference.startsWith(".") || !resolvesOwned(reference))) || blockReferences.some((reference) => !resolvesOwned(reference)))) unresolvedStyleImport = true;
1446
- const imported = new Set(references.flatMap((reference) => {
2062
+ const imported = new Set(vueReferences.flatMap((reference) => {
1447
2063
  const target = vueReferenceTarget(file.path, reference, vuePaths);
1448
2064
  return target ? [target] : [];
1449
2065
  }));
1450
2066
  for (const target of imported) if (extname(file.path) !== ".vue" || !analyses.get(file.path)?.setupImports?.has(target)) callerOpen.add(target);
1451
- for (const target of vueLiteralTargets(file, vuePaths)) if (!imported.has(target)) callerOpen.add(target);
2067
+ for (const target of vueGlobTargets(file.path, vueGlobPatterns, vuePaths)) if (!imported.has(target)) callerOpen.add(target);
1452
2068
  }
1453
2069
  return {
1454
2070
  callers,
@@ -1456,15 +2072,16 @@ function buildVueComponentGraph(ownedVue, sourceFiles, analyses, { styleSources,
1456
2072
  unresolvedStyleImport
1457
2073
  };
1458
2074
  }
1459
- function vueWarningsOnlyResult(preparedVue) {
1460
- if (preparedVue.warnings.length === 0) return {};
2075
+ function vueWarningsOnlyResult(preparedVue, extraWarnings = []) {
2076
+ const warnings = [...preparedVue.warnings, ...extraWarnings];
2077
+ if (warnings.length === 0) return {};
1461
2078
  return { plan: {
1462
2079
  files: [],
1463
2080
  deletedFiles: [],
1464
2081
  unlinkedFiles: [],
1465
2082
  candidates: [],
1466
2083
  rules: [],
1467
- warnings: preparedVue.warnings,
2084
+ warnings,
1468
2085
  convertedRules: 0,
1469
2086
  retainedRules: 0
1470
2087
  } };
@@ -1473,6 +2090,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1473
2090
  const none = {
1474
2091
  files: /* @__PURE__ */ new Map(),
1475
2092
  stylesheets: [],
2093
+ styleRanges: /* @__PURE__ */ new Map(),
1476
2094
  stylePaths: /* @__PURE__ */ new Set(),
1477
2095
  unscopedPaths: /* @__PURE__ */ new Set(),
1478
2096
  warnings: [],
@@ -1545,13 +2163,17 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1545
2163
  pathOwners,
1546
2164
  packageRoot
1547
2165
  });
1548
- const generatesSelectors = (text) => /#\{|@\{|&[\w-]/.test(text);
1549
2166
  const vueShadowCss = [];
1550
2167
  const vueShadowModuleCss = [];
1551
2168
  let vueShadowUnverifiable = vueGraph.unresolvedStyleImport;
1552
2169
  for (const [path, source] of styleSources) {
1553
2170
  if (pathOwners.get(path) !== packageRoot) continue;
1554
- if (isPreprocessorPath(path) && generatesSelectors(source)) {
2171
+ try {
2172
+ if (stylesheetAnalysis(path, source).selectorsUnverifiable) {
2173
+ vueShadowUnverifiable = true;
2174
+ continue;
2175
+ }
2176
+ } catch {
1555
2177
  vueShadowUnverifiable = true;
1556
2178
  continue;
1557
2179
  }
@@ -1561,21 +2183,23 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1561
2183
  source
1562
2184
  });
1563
2185
  }
1564
- 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
+ }
1565
2191
  for (const file of ownedVue) {
1566
2192
  const analysis = analysisOf(file.path);
1567
2193
  if (analysis.retained) {
1568
- if (/<style/i.test(file.source)) vueShadowUnverifiable = true;
2194
+ if (analysis.styleRanges.length > 0) vueShadowUnverifiable = true;
1569
2195
  continue;
1570
2196
  }
1571
2197
  if (analysis.escapeUnverifiable || analysis.scriptImportsUnverifiable) vueShadowUnverifiable = true;
1572
- for (const text of analysis.shadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
1573
- else vueShadowCss.push({
2198
+ for (const text of analysis.shadowPreprocessorTexts) vueShadowCss.push({
1574
2199
  path: file.path,
1575
2200
  source: text
1576
2201
  });
1577
- for (const text of analysis.unscopedShadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
1578
- else vueShadowCss.push({
2202
+ for (const text of analysis.unscopedShadowPreprocessorTexts) vueShadowCss.push({
1579
2203
  path: file.path,
1580
2204
  source: text,
1581
2205
  migratingUnscoped: true
@@ -1589,7 +2213,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1589
2213
  migratingUnscoped: true
1590
2214
  })), ...analysis.blocks.map((block) => ({
1591
2215
  path: file.path,
1592
- source: block.analysisSource ?? block.content,
2216
+ source: block.shadowSource ?? block.analysisSource ?? block.content,
1593
2217
  scoped: true
1594
2218
  })));
1595
2219
  vueShadowModuleCss.push(...analysis.shadowModuleCssTexts, ...analysis.moduleBlocks.map((block) => block.analysisSource ?? block.content));
@@ -1630,7 +2254,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1630
2254
  for (const path of importedStyles) {
1631
2255
  stylePaths.add(path);
1632
2256
  if (extname(path) !== ".css") continue;
1633
- for (const imported of cssImports(styleSources.get(path) ?? "")) {
2257
+ for (const imported of cssImports(path, styleSources.get(path) ?? "")) {
1634
2258
  if (imported.media) continue;
1635
2259
  for (const target of stylesheetReferenceTargets(path, imported.href, styleSources)) if (pathOwners.get(target) === packageRoot) importedStyles.add(target);
1636
2260
  }
@@ -1715,6 +2339,19 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1715
2339
  message
1716
2340
  });
1717
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
+ }
1718
2355
  const vueBlocks = migrateUnscoped ? analysis.unscopedBlocks : analysis.blocks;
1719
2356
  if (vueBlocks.length === 0 && !migrateModule) continue;
1720
2357
  const vueRetention = migrateUnscoped ? void 0 : retention;
@@ -1722,19 +2359,20 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1722
2359
  if (migrateModule) stylesheets.push({
1723
2360
  cssPath: file.path,
1724
2361
  cssSource: file.source,
1725
- cssModuleId: normalizedRelativePath(packageRoot, file.path),
2362
+ cssModuleId: normalizedRelativePath(workspaceRoot, file.path),
1726
2363
  syntax: "css",
1727
2364
  isModule: true,
1728
2365
  vueBlocks: analysis.moduleBlocks,
1729
2366
  vueModule: true,
1730
2367
  vueShadowCss: shadowCss,
1731
2368
  vueShadowModuleCss,
1732
- vueShadowUnverifiable
2369
+ vueShadowUnverifiable,
2370
+ vueAtRuleIdentities
1733
2371
  });
1734
2372
  if (vueBlocks.length > 0) stylesheets.push({
1735
2373
  cssPath: file.path,
1736
2374
  cssSource: file.source,
1737
- cssModuleId: normalizedRelativePath(packageRoot, file.path),
2375
+ cssModuleId: normalizedRelativePath(workspaceRoot, file.path),
1738
2376
  syntax: "css",
1739
2377
  isModule: !vueRetention,
1740
2378
  vueBlocks,
@@ -1742,7 +2380,8 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1742
2380
  vueUnscoped: migrateUnscoped,
1743
2381
  vueShadowCss: vueRetention ? void 0 : shadowCss,
1744
2382
  vueShadowModuleCss: vueRetention ? void 0 : vueShadowModuleCss,
1745
- vueShadowUnverifiable: vueRetention ? void 0 : vueShadowUnverifiable
2383
+ vueShadowUnverifiable: vueRetention ? void 0 : vueShadowUnverifiable,
2384
+ vueAtRuleIdentities
1746
2385
  });
1747
2386
  }
1748
2387
  for (const file of ownedVue) {
@@ -1750,24 +2389,28 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1750
2389
  if (analysis.retained) continue;
1751
2390
  const elements = elementsByFile.get(file.path) ?? [];
1752
2391
  const contextPaths = [...new Set(elements.flatMap((element) => element.cssPaths))];
1753
- if (contextPaths.length === 0) continue;
1754
- await rejectSymlinkTarget(file.path, packageRoot);
2392
+ if (contextPaths.length > 0) await rejectSymlinkTarget(file.path, packageRoot);
1755
2393
  files.set(file.path, {
1756
2394
  ...file,
1757
- htmlElements: elements,
1758
- htmlStylesheets: contextPaths.map((cssPath) => ({
1759
- cssPath,
1760
- variants: [],
1761
- direct: cssPath === file.path,
1762
- analyzable: true
1763
- })),
1764
- htmlReferencesSafe: !analysis.dynamic,
1765
- htmlScriptText: analysis.scriptText
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
1766
2408
  });
1767
2409
  }
1768
2410
  return {
1769
2411
  files,
1770
2412
  stylesheets,
2413
+ styleRanges: new Map([...analyses].map(([path, analysis]) => [path, analysis.styleRanges])),
1771
2414
  stylePaths,
1772
2415
  unscopedPaths,
1773
2416
  warnings,
@@ -1783,7 +2426,7 @@ async function verifySnapshots(snapshots) {
1783
2426
  const read = reads[index];
1784
2427
  if (read.status === "rejected") {
1785
2428
  const error = read.reason;
1786
- const detail = errorCode(error) ?? (error instanceof Error ? error.message : String(error));
2429
+ const detail = errorCode(error) ?? errorMessage(error);
1787
2430
  throw new Error(`Source changed after planning: ${path} (${detail})`);
1788
2431
  }
1789
2432
  if (read.value !== before) throw new Error(`Source changed after planning: ${path}`);
@@ -1818,6 +2461,38 @@ async function writeChanges(changes, deletions) {
1818
2461
  }
1819
2462
  //#endregion
1820
2463
  //#region src/index.ts
2464
+ function indexWarningPositions(source, offsets, unicodeSeparatorsAreNewlines, formFeedIsNewline, initial = {
2465
+ line: 1,
2466
+ column: 1
2467
+ }) {
2468
+ const targets = [...offsets].sort((left, right) => left - right);
2469
+ const positions = /* @__PURE__ */ new Map();
2470
+ let target = 0;
2471
+ let byte = 0;
2472
+ let { line, column } = initial;
2473
+ let previousWasCarriageReturn = false;
2474
+ const record = () => {
2475
+ while (targets[target] === byte) {
2476
+ positions.set(byte, {
2477
+ line,
2478
+ column
2479
+ });
2480
+ target += 1;
2481
+ }
2482
+ };
2483
+ record();
2484
+ for (const character of source) {
2485
+ byte += Buffer.byteLength(character);
2486
+ while ((targets[target] ?? Infinity) < byte) target += 1;
2487
+ if (character === "\r" || character === "\n" && !previousWasCarriageReturn || unicodeSeparatorsAreNewlines && (character === "\u2028" || character === "\u2029") || formFeedIsNewline && character === "\f") {
2488
+ line += 1;
2489
+ column = 1;
2490
+ } else if (character !== "\n") column += character.length;
2491
+ previousWasCarriageReturn = character === "\r";
2492
+ record();
2493
+ }
2494
+ return positions;
2495
+ }
1821
2496
  async function migrate(options = {}) {
1822
2497
  if (options.styleFile && options.workspaces) throw new TypeError("styleFile cannot be combined with workspaces");
1823
2498
  if (options.tailwindCss && options.workspaces) throw new TypeError("tailwindCss cannot be combined with workspaces");
@@ -1835,7 +2510,13 @@ async function migrate(options = {}) {
1835
2510
  Promise.all(stylePaths.map(async (path) => [path, await snapshotFile(snapshots, path)])).then((entries) => new Map(entries)),
1836
2511
  Promise.all(sourcePaths.map(async (path) => {
1837
2512
  const source = await readFile(path, "utf8");
1838
- 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
+ })();
1839
2520
  if (!scope.targetable.has(path) && !mayReferenceModule) return void 0;
1840
2521
  return {
1841
2522
  path,
@@ -1847,21 +2528,35 @@ async function migrate(options = {}) {
1847
2528
  const sourceFiles = sourceCandidates.flatMap((file) => file ? [file] : []);
1848
2529
  if (explicitStyle && isStylesheetPath(explicitStyle) && !styleSources.has(explicitStyle)) styleSources.set(explicitStyle, await snapshotFile(snapshots, explicitStyle));
1849
2530
  if (configuredEntry && !styleSources.has(configuredEntry)) styleSources.set(configuredEntry, await snapshotFile(snapshots, configuredEntry));
2531
+ const vueStyleRanges = /* @__PURE__ */ new Map();
1850
2532
  const context = {
1851
2533
  ...scope,
1852
2534
  options,
1853
2535
  snapshots,
1854
2536
  styleSources,
1855
2537
  sourceFiles,
1856
- styleDependents: indexStylesheetDependents(styleSources)
2538
+ styleDependents: indexStylesheetDependents(styleSources),
2539
+ vueStyleRanges,
2540
+ entryCatalog: tailwindEntryCatalog(styleSources, scope.pathOwners),
2541
+ ignoredPaths: options.workspaces ? await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)) : /* @__PURE__ */ new Set()
1857
2542
  };
1858
2543
  const failures = [];
1859
2544
  const plans = [];
2545
+ const prepared = [];
1860
2546
  for (const packageRoot of selectedPackages) {
1861
- const result = await planPackage(context, packageRoot);
1862
- if (result.failure) failures.push(result.failure);
1863
- else if (result.plan) plans.push(result.plan);
1864
- }
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);
1865
2560
  const originals = new Map([...styleSources, ...sourceFiles.map((file) => [file.path, file.source])]);
1866
2561
  const { filesByPath, deletedPaths, candidates, rules, warnings, convertedRules, retainedRules } = mergePlans(plans, originals);
1867
2562
  const changed = [...filesByPath.values()].map((file) => ({
@@ -1880,6 +2575,42 @@ async function migrate(options = {}) {
1880
2575
  await writeChanges(changed, deleted);
1881
2576
  }
1882
2577
  warnings.sort((left, right) => left.file.localeCompare(right.file) || left.start - right.start || left.end - right.end || left.code.localeCompare(right.code));
2578
+ const warningOffsets = /* @__PURE__ */ new Map();
2579
+ for (const warning of warnings) {
2580
+ if (warning.start === 0 && warning.end === 0) continue;
2581
+ const offsets = warningOffsets.get(warning.file) ?? /* @__PURE__ */ new Set();
2582
+ offsets.add(warning.start).add(warning.end);
2583
+ warningOffsets.set(warning.file, offsets);
2584
+ }
2585
+ const warningPositions = new Map([...warningOffsets].flatMap(([file, offsets]) => {
2586
+ const source = snapshots.get(file);
2587
+ if (!source) return [];
2588
+ const extension = extname(file);
2589
+ const javascriptSource = SOURCE_EXTENSIONS.has(extension) && extension !== ".html" && extension !== ".vue";
2590
+ const styleRanges = vueStyleRanges.get(file) ?? [];
2591
+ const defaultOffsets = new Set(offsets);
2592
+ for (const range of styleRanges) defaultOffsets.add(range.start);
2593
+ const defaultPositions = indexWarningPositions(source, defaultOffsets, javascriptSource, isStylesheetPath(file));
2594
+ const bytes = Buffer.from(source);
2595
+ const sortedOffsets = [...offsets].sort((left, right) => left - right);
2596
+ const stylePositions = /* @__PURE__ */ new Map();
2597
+ let offsetIndex = 0;
2598
+ for (const range of styleRanges) {
2599
+ while ((sortedOffsets[offsetIndex] ?? Infinity) < range.start) offsetIndex += 1;
2600
+ const relativeOffsets = /* @__PURE__ */ new Set();
2601
+ while ((sortedOffsets[offsetIndex] ?? Infinity) <= range.end) {
2602
+ relativeOffsets.add(sortedOffsets[offsetIndex] - range.start);
2603
+ offsetIndex += 1;
2604
+ }
2605
+ const initial = defaultPositions.get(range.start);
2606
+ if (!initial) continue;
2607
+ for (const [offset, position] of indexWarningPositions(bytes.subarray(range.start, range.end).toString(), relativeOffsets, false, true, initial)) stylePositions.set(range.start + offset, position);
2608
+ }
2609
+ return [[file, {
2610
+ default: defaultPositions,
2611
+ style: stylePositions
2612
+ }]];
2613
+ }));
1883
2614
  failures.sort((left, right) => left.package.localeCompare(right.package));
1884
2615
  return {
1885
2616
  changedFiles,
@@ -1891,15 +2622,29 @@ async function migrate(options = {}) {
1891
2622
  file: normalizedRelativePath(cwd, rule.file)
1892
2623
  })),
1893
2624
  candidates: [...candidates].sort(),
1894
- warnings: warnings.map((warning) => ({
1895
- ...warning,
1896
- file: normalizedRelativePath(cwd, warning.file)
1897
- })),
2625
+ warnings: warnings.map((warning) => {
2626
+ const indexed = warningPositions.get(warning.file);
2627
+ const styleFrom = indexed?.style.get(warning.start);
2628
+ const styleTo = indexed?.style.get(warning.end);
2629
+ const located = warning.start !== 0 || warning.end !== 0;
2630
+ const from = located ? styleFrom ?? indexed?.default.get(warning.start) : void 0;
2631
+ const to = located ? styleFrom && styleTo ? styleTo : indexed?.default.get(warning.end) : void 0;
2632
+ return {
2633
+ ...warning,
2634
+ ...from && to ? {
2635
+ line: from.line,
2636
+ column: from.column,
2637
+ endLine: to.line,
2638
+ endColumn: to.column
2639
+ } : {},
2640
+ file: normalizedRelativePath(cwd, warning.file)
2641
+ };
2642
+ }),
1898
2643
  failures
1899
2644
  };
1900
2645
  }
1901
- async function planPackage(context, packageRoot) {
1902
- const { options, snapshots, workspaceRoot, explicitStyle, configuredEntry, styleSources, sourceFiles, styleDependents, pathOwners, targetable, selectedPackages, scannedPaths } = context;
2646
+ async function preparePackage(context, packageRoot) {
2647
+ const { options, snapshots, workspaceRoot, explicitStyle, configuredEntry, styleSources, sourceFiles, styleDependents, pathOwners, targetable, selectedPackages, scannedPaths, vueStyleRanges } = context;
1903
2648
  const recover = (error, fatal = false) => {
1904
2649
  if (!options.force || fatal) throw error;
1905
2650
  return { failure: packageFailure(workspaceRoot, packageRoot, error) };
@@ -1936,25 +2681,96 @@ async function planPackage(context, packageRoot) {
1936
2681
  } catch (error) {
1937
2682
  return recover(error, isIntegrityError(error));
1938
2683
  }
2684
+ for (const [path, preparedRanges] of preparedVue.styleRanges) {
2685
+ const ranges = vueStyleRanges.get(path) ?? [];
2686
+ ranges.push(...preparedRanges);
2687
+ ranges.sort((left, right) => left.start - right.start);
2688
+ vueStyleRanges.set(path, ranges);
2689
+ }
1939
2690
  if (explicitStyle && extname(explicitStyle) === ".vue" && preparedVue.stylesheets.length === 0 && ![...preparedVue.stylePaths].some((path) => !isStylesheetModule(path))) return vueWarningsOnlyResult(preparedVue);
1940
2691
  const packageSources = [...sourceFiles.filter((file) => extname(file.path) !== ".html").map((file) => preparedVue.files.get(file.path) ?? file), ...preparedHtml.files];
1941
2692
  const ownedStyles = [...styleSources.keys()].filter((path) => pathOwners.get(path) === packageRoot && (targetable.has(path) || preparedHtml.stylePaths.has(path) || preparedVue.stylePaths.has(path)));
1942
2693
  if (ownedStyles.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
1943
2694
  let tailwindPath;
1944
2695
  let tailwindEntries;
1945
- try {
1946
- ({path: tailwindPath, entries: tailwindEntries} = resolveTailwindEntry(ownedStyles, styleSources, configuredEntry));
1947
- } catch (error) {
1948
- 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
+ }
1949
2746
  }
1950
2747
  const excludedEntries = /* @__PURE__ */ new Set([...tailwindEntries, tailwindPath]);
1951
2748
  const explicitCss = explicitStyle && extname(explicitStyle) !== ".vue" ? explicitStyle : void 0;
1952
- const targets = explicitCss ? [explicitCss] : explicitStyle ? [...preparedVue.stylePaths].filter((path) => !isStylesheetModule(path) && !excludedEntries.has(path)) : ownedStyles.filter((path) => !excludedEntries.has(path) && !preparedHtml.generatedPaths.has(path) && (!isPreprocessorPath(path) || preparedHtml.stylePaths.has(path) || packageSources.some((file) => sourceReferencesStyle(file, path))));
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))));
1953
2750
  if (targets.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
1954
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);
1955
2771
  let tailwind;
1956
2772
  try {
1957
- tailwind = await loadTailwind(packageRoot, tailwindPath, snapshots, workspaceRoot);
2773
+ tailwind = await loadTailwind(entryOwner, tailwindPath, snapshots, workspaceRoot);
1958
2774
  } catch (error) {
1959
2775
  return recover(error, isIntegrityError(error));
1960
2776
  }
@@ -1969,13 +2785,13 @@ async function planPackage(context, packageRoot) {
1969
2785
  try {
1970
2786
  stylesheets = [];
1971
2787
  const compilerDependents = /* @__PURE__ */ new Map();
1972
- for (const stylePath of targets.sort()) {
2788
+ for (const stylePath of provenTargets.sort()) {
1973
2789
  await rejectSymlinkTarget(stylePath, packageRoot);
1974
2790
  const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith("_");
1975
2791
  const stylesheet = {
1976
2792
  cssPath: stylePath,
1977
2793
  cssSource: styleSources.get(stylePath) ?? "",
1978
- cssModuleId: normalizedRelativePath(packageRoot, stylePath),
2794
+ cssModuleId: normalizedRelativePath(workspaceRoot, stylePath),
1979
2795
  cssDependents: styleDependents.get(stylePath) ?? [],
1980
2796
  syntax: stylesheetSyntax(stylePath),
1981
2797
  isModule: isStylesheetModule(stylePath),
@@ -2009,125 +2825,405 @@ async function planPackage(context, packageRoot) {
2009
2825
  } catch (error) {
2010
2826
  return recover(error, isIntegrityError(error));
2011
2827
  }
2012
- stylesheets.push(...preparedVue.stylesheets);
2013
- const request = {
2828
+ stylesheets.push(...provenVueStylesheets);
2829
+ return { prepared: {
2830
+ packageRoot,
2831
+ preparedHtml,
2832
+ preparedVue,
2833
+ styleCompilers,
2834
+ files,
2014
2835
  stylesheets,
2015
- tailwindPath: tailwind.path,
2016
- tailwindSource: tailwind.css,
2017
- utilityPrefix: tailwind.designSystem.theme.prefix,
2018
- themeTokens: tailwind.themeTokens,
2019
- 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;
2020
2854
  };
2021
- let plan;
2022
- try {
2023
- 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));
2024
2895
  } catch (error) {
2025
- 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;
2026
2925
  }
2027
- try {
2028
- plan = replanCompileFailures(tailwind, request, plan);
2029
- } catch (error) {
2030
- 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);
2031
3144
  }
2032
- removeMigratedHtmlLinks(plan, preparedHtml);
2033
- plan.warnings.push(...preparedHtml.warnings);
2034
- plan.warnings.push(...preparedVue.warnings);
2035
3145
  try {
2036
- 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
+ }
2037
3155
  for (const file of plan.files.filter((file) => extname(file.path) === ".vue")) {
2038
- 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;
2039
3159
  if (!vueCompiler) throw new Error(`No Vue compiler is available to verify ${file.path}`);
2040
- const includeUnscoped = preparedVue.unscopedPaths.has(file.path);
2041
- const blocks = verifyVueSource(vueCompiler, file.path, file.source, includeUnscoped);
2042
- const untouched = /* @__PURE__ */ new Map();
2043
- for (const block of verifyVueSource(vueCompiler, file.path, snapshots.get(file.path) ?? "", includeUnscoped)) {
2044
- const key = `${block.syntax}\0${block.content}`;
2045
- untouched.set(key, (untouched.get(key) ?? 0) + 1);
2046
- }
2047
- for (const [index, block] of blocks.entries()) {
2048
- const key = `${block.syntax}\0${block.content}`;
2049
- const remaining = untouched.get(key) ?? 0;
2050
- if (remaining > 0) {
2051
- untouched.set(key, remaining - 1);
2052
- continue;
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);
2053
3167
  }
2054
- if (block.syntax === "css") validateCss(block.content);
2055
- else validateCss((await compileStyleEntry(styleCompilers, packageRoot, `${file.path}.${index}.${block.syntax}`, block.content, { virtualEntry: true })).css);
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
+ };
2056
3187
  }
2057
3188
  }
2058
- 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))) {
2059
3190
  const changed = plan.files.find((file) => file.path === stylesheet.cssPath);
2060
3191
  if (!changed && !plan.deletedFiles.includes(stylesheet.cssPath)) continue;
2061
3192
  const source = changed?.source ?? "";
2062
- validateCss((await compileStyleEntry(styleCompilers, packageRoot, stylesheet.cssPath, source)).css);
2063
- }
2064
- } catch (error) {
2065
- return recover(error, !isMissingStyleCompilerError(error));
2066
- }
2067
- return { plan };
2068
- }
2069
- function replanCompileFailures(tailwind, request, initialPlan) {
2070
- let plan = initialPlan;
2071
- const blockedByStylesheet = /* @__PURE__ */ new Map();
2072
- while (true) {
2073
- const failing = invalidCandidates(tailwind, plan.candidates);
2074
- if (failing.length === 0) break;
2075
- let progressed = false;
2076
- for (const rule of plan.rules) {
2077
- const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
2078
- if (failed.length === 0) continue;
2079
- let blocked = blockedByStylesheet.get(rule.stylesheet);
2080
- if (!blocked) blockedByStylesheet.set(rule.stylesheet, blocked = /* @__PURE__ */ new Map());
2081
- const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
2082
- let entry = blocked.get(key);
2083
- if (!entry) {
2084
- blocked.set(key, entry = {
2085
- ruleId: rule.ruleId,
2086
- authoredSpan: rule.authoredSpan,
2087
- candidates: /* @__PURE__ */ new Set()
2088
- });
2089
- progressed = true;
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
+ };
2090
3201
  }
2091
- for (const candidate of failed) entry.candidates.add(candidate);
2092
- }
2093
- if (!progressed) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
2094
- plan = JSON.parse(planBatchMigration(JSON.stringify({
2095
- ...request,
2096
- stylesheets: request.stylesheets.map((stylesheet, index) => ({
2097
- ...stylesheet,
2098
- blockedRules: [...blockedByStylesheet.get(index)?.values() ?? []].map((entry) => entry.ruleId)
2099
- }))
2100
- })));
2101
- }
2102
- for (const [index, blocked] of blockedByStylesheet) {
2103
- const cssPath = request.stylesheets[index].cssPath;
2104
- for (const { authoredSpan, candidates } of blocked.values()) {
2105
- const failed = [...candidates].sort().map((candidate) => `\`${candidate}\``).join(", ");
2106
- plan.warnings.push({
2107
- code: "candidate-compilation-failure",
2108
- file: cssPath,
2109
- start: authoredSpan.start,
2110
- end: authoredSpan.end,
2111
- message: `Tailwind did not generate CSS for ${failed}, so the rule is retained.`
2112
- });
2113
3202
  }
3203
+ } catch (error) {
3204
+ return {
3205
+ error,
3206
+ fatal: !isMissingStyleCompilerError(error)
3207
+ };
2114
3208
  }
2115
- return plan;
2116
3209
  }
2117
- function removeMigratedHtmlLinks(plan, preparedHtml) {
3210
+ function removeGroupHtmlLinks(plan, members) {
2118
3211
  const unlinked = new Set(plan.unlinkedFiles);
2119
3212
  const linksByFile = /* @__PURE__ */ new Map();
2120
- for (const link of preparedHtml.removableLinks) {
2121
- if (!unlinked.has(link.cssPath)) continue;
2122
- const links = linksByFile.get(link.filePath) ?? /* @__PURE__ */ new Set();
2123
- links.add(`${link.href}\0${link.media}`);
2124
- linksByFile.set(link.filePath, links);
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);
2125
3222
  }
2126
3223
  for (const [filePath, removable] of linksByFile) {
2127
3224
  const planned = plan.files.find((file) => file.path === filePath);
2128
- const original = preparedHtml.files.find((file) => file.path === filePath);
2129
- if (!original) continue;
2130
- const source = planned?.source ?? original.source;
3225
+ const source = planned?.source ?? originals.get(filePath);
3226
+ if (source === void 0) continue;
2131
3227
  const links = parseHtmlSource(filePath, source).links.filter((link) => removable.has(`${link.href}\0${link.media}`)).sort((left, right) => right.tagStart - left.tagStart);
2132
3228
  let bytes = Buffer.from(source);
2133
3229
  for (const link of links) bytes = Buffer.concat([bytes.subarray(0, link.tagStart), bytes.subarray(link.tagEnd)]);
@@ -2167,7 +3263,7 @@ function mergePlans(plans, originals) {
2167
3263
  retainedRules += plan.retainedRules;
2168
3264
  rules.push(...plan.rules);
2169
3265
  for (const warning of plan.warnings) {
2170
- const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}`;
3266
+ const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}\0${warning.message}`;
2171
3267
  if (seenWarnings.has(key)) continue;
2172
3268
  seenWarnings.add(key);
2173
3269
  warnings.push(warning);