tw-migrate 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { promisify } from "node:util";
3
3
  import { chmod, lstat, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { execFile } from "node:child_process";
6
- import { parse } from "parse5";
7
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { parse } from "parse5";
8
8
  //#region src/util/diff.ts
9
9
  function unifiedDiff(path, before, after) {
10
10
  const oldLines = before.split("\n");
@@ -18,186 +18,10 @@ function unifiedDiff(path, before, after) {
18
18
  ].join("");
19
19
  }
20
20
  //#endregion
21
- //#region src/parser/html.ts
22
- const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
23
- function parseHtmlSource(path, source) {
24
- const errors = [];
25
- const document = parse(source, {
26
- sourceCodeLocationInfo: true,
27
- onParseError(error) {
28
- if (error.code !== "missing-doctype") errors.push(error);
29
- }
30
- });
31
- if (errors.length > 0) throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
32
- const links = [];
33
- const bases = [];
34
- const elements = [];
35
- const dynamicAttributes = [];
36
- const scriptTexts = [];
37
- const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
38
- function visit(node) {
39
- if (node.tagName === "script") {
40
- for (const child of node.childNodes ?? []) if (child.nodeName === "#text" && child.value) scriptTexts.push(child.value);
41
- }
42
- if (node.tagName) {
43
- const attributes = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
44
- const locations = node.sourceCodeLocation?.attrs;
45
- if (locations) {
46
- if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
47
- const href = locatedAttribute(source, locations.href, attributes.get("href"));
48
- const media = locatedAttribute(source, locations.media, attributes.get("media"));
49
- const tag = node.sourceCodeLocation?.startTag ?? node.sourceCodeLocation;
50
- if (href && tag) links.push({
51
- href: href.value,
52
- media: media?.value ?? "",
53
- start: href.start,
54
- end: href.end,
55
- tagStart: tag.startOffset,
56
- tagEnd: tag.endOffset
57
- });
58
- }
59
- if (node.tagName === "base" && bases.length === 0) {
60
- const href = locatedAttribute(source, locations.href, attributes.get("href"));
61
- if (href) bases.push({
62
- href: href.value,
63
- writable: href.writable,
64
- start: href.start,
65
- end: href.end
66
- });
67
- }
68
- let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
69
- const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
70
- const dynamic = (!classAttribute && idAttribute && locations.class ? {
71
- start: locations.class.startOffset,
72
- end: locations.class.endOffset
73
- } : void 0) ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
74
- if (dynamic) dynamicAttributes.push({
75
- start: dynamic.start,
76
- end: dynamic.end
77
- });
78
- else if (classAttribute || idAttribute) {
79
- if (!classAttribute && idAttribute) {
80
- const startTag = node.sourceCodeLocation?.startTag;
81
- if (startTag && source[startTag.endOffset - 1] === ">") {
82
- const insertion = classInsertionOffset(source, startTag.startOffset, startTag.endOffset - 1);
83
- classAttribute = {
84
- value: "",
85
- start: insertion,
86
- end: insertion,
87
- synthetic: true
88
- };
89
- }
90
- }
91
- elements.push({
92
- classAttribute,
93
- idAttribute
94
- });
95
- }
96
- }
97
- }
98
- for (const child of node.childNodes ?? []) visit(child);
99
- if (node.content) visit(node.content);
100
- }
101
- visit(document);
102
- return {
103
- ...toByteOffsets(source, {
104
- links,
105
- bases,
106
- elements,
107
- dynamicAttributes
108
- }),
109
- scriptText: scriptTexts.join("\n")
110
- };
111
- }
112
- function utf8OffsetMap(source, indices) {
113
- const sorted = [...new Set(indices)].sort((left, right) => left - right);
114
- const map = /* @__PURE__ */ new Map();
115
- let lastIndex = 0;
116
- let bytes = 0;
117
- for (const index of sorted) {
118
- bytes += Buffer.byteLength(source.slice(lastIndex, index));
119
- lastIndex = index;
120
- map.set(index, bytes);
121
- }
122
- return map;
123
- }
124
- function offsetLookup(offsets) {
125
- return (index) => {
126
- const byte = offsets.get(index);
127
- if (byte === void 0) throw new Error(`No byte offset was mapped for index ${index}`);
128
- return byte;
129
- };
130
- }
131
- function toByteOffsets(source, parsed) {
132
- const offset = offsetLookup(utf8OffsetMap(source, [
133
- ...parsed.links.flatMap((link) => [
134
- link.start,
135
- link.end,
136
- link.tagStart,
137
- link.tagEnd
138
- ]),
139
- ...parsed.bases.flatMap((base) => [base.start, base.end]),
140
- ...parsed.elements.flatMap((element) => [element.classAttribute, element.idAttribute].flatMap((value) => value ? [value.start, value.end] : [])),
141
- ...parsed.dynamicAttributes.flatMap((value) => [value.start, value.end])
142
- ]));
143
- const attribute = (value) => ({
144
- ...value,
145
- start: offset(value.start),
146
- end: offset(value.end)
147
- });
148
- return {
149
- links: parsed.links.map((link) => ({
150
- ...link,
151
- start: offset(link.start),
152
- end: offset(link.end),
153
- tagStart: offset(link.tagStart),
154
- tagEnd: offset(link.tagEnd)
155
- })),
156
- bases: parsed.bases.map((base) => attribute(base)),
157
- elements: parsed.elements.map((element) => ({
158
- classAttribute: element.classAttribute && attribute(element.classAttribute),
159
- idAttribute: element.idAttribute && attribute(element.idAttribute)
160
- })),
161
- dynamicAttributes: parsed.dynamicAttributes.map((value) => attribute(value))
162
- };
163
- }
164
- function classInsertionOffset(source, floor, tagEnd) {
165
- let offset = tagEnd;
166
- if (source[offset - 1] === "/") offset -= 1;
167
- while (offset > floor && /\s/.test(source[offset - 1] ?? "")) offset -= 1;
168
- return offset;
169
- }
170
- function stylesheetRel(value = "") {
171
- const tokens = value.toLowerCase().split(/\s+/);
172
- return tokens.includes("stylesheet") && !tokens.includes("alternate");
173
- }
174
- function locatedAttribute(source, location, parsedValue) {
175
- if (!location || parsedValue === void 0) return void 0;
176
- const raw = source.slice(location.startOffset, location.endOffset);
177
- const equals = raw.indexOf("=");
178
- if (equals < 0) return void 0;
179
- let start = equals + 1;
180
- while (/\s/.test(raw[start] ?? "")) start += 1;
181
- const quote = raw[start];
182
- let end;
183
- if (quote === "\"" || quote === "'") {
184
- start += 1;
185
- end = raw.lastIndexOf(quote);
186
- if (end < start) return void 0;
187
- } else {
188
- end = start;
189
- while (end < raw.length && !/\s/.test(raw[end] ?? "")) end += 1;
190
- }
191
- const value = raw.slice(start, end);
192
- return {
193
- value: value.includes("&") ? parsedValue : value,
194
- writable: !value.includes("&"),
195
- start: location.startOffset + start,
196
- end: location.startOffset + end
197
- };
198
- }
199
- //#endregion
200
21
  //#region src/native.ts
22
+ function errorCode(error) {
23
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
24
+ }
201
25
  const require = createRequire(import.meta.url);
202
26
  const target = {
203
27
  "darwin-arm64": "darwin-arm64",
@@ -219,7 +43,85 @@ 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.templatePrefixes) && value.templatePrefixes.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") && Array.isArray(value.stylesheetLinks) && value.stylesheetLinks.every((item) => typeof item === "string") && Array.isArray(value.customProperties) && value.customProperties.every((item) => typeof item === "string") && [
68
+ value.vueGlobUnverifiable,
69
+ value.hasDynamicImport,
70
+ value.hasVueFallthroughMacro,
71
+ value.usesCssModule,
72
+ value.hasUnboundUseCssModule,
73
+ value.definesRootUseCssModule,
74
+ value.stylesheetLinksUnverifiable,
75
+ value.rendersStyleElement,
76
+ value.injectsMarkup,
77
+ value.customPropertiesUnbounded
78
+ ].every((item) => typeof item === "boolean");
79
+ }
80
+ const ANALYSIS_CACHE_LIMIT = 4096;
81
+ const sourceAnalysisCache = /* @__PURE__ */ new Map();
82
+ const stylesheetAnalysisCache = /* @__PURE__ */ new Map();
83
+ function bound(cache) {
84
+ while (cache.size > ANALYSIS_CACHE_LIMIT) {
85
+ const oldest = cache.keys().next().value;
86
+ if (oldest === void 0) break;
87
+ cache.delete(oldest);
88
+ }
89
+ }
90
+ function expressionAnalysis(path, source) {
91
+ 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") && Array.isArray(value.templatePrefixes) && value.templatePrefixes.every((item) => typeof item === "string"));
92
+ }
93
+ function sourceAnalysis(path, source) {
94
+ const cached = sourceAnalysisCache.get(path);
95
+ if (cached?.source === source) return cached.analysis;
96
+ const analysis = decode("source analysis", binding.sourceAnalysis(path, source), sourceAnalysisResult);
97
+ sourceAnalysisCache.set(path, {
98
+ source,
99
+ analysis
100
+ });
101
+ bound(sourceAnalysisCache);
102
+ return analysis;
103
+ }
104
+ function fontFamilyStack(value) {
105
+ return decode("font family stack", binding.fontFamilyStack(value), (parsed) => parsed === null || object(parsed) && typeof parsed.value === "string" && object(parsed.firstFamily) && typeof parsed.firstFamily.name === "string" && (parsed.firstFamily.kind === "name" || parsed.firstFamily.kind === "generic" || parsed.firstFamily.kind === "css-wide"));
106
+ }
107
+ function compiledShape(css) {
108
+ return decode("compiled shape", binding.compiledShape(css), (value) => object(value) && Array.isArray(value.declarations) && value.declarations.every((item) => object(item) && typeof item.property === "string" && typeof item.important === "boolean" && typeof item.value === "string") && Array.isArray(value.referencedProperties) && value.referencedProperties.every((item) => typeof item === "string"));
109
+ }
110
+ function stylesheetAnalysis(path, source) {
111
+ const cached = stylesheetAnalysisCache.get(path);
112
+ if (cached?.source === source) return cached.analysis;
113
+ 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") && [
114
+ value.unverifiable,
115
+ value.scopeEscapesUnverifiable,
116
+ value.selectorsUnverifiable
117
+ ].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" && Array.isArray(value.classNames) && value.classNames.every((item) => typeof item === "string") && typeof value.classReservationsUnbounded === "boolean" && Array.isArray(value.customProperties) && value.customProperties.every((item) => typeof item === "string") && typeof value.customPropertiesUnbounded === "boolean");
118
+ stylesheetAnalysisCache.set(path, {
119
+ source,
120
+ analysis
121
+ });
122
+ bound(stylesheetAnalysisCache);
123
+ return analysis;
124
+ }
223
125
  //#endregion
224
126
  //#region src/parser/style-compiler.ts
225
127
  const SASS_EXTENSIONS = /* @__PURE__ */ new Set([".scss", ".sass"]);
@@ -355,9 +257,6 @@ const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
355
257
  "node_modules"
356
258
  ]);
357
259
  const RECOVERABLE_INPUT_ERROR = "TW_MIGRATE_RECOVERABLE_INPUT:";
358
- function errorCode(error) {
359
- return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
360
- }
361
260
  function errorMessage(error) {
362
261
  return error instanceof Error ? error.message : String(error);
363
262
  }
@@ -374,15 +273,6 @@ function isStylesheetModule(path) {
374
273
  const syntax = stylesheetSyntax(path);
375
274
  return syntax !== void 0 && path.endsWith(`.module.${syntax}`);
376
275
  }
377
- function sourceReferencesStyle(file, stylePath) {
378
- let importPath = normalizedRelativePath(dirname(file.path), stylePath);
379
- if (!importPath.startsWith(".")) importPath = `./${importPath}`;
380
- return [
381
- `'${importPath}'`,
382
- `"${importPath}"`,
383
- `\`${importPath}\``
384
- ].some((literal) => file.source.includes(literal));
385
- }
386
276
  function isProjectInput(workspaceRoot, path) {
387
277
  return isWithin(workspaceRoot, path) && !relative(workspaceRoot, path).split(/[\\/]/).includes("node_modules");
388
278
  }
