tw-migrate 0.5.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/README.md CHANGED
@@ -81,6 +81,8 @@ Everything outside this subset is retained and reported with one of the warning
81
81
  | `dynamic-html-attribute` | An HTML attribute is not a safely writable quoted literal, so the element cannot be migrated. |
82
82
  | `dynamic-template-class` | A dynamic class binding makes a Vue template's class set unprovable, so its scoped rules are retained. |
83
83
  | `existing-tailwind-conflict` | A generated utility may conflict with a Tailwind class already on the element. |
84
+ | `font-theme-registration-failed` | A safe Tailwind font theme token could not be registered, so the rule is retained. |
85
+ | `font-theme-registration-required` | The font family requires a Tailwind theme token, but the Tailwind entry cannot be edited, so the rule is retained. |
84
86
  | `inferred-preprocessor-source` | A linked CSS file was matched to a uniquely named preprocessor source file. |
85
87
  | `media-query-definition-fallback` | The Tailwind entry could not be edited safely, so media behavior was preserved with arbitrary variants. |
86
88
  | `module-utilities-conflict` | Utilities generated from different module classes on one element overlap, so their rules are retained. |
package/dist/bin.js CHANGED
@@ -4,7 +4,7 @@ import { styleText } from "node:util";
4
4
  //#region src/bin.ts
5
5
  const { version } = {
6
6
  name: "tw-migrate",
7
- version: "0.5.0",
7
+ version: "0.6.0",
8
8
  description: "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
9
9
  keywords: [
10
10
  "codemod",
package/dist/index.js CHANGED
@@ -64,13 +64,17 @@ function sourceImport(value) {
64
64
  return object(value) && typeof value.specifier === "string" && typeof value.typeOnly === "boolean" && typeof value.dynamic === "boolean";
65
65
  }
66
66
  function sourceAnalysisResult(value) {
67
- return object(value) && Array.isArray(value.imports) && value.imports.every(sourceImport) && Array.isArray(value.staticImports) && value.staticImports.every((item) => typeof item === "string") && Array.isArray(value.defaultImports) && value.defaultImports.every((item) => object(item) && typeof item.source === "string" && typeof item.local === "string") && Array.isArray(value.vueGlobPatterns) && value.vueGlobPatterns.every((item) => typeof item === "string") && Array.isArray(value.useCssModuleLocals) && value.useCssModuleLocals.every((item) => typeof item === "string") && Array.isArray(value.unboundReferences) && value.unboundReferences.every((item) => typeof item === "string") && [
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
68
  value.vueGlobUnverifiable,
69
69
  value.hasDynamicImport,
70
70
  value.hasVueFallthroughMacro,
71
71
  value.usesCssModule,
72
72
  value.hasUnboundUseCssModule,
73
- value.definesRootUseCssModule
73
+ value.definesRootUseCssModule,
74
+ value.stylesheetLinksUnverifiable,
75
+ value.rendersStyleElement,
76
+ value.injectsMarkup,
77
+ value.customPropertiesUnbounded
74
78
  ].every((item) => typeof item === "boolean");
75
79
  }
76
80
  const ANALYSIS_CACHE_LIMIT = 4096;
@@ -84,7 +88,7 @@ function bound(cache) {
84
88
  }
85
89
  }
86
90
  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"));
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"));
88
92
  }
