tw-migrate 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.8.0",
7
+ version: "0.8.1",
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
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { promisify } from "node:util";
3
- import { chmod, lstat, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
4
  import * as z from "zod/mini";
6
5
  import { FILE_HEADERS_ONLY, createTwoFilesPatch } from "diff";
7
6
  import { execFile } from "node:child_process";
7
+ import { chmod, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { parse } from "parse5";
10
10
  //#region src/util/diff.ts
@@ -278,12 +278,6 @@ function isStylesheetModule(path) {
278
278
  function isProjectInput(workspaceRoot, path) {
279
279
  return isWithin(workspaceRoot, path) && !relative(workspaceRoot, path).split(/[\\/]/).includes("node_modules");
280
280
  }
281
- async function rejectSymlinkTarget(path, root) {
282
- for (let current = path; isWithin(root, current); current = dirname(current)) {
283
- if ((await lstat(current)).isSymbolicLink()) throw new Error(`Refusing to migrate a symbolic-link target: ${path}`);
284
- if (current === root) break;
285
- }
286
- }
287
281
  async function* readInBatches(paths) {
288
282
  for (let offset = 0; offset < paths.length; offset += 32) {
289
283
  const batch = paths.slice(offset, offset + 32);
@@ -393,7 +387,6 @@ async function resolveScope(options) {
393
387
  if (explicitStyle && !isStylesheetPath(explicitStyle) && extname(explicitStyle) !== ".vue") throw new TypeError("Only .css, .scss, .sass, .less, and .vue files can be migrated");
394
388
  if (configuredEntry && extname(configuredEntry) !== ".css") throw new TypeError("The Tailwind CSS entry must be a .css file");
395
389
  if (configuredEntry && !(await stat(configuredEntry)).isFile()) throw new TypeError("The Tailwind CSS entry must be a file");
396
- for (const path of [explicitStyle, configuredEntry]) if (path) await rejectSymlinkTarget(path, currentPackage);
397
390
  if (explicitStyle && !isWithin(currentPackage, explicitStyle)) throw new TypeError("The selected stylesheet must belong to the current package");
398
391
  for (const path of [explicitStyle, configuredEntry]) {
399
392
  if (path && !allPaths.includes(path)) allPaths.push(path);
@@ -761,12 +754,17 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
761
754
  const packageJson = tailwindPackageSchema.parse(JSON.parse(await readFile(packagePath, "utf8")));
762
755
  if (!packageJson.version.startsWith("4.")) throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
763
756
  const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
764
- const css = await snapshotFile(snapshots, tailwindCss);
757
+ const inputPaths = /* @__PURE__ */ new Set();
758
+ const readInput = (path) => {
759
+ inputPaths.add(path);
760
+ return snapshotFile(snapshots, path);
761
+ };
762
+ const css = await readInput(tailwindCss);
765
763
  const base = dirname(tailwindCss);
766
764
  const tailwindRoot = dirname(packagePath);
767
765
  const loadModule = async (id, moduleBase) => {
768
766
  const path = createRequire(join(moduleBase, "package.json")).resolve(id);
769
- if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
767
+ if (isProjectInput(workspaceRoot, path)) await readInput(path);
770
768
  const imported = await import(pathToFileURL(path).href);
771
769
  return {
772
770
  path,
@@ -787,7 +785,7 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
787
785
  } else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
788
786
  else path = projectRequire.resolve(id);
789
787
  return {
790
- content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
788
+ content: isProjectInput(workspaceRoot, path) ? await readInput(path) : await readFile(path, "utf8"),
791
789
  base: dirname(path)
792
790
  };
793
791
  };
@@ -819,6 +817,7 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
819
817
  themeTokens,
820
818
  referenceTokens,
821
819
  graphSources,
820
+ inputPaths,
822
821
  loadWith
823
822
  };
824
823
  }
@@ -1355,15 +1354,26 @@ function canonicalCandidate(system, candidate) {
1355
1354
  cache.set(candidate, canonical);
1356
1355
  return canonical;
1357
1356
  }
1358
- function spellingReservations(styleSources, resolvedExtras, resolvedImports = /* @__PURE__ */ new Set()) {
1357
+ function spellingReservations(styleSources, resolvedExtras, resolvedImports = /* @__PURE__ */ new Set(), cache) {
1358
+ const key = JSON.stringify([...resolvedImports].sort());
1359
+ let base = cache?.get(key);
1360
+ if (base === void 0) {
1361
+ base = {
1362
+ names: /* @__PURE__ */ new Set(),
1363
+ prefixes: /* @__PURE__ */ new Set(),
1364
+ unbounded: false,
1365
+ properties: /* @__PURE__ */ new Set(),
1366
+ propertiesUnbounded: false
1367
+ };
1368
+ for (const [path, source] of styleSources) scanStylesheetReservations(path, source, styleSources, base, resolvedImports);
1369
+ cache?.set(key, base);
1370
+ }
1359
1371
  const reservations = {
1360
- names: /* @__PURE__ */ new Set(),
1361
- prefixes: /* @__PURE__ */ new Set(),
1362
- unbounded: false,
1363
- properties: /* @__PURE__ */ new Set(),
1364
- propertiesUnbounded: false
1372
+ ...base,
1373
+ names: new Set(base.names),
1374
+ prefixes: new Set(base.prefixes),
1375
+ properties: new Set(base.properties)
1365
1376
  };
1366
- for (const [path, source] of styleSources) scanStylesheetReservations(path, source, styleSources, reservations, resolvedImports);
1367
1377
  for (const [path, source] of resolvedExtras) try {
1368
1378
  const analysis = stylesheetAnalysis(path, source);
1369
1379
  for (const name of analysis.classNames) reservations.names.add(name);
@@ -1434,25 +1444,32 @@ function sameShape(left, right) {
1434
1444
  }
1435
1445
  function acceptedCandidateAliases(system, probes, reservations) {
1436
1446
  const aliases = {};
1437
- if (reservations.unbounded) return aliases;
1447
+ if (reservations.unbounded || typeof system.canonicalizeCandidates !== "function") return aliases;
1438
1448
  const prefix = system.theme.prefix;
1439
1449
  for (const probe of new Set(probes)) {
1450
+ const compiledProbe = system.candidatesToCss([probe])[0];
1451
+ if (compiledProbe == null) continue;
1452
+ let probeShape;
1453
+ try {
1454
+ probeShape = compiledShape(compiledProbe);
1455
+ } catch {
1456
+ continue;
1457
+ }
1458
+ if (probeShape.referencedProperties.length > 0) continue;
1440
1459
  const canonical = canonicalCandidate(system, probe);
1441
1460
  if (canonical === null) continue;
1442
1461
  const unprefixed = prefix !== null && canonical.startsWith(`${prefix}:`) ? canonical.slice(prefix.length + 1) : null;
1443
1462
  const segment = utilitySegment(canonical).replace(/!$/, "");
1444
1463
  if (segment.includes("[") || reservedSpelling(reservations, canonical) || unprefixed !== null && reservedSpelling(reservations, unprefixed) || reservedSpelling(reservations, segment)) continue;
1445
- const [compiledProbe, compiledCanonical] = system.candidatesToCss([probe, canonical]);
1446
- if (compiledProbe == null || compiledCanonical == null) continue;
1447
- let probeShape;
1464
+ const compiledCanonical = system.candidatesToCss([canonical])[0];
1465
+ if (compiledCanonical == null) continue;
1448
1466
  let canonicalShape;
1449
1467
  try {
1450
- probeShape = compiledShape(compiledProbe);
1451
1468
  canonicalShape = compiledShape(compiledCanonical);
1452
1469
  } catch {
1453
1470
  continue;
1454
1471
  }
1455
- if (canonicalShape.referencedProperties.length > 0 || probeShape.referencedProperties.length > 0 || !sameShape(probeShape.declarations, canonicalShape.declarations)) continue;
1472
+ if (canonicalShape.referencedProperties.length > 0 || !sameShape(probeShape.declarations, canonicalShape.declarations)) continue;
1456
1473
  aliases[probe] = canonical;
1457
1474
  }
1458
1475
  return aliases;
@@ -1745,7 +1762,7 @@ function scannedVariantReservations(sources) {
1745
1762
  */
1746
1763
  function collectMemberMediaConditions(stylesheets, tailwind) {
1747
1764
  return JSON.parse(collectMediaConditions(JSON.stringify({
1748
- stylesheets,
1765
+ stylesheets: stylesheets.map(({ vueShadowCss: _shadowCss, vueShadowModuleCss: _shadowModuleCss, ...stylesheet }) => stylesheet),
1749
1766
  themeTokens: tailwind.themeTokens,
1750
1767
  tailwindSources: tailwind.graphSources
1751
1768
  })));
@@ -2691,7 +2708,6 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
2691
2708
  }
2692
2709
  }
2693
2710
  for (const file of selected) {
2694
- await rejectSymlinkTarget(file.path, packageRoot);
2695
2711
  const analysis = analysisOf(file.path);
2696
2712
  warnings.push(...analysis.warnings);
2697
2713
  if (analysis.retained) continue;
@@ -2784,7 +2800,6 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
2784
2800
  if (analysis.retained) continue;
2785
2801
  const elements = elementsByFile.get(file.path) ?? [];
2786
2802
  const contextPaths = [...new Set(elements.flatMap((element) => element.cssPaths))];
2787
- if (contextPaths.length > 0) await rejectSymlinkTarget(file.path, packageRoot);
2788
2803
  files.set(file.path, {
2789
2804
  ...file,
2790
2805
  ...contextPaths.length > 0 ? {
@@ -2956,6 +2971,8 @@ async function migrate(options = {}) {
2956
2971
  styleDependents: indexStylesheetDependents(styleSources),
2957
2972
  vueStyleRanges,
2958
2973
  entryCatalog: tailwindEntryCatalog(styleSources, scope.pathOwners),
2974
+ loadedTailwind: /* @__PURE__ */ new Map(),
2975
+ stylesheetReservationCache: /* @__PURE__ */ new Map(),
2959
2976
  ignoredPaths: await scannerIgnoredPaths(workspaceRoot, sourceFiles.map((file) => file.path)),
2960
2977
  fontAllocations: /* @__PURE__ */ new Map()
2961
2978
  };
@@ -3194,7 +3211,12 @@ async function preparePackage(context, packageRoot) {
3194
3211
  if (provenTargets.length === 0 && provenVueStylesheets.length === 0) return vueWarningsOnlyResult(preparedVue, proofWarnings);
3195
3212
  let tailwind;
3196
3213
  try {
3197
- tailwind = await loadTailwind(entryOwner, tailwindPath, snapshots, workspaceRoot);
3214
+ const key = `${entryOwner}\0${tailwindPath}`;
3215
+ tailwind = context.loadedTailwind.get(key);
3216
+ if (tailwind === void 0) {
3217
+ tailwind = await loadTailwind(entryOwner, tailwindPath, snapshots, workspaceRoot);
3218
+ context.loadedTailwind.set(key, tailwind);
3219
+ } else for (const path of tailwind.inputPaths) await snapshotFile(snapshots, path);
3198
3220
  } catch (error) {
3199
3221
  return recover(error, isIntegrityError(error));
3200
3222
  }
@@ -3210,7 +3232,6 @@ async function preparePackage(context, packageRoot) {
3210
3232
  stylesheets = [];
3211
3233
  const compilerDependents = /* @__PURE__ */ new Map();
3212
3234
  for (const stylePath of provenTargets.sort()) {
3213
- await rejectSymlinkTarget(stylePath, packageRoot);
3214
3235
  const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith("_");
3215
3236
  const stylesheet = {
3216
3237
  cssPath: stylePath,
@@ -3271,7 +3292,7 @@ async function planPreparedGroup(context, members) {
3271
3292
  return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
3272
3293
  });
3273
3294
  const entry = active[0].tailwind;
3274
- 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"))));
3295
+ 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"))), context.stylesheetReservationCache);
3275
3296
  const groupFiles = new Map(active.flatMap((member) => member.files.map((file) => [file.path, file])));
3276
3297
  const mentionedSegments = /* @__PURE__ */ new Set();
3277
3298
  const mentionedSources = [...[...groupFiles.values()].map((file) => file.source), ...entry.graphSources.map((graphSource) => graphSource.source)];
@@ -3478,7 +3499,7 @@ async function planPreparedGroup(context, members) {
3478
3499
  } });
3479
3500
  active = active.filter((remaining) => remaining.stylesheets.length > 0);
3480
3501
  if (active.length === 0) return results;
3481
- const groupWritable = !await entryUnsafe(entry.path, workspaceRoot, targetable);
3502
+ const groupWritable = targetable.has(entry.path) && isProjectInput(workspaceRoot, entry.path);
3482
3503
  const mediaWarnings = [];
3483
3504
  let extraction;
3484
3505
  const collections = [];
@@ -3580,13 +3601,29 @@ async function planPreparedGroup(context, members) {
3580
3601
  htmlStylesheets: merged
3581
3602
  };
3582
3603
  }).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
3604
+ const shadowSourceIds = /* @__PURE__ */ new Map();
3605
+ const shadowRefs = (sources) => sources?.map((source) => {
3606
+ let id = shadowSourceIds.get(source);
3607
+ if (id === void 0) {
3608
+ id = shadowSourceIds.size;
3609
+ shadowSourceIds.set(source, id);
3610
+ }
3611
+ return id;
3612
+ });
3613
+ const pooledSheets = sheets.map(({ stylesheet }) => ({
3614
+ ...stylesheet,
3615
+ vueShadowCss: shadowRefs(stylesheet.vueShadowCss),
3616
+ vueShadowModuleCss: shadowRefs(stylesheet.vueShadowModuleCss)
3617
+ }));
3618
+ const vueShadowSources = [...shadowSourceIds.keys()];
3583
3619
  planning: for (;;) {
3584
3620
  const request = {
3585
- stylesheets: sheets.map(({ member, stylesheet }, index) => {
3621
+ vueShadowSources,
3622
+ stylesheets: sheets.map(({ member }, index) => {
3586
3623
  const rules = blocked.get(index);
3587
3624
  const movesOff = member.sharedProofs !== void 0 || movesDisabled.has(index);
3588
3625
  return {
3589
- ...stylesheet,
3626
+ ...pooledSheets[index],
3590
3627
  ...rules ? { blockedRules: [...rules.values()].map((blockedRule) => blockedRule.ruleId) } : {},
3591
3628
  ...movesOff ? { globalAtRuleMoves: false } : {}
3592
3629
  };
@@ -3775,10 +3812,6 @@ function accumulateBlockedRules(blocked, plan, failing) {
3775
3812
  }
3776
3813
  return progressed;
3777
3814
  }
3778
- async function entryUnsafe(entryPath, workspaceRoot, targetable) {
3779
- if (!targetable.has(entryPath) || !isProjectInput(workspaceRoot, entryPath)) return true;
3780
- return (await lstat(entryPath)).isSymbolicLink();
3781
- }
3782
3815
  function atRuleIdentities(key, css) {
3783
3816
  return css.trim() === "" ? [] : stylesheetAnalysis(key, css).globalAtRuleIdentities;
3784
3817
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tw-migrate",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
5
5
  "keywords": [
6
6
  "codemod",
@@ -66,11 +66,11 @@
66
66
  "vue": "catalog:"
67
67
  },
68
68
  "optionalDependencies": {
69
- "tw-migrate-darwin-arm64": "0.8.0",
70
- "tw-migrate-darwin-x64": "0.8.0",
71
- "tw-migrate-linux-arm64-gnu": "0.8.0",
72
- "tw-migrate-linux-x64-gnu": "0.8.0",
73
- "tw-migrate-win32-x64-msvc": "0.8.0"
69
+ "tw-migrate-darwin-arm64": "0.8.1",
70
+ "tw-migrate-darwin-x64": "0.8.1",
71
+ "tw-migrate-linux-arm64-gnu": "0.8.1",
72
+ "tw-migrate-linux-x64-gnu": "0.8.1",
73
+ "tw-migrate-win32-x64-msvc": "0.8.1"
74
74
  },
75
75
  "napi": {
76
76
  "binaryName": "tw-migrate",