tw-migrate 0.5.0 → 0.7.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.
Files changed (4) hide show
  1. package/README.md +13 -129
  2. package/dist/bin.js +9 -3
  3. package/dist/index.js +816 -171
  4. package/package.json +12 -8
package/dist/index.js CHANGED
@@ -2,20 +2,18 @@ import { createRequire } from "node:module";
2
2
  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
+ import * as z from "zod/mini";
6
+ import { FILE_HEADERS_ONLY, createTwoFilesPatch } from "diff";
5
7
  import { execFile } from "node:child_process";
6
8
  import { fileURLToPath, pathToFileURL } from "node:url";
7
9
  import { parse } from "parse5";
8
10
  //#region src/util/diff.ts
9
11
  function unifiedDiff(path, before, after) {
10
- const oldLines = before.split("\n");
11
- const newLines = after.split("\n");
12
- return [
13
- `--- a/${path}\n`,
14
- `+++ b/${path}\n`,
15
- `@@ -1,${oldLines.length} +1,${newLines.length} @@\n`,
16
- ...oldLines.map((line, index) => `${index === oldLines.length - 1 && line === "" ? " " : "-"}${line}\n`),
17
- ...newLines.map((line, index) => `${index === newLines.length - 1 && line === "" ? " " : "+"}${line}\n`)
18
- ].join("");
12
+ if (before === after) return "";
13
+ return createTwoFilesPatch(`a/${path}`, `b/${path}`, before, after, void 0, void 0, {
14
+ context: 3,
15
+ headerOptions: FILE_HEADERS_ONLY
16
+ });
19
17
  }
20
18
  //#endregion
21
19
  //#region src/native.ts
@@ -64,13 +62,17 @@ function sourceImport(value) {
64
62
  return object(value) && typeof value.specifier === "string" && typeof value.typeOnly === "boolean" && typeof value.dynamic === "boolean";
65
63
  }
66
64
  function sourceAnalysisResult(value) {
67
- return object(value) && Array.isArray(value.imports) && value.imports.every(sourceImport) && Array.isArray(value.staticImports) && value.staticImports.every((item) => typeof item === "string") && Array.isArray(value.defaultImports) && value.defaultImports.every((item) => object(item) && typeof item.source === "string" && typeof item.local === "string") && Array.isArray(value.vueGlobPatterns) && value.vueGlobPatterns.every((item) => typeof item === "string") && Array.isArray(value.useCssModuleLocals) && value.useCssModuleLocals.every((item) => typeof item === "string") && Array.isArray(value.unboundReferences) && value.unboundReferences.every((item) => typeof item === "string") && [
65
+ 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
66
  value.vueGlobUnverifiable,
69
67
  value.hasDynamicImport,
70
68
  value.hasVueFallthroughMacro,
71
69
  value.usesCssModule,
72
70
  value.hasUnboundUseCssModule,
73
- value.definesRootUseCssModule
71
+ value.definesRootUseCssModule,
72
+ value.stylesheetLinksUnverifiable,
73
+ value.rendersStyleElement,
74
+ value.injectsMarkup,
75
+ value.customPropertiesUnbounded
74
76
  ].every((item) => typeof item === "boolean");
75
77
  }
76
78
  const ANALYSIS_CACHE_LIMIT = 4096;
@@ -84,7 +86,7 @@ function bound(cache) {
84
86
  }
85
87
  }
86
88
  function expressionAnalysis(path, source) {
87
- return decode("expression analysis", binding.expressionAnalysis(path, source), (value) => object(value) && (typeof value.staticString === "string" || value.staticString === null) && (typeof value.vueModuleMember === "string" || value.vueModuleMember === null) && typeof value.usesCssModule === "boolean" && typeof value.referencesUseCssModule === "boolean" && Array.isArray(value.references) && value.references.every((item) => typeof item === "string"));
89
+ 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"));
88
90
  }
89
91
  function sourceAnalysis(path, source) {
90
92
  const cached = sourceAnalysisCache.get(path);
@@ -97,6 +99,12 @@ function sourceAnalysis(path, source) {
97
99
  bound(sourceAnalysisCache);
98
100
  return analysis;
99
101
  }
102
+ function fontFamilyStack(value) {
103
+ 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"));
104
+ }
105
+ function compiledShape(css) {
106
+ 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"));
107
+ }
100
108
  function stylesheetAnalysis(path, source) {
101
109
  const cached = stylesheetAnalysisCache.get(path);
102
110
  if (cached?.source === source) return cached.analysis;
@@ -104,7 +112,7 @@ function stylesheetAnalysis(path, source) {
104
112
  value.unverifiable,
105
113
  value.scopeEscapesUnverifiable,
106
114
  value.selectorsUnverifiable
107
- ].every((item) => typeof item === "boolean") && Array.isArray(value.scopeEscapes) && value.scopeEscapes.every((item) => typeof item === "string") && Array.isArray(value.scopeShadowCss) && value.scopeShadowCss.every((item) => typeof item === "string") && object(value.themeTokens) && Object.values(value.themeTokens).every((item) => typeof item === "string") && Array.isArray(value.globalAtRuleIdentities) && value.globalAtRuleIdentities.every((item) => typeof item === "string") && typeof value.globalAtRulesUnverifiable === "boolean");
115
+ ].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");
108
116
  stylesheetAnalysisCache.set(path, {
109
117
  source,
110
118
  analysis
@@ -221,6 +229,10 @@ function sourcePathFromMap(source, sourceBase) {
221
229
  }
222
230
  //#endregion
223
231
  //#region src/util/shared.ts
232
+ const projectPackageSchema = z.looseObject({
233
+ name: z.optional(z.string()),
234
+ private: z.optional(z.boolean())
235
+ });
224
236
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
225
237
  ".html",
226
238
  ".vue",
@@ -348,6 +360,11 @@ function stylesheetReferenceTargets(importer, reference, styleSources) {
348
360
  join(target, `index${syntax}`)
349
361
  ])).filter((path) => styleSources.has(path));
350
362
  }