89
93
  function sourceAnalysis(path, source) {
90
94
  const cached = sourceAnalysisCache.get(path);
@@ -97,6 +101,12 @@ function sourceAnalysis(path, source) {
97
101
  bound(sourceAnalysisCache);
98
102
  return analysis;
99
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
+ }
100
110
  function stylesheetAnalysis(path, source) {
101
111
  const cached = stylesheetAnalysisCache.get(path);
102
112
  if (cached?.source === source) return cached.analysis;
@@ -104,7 +114,7 @@ function stylesheetAnalysis(path, source) {
104
114
  value.unverifiable,
105
115
  value.scopeEscapesUnverifiable,
106
116
  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");
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");
108
118
  stylesheetAnalysisCache.set(path, {
109
119
  source,
110
120
  analysis
@@ -348,6 +358,11 @@ function stylesheetReferenceTargets(importer, reference, styleSources) {
348
358
  join(target, `index${syntax}`)
349
359
  ])).filter((path) => styleSources.has(path));
350
360
  }
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
+ }
351
366
  function cssImports(path, source) {
352
367
  return stylesheetAnalysis(path, source).imports;
353
368
  }
@@ -510,6 +525,9 @@ async function collectFiles(root, include) {
510
525
  //#endregion
511
526
  //#region src/parser/html.ts
512
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;
513
531
  function parseHtmlSource(path, source) {
514
532
  const errors = [];
515
533
  const document = parse(source, {
@@ -524,6 +542,9 @@ function parseHtmlSource(path, source) {
524
542
  const bases = [];
525
543
  const elements = [];
526
544
  const dynamicAttributes = [];
545
+ const dynamicClassValues = [];
546
+ const styleAttributeValues = [];
547
+ const handlerTexts = [];
527
548
  const scriptTexts = [];
528
549
  let hasStyle = false;
529
550
  const unquoted = (attribute) => source[attribute.start - 1] !== "\"" && source[attribute.start - 1] !== "'";
@@ -534,6 +555,9 @@ function parseHtmlSource(path, source) {
534
555
  }
535
556
  if (node.tagName) {
536
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);
537
561
  const locations = node.sourceCodeLocation?.attrs;
538
562
  if (locations) {
539
563
  if (node.tagName === "link" && stylesheetRel(attributes.get("rel")) && !attributes.has("disabled")) {
@@ -560,15 +584,20 @@ function parseHtmlSource(path, source) {
560
584
  }
561
585
  let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
562
586
  const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
563
- const dynamic = (!classAttribute && idAttribute && locations.class ? {
587
+ const unparsedClass = !classAttribute && idAttribute && locations.class ? {
564
588
  start: locations.class.startOffset,
565
589
  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) {
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) {
572
601
  if (!classAttribute && idAttribute) {
573
602
  const startTag = node.sourceCodeLocation?.startTag;
574
603
  if (startTag && source[startTag.endOffset - 1] === ">") {
@@ -599,6 +628,9 @@ function parseHtmlSource(path, source) {
599
628
  elements,
600
629
  dynamicAttributes
601
630
  }),
631
+ dynamicClassValues,
632
+ styleAttributeValues,
633
+ handlerText: handlerTexts.join(";\n"),
602
634
  scriptText: scriptTexts.join("\n"),
603
635
  hasStyle
604
636
  };
@@ -778,8 +810,10 @@ function scanProof(options) {
778
810
  } else if (directive.kind === "import" && directive.tailwind) {
779
811
  if (directive.sourceUnreadable) return null;
780
812
  if (directive.source === "none") automaticDisabled = true;
781
- else if (directive.source !== null) if (coversChild(directive.source)) literal = true;
782
- else automaticDisabled = true;
813
+ else if (directive.source !== null) {
814
+ if (coversChild(directive.source)) literal = true;
815
+ else automaticDisabled = true;
816
+ }
783
817
  }
784
818
  }
785
819
  if (literal) return "literal";
@@ -965,8 +999,8 @@ function htmlLinksEntry(file, entry) {
965
999
  return parsed.links.some((link) => {
966
1000
  const media = link.media.trim().toLowerCase();
967
1001
  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;
1002
+ const href = localHrefTarget(link.href);
1003
+ if (href === void 0) return false;
970
1004
  return resolve(dirname(file.path), href) === entry;
971
1005
  });
972
1006
  } catch {
@@ -1142,8 +1176,8 @@ function addInferredPreprocessorContext(state) {
1142
1176
  return true;
1143
1177
  }
1144
1178
  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;
1179
+ const path = localHrefTarget(reference);
1180
+ if (path === void 0) return void 0;
1147
1181
  let decoded;
1148
1182
  try {
1149
1183
  decoded = decodeURIComponent(path);
@@ -1179,7 +1213,7 @@ function htmlWarning(code, file, start, end, message) {
1179
1213
  //#endregion
1180
1214
  //#region src/tailwind.ts
1181
1215
  function findTailwindEntries(stylePaths, styleSources) {
1182
- return stylePaths.filter((path) => extname(path) === ".css" && importSpecifiers(styleSources.get(path) ?? "").some((specifier) => specifier === "tailwindcss" || specifier.startsWith("tailwindcss/")));
1216
+ return stylePaths.filter((path) => extname(path) === ".css" && importEntries(styleSources.get(path) ?? "").some(({ specifier }) => specifier === "tailwindcss" || specifier.startsWith("tailwindcss/")));
1183
1217
  }
1184
1218
  function selectTailwindEntry(entries, configuredPath) {
1185
1219
  if (configuredPath) return configuredPath;
@@ -1226,9 +1260,10 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
1226
1260
  };
1227
1261
  const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
1228
1262
  const graphSources = [];
1263
+ const referenceTokens = /* @__PURE__ */ new Set();
1229
1264
  const themeTokens = {
1230
1265
  ...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
1231
- ...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources)
1266
+ ...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources, referenceTokens)
1232
1267
  };
1233
1268
  graphSources.push({
1234
1269
  path: tailwindCss,
@@ -1249,6 +1284,7 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
1249
1284
  css,
1250
1285
  path: tailwindCss,
1251
1286
  themeTokens,
1287
+ referenceTokens,
1252
1288
  graphSources,
1253
1289
  loadWith
1254
1290
  };
@@ -1256,12 +1292,15 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
1256
1292
  function extractThemeTokens(key, css) {
1257
1293
  return stylesheetAnalysis(key, css).themeTokens;
1258
1294
  }
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] : []);
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
+ }] : []);
1261
1300
  }
1262
- async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, seen = /* @__PURE__ */ new Set()) {
1301
+ async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, referenceTokens, inReference = false, seen = /* @__PURE__ */ new Set()) {
1263
1302
  const tokens = {};
1264
- for (const spec of importSpecifiers(css)) {
1303
+ for (const { specifier: spec, reference } of importEntries(css)) {
1265
1304
  const key = `${base}\0${spec}`;
1266
1305
  if (seen.has(key)) continue;
1267
1306
  seen.add(key);
@@ -1270,15 +1309,301 @@ async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet,
1270
1309
  path: key,
1271
1310
  source: loaded.content
1272
1311
  });
1273
- Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, seen));
1312
+ Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, referenceTokens, inReference || reference, seen));
1274
1313
  }
1275
- return Object.assign(tokens, extractThemeTokens(`${identity}.theme.css`, css));
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);
1276
1317
  }