@@ -427,24 +317,26 @@ function isIntegrityError(error) {
427
317
  function normalizedRelativePath(root, path) {
428
318
  return relative(root, path).split(sep).join("/");
429
319
  }
430
- function maskCssComments(source) {
431
- return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, " "));
432
- }
433
320
  function indexStylesheetDependents(styleSources) {
434
321
  const dependents = /* @__PURE__ */ new Map();
435
- for (const [path, rawSource] of styleSources) {
436
- const source = maskCssComments(rawSource);
437
- const references = [
438
- ...[...source.matchAll(/composes\s*:[^;{}]*?\bfrom\s+["']([^"']+)["']/g)].map((match) => match[1]),
439
- ...[...source.matchAll(/@(?:use|forward)\s+["']([^"']+)["']/g)].map((match) => match[1]),
440
- ...[...source.matchAll(/@import\s+(?:\([^)]*\)\s*)?(?:([^;{}]+);|([^;{}\r\n]+))/g)].flatMap((statement) => [...(statement[1] ?? statement[2]).matchAll(/["']([^"']+)["']/g)].map((match) => match[1])),
441
- ...[...source.matchAll(/@import\s+url\(\s*([^"'()\s]+)\s*\)/g)].map((match) => match[1])
442
- ];
443
- for (const reference of new Set(references)) for (const target of stylesheetReferenceTargets(path, reference, styleSources)) {
444
- if (target === path || !isStylesheetModule(target) && !isPreprocessorPath(target)) continue;
445
- const paths = dependents.get(target) ?? [];
446
- paths.push(path);
447
- dependents.set(target, paths);
322
+ const allPossibleTargets = [...styleSources.keys()].filter((path) => isStylesheetModule(path) || isPreprocessorPath(path));
323
+ for (const [path, source] of styleSources) {
324
+ let references;
325
+ try {
326
+ const analysis = stylesheetAnalysis(path, source);
327
+ references = analysis.references;
328
+ if (analysis.unverifiable) references = [...references, ...allPossibleTargets];
329
+ } catch {
330
+ references = allPossibleTargets;
331
+ }
332
+ for (const reference of new Set(references)) {
333
+ const targets = styleSources.has(reference) ? [reference] : stylesheetReferenceTargets(path, reference, styleSources);
334
+ for (const target of targets) {
335
+ if (target === path || !isStylesheetModule(target) && !isPreprocessorPath(target)) continue;
336
+ const paths = dependents.get(target) ?? [];
337
+ paths.push(path);
338
+ dependents.set(target, paths);
339
+ }
448
340
  }
449
341
  }
450
342
  for (const [target, paths] of dependents) dependents.set(target, [...new Set(paths)].sort());
@@ -466,63 +358,13 @@ function stylesheetReferenceTargets(importer, reference, styleSources) {
466
358
  join(target, `index${syntax}`)
467
359
  ])).filter((path) => styleSources.has(path));
468
360
  }
469
- function cssImports(source) {
470
- const imports = [];
471
- const masked = maskCssComments(source);
472
- let depth = 0;
473
- let quote;
474
- let importsAllowed = true;
475
- for (let index = 0; index < masked.length; index += 1) {
476
- const character = masked[index];
477
- if (quote) {
478
- if (character === "\\") index += 1;
479
- else if (character === quote) quote = void 0;
480
- continue;
481
- }
482
- if (character === "\"" || character === "'") {
483
- quote = character;
484
- continue;
485
- }
486
- if (character === "{") {
487
- depth += 1;
488
- importsAllowed = false;
489
- continue;
490
- }
491
- if (character === "}") {
492
- depth = Math.max(0, depth - 1);
493
- continue;
494
- }
495
- if (!importsAllowed || depth !== 0 || masked.slice(index, index + 7).toLowerCase() !== "@import" || /[-\w]/.test(masked[index + 7] ?? "")) continue;
496
- let end = index + 7;
497
- let importQuote;
498
- let parentheses = 0;
499
- for (; end < masked.length; end += 1) {
500
- const next = masked[end];
501
- if (importQuote) {
502
- if (next === "\\") end += 1;
503
- else if (next === importQuote) importQuote = void 0;
504
- } else if (next === "\"" || next === "'") importQuote = next;
505
- else if (next === "(") parentheses += 1;
506
- else if (next === ")") parentheses = Math.max(0, parentheses - 1);
507
- else if (next === ";" && parentheses === 0) break;
508
- }
509
- if (end >= masked.length) continue;
510
- const statement = masked.slice(index, end + 1);
511
- const match = /^@import\s+(?:url\(\s*)?(?:["']([^"']+)["']|([^"'()\s;]+))\s*\)?\s*([^;]*);$/i.exec(statement);
512
- if (match) imports.push({
513
- href: match[1] ?? match[2],
514
- media: match[3].trim(),
515
- start: index,
516
- end: end + 1
517
- });
518
- index = end;
519
- }
520
- const offsets = utf8OffsetMap(source, imports.flatMap((imported) => [imported.start, imported.end]));
521
- return imports.map((imported) => ({
522
- ...imported,
523
- start: offsets.get(imported.start) ?? imported.start,
524
- end: offsets.get(imported.end) ?? imported.end
525
- }));
361
+ function localHrefTarget(href) {
362
+ const target = href.split(/[?#]/, 1)[0];
363
+ if (!target || /^[a-z][a-z0-9+.-]*:|^\/\//i.test(target)) return void 0;
364
+ return target;
365
+ }
366
+ function cssImports(path, source) {
367
+ return stylesheetAnalysis(path, source).imports;
526
368
  }
527
369
  //#endregion
528
370
  //#region src/discovery.ts
@@ -593,6 +435,28 @@ async function findGitRoot(cwd) {
593
435
  return;
594
436
  }
595
437
  }
438
+ async function scannerIgnoredPaths(workspaceRoot, paths) {
439
+ if (paths.length === 0 || await findGitRoot(workspaceRoot) !== workspaceRoot) return /* @__PURE__ */ new Set();
440
+ return new Promise((resolvePromise) => {
441
+ execFile("git", [
442
+ "check-ignore",
443
+ "--no-index",
444
+ "-z",
445
+ "--stdin"
446
+ ], {
447
+ cwd: workspaceRoot,
448
+ maxBuffer: 67108864
449
+ }, (error, stdout) => {
450
+ if (error !== null && error.code !== 1) {
451
+ resolvePromise(new Set(paths));
452
+ return;
453
+ }
454
+ const ignored = /* @__PURE__ */ new Set();
455
+ for (const entry of stdout.split("\0")) if (entry) ignored.add(resolve(workspaceRoot, entry));
456
+ resolvePromise(ignored);
457
+ }).stdin?.end(paths.map((path) => `${relative(workspaceRoot, path)}\0`).join(""));
458
+ });
459
+ }
596
460
  async function isIgnoredByGit(gitRoot, path) {
597
461
  if (gitRoot === path) return false;
598
462
  try {
@@ -617,7 +481,7 @@ async function discoverGitFiles(root) {
617
481
  "."
618
482
  ], {
619
483
  cwd: root,
620
- maxBuffer: 64 * 1024 * 1024
484
+ maxBuffer: 67108864
621
485
  });
622
486
  const paths = stdout.split("\0").filter(Boolean).map((path) => resolve(root, path)).filter((path) => !hasIgnoredDirectory(root, path) && isRelevantDiscoveredFile(path));
623
487
  return (await Promise.all(paths.map(async (path) => {
@@ -659,41 +523,532 @@ async function collectFiles(root, include) {
659
523
  return files;
660
524
  }
661
525
  //#endregion
662
- //#region src/plan/html.ts
663
- async function preparePackageHtml({ packageRoot, sourceFiles, styleSources, snapshots, pathOwners, styleDependents }) {
664
- const files = [];
665
- const stylePaths = /* @__PURE__ */ new Set();
666
- const generatedPaths = /* @__PURE__ */ new Set();
667
- const removableLinks = [];
668
- const warnings = [];
669
- const htmlFiles = sourceFiles.filter((file) => extname(file.path) === ".html");
670
- const orderedFiles = [...htmlFiles.filter((file) => pathOwners.get(file.path) === packageRoot), ...htmlFiles.filter((file) => pathOwners.get(file.path) !== packageRoot)];
671
- for (const file of orderedFiles) {
672
- const owner = pathOwners.get(file.path);
673
- const foreign = owner !== packageRoot;
674
- if (foreign && !owner) continue;
675
- const referenceRoot = foreign ? owner : packageRoot;
676
- const parsed = parseHtmlSource(file.path, file.source);
677
- const contexts = [];
678
- let linkBase = dirname(file.path);
679
- const base = parsed.bases[0];
680
- if (base) {
681
- const baseReference = base.href.split(/[?#]/, 1)[0];
682
- const basePath = base.writable && (baseReference === "" ? file.path : localHtmlReference(referenceRoot, dirname(file.path), base.href));
683
- if (!basePath || !isWithin(referenceRoot, basePath)) {
684
- if (!foreign) warnings.push(htmlWarning("unsupported-html-base", file.path, base.start, base.end, "A remote or unrepresentable base URL prevents safe stylesheet link resolution."));
685
- linkBase = void 0;
686
- } else linkBase = base.href.split(/[?#]/, 1)[0].endsWith("/") ? basePath : dirname(basePath);
526
+ //#region src/parser/html.ts
527
+ const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
528
+ /** Balanced template expressions, for masking a templated attribute value
529
+ * before extracting its static class-prefix tokens. */
530
+ const TEMPLATE_EXPRESSIONS = /\{\{[\s\S]*?\}\}|\$\{[\s\S]*?\}|<%[\s\S]*?%>|<#[\s\S]*?#>|\[%[\s\S]*?%\]/g;
531
+ function parseHtmlSource(path, source) {
532
+ const errors = [];
533
+ const document = parse(source, {
534
+ sourceCodeLocationInfo: true,
535
+ scriptingEnabled: false,
536
+ onParseError(error) {
537
+ if (error.code !== "missing-doctype") errors.push(error);
687
538
  }
688
- for (const link of parsed.links) {
689
- const linkedPath = linkBase && localHtmlReference(referenceRoot, linkBase, link.href);
690
- if (!linkedPath || !isWithin(packageRoot, linkedPath) || foreign && pathOwners.get(linkedPath) !== packageRoot) {
691
- if (!foreign) warnings.push(htmlWarning("unsupported-html-stylesheet-link", file.path, link.start, link.end, "Only local package stylesheet links are analyzed."));
692
- continue;
693
- }
694
- const variants = mediaVariants(link.media);
695
- if (variants === void 0) {
696
- const cssPath = (foreign ? void 0 : inferredPreprocessorPath({
539
+ });
540
+ if (errors.length > 0) throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
541
+ const links = [];
542
+ const bases = [];
543
+ const elements = [];
544
+ const dynamicAttributes = [];
545
+ const dynamicClassValues = [];
546
+ const styleAttributeValues = [];
547
+ const handlerTexts = [];
548
+ const scriptTexts = [];
549
+ let hasStyle = false;
550
+ const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
551
+ function visit(node) {
552
+ if (node.tagName === "style") hasStyle = true;
553
+ if (node.tagName === "script") {
554
+ for (const child of node.childNodes ?? []) if (child.nodeName === "#text" && child.value) scriptTexts.push(child.value);
555
+ }
556
+ if (node.tagName) {
557
+ const attributes = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
558
+ const style = attributes.get("style");
559
+ if (style !== void 0 && style !== "") styleAttributeValues.push(style);
560
+ for (const [name, value] of attributes) if (name.startsWith("on") && value !== "") handlerTexts.push(value);
561
+ const locations = node.sourceCodeLocation?.attrs;
562
+ if (locations) {
563
+ if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
564
+ const href = locatedAttribute(source, locations.href, attributes.get("href"));
565
+ const media = locatedAttribute(source, locations.media, attributes.get("media"));
566
+ const tag = node.sourceCodeLocation?.startTag ?? node.sourceCodeLocation;
567
+ if (href && tag) links.push({
568
+ href: href.value,
569
+ media: media?.value ?? "",
570
+ start: href.start,
571
+ end: href.end,
572
+ tagStart: tag.startOffset,
573
+ tagEnd: tag.endOffset
574
+ });
575
+ }
576
+ if (node.tagName === "base" && bases.length === 0) {
577
+ const href = locatedAttribute(source, locations.href, attributes.get("href"));
578
+ if (href) bases.push({
579
+ href: href.value,
580
+ writable: href.writable,
581
+ start: href.start,
582
+ end: href.end
583
+ });
584
+ }
585
+ let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
586
+ const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
587
+ const unparsedClass = !classAttribute && idAttribute && locations.class ? {
588
+ start: locations.class.startOffset,
589
+ end: locations.class.endOffset
590
+ } : void 0;
591
+ const dynamic = unparsedClass ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
592
+ if (dynamic) {
593
+ dynamicAttributes.push({
594
+ start: dynamic.start,
595
+ end: dynamic.end
596
+ });
597
+ const decoded = attributes.get("class");
598
+ const entityBearing = classAttribute !== void 0 && dynamic === classAttribute && classAttribute.writable === false;
599
+ if (dynamic !== idAttribute && decoded !== void 0 && (TEMPLATE_MARKERS.test(decoded) || entityBearing || dynamic === unparsedClass)) dynamicClassValues.push(decoded);
600
+ } else if (classAttribute || idAttribute) {
601
+ if (!classAttribute && idAttribute) {
602
+ const startTag = node.sourceCodeLocation?.startTag;
603
+ if (startTag && source[startTag.endOffset - 1] === ">") {
604
+ const insertion = classInsertionOffset(source, startTag.startOffset, startTag.endOffset - 1);
605
+ classAttribute = {
606
+ value: "",
607
+ start: insertion,
608
+ end: insertion,
609
+ synthetic: true
610
+ };
611
+ }
612
+ }
613
+ elements.push({
614
+ classAttribute,
615
+ idAttribute
616
+ });
617
+ }
618
+ }
619
+ }
620
+ for (const child of node.childNodes ?? []) visit(child);
621
+ if (node.content) visit(node.content);
622
+ }
623
+ visit(document);
624
+ return {
625
+ ...toByteOffsets(source, {
626
+ links,
627
+ bases,
628
+ elements,
629
+ dynamicAttributes
630
+ }),
631
+ dynamicClassValues,
632
+ styleAttributeValues,
633
+ handlerText: handlerTexts.join(";\n"),
634
+ scriptText: scriptTexts.join("\n"),
635
+ hasStyle
636
+ };
637
+ }
638
+ function utf8OffsetMap(source, indices) {
639
+ const sorted = [...new Set(indices)].sort((left, right) => left - right);
640
+ const map = /* @__PURE__ */ new Map();
641
+ let lastIndex = 0;
642
+ let bytes = 0;
643
+ for (const index of sorted) {
644
+ bytes += Buffer.byteLength(source.slice(lastIndex, index));
645
+ lastIndex = index;
646
+ map.set(index, bytes);
647
+ }
648
+ return map;
649
+ }
650
+ function offsetLookup(offsets) {
651
+ return (index) => {
652
+ const byte = offsets.get(index);
653
+ if (byte === void 0) throw new Error(`No byte offset was mapped for index ${index}`);
654
+ return byte;
655
+ };
656
+ }
657
+ function toByteOffsets(source, parsed) {
658
+ const offset = offsetLookup(utf8OffsetMap(source, [
659
+ ...parsed.links.flatMap((link) => [
660
+ link.start,
661
+ link.end,
662
+ link.tagStart,
663
+ link.tagEnd
664
+ ]),
665
+ ...parsed.bases.flatMap((base) => [base.start, base.end]),
666
+ ...parsed.elements.flatMap((element) => [element.classAttribute, element.idAttribute].flatMap((value) => value ? [value.start, value.end] : [])),
667
+ ...parsed.dynamicAttributes.flatMap((value) => [value.start, value.end])
668
+ ]));
669
+ const attribute = (value) => ({
670
+ ...value,
671
+ start: offset(value.start),
672
+ end: offset(value.end)
673
+ });
674
+ return {
675
+ links: parsed.links.map((link) => ({
676
+ ...link,
677
+ start: offset(link.start),
678
+ end: offset(link.end),
679
+ tagStart: offset(link.tagStart),
680
+ tagEnd: offset(link.tagEnd)
681
+ })),
682
+ bases: parsed.bases.map((base) => attribute(base)),
683
+ elements: parsed.elements.map((element) => ({
684
+ classAttribute: element.classAttribute && attribute(element.classAttribute),
685
+ idAttribute: element.idAttribute && attribute(element.idAttribute)
686
+ })),
687
+ dynamicAttributes: parsed.dynamicAttributes.map((value) => attribute(value))
688
+ };
689
+ }
690
+ function classInsertionOffset(source, floor, tagEnd) {
691
+ let offset = tagEnd;
692
+ if (source[offset - 1] === "/") offset -= 1;
693
+ while (offset > floor && /\s/.test(source[offset - 1] ?? "")) offset -= 1;
694
+ return offset;
695
+ }
696
+ function stylesheetRel(value = "") {
697
+ const tokens = value.toLowerCase().split(/\s+/);
698
+ return tokens.includes("stylesheet") && !tokens.includes("alternate");
699
+ }
700
+ function locatedAttribute(source, location, parsedValue) {
701
+ if (!location || parsedValue === void 0) return void 0;
702
+ const raw = source.slice(location.startOffset, location.endOffset);
703
+ const equals = raw.indexOf("=");
704
+ if (equals < 0) return void 0;
705
+ let start = equals + 1;
706
+ while (/\s/.test(raw[start] ?? "")) start += 1;
707
+ const quote = raw[start];
708
+ let end;
709
+ if (quote === "\"" || quote === "'") {
710
+ start += 1;
711
+ end = raw.lastIndexOf(quote);
712
+ if (end < start) return void 0;
713
+ } else {
714
+ end = start;
715
+ while (end < raw.length && !/\s/.test(raw[end] ?? "")) end += 1;
716
+ }
717
+ const value = raw.slice(start, end);
718
+ return {
719
+ value: value.includes("&") ? parsedValue : value,
720
+ writable: !value.includes("&"),
721
+ start: location.startOffset + start,
722
+ end: location.startOffset + end
723
+ };
724
+ }
725
+ //#endregion
726
+ //#region src/plan/entry.ts
727
+ function emitsUtilities(specifier) {
728
+ return specifier === "tailwindcss" || specifier === "tailwindcss/utilities" || specifier === "tailwindcss/utilities.css";
729
+ }
730
+ function hasTailwindImport(source) {
731
+ return cssDirectives(source)?.some((directive) => directive.kind === "import" && directive.specifier !== null && emitsUtilities(directive.specifier)) ?? false;
732
+ }
733
+ function tailwindEntryCatalog(styleSources, pathOwners) {
734
+ const catalog = /* @__PURE__ */ new Map();
735
+ for (const [path, source] of styleSources) {
736
+ if (extname(path) !== ".css") continue;
737
+ if (!hasTailwindImport(source)) continue;
738
+ const owner = pathOwners.get(path);
739
+ if (owner === void 0) continue;
740
+ const entries = catalog.get(owner) ?? [];
741
+ entries.push(path);
742
+ catalog.set(owner, entries);
743
+ }
744
+ for (const entries of catalog.values()) entries.sort();
745
+ return catalog;
746
+ }
747
+ function cssDirectives(source) {
748
+ return cssDirectives$1(source)?.filter((directive) => directive !== null && typeof directive === "object" && "kind" in directive && typeof directive.kind === "string") ?? null;
749
+ }
750
+ function entryGraphSheets(entry, styleSources) {
751
+ const sheets = [];
752
+ const seen = /* @__PURE__ */ new Set();
753
+ const pending = [entry];
754
+ while (pending.length > 0) {
755
+ const path = pending.pop();
756
+ if (path === void 0 || seen.has(path)) continue;
757
+ seen.add(path);
758
+ const raw = styleSources.get(path);
759
+ if (raw === void 0) return null;
760
+ const directives = cssDirectives(raw);
761
+ if (directives === null) return null;
762
+ sheets.push({
763
+ path,
764
+ directives
765
+ });
766
+ for (const directive of directives) {
767
+ if (directive.kind !== "import") continue;
768
+ if (directive.specifier === null) return null;
769
+ const spec = directive.specifier;
770
+ if (directive.tailwind) continue;
771
+ if (!spec.startsWith(".") && !spec.startsWith("/")) {
772
+ let resolved;
773
+ try {
774
+ resolved = createRequire(join(dirname(path), "package.json")).resolve(spec);
775
+ } catch {
776
+ return null;
777
+ }
778
+ if (/[\\/]node_modules[\\/]/.test(resolved)) continue;
779
+ pending.push(resolved);
780
+ continue;
781
+ }
782
+ const resolved = resolve(dirname(path), spec);
783
+ pending.push(styleSources.has(resolved) ? resolved : `${resolved}.css`);
784
+ }
785
+ }
786
+ return sheets;
787
+ }
788
+ function scanProof(options) {
789
+ const sheets = entryGraphSheets(options.entry, new Map([...options.styleSources ?? [], [options.entry, options.entrySource]]));
790
+ if (sheets === null) return null;
791
+ let literal = false;
792
+ let automaticDisabled = false;
793
+ for (const { path, directives } of sheets) {
794
+ const base = dirname(path);
795
+ const coversChild = (scope) => {
796
+ if (scope.includes("*")) return false;
797
+ return isWithin(resolve(base, scope), options.packageRoot);
798
+ };
799
+ const excludesChild = (scope) => {
800
+ const stripped = resolve(base, scope.replace(/\*.*$/, ""));
801
+ if (scope.includes("*")) return options.packageRoot.startsWith(stripped) || stripped.startsWith(options.packageRoot);
802
+ return isWithin(stripped, options.packageRoot) || isWithin(options.packageRoot, stripped);
803
+ };
804
+ for (const directive of directives) if (directive.kind === "source") {
805
+ if (directive.inline) continue;
806
+ if (directive.unreadable || directive.scope === null) return null;
807
+ if (directive.not) {
808
+ if (excludesChild(directive.scope)) return null;
809
+ } else if (coversChild(directive.scope)) literal = true;
810
+ } else if (directive.kind === "import" && directive.tailwind) {
811
+ if (directive.sourceUnreadable) return null;
812
+ if (directive.source === "none") automaticDisabled = true;
813
+ else if (directive.source !== null) {
814
+ if (coversChild(directive.source)) literal = true;
815
+ else automaticDisabled = true;
816
+ }
817
+ }
818
+ }
819
+ if (literal) return "literal";
820
+ if (automaticDisabled) return null;
821
+ return isWithin(dirname(options.entry), options.packageRoot) ? "automatic" : null;
822
+ }
823
+ const OPAQUE_SOURCE_IMPORT = {
824
+ specifier: "#unverifiable-source",
825
+ typeOnly: false,
826
+ dynamic: true
827
+ };
828
+ function sourceImports(file) {
829
+ const extension = extname(file.path);
830
+ if (extension === ".html") return [];
831
+ if (extension === ".vue") return file.sourceImports && !file.sourceImportsUnverifiable ? file.sourceImports : [OPAQUE_SOURCE_IMPORT];
832
+ try {
833
+ return sourceAnalysis(file.path, file.source).imports;
834
+ } catch {
835
+ return [OPAQUE_SOURCE_IMPORT];
836
+ }
837
+ }
838
+ const RESOLVABLE_EXTENSIONS = [
839
+ ".ts",
840
+ ".tsx",
841
+ ".mts",
842
+ ".cts",
843
+ ".js",
844
+ ".jsx",
845
+ ".mjs",
846
+ ".cjs",
847
+ ".vue"
848
+ ];
849
+ function resolveImport(fromDir, spec, known) {
850
+ if (!spec.startsWith(".")) return void 0;
851
+ const base = resolve(fromDir, spec);
852
+ const emitted = base.match(/^(.*)\.(?:mjs|cjs|js|jsx)$/);
853
+ return [
854
+ base,
855
+ ...emitted ? [
856
+ ".ts",
857
+ ".tsx",
858
+ ".mts",
859
+ ".cts"
860
+ ].map((extension) => `${emitted[1]}${extension}`) : [],
861
+ ...RESOLVABLE_EXTENSIONS.map((extension) => `${base}${extension}`),
862
+ ...RESOLVABLE_EXTENSIONS.map((extension) => join(base, `index${extension}`))
863
+ ].find((candidate) => known.has(candidate));
864
+ }
865
+ function scriptLikeSpecifier(spec) {
866
+ if (!spec.startsWith(".")) return false;
867
+ const trimmed = spec.split(/[?#]/, 1)[0];
868
+ return /\.(?:m?[jt]sx?|c[jt]s|vue)$/.test(trimmed) || !/\.[^/.]+$/.test(trimmed);
869
+ }
870
+ function externalPackageSpecifier(spec) {
871
+ return /^(?:node:[\w/.-]+|(?:@[a-z0-9~][\w.~-]*\/)?[a-z0-9~][\w.~-]*(?:\/.*)?)$/.test(spec);
872
+ }
873
+ function importEdges(files) {
874
+ const known = new Set(files.map((file) => file.path));
875
+ const edges = /* @__PURE__ */ new Map();
876
+ for (const file of files) {
877
+ if (extname(file.path) === ".html") continue;
878
+ const targets = [];
879
+ for (const record of sourceImports(file)) {
880
+ if (record.typeOnly) continue;
881
+ const resolved = resolveImport(dirname(file.path), record.specifier, known);
882
+ if (resolved !== void 0) targets.push(resolved);
883
+ }
884
+ edges.set(file.path, targets);
885
+ }
886
+ return edges;
887
+ }
888
+ function reachableFrom(starts, edges) {
889
+ const reached = new Set(starts);
890
+ const pending = [...reached];
891
+ while (pending.length > 0) {
892
+ const current = pending.pop();
893
+ if (current === void 0) break;
894
+ for (const target of edges.get(current) ?? []) if (!reached.has(target)) {
895
+ reached.add(target);
896
+ pending.push(target);
897
+ }
898
+ }
899
+ return reached;
900
+ }
901
+ function exposedFiles(packageRoot, packageJson, files, inboundSeeds) {
902
+ const known = new Set(files.map((file) => file.path));
903
+ const specs = [];
904
+ const collect = (value) => {
905
+ if (typeof value === "string") {
906
+ if (value.includes("*")) return "all";
907
+ specs.push(value);
908
+ return;
909
+ }
910
+ if (value !== null && typeof value === "object") for (const [key, child] of Object.entries(value)) {
911
+ if (key.includes("*")) return "all";
912
+ if (collect(child) === "all") return "all";
913
+ }
914
+ };
915
+ for (const field of [
916
+ "main",
917
+ "module",
918
+ "exports",
919
+ "browser"
920
+ ]) if (collect(packageJson[field]) === "all") return "all";
921
+ if (packageJson.exports === void 0 && packageJson.private !== true) return "all";
922
+ const declared = specs.length > 0;
923
+ const entryFileSeeds = [...inboundSeeds];
924
+ if (!declared) specs.push("./index");
925
+ const entryFiles = [...entryFileSeeds];
926
+ for (const spec of specs) {
927
+ const resolved = resolveImport(packageRoot, spec.startsWith(".") ? spec : `./${spec}`, known);
928
+ if (resolved === void 0) {
929
+ if (declared) return "all";
930
+ continue;
931
+ }
932
+ entryFiles.push(resolved);
933
+ }
934
+ const byPath = new Map(files.map((file) => [file.path, file]));
935
+ const exposed = new Set(entryFiles);
936
+ const pending = [...entryFiles];
937
+ while (pending.length > 0) {
938
+ const current = pending.pop();
939
+ if (current === void 0) break;
940
+ const file = byPath.get(current);
941
+ if (file === void 0 || extname(file.path) === ".html") continue;
942
+ for (const record of sourceImports(file)) {
943
+ if (record.typeOnly) continue;
944
+ const resolved = resolveImport(dirname(file.path), record.specifier, known);
945
+ if (resolved === void 0) {
946
+ if (scriptLikeSpecifier(record.specifier) || !record.specifier.startsWith(".") && !externalPackageSpecifier(record.specifier)) return "all";
947
+ continue;
948
+ }
949
+ if (!exposed.has(resolved)) {
950
+ exposed.add(resolved);
951
+ pending.push(resolved);
952
+ }
953
+ }
954
+ }
955
+ return exposed;
956
+ }
957
+ function proveSharedEntry(options) {
958
+ const scan = scanProof(options);
959
+ if (scan === null) return null;
960
+ const ownedSources = options.packageSources.filter((file) => options.owned(file.path));
961
+ 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);
962
+ if (loaders.length === 0) return null;
963
+ const reachable = reachableFrom(loaders, importEdges(ownedSources));
964
+ const ownedPaths = new Set(ownedSources.map((file) => file.path));
965
+ const packageName = typeof options.packageJson.name === "string" ? options.packageJson.name : void 0;
966
+ const resolveInbound = (fromDir, spec) => {
967
+ const relative = resolveImport(fromDir, spec, ownedPaths);
968
+ if (relative !== void 0) return relative;
969
+ if (packageName !== void 0 && spec.startsWith(`${packageName}/`)) return resolveImport(options.packageRoot, `./${spec.slice(packageName.length + 1)}`, ownedPaths);
970
+ };
971
+ const inboundSeeds = [];
972
+ let foreignUnverifiable = false;
973
+ for (const file of options.packageSources) {
974
+ if (options.owned(file.path) || extname(file.path) === ".html") continue;
975
+ if (importsEntry(file, options.entry)) continue;
976
+ const records = sourceImports(file);
977
+ if (records.includes(OPAQUE_SOURCE_IMPORT)) {
978
+ foreignUnverifiable = true;
979
+ continue;
980
+ }
981
+ for (const record of records) {
982
+ if (record.typeOnly) continue;
983
+ const resolved = resolveInbound(dirname(file.path), record.specifier);
984
+ if (resolved !== void 0) inboundSeeds.push(resolved);
985
+ }
986
+ }
987
+ const exposed = foreignUnverifiable ? "all" : exposedFiles(options.packageRoot, options.packageJson, ownedSources, inboundSeeds);
988
+ 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));
989
+ return {
990
+ provenConsumer,
991
+ provenStyle: (consumers) => consumers.length > 0 && consumers.every(provenConsumer)
992
+ };
993
+ }
994
+ function htmlLinksEntry(file, entry) {
995
+ if (extname(file.path) !== ".html") return false;
996
+ try {
997
+ const parsed = parseHtmlSource(file.path, file.source);
998
+ if (parsed.bases.length > 0) return false;
999
+ return parsed.links.some((link) => {
1000
+ const media = link.media.trim().toLowerCase();
1001
+ if (media !== "" && media !== "all") return false;
1002
+ const href = localHrefTarget(link.href);
1003
+ if (href === void 0) return false;
1004
+ return resolve(dirname(file.path), href) === entry;
1005
+ });
1006
+ } catch {
1007
+ return false;
1008
+ }
1009
+ }
1010
+ function importsStylesheet(file, stylePath) {
1011
+ 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);
1012
+ }
1013
+ function importsEntry(file, entry) {
1014
+ return sourceImports(file).some((record) => !record.typeOnly && !record.dynamic && record.specifier.startsWith(".") && resolve(dirname(file.path), record.specifier) === entry);
1015
+ }
1016
+ //#endregion
1017
+ //#region src/plan/html.ts
1018
+ async function preparePackageHtml({ packageRoot, sourceFiles, styleSources, snapshots, pathOwners, styleDependents }) {
1019
+ const files = [];
1020
+ const stylePaths = /* @__PURE__ */ new Set();
1021
+ const generatedPaths = /* @__PURE__ */ new Set();
1022
+ const removableLinks = [];
1023
+ const warnings = [];
1024
+ const htmlFiles = sourceFiles.filter((file) => extname(file.path) === ".html");
1025
+ const orderedFiles = [...htmlFiles.filter((file) => pathOwners.get(file.path) === packageRoot), ...htmlFiles.filter((file) => pathOwners.get(file.path) !== packageRoot)];
1026
+ for (const file of orderedFiles) {
1027
+ const owner = pathOwners.get(file.path);
1028
+ const foreign = owner !== packageRoot;
1029
+ if (foreign && !owner) continue;
1030
+ const referenceRoot = foreign ? owner : packageRoot;
1031
+ const parsed = parseHtmlSource(file.path, file.source);
1032
+ const contexts = [];
1033
+ let linkBase = dirname(file.path);
1034
+ const base = parsed.bases[0];
1035
+ if (base) {
1036
+ const baseReference = base.href.split(/[?#]/, 1)[0];
1037
+ const basePath = base.writable && (baseReference === "" ? file.path : localHtmlReference(referenceRoot, dirname(file.path), base.href));
1038
+ if (!basePath || !isWithin(referenceRoot, basePath)) {
1039
+ if (!foreign) warnings.push(htmlWarning("unsupported-html-base", file.path, base.start, base.end, "A remote or unrepresentable base URL prevents safe stylesheet link resolution."));
1040
+ linkBase = void 0;
1041
+ } else linkBase = base.href.split(/[?#]/, 1)[0].endsWith("/") ? basePath : dirname(basePath);
1042
+ }
1043
+ for (const link of parsed.links) {
1044
+ const linkedPath = linkBase && localHtmlReference(referenceRoot, linkBase, link.href);
1045
+ if (!linkedPath || !isWithin(packageRoot, linkedPath) || foreign && pathOwners.get(linkedPath) !== packageRoot) {
1046
+ if (!foreign) warnings.push(htmlWarning("unsupported-html-stylesheet-link", file.path, link.start, link.end, "Only local package stylesheet links are analyzed."));
1047
+ continue;
1048
+ }
1049
+ const variants = mediaVariants(link.media);
1050
+ if (variants === void 0) {
1051
+ const cssPath = (foreign ? void 0 : inferredPreprocessorPath({
697
1052
  path: linkedPath,
698
1053
  packageRoot,
699
1054
  styleSources,
@@ -783,7 +1138,7 @@ async function collectHtmlStyleContexts(state) {
783
1138
  analyzable: true
784
1139
  });
785
1140
  if (extname(state.path) !== ".css") return;
786
- for (const imported of cssImports(source)) {
1141
+ for (const imported of cssImports(state.path, source)) {
787
1142
  const variants = mediaVariants(imported.media);
788
1143
  if (variants === void 0) {
789
1144
  state.warnings.push(htmlWarning("unsupported-link-media", state.path, imported.start, imported.end, `The stylesheet import media condition ${JSON.stringify(imported.media)} cannot be represented safely.`));
@@ -806,7 +1161,7 @@ function inferredPreprocessorPath(state) {
806
1161
  return matches.length === 1 ? matches[0] : void 0;
807
1162
  }
808
1163
  function addInferredPreprocessorContext(state) {
809
- if (state.styleSources.has(state.path) && state.sourceFiles.some((file) => extname(file.path) !== ".html" && sourceReferencesStyle(file, state.path))) return false;
1164
+ if (state.styleSources.has(state.path) && state.sourceFiles.some((file) => extname(file.path) !== ".html" && importsStylesheet(file, state.path))) return false;
810
1165
  const path = inferredPreprocessorPath(state);
811
1166
  if (!path) return false;
812
1167
  state.generatedPaths.add(state.path);
@@ -821,8 +1176,8 @@ function addInferredPreprocessorContext(state) {
821
1176
  return true;
822
1177
  }
823
1178
  function localHtmlReference(packageRoot, base, reference) {
824
- const path = reference.split(/[?#]/, 1)[0];
825
- if (!path || path.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(path)) return void 0;
1179
+ const path = localHrefTarget(reference);
1180
+ if (path === void 0) return void 0;
826
1181
  let decoded;
827
1182
  try {
828
1183
  decoded = decodeURIComponent(path);
@@ -857,22 +1212,14 @@ function htmlWarning(code, file, start, end, message) {
857
1212
  }
858
1213
  //#endregion
859
1214
  //#region src/tailwind.ts
860
- function resolveTailwindEntry(stylePaths, styleSources, configuredPath) {
861
- const entries = stylePaths.filter((path) => {
862
- if (extname(path) !== ".css") return false;
863
- const source = maskCssComments(styleSources.get(path) ?? "");
864
- return /@import\s+["']tailwindcss(?:\/[^"']*)?["']/.test(source);
865
- });
866
- if (configuredPath) return {
867
- path: configuredPath,
868
- entries
869
- };
1215
+ function findTailwindEntries(stylePaths, styleSources) {
1216
+ return stylePaths.filter((path) => extname(path) === ".css" && importEntries(styleSources.get(path) ?? "").some(({ specifier }) => specifier === "tailwindcss" || specifier.startsWith("tailwindcss/")));
1217
+ }
1218
+ function selectTailwindEntry(entries, configuredPath) {
1219
+ if (configuredPath) return configuredPath;
870
1220
  if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
871
1221
  if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
872
- return {
873
- path: entries[0],
874
- entries
875
- };
1222
+ return entries[0];
876
1223
  }
877
1224
  async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
878
1225
  const projectRequire = createRequire(join(packageRoot, "package.json"));
@@ -907,49 +1254,560 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
907
1254
  } else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
908
1255
  else path = projectRequire.resolve(id);
909
1256
  return {
910
- content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
911
- base: dirname(path)
1257
+ content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
1258
+ base: dirname(path)
1259
+ };
1260
+ };
1261
+ const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
1262
+ const graphSources = [];
1263
+ const referenceTokens = /* @__PURE__ */ new Set();
1264
+ const themeTokens = {
1265
+ ...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
1266
+ ...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources, referenceTokens)
1267
+ };
1268
+ graphSources.push({
1269
+ path: tailwindCss,
1270
+ source: css
1271
+ });
1272
+ const designSystem = await loadDesignSystem(css, {
1273
+ base,
1274
+ loadModule,
1275
+ loadStylesheet
1276
+ });
1277
+ const loadWith = (replacement) => loadDesignSystem(replacement, {
1278
+ base,
1279
+ loadModule,
1280
+ loadStylesheet
1281
+ });
1282
+ return {
1283
+ designSystem,
1284
+ css,
1285
+ path: tailwindCss,
1286
+ themeTokens,
1287
+ referenceTokens,
1288
+ graphSources,
1289
+ loadWith
1290
+ };
1291
+ }
1292
+ function extractThemeTokens(key, css) {
1293
+ return stylesheetAnalysis(key, css).themeTokens;
1294
+ }
1295
+ function importEntries(css) {
1296
+ return (cssDirectives$1(css) ?? []).flatMap((directive) => directive !== null && typeof directive === "object" && "kind" in directive && directive.kind === "import" && "specifier" in directive && typeof directive.specifier === "string" ? [{
1297
+ specifier: directive.specifier,
1298
+ reference: "reference" in directive && directive.reference === true
1299
+ }] : []);
1300
+ }
1301
+ async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, referenceTokens, inReference = false, seen = /* @__PURE__ */ new Set()) {
1302
+ const tokens = {};
1303
+ for (const { specifier: spec, reference } of importEntries(css)) {
1304
+ const key = `${base}\0${spec}`;
1305
+ if (seen.has(key)) continue;
1306
+ seen.add(key);
1307
+ const loaded = await loadStylesheet(spec, base);
1308
+ if (spec !== "tailwindcss" && !spec.startsWith("tailwindcss/")) graphSources.push({
1309
+ path: key,
1310
+ source: loaded.content
1311
+ });
1312
+ Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, referenceTokens, inReference || reference, seen));
1313
+ }
1314
+ const own = extractThemeTokens(`${identity}.theme.css`, css);
1315
+ if (inReference) for (const token of Object.keys(own)) referenceTokens.add(token);
1316
+ return Object.assign(tokens, own);
1317
+ }
1318
+ function invalidCandidates(system, candidates) {
1319
+ const generated = system.candidatesToCss(candidates);
1320
+ return candidates.filter((_, index) => generated[index] === null);
1321
+ }
1322
+ //#endregion
1323
+ //#region src/plan/canonicalize.ts
1324
+ const caches = /* @__PURE__ */ new WeakMap();
1325
+ function canonicalCandidate(system, candidate) {
1326
+ if (typeof system.canonicalizeCandidates !== "function") return null;
1327
+ let cache = caches.get(system);
1328
+ if (cache === void 0) {
1329
+ cache = /* @__PURE__ */ new Map();
1330
+ caches.set(system, cache);
1331
+ }
1332
+ const cached = cache.get(candidate);
1333
+ if (cached !== void 0) return cached;
1334
+ let canonical;
1335
+ try {
1336
+ const results = system.canonicalizeCandidates([candidate]);
1337
+ canonical = results.length === 1 && typeof results[0] === "string" && results[0] !== candidate ? results[0] : null;
1338
+ } catch {
1339
+ canonical = null;
1340
+ }
1341
+ cache.set(candidate, canonical);
1342
+ return canonical;
1343
+ }
1344
+ function spellingReservations(styleSources, resolvedExtras, resolvedImports = /* @__PURE__ */ new Set()) {
1345
+ const reservations = {
1346
+ names: /* @__PURE__ */ new Set(),
1347
+ prefixes: /* @__PURE__ */ new Set(),
1348
+ unbounded: false,
1349
+ properties: /* @__PURE__ */ new Set(),
1350
+ propertiesUnbounded: false
1351
+ };
1352
+ for (const [path, source] of styleSources) scanStylesheetReservations(path, source, styleSources, reservations, resolvedImports);
1353
+ for (const [path, source] of resolvedExtras) try {
1354
+ const analysis = stylesheetAnalysis(path, source);
1355
+ for (const name of analysis.classNames) reservations.names.add(name);
1356
+ if (analysis.classReservationsUnbounded) reservations.unbounded = true;
1357
+ for (const property of analysis.customProperties) reservations.properties.add(property);
1358
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
1359
+ if (entryDirectiveHazard(source)) reservations.unbounded = true;
1360
+ } catch {
1361
+ reservations.unbounded = true;
1362
+ }
1363
+ return reservations;
1364
+ }
1365
+ function scanStylesheetReservations(path, source, styleSources, reservations, resolvedImports = /* @__PURE__ */ new Set()) {
1366
+ try {
1367
+ const analysis = stylesheetAnalysis(path, source);
1368
+ for (const name of analysis.classNames) reservations.names.add(name);
1369
+ if (analysis.classReservationsUnbounded) reservations.unbounded = true;
1370
+ for (const property of analysis.customProperties) reservations.properties.add(property);
1371
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
1372
+ if (analysis.unverifiable) reservations.unbounded = true;
1373
+ for (const reference of analysis.references) {
1374
+ if (reference === "tailwindcss" || reference.startsWith("tailwindcss/")) continue;
1375
+ if (reference.startsWith("sass:")) continue;
1376
+ if (resolvedImports.has(`${dirname(path)}\0${reference}`) || styleSources.has(reference)) continue;
1377
+ if (stylesheetReferenceTargets(path, reference, styleSources).length === 0) reservations.unbounded = true;
1378
+ }
1379
+ } catch {
1380
+ reservations.unbounded = true;
1381
+ }
1382
+ }
1383
+ function entryDirectiveHazard(source) {
1384
+ const directives = cssDirectives$1(source);
1385
+ if (directives === null) return true;
1386
+ return directives.some((directive) => {
1387
+ if (directive === null || typeof directive !== "object" || !("kind" in directive)) return false;
1388
+ if (directive.kind === "import") return "source" in directive && directive.source !== null || "sourceUnreadable" in directive && directive.sourceUnreadable === true;
1389
+ if (directive.kind === "source") return "not" in directive && directive.not === true || "unreadable" in directive && directive.unreadable === true;
1390
+ return directive.kind === "other" && "name" in directive && (directive.name === "plugin" || directive.name === "config");
1391
+ });
1392
+ }
1393
+ function scanInlineStyleReservations(key, value, reservations) {
1394
+ try {
1395
+ const analysis = stylesheetAnalysis(key, `.x{${value}}`);
1396
+ for (const property of analysis.customProperties) reservations.properties.add(property);
1397
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
1398
+ } catch {
1399
+ reservations.propertiesUnbounded = true;
1400
+ }
1401
+ }
1402
+ function reservedSpelling(reservations, spelling) {
1403
+ if (reservations.names.has(spelling)) return true;
1404
+ for (const prefix of reservations.prefixes) if (spelling.startsWith(prefix)) return true;
1405
+ return false;
1406
+ }
1407
+ function utilitySegment(candidate) {
1408
+ let depth = 0;
1409
+ let start = 0;
1410
+ for (let index = 0; index < candidate.length; index += 1) {
1411
+ const character = candidate[index];
1412
+ if (character === "[" || character === "(") depth += 1;
1413
+ else if (character === "]" || character === ")") depth -= 1;
1414
+ else if (character === ":" && depth === 0) start = index + 1;
1415
+ }
1416
+ return candidate.slice(start);
1417
+ }
1418
+ function sameShape(left, right) {
1419
+ return left.length === right.length && left.every((declaration, index) => declaration.property === right[index].property && declaration.important === right[index].important);
1420
+ }
1421
+ function acceptedCandidateAliases(system, probes, reservations) {
1422
+ const aliases = {};
1423
+ if (reservations.unbounded) return aliases;
1424
+ const prefix = system.theme.prefix;
1425
+ for (const probe of new Set(probes)) {
1426
+ const canonical = canonicalCandidate(system, probe);
1427
+ if (canonical === null) continue;
1428
+ const unprefixed = prefix !== null && canonical.startsWith(`${prefix}:`) ? canonical.slice(prefix.length + 1) : null;
1429
+ const segment = utilitySegment(canonical).replace(/!$/, "");
1430
+ if (segment.includes("[") || reservedSpelling(reservations, canonical) || unprefixed !== null && reservedSpelling(reservations, unprefixed) || reservedSpelling(reservations, segment)) continue;
1431
+ const [compiledProbe, compiledCanonical] = system.candidatesToCss([probe, canonical]);
1432
+ if (compiledProbe == null || compiledCanonical == null) continue;
1433
+ let probeShape;
1434
+ let canonicalShape;
1435
+ try {
1436
+ probeShape = compiledShape(compiledProbe);
1437
+ canonicalShape = compiledShape(compiledCanonical);
1438
+ } catch {
1439
+ continue;
1440
+ }
1441
+ if (canonicalShape.referencedProperties.length > 0 || probeShape.referencedProperties.length > 0 || !sameShape(probeShape.declarations, canonicalShape.declarations)) continue;
1442
+ aliases[probe] = canonical;
1443
+ }
1444
+ return aliases;
1445
+ }
1446
+ //#endregion
1447
+ //#region src/plan/fonts.ts
1448
+ function fontTokenName(family) {
1449
+ const collapsed = family.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
1450
+ if (collapsed === "") return "family";
1451
+ return /^\p{N}/u.test(collapsed) ? `family-${collapsed}` : collapsed;
1452
+ }
1453
+ function aliasedCandidate(candidate, utility) {
1454
+ const segment = utilitySegment(candidate);
1455
+ const important = segment.endsWith("!") ? "!" : "";
1456
+ return candidate.slice(0, candidate.length - segment.length) + utility + important;
1457
+ }
1458
+ function appendFontTheme(css, tokens) {
1459
+ const names = Object.keys(tokens).sort();
1460
+ if (names.length === 0) return css;
1461
+ let output = css;
1462
+ if (!output.endsWith("\n")) output += "\n";
1463
+ if (!output.endsWith("\n\n")) output += "\n";
1464
+ output += `@theme {\n${names.map((name) => ` --font-${name}: ${tokens[name]};\n`).join("")}}\n`;
1465
+ return output;
1466
+ }
1467
+ function fontShapesAccept(system, probeCandidate, aliased, reservations, backingToken) {
1468
+ const [compiledProbe, compiledAliased] = system.candidatesToCss([probeCandidate, aliased]);
1469
+ if (compiledProbe == null || compiledAliased == null) return false;
1470
+ let probeShape;
1471
+ let aliasedShape;
1472
+ try {
1473
+ probeShape = compiledShape(compiledProbe);
1474
+ aliasedShape = compiledShape(compiledAliased);
1475
+ } catch {
1476
+ return false;
1477
+ }
1478
+ if (probeShape.referencedProperties.length > 0) return false;
1479
+ if (aliasedShape.referencedProperties.some((property) => reservations.properties.has(property))) return false;
1480
+ if (backingToken !== void 0) {
1481
+ if (!aliasedShape.declarations.some((declaration) => declaration.property === "font-family" && /^var\(--[\w-]+\)$/.test(declaration.value) && (declaration.value === `var(--${backingToken})` || declaration.value.endsWith(`-${backingToken})`)))) return false;
1482
+ }
1483
+ return sameShape(probeShape.declarations, aliasedShape.declarations);
1484
+ }
1485
+ async function registerFontTokens(options) {
1486
+ const { system, reservations } = options;
1487
+ if (reservations.unbounded || reservations.propertiesUnbounded) return {
1488
+ aliases: {},
1489
+ tokens: {},
1490
+ failures: /* @__PURE__ */ new Set()
1491
+ };
1492
+ const prefix = system.theme.prefix;
1493
+ const aliases = {};
1494
+ const failures = /* @__PURE__ */ new Set();
1495
+ const seen = /* @__PURE__ */ new Set();
1496
+ const allocatedByStack = /* @__PURE__ */ new Map();
1497
+ const proposals = [];
1498
+ const claimed = /* @__PURE__ */ new Set();
1499
+ const tokenStacks = fontTokenStacks(options.themeTokens, options.referenceTokens);
1500
+ for (const probe of options.probes) {
1501
+ if (seen.has(probe.candidate)) continue;
1502
+ seen.add(probe.candidate);
1503
+ if (probe.firstFamily.kind !== "name") continue;
1504
+ if (probe.candidate in options.existingAliases) continue;
1505
+ let reused = false;
1506
+ for (const token of matchingFontTokens(tokenStacks, probe.value)) {
1507
+ const aliased = aliasedCandidate(probe.candidate, token);
1508
+ if (reservedSpelling(reservations, token) || reservedSpelling(reservations, aliased)) continue;
1509
+ const reuseName = token.replace(/^font-/, "");
1510
+ const owner = options.globalAllocations.get(reuseName);
1511
+ if (owner !== void 0 && (owner === null || owner !== probe.value)) continue;
1512
+ if (fontShapesAccept(system, probe.candidate, aliased, reservations, token)) {
1513
+ aliases[probe.candidate] = aliased;
1514
+ options.globalAllocations.set(reuseName, probe.value);
1515
+ reused = true;
1516
+ break;
1517
+ }
1518
+ }
1519
+ if (reused) continue;
1520
+ if (!options.generate) continue;
1521
+ const stackKey = probe.value;
1522
+ const shared = allocatedByStack.get(stackKey);
1523
+ if (shared !== void 0) {
1524
+ proposals.push({
1525
+ probe,
1526
+ name: shared
1527
+ });
1528
+ continue;
1529
+ }
1530
+ const base = fontTokenName(probe.firstFamily.name);
1531
+ let allocated = false;
1532
+ for (let attempt = 1; attempt <= 100; attempt += 1) {
1533
+ const name = attempt === 1 ? base : `${base}-${attempt}`;
1534
+ if (claimed.has(name)) continue;
1535
+ if (reservations.properties.has(`--font-${name}`)) continue;
1536
+ if (prefix !== null && reservations.properties.has(`--${prefix}-font-${name}`)) continue;
1537
+ if (reservedSpelling(reservations, `font-${name}`)) continue;
1538
+ if (options.mentionedSegments.has(`font-${name}`)) continue;
1539
+ const global = options.globalAllocations.get(name);
1540
+ if (global !== void 0 && (global === null || global !== probe.value)) continue;
1541
+ const compiled = prefix !== null ? `${prefix}:font-${name}` : `font-${name}`;
1542
+ if (system.candidatesToCss([compiled])[0] !== null) continue;
1543
+ claimed.add(name);
1544
+ allocatedByStack.set(stackKey, name);
1545
+ proposals.push({
1546
+ probe,
1547
+ name
1548
+ });
1549
+ allocated = true;
1550
+ break;
1551
+ }
1552
+ if (!allocated) failures.add(probe.candidate);
1553
+ }
1554
+ if (proposals.length === 0) return {
1555
+ aliases,
1556
+ tokens: {},
1557
+ failures
1558
+ };
1559
+ const tokens = {};
1560
+ for (const { probe, name } of proposals) tokens[name] = probe.value;
1561
+ let fontSystem;
1562
+ try {
1563
+ fontSystem = await options.loadWith(appendFontTheme(options.entryCss, tokens));
1564
+ } catch (error) {
1565
+ if (isIntegrityError(error)) throw error;
1566
+ for (const { probe } of proposals) failures.add(probe.candidate);
1567
+ return {
1568
+ aliases,
1569
+ tokens: {},
1570
+ failures
912
1571
  };
1572
+ }
1573
+ const before = system.candidatesToCss(options.corpus);
1574
+ const after = fontSystem.candidatesToCss(options.corpus);
1575
+ const corpusChanged = options.corpus.some((candidate, index) => before[index] !== after[index]);
1576
+ const accepted = {};
1577
+ for (const { probe, name } of proposals) {
1578
+ const canonical = corpusChanged ? null : canonicalCandidate(fontSystem, probe.candidate);
1579
+ const expected = aliasedCandidate(probe.candidate, `font-${name}`);
1580
+ if (canonical !== expected || !fontShapesAccept(fontSystem, probe.candidate, expected, reservations)) {
1581
+ failures.add(probe.candidate);
1582
+ continue;
1583
+ }
1584
+ aliases[probe.candidate] = expected;
1585
+ accepted[name] = probe.value;
1586
+ }
1587
+ return {
1588
+ aliases,
1589
+ tokens: accepted,
1590
+ failures
913
1591
  };
914
- const themeTokens = {
915
- ...extractThemeTokens(await readFile(join(dirname(packagePath), "theme.css"), "utf8")),
916
- ...await extractThemeTokensFromGraph(css, base, loadStylesheet)
1592
+ }
1593
+ function fontTokenStacks(themeTokens, referenceTokens = /* @__PURE__ */ new Set()) {
1594
+ const stacks = /* @__PURE__ */ new Map();
1595
+ for (const [token, value] of Object.entries(themeTokens)) {
1596
+ if (!token.startsWith("font-")) continue;
1597
+ if (referenceTokens.has(token)) continue;
1598
+ const parsed = fontFamilyStack(value);
1599
+ if (parsed !== null) stacks.set(token, parsed.value);
1600
+ }
1601
+ return stacks;
1602
+ }
1603
+ function matchingFontTokens(stacks, normalizedStack) {
1604
+ return [...stacks].filter(([, stack]) => stack === normalizedStack).map(([token]) => token).sort();
1605
+ }
1606
+ //#endregion
1607
+ //#region src/plan/media.ts
1608
+ const GENERATED_NAMESPACE = "twm-media";
1609
+ /**
1610
+ * Resolve one fixed name per key across a complete entry group.
1611
+ *
1612
+ * Resolution follows the RFC order: a built-in candidate with a verified
1613
+ * effective expansion, an existing breakpoint matched by exact value and
1614
+ * verified the same way, an adopted definition whose name, normalized
1615
+ * meaning, and effective expansion equal what the migration would emit,
1616
+ * the readable generated name, the digest name, and finally the
1617
+ * arbitrary-variant fallback. Reservation is ownership-blind:
1618
+ * authored names with any different meaning, active theme names, explicit
1619
+ * reserved names such as inert scanned candidates, and any name the design
1620
+ * system resolves all block a generated name.
1621
+ */
1622
+ function resolveMediaNames(collections, themeTokens, probes, reservedNames = []) {
1623
+ const components = mergeComponents(collections);
1624
+ const authoredKeysByName = mergeAuthoredVariants(collections);
1625
+ const reserved = new Set(reservedNames);
1626
+ for (const token of Object.keys(themeTokens)) if (token.startsWith("breakpoint-")) reserved.add(token.slice(11));
1627
+ const claimed = /* @__PURE__ */ new Set();
1628
+ const adoptableKey = (name) => {
1629
+ const keys = authoredKeysByName.get(name);
1630
+ if (keys === void 0 || keys.size !== 1) return null;
1631
+ const [key] = keys;
1632
+ return key ?? null;
917
1633
  };
1634
+ const taken = (name) => claimed.has(name) || reserved.has(name) || authoredKeysByName.has(name) || probes.resolves(name);
1635
+ const names = /* @__PURE__ */ new Map();
1636
+ const fallbacks = /* @__PURE__ */ new Set();
1637
+ for (const component of components) {
1638
+ const resolved = (name, kind) => {
1639
+ claimed.add(name);
1640
+ names.set(component.key, {
1641
+ name,
1642
+ kind
1643
+ });
1644
+ };
1645
+ if (component.builtin !== null && probes.expansionMatches(component.builtin, component.key)) {
1646
+ resolved(component.builtin, "builtin");
1647
+ continue;
1648
+ }
1649
+ if (component.breakpoint !== null && probes.expansionMatches(component.breakpoint, component.key)) {
1650
+ resolved(component.breakpoint, "breakpoint");
1651
+ continue;
1652
+ }
1653
+ const digestName = `${GENERATED_NAMESPACE}-${component.digest}`;
1654
+ const candidates = component.readableName === null ? [digestName] : [component.readableName, digestName];
1655
+ let done = false;
1656
+ for (const candidate of candidates) {
1657
+ if (adoptableKey(candidate) === component.key && !claimed.has(candidate) && probes.expansionMatches(candidate, component.key)) {
1658
+ resolved(candidate, "adopted");
1659
+ done = true;
1660
+ break;
1661
+ }
1662
+ if (!taken(candidate)) {
1663
+ resolved(candidate, "generated");
1664
+ done = true;
1665
+ break;
1666
+ }
1667
+ }
1668
+ if (!done) fallbacks.add(component.key);
1669
+ }
918
1670
  return {
919
- designSystem: await loadDesignSystem(css, {
920
- base,
921
- loadModule,
922
- loadStylesheet
923
- }),
924
- css,
925
- path: tailwindCss,
926
- themeTokens
1671
+ names,
1672
+ fallbacks
927
1673
  };
928
1674
  }
929
- function extractThemeTokens(css) {
930
- const tokens = {};
931
- for (const block of css.matchAll(/@theme[^{]*\{([^}]*)\}/gs)) for (const match of block[1].matchAll(/--([\w-]+):\s*([^;{}]+);/g)) tokens[match[1]] = match[2].trim();
932
- return tokens;
1675
+ function mergeComponents(collections) {
1676
+ const byKey = /* @__PURE__ */ new Map();
1677
+ for (const collection of collections) for (const component of collection.components) if (!byKey.has(component.key)) byKey.set(component.key, component);
1678
+ return [...byKey.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
1679
+ }
1680
+ function mergeAuthoredVariants(collections) {
1681
+ const byName = /* @__PURE__ */ new Map();
1682
+ for (const collection of collections) for (const variant of collection.authoredVariants) {
1683
+ const keys = byName.get(variant.name) ?? /* @__PURE__ */ new Set();
1684
+ keys.add(variant.mediaQueryKey);
1685
+ byName.set(variant.name, keys);
1686
+ }
1687
+ return byName;
1688
+ }
1689
+ const PROBE_UTILITY = "[--tw-probe:1]";
1690
+ function probeCandidate(prefix, name) {
1691
+ return `${prefix === null || prefix === "" ? "" : `${prefix}:`}${name}:${PROBE_UTILITY}`;
1692
+ }
1693
+ /**
1694
+ * Build the effective-expansion probes against the unaugmented loaded
1695
+ * design system. `resolves` proves only existence; `expansionMatches`
1696
+ * proves that compiling the name yields exactly one `@media` wrapper whose
1697
+ * normalized query equals the key.
1698
+ */
1699
+ function buildMediaProbes(tailwind) {
1700
+ const compiled = /* @__PURE__ */ new Map();
1701
+ const prefix = tailwind.designSystem.theme.prefix;
1702
+ const compile = (name) => {
1703
+ let css = compiled.get(name);
1704
+ if (css === void 0) {
1705
+ [css = null] = tailwind.designSystem.candidatesToCss([probeCandidate(prefix, name)]);
1706
+ compiled.set(name, css);
1707
+ }
1708
+ return css;
1709
+ };
1710
+ return {
1711
+ resolves: (name) => compile(name) !== null,
1712
+ expansionMatches: (name, key) => {
1713
+ const css = compile(name);
1714
+ if (css === null) return false;
1715
+ return JSON.parse(mediaProbeKey(css)) === key;
1716
+ }
1717
+ };
933
1718
  }
934
- async function extractThemeTokensFromGraph(css, base, loadStylesheet, seen = /* @__PURE__ */ new Set()) {
935
- const tokens = {};
936
- for (const match of css.matchAll(/@import\s+["']([^"']+)["']/g)) {
937
- const key = `${base}\0${match[1]}`;
938
- if (seen.has(key)) continue;
939
- seen.add(key);
940
- const loaded = await loadStylesheet(match[1], base);
941
- Object.assign(tokens, await extractThemeTokensFromGraph(loaded.content, loaded.base, loadStylesheet, seen));
1719
+ /**
1720
+ * Conservative scanned-corpus reservation: every colon-chained token in the
1721
+ * package's scanned sources reserves its variant segments, so an inert
1722
+ * candidate such as `width-lte-768px:hidden` is never activated with a new
1723
+ * meaning. Over-reservation only pushes a generated name to its digest
1724
+ * form, and content-identity adoption bypasses these reservations, so
1725
+ * reruns stay idempotent.
1726
+ */
1727
+ function scannedVariantReservations(sources) {
1728
+ const reserved = /* @__PURE__ */ new Set();
1729
+ 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);
1730
+ return reserved;
1731
+ }
1732
+ /**
1733
+ * Run native condition collection for one member package of an entry
1734
+ * group. Kept separate from resolution so a recoverable collection
1735
+ * failure is attributable to its member.
1736
+ */
1737
+ function collectMemberMediaConditions(stylesheets, tailwind) {
1738
+ return JSON.parse(collectMediaConditions(JSON.stringify({
1739
+ stylesheets,
1740
+ themeTokens: tailwind.themeTokens,
1741
+ tailwindSources: tailwind.graphSources
1742
+ })));
1743
+ }
1744
+ /**
1745
+ * Resolve the fixed map the planner consumes plus the definitions
1746
+ * extraction may emit from the group's member collections. Name
1747
+ * allocation runs once for the complete group, so different keys from
1748
+ * different packages can never independently claim one name.
1749
+ */
1750
+ function planMediaExtraction(options) {
1751
+ const resolution = resolveMediaNames(options.collections, options.tailwind.themeTokens, buildMediaProbes(options.tailwind), scannedVariantReservations([...options.scannedSources, ...options.tailwind.graphSources.map((graphSource) => graphSource.source)]));
1752
+ const names = {};
1753
+ const generated = [];
1754
+ for (const [key, resolved] of resolution.names) {
1755
+ names[key] = resolved.name;
1756
+ if (resolved.kind === "generated") generated.push({
1757
+ key,
1758
+ name: resolved.name
1759
+ });
942
1760
  }
943
- return Object.assign(tokens, extractThemeTokens(css));
1761
+ return {
1762
+ names,
1763
+ generated
1764
+ };
944
1765
  }
945
- function invalidCandidates(tailwind, candidates) {
946
- const generated = tailwind.designSystem.candidatesToCss(candidates);
947
- return candidates.filter((_, index) => generated[index] === null);
1766
+ /**
1767
+ * The generated definitions the final plan actually uses, ordered by the
1768
+ * first applied rule that uses each name. A rule counts as applied only
1769
+ * when every one of its candidates survived into `plan.candidates`: a
1770
+ * retained global rule still applies its utilities to proven consumers,
1771
+ * while a blocked rule always contains a dropped candidate, so its
1772
+ * occurrences must not fix registration order for a candidate another rule
1773
+ * still applies. Registration order controls conflicting utility
1774
+ * precedence, so the order follows the applied rules rather than every
1775
+ * encountered condition.
1776
+ */
1777
+ function usedGeneratedDefinitions(extraction, plan) {
1778
+ const applied = new Set(plan.candidates);
1779
+ const byName = new Map(extraction.generated.map((definition) => [definition.name, definition]));
1780
+ const ordered = [];
1781
+ const seen = /* @__PURE__ */ new Set();
1782
+ for (const rule of plan.rules) {
1783
+ if (!rule.candidates.every((candidate) => applied.has(candidate))) continue;
1784
+ for (const candidate of rule.candidates) for (const segment of candidate.split(":").slice(0, -1)) {
1785
+ const definition = byName.get(segment);
1786
+ if (definition !== void 0 && !seen.has(segment)) {
1787
+ seen.add(segment);
1788
+ ordered.push(definition);
1789
+ }
1790
+ }
1791
+ }
1792
+ return ordered;
1793
+ }
1794
+ /**
1795
+ * Append the used definitions to the entry source, each as its own block,
1796
+ * matching the planner's moved-block formatting. Definitions already
1797
+ * present are recognized through adoption before this runs, so appending
1798
+ * never duplicates one.
1799
+ */
1800
+ function appendMediaDefinitions(source, definitions) {
1801
+ let output = source;
1802
+ for (const { key, name } of definitions) {
1803
+ if (!output.endsWith("\n")) output += "\n";
1804
+ if (!output.endsWith("\n\n")) output += "\n";
1805
+ output += `@custom-variant ${name} {\n @media ${key} {\n @slot;\n }\n}\n`;
1806
+ }
1807
+ return output;
948
1808
  }
949
1809
  //#endregion
950
1810
  //#region src/parser/vue.ts
951
- const ESCAPE_SELECTOR = /(?:::v-|:)(?:deep|global|slotted)\(([^)]*)\)/g;
952
- const ESCAPE_RESIDUE = /(?:>>>|\/deep\/|::v-deep|:deep|::v-slotted|:slotted|::v-global|:global)/;
953
1811
  const SUPPORTED_STYLE_ATTRIBUTES = /* @__PURE__ */ new Set([
954
1812
  "lang",
955
1813
  "module",
@@ -1002,7 +1860,9 @@ function analyzeVueSource(compiler, path, source) {
1002
1860
  const { descriptor, errors } = compiler.parse(source, { filename: path });
1003
1861
  const styleRanges = descriptor.styles.map((style) => ({
1004
1862
  start: style.loc.start.offset,
1005
- end: style.loc.end.offset
1863
+ end: style.loc.end.offset,
1864
+ lang: style.lang ?? "css",
1865
+ ...style.src !== void 0 ? { src: style.src } : {}
1006
1866
  }));
1007
1867
  const byteRanges = (offset) => ({
1008
1868
  warnings: warnings.map((warning) => ({
@@ -1011,6 +1871,7 @@ function analyzeVueSource(compiler, path, source) {
1011
1871
  end: offset(warning.end)
1012
1872
  })),
1013
1873
  styleRanges: styleRanges.map((range) => ({
1874
+ ...range,
1014
1875
  start: offset(range.start),
1015
1876
  end: offset(range.end)
1016
1877
  }))
@@ -1053,6 +1914,7 @@ function analyzeVueSource(compiler, path, source) {
1053
1914
  const shadowPreprocessorTexts = [];
1054
1915
  const unscopedShadowPreprocessorTexts = [];
1055
1916
  let escapeUnverifiable = false;
1917
+ let opaqueStyleBlocks = false;
1056
1918
  let moduleSiblingUnsupported = false;
1057
1919
  for (const style of descriptor.styles) {
1058
1920
  const start = style.loc.start.offset;
@@ -1063,6 +1925,7 @@ function analyzeVueSource(compiler, path, source) {
1063
1925
  warn("unsupported-sfc-block", start, end, `Unsupported <style> attributes: ${unsupportedAttributes.join(", ")}.`);
1064
1926
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1065
1927
  escapeUnverifiable = true;
1928
+ opaqueStyleBlocks = true;
1066
1929
  continue;
1067
1930
  }
1068
1931
  if (style.src !== void 0) {
@@ -1070,6 +1933,7 @@ function analyzeVueSource(compiler, path, source) {
1070
1933
  warn("unsupported-sfc-block", start, end, "A <style module src> block is not supported.");
1071
1934
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1072
1935
  escapeUnverifiable = true;
1936
+ opaqueStyleBlocks = true;
1073
1937
  } else styleBlockImports.push({
1074
1938
  reference: style.src,
1075
1939
  start,
@@ -1081,12 +1945,19 @@ function analyzeVueSource(compiler, path, source) {
1081
1945
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1082
1946
  warn("unsupported-sfc-block", start, end, "Named <style module> blocks are not supported.");
1083
1947
  shadowModuleCssTexts.push(style.content);
1948
+ opaqueStyleBlocks = true;
1084
1949
  continue;
1085
1950
  }
1086
1951
  if (!SUPPORTED_STYLE_LANGUAGES.has(style.lang)) {
1087
1952
  if (usesDefaultModuleBinding) moduleSiblingUnsupported = true;
1088
1953
  warn("preprocessor-style-block", start, end, `The <style lang="${style.lang}"> language is not supported.`);
1089
1954
  shadowPreprocessorTexts.push(style.content);
1955
+ opaqueStyleBlocks = true;
1956
+ try {
1957
+ if (stylesheetAnalysis(`${path}.css`, style.content).selectorsUnverifiable) escapeUnverifiable = true;
1958
+ } catch {
1959
+ escapeUnverifiable = true;
1960
+ }
1090
1961
  continue;
1091
1962
  }
1092
1963
  const outerStart = source.lastIndexOf("<style", start);
@@ -1094,6 +1965,7 @@ function analyzeVueSource(compiler, path, source) {
1094
1965
  if (outerStart < 0 || !closing) {
1095
1966
  warn("unsupported-sfc-block", start, end, "The style block tags could not be located.");
1096
1967
  escapeUnverifiable = true;
1968
+ opaqueStyleBlocks = true;
1097
1969
  continue;
1098
1970
  }
1099
1971
  const block = {
@@ -1110,24 +1982,45 @@ function analyzeVueSource(compiler, path, source) {
1110
1982
  }
1111
1983
  if (!style.scoped) {
1112
1984
  unscopedBlocks.push(block);
1113
- if (style.lang && style.lang !== "css") unscopedShadowPreprocessorTexts.push(style.content);
1114
- else unscopedShadowCssTexts.push(style.content);
1985
+ if (style.lang && style.lang !== "css") {
1986
+ unscopedShadowPreprocessorTexts.push(style.content);
1987
+ try {
1988
+ if (stylesheetAnalysis(`${path}.${style.lang}`, style.content).selectorsUnverifiable) escapeUnverifiable = true;
1989
+ } catch {
1990
+ escapeUnverifiable = true;
1991
+ }
1992
+ } else unscopedShadowCssTexts.push(style.content);
1115
1993
  continue;
1116
1994
  }
1117
- for (const [, inner] of style.content.matchAll(ESCAPE_SELECTOR)) {
1118
- shadowCssTexts.push(`${inner} {}`);
1119
- if (inner.includes("(")) escapeUnverifiable = true;
1995
+ try {
1996
+ const escapes = stylesheetAnalysis(`${path}.${style.lang ?? "css"}`, style.content);
1997
+ shadowCssTexts.push(...escapes.scopeEscapes);
1998
+ if (escapes.scopeEscapes.length > 0) block.shadowSource = escapes.scopeShadowCss.join("\n") || "/* scope escapes extracted */";
1999
+ if (escapes.scopeEscapesUnverifiable || escapes.selectorsUnverifiable) escapeUnverifiable = true;
2000
+ } catch {
2001
+ escapeUnverifiable = true;
1120
2002
  }
1121
- if (ESCAPE_RESIDUE.test(style.content.replace(ESCAPE_SELECTOR, " "))) escapeUnverifiable = true;
1122
2003
  blocks.push(block);
1123
2004
  }
2005
+ const scriptLang = descriptor.scriptSetup?.lang ?? descriptor.script?.lang;
1124
2006
  const state = {
1125
2007
  elements: [],
1126
2008
  components: [],
1127
2009
  dynamic: false,
1128
2010
  vHtml: false,
1129
2011
  hasSlot: false,
1130
- expressionTexts: []
2012
+ moduleClosureBroken: false,
2013
+ referencesUseCssModule: false,
2014
+ expressionReferences: /* @__PURE__ */ new Set(),
2015
+ templatePrefixes: /* @__PURE__ */ new Set(),
2016
+ entityClassTokens: [],
2017
+ styleAttributeValues: [],
2018
+ styleAttributesUnverifiable: false,
2019
+ customProperties: /* @__PURE__ */ new Set(),
2020
+ customPropertiesUnbounded: false,
2021
+ stylesheetLinks: [],
2022
+ stylesheetLinksUnverifiable: false,
2023
+ expressionPath: scriptLang === "ts" || scriptLang === "tsx" ? "Component.ts" : "Component.js"
1131
2024
  };
1132
2025
  visitTemplateNode(source, template.ast, state);
1133
2026
  const alwaysRenderedRoots = template.ast.children.filter((node) => node.type === NODE_ELEMENT && (node.tagType === TAG_ELEMENT || node.tagType === TAG_COMPONENT) && !node.props?.some((prop) => prop.type === PROP_DIRECTIVE && [
@@ -1137,8 +2030,17 @@ function analyzeVueSource(compiler, path, source) {
1137
2030
  "for"
1138
2031
  ].includes(prop.name))).length;
1139
2032
  const scriptText = [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean).join("\n");
1140
- const fallthroughUnverifiable = Boolean(descriptor.script) || descriptor.scriptSetup?.src !== void 0 || /\b(?:defineOptions|defineProps|inheritAttrs)\b/.test(descriptor.scriptSetup?.content ?? "");
1141
2033
  let scriptImportsUnverifiable = false;
2034
+ const scriptImports = [];
2035
+ let setupAnalysis;
2036
+ let scriptUsesCssModule = false;
2037
+ let scriptHasDynamicImport = false;
2038
+ const scriptCustomProperties = [];
2039
+ let scriptCustomPropertiesUnbounded = false;
2040
+ const scriptVueReferences = [];
2041
+ const scriptVueGlobPatterns = [];
2042
+ const scriptTemplatePrefixes = [];
2043
+ let scriptVueGlobUnverifiable = false;
1142
2044
  const styleImports = [descriptor.script, descriptor.scriptSetup].flatMap((script) => {
1143
2045
  if (!script) return [];
1144
2046
  if (script.src || !SUPPORTED_SCRIPT_LANGUAGES.has(script.lang)) {
@@ -1146,18 +2048,25 @@ function analyzeVueSource(compiler, path, source) {
1146
2048
  return [];
1147
2049
  }
1148
2050
  try {
1149
- return staticImports(`${path}.${script.lang ?? "js"}`, script.content);
2051
+ const analysis = sourceAnalysis(`${path}.${script.lang ?? "js"}`, script.content);
2052
+ if (script === descriptor.scriptSetup) setupAnalysis = analysis;
2053
+ if (analysis.usesCssModule || analysis.hasUnboundUseCssModule) scriptUsesCssModule = true;
2054
+ if (analysis.hasDynamicImport) scriptHasDynamicImport = true;
2055
+ scriptImports.push(...analysis.imports);
2056
+ scriptVueReferences.push(...analysis.imports.filter((record) => !record.typeOnly).map((record) => record.specifier));
2057
+ scriptVueGlobPatterns.push(...analysis.vueGlobPatterns);
2058
+ scriptTemplatePrefixes.push(...analysis.templatePrefixes);
2059
+ scriptCustomProperties.push(...analysis.customProperties);
2060
+ if (analysis.customPropertiesUnbounded) scriptCustomPropertiesUnbounded = true;
2061
+ if (analysis.vueGlobUnverifiable) scriptVueGlobUnverifiable = true;
2062
+ return analysis.staticImports;
1150
2063
  } catch {
1151
2064
  scriptImportsUnverifiable = true;
1152
2065
  return [];
1153
2066
  }
1154
2067
  });
1155
- let componentImports = [];
1156
- if (descriptor.scriptSetup && !descriptor.scriptSetup.src && SUPPORTED_SCRIPT_LANGUAGES.has(descriptor.scriptSetup.lang)) try {
1157
- componentImports = staticImportBindings(`${path}.${descriptor.scriptSetup.lang ?? "js"}`, descriptor.scriptSetup.content);
1158
- } catch {
1159
- componentImports = [];
1160
- }
2068
+ const fallthroughUnverifiable = Boolean(descriptor.script) || descriptor.scriptSetup?.src !== void 0 || Boolean(descriptor.scriptSetup) && !setupAnalysis || setupAnalysis?.hasVueFallthroughMacro === true;
2069
+ const componentImports = setupAnalysis?.defaultImports ?? [];
1161
2070
  const offsets = utf8OffsetMap(source, [
1162
2071
  ...warnings.flatMap((warning) => [warning.start, warning.end]),
1163
2072
  ...styleRanges.flatMap((range) => [range.start, range.end]),
@@ -1209,7 +2118,7 @@ function analyzeVueSource(compiler, path, source) {
1209
2118
  blocks: blocks.map(toByteBlock),
1210
2119
  unscopedBlocks: unscopedBlocks.map(toByteBlock),
1211
2120
  moduleBlocks: moduleBlocks.map(toByteBlock),
1212
- moduleClosureBroken: moduleSiblingUnsupported || scriptImportsUnverifiable || /\$style|useCssModule/.test([...state.expressionTexts, scriptText].join("\n")),
2121
+ moduleClosureBroken: moduleSiblingUnsupported || scriptImportsUnverifiable || scriptUsesCssModule || state.moduleClosureBroken || state.referencesUseCssModule && setupAnalysis?.definesRootUseCssModule !== true || (setupAnalysis?.useCssModuleLocals ?? []).some((name) => state.expressionReferences.has(name)),
1213
2122
  htmlElements: state.elements.map(toByteSite),
1214
2123
  componentSites: state.components.map(toByteSite),
1215
2124
  rootStarts: template.ast.children.filter((node) => node.type === NODE_ELEMENT).map((node) => offsets.get(node.loc.start.offset)),
@@ -1217,7 +2126,21 @@ function analyzeVueSource(compiler, path, source) {
1217
2126
  rootFragment: template.ast.children.filter((node) => node.type === NODE_ELEMENT || node.type === NODE_INTERPOLATION || node.type === NODE_TEXT && node.content?.trim?.() !== "").length !== 1,
1218
2127
  scriptText,
1219
2128
  scriptStyleImports: [...new Set(styleImports)],
2129
+ scriptImports,
1220
2130
  scriptImportsUnverifiable,
2131
+ scriptHasDynamicImport,
2132
+ scriptVueReferences,
2133
+ scriptVueGlobPatterns,
2134
+ scriptVueGlobUnverifiable,
2135
+ templatePrefixes: [.../* @__PURE__ */ new Set([...state.templatePrefixes, ...scriptTemplatePrefixes])],
2136
+ templateClassTokens: [...new Set(state.entityClassTokens)],
2137
+ templateStyleValues: state.styleAttributeValues,
2138
+ templateStylesUnverifiable: state.styleAttributesUnverifiable,
2139
+ scriptCustomProperties: [...scriptCustomProperties, ...state.customProperties],
2140
+ scriptCustomPropertiesUnbounded: scriptCustomPropertiesUnbounded || state.customPropertiesUnbounded,
2141
+ templateStylesheetLinks: state.stylesheetLinks,
2142
+ templateStylesheetLinksUnverifiable: state.stylesheetLinksUnverifiable,
2143
+ hasOpaqueStyleBlocks: opaqueStyleBlocks,
1221
2144
  styleBlockImports: styleBlockImports.map((entry) => ({
1222
2145
  ...entry,
1223
2146
  start: offset(entry.start),
@@ -1249,32 +2172,68 @@ function verifyVueSource(compiler, path, source, includeUnscoped = false) {
1249
2172
  function visitTemplateNode(source, node, state) {
1250
2173
  if (node.type === NODE_ELEMENT) {
1251
2174
  if (node.tagType === TAG_SLOT) state.hasSlot = true;
2175
+ for (const prop of node.props ?? []) {
2176
+ if (prop.type === PROP_ATTRIBUTE && prop.name === "style" && prop.value?.content) state.styleAttributeValues.push(prop.value.content);
2177
+ if (prop.type === PROP_DIRECTIVE && prop.name === "bind") {
2178
+ const argument = prop.arg?.isStatic === true ? prop.arg.content : void 0;
2179
+ if (prop.arg === void 0 || argument === void 0 || argument === "style") state.styleAttributesUnverifiable = true;
2180
+ }
2181
+ }
2182
+ if (node.tag === "link" && node.tagType === TAG_ELEMENT) {
2183
+ let rel;
2184
+ let href;
2185
+ let dynamic = false;
2186
+ for (const prop of node.props ?? []) if (prop.type === PROP_ATTRIBUTE) {
2187
+ if (prop.name === "rel") rel = prop.value?.content ?? "";
2188
+ if (prop.name === "href") href = prop.value?.content ?? "";
2189
+ } else if (prop.type === PROP_DIRECTIVE && prop.name === "bind") {
2190
+ const argument = prop.arg?.isStatic ? prop.arg.content : void 0;
2191
+ if (argument === void 0 || argument === "rel" || argument === "href") dynamic = true;
2192
+ }
2193
+ if ((rel?.toLowerCase().split(/\s+/) ?? []).includes("stylesheet") || rel === void 0 && dynamic) {
2194
+ if (href !== void 0 && !dynamic) state.stylesheetLinks.push(href);
2195
+ else state.stylesheetLinksUnverifiable = true;
2196
+ } else if (dynamic) state.stylesheetLinksUnverifiable = true;
2197
+ }
1252
2198
  const bindingClasses = [];
1253
2199
  let classOpaque = false;
1254
2200
  let moduleBinding;
1255
2201
  for (const prop of node.props ?? []) {
1256
2202
  if (prop.type !== PROP_DIRECTIVE) continue;
2203
+ const expression = prop.exp?.content ? prop.name === "on" ? templateHandler(prop.exp.content) : templateExpression(state.expressionPath, prop.exp.content) : void 0;
2204
+ if (prop.exp?.content && !expression) state.moduleClosureBroken = true;
1257
2205
  let provenModuleExpression = false;
1258
2206
  if (prop.name === "bind") {
1259
2207
  if (!prop.arg || !prop.arg.isStatic) classOpaque = true;
1260
2208
  else if (prop.arg.content === "class") {
1261
- const member = node.tagType === TAG_ELEMENT && prop.exp ? /^\s*\$style\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(prop.exp.content) : void 0;
2209
+ const member = node.tagType === TAG_ELEMENT ? expression?.vueModuleMember : void 0;
1262
2210
  if (member && !moduleBinding) {
1263
2211
  moduleBinding = {
1264
- name: member[1],
2212
+ name: member,
1265
2213
  start: attributeRemovalStart(source, node, prop),
1266
2214
  end: prop.loc.end.offset
1267
2215
  };
1268
2216
  provenModuleExpression = true;
1269
2217
  } else {
1270
- const value = staticClassBinding(prop);
2218
+ const value = expression?.staticString ?? void 0;
1271
2219
  if (value === void 0) classOpaque = true;
1272
2220
  else bindingClasses.push(...value.split(/[\t\n\f\r ]+/).filter(Boolean));
1273
2221
  }
1274
2222
  } else if (prop.arg.content === "id") classOpaque = true;
1275
2223
  }
1276
- if (!provenModuleExpression && prop.exp?.content) state.expressionTexts.push(prop.exp.content);
1277
- if (prop.arg && !prop.arg.isStatic && prop.arg.content) state.expressionTexts.push(prop.arg.content);
2224
+ if (!provenModuleExpression && expression?.usesCssModule) state.moduleClosureBroken = true;
2225
+ if (expression?.referencesUseCssModule) state.referencesUseCssModule = true;
2226
+ for (const name of expression?.references ?? []) state.expressionReferences.add(name);
2227
+ for (const prefix of expression?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
2228
+ for (const name of expression?.customProperties ?? []) state.customProperties.add(name);
2229
+ if (expression?.customPropertiesUnbounded) state.customPropertiesUnbounded = true;
2230
+ if (prop.arg && !prop.arg.isStatic && prop.arg.content) {
2231
+ const argExpression = templateExpression(state.expressionPath, prop.arg.content);
2232
+ state.moduleClosureBroken ||= argExpression?.usesCssModule ?? true;
2233
+ state.referencesUseCssModule ||= argExpression?.referencesUseCssModule ?? false;
2234
+ for (const name of argExpression?.references ?? []) state.expressionReferences.add(name);
2235
+ for (const prefix of argExpression?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
2236
+ }
1278
2237
  if (prop.name === "html") state.vHtml = true;
1279
2238
  }
1280
2239
  if (classOpaque) state.dynamic = true;
@@ -1288,7 +2247,13 @@ function visitTemplateNode(source, node, state) {
1288
2247
  if (node.tagType === TAG_COMPONENT) state.components.push(element);
1289
2248
  else if (node.tagType === TAG_ELEMENT) state.elements.push(element);
1290
2249
  }
1291
- if (node.type === NODE_INTERPOLATION && node.content?.content) state.expressionTexts.push(node.content.content);
2250
+ if (node.type === NODE_INTERPOLATION && node.content?.content) {
2251
+ const interpolation = templateExpression(state.expressionPath, node.content.content);
2252
+ state.moduleClosureBroken ||= interpolation?.usesCssModule ?? true;
2253
+ state.referencesUseCssModule ||= interpolation?.referencesUseCssModule ?? false;
2254
+ for (const name of interpolation?.references ?? []) state.expressionReferences.add(name);
2255
+ for (const prefix of interpolation?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
2256
+ }
1292
2257
  for (const child of node.children ?? []) visitTemplateNode(source, child, state);
1293
2258
  }
1294
2259
  function attributeRemovalStart(source, node, prop) {
@@ -1297,20 +2262,39 @@ function attributeRemovalStart(source, node, prop) {
1297
2262
  while (start > node.loc.start.offset + 1 && /\s/.test(source[start - 1])) start -= 1;
1298
2263
  return /[\r\n]/.test(source.slice(start, attributeStart)) ? attributeStart : start;
1299
2264
  }
1300
- function staticClassBinding(prop) {
1301
- if (!prop.exp) return void 0;
2265
+ function templateExpression(path, source) {
2266
+ try {
2267
+ return expressionAnalysis(path, source);
2268
+ } catch {
2269
+ return;
2270
+ }
2271
+ }
2272
+ function templateHandler(source) {
1302
2273
  try {
1303
- return staticStringExpression("Component.js", prop.exp.content) ?? void 0;
2274
+ const analysis = sourceAnalysis("Component.handler.ts", `async function handler($event) { ${source} }`);
2275
+ return {
2276
+ staticString: null,
2277
+ vueModuleMember: null,
2278
+ usesCssModule: analysis.usesCssModule,
2279
+ referencesUseCssModule: analysis.hasUnboundUseCssModule,
2280
+ references: analysis.unboundReferences,
2281
+ templatePrefixes: analysis.templatePrefixes,
2282
+ customProperties: analysis.customProperties,
2283
+ customPropertiesUnbounded: analysis.customPropertiesUnbounded
2284
+ };
1304
2285
  } catch {
1305
2286
  return;
1306
2287
  }
1307
2288
  }
1308
2289
  function templateSite(source, node, bindingClasses, classOpaque, state) {
1309
- let classAttribute = literalAttribute(source, node, "class");
1310
- const idAttribute = literalAttribute(source, node, "id");
1311
- const hasClassAttr = node.props?.some((prop) => prop.type === PROP_ATTRIBUTE && prop.name === "class");
1312
- const hasIdAttr = node.props?.some((prop) => prop.type === PROP_ATTRIBUTE && prop.name === "id");
1313
- if (hasClassAttr && !classAttribute || hasIdAttr && !idAttribute) {
2290
+ const classProp = staticAttribute(node, "class");
2291
+ const idProp = staticAttribute(node, "id");
2292
+ let classAttribute = literalAttribute(source, classProp);
2293
+ const idAttribute = literalAttribute(source, idProp);
2294
+ if (classProp && !classAttribute || idProp && !idAttribute) {
2295
+ if (classProp && !classAttribute) {
2296
+ for (const token of classProp.value?.content.split(/[\t\n\f\r ]+/) ?? []) if (token !== "") state.entityClassTokens.push(token);
2297
+ }
1314
2298
  state.dynamic = true;
1315
2299
  return { idAttribute };
1316
2300
  }
@@ -1331,8 +2315,10 @@ function templateSite(source, node, bindingClasses, classOpaque, state) {
1331
2315
  matchClasses
1332
2316
  };
1333
2317
  }
1334
- function literalAttribute(source, node, name) {
1335
- const prop = node.props?.find((prop) => prop.type === PROP_ATTRIBUTE && prop.name === name);
2318
+ function staticAttribute(node, name) {
2319
+ return node.props?.find((prop) => prop.type === PROP_ATTRIBUTE && prop.name === name);
2320
+ }
2321
+ function literalAttribute(source, prop) {
1336
2322
  if (!prop?.value) return void 0;
1337
2323
  const start = prop.value.loc.start.offset;
1338
2324
  const end = prop.value.loc.end.offset;
@@ -1371,12 +2357,12 @@ function vueReferenceTarget(importer, reference, vuePaths) {
1371
2357
  join(target, "index.vue")
1372
2358
  ].find((path) => vuePaths.has(path));
1373
2359
  }
1374
- function vueLiteralTargets(file, vuePaths) {
1375
- return new Set([...file.source.matchAll(/(["'`])([^"'`\r\n]+)\1/g)].flatMap((match) => {
1376
- if (/[?*[\]{}]/.test(match[2]) && (match[2].includes("vue") || isLocalVueReference(match[2]))) return [...vuePaths];
1377
- const target = vueReferenceTarget(file.path, match[2], vuePaths);
2360
+ function vueGlobTargets(importer, patterns, vuePaths) {
2361
+ return new Set(patterns.flatMap((pattern) => {
2362
+ if (/[?*[\]{}]/.test(pattern)) return pattern.includes("vue") || isLocalVueReference(pattern) ? [...vuePaths] : [];
2363
+ const target = vueReferenceTarget(importer, pattern, vuePaths);
1378
2364
  if (target) return [target];
1379
- return match[2].includes(".vue") ? [...vuePaths] : [];
2365
+ return pattern.includes(".vue") ? [...vuePaths] : [];
1380
2366
  }));
1381
2367
  }
1382
2368
  function normalizedVueTag(value) {
@@ -1423,28 +2409,48 @@ function buildVueComponentGraph(ownedVue, sourceFiles, analyses, { styleSources,
1423
2409
  if (analysis.componentsOpen) for (const target of vuePaths) callerOpen.add(target);
1424
2410
  }
1425
2411
  for (const file of sourceFiles) {
2412
+ if (extname(file.path) === ".html") continue;
1426
2413
  let references;
2414
+ let vueReferences;
1427
2415
  let blockReferences = [];
2416
+ let hasDynamicImport = false;
2417
+ let vueGlobPatterns = [];
2418
+ let vueGlobUnverifiable = false;
1428
2419
  if (extname(file.path) === ".vue") {
1429
2420
  const fileAnalysis = analyses.get(file.path);
1430
2421
  if (fileAnalysis && !fileAnalysis.retained) {
1431
2422
  references = fileAnalysis.scriptStyleImports;
2423
+ vueReferences = fileAnalysis.scriptVueReferences;
1432
2424
  blockReferences = fileAnalysis.styleBlockImports.map((entry) => entry.reference);
1433
- } else references = [];
2425
+ hasDynamicImport = fileAnalysis.scriptHasDynamicImport || fileAnalysis.scriptImportsUnverifiable;
2426
+ vueGlobPatterns = fileAnalysis.scriptVueGlobPatterns;
2427
+ vueGlobUnverifiable = fileAnalysis.scriptVueGlobUnverifiable;
2428
+ } else {
2429
+ references = [];
2430
+ vueReferences = [];
2431
+ hasDynamicImport = true;
2432
+ }
1434
2433
  } else try {
1435
- references = staticImports(file.path, file.source);
2434
+ const analysis = sourceAnalysis(file.path, file.source);
2435
+ references = analysis.staticImports;
2436
+ vueReferences = analysis.imports.filter((record) => !record.typeOnly).map((record) => record.specifier);
2437
+ hasDynamicImport = analysis.hasDynamicImport;
2438
+ vueGlobPatterns = analysis.vueGlobPatterns;
2439
+ vueGlobUnverifiable = analysis.vueGlobUnverifiable;
1436
2440
  } catch {
1437
2441
  references = [];
2442
+ vueReferences = [];
2443
+ hasDynamicImport = true;
1438
2444
  }
1439
- if (/\bimport\s*\(/.test(file.source) || references.some((reference) => !stylesheetReference.test(reference) && isLocalVueReference(reference) && couldResolveToVue(reference) && !vueReferenceTarget(file.path, reference, vuePaths))) for (const target of vuePaths) callerOpen.add(target);
2445
+ if (hasDynamicImport || vueGlobUnverifiable || vueReferences.some((reference) => !stylesheetReference.test(reference) && isLocalVueReference(reference) && couldResolveToVue(reference) && !vueReferenceTarget(file.path, reference, vuePaths))) for (const target of vuePaths) callerOpen.add(target);
1440
2446
  const resolvesOwned = (reference) => stylesheetReferenceTargets(file.path, reference, styleSources).some((path) => pathOwners.get(path) === packageRoot);
1441
2447
  if (pathOwners.get(file.path) === packageRoot && (references.some((reference) => stylesheetReference.test(reference) && (!reference.startsWith(".") || !resolvesOwned(reference))) || blockReferences.some((reference) => !resolvesOwned(reference)))) unresolvedStyleImport = true;
1442
- const imported = new Set(references.flatMap((reference) => {
2448
+ const imported = new Set(vueReferences.flatMap((reference) => {
1443
2449
  const target = vueReferenceTarget(file.path, reference, vuePaths);
1444
2450
  return target ? [target] : [];
1445
2451
  }));
1446
2452
  for (const target of imported) if (extname(file.path) !== ".vue" || !analyses.get(file.path)?.setupImports?.has(target)) callerOpen.add(target);
1447
- for (const target of vueLiteralTargets(file, vuePaths)) if (!imported.has(target)) callerOpen.add(target);
2453
+ for (const target of vueGlobTargets(file.path, vueGlobPatterns, vuePaths)) if (!imported.has(target)) callerOpen.add(target);
1448
2454
  }
1449
2455
  return {
1450
2456
  callers,
@@ -1452,15 +2458,16 @@ function buildVueComponentGraph(ownedVue, sourceFiles, analyses, { styleSources,
1452
2458
  unresolvedStyleImport
1453
2459
  };
1454
2460
  }
1455
- function vueWarningsOnlyResult(preparedVue) {
1456
- if (preparedVue.warnings.length === 0) return {};
2461
+ function vueWarningsOnlyResult(preparedVue, extraWarnings = []) {
2462
+ const warnings = [...preparedVue.warnings, ...extraWarnings];
2463
+ if (warnings.length === 0) return {};
1457
2464
  return { plan: {
1458
2465
  files: [],
1459
2466
  deletedFiles: [],
1460
2467
  unlinkedFiles: [],
1461
2468
  candidates: [],
1462
2469
  rules: [],
1463
- warnings: preparedVue.warnings,
2470
+ warnings,
1464
2471
  convertedRules: 0,
1465
2472
  retainedRules: 0
1466
2473
  } };
@@ -1542,13 +2549,17 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1542
2549
  pathOwners,
1543
2550
  packageRoot
1544
2551
  });
1545
- const generatesSelectors = (text) => /#\{|@\{|&[\w-]/.test(text);
1546
2552
  const vueShadowCss = [];
1547
2553
  const vueShadowModuleCss = [];
1548
2554
  let vueShadowUnverifiable = vueGraph.unresolvedStyleImport;
1549
2555
  for (const [path, source] of styleSources) {
1550
2556
  if (pathOwners.get(path) !== packageRoot) continue;
1551
- if (isPreprocessorPath(path) && generatesSelectors(source)) {
2557
+ try {
2558
+ if (stylesheetAnalysis(path, source).selectorsUnverifiable) {
2559
+ vueShadowUnverifiable = true;
2560
+ continue;
2561
+ }
2562
+ } catch {
1552
2563
  vueShadowUnverifiable = true;
1553
2564
  continue;
1554
2565
  }
@@ -1558,21 +2569,23 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1558
2569
  source
1559
2570
  });
1560
2571
  }
1561
- for (const file of sourceFiles) if (extname(file.path) === ".html" && pathOwners.get(file.path) === packageRoot && /<style/i.test(file.source)) vueShadowUnverifiable = true;
2572
+ for (const file of sourceFiles) if (extname(file.path) === ".html" && pathOwners.get(file.path) === packageRoot) try {
2573
+ if (parseHtmlSource(file.path, file.source).hasStyle) vueShadowUnverifiable = true;
2574
+ } catch {
2575
+ vueShadowUnverifiable = true;
2576
+ }
1562
2577
  for (const file of ownedVue) {
1563
2578
  const analysis = analysisOf(file.path);
1564
2579
  if (analysis.retained) {
1565
- if (/<style/i.test(file.source)) vueShadowUnverifiable = true;
2580
+ if (analysis.styleRanges.length > 0) vueShadowUnverifiable = true;
1566
2581
  continue;
1567
2582
  }
1568
2583
  if (analysis.escapeUnverifiable || analysis.scriptImportsUnverifiable) vueShadowUnverifiable = true;
1569
- for (const text of analysis.shadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
1570
- else vueShadowCss.push({
2584
+ for (const text of analysis.shadowPreprocessorTexts) vueShadowCss.push({
1571
2585
  path: file.path,
1572
2586
  source: text
1573
2587
  });
1574
- for (const text of analysis.unscopedShadowPreprocessorTexts) if (generatesSelectors(text)) vueShadowUnverifiable = true;
1575
- else vueShadowCss.push({
2588
+ for (const text of analysis.unscopedShadowPreprocessorTexts) vueShadowCss.push({
1576
2589
  path: file.path,
1577
2590
  source: text,
1578
2591
  migratingUnscoped: true
@@ -1586,7 +2599,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1586
2599
  migratingUnscoped: true
1587
2600
  })), ...analysis.blocks.map((block) => ({
1588
2601
  path: file.path,
1589
- source: block.analysisSource ?? block.content,
2602
+ source: block.shadowSource ?? block.analysisSource ?? block.content,
1590
2603
  scoped: true
1591
2604
  })));
1592
2605
  vueShadowModuleCss.push(...analysis.shadowModuleCssTexts, ...analysis.moduleBlocks.map((block) => block.analysisSource ?? block.content));
@@ -1627,7 +2640,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1627
2640
  for (const path of importedStyles) {
1628
2641
  stylePaths.add(path);
1629
2642
  if (extname(path) !== ".css") continue;
1630
- for (const imported of cssImports(styleSources.get(path) ?? "")) {
2643
+ for (const imported of cssImports(path, styleSources.get(path) ?? "")) {
1631
2644
  if (imported.media) continue;
1632
2645
  for (const target of stylesheetReferenceTargets(path, imported.href, styleSources)) if (pathOwners.get(target) === packageRoot) importedStyles.add(target);
1633
2646
  }
@@ -1712,6 +2725,19 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1712
2725
  message
1713
2726
  });
1714
2727
  }
2728
+ const allStyleBlocks = [
2729
+ ...analysis.blocks,
2730
+ ...analysis.moduleBlocks,
2731
+ ...analysis.unscopedBlocks
2732
+ ];
2733
+ let vueAtRuleIdentities;
2734
+ if (analysis.hasOpaqueStyleBlocks) vueAtRuleIdentities = null;
2735
+ else try {
2736
+ const registrations = allStyleBlocks.map((block) => stylesheetAnalysis(`${file.path}.${block.analysisSource ? "css" : block.syntax}`, block.analysisSource ?? block.content));
2737
+ vueAtRuleIdentities = registrations.some((entry) => entry.globalAtRulesUnverifiable) ? null : registrations.flatMap((entry) => entry.globalAtRuleIdentities);
2738
+ } catch {
2739
+ vueAtRuleIdentities = null;
2740
+ }
1715
2741
  const vueBlocks = migrateUnscoped ? analysis.unscopedBlocks : analysis.blocks;
1716
2742
  if (vueBlocks.length === 0 && !migrateModule) continue;
1717
2743
  const vueRetention = migrateUnscoped ? void 0 : retention;
@@ -1719,19 +2745,20 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1719
2745
  if (migrateModule) stylesheets.push({
1720
2746
  cssPath: file.path,
1721
2747
  cssSource: file.source,
1722
- cssModuleId: normalizedRelativePath(packageRoot, file.path),
2748
+ cssModuleId: normalizedRelativePath(workspaceRoot, file.path),
1723
2749
  syntax: "css",
1724
2750
  isModule: true,
1725
2751
  vueBlocks: analysis.moduleBlocks,
1726
2752
  vueModule: true,
1727
2753
  vueShadowCss: shadowCss,
1728
2754
  vueShadowModuleCss,
1729
- vueShadowUnverifiable
2755
+ vueShadowUnverifiable,
2756
+ vueAtRuleIdentities
1730
2757
  });
1731
2758
  if (vueBlocks.length > 0) stylesheets.push({
1732
2759
  cssPath: file.path,
1733
2760
  cssSource: file.source,
1734
- cssModuleId: normalizedRelativePath(packageRoot, file.path),
2761
+ cssModuleId: normalizedRelativePath(workspaceRoot, file.path),
1735
2762
  syntax: "css",
1736
2763
  isModule: !vueRetention,
1737
2764
  vueBlocks,
@@ -1739,7 +2766,8 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1739
2766
  vueUnscoped: migrateUnscoped,
1740
2767
  vueShadowCss: vueRetention ? void 0 : shadowCss,
1741
2768
  vueShadowModuleCss: vueRetention ? void 0 : vueShadowModuleCss,
1742
- vueShadowUnverifiable: vueRetention ? void 0 : vueShadowUnverifiable
2769
+ vueShadowUnverifiable: vueRetention ? void 0 : vueShadowUnverifiable,
2770
+ vueAtRuleIdentities
1743
2771
  });
1744
2772
  }
1745
2773
  for (const file of ownedVue) {
@@ -1747,19 +2775,31 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
1747
2775
  if (analysis.retained) continue;
1748
2776
  const elements = elementsByFile.get(file.path) ?? [];
1749
2777
  const contextPaths = [...new Set(elements.flatMap((element) => element.cssPaths))];
1750
- if (contextPaths.length === 0) continue;
1751
- await rejectSymlinkTarget(file.path, packageRoot);
2778
+ if (contextPaths.length > 0) await rejectSymlinkTarget(file.path, packageRoot);
1752
2779
  files.set(file.path, {
1753
2780
  ...file,
1754
- htmlElements: elements,
1755
- htmlStylesheets: contextPaths.map((cssPath) => ({
1756
- cssPath,
1757
- variants: [],
1758
- direct: cssPath === file.path,
1759
- analyzable: true
1760
- })),
1761
- htmlReferencesSafe: !analysis.dynamic,
1762
- htmlScriptText: analysis.scriptText
2781
+ ...contextPaths.length > 0 ? {
2782
+ htmlElements: elements,
2783
+ htmlStylesheets: contextPaths.map((cssPath) => ({
2784
+ cssPath,
2785
+ variants: [],
2786
+ direct: cssPath === file.path,
2787
+ analyzable: true
2788
+ })),
2789
+ htmlReferencesSafe: !analysis.dynamic,
2790
+ htmlScriptText: analysis.scriptText
2791
+ } : {},
2792
+ sourceImports: analysis.scriptImports,
2793
+ sourceImportsUnverifiable: analysis.scriptImportsUnverifiable,
2794
+ templatePrefixes: analysis.templatePrefixes,
2795
+ templateInjectsMarkup: analysis.vHtml,
2796
+ templateClassTokens: analysis.templateClassTokens,
2797
+ templateStyleValues: analysis.templateStyleValues,
2798
+ templateStylesUnverifiable: analysis.templateStylesUnverifiable,
2799
+ sourceCustomProperties: analysis.scriptCustomProperties,
2800
+ sourceCustomPropertiesUnbounded: analysis.scriptCustomPropertiesUnbounded,
2801
+ templateStylesheetLinks: analysis.templateStylesheetLinks,
2802
+ templateStylesheetLinksUnverifiable: analysis.templateStylesheetLinksUnverifiable
1763
2803
  });
1764
2804
  }
1765
2805
  return {
@@ -1865,7 +2905,13 @@ async function migrate(options = {}) {
1865
2905
  Promise.all(stylePaths.map(async (path) => [path, await snapshotFile(snapshots, path)])).then((entries) => new Map(entries)),
1866
2906
  Promise.all(sourcePaths.map(async (path) => {
1867
2907
  const source = await readFile(path, "utf8");
1868
- const mayReferenceModule = extname(path) !== ".html" || /<link\b/i.test(source);
2908
+ const mayReferenceModule = extname(path) !== ".html" || (() => {
2909
+ try {
2910
+ return parseHtmlSource(path, source).links.length > 0;
2911
+ } catch {
2912
+ return /<link\b/i.test(source);
2913
+ }
2914
+ })();
1869
2915
  if (!scope.targetable.has(path) && !mayReferenceModule) return void 0;
1870
2916
  return {
1871
2917
  path,
@@ -1885,15 +2931,36 @@ async function migrate(options = {}) {
1885
2931
  styleSources,
1886
2932
  sourceFiles,
1887
2933
  styleDependents: indexStylesheetDependents(styleSources),
1888
- vueStyleRanges
2934
+ vueStyleRanges,
2935
+ entryCatalog: tailwindEntryCatalog(styleSources, scope.pathOwners),
2936
+ ignoredPaths: await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)),
2937
+ fontAllocations: /* @__PURE__ */ new Map()
1889
2938
  };
1890
2939
  const failures = [];
1891
2940
  const plans = [];
2941
+ const prepared = [];
1892
2942
  for (const packageRoot of selectedPackages) {
1893
- const result = await planPackage(context, packageRoot);
1894
- if (result.failure) failures.push(result.failure);
1895
- else if (result.plan) plans.push(result.plan);
1896
- }
2943
+ const preparation = await preparePackage(context, packageRoot);
2944
+ if (preparation.failure) failures.push(preparation.failure);
2945
+ else if (preparation.plan) plans.push(preparation.plan);
2946
+ if (preparation.prepared) prepared.push(preparation.prepared);
2947
+ }
2948
+ const groups = /* @__PURE__ */ new Map();
2949
+ for (const preparation of prepared) {
2950
+ const members = groups.get(preparation.tailwind.path) ?? [];
2951
+ members.push(preparation);
2952
+ groups.set(preparation.tailwind.path, members);
2953
+ }
2954
+ for (const [, members] of groups) for (const [token, value] of Object.entries(members[0].tailwind.themeTokens)) {
2955
+ if (!token.startsWith("font-")) continue;
2956
+ const parsed = fontFamilyStack(value);
2957
+ const name = token.replace(/^font-/, "");
2958
+ const stack = parsed && parsed.value;
2959
+ const existing = context.fontAllocations.get(name);
2960
+ context.fontAllocations.set(name, existing === void 0 || existing === stack ? stack : null);
2961
+ }
2962
+ 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);
2963
+ else if (result.plan) plans.push(result.plan);
1897
2964
  const originals = new Map([...styleSources, ...sourceFiles.map((file) => [file.path, file.source])]);
1898
2965
  const { filesByPath, deletedPaths, candidates, rules, warnings, convertedRules, retainedRules } = mergePlans(plans, originals);
1899
2966
  const changed = [...filesByPath.values()].map((file) => ({
@@ -1980,7 +3047,7 @@ async function migrate(options = {}) {
1980
3047
  failures
1981
3048
  };
1982
3049
  }
1983
- async function planPackage(context, packageRoot) {
3050
+ async function preparePackage(context, packageRoot) {
1984
3051
  const { options, snapshots, workspaceRoot, explicitStyle, configuredEntry, styleSources, sourceFiles, styleDependents, pathOwners, targetable, selectedPackages, scannedPaths, vueStyleRanges } = context;
1985
3052
  const recover = (error, fatal = false) => {
1986
3053
  if (!options.force || fatal) throw error;
@@ -2030,19 +3097,84 @@ async function planPackage(context, packageRoot) {
2030
3097
  if (ownedStyles.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
2031
3098
  let tailwindPath;
2032
3099
  let tailwindEntries;
2033
- try {
2034
- ({path: tailwindPath, entries: tailwindEntries} = resolveTailwindEntry(ownedStyles, styleSources, configuredEntry));
2035
- } catch (error) {
2036
- return recover(error);
3100
+ let entryOwner = packageRoot;
3101
+ let sharedProofs;
3102
+ const ownEntries = findTailwindEntries(ownedStyles, styleSources);
3103
+ if (ownEntries.length === 0 && configuredEntry === void 0 && options.workspaces && options.extractMediaQueries !== false) {
3104
+ let packageJson = {};
3105
+ try {
3106
+ const rawPackageJson = snapshots.get(join(packageRoot, "package.json"));
3107
+ if (rawPackageJson !== void 0) {
3108
+ const parsed = JSON.parse(rawPackageJson);
3109
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) packageJson = { ...parsed };
3110
+ }
3111
+ } catch (error) {
3112
+ return recover(error);
3113
+ }
3114
+ const ancestors = selectedPackages.filter((root) => root !== packageRoot && isWithin(root, packageRoot)).sort((left, right) => right.length - left.length);
3115
+ for (const ancestor of ancestors) {
3116
+ const proven = [];
3117
+ for (const candidate of context.entryCatalog.get(ancestor) ?? []) {
3118
+ const proofs = proveSharedEntry({
3119
+ packageRoot,
3120
+ entry: candidate,
3121
+ entrySource: styleSources.get(candidate) ?? "",
3122
+ styleSources,
3123
+ packageSources,
3124
+ owned: (path) => pathOwners.get(path) === packageRoot,
3125
+ writable: (path) => targetable.has(path) && pathOwners.get(path) === packageRoot,
3126
+ packageJson,
3127
+ ignoredPaths: context.ignoredPaths
3128
+ });
3129
+ if (proofs) proven.push({
3130
+ entry: candidate,
3131
+ proofs
3132
+ });
3133
+ }
3134
+ if (proven.length > 1) return recover(/* @__PURE__ */ new Error("Multiple proven ancestor Tailwind entries were found. Pass --tailwind-css."));
3135
+ if (proven.length === 1) {
3136
+ [{entry: tailwindPath, proofs: sharedProofs}] = proven;
3137
+ entryOwner = ancestor;
3138
+ break;
3139
+ }
3140
+ }
3141
+ }
3142
+ if (sharedProofs && tailwindPath !== void 0) tailwindEntries = [];
3143
+ else {
3144
+ tailwindEntries = ownEntries;
3145
+ try {
3146
+ tailwindPath = selectTailwindEntry(ownEntries, configuredEntry);
3147
+ } catch (error) {
3148
+ return recover(error);
3149
+ }
2037
3150
  }
2038
3151
  const excludedEntries = /* @__PURE__ */ new Set([...tailwindEntries, tailwindPath]);
2039
3152
  const explicitCss = explicitStyle && extname(explicitStyle) !== ".vue" ? explicitStyle : void 0;
2040
- const targets = explicitCss ? [explicitCss] : explicitStyle ? [...preparedVue.stylePaths].filter((path) => !isStylesheetModule(path) && !excludedEntries.has(path)) : ownedStyles.filter((path) => !excludedEntries.has(path) && !preparedHtml.generatedPaths.has(path) && (!isPreprocessorPath(path) || preparedHtml.stylePaths.has(path) || packageSources.some((file) => sourceReferencesStyle(file, path))));
3153
+ const targets = explicitCss ? [explicitCss] : explicitStyle ? [...preparedVue.stylePaths].filter((path) => !isStylesheetModule(path) && !excludedEntries.has(path)) : ownedStyles.filter((path) => !excludedEntries.has(path) && !preparedHtml.generatedPaths.has(path) && (!isPreprocessorPath(path) || preparedHtml.stylePaths.has(path) || preparedVue.stylePaths.has(path) || packageSources.some((file) => importsStylesheet(file, path))));
2041
3154
  if (targets.length === 0 && preparedVue.stylesheets.length === 0) return vueWarningsOnlyResult(preparedVue);
2042
3155
  if (targets.some((path) => excludedEntries.has(path))) throw new Error("The Tailwind CSS entry cannot be migrated.");
3156
+ const proofWarnings = [];
3157
+ const unprovenFlow = (path) => ({
3158
+ code: "unproven-shared-entry-flow",
3159
+ file: path,
3160
+ start: 0,
3161
+ end: 0,
3162
+ message: "A consumer flow could not be proven to load the shared Tailwind entry, so the stylesheet is retained."
3163
+ });
3164
+ const provenTargets = sharedProofs ? targets.filter((path) => {
3165
+ const proven = sharedProofs.provenStyle(packageSources.filter((file) => importsStylesheet(file, path) || (file.htmlStylesheets ?? []).some((linked) => linked.cssPath === path)));
3166
+ if (!proven) proofWarnings.push(unprovenFlow(path));
3167
+ return proven;
3168
+ }) : targets;
3169
+ const provenVueStylesheets = sharedProofs ? preparedVue.stylesheets.filter((stylesheet) => {
3170
+ const proven = sharedProofs.provenStyle(packageSources.filter((file) => file.path === stylesheet.cssPath));
3171
+ if (!proven && !proofWarnings.some((warning) => warning.file === stylesheet.cssPath)) proofWarnings.push(unprovenFlow(stylesheet.cssPath));
3172
+ return proven;
3173
+ }) : preparedVue.stylesheets;
3174
+ if (provenTargets.length === 0 && provenVueStylesheets.length === 0) return vueWarningsOnlyResult(preparedVue, proofWarnings);
2043
3175
  let tailwind;
2044
3176
  try {
2045
- tailwind = await loadTailwind(packageRoot, tailwindPath, snapshots, workspaceRoot);
3177
+ tailwind = await loadTailwind(entryOwner, tailwindPath, snapshots, workspaceRoot);
2046
3178
  } catch (error) {
2047
3179
  return recover(error, isIntegrityError(error));
2048
3180
  }
@@ -2057,13 +3189,13 @@ async function planPackage(context, packageRoot) {
2057
3189
  try {
2058
3190
  stylesheets = [];
2059
3191
  const compilerDependents = /* @__PURE__ */ new Map();
2060
- for (const stylePath of targets.sort()) {
3192
+ for (const stylePath of provenTargets.sort()) {
2061
3193
  await rejectSymlinkTarget(stylePath, packageRoot);
2062
3194
  const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith("_");
2063
3195
  const stylesheet = {
2064
3196
  cssPath: stylePath,
2065
3197
  cssSource: styleSources.get(stylePath) ?? "",
2066
- cssModuleId: normalizedRelativePath(packageRoot, stylePath),
3198
+ cssModuleId: normalizedRelativePath(workspaceRoot, stylePath),
2067
3199
  cssDependents: styleDependents.get(stylePath) ?? [],
2068
3200
  syntax: stylesheetSyntax(stylePath),
2069
3201
  isModule: isStylesheetModule(stylePath),
@@ -2097,125 +3229,637 @@ async function planPackage(context, packageRoot) {
2097
3229
  } catch (error) {
2098
3230
  return recover(error, isIntegrityError(error));
2099
3231
  }
2100
- stylesheets.push(...preparedVue.stylesheets);
2101
- const request = {
3232
+ stylesheets.push(...provenVueStylesheets);
3233
+ return { prepared: {
3234
+ packageRoot,
3235
+ preparedHtml,
3236
+ preparedVue,
3237
+ styleCompilers,
3238
+ files,
2102
3239
  stylesheets,
2103
- tailwindPath: tailwind.path,
2104
- tailwindSource: tailwind.css,
2105
- utilityPrefix: tailwind.designSystem.theme.prefix,
2106
- themeTokens: tailwind.themeTokens,
2107
- files
3240
+ tailwind,
3241
+ sharedProofs,
3242
+ warnings: proofWarnings
3243
+ } };
3244
+ }
3245
+ async function planPreparedGroup(context, members) {
3246
+ const { options, workspaceRoot, targetable } = context;
3247
+ const results = [];
3248
+ let active = [...members].sort((left, right) => {
3249
+ const leftPath = normalizedRelativePath(workspaceRoot, left.packageRoot);
3250
+ const rightPath = normalizedRelativePath(workspaceRoot, right.packageRoot);
3251
+ return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
3252
+ });
3253
+ const entry = active[0].tailwind;
3254
+ const reservations = spellingReservations(context.styleSources, entry.graphSources.map((graphSource) => [`${graphSource.path}.graph.css`, graphSource.source]), new Set(entry.graphSources.map((graphSource) => graphSource.path).filter((path) => path.includes("\0"))));
3255
+ const groupFiles = new Map(active.flatMap((member) => member.files.map((file) => [file.path, file])));
3256
+ const mentionedSegments = /* @__PURE__ */ new Set();
3257
+ const mentionedSources = [...[...groupFiles.values()].map((file) => file.source), ...entry.graphSources.map((graphSource) => graphSource.source)];
3258
+ const addSegment = (token) => {
3259
+ if (token === "") return;
3260
+ const segment = utilitySegment(token).replace(/!$/, "");
3261
+ if (segment) mentionedSegments.add(segment);
2108
3262
  };
2109
- let plan;
2110
- try {
2111
- plan = JSON.parse(planBatchMigration(JSON.stringify(request)));
2112
- } catch (error) {
2113
- return recover(error, !isRecoverablePlanningError(error));
2114
- }
2115
- try {
2116
- plan = replanCompileFailures(tailwind, request, plan);
2117
- } catch (error) {
2118
- return recover(error);
3263
+ for (const source of mentionedSources) {
3264
+ for (const token of source.split(/[\s"'`<>=,;{}()\\]+/)) addSegment(token);
3265
+ for (const inline of source.matchAll(/inline\(([^)]*)\)/g)) for (const candidate of inline[1].matchAll(/([^\s"',{}]*)\{([^{}]*)\}([^\s"',{}]*)/g)) for (const option of candidate[2].split(",")) {
3266
+ const range = /^(\d+)\.\.(\d+)(?:\.\.(\d+))?$/.exec(option);
3267
+ const stride = range === null ? 0 : Number(range[3] ?? "1");
3268
+ if (range !== null && stride > 0) {
3269
+ const from = Number(range[1]);
3270
+ const to = Number(range[2]);
3271
+ let steps = 0;
3272
+ for (let value = Math.min(from, to); value <= Math.max(from, to); value += stride) {
3273
+ addSegment(`${candidate[1]}${value}${candidate[3]}`);
3274
+ if ((steps += 1) >= 1e3) break;
3275
+ }
3276
+ continue;
3277
+ }
3278
+ addSegment(`${candidate[1]}${option}${candidate[3]}`);
3279
+ }
2119
3280
  }
2120
- removeMigratedHtmlLinks(plan, preparedHtml);
2121
- plan.warnings.push(...preparedHtml.warnings);
2122
- plan.warnings.push(...preparedVue.warnings);
2123
- try {
2124
- for (const file of plan.files.filter((file) => extname(file.path) === ".html")) parseHtmlSource(file.path, file.source);
2125
- for (const file of plan.files.filter((file) => extname(file.path) === ".vue")) {
2126
- const vueCompiler = preparedVue.compiler;
2127
- if (!vueCompiler) throw new Error(`No Vue compiler is available to verify ${file.path}`);
2128
- const includeUnscoped = preparedVue.unscopedPaths.has(file.path);
2129
- const blocks = verifyVueSource(vueCompiler, file.path, file.source, includeUnscoped);
2130
- const untouched = /* @__PURE__ */ new Map();
2131
- for (const block of verifyVueSource(vueCompiler, file.path, snapshots.get(file.path) ?? "", includeUnscoped)) {
2132
- const key = `${block.syntax}\0${block.content}`;
2133
- untouched.set(key, (untouched.get(key) ?? 0) + 1);
3281
+ const fileRoots = new Map(active.flatMap((member) => member.files.map((file) => [file.path, member.packageRoot])));
3282
+ const workspaceEntries = new Set([...context.entryCatalog.values()].flat());
3283
+ for (const file of groupFiles.values()) {
3284
+ if (reservations.unbounded) break;
3285
+ if (context.ignoredPaths.has(file.path)) {
3286
+ reservations.unbounded = true;
3287
+ break;
3288
+ }
3289
+ const opaqueStylesheetLink = (link) => {
3290
+ const resolved = localHtmlReference(fileRoots.get(file.path) ?? context.workspaceRoot, dirname(file.path), link);
3291
+ return resolved === void 0 || !context.styleSources.has(resolved) || workspaceEntries.has(resolved) && resolved !== entry.path;
3292
+ };
3293
+ const opaqueStylesheetImports = (records) => {
3294
+ for (const record of records) {
3295
+ if (record.typeOnly) continue;
3296
+ const specifier = record.specifier.split(/[?#]/, 1)[0];
3297
+ if (!isStylesheetPath(specifier)) {
3298
+ if (extname(specifier) !== "" || specifier.startsWith(".")) continue;
3299
+ try {
3300
+ const resolved = createRequire(file.path).resolve(specifier);
3301
+ if (isStylesheetPath(resolved) && !context.styleSources.has(resolved)) reservations.unbounded = true;
3302
+ } catch {}
3303
+ continue;
3304
+ }
3305
+ const resolved = specifier.startsWith(".") ? resolve(dirname(file.path), specifier) : void 0;
3306
+ if (resolved === void 0 || !context.styleSources.has(resolved) || workspaceEntries.has(resolved) && resolved !== entry.path) reservations.unbounded = true;
3307
+ }
3308
+ };
3309
+ if (extname(file.path) === ".html") {
3310
+ try {
3311
+ const parsed = parseHtmlSource(file.path, file.source);
3312
+ if (parsed.hasStyle) reservations.unbounded = true;
3313
+ if (parsed.bases.length > 0) reservations.unbounded = true;
3314
+ for (const link of parsed.links) if (opaqueStylesheetLink(link.href)) reservations.unbounded = true;
3315
+ for (const [index, value] of parsed.styleAttributeValues.entries()) {
3316
+ if (TEMPLATE_MARKERS.test(value)) {
3317
+ reservations.propertiesUnbounded = true;
3318
+ continue;
3319
+ }
3320
+ scanInlineStyleReservations(`${file.path}.style.${index}.css`, value, reservations);
3321
+ }
3322
+ for (const value of parsed.dynamicClassValues) {
3323
+ const masked = value.replace(TEMPLATE_EXPRESSIONS, "\0");
3324
+ if (TEMPLATE_MARKERS.test(masked)) {
3325
+ reservations.unbounded = true;
3326
+ continue;
3327
+ }
3328
+ for (const token of masked.split(/\s+/)) {
3329
+ if (token === "") continue;
3330
+ const index = token.indexOf("\0");
3331
+ if (index < 0) reservations.names.add(token);
3332
+ else if (index === 0) reservations.unbounded = true;
3333
+ else reservations.prefixes.add(token.slice(0, index));
3334
+ }
3335
+ }
3336
+ if (parsed.handlerText.trim() !== "") try {
3337
+ const handlers = sourceAnalysis(`${file.path}.handlers.js`, parsed.handlerText);
3338
+ for (const prefix of handlers.templatePrefixes) reservations.prefixes.add(prefix);
3339
+ for (const property of handlers.customProperties) reservations.properties.add(property);
3340
+ if (handlers.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
3341
+ } catch {
3342
+ reservations.unbounded = true;
3343
+ }
3344
+ if (parsed.scriptText.trim() !== "") try {
3345
+ const script = sourceAnalysis(`${file.path}.inline.js`, parsed.scriptText);
3346
+ for (const prefix of script.templatePrefixes) reservations.prefixes.add(prefix);
3347
+ for (const property of script.customProperties) reservations.properties.add(property);
3348
+ if (script.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
3349
+ opaqueStylesheetImports(script.imports);
3350
+ } catch {
3351
+ reservations.unbounded = true;
3352
+ }
3353
+ } catch {
3354
+ reservations.unbounded = true;
3355
+ }
3356
+ continue;
3357
+ }
3358
+ if (extname(file.path) === ".vue") {
3359
+ if (file.templatePrefixes === void 0 || file.sourceImportsUnverifiable === true || file.templateInjectsMarkup === true) {
3360
+ reservations.unbounded = true;
3361
+ continue;
3362
+ }
3363
+ const styleRanges = context.vueStyleRanges.get(file.path);
3364
+ if (styleRanges === void 0) {
3365
+ reservations.unbounded = true;
3366
+ continue;
2134
3367
  }
2135
- for (const [index, block] of blocks.entries()) {
2136
- const key = `${block.syntax}\0${block.content}`;
2137
- const remaining = untouched.get(key) ?? 0;
2138
- if (remaining > 0) {
2139
- untouched.set(key, remaining - 1);
3368
+ const fileBytes = Buffer.from(file.source);
3369
+ for (const [index, range] of styleRanges.entries()) {
3370
+ if (range.src !== void 0) {
3371
+ const resolved = range.src.startsWith(".") ? resolve(dirname(file.path), range.src) : void 0;
3372
+ if (resolved === void 0 || !context.styleSources.has(resolved)) reservations.unbounded = true;
2140
3373
  continue;
2141
3374
  }
2142
- if (block.syntax === "css") validateCss(block.content);
2143
- else validateCss((await compileStyleEntry(styleCompilers, packageRoot, `${file.path}.${index}.${block.syntax}`, block.content, { virtualEntry: true })).css);
3375
+ const block = fileBytes.subarray(range.start, range.end).toString();
3376
+ if (![
3377
+ "css",
3378
+ "scss",
3379
+ "sass",
3380
+ "less"
3381
+ ].includes(range.lang)) {
3382
+ reservations.unbounded = true;
3383
+ continue;
3384
+ }
3385
+ scanStylesheetReservations(`${file.path}.block.${index}.${range.lang}`, block, context.styleSources, reservations);
2144
3386
  }
3387
+ for (const prefix of file.templatePrefixes) reservations.prefixes.add(prefix);
3388
+ for (const token of file.templateClassTokens ?? []) reservations.names.add(token);
3389
+ for (const property of file.sourceCustomProperties ?? []) reservations.properties.add(property);
3390
+ if (file.sourceCustomPropertiesUnbounded === true) reservations.propertiesUnbounded = true;
3391
+ if (file.templateStylesUnverifiable === true) reservations.propertiesUnbounded = true;
3392
+ for (const [index, value] of (file.templateStyleValues ?? []).entries()) scanInlineStyleReservations(`${file.path}.style.${index}.css`, value, reservations);
3393
+ if (file.templateStylesheetLinksUnverifiable === true) reservations.unbounded = true;
3394
+ for (const link of file.templateStylesheetLinks ?? []) if (opaqueStylesheetLink(link)) reservations.unbounded = true;
3395
+ opaqueStylesheetImports(file.sourceImports ?? []);
3396
+ continue;
2145
3397
  }
2146
- for (const stylesheet of stylesheets.filter((stylesheet) => isPreprocessorPath(stylesheet.cssPath))) {
2147
- const changed = plan.files.find((file) => file.path === stylesheet.cssPath);
2148
- if (!changed && !plan.deletedFiles.includes(stylesheet.cssPath)) continue;
2149
- const source = changed?.source ?? "";
2150
- validateCss((await compileStyleEntry(styleCompilers, packageRoot, stylesheet.cssPath, source)).css);
3398
+ if (![
3399
+ ".js",
3400
+ ".jsx",
3401
+ ".ts",
3402
+ ".tsx",
3403
+ ".mjs",
3404
+ ".cjs",
3405
+ ".mts",
3406
+ ".cts"
3407
+ ].includes(extname(file.path))) continue;
3408
+ try {
3409
+ const analysis = sourceAnalysis(file.path, file.source);
3410
+ for (const prefix of analysis.templatePrefixes) reservations.prefixes.add(prefix);
3411
+ for (const property of analysis.customProperties) reservations.properties.add(property);
3412
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
3413
+ opaqueStylesheetImports(analysis.imports);
3414
+ if (analysis.rendersStyleElement) reservations.unbounded = true;
3415
+ if (analysis.injectsMarkup) reservations.unbounded = true;
3416
+ if (analysis.stylesheetLinksUnverifiable) reservations.unbounded = true;
3417
+ for (const link of analysis.stylesheetLinks) if (opaqueStylesheetLink(link)) reservations.unbounded = true;
3418
+ } catch {
3419
+ reservations.unbounded = true;
2151
3420
  }
2152
- } catch (error) {
2153
- return recover(error, !isMissingStyleCompilerError(error));
2154
3421
  }
2155
- return { plan };
2156
- }
2157
- function replanCompileFailures(tailwind, request, initialPlan) {
2158
- let plan = initialPlan;
2159
- const blockedByStylesheet = /* @__PURE__ */ new Map();
2160
- while (true) {
2161
- const failing = invalidCandidates(tailwind, plan.candidates);
2162
- if (failing.length === 0) break;
2163
- let progressed = false;
2164
- for (const rule of plan.rules) {
2165
- const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
2166
- if (failed.length === 0) continue;
2167
- let blocked = blockedByStylesheet.get(rule.stylesheet);
2168
- if (!blocked) blockedByStylesheet.set(rule.stylesheet, blocked = /* @__PURE__ */ new Map());
2169
- const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
2170
- let entry = blocked.get(key);
2171
- if (!entry) {
2172
- blocked.set(key, entry = {
2173
- ruleId: rule.ruleId,
2174
- authoredSpan: rule.authoredSpan,
2175
- candidates: /* @__PURE__ */ new Set()
3422
+ const recoverGroup = (error, fatal = false) => {
3423
+ if (!options.force || fatal) throw error;
3424
+ for (const member of active) results.push({ failure: packageFailure(workspaceRoot, member.packageRoot, error) });
3425
+ return results;
3426
+ };
3427
+ const membersByRoot = new Map(active.map((member) => [member.packageRoot, member]));
3428
+ for (const member of options.extractMediaQueries !== false ? active : []) member.stylesheets = member.stylesheets.filter((stylesheet) => {
3429
+ 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) => {
3430
+ const consumerRoot = context.pathOwners.get(consumer.path);
3431
+ const consumerMember = consumerRoot === void 0 ? void 0 : membersByRoot.get(consumerRoot);
3432
+ if (consumerMember === void 0) return false;
3433
+ if (consumerMember.sharedProofs === void 0) return true;
3434
+ return consumerMember.sharedProofs.provenConsumer(consumer);
3435
+ });
3436
+ if (!proven) member.warnings.push({
3437
+ code: "unproven-shared-entry-flow",
3438
+ file: stylesheet.cssPath,
3439
+ start: 0,
3440
+ end: 0,
3441
+ message: "A consumer flow could not be proven to load the shared Tailwind entry, so the stylesheet is retained."
3442
+ });
3443
+ return proven;
3444
+ });
3445
+ for (const member of active.filter((remaining) => remaining.stylesheets.length === 0)) if (member.warnings.length > 0) results.push({ plan: {
3446
+ files: [],
3447
+ deletedFiles: [],
3448
+ unlinkedFiles: [],
3449
+ candidates: [],
3450
+ rules: [],
3451
+ warnings: [
3452
+ ...member.warnings,
3453
+ ...member.preparedHtml.warnings,
3454
+ ...member.preparedVue.warnings
3455
+ ],
3456
+ convertedRules: 0,
3457
+ retainedRules: 0
3458
+ } });
3459
+ active = active.filter((remaining) => remaining.stylesheets.length > 0);
3460
+ if (active.length === 0) return results;
3461
+ const groupWritable = !await entryUnsafe(entry.path, workspaceRoot, targetable);
3462
+ const mediaWarnings = [];
3463
+ let extraction;
3464
+ const collections = [];
3465
+ for (const member of active) try {
3466
+ collections.push(collectMemberMediaConditions(member.stylesheets, entry));
3467
+ } catch (error) {
3468
+ if (!options.force || !isRecoverablePlanningError(error)) throw error;
3469
+ results.push({ failure: packageFailure(workspaceRoot, member.packageRoot, error) });
3470
+ active = active.filter((remaining) => remaining !== member);
3471
+ }
3472
+ if (active.length === 0) return results;
3473
+ let names = extraction?.names;
3474
+ let generated = extraction?.generated ?? [];
3475
+ if (options.extractMediaQueries !== false) {
3476
+ if (!groupWritable) {
3477
+ names = {};
3478
+ generated = [];
3479
+ if (collections.some((collection) => collection.components.length > 0)) mediaWarnings.push({
3480
+ code: "media-query-definition-fallback",
3481
+ file: entry.path,
3482
+ start: 0,
3483
+ end: 0,
3484
+ message: "The Tailwind entry could not be edited safely, so media behavior was preserved with arbitrary variants."
3485
+ });
3486
+ } else {
3487
+ try {
3488
+ extraction = planMediaExtraction({
3489
+ collections,
3490
+ tailwind: entry,
3491
+ scannedSources: context.sourceFiles.map((file) => file.source)
2176
3492
  });
2177
- progressed = true;
3493
+ } catch (error) {
3494
+ return recoverGroup(error, !isRecoverablePlanningError(error));
2178
3495
  }
2179
- for (const candidate of failed) entry.candidates.add(candidate);
2180
- }
2181
- if (!progressed) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
2182
- plan = JSON.parse(planBatchMigration(JSON.stringify({
2183
- ...request,
2184
- stylesheets: request.stylesheets.map((stylesheet, index) => ({
2185
- ...stylesheet,
2186
- blockedRules: [...blockedByStylesheet.get(index)?.values() ?? []].map((entry) => entry.ruleId)
2187
- }))
2188
- })));
3496
+ names = extraction.names;
3497
+ generated = extraction.generated;
3498
+ }
2189
3499
  }
2190
- for (const [index, blocked] of blockedByStylesheet) {
2191
- const cssPath = request.stylesheets[index].cssPath;
2192
- for (const { authoredSpan, candidates } of blocked.values()) {
2193
- const failed = [...candidates].sort().map((candidate) => `\`${candidate}\``).join(", ");
3500
+ let plan = {
3501
+ files: [],
3502
+ deletedFiles: [],
3503
+ unlinkedFiles: [],
3504
+ candidates: [],
3505
+ rules: [],
3506
+ warnings: [],
3507
+ convertedRules: 0,
3508
+ retainedRules: 0
3509
+ };
3510
+ let augmented = entry.css;
3511
+ const blocked = /* @__PURE__ */ new Map();
3512
+ let candidateAliases;
3513
+ let canonicalized = false;
3514
+ let fontTokens = {};
3515
+ let fontFailures = /* @__PURE__ */ new Set();
3516
+ let composeEntry = () => entry.css;
3517
+ finalization: for (;;) {
3518
+ const baseIdentities = new Set(atRuleIdentities(`${entry.path}.group-base.css`, [entry.css, ...entry.graphSources.map((graphSource) => graphSource.source)].join("\n")));
3519
+ const sheets = active.flatMap((member) => member.stylesheets.map((stylesheet) => ({
3520
+ member,
3521
+ stylesheet
3522
+ })));
3523
+ 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);
3524
+ for (const sheet of sheets) {
3525
+ if (movable(sheet)) continue;
3526
+ for (const identity of stylesheetAtRuleIdentities(sheet.stylesheet)) baseIdentities.add(identity);
3527
+ }
3528
+ const sheetIdentities = sheets.map((sheet) => movable(sheet) ? new Set(stylesheetAtRuleIdentities(sheet.stylesheet)) : /* @__PURE__ */ new Set());
3529
+ const opaqueVueRegistrations = sheets.some(({ stylesheet }) => stylesheet.vueAtRuleIdentities === null);
3530
+ const movesDisabled = new Set(opaqueVueRegistrations ? sheets.flatMap((sheet, index) => movable(sheet) ? [index] : []) : []);
3531
+ for (const [index, identities] of sheetIdentities.entries()) for (const identity of identities) {
3532
+ if (baseIdentities.has(identity)) movesDisabled.add(index);
3533
+ for (const [other, otherIdentities] of sheetIdentities.entries()) if (other !== index && otherIdentities.has(identity)) {
3534
+ movesDisabled.add(index);
3535
+ movesDisabled.add(other);
3536
+ }
3537
+ }
3538
+ const filesByPath = /* @__PURE__ */ new Map();
3539
+ const contextsByPath = /* @__PURE__ */ new Map();
3540
+ for (const member of active) for (const file of member.files) {
3541
+ if (file.htmlStylesheets !== void 0 && file.htmlStylesheets.length > 0) {
3542
+ const contexts = contextsByPath.get(file.path) ?? [];
3543
+ contexts.push(...file.htmlStylesheets);
3544
+ contextsByPath.set(file.path, contexts);
3545
+ }
3546
+ if (!filesByPath.has(file.path) || context.pathOwners.get(file.path) === member.packageRoot) filesByPath.set(file.path, file);
3547
+ }
3548
+ const files = [...filesByPath.values()].map((file) => {
3549
+ const contexts = contextsByPath.get(file.path);
3550
+ if (contexts === void 0) return file;
3551
+ const seen = /* @__PURE__ */ new Set();
3552
+ const merged = contexts.filter((linked) => {
3553
+ const key = `${linked.cssPath}\0${linked.variants.join(",")}\0${linked.direct}\0${linked.analyzable}`;
3554
+ if (seen.has(key)) return false;
3555
+ seen.add(key);
3556
+ return true;
3557
+ });
3558
+ return {
3559
+ ...file,
3560
+ htmlStylesheets: merged
3561
+ };
3562
+ }).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
3563
+ planning: for (;;) {
3564
+ const request = {
3565
+ stylesheets: sheets.map(({ member, stylesheet }, index) => {
3566
+ const rules = blocked.get(index);
3567
+ const movesOff = member.sharedProofs !== void 0 || movesDisabled.has(index);
3568
+ return {
3569
+ ...stylesheet,
3570
+ ...rules ? { blockedRules: [...rules.values()].map((blockedRule) => blockedRule.ruleId) } : {},
3571
+ ...movesOff ? { globalAtRuleMoves: false } : {}
3572
+ };
3573
+ }),
3574
+ tailwindPath: entry.path,
3575
+ tailwindSource: entry.css,
3576
+ utilityPrefix: entry.designSystem.theme.prefix,
3577
+ themeTokens: entry.themeTokens,
3578
+ ...names ? { mediaNames: names } : {},
3579
+ ...candidateAliases ? { candidateAliases } : {},
3580
+ ...groupWritable ? {} : { entryWritable: false },
3581
+ files
3582
+ };
3583
+ try {
3584
+ plan = JSON.parse(planBatchMigration(JSON.stringify(request)));
3585
+ } catch (error) {
3586
+ return recoverGroup(error, !isRecoverablePlanningError(error));
3587
+ }
3588
+ let composed = entry.css;
3589
+ const plannedEntry = plan.files.findIndex((file) => file.path === entry.path);
3590
+ if (plannedEntry >= 0) {
3591
+ if (!groupWritable) throw new Error(`The planner claimed an unwritable Tailwind entry: ${entry.path}`);
3592
+ composed = plan.files[plannedEntry].source;
3593
+ plan.files.splice(plannedEntry, 1);
3594
+ if (!composed.startsWith(entry.css)) throw new Error(`The planner rewrote the Tailwind entry instead of appending: ${entry.path}`);
3595
+ }
3596
+ const currentExtraction = names !== void 0 ? {
3597
+ names,
3598
+ generated
3599
+ } : void 0;
3600
+ const used = currentExtraction ? usedGeneratedDefinitions(currentExtraction, plan) : [];
3601
+ composeEntry = (tokens) => {
3602
+ const withFonts = appendFontTheme(composed, tokens);
3603
+ return currentExtraction ? appendMediaDefinitions(withFonts, used) : withFonts;
3604
+ };
3605
+ augmented = composeEntry(fontTokens);
3606
+ let system;
3607
+ try {
3608
+ system = augmented === entry.css ? entry.designSystem : await entry.loadWith(augmented);
3609
+ } catch (error) {
3610
+ if (isIntegrityError(error)) throw error;
3611
+ if (names !== void 0 && generated.length > 0) {
3612
+ const generatedKeys = new Set(generated.map((definition) => definition.key));
3613
+ names = Object.fromEntries(Object.entries(names).filter(([key]) => !generatedKeys.has(key)));
3614
+ generated = [];
3615
+ continue planning;
3616
+ }
3617
+ return recoverGroup(error);
3618
+ }
3619
+ if (currentExtraction && names !== void 0) {
3620
+ const prefix = entry.designSystem.theme.prefix;
3621
+ const rejected = new Set(used.filter(({ name }) => system.candidatesToCss([probeCandidate(prefix, name)])[0] === null).map(({ key }) => key));
3622
+ if (rejected.size > 0) {
3623
+ names = Object.fromEntries(Object.entries(names).filter(([key]) => !rejected.has(key)));
3624
+ generated = generated.filter(({ key }) => !rejected.has(key));
3625
+ continue planning;
3626
+ }
3627
+ }
3628
+ const failing = invalidCandidates(system, plan.candidates);
3629
+ if (failing.length > 0) {
3630
+ if (!accumulateBlockedRules(blocked, plan, failing)) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
3631
+ continue planning;
3632
+ }
3633
+ if (!canonicalized) {
3634
+ canonicalized = true;
3635
+ const aliases = acceptedCandidateAliases(system, plan.candidateProbes ?? [], reservations);
3636
+ const fonts = await registerFontTokens({
3637
+ system,
3638
+ entryCss: augmented,
3639
+ loadWith: entry.loadWith,
3640
+ themeTokens: entry.themeTokens,
3641
+ probes: plan.fontFamilyProbes ?? [],
3642
+ corpus: [.../* @__PURE__ */ new Set([...plan.candidates, ...plan.candidateProbes ?? []])],
3643
+ reservations,
3644
+ existingAliases: aliases,
3645
+ generate: groupWritable,
3646
+ mentionedSegments,
3647
+ referenceTokens: entry.referenceTokens,
3648
+ globalAllocations: context.fontAllocations
3649
+ });
3650
+ Object.assign(aliases, fonts.aliases);
3651
+ fontTokens = fonts.tokens;
3652
+ fontFailures = fonts.failures;
3653
+ for (const [name, value] of Object.entries(fonts.tokens)) context.fontAllocations.set(name, value);
3654
+ if (Object.keys(aliases).length > 0) {
3655
+ candidateAliases = aliases;
3656
+ continue planning;
3657
+ }
3658
+ }
3659
+ break;
3660
+ }
3661
+ const ruleStatus = new Map(plan.rules.map((rule) => [`${rule.stylesheet}:${rule.ruleId.start}-${rule.ruleId.end}`, rule.status]));
3662
+ const warned = /* @__PURE__ */ new Set();
3663
+ for (const probe of plan.fontFamilyProbes ?? []) {
3664
+ if (probe.firstFamily.kind !== "name") continue;
3665
+ if (candidateAliases?.[probe.candidate] !== void 0) continue;
3666
+ const ruleKey = `${probe.stylesheet}:${probe.ruleId.start}-${probe.ruleId.end}`;
3667
+ if (ruleStatus.get(ruleKey) !== "retained" || warned.has(ruleKey)) continue;
3668
+ const failed = fontFailures.has(probe.candidate);
3669
+ if (groupWritable && !failed) continue;
3670
+ warned.add(ruleKey);
2194
3671
  plan.warnings.push({
2195
- code: "candidate-compilation-failure",
2196
- file: cssPath,
2197
- start: authoredSpan.start,
2198
- end: authoredSpan.end,
2199
- message: `Tailwind did not generate CSS for ${failed}, so the rule is retained.`
3672
+ code: groupWritable ? "font-theme-registration-failed" : "font-theme-registration-required",
3673
+ file: sheets[probe.stylesheet].stylesheet.cssPath,
3674
+ start: probe.authoredSpan.start,
3675
+ end: probe.authoredSpan.end,
3676
+ message: groupWritable ? "A safe Tailwind font theme token could not be registered, so the rule is retained." : "The font family requires a Tailwind theme token, but the Tailwind entry cannot be edited, so the rule is retained."
3677
+ });
3678
+ }
3679
+ if (Object.keys(fontTokens).length > 0) {
3680
+ const applied = new Set(plan.candidates.map((candidate) => utilitySegment(candidate).replace(/!$/, "")));
3681
+ const pruned = Object.fromEntries(Object.entries(fontTokens).filter(([name]) => applied.has(`font-${name}`)));
3682
+ if (Object.keys(pruned).length !== Object.keys(fontTokens).length) {
3683
+ fontTokens = pruned;
3684
+ augmented = composeEntry(pruned);
3685
+ }
3686
+ }
3687
+ for (const [index, rules] of blocked) {
3688
+ const cssPath = sheets[index].stylesheet.cssPath;
3689
+ for (const { authoredSpan, candidates } of rules.values()) {
3690
+ const failed = [...candidates].sort().map((candidate) => `\`${candidate}\``).join(", ");
3691
+ plan.warnings.push({
3692
+ code: "candidate-compilation-failure",
3693
+ file: cssPath,
3694
+ start: authoredSpan.start,
3695
+ end: authoredSpan.end,
3696
+ message: `Tailwind did not generate CSS for ${failed}, so the rule is retained.`
3697
+ });
3698
+ }
3699
+ }
3700
+ plan.warnings.push(...mediaWarnings);
3701
+ for (const member of active) plan.warnings.push(...member.warnings);
3702
+ removeGroupHtmlLinks(plan, active);
3703
+ const failure = await finishGroupPlan(context, active, plan);
3704
+ if (failure === void 0) break;
3705
+ if (!options.force || failure.fatal) throw failure.error;
3706
+ const failed = failure.member ?? active[0];
3707
+ results.push({ failure: packageFailure(workspaceRoot, failed.packageRoot, failure.error) });
3708
+ active = active.filter((member) => member !== failed);
3709
+ if (active.length === 0) return results;
3710
+ blocked.clear();
3711
+ plan.warnings = [];
3712
+ continue finalization;
3713
+ }
3714
+ const seenWarnings = /* @__PURE__ */ new Set();
3715
+ plan.warnings = plan.warnings.filter((warning) => {
3716
+ const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}\0${warning.message}`;
3717
+ if (seenWarnings.has(key)) return false;
3718
+ seenWarnings.add(key);
3719
+ return true;
3720
+ });
3721
+ results.push({ plan });
3722
+ if (augmented !== entry.css) results.push({ plan: {
3723
+ files: [{
3724
+ path: entry.path,
3725
+ source: augmented
3726
+ }],
3727
+ deletedFiles: [],
3728
+ unlinkedFiles: [],
3729
+ candidates: [],
3730
+ rules: [],
3731
+ warnings: [],
3732
+ convertedRules: 0,
3733
+ retainedRules: 0
3734
+ } });
3735
+ return results;
3736
+ }
3737
+ function accumulateBlockedRules(blocked, plan, failing) {
3738
+ let progressed = false;
3739
+ for (const rule of plan.rules) {
3740
+ const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
3741
+ if (failed.length === 0) continue;
3742
+ let rules = blocked.get(rule.stylesheet);
3743
+ if (!rules) blocked.set(rule.stylesheet, rules = /* @__PURE__ */ new Map());
3744
+ const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
3745
+ let blockedRule = rules.get(key);
3746
+ if (!blockedRule) {
3747
+ rules.set(key, blockedRule = {
3748
+ ruleId: rule.ruleId,
3749
+ authoredSpan: rule.authoredSpan,
3750
+ candidates: /* @__PURE__ */ new Set()
2200
3751
  });
3752
+ progressed = true;
3753
+ }
3754
+ for (const candidate of failed) blockedRule.candidates.add(candidate);
3755
+ }
3756
+ return progressed;
3757
+ }
3758
+ async function entryUnsafe(entryPath, workspaceRoot, targetable) {
3759
+ if (!targetable.has(entryPath) || !isProjectInput(workspaceRoot, entryPath)) return true;
3760
+ return (await lstat(entryPath)).isSymbolicLink();
3761
+ }
3762
+ function atRuleIdentities(key, css) {
3763
+ return css.trim() === "" ? [] : stylesheetAnalysis(key, css).globalAtRuleIdentities;
3764
+ }
3765
+ function stylesheetAtRuleIdentities(stylesheet) {
3766
+ if (stylesheet.vueAtRuleIdentities) return stylesheet.vueAtRuleIdentities;
3767
+ if (stylesheet.vueBlocks) return stylesheet.vueBlocks.flatMap((block, index) => atRuleIdentities(`${stylesheet.cssPath}.${index}.at-rules.${block.analysisSource ? "css" : block.syntax}`, block.analysisSource ?? block.content));
3768
+ return stylesheet.analysisSource === void 0 ? atRuleIdentities(`${stylesheet.cssPath}.at-rules.${stylesheet.syntax ?? "css"}`, stylesheet.cssSource) : atRuleIdentities(`${stylesheet.cssPath}.at-rules.css`, stylesheet.analysisSource);
3769
+ }
3770
+ async function finishGroupPlan(context, members, plan) {
3771
+ const { snapshots } = context;
3772
+ const membersByRoot = new Map(members.map((member) => [member.packageRoot, member]));
3773
+ const ownerOf = (path) => {
3774
+ const root = context.pathOwners.get(path);
3775
+ return root === void 0 ? void 0 : membersByRoot.get(root);
3776
+ };
3777
+ for (const member of members) {
3778
+ plan.warnings.push(...member.preparedHtml.warnings);
3779
+ plan.warnings.push(...member.preparedVue.warnings);
3780
+ }
3781
+ try {
3782
+ for (const file of plan.files.filter((file) => extname(file.path) === ".html")) try {
3783
+ parseHtmlSource(file.path, file.source);
3784
+ } catch (error) {
3785
+ return {
3786
+ error,
3787
+ member: ownerOf(file.path),
3788
+ fatal: true
3789
+ };
3790
+ }
3791
+ for (const file of plan.files.filter((file) => extname(file.path) === ".vue")) {
3792
+ const owner = ownerOf(file.path);
3793
+ const preparedVue = owner?.preparedVue ?? members[0].preparedVue;
3794
+ const vueCompiler = preparedVue.compiler ?? members[0].preparedVue.compiler;
3795
+ if (!vueCompiler) throw new Error(`No Vue compiler is available to verify ${file.path}`);
3796
+ try {
3797
+ const includeUnscoped = preparedVue.unscopedPaths.has(file.path);
3798
+ const blocks = verifyVueSource(vueCompiler, file.path, file.source, includeUnscoped);
3799
+ const untouched = /* @__PURE__ */ new Map();
3800
+ for (const block of verifyVueSource(vueCompiler, file.path, snapshots.get(file.path) ?? "", includeUnscoped)) {
3801
+ const key = `${block.syntax}\0${block.content}`;
3802
+ untouched.set(key, (untouched.get(key) ?? 0) + 1);
3803
+ }
3804
+ for (const [index, block] of blocks.entries()) {
3805
+ const key = `${block.syntax}\0${block.content}`;
3806
+ const remaining = untouched.get(key) ?? 0;
3807
+ if (remaining > 0) {
3808
+ untouched.set(key, remaining - 1);
3809
+ continue;
3810
+ }
3811
+ if (block.syntax === "css") validateCss(block.content);
3812
+ else {
3813
+ const compilerOwner = owner ?? members[0];
3814
+ validateCss((await compileStyleEntry(compilerOwner.styleCompilers, compilerOwner.packageRoot, `${file.path}.${index}.${block.syntax}`, block.content, { virtualEntry: true })).css);
3815
+ }
3816
+ }
3817
+ } catch (error) {
3818
+ return {
3819
+ error,
3820
+ member: owner,
3821
+ fatal: !isMissingStyleCompilerError(error)
3822
+ };
3823
+ }
3824
+ }
3825
+ for (const member of members) for (const stylesheet of member.stylesheets.filter((stylesheet) => isPreprocessorPath(stylesheet.cssPath))) {
3826
+ const changed = plan.files.find((file) => file.path === stylesheet.cssPath);
3827
+ if (!changed && !plan.deletedFiles.includes(stylesheet.cssPath)) continue;
3828
+ const source = changed?.source ?? "";
3829
+ try {
3830
+ validateCss((await compileStyleEntry(member.styleCompilers, member.packageRoot, stylesheet.cssPath, source)).css);
3831
+ } catch (error) {
3832
+ return {
3833
+ error,
3834
+ member,
3835
+ fatal: !isMissingStyleCompilerError(error)
3836
+ };
3837
+ }
2201
3838
  }
3839
+ } catch (error) {
3840
+ return {
3841
+ error,
3842
+ fatal: !isMissingStyleCompilerError(error)
3843
+ };
2202
3844
  }
2203
- return plan;
2204
3845
  }
2205
- function removeMigratedHtmlLinks(plan, preparedHtml) {
3846
+ function removeGroupHtmlLinks(plan, members) {
2206
3847
  const unlinked = new Set(plan.unlinkedFiles);
2207
3848
  const linksByFile = /* @__PURE__ */ new Map();
2208
- for (const link of preparedHtml.removableLinks) {
2209
- if (!unlinked.has(link.cssPath)) continue;
2210
- const links = linksByFile.get(link.filePath) ?? /* @__PURE__ */ new Set();
2211
- links.add(`${link.href}\0${link.media}`);
2212
- linksByFile.set(link.filePath, links);
3849
+ const originals = /* @__PURE__ */ new Map();
3850
+ for (const member of members) {
3851
+ for (const link of member.preparedHtml.removableLinks) {
3852
+ if (!unlinked.has(link.cssPath)) continue;
3853
+ const links = linksByFile.get(link.filePath) ?? /* @__PURE__ */ new Set();
3854
+ links.add(`${link.href}\0${link.media}`);
3855
+ linksByFile.set(link.filePath, links);
3856
+ }
3857
+ for (const file of member.preparedHtml.files) if (!originals.has(file.path)) originals.set(file.path, file.source);
2213
3858
  }
2214
3859
  for (const [filePath, removable] of linksByFile) {
2215
3860
  const planned = plan.files.find((file) => file.path === filePath);
2216
- const original = preparedHtml.files.find((file) => file.path === filePath);
2217
- if (!original) continue;
2218
- const source = planned?.source ?? original.source;
3861
+ const source = planned?.source ?? originals.get(filePath);
3862
+ if (source === void 0) continue;
2219
3863
  const links = parseHtmlSource(filePath, source).links.filter((link) => removable.has(`${link.href}\0${link.media}`)).sort((left, right) => right.tagStart - left.tagStart);
2220
3864
  let bytes = Buffer.from(source);
2221
3865
  for (const link of links) bytes = Buffer.concat([bytes.subarray(0, link.tagStart), bytes.subarray(link.tagEnd)]);