363
+ function localHrefTarget(href) {
364
+ const target = href.split(/[?#]/, 1)[0];
365
+ if (!target || /^[a-z][a-z0-9+.-]*:|^\/\//i.test(target)) return void 0;
366
+ return target;
367
+ }
351
368
  function cssImports(path, source) {
352
369
  return stylesheetAnalysis(path, source).imports;
353
370
  }
@@ -510,6 +527,9 @@ async function collectFiles(root, include) {
510
527
  //#endregion
511
528
  //#region src/parser/html.ts
512
529
  const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
530
+ /** Balanced template expressions, for masking a templated attribute value
531
+ * before extracting its static class-prefix tokens. */
532
+ const TEMPLATE_EXPRESSIONS = /\{\{[\s\S]*?\}\}|\$\{[\s\S]*?\}|<%[\s\S]*?%>|<#[\s\S]*?#>|\[%[\s\S]*?%\]/g;
513
533
  function parseHtmlSource(path, source) {
514
534
  const errors = [];
515
535
  const document = parse(source, {
@@ -524,6 +544,9 @@ function parseHtmlSource(path, source) {
524
544
  const bases = [];
525
545
  const elements = [];
526
546
  const dynamicAttributes = [];
547
+ const dynamicClassValues = [];
548
+ const styleAttributeValues = [];
549
+ const handlerTexts = [];
527
550
  const scriptTexts = [];
528
551
  let hasStyle = false;
529
552
  const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
@@ -534,6 +557,9 @@ function parseHtmlSource(path, source) {
534
557
  }
535
558
  if (node.tagName) {
536
559
  const attributes = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
560
+ const style = attributes.get("style");
561
+ if (style !== void 0 && style !== "") styleAttributeValues.push(style);
562
+ for (const [name, value] of attributes) if (name.startsWith("on") && value !== "") handlerTexts.push(value);
537
563
  const locations = node.sourceCodeLocation?.attrs;
538
564
  if (locations) {
539
565
  if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
@@ -560,15 +586,20 @@ function parseHtmlSource(path, source) {
560
586
  }
561
587
  let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
562
588
  const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
563
- const dynamic = (!classAttribute && idAttribute && locations.class ? {
589
+ const unparsedClass = !classAttribute && idAttribute && locations.class ? {
564
590
  start: locations.class.startOffset,
565
591
  end: locations.class.endOffset
566
- } : void 0) ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
567
- if (dynamic) dynamicAttributes.push({
568
- start: dynamic.start,
569
- end: dynamic.end
570
- });
571
- else if (classAttribute || idAttribute) {
592
+ } : void 0;
593
+ const dynamic = unparsedClass ?? [classAttribute, idAttribute].find((attribute) => attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value))) ?? (classAttribute && unquoted(classAttribute) ? classAttribute : void 0);
594
+ if (dynamic) {
595
+ dynamicAttributes.push({
596
+ start: dynamic.start,
597
+ end: dynamic.end
598
+ });
599
+ const decoded = attributes.get("class");
600
+ const entityBearing = classAttribute !== void 0 && dynamic === classAttribute && classAttribute.writable === false;
601
+ if (dynamic !== idAttribute && decoded !== void 0 && (TEMPLATE_MARKERS.test(decoded) || entityBearing || dynamic === unparsedClass)) dynamicClassValues.push(decoded);
602
+ } else if (classAttribute || idAttribute) {
572
603
  if (!classAttribute && idAttribute) {
573
604
  const startTag = node.sourceCodeLocation?.startTag;
574
605
  if (startTag && source[startTag.endOffset - 1] === ">") {
@@ -599,6 +630,9 @@ function parseHtmlSource(path, source) {
599
630
  elements,
600
631
  dynamicAttributes
601
632
  }),
633
+ dynamicClassValues,
634
+ styleAttributeValues,
635
+ handlerText: handlerTexts.join(";\n"),
602
636
  scriptText: scriptTexts.join("\n"),
603
637
  hasStyle
604
638
  };
@@ -691,18 +725,129 @@ function locatedAttribute(source, location, parsedValue) {
691
725
  };
692
726
  }
693
727
  //#endregion
694
- //#region src/plan/entry.ts
695
- function emitsUtilities(specifier) {
696
- return specifier === "tailwindcss" || specifier === "tailwindcss/utilities" || specifier === "tailwindcss/utilities.css";
728
+ //#region src/tailwind.ts
729
+ function findTailwindEntries(stylePaths, styleSources) {
730
+ return stylePaths.filter((path) => extname(path) === ".css" && importEntries(styleSources.get(path) ?? "").some(({ specifier }) => specifier === "tailwindcss" || specifier === "tailwindcss/index" || specifier === "tailwindcss/index.css" || specifier === "tailwindcss/utilities" || specifier === "tailwindcss/utilities.css"));
731
+ }
732
+ const externalStylesheetUrl = z.url({ protocol: /^(https?|data)$/ });
733
+ const tailwindPackageSchema = z.object({ version: z.string() });
734
+ function isExternalStylesheet(specifier) {
735
+ return specifier.startsWith("//") || /^(?:https?:\/\/|data:)/i.test(specifier) && externalStylesheetUrl.safeParse(specifier).success;
736
+ }
737
+ function selectTailwindEntry(entries, configuredPath) {
738
+ if (configuredPath) return configuredPath;
739
+ if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
740
+ if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
741
+ return entries[0];
742
+ }
743
+ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
744
+ const projectRequire = createRequire(join(packageRoot, "package.json"));
745
+ let packagePath;
746
+ try {
747
+ packagePath = projectRequire.resolve("tailwindcss/package.json");
748
+ } catch {
749
+ throw new Error("Tailwind v4 must be installed in the target project.");
750
+ }
751
+ const packageJson = tailwindPackageSchema.parse(JSON.parse(await readFile(packagePath, "utf8")));
752
+ if (!packageJson.version.startsWith("4.")) throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
753
+ const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
754
+ const css = await snapshotFile(snapshots, tailwindCss);
755
+ const base = dirname(tailwindCss);
756
+ const tailwindRoot = dirname(packagePath);
757
+ const loadModule = async (id, moduleBase) => {
758
+ const path = createRequire(join(moduleBase, "package.json")).resolve(id);
759
+ if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
760
+ const imported = await import(pathToFileURL(path).href);
761
+ return {
762
+ path,
763
+ base: dirname(path),
764
+ module: imported.default ?? imported
765
+ };
766
+ };
767
+ const loadStylesheet = async (id, sheetBase) => {
768
+ if (isExternalStylesheet(id)) return {
769
+ content: "",
770
+ base: sheetBase
771
+ };
772
+ let path;
773
+ if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
774
+ else if (id.startsWith("tailwindcss/")) {
775
+ const subpath = id.slice(12);
776
+ path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
777
+ } else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
778
+ else path = projectRequire.resolve(id);
779
+ return {
780
+ content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
781
+ base: dirname(path)
782
+ };
783
+ };
784
+ const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
785
+ const graphSources = [];
786
+ const referenceTokens = /* @__PURE__ */ new Set();
787
+ const themeTokens = {
788
+ ...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
789
+ ...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources, referenceTokens)
790
+ };
791
+ graphSources.push({
792
+ path: tailwindCss,
793
+ source: css
794
+ });
795
+ const designSystem = await loadDesignSystem(css, {
796
+ base,
797
+ loadModule,
798
+ loadStylesheet
799
+ });
800
+ const loadWith = (replacement) => loadDesignSystem(replacement, {
801
+ base,
802
+ loadModule,
803
+ loadStylesheet
804
+ });
805
+ return {
806
+ designSystem,
807
+ css,
808
+ path: tailwindCss,
809
+ themeTokens,
810
+ referenceTokens,
811
+ graphSources,
812
+ loadWith
813
+ };
814
+ }
815
+ function extractThemeTokens(key, css) {
816
+ return stylesheetAnalysis(key, css).themeTokens;
817
+ }
818
+ function importEntries(css) {
819
+ return (cssDirectives$1(css) ?? []).flatMap((directive) => directive !== null && typeof directive === "object" && "kind" in directive && directive.kind === "import" && "specifier" in directive && typeof directive.specifier === "string" ? [{
820
+ specifier: directive.specifier,
821
+ reference: "reference" in directive && directive.reference === true
822
+ }] : []);
697
823
  }
698
- function hasTailwindImport(source) {
699
- return cssDirectives(source)?.some((directive) => directive.kind === "import" && directive.specifier !== null && emitsUtilities(directive.specifier)) ?? false;
824
+ async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, referenceTokens, inReference = false, seen = /* @__PURE__ */ new Set()) {
825
+ const tokens = {};
826
+ for (const { specifier: spec, reference } of importEntries(css)) {
827
+ if (isExternalStylesheet(spec)) continue;
828
+ const key = `${base}\0${spec}`;
829
+ if (seen.has(key)) continue;
830
+ seen.add(key);
831
+ const loaded = await loadStylesheet(spec, base);
832
+ if (spec !== "tailwindcss" && !spec.startsWith("tailwindcss/")) graphSources.push({
833
+ path: key,
834
+ source: loaded.content
835
+ });
836
+ Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, referenceTokens, inReference || reference, seen));
837
+ }
838
+ const own = extractThemeTokens(`${identity}.theme.css`, css);
839
+ if (inReference) for (const token of Object.keys(own)) referenceTokens.add(token);
840
+ return Object.assign(tokens, own);
700
841
  }
842
+ function invalidCandidates(system, candidates) {
843
+ const generated = system.candidatesToCss(candidates);
844
+ return candidates.filter((_, index) => generated[index] === null);
845
+ }
846
+ //#endregion
847
+ //#region src/plan/entry.ts
701
848
  function tailwindEntryCatalog(styleSources, pathOwners) {
702
849
  const catalog = /* @__PURE__ */ new Map();
703
- for (const [path, source] of styleSources) {
704
- if (extname(path) !== ".css") continue;
705
- if (!hasTailwindImport(source)) continue;
850
+ for (const path of findTailwindEntries([...styleSources.keys()], styleSources)) {
706
851
  const owner = pathOwners.get(path);
707
852
  if (owner === void 0) continue;
708
853
  const entries = catalog.get(owner) ?? [];
@@ -735,7 +880,7 @@ function entryGraphSheets(entry, styleSources) {
735
880
  if (directive.kind !== "import") continue;
736
881
  if (directive.specifier === null) return null;
737
882
  const spec = directive.specifier;
738
- if (directive.tailwind) continue;
883
+ if (directive.tailwind || isExternalStylesheet(spec)) continue;
739
884
  if (!spec.startsWith(".") && !spec.startsWith("/")) {
740
885
  let resolved;
741
886
  try {
@@ -778,8 +923,10 @@ function scanProof(options) {
778
923
  } else if (directive.kind === "import" && directive.tailwind) {
779
924
  if (directive.sourceUnreadable) return null;
780
925
  if (directive.source === "none") automaticDisabled = true;
781
- else if (directive.source !== null) if (coversChild(directive.source)) literal = true;
782
- else automaticDisabled = true;
926
+ else if (directive.source !== null) {
927
+ if (coversChild(directive.source)) literal = true;
928
+ else automaticDisabled = true;
929
+ }
783
930
  }
784
931
  }
785
932
  if (literal) return "literal";
@@ -965,8 +1112,8 @@ function htmlLinksEntry(file, entry) {
965
1112
  return parsed.links.some((link) => {
966
1113
  const media = link.media.trim().toLowerCase();
967
1114
  if (media !== "" && media !== "all") return false;
968
- const href = link.href.split(/[?#]/, 1)[0];
969
- if (!href || /^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return false;
1115
+ const href = localHrefTarget(link.href);
1116
+ if (href === void 0) return false;
970
1117
  return resolve(dirname(file.path), href) === entry;
971
1118
  });
972
1119
  } catch {
@@ -1142,8 +1289,8 @@ function addInferredPreprocessorContext(state) {
1142
1289
  return true;
1143
1290
  }
1144
1291
  function localHtmlReference(packageRoot, base, reference) {
1145
- const path = reference.split(/[?#]/, 1)[0];
1146
- if (!path || path.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(path)) return void 0;
1292
+ const path = localHrefTarget(reference);
1293
+ if (path === void 0) return void 0;
1147
1294
  let decoded;
1148
1295
  try {
1149
1296
  decoded = decodeURIComponent(path);
@@ -1177,112 +1324,297 @@ function htmlWarning(code, file, start, end, message) {
1177
1324
  };
1178
1325
  }
1179
1326
  //#endregion
1180
- //#region src/tailwind.ts
1181
- function findTailwindEntries(stylePaths, styleSources) {
1182
- return stylePaths.filter((path) => extname(path) === ".css" && importSpecifiers(styleSources.get(path) ?? "").some((specifier) => specifier === "tailwindcss" || specifier.startsWith("tailwindcss/")));
1327
+ //#region src/plan/canonicalize.ts
1328
+ const caches = /* @__PURE__ */ new WeakMap();
1329
+ function canonicalCandidate(system, candidate) {
1330
+ if (typeof system.canonicalizeCandidates !== "function") return null;
1331
+ let cache = caches.get(system);
1332
+ if (cache === void 0) {
1333
+ cache = /* @__PURE__ */ new Map();
1334
+ caches.set(system, cache);
1335
+ }
1336
+ const cached = cache.get(candidate);
1337
+ if (cached !== void 0) return cached;
1338
+ let canonical;
1339
+ try {
1340
+ const results = system.canonicalizeCandidates([candidate]);
1341
+ canonical = results.length === 1 && typeof results[0] === "string" && results[0] !== candidate ? results[0] : null;
1342
+ } catch {
1343
+ canonical = null;
1344
+ }
1345
+ cache.set(candidate, canonical);
1346
+ return canonical;
1347
+ }
1348
+ function spellingReservations(styleSources, resolvedExtras, resolvedImports = /* @__PURE__ */ new Set()) {
1349
+ const reservations = {
1350
+ names: /* @__PURE__ */ new Set(),
1351
+ prefixes: /* @__PURE__ */ new Set(),
1352
+ unbounded: false,
1353
+ properties: /* @__PURE__ */ new Set(),
1354
+ propertiesUnbounded: false
1355
+ };
1356
+ for (const [path, source] of styleSources) scanStylesheetReservations(path, source, styleSources, reservations, resolvedImports);
1357
+ for (const [path, source] of resolvedExtras) try {
1358
+ const analysis = stylesheetAnalysis(path, source);
1359
+ for (const name of analysis.classNames) reservations.names.add(name);
1360
+ if (analysis.classReservationsUnbounded) reservations.unbounded = true;
1361
+ for (const property of analysis.customProperties) reservations.properties.add(property);
1362
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
1363
+ if (entryDirectiveHazard(source)) reservations.unbounded = true;
1364
+ } catch {
1365
+ reservations.unbounded = true;
1366
+ }
1367
+ return reservations;
1183
1368
  }
1184
- function selectTailwindEntry(entries, configuredPath) {
1185
- if (configuredPath) return configuredPath;
1186
- if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
1187
- if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
1188
- return entries[0];
1369
+ function scanStylesheetReservations(path, source, styleSources, reservations, resolvedImports = /* @__PURE__ */ new Set()) {
1370
+ try {
1371
+ const analysis = stylesheetAnalysis(path, source);
1372
+ for (const name of analysis.classNames) reservations.names.add(name);
1373
+ if (analysis.classReservationsUnbounded) reservations.unbounded = true;
1374
+ for (const property of analysis.customProperties) reservations.properties.add(property);
1375
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
1376
+ if (analysis.unverifiable) reservations.unbounded = true;
1377
+ for (const reference of analysis.references) {
1378
+ if (reference === "tailwindcss" || reference.startsWith("tailwindcss/")) continue;
1379
+ if (reference.startsWith("sass:")) continue;
1380
+ if (resolvedImports.has(`${dirname(path)}\0${reference}`) || styleSources.has(reference)) continue;
1381
+ if (stylesheetReferenceTargets(path, reference, styleSources).length === 0) reservations.unbounded = true;
1382
+ }
1383
+ } catch {
1384
+ reservations.unbounded = true;
1385
+ }
1189
1386
  }
1190
- async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
1191
- const projectRequire = createRequire(join(packageRoot, "package.json"));
1192
- let packagePath;
1387
+ function entryDirectiveHazard(source) {
1388
+ const directives = cssDirectives$1(source);
1389
+ if (directives === null) return true;
1390
+ return directives.some((directive) => {
1391
+ if (directive === null || typeof directive !== "object" || !("kind" in directive)) return false;
1392
+ if (directive.kind === "import") return "source" in directive && directive.source !== null || "sourceUnreadable" in directive && directive.sourceUnreadable === true;
1393
+ if (directive.kind === "source") return "not" in directive && directive.not === true || "unreadable" in directive && directive.unreadable === true;
1394
+ return directive.kind === "other" && "name" in directive && (directive.name === "plugin" || directive.name === "config");
1395
+ });
1396
+ }
1397
+ function scanInlineStyleReservations(key, value, reservations) {
1193
1398
  try {
1194
- packagePath = projectRequire.resolve("tailwindcss/package.json");
1399
+ const analysis = stylesheetAnalysis(key, `.x{${value}}`);
1400
+ for (const property of analysis.customProperties) reservations.properties.add(property);
1401
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
1195
1402
  } catch {
1196
- throw new Error("Tailwind v4 must be installed in the target project.");
1403
+ reservations.propertiesUnbounded = true;
1404
+ }
1405
+ }
1406
+ function reservedSpelling(reservations, spelling) {
1407
+ if (reservations.names.has(spelling)) return true;
1408
+ for (const prefix of reservations.prefixes) if (spelling.startsWith(prefix)) return true;
1409
+ return false;
1410
+ }
1411
+ function utilitySegment(candidate) {
1412
+ let depth = 0;
1413
+ let start = 0;
1414
+ for (let index = 0; index < candidate.length; index += 1) {
1415
+ const character = candidate[index];
1416
+ if (character === "[" || character === "(") depth += 1;
1417
+ else if (character === "]" || character === ")") depth -= 1;
1418
+ else if (character === ":" && depth === 0) start = index + 1;
1419
+ }
1420
+ return candidate.slice(start);
1421
+ }
1422
+ function sameShape(left, right) {
1423
+ return left.length === right.length && left.every((declaration, index) => declaration.property === right[index].property && declaration.important === right[index].important);
1424
+ }
1425
+ function acceptedCandidateAliases(system, probes, reservations) {
1426
+ const aliases = {};
1427
+ if (reservations.unbounded) return aliases;
1428
+ const prefix = system.theme.prefix;
1429
+ for (const probe of new Set(probes)) {
1430
+ const canonical = canonicalCandidate(system, probe);
1431
+ if (canonical === null) continue;
1432
+ const unprefixed = prefix !== null && canonical.startsWith(`${prefix}:`) ? canonical.slice(prefix.length + 1) : null;
1433
+ const segment = utilitySegment(canonical).replace(/!$/, "");
1434
+ if (segment.includes("[") || reservedSpelling(reservations, canonical) || unprefixed !== null && reservedSpelling(reservations, unprefixed) || reservedSpelling(reservations, segment)) continue;
1435
+ const [compiledProbe, compiledCanonical] = system.candidatesToCss([probe, canonical]);
1436
+ if (compiledProbe == null || compiledCanonical == null) continue;
1437
+ let probeShape;
1438
+ let canonicalShape;
1439
+ try {
1440
+ probeShape = compiledShape(compiledProbe);
1441
+ canonicalShape = compiledShape(compiledCanonical);
1442
+ } catch {
1443
+ continue;
1444
+ }
1445
+ if (canonicalShape.referencedProperties.length > 0 || probeShape.referencedProperties.length > 0 || !sameShape(probeShape.declarations, canonicalShape.declarations)) continue;
1446
+ aliases[probe] = canonical;
1197
1447
  }
1198
- const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
1199
- if (!String(packageJson.version).startsWith("4.")) throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
1200
- const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
1201
- const css = await snapshotFile(snapshots, tailwindCss);
1202
- const base = dirname(tailwindCss);
1203
- const tailwindRoot = dirname(packagePath);
1204
- const loadModule = async (id, moduleBase) => {
1205
- const path = createRequire(join(moduleBase, "package.json")).resolve(id);
1206
- if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
1207
- const imported = await import(pathToFileURL(path).href);
1208
- return {
1209
- path,
1210
- base: dirname(path),
1211
- module: imported.default ?? imported
1212
- };
1448
+ return aliases;
1449
+ }
1450
+ //#endregion
1451
+ //#region src/plan/fonts.ts
1452
+ function fontTokenName(family) {
1453
+ const collapsed = family.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
1454
+ if (collapsed === "") return "family";
1455
+ return /^\p{N}/u.test(collapsed) ? `family-${collapsed}` : collapsed;
1456
+ }
1457
+ function aliasedCandidate(candidate, utility) {
1458
+ const segment = utilitySegment(candidate);
1459
+ const important = segment.endsWith("!") ? "!" : "";
1460
+ return candidate.slice(0, candidate.length - segment.length) + utility + important;
1461
+ }
1462
+ function appendFontTheme(css, tokens) {
1463
+ const names = Object.keys(tokens).sort();
1464
+ if (names.length === 0) return css;
1465
+ let output = css;
1466
+ if (!output.endsWith("\n")) output += "\n";
1467
+ if (!output.endsWith("\n\n")) output += "\n";
1468
+ output += `@theme {\n${names.map((name) => ` --font-${name}: ${tokens[name]};\n`).join("")}}\n`;
1469
+ return output;
1470
+ }
1471
+ function fontShapesAccept(system, probeCandidate, aliased, reservations, backingToken) {
1472
+ const [compiledProbe, compiledAliased] = system.candidatesToCss([probeCandidate, aliased]);
1473
+ if (compiledProbe == null || compiledAliased == null) return false;
1474
+ let probeShape;
1475
+ let aliasedShape;
1476
+ try {
1477
+ probeShape = compiledShape(compiledProbe);
1478
+ aliasedShape = compiledShape(compiledAliased);
1479
+ } catch {
1480
+ return false;
1481
+ }
1482
+ if (probeShape.referencedProperties.length > 0) return false;
1483
+ if (aliasedShape.referencedProperties.some((property) => reservations.properties.has(property))) return false;
1484
+ if (backingToken !== void 0) {
1485
+ if (!aliasedShape.declarations.some((declaration) => declaration.property === "font-family" && /^var\(--[\w-]+\)$/.test(declaration.value) && (declaration.value === `var(--${backingToken})` || declaration.value.endsWith(`-${backingToken})`)))) return false;
1486
+ }
1487
+ return sameShape(probeShape.declarations, aliasedShape.declarations);
1488
+ }
1489
+ async function registerFontTokens(options) {
1490
+ const { system, reservations } = options;
1491
+ if (reservations.unbounded || reservations.propertiesUnbounded) return {
1492
+ aliases: {},
1493
+ tokens: {},
1494
+ failures: /* @__PURE__ */ new Set()
1213
1495
  };
1214
- const loadStylesheet = async (id, sheetBase) => {
1215
- let path;
1216
- if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
1217
- else if (id.startsWith("tailwindcss/")) {
1218
- const subpath = id.slice(12);
1219
- path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
1220
- } else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
1221
- else path = projectRequire.resolve(id);
1496
+ const prefix = system.theme.prefix;
1497
+ const aliases = {};
1498
+ const failures = /* @__PURE__ */ new Set();
1499
+ const seen = /* @__PURE__ */ new Set();
1500
+ const allocatedByStack = /* @__PURE__ */ new Map();
1501
+ const proposals = [];
1502
+ const claimed = /* @__PURE__ */ new Set();
1503
+ const tokenStacks = fontTokenStacks(options.themeTokens, options.referenceTokens);
1504
+ for (const probe of options.probes) {
1505
+ if (seen.has(probe.candidate)) continue;
1506
+ seen.add(probe.candidate);
1507
+ if (probe.firstFamily.kind !== "name") continue;
1508
+ if (probe.candidate in options.existingAliases) continue;
1509
+ let reused = false;
1510
+ for (const token of matchingFontTokens(tokenStacks, probe.value)) {
1511
+ const aliased = aliasedCandidate(probe.candidate, token);
1512
+ if (reservedSpelling(reservations, token) || reservedSpelling(reservations, aliased)) continue;
1513
+ const reuseName = token.replace(/^font-/, "");
1514
+ const owner = options.globalAllocations.get(reuseName);
1515
+ if (owner !== void 0 && (owner === null || owner !== probe.value)) continue;
1516
+ if (fontShapesAccept(system, probe.candidate, aliased, reservations, token)) {
1517
+ aliases[probe.candidate] = aliased;
1518
+ options.globalAllocations.set(reuseName, probe.value);
1519
+ reused = true;
1520
+ break;
1521
+ }
1522
+ }
1523
+ if (reused) continue;
1524
+ if (!options.generate) continue;
1525
+ const stackKey = probe.value;
1526
+ const shared = allocatedByStack.get(stackKey);
1527
+ if (shared !== void 0) {
1528
+ proposals.push({
1529
+ probe,
1530
+ name: shared
1531
+ });
1532
+ continue;
1533
+ }
1534
+ const base = fontTokenName(probe.firstFamily.name);
1535
+ let allocated = false;
1536
+ for (let attempt = 1; attempt <= 100; attempt += 1) {
1537
+ const name = attempt === 1 ? base : `${base}-${attempt}`;
1538
+ if (claimed.has(name)) continue;
1539
+ if (reservations.properties.has(`--font-${name}`)) continue;
1540
+ if (prefix !== null && reservations.properties.has(`--${prefix}-font-${name}`)) continue;
1541
+ if (reservedSpelling(reservations, `font-${name}`)) continue;
1542
+ if (options.mentionedSegments.has(`font-${name}`)) continue;
1543
+ const global = options.globalAllocations.get(name);
1544
+ if (global !== void 0 && (global === null || global !== probe.value)) continue;
1545
+ const compiled = prefix !== null ? `${prefix}:font-${name}` : `font-${name}`;
1546
+ if (system.candidatesToCss([compiled])[0] !== null) continue;
1547
+ claimed.add(name);
1548
+ allocatedByStack.set(stackKey, name);
1549
+ proposals.push({
1550
+ probe,
1551
+ name
1552
+ });
1553
+ allocated = true;
1554
+ break;
1555
+ }
1556
+ if (!allocated) failures.add(probe.candidate);
1557
+ }
1558
+ if (proposals.length === 0) return {
1559
+ aliases,
1560
+ tokens: {},
1561
+ failures
1562
+ };
1563
+ const tokens = {};
1564
+ for (const { probe, name } of proposals) tokens[name] = probe.value;
1565
+ let fontSystem;
1566
+ try {
1567
+ fontSystem = await options.loadWith(appendFontTheme(options.entryCss, tokens));
1568
+ } catch (error) {
1569
+ if (isIntegrityError(error)) throw error;
1570
+ for (const { probe } of proposals) failures.add(probe.candidate);
1222
1571
  return {
1223
- content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
1224
- base: dirname(path)
1572
+ aliases,
1573
+ tokens: {},
1574
+ failures
1225
1575
  };
1226
- };
1227
- const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
1228
- const graphSources = [];
1229
- const themeTokens = {
1230
- ...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
1231
- ...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources)
1232
- };
1233
- graphSources.push({
1234
- path: tailwindCss,
1235
- source: css
1236
- });
1237
- const designSystem = await loadDesignSystem(css, {
1238
- base,
1239
- loadModule,
1240
- loadStylesheet
1241
- });
1242
- const loadWith = (replacement) => loadDesignSystem(replacement, {
1243
- base,
1244
- loadModule,
1245
- loadStylesheet
1246
- });
1576
+ }
1577
+ const before = system.candidatesToCss(options.corpus);
1578
+ const after = fontSystem.candidatesToCss(options.corpus);
1579
+ const corpusChanged = options.corpus.some((candidate, index) => before[index] !== after[index]);
1580
+ const accepted = {};
1581
+ for (const { probe, name } of proposals) {
1582
+ const canonical = corpusChanged ? null : canonicalCandidate(fontSystem, probe.candidate);
1583
+ const expected = aliasedCandidate(probe.candidate, `font-${name}`);
1584
+ if (canonical !== expected || !fontShapesAccept(fontSystem, probe.candidate, expected, reservations)) {
1585
+ failures.add(probe.candidate);
1586
+ continue;
1587
+ }
1588
+ aliases[probe.candidate] = expected;
1589
+ accepted[name] = probe.value;
1590
+ }
1247
1591
  return {
1248
- designSystem,
1249
- css,
1250
- path: tailwindCss,
1251
- themeTokens,
1252
- graphSources,
1253
- loadWith
1592
+ aliases,
1593
+ tokens: accepted,
1594
+ failures
1254
1595
  };
1255
1596
  }
1256
- function extractThemeTokens(key, css) {
1257
- return stylesheetAnalysis(key, css).themeTokens;
1258
- }
1259
- function importSpecifiers(css) {
1260
- return (cssDirectives$1(css) ?? []).flatMap((directive) => directive !== null && typeof directive === "object" && "kind" in directive && directive.kind === "import" && "specifier" in directive && typeof directive.specifier === "string" ? [directive.specifier] : []);
1261
- }
1262
- async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, seen = /* @__PURE__ */ new Set()) {
1263
- const tokens = {};
1264
- for (const spec of importSpecifiers(css)) {
1265
- const key = `${base}\0${spec}`;
1266
- if (seen.has(key)) continue;
1267
- seen.add(key);
1268
- const loaded = await loadStylesheet(spec, base);
1269
- if (spec !== "tailwindcss" && !spec.startsWith("tailwindcss/")) graphSources.push({
1270
- path: key,
1271
- source: loaded.content
1272
- });
1273
- Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, seen));
1597
+ function fontTokenStacks(themeTokens, referenceTokens = /* @__PURE__ */ new Set()) {
1598
+ const stacks = /* @__PURE__ */ new Map();
1599
+ for (const [token, value] of Object.entries(themeTokens)) {
1600
+ if (!token.startsWith("font-")) continue;
1601
+ if (referenceTokens.has(token)) continue;
1602
+ const parsed = fontFamilyStack(value);
1603
+ if (parsed !== null) stacks.set(token, parsed.value);
1274
1604
  }
1275
- return Object.assign(tokens, extractThemeTokens(`${identity}.theme.css`, css));
1605
+ return stacks;
1276
1606
  }
1277
- function invalidCandidates(system, candidates) {
1278
- const generated = system.candidatesToCss(candidates);
1279
- return candidates.filter((_, index) => generated[index] === null);
1607
+ function matchingFontTokens(stacks, normalizedStack) {
1608
+ return [...stacks].filter(([, stack]) => stack === normalizedStack).map(([token]) => token).sort();
1280
1609
  }
1281
1610
  //#endregion
1282
1611
  //#region src/plan/media.ts
1283
1612
  const GENERATED_NAMESPACE = "twm-media";
1284
1613
  /**
1285
- * Resolve one fixed name per key across a complete entry group.
1614
+ * Resolve one fixed name per key across a complete entry group: key to
1615
+ * resolved name, in sorted key order. Only `generated` entries emit a
1616
+ * definition; a key absent from the map could not escape existing owners
1617
+ * and keeps the arbitrary-variant fallback.
1286
1618
  *
1287
1619
  * Resolution follows the RFC order: a built-in candidate with a verified
1288
1620
  * effective expansion, an existing breakpoint matched by exact value and
@@ -1308,7 +1640,6 @@ function resolveMediaNames(collections, themeTokens, probes, reservedNames = [])
1308
1640
  };
1309
1641
  const taken = (name) => claimed.has(name) || reserved.has(name) || authoredKeysByName.has(name) || probes.resolves(name);
1310
1642
  const names = /* @__PURE__ */ new Map();
1311
- const fallbacks = /* @__PURE__ */ new Set();
1312
1643
  for (const component of components) {
1313
1644
  const resolved = (name, kind) => {
1314
1645
  claimed.add(name);
@@ -1327,25 +1658,18 @@ function resolveMediaNames(collections, themeTokens, probes, reservedNames = [])
1327
1658
  }
1328
1659
  const digestName = `${GENERATED_NAMESPACE}-${component.digest}`;
1329
1660
  const candidates = component.readableName === null ? [digestName] : [component.readableName, digestName];
1330
- let done = false;
1331
1661
  for (const candidate of candidates) {
1332
1662
  if (adoptableKey(candidate) === component.key && !claimed.has(candidate) && probes.expansionMatches(candidate, component.key)) {
1333
1663
  resolved(candidate, "adopted");
1334
- done = true;
1335
1664
  break;
1336
1665
  }
1337
1666
  if (!taken(candidate)) {
1338
1667
  resolved(candidate, "generated");
1339
- done = true;
1340
1668
  break;
1341
1669
  }
1342
1670
  }
1343
- if (!done) fallbacks.add(component.key);
1344
1671
  }
1345
- return {
1346
- names,
1347
- fallbacks
1348
- };
1672
+ return names;
1349
1673
  }
1350
1674
  function mergeComponents(collections) {
1351
1675
  const byKey = /* @__PURE__ */ new Map();
@@ -1426,7 +1750,7 @@ function planMediaExtraction(options) {
1426
1750
  const resolution = resolveMediaNames(options.collections, options.tailwind.themeTokens, buildMediaProbes(options.tailwind), scannedVariantReservations([...options.scannedSources, ...options.tailwind.graphSources.map((graphSource) => graphSource.source)]));
1427
1751
  const names = {};
1428
1752
  const generated = [];
1429
- for (const [key, resolved] of resolution.names) {
1753
+ for (const [key, resolved] of resolution) {
1430
1754
  names[key] = resolved.name;
1431
1755
  if (resolved.kind === "generated") generated.push({
1432
1756
  key,
@@ -1535,7 +1859,9 @@ function analyzeVueSource(compiler, path, source) {
1535
1859
  const { descriptor, errors } = compiler.parse(source, { filename: path });
1536
1860
  const styleRanges = descriptor.styles.map((style) => ({
1537
1861
  start: style.loc.start.offset,
1538
- end: style.loc.end.offset
1862
+ end: style.loc.end.offset,
1863
+ lang: style.lang ?? "css",
1864
+ ...style.src !== void 0 ? { src: style.src } : {}
1539
1865
  }));
1540
1866
  const byteRanges = (offset) => ({
1541
1867
  warnings: warnings.map((warning) => ({
@@ -1544,6 +1870,7 @@ function analyzeVueSource(compiler, path, source) {
1544
1870
  end: offset(warning.end)
1545
1871
  })),
1546
1872
  styleRanges: styleRanges.map((range) => ({
1873
+ ...range,
1547
1874
  start: offset(range.start),
1548
1875
  end: offset(range.end)
1549
1876
  }))
@@ -1684,6 +2011,14 @@ function analyzeVueSource(compiler, path, source) {
1684
2011
  moduleClosureBroken: false,
1685
2012
  referencesUseCssModule: false,
1686
2013
  expressionReferences: /* @__PURE__ */ new Set(),
2014
+ templatePrefixes: /* @__PURE__ */ new Set(),
2015
+ entityClassTokens: [],
2016
+ styleAttributeValues: [],
2017
+ styleAttributesUnverifiable: false,
2018
+ customProperties: /* @__PURE__ */ new Set(),
2019
+ customPropertiesUnbounded: false,
2020
+ stylesheetLinks: [],
2021
+ stylesheetLinksUnverifiable: false,
1687
2022
  expressionPath: scriptLang === "ts" || scriptLang === "tsx" ? "Component.ts" : "Component.js"
1688
2023
  };
1689
2024
  visitTemplateNode(source, template.ast, state);
@@ -1699,8 +2034,11 @@ function analyzeVueSource(compiler, path, source) {
1699
2034
  let setupAnalysis;
1700
2035
  let scriptUsesCssModule = false;
1701
2036
  let scriptHasDynamicImport = false;
2037
+ const scriptCustomProperties = [];
2038
+ let scriptCustomPropertiesUnbounded = false;
1702
2039
  const scriptVueReferences = [];
1703
2040
  const scriptVueGlobPatterns = [];
2041
+ const scriptTemplatePrefixes = [];
1704
2042
  let scriptVueGlobUnverifiable = false;
1705
2043
  const styleImports = [descriptor.script, descriptor.scriptSetup].flatMap((script) => {
1706
2044
  if (!script) return [];
@@ -1716,6 +2054,9 @@ function analyzeVueSource(compiler, path, source) {
1716
2054
  scriptImports.push(...analysis.imports);
1717
2055
  scriptVueReferences.push(...analysis.imports.filter((record) => !record.typeOnly).map((record) => record.specifier));
1718
2056
  scriptVueGlobPatterns.push(...analysis.vueGlobPatterns);
2057
+ scriptTemplatePrefixes.push(...analysis.templatePrefixes);
2058
+ scriptCustomProperties.push(...analysis.customProperties);
2059
+ if (analysis.customPropertiesUnbounded) scriptCustomPropertiesUnbounded = true;
1719
2060
  if (analysis.vueGlobUnverifiable) scriptVueGlobUnverifiable = true;
1720
2061
  return analysis.staticImports;
1721
2062
  } catch {
@@ -1790,6 +2131,14 @@ function analyzeVueSource(compiler, path, source) {
1790
2131
  scriptVueReferences,
1791
2132
  scriptVueGlobPatterns,
1792
2133
  scriptVueGlobUnverifiable,
2134
+ templatePrefixes: [.../* @__PURE__ */ new Set([...state.templatePrefixes, ...scriptTemplatePrefixes])],
2135
+ templateClassTokens: [...new Set(state.entityClassTokens)],
2136
+ templateStyleValues: state.styleAttributeValues,
2137
+ templateStylesUnverifiable: state.styleAttributesUnverifiable,
2138
+ scriptCustomProperties: [...scriptCustomProperties, ...state.customProperties],
2139
+ scriptCustomPropertiesUnbounded: scriptCustomPropertiesUnbounded || state.customPropertiesUnbounded,
2140
+ templateStylesheetLinks: state.stylesheetLinks,
2141
+ templateStylesheetLinksUnverifiable: state.stylesheetLinksUnverifiable,
1793
2142
  hasOpaqueStyleBlocks: opaqueStyleBlocks,
1794
2143
  styleBlockImports: styleBlockImports.map((entry) => ({
1795
2144
  ...entry,
@@ -1822,6 +2171,29 @@ function verifyVueSource(compiler, path, source, includeUnscoped = false) {
1822
2171
  function visitTemplateNode(source, node, state) {
1823
2172
  if (node.type === NODE_ELEMENT) {
1824
2173
  if (node.tagType === TAG_SLOT) state.hasSlot = true;
2174
+ for (const prop of node.props ?? []) {
2175
+ if (prop.type === PROP_ATTRIBUTE && prop.name === "style" && prop.value?.content) state.styleAttributeValues.push(prop.value.content);
2176
+ if (prop.type === PROP_DIRECTIVE && prop.name === "bind") {
2177
+ const argument = prop.arg?.isStatic === true ? prop.arg.content : void 0;
2178
+ if (prop.arg === void 0 || argument === void 0 || argument === "style") state.styleAttributesUnverifiable = true;
2179
+ }
2180
+ }
2181
+ if (node.tag === "link" && node.tagType === TAG_ELEMENT) {
2182
+ let rel;
2183
+ let href;
2184
+ let dynamic = false;
2185
+ for (const prop of node.props ?? []) if (prop.type === PROP_ATTRIBUTE) {
2186
+ if (prop.name === "rel") rel = prop.value?.content ?? "";
2187
+ if (prop.name === "href") href = prop.value?.content ?? "";
2188
+ } else if (prop.type === PROP_DIRECTIVE && prop.name === "bind") {
2189
+ const argument = prop.arg?.isStatic ? prop.arg.content : void 0;
2190
+ if (argument === void 0 || argument === "rel" || argument === "href") dynamic = true;
2191
+ }
2192
+ if ((rel?.toLowerCase().split(/\s+/) ?? []).includes("stylesheet") || rel === void 0 && dynamic) {
2193
+ if (href !== void 0 && !dynamic) state.stylesheetLinks.push(href);
2194
+ else state.stylesheetLinksUnverifiable = true;
2195
+ } else if (dynamic) state.stylesheetLinksUnverifiable = true;
2196
+ }
1825
2197
  const bindingClasses = [];
1826
2198
  let classOpaque = false;
1827
2199
  let moduleBinding;
@@ -1851,11 +2223,15 @@ function visitTemplateNode(source, node, state) {
1851
2223
  if (!provenModuleExpression && expression?.usesCssModule) state.moduleClosureBroken = true;
1852
2224
  if (expression?.referencesUseCssModule) state.referencesUseCssModule = true;
1853
2225
  for (const name of expression?.references ?? []) state.expressionReferences.add(name);
2226
+ for (const prefix of expression?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
2227
+ for (const name of expression?.customProperties ?? []) state.customProperties.add(name);
2228
+ if (expression?.customPropertiesUnbounded) state.customPropertiesUnbounded = true;
1854
2229
  if (prop.arg && !prop.arg.isStatic && prop.arg.content) {
1855
2230
  const argExpression = templateExpression(state.expressionPath, prop.arg.content);
1856
2231
  state.moduleClosureBroken ||= argExpression?.usesCssModule ?? true;
1857
2232
  state.referencesUseCssModule ||= argExpression?.referencesUseCssModule ?? false;
1858
2233
  for (const name of argExpression?.references ?? []) state.expressionReferences.add(name);
2234
+ for (const prefix of argExpression?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
1859
2235
  }
1860
2236
  if (prop.name === "html") state.vHtml = true;
1861
2237
  }
@@ -1875,6 +2251,7 @@ function visitTemplateNode(source, node, state) {
1875
2251
  state.moduleClosureBroken ||= interpolation?.usesCssModule ?? true;
1876
2252
  state.referencesUseCssModule ||= interpolation?.referencesUseCssModule ?? false;
1877
2253
  for (const name of interpolation?.references ?? []) state.expressionReferences.add(name);
2254
+ for (const prefix of interpolation?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
1878
2255
  }
1879
2256
  for (const child of node.children ?? []) visitTemplateNode(source, child, state);
1880
2257
  }
@@ -1899,18 +2276,24 @@ function templateHandler(source) {
1899
2276
  vueModuleMember: null,
1900
2277
  usesCssModule: analysis.usesCssModule,
1901
2278
  referencesUseCssModule: analysis.hasUnboundUseCssModule,
1902
- references: analysis.unboundReferences
2279
+ references: analysis.unboundReferences,
2280
+ templatePrefixes: analysis.templatePrefixes,
2281
+ customProperties: analysis.customProperties,
2282
+ customPropertiesUnbounded: analysis.customPropertiesUnbounded
1903
2283
  };
1904
2284
  } catch {
1905
2285
  return;
1906
2286
  }
1907
2287
  }
1908
2288
  function templateSite(source, node, bindingClasses, classOpaque, state) {
1909
- let classAttribute = literalAttribute(source, node, "class");
1910
- const idAttribute = literalAttribute(source, node, "id");
1911
- const hasClassAttr = node.props?.some((prop) => prop.type === PROP_ATTRIBUTE && prop.name === "class");
1912
- const hasIdAttr = node.props?.some((prop) => prop.type === PROP_ATTRIBUTE && prop.name === "id");
1913
- if (hasClassAttr && !classAttribute || hasIdAttr && !idAttribute) {
2289
+ const classProp = staticAttribute(node, "class");
2290
+ const idProp = staticAttribute(node, "id");
2291
+ let classAttribute = literalAttribute(source, classProp);
2292
+ const idAttribute = literalAttribute(source, idProp);
2293
+ if (classProp && !classAttribute || idProp && !idAttribute) {
2294
+ if (classProp && !classAttribute) {
2295
+ for (const token of classProp.value?.content.split(/[\t\n\f\r ]+/) ?? []) if (token !== "") state.entityClassTokens.push(token);
2296
+ }
1914
2297
  state.dynamic = true;
1915
2298
  return { idAttribute };
1916
2299
  }
@@ -1931,8 +2314,10 @@ function templateSite(source, node, bindingClasses, classOpaque, state) {
1931
2314
  matchClasses
1932
2315
  };
1933
2316
  }
1934
- function literalAttribute(source, node, name) {
1935
- const prop = node.props?.find((prop) => prop.type === PROP_ATTRIBUTE && prop.name === name);
2317
+ function staticAttribute(node, name) {
2318
+ return node.props?.find((prop) => prop.type === PROP_ATTRIBUTE && prop.name === name);
2319
+ }
2320
+ function literalAttribute(source, prop) {
1936
2321
  if (!prop?.value) return void 0;
1937
2322
  const start = prop.value.loc.start.offset;
1938
2323
  const end = prop.value.loc.end.offset;
@@ -2218,7 +2603,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
2218
2603
  })));
2219
2604
  vueShadowModuleCss.push(...analysis.shadowModuleCssTexts, ...analysis.moduleBlocks.map((block) => block.analysisSource ?? block.content));
2220
2605
  }
2221
- const packageIsPrivate = JSON.parse(await snapshotFile(snapshots, join(packageRoot, "package.json"))).private === true;
2606
+ const packageIsPrivate = projectPackageSchema.parse(JSON.parse(await snapshotFile(snapshots, join(packageRoot, "package.json")))).private === true;
2222
2607
  const files = /* @__PURE__ */ new Map();
2223
2608
  const stylesheets = [];
2224
2609
  const stylePaths = /* @__PURE__ */ new Set();
@@ -2404,7 +2789,16 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
2404
2789
  htmlScriptText: analysis.scriptText
2405
2790
  } : {},
2406
2791
  sourceImports: analysis.scriptImports,
2407
- sourceImportsUnverifiable: analysis.scriptImportsUnverifiable
2792
+ sourceImportsUnverifiable: analysis.scriptImportsUnverifiable,
2793
+ templatePrefixes: analysis.templatePrefixes,
2794
+ templateInjectsMarkup: analysis.vHtml,
2795
+ templateClassTokens: analysis.templateClassTokens,
2796
+ templateStyleValues: analysis.templateStyleValues,
2797
+ templateStylesUnverifiable: analysis.templateStylesUnverifiable,
2798
+ sourceCustomProperties: analysis.scriptCustomProperties,
2799
+ sourceCustomPropertiesUnbounded: analysis.scriptCustomPropertiesUnbounded,
2800
+ templateStylesheetLinks: analysis.templateStylesheetLinks,
2801
+ templateStylesheetLinksUnverifiable: analysis.templateStylesheetLinksUnverifiable
2408
2802
  });
2409
2803
  }
2410
2804
  return {
@@ -2493,7 +2887,20 @@ function indexWarningPositions(source, offsets, unicodeSeparatorsAreNewlines, fo
2493
2887
  }
2494
2888
  return positions;
2495
2889
  }
2890
+ const optionPath = z.string().check(z.minLength(1), z.refine((path) => !path.includes("\0")));
2891
+ const migrateOptionsSchema = z.strictObject({
2892
+ styleFile: z.optional(optionPath),
2893
+ cwd: z.optional(optionPath),
2894
+ write: z.optional(z.boolean()),
2895
+ tailwindCss: z.optional(optionPath),
2896
+ workspaces: z.optional(z.boolean()),
2897
+ force: z.optional(z.boolean()),
2898
+ extractMediaQueries: z.optional(z.boolean())
2899
+ });
2496
2900
  async function migrate(options = {}) {
2901
+ const parsedOptions = migrateOptionsSchema.safeParse(options);
2902
+ if (!parsedOptions.success) throw new TypeError(`Invalid migrate options: ${parsedOptions.error.message}`);
2903
+ options = parsedOptions.data;
2497
2904
  if (options.styleFile && options.workspaces) throw new TypeError("styleFile cannot be combined with workspaces");
2498
2905
  if (options.tailwindCss && options.workspaces) throw new TypeError("tailwindCss cannot be combined with workspaces");
2499
2906
  const scope = await resolveScope(options);
@@ -2538,7 +2945,8 @@ async function migrate(options = {}) {
2538
2945
  styleDependents: indexStylesheetDependents(styleSources),
2539
2946
  vueStyleRanges,
2540
2947
  entryCatalog: tailwindEntryCatalog(styleSources, scope.pathOwners),
2541
- ignoredPaths: options.workspaces ? await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)) : /* @__PURE__ */ new Set()
2948
+ ignoredPaths: await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)),
2949
+ fontAllocations: /* @__PURE__ */ new Map()
2542
2950
  };
2543
2951
  const failures = [];
2544
2952
  const plans = [];
@@ -2555,6 +2963,14 @@ async function migrate(options = {}) {
2555
2963
  members.push(preparation);
2556
2964
  groups.set(preparation.tailwind.path, members);
2557
2965
  }
2966
+ for (const [, members] of groups) for (const [token, value] of Object.entries(members[0].tailwind.themeTokens)) {
2967
+ if (!token.startsWith("font-")) continue;
2968
+ const parsed = fontFamilyStack(value);
2969
+ const name = token.replace(/^font-/, "");
2970
+ const stack = parsed && parsed.value;
2971
+ const existing = context.fontAllocations.get(name);
2972
+ context.fontAllocations.set(name, existing === void 0 || existing === stack ? stack : null);
2973
+ }
2558
2974
  for (const [, members] of [...groups].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) for (const result of await planPreparedGroup(context, members)) if (result.failure) failures.push(result.failure);
2559
2975
  else if (result.plan) plans.push(result.plan);
2560
2976
  const originals = new Map([...styleSources, ...sourceFiles.map((file) => [file.path, file.source])]);
@@ -2700,10 +3116,7 @@ async function preparePackage(context, packageRoot) {
2700
3116
  let packageJson = {};
2701
3117
  try {
2702
3118
  const rawPackageJson = snapshots.get(join(packageRoot, "package.json"));
2703
- if (rawPackageJson !== void 0) {
2704
- const parsed = JSON.parse(rawPackageJson);
2705
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) packageJson = { ...parsed };
2706
- }
3119
+ if (rawPackageJson !== void 0) packageJson = projectPackageSchema.parse(JSON.parse(rawPackageJson));
2707
3120
  } catch (error) {
2708
3121
  return recover(error);
2709
3122
  }
@@ -2847,6 +3260,174 @@ async function planPreparedGroup(context, members) {
2847
3260
  return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
2848
3261
  });
2849
3262
  const entry = active[0].tailwind;
3263
+ 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"))));
3264
+ const groupFiles = new Map(active.flatMap((member) => member.files.map((file) => [file.path, file])));
3265
+ const mentionedSegments = /* @__PURE__ */ new Set();
3266
+ const mentionedSources = [...[...groupFiles.values()].map((file) => file.source), ...entry.graphSources.map((graphSource) => graphSource.source)];
3267
+ const addSegment = (token) => {
3268
+ if (token === "") return;
3269
+ const segment = utilitySegment(token).replace(/!$/, "");
3270
+ if (segment) mentionedSegments.add(segment);
3271
+ };
3272
+ for (const source of mentionedSources) {
3273
+ for (const token of source.split(/[\s"'`<>=,;{}()\\]+/)) addSegment(token);
3274
+ 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(",")) {
3275
+ const range = /^(\d+)\.\.(\d+)(?:\.\.(\d+))?$/.exec(option);
3276
+ const stride = range === null ? 0 : Number(range[3] ?? "1");
3277
+ if (range !== null && stride > 0) {
3278
+ const from = Number(range[1]);
3279
+ const to = Number(range[2]);
3280
+ let steps = 0;
3281
+ for (let value = Math.min(from, to); value <= Math.max(from, to); value += stride) {
3282
+ addSegment(`${candidate[1]}${value}${candidate[3]}`);
3283
+ if ((steps += 1) >= 1e3) break;
3284
+ }
3285
+ continue;
3286
+ }
3287
+ addSegment(`${candidate[1]}${option}${candidate[3]}`);
3288
+ }
3289
+ }
3290
+ const fileRoots = new Map(active.flatMap((member) => member.files.map((file) => [file.path, member.packageRoot])));
3291
+ const workspaceEntries = new Set([...context.entryCatalog.values()].flat());
3292
+ for (const file of groupFiles.values()) {
3293
+ if (reservations.unbounded) break;
3294
+ if (context.ignoredPaths.has(file.path)) {
3295
+ reservations.unbounded = true;
3296
+ break;
3297
+ }
3298
+ const opaqueStylesheetLink = (link) => {
3299
+ const resolved = localHtmlReference(fileRoots.get(file.path) ?? context.workspaceRoot, dirname(file.path), link);
3300
+ return resolved === void 0 || !context.styleSources.has(resolved) || workspaceEntries.has(resolved) && resolved !== entry.path;
3301
+ };
3302
+ const opaqueStylesheetImports = (records) => {
3303
+ for (const record of records) {
3304
+ if (record.typeOnly) continue;
3305
+ const specifier = record.specifier.split(/[?#]/, 1)[0];
3306
+ if (!isStylesheetPath(specifier)) {
3307
+ if (extname(specifier) !== "" || specifier.startsWith(".")) continue;
3308
+ try {
3309
+ const resolved = createRequire(file.path).resolve(specifier);
3310
+ if (isStylesheetPath(resolved) && !context.styleSources.has(resolved)) reservations.unbounded = true;
3311
+ } catch {}
3312
+ continue;
3313
+ }
3314
+ const resolved = specifier.startsWith(".") ? resolve(dirname(file.path), specifier) : void 0;
3315
+ if (resolved === void 0 || !context.styleSources.has(resolved) || workspaceEntries.has(resolved) && resolved !== entry.path) reservations.unbounded = true;
3316
+ }
3317
+ };
3318
+ if (extname(file.path) === ".html") {
3319
+ try {
3320
+ const parsed = parseHtmlSource(file.path, file.source);
3321
+ if (parsed.hasStyle) reservations.unbounded = true;
3322
+ if (parsed.bases.length > 0) reservations.unbounded = true;
3323
+ for (const link of parsed.links) if (opaqueStylesheetLink(link.href)) reservations.unbounded = true;
3324
+ for (const [index, value] of parsed.styleAttributeValues.entries()) {
3325
+ if (TEMPLATE_MARKERS.test(value)) {
3326
+ reservations.propertiesUnbounded = true;
3327
+ continue;
3328
+ }
3329
+ scanInlineStyleReservations(`${file.path}.style.${index}.css`, value, reservations);
3330
+ }
3331
+ for (const value of parsed.dynamicClassValues) {
3332
+ const masked = value.replace(TEMPLATE_EXPRESSIONS, "\0");
3333
+ if (TEMPLATE_MARKERS.test(masked)) {
3334
+ reservations.unbounded = true;
3335
+ continue;
3336
+ }
3337
+ for (const token of masked.split(/\s+/)) {
3338
+ if (token === "") continue;
3339
+ const index = token.indexOf("\0");
3340
+ if (index < 0) reservations.names.add(token);
3341
+ else if (index === 0) reservations.unbounded = true;
3342
+ else reservations.prefixes.add(token.slice(0, index));
3343
+ }
3344
+ }
3345
+ if (parsed.handlerText.trim() !== "") try {
3346
+ const handlers = sourceAnalysis(`${file.path}.handlers.js`, parsed.handlerText);
3347
+ for (const prefix of handlers.templatePrefixes) reservations.prefixes.add(prefix);
3348
+ for (const property of handlers.customProperties) reservations.properties.add(property);
3349
+ if (handlers.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
3350
+ } catch {
3351
+ reservations.unbounded = true;
3352
+ }
3353
+ if (parsed.scriptText.trim() !== "") try {
3354
+ const script = sourceAnalysis(`${file.path}.inline.js`, parsed.scriptText);
3355
+ for (const prefix of script.templatePrefixes) reservations.prefixes.add(prefix);
3356
+ for (const property of script.customProperties) reservations.properties.add(property);
3357
+ if (script.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
3358
+ opaqueStylesheetImports(script.imports);
3359
+ } catch {
3360
+ reservations.unbounded = true;
3361
+ }
3362
+ } catch {
3363
+ reservations.unbounded = true;
3364
+ }
3365
+ continue;
3366
+ }
3367
+ if (extname(file.path) === ".vue") {
3368
+ if (file.templatePrefixes === void 0 || file.sourceImportsUnverifiable === true || file.templateInjectsMarkup === true) {
3369
+ reservations.unbounded = true;
3370
+ continue;
3371
+ }
3372
+ const styleRanges = context.vueStyleRanges.get(file.path);
3373
+ if (styleRanges === void 0) {
3374
+ reservations.unbounded = true;
3375
+ continue;
3376
+ }
3377
+ const fileBytes = Buffer.from(file.source);
3378
+ for (const [index, range] of styleRanges.entries()) {
3379
+ if (range.src !== void 0) {
3380
+ const resolved = range.src.startsWith(".") ? resolve(dirname(file.path), range.src) : void 0;
3381
+ if (resolved === void 0 || !context.styleSources.has(resolved)) reservations.unbounded = true;
3382
+ continue;
3383
+ }
3384
+ const block = fileBytes.subarray(range.start, range.end).toString();
3385
+ if (![
3386
+ "css",
3387
+ "scss",
3388
+ "sass",
3389
+ "less"
3390
+ ].includes(range.lang)) {
3391
+ reservations.unbounded = true;
3392
+ continue;
3393
+ }
3394
+ scanStylesheetReservations(`${file.path}.block.${index}.${range.lang}`, block, context.styleSources, reservations);
3395
+ }
3396
+ for (const prefix of file.templatePrefixes) reservations.prefixes.add(prefix);
3397
+ for (const token of file.templateClassTokens ?? []) reservations.names.add(token);
3398
+ for (const property of file.sourceCustomProperties ?? []) reservations.properties.add(property);
3399
+ if (file.sourceCustomPropertiesUnbounded === true) reservations.propertiesUnbounded = true;
3400
+ if (file.templateStylesUnverifiable === true) reservations.propertiesUnbounded = true;
3401
+ for (const [index, value] of (file.templateStyleValues ?? []).entries()) scanInlineStyleReservations(`${file.path}.style.${index}.css`, value, reservations);
3402
+ if (file.templateStylesheetLinksUnverifiable === true) reservations.unbounded = true;
3403
+ for (const link of file.templateStylesheetLinks ?? []) if (opaqueStylesheetLink(link)) reservations.unbounded = true;
3404
+ opaqueStylesheetImports(file.sourceImports ?? []);
3405
+ continue;
3406
+ }
3407
+ if (![
3408
+ ".js",
3409
+ ".jsx",
3410
+ ".ts",
3411
+ ".tsx",
3412
+ ".mjs",
3413
+ ".cjs",
3414
+ ".mts",
3415
+ ".cts"
3416
+ ].includes(extname(file.path))) continue;
3417
+ try {
3418
+ const analysis = sourceAnalysis(file.path, file.source);
3419
+ for (const prefix of analysis.templatePrefixes) reservations.prefixes.add(prefix);
3420
+ for (const property of analysis.customProperties) reservations.properties.add(property);
3421
+ if (analysis.customPropertiesUnbounded) reservations.propertiesUnbounded = true;
3422
+ opaqueStylesheetImports(analysis.imports);
3423
+ if (analysis.rendersStyleElement) reservations.unbounded = true;
3424
+ if (analysis.injectsMarkup) reservations.unbounded = true;
3425
+ if (analysis.stylesheetLinksUnverifiable) reservations.unbounded = true;
3426
+ for (const link of analysis.stylesheetLinks) if (opaqueStylesheetLink(link)) reservations.unbounded = true;
3427
+ } catch {
3428
+ reservations.unbounded = true;
3429
+ }
3430
+ }
2850
3431
  const recoverGroup = (error, fatal = false) => {
2851
3432
  if (!options.force || fatal) throw error;
2852
3433
  for (const member of active) results.push({ failure: packageFailure(workspaceRoot, member.packageRoot, error) });
@@ -2900,28 +3481,30 @@ async function planPreparedGroup(context, members) {
2900
3481
  if (active.length === 0) return results;
2901
3482
  let names = extraction?.names;
2902
3483
  let generated = extraction?.generated ?? [];
2903
- if (options.extractMediaQueries !== false) if (!groupWritable) {
2904
- names = {};
2905
- generated = [];
2906
- if (collections.some((collection) => collection.components.length > 0)) mediaWarnings.push({
2907
- code: "media-query-definition-fallback",
2908
- file: entry.path,
2909
- start: 0,
2910
- end: 0,
2911
- message: "The Tailwind entry could not be edited safely, so media behavior was preserved with arbitrary variants."
2912
- });
2913
- } else {
2914
- try {
2915
- extraction = planMediaExtraction({
2916
- collections,
2917
- tailwind: entry,
2918
- scannedSources: context.sourceFiles.map((file) => file.source)
3484
+ if (options.extractMediaQueries !== false) {
3485
+ if (!groupWritable) {
3486
+ names = {};
3487
+ generated = [];
3488
+ if (collections.some((collection) => collection.components.length > 0)) mediaWarnings.push({
3489
+ code: "media-query-definition-fallback",
3490
+ file: entry.path,
3491
+ start: 0,
3492
+ end: 0,
3493
+ message: "The Tailwind entry could not be edited safely, so media behavior was preserved with arbitrary variants."
2919
3494
  });
2920
- } catch (error) {
2921
- return recoverGroup(error, !isRecoverablePlanningError(error));
3495
+ } else {
3496
+ try {
3497
+ extraction = planMediaExtraction({
3498
+ collections,
3499
+ tailwind: entry,
3500
+ scannedSources: context.sourceFiles.map((file) => file.source)
3501
+ });
3502
+ } catch (error) {
3503
+ return recoverGroup(error, !isRecoverablePlanningError(error));
3504
+ }
3505
+ names = extraction.names;
3506
+ generated = extraction.generated;
2922
3507
  }
2923
- names = extraction.names;
2924
- generated = extraction.generated;
2925
3508
  }
2926
3509
  let plan = {
2927
3510
  files: [],
@@ -2935,6 +3518,11 @@ async function planPreparedGroup(context, members) {
2935
3518
  };
2936
3519
  let augmented = entry.css;
2937
3520
  const blocked = /* @__PURE__ */ new Map();
3521
+ let candidateAliases;
3522
+ let canonicalized = false;
3523
+ let fontTokens = {};
3524
+ let fontFailures = /* @__PURE__ */ new Set();
3525
+ let composeEntry = () => entry.css;
2938
3526
  finalization: for (;;) {
2939
3527
  const baseIdentities = new Set(atRuleIdentities(`${entry.path}.group-base.css`, [entry.css, ...entry.graphSources.map((graphSource) => graphSource.source)].join("\n")));
2940
3528
  const sheets = active.flatMap((member) => member.stylesheets.map((stylesheet) => ({
@@ -2997,6 +3585,7 @@ async function planPreparedGroup(context, members) {
2997
3585
  utilityPrefix: entry.designSystem.theme.prefix,
2998
3586
  themeTokens: entry.themeTokens,
2999
3587
  ...names ? { mediaNames: names } : {},
3588
+ ...candidateAliases ? { candidateAliases } : {},
3000
3589
  ...groupWritable ? {} : { entryWritable: false },
3001
3590
  files
3002
3591
  };
@@ -3018,7 +3607,11 @@ async function planPreparedGroup(context, members) {
3018
3607
  generated
3019
3608
  } : void 0;
3020
3609
  const used = currentExtraction ? usedGeneratedDefinitions(currentExtraction, plan) : [];
3021
- augmented = currentExtraction ? appendMediaDefinitions(composed, used) : composed;
3610
+ composeEntry = (tokens) => {
3611
+ const withFonts = appendFontTheme(composed, tokens);
3612
+ return currentExtraction ? appendMediaDefinitions(withFonts, used) : withFonts;
3613
+ };
3614
+ augmented = composeEntry(fontTokens);
3022
3615
  let system;
3023
3616
  try {
3024
3617
  system = augmented === entry.css ? entry.designSystem : await entry.loadWith(augmented);
@@ -3041,13 +3634,65 @@ async function planPreparedGroup(context, members) {
3041
3634
  continue planning;
3042
3635
  }
3043
3636
  }
3044
- const failing = invalidCandidates(currentExtraction ? system : entry.designSystem, plan.candidates);
3637
+ const failing = invalidCandidates(system, plan.candidates);
3045
3638
  if (failing.length > 0) {
3046
3639
  if (!accumulateBlockedRules(blocked, plan, failing)) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
3047
3640
  continue planning;
3048
3641
  }
3642
+ if (!canonicalized) {
3643
+ canonicalized = true;
3644
+ const aliases = acceptedCandidateAliases(system, plan.candidateProbes ?? [], reservations);
3645
+ const fonts = await registerFontTokens({
3646
+ system,
3647
+ entryCss: augmented,
3648
+ loadWith: entry.loadWith,
3649
+ themeTokens: entry.themeTokens,
3650
+ probes: plan.fontFamilyProbes ?? [],
3651
+ corpus: [.../* @__PURE__ */ new Set([...plan.candidates, ...plan.candidateProbes ?? []])],
3652
+ reservations,
3653
+ existingAliases: aliases,
3654
+ generate: groupWritable,
3655
+ mentionedSegments,
3656
+ referenceTokens: entry.referenceTokens,
3657
+ globalAllocations: context.fontAllocations
3658
+ });
3659
+ Object.assign(aliases, fonts.aliases);
3660
+ fontTokens = fonts.tokens;
3661
+ fontFailures = fonts.failures;
3662
+ for (const [name, value] of Object.entries(fonts.tokens)) context.fontAllocations.set(name, value);
3663
+ if (Object.keys(aliases).length > 0) {
3664
+ candidateAliases = aliases;
3665
+ continue planning;
3666
+ }
3667
+ }
3049
3668
  break;
3050
3669
  }
3670
+ const ruleStatus = new Map(plan.rules.map((rule) => [`${rule.stylesheet}:${rule.ruleId.start}-${rule.ruleId.end}`, rule.status]));
3671
+ const warned = /* @__PURE__ */ new Set();
3672
+ for (const probe of plan.fontFamilyProbes ?? []) {
3673
+ if (probe.firstFamily.kind !== "name") continue;
3674
+ if (candidateAliases?.[probe.candidate] !== void 0) continue;
3675
+ const ruleKey = `${probe.stylesheet}:${probe.ruleId.start}-${probe.ruleId.end}`;
3676
+ if (ruleStatus.get(ruleKey) !== "retained" || warned.has(ruleKey)) continue;
3677
+ const failed = fontFailures.has(probe.candidate);
3678
+ if (groupWritable && !failed) continue;
3679
+ warned.add(ruleKey);
3680
+ plan.warnings.push({
3681
+ code: groupWritable ? "font-theme-registration-failed" : "font-theme-registration-required",
3682
+ file: sheets[probe.stylesheet].stylesheet.cssPath,
3683
+ start: probe.authoredSpan.start,
3684
+ end: probe.authoredSpan.end,
3685
+ 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."
3686
+ });
3687
+ }
3688
+ if (Object.keys(fontTokens).length > 0) {
3689
+ const applied = new Set(plan.candidates.map((candidate) => utilitySegment(candidate).replace(/!$/, "")));
3690
+ const pruned = Object.fromEntries(Object.entries(fontTokens).filter(([name]) => applied.has(`font-${name}`)));
3691
+ if (Object.keys(pruned).length !== Object.keys(fontTokens).length) {
3692
+ fontTokens = pruned;
3693
+ augmented = composeEntry(pruned);
3694
+ }
3695
+ }
3051
3696
  for (const [index, rules] of blocked) {
3052
3697
  const cssPath = sheets[index].stylesheet.cssPath;
3053
3698
  for (const { authoredSpan, candidates } of rules.values()) {