1277
1318
  function invalidCandidates(system, candidates) {
1278
1319
  const generated = system.candidatesToCss(candidates);
1279
1320
  return candidates.filter((_, index) => generated[index] === null);
1280
1321
  }
1281
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
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
1591
+ };
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
1282
1607
  //#region src/plan/media.ts
1283
1608
  const GENERATED_NAMESPACE = "twm-media";
1284
1609
  /**
@@ -1535,7 +1860,9 @@ function analyzeVueSource(compiler, path, source) {
1535
1860
  const { descriptor, errors } = compiler.parse(source, { filename: path });
1536
1861
  const styleRanges = descriptor.styles.map((style) => ({
1537
1862
  start: style.loc.start.offset,
1538
- end: style.loc.end.offset
1863
+ end: style.loc.end.offset,
1864
+ lang: style.lang ?? "css",
1865
+ ...style.src !== void 0 ? { src: style.src } : {}
1539
1866
  }));
1540
1867
  const byteRanges = (offset) => ({
1541
1868
  warnings: warnings.map((warning) => ({
@@ -1544,6 +1871,7 @@ function analyzeVueSource(compiler, path, source) {
1544
1871
  end: offset(warning.end)
1545
1872
  })),
1546
1873
  styleRanges: styleRanges.map((range) => ({
1874
+ ...range,
1547
1875
  start: offset(range.start),
1548
1876
  end: offset(range.end)
1549
1877
  }))
@@ -1684,6 +2012,14 @@ function analyzeVueSource(compiler, path, source) {
1684
2012
  moduleClosureBroken: false,
1685
2013
  referencesUseCssModule: false,
1686
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,
1687
2023
  expressionPath: scriptLang === "ts" || scriptLang === "tsx" ? "Component.ts" : "Component.js"
1688
2024
  };
1689
2025
  visitTemplateNode(source, template.ast, state);
@@ -1699,8 +2035,11 @@ function analyzeVueSource(compiler, path, source) {
1699
2035
  let setupAnalysis;
1700
2036
  let scriptUsesCssModule = false;
1701
2037
  let scriptHasDynamicImport = false;
2038
+ const scriptCustomProperties = [];
2039
+ let scriptCustomPropertiesUnbounded = false;
1702
2040
  const scriptVueReferences = [];
1703
2041
  const scriptVueGlobPatterns = [];
2042
+ const scriptTemplatePrefixes = [];
1704
2043
  let scriptVueGlobUnverifiable = false;
1705
2044
  const styleImports = [descriptor.script, descriptor.scriptSetup].flatMap((script) => {
1706
2045
  if (!script) return [];
@@ -1716,6 +2055,9 @@ function analyzeVueSource(compiler, path, source) {
1716
2055
  scriptImports.push(...analysis.imports);
1717
2056
  scriptVueReferences.push(...analysis.imports.filter((record) => !record.typeOnly).map((record) => record.specifier));
1718
2057
  scriptVueGlobPatterns.push(...analysis.vueGlobPatterns);
2058
+ scriptTemplatePrefixes.push(...analysis.templatePrefixes);
2059
+ scriptCustomProperties.push(...analysis.customProperties);
2060
+ if (analysis.customPropertiesUnbounded) scriptCustomPropertiesUnbounded = true;
1719
2061
  if (analysis.vueGlobUnverifiable) scriptVueGlobUnverifiable = true;
1720
2062
  return analysis.staticImports;
1721
2063
  } catch {
@@ -1790,6 +2132,14 @@ function analyzeVueSource(compiler, path, source) {
1790
2132
  scriptVueReferences,
1791
2133
  scriptVueGlobPatterns,
1792
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,
1793
2143
  hasOpaqueStyleBlocks: opaqueStyleBlocks,
1794
2144
  styleBlockImports: styleBlockImports.map((entry) => ({
1795
2145
  ...entry,
@@ -1822,6 +2172,29 @@ function verifyVueSource(compiler, path, source, includeUnscoped = false) {
1822
2172
  function visitTemplateNode(source, node, state) {
1823
2173
  if (node.type === NODE_ELEMENT) {
1824
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
+ }
1825
2198
  const bindingClasses = [];
1826
2199
  let classOpaque = false;
1827
2200
  let moduleBinding;
@@ -1851,11 +2224,15 @@ function visitTemplateNode(source, node, state) {
1851
2224
  if (!provenModuleExpression && expression?.usesCssModule) state.moduleClosureBroken = true;
1852
2225
  if (expression?.referencesUseCssModule) state.referencesUseCssModule = true;
1853
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;
1854
2230
  if (prop.arg && !prop.arg.isStatic && prop.arg.content) {
1855
2231
  const argExpression = templateExpression(state.expressionPath, prop.arg.content);
1856
2232
  state.moduleClosureBroken ||= argExpression?.usesCssModule ?? true;
1857
2233
  state.referencesUseCssModule ||= argExpression?.referencesUseCssModule ?? false;
1858
2234
  for (const name of argExpression?.references ?? []) state.expressionReferences.add(name);
2235
+ for (const prefix of argExpression?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
1859
2236
  }
1860
2237
  if (prop.name === "html") state.vHtml = true;
1861
2238
  }
@@ -1875,6 +2252,7 @@ function visitTemplateNode(source, node, state) {
1875
2252
  state.moduleClosureBroken ||= interpolation?.usesCssModule ?? true;
1876
2253
  state.referencesUseCssModule ||= interpolation?.referencesUseCssModule ?? false;
1877
2254
  for (const name of interpolation?.references ?? []) state.expressionReferences.add(name);
2255
+ for (const prefix of interpolation?.templatePrefixes ?? []) state.templatePrefixes.add(prefix);
1878
2256
  }
1879
2257
  for (const child of node.children ?? []) visitTemplateNode(source, child, state);
1880
2258
  }
@@ -1899,18 +2277,24 @@ function templateHandler(source) {
1899
2277
  vueModuleMember: null,
1900
2278
  usesCssModule: analysis.usesCssModule,
1901
2279
  referencesUseCssModule: analysis.hasUnboundUseCssModule,
1902
- references: analysis.unboundReferences
2280
+ references: analysis.unboundReferences,
2281
+ templatePrefixes: analysis.templatePrefixes,
2282
+ customProperties: analysis.customProperties,
2283
+ customPropertiesUnbounded: analysis.customPropertiesUnbounded
1903
2284
  };
1904
2285
  } catch {
1905
2286
  return;
1906
2287
  }
1907
2288
  }
1908
2289
  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) {
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
+ }
1914
2298
  state.dynamic = true;
1915
2299
  return { idAttribute };
1916
2300
  }
@@ -1931,8 +2315,10 @@ function templateSite(source, node, bindingClasses, classOpaque, state) {
1931
2315
  matchClasses
1932
2316
  };
1933
2317
  }
1934
- function literalAttribute(source, node, name) {
1935
- 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) {
1936
2322
  if (!prop?.value) return void 0;
1937
2323
  const start = prop.value.loc.start.offset;
1938
2324
  const end = prop.value.loc.end.offset;
@@ -2404,7 +2790,16 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
2404
2790
  htmlScriptText: analysis.scriptText
2405
2791
  } : {},
2406
2792
  sourceImports: analysis.scriptImports,
2407
- sourceImportsUnverifiable: analysis.scriptImportsUnverifiable
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
2408
2803
  });
2409
2804
  }
2410
2805
  return {
@@ -2538,7 +2933,8 @@ async function migrate(options = {}) {
2538
2933
  styleDependents: indexStylesheetDependents(styleSources),
2539
2934
  vueStyleRanges,
2540
2935
  entryCatalog: tailwindEntryCatalog(styleSources, scope.pathOwners),
2541
- ignoredPaths: options.workspaces ? await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)) : /* @__PURE__ */ new Set()
2936
+ ignoredPaths: await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)),
2937
+ fontAllocations: /* @__PURE__ */ new Map()
2542
2938
  };
2543
2939
  const failures = [];
2544
2940
  const plans = [];
@@ -2555,6 +2951,14 @@ async function migrate(options = {}) {
2555
2951
  members.push(preparation);
2556
2952
  groups.set(preparation.tailwind.path, members);
2557
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
+ }
2558
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);
2559
2963
  else if (result.plan) plans.push(result.plan);
2560
2964
  const originals = new Map([...styleSources, ...sourceFiles.map((file) => [file.path, file.source])]);
@@ -2847,6 +3251,174 @@ async function planPreparedGroup(context, members) {
2847
3251
  return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
2848
3252
  });
2849
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);
3262
+ };
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
+ }
3280
+ }
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;
3367
+ }
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;
3373
+ continue;
3374
+ }
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);
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;
3397
+ }
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;
3420
+ }
3421
+ }
2850
3422
  const recoverGroup = (error, fatal = false) => {
2851
3423
  if (!options.force || fatal) throw error;
2852
3424
  for (const member of active) results.push({ failure: packageFailure(workspaceRoot, member.packageRoot, error) });
@@ -2900,28 +3472,30 @@ async function planPreparedGroup(context, members) {
2900
3472
  if (active.length === 0) return results;
2901
3473
  let names = extraction?.names;
2902
3474
  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)
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."
2919
3485
  });
2920
- } catch (error) {
2921
- return recoverGroup(error, !isRecoverablePlanningError(error));
3486
+ } else {
3487
+ try {
3488
+ extraction = planMediaExtraction({
3489
+ collections,
3490
+ tailwind: entry,
3491
+ scannedSources: context.sourceFiles.map((file) => file.source)
3492
+ });
3493
+ } catch (error) {
3494
+ return recoverGroup(error, !isRecoverablePlanningError(error));
3495
+ }
3496
+ names = extraction.names;
3497
+ generated = extraction.generated;
2922
3498
  }
2923
- names = extraction.names;
2924
- generated = extraction.generated;
2925
3499
  }
2926
3500
  let plan = {
2927
3501
  files: [],
@@ -2935,6 +3509,11 @@ async function planPreparedGroup(context, members) {
2935
3509
  };
2936
3510
  let augmented = entry.css;
2937
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;
2938
3517
  finalization: for (;;) {
2939
3518
  const baseIdentities = new Set(atRuleIdentities(`${entry.path}.group-base.css`, [entry.css, ...entry.graphSources.map((graphSource) => graphSource.source)].join("\n")));
2940
3519
  const sheets = active.flatMap((member) => member.stylesheets.map((stylesheet) => ({
@@ -2997,6 +3576,7 @@ async function planPreparedGroup(context, members) {
2997
3576
  utilityPrefix: entry.designSystem.theme.prefix,
2998
3577
  themeTokens: entry.themeTokens,
2999
3578
  ...names ? { mediaNames: names } : {},
3579
+ ...candidateAliases ? { candidateAliases } : {},
3000
3580
  ...groupWritable ? {} : { entryWritable: false },
3001
3581
  files
3002
3582
  };
@@ -3018,7 +3598,11 @@ async function planPreparedGroup(context, members) {
3018
3598
  generated
3019
3599
  } : void 0;
3020
3600
  const used = currentExtraction ? usedGeneratedDefinitions(currentExtraction, plan) : [];
3021
- augmented = currentExtraction ? appendMediaDefinitions(composed, used) : composed;
3601
+ composeEntry = (tokens) => {
3602
+ const withFonts = appendFontTheme(composed, tokens);
3603
+ return currentExtraction ? appendMediaDefinitions(withFonts, used) : withFonts;
3604
+ };
3605
+ augmented = composeEntry(fontTokens);
3022
3606
  let system;
3023
3607
  try {
3024
3608
  system = augmented === entry.css ? entry.designSystem : await entry.loadWith(augmented);
@@ -3041,13 +3625,65 @@ async function planPreparedGroup(context, members) {
3041
3625
  continue planning;
3042
3626
  }
3043
3627
  }
3044
- const failing = invalidCandidates(currentExtraction ? system : entry.designSystem, plan.candidates);
3628
+ const failing = invalidCandidates(system, plan.candidates);
3045
3629
  if (failing.length > 0) {
3046
3630
  if (!accumulateBlockedRules(blocked, plan, failing)) throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
3047
3631
  continue planning;
3048
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
+ }
3049
3659
  break;
3050
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);
3671
+ plan.warnings.push({
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
+ }
3051
3687
  for (const [index, rules] of blocked) {
3052
3688
  const cssPath = sheets[index].stylesheet.cssPath;
3053
3689
  for (const { authoredSpan, candidates } of rules.values()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tw-migrate",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
5
5
  "keywords": [
6
6
  "codemod",
@@ -62,11 +62,11 @@
62
62
  "vue": "catalog:"
63
63
  },
64
64
  "optionalDependencies": {
65
- "tw-migrate-darwin-arm64": "0.5.0",
66
- "tw-migrate-darwin-x64": "0.5.0",
67
- "tw-migrate-linux-arm64-gnu": "0.5.0",
68
- "tw-migrate-linux-x64-gnu": "0.5.0",
69
- "tw-migrate-win32-x64-msvc": "0.5.0"
65
+ "tw-migrate-darwin-arm64": "0.6.0",
66
+ "tw-migrate-darwin-x64": "0.6.0",
67
+ "tw-migrate-linux-arm64-gnu": "0.6.0",
68
+ "tw-migrate-linux-x64-gnu": "0.6.0",
69
+ "tw-migrate-win32-x64-msvc": "0.6.0"
70
70
  },
71
71
  "napi": {
72
72
  "binaryName": "tw-migrate",