tw-migrate 0.6.0 → 0.7.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/README.md +13 -131
- package/dist/bin.js +9 -3
- package/dist/index.js +190 -170
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -1,144 +1,26 @@
|
|
|
1
1
|
# tw-migrate
|
|
2
2
|
|
|
3
|
-
Migrate static React/Next.js, Vue 3 SFC, and HTML stylesheet
|
|
3
|
+
Migrate static React/Next.js, Vue 3 SFC, and HTML stylesheet usage to Tailwind CSS v4 utilities. Supports CSS, SCSS, Sass, Less, and CSS Modules.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Install and configure Tailwind CSS v4 in the package you want to migrate by following the [official installation guide](https://tailwindcss.com/docs/installation/). The package must have a CSS entry that loads Tailwind CSS, usually with `@import "tailwindcss";`, so `tw-migrate` can validate generated utilities against the project's Tailwind configuration.
|
|
5
|
+
Requires Node.js 22.23.2+ and [Tailwind CSS v4 configured in your project](https://tailwindcss.com/docs/installation/) with a CSS entry such as `@import "tailwindcss";`. For Sass or Less, install the compiler in the project you are migrating.
|
|
8
6
|
|
|
9
7
|
## Usage
|
|
10
8
|
|
|
11
|
-
Run
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx tw-migrate # Migrate the current package
|
|
15
|
-
npx tw-migrate --dry-run # Preview the current package
|
|
16
|
-
npx tw-migrate path/to/Button.module.scss # Migrate one stylesheet
|
|
17
|
-
npx tw-migrate path/to/Button.module.scss --dry-run # Preview one stylesheet
|
|
18
|
-
npx tw-migrate --workspaces # Migrate every package
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
`pnpm dlx tw-migrate` and `yarn dlx tw-migrate` take the same arguments.
|
|
22
|
-
|
|
23
|
-
The CLI applies changes by default; `--dry-run` prints the diff without touching files. Pass `--tailwind-css path/to/globals.css` when the current package has multiple Tailwind entries. `--force` skips package groups that fail discovery or input parsing; plan-integrity and write failures always stop the run.
|
|
24
|
-
|
|
25
|
-
Unmatched media conditions are extracted into named `@custom-variant` definitions in the Tailwind entry and consumers receive stacked variants such as `screen:width-lte-700px:m-0`; verified built-ins and existing breakpoints are reused instead of redefined, and a second run recognizes the first run's definitions. Pass `--no-extract-media-queries` to keep the arbitrary-variant form instead.
|
|
26
|
-
|
|
27
|
-
## Run from a clone
|
|
28
|
-
|
|
29
|
-
The migration planner is a native addon, so a clone needs one build before the CLI runs:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
vp install # or: pnpm install
|
|
33
|
-
vp run build:debug # compiles the addon for your platform
|
|
34
|
-
node src/bin.ts --help
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Then use `node src/bin.ts` wherever the commands above use `npx tw-migrate`. The CLI always migrates the package it is run from, so to migrate a different project, run the built entrypoint by absolute path from that project's directory:
|
|
9
|
+
Run from the package you want to migrate:
|
|
38
10
|
|
|
39
11
|
```bash
|
|
40
|
-
|
|
41
|
-
|
|
12
|
+
npx tw-migrate --dry-run # Preview changes
|
|
13
|
+
npx tw-migrate # Apply changes
|
|
14
|
+
npx tw-migrate path/to/Button.module.scss # Migrate one stylesheet
|
|
15
|
+
npx tw-migrate --workspaces # Migrate all packages
|
|
16
|
+
npx tw-migrate --help # See all options
|
|
42
17
|
```
|
|
43
18
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- `.css`, `.scss`, `.sass`, and `.less` stylesheets
|
|
47
|
-
- SCSS/Sass/Less values evaluated with the target project's installed compiler; ambiguous mixin and partial origins are retained
|
|
48
|
-
- `.js`, `.jsx`, `.ts`, and `.tsx` source files
|
|
49
|
-
- static `.html` literal `class`/`id` attributes scoped by local external stylesheet links (link-level `print` media supported; other link media conditions are retained)
|
|
50
|
-
- Vue 3 `.vue` files: literal template `class`/`id` attributes and static string/template `:class` bindings matched against inline CSS, SCSS, Sass, and Less `<style scoped>` blocks plus statically imported stylesheets, parsed and compiled with the target project's dependencies
|
|
51
|
-
- direct `<script setup>` component imports can prove child-root and caller class surfaces across SFCs; unresolved components, exported roots, non-static bindings, and callers outside that graph retain scoped rules
|
|
52
|
-
- unscoped Vue styles migrate only in private single-source packages where their global usage is closed; broader unscoped surfaces retain with warnings
|
|
53
|
-
- Vue `<style module>` blocks migrate against direct `:class="$style.x"` member bindings; any other `$style`/`useCssModule` appearance or a named module retains the block
|
|
54
|
-
- direct CSS Module members, static template literals, and static expression literals
|
|
55
|
-
- global `className` and `id` literals
|
|
56
|
-
- React/Next.js `className` logical (`&&`, `||`, `??`) and conditional (`?:`) expressions: string and direct CSS Module member result leaves migrate without evaluating their conditions, `null`/`undefined`/`false`/empty-string results are warning-free, and unsupported result leaves retain with a warning while supported siblings still migrate
|
|
57
|
-
- multi-compound CSS Module selectors whose element relationships are proven from the JSX graph
|
|
58
|
-
- common state pseudo-classes, global arbitrary descendant variants, and conditional at-rules (`@media`, `@supports`, `@container`, `@starting-style`)
|
|
59
|
-
- `and`-joined media conditions decompose into stacked named variants with definitions extracted into the Tailwind entry; ordering-sensitive conflicts between overlapping conditions are retained unless the original winner is provable
|
|
60
|
-
- in `--workspaces` mode a package without its own Tailwind entry may reuse a proven ancestor entry, packages sharing one entry plan as a single group with one composed entry edit, and cross-package consumers must provably load the shared entry
|
|
61
|
-
- the tier-1 property mapping families with shorthand/longhand normalization
|
|
62
|
-
- exact Tailwind theme tokens and breakpoints with arbitrary-value fallback
|
|
63
|
-
- generated candidates are compiled against the project's Tailwind entry; failures retain the source rule
|
|
64
|
-
- batch migration of every stylesheet in a package, and `--workspaces` runs across packages
|
|
65
|
-
- CSS Module cleanup when every reference is safely migrated
|
|
66
|
-
|
|
67
|
-
Everything outside this subset is retained and reported with one of the warning codes below.
|
|
68
|
-
|
|
69
|
-
## Warning codes
|
|
70
|
-
|
|
71
|
-
| Code | Meaning |
|
|
72
|
-
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
73
|
-
| `aliased-css-module-reference` | A CSS Module class is aliased to a local binding, so the module is retained. |
|
|
74
|
-
| `batch-stylesheet-conflict` | Utilities generated from different stylesheets, or under overlapping media conditions with an unprovable order, conflict on the same element, so the contributing rule is retained. |
|
|
75
|
-
| `candidate-compilation-failure` | A generated candidate did not compile under the project's Tailwind entry, so its rule is retained. |
|
|
76
|
-
| `component-class-target` | A child component's root element can carry classes a Vue scoped rule matches, so the rule is retained. |
|
|
77
|
-
| `computed-css-module-reference` | A computed CSS Module access cannot be verified, so the module is retained. |
|
|
78
|
-
| `cross-package-stylesheet-link` | A linked stylesheet is owned by another package, so it is not analyzed outside workspace mode. |
|
|
79
|
-
| `css-module-composes` | The rule uses or is targeted by `composes`, so it is retained. |
|
|
80
|
-
| `dynamic-class-name` | A `className` value or expression result is dynamic, so that class site cannot be migrated. |
|
|
81
|
-
| `dynamic-html-attribute` | An HTML attribute is not a safely writable quoted literal, so the element cannot be migrated. |
|
|
82
|
-
| `dynamic-template-class` | A dynamic class binding makes a Vue template's class set unprovable, so its scoped rules are retained. |
|
|
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. |
|
|
86
|
-
| `inferred-preprocessor-source` | A linked CSS file was matched to a uniquely named preprocessor source file. |
|
|
87
|
-
| `media-query-definition-fallback` | The Tailwind entry could not be edited safely, so media behavior was preserved with arbitrary variants. |
|
|
88
|
-
| `module-utilities-conflict` | Utilities generated from different module classes on one element overlap, so their rules are retained. |
|
|
89
|
-
| `non-classname-css-module-reference` | A CSS Module class is used outside a supported `className`, so the module is retained. |
|
|
90
|
-
| `open-root-fallthrough` | A parent component can merge classes onto a Vue SFC's single root element, so its scoped rules are retained. |
|
|
91
|
-
| `preprocessor-style-block` | A Vue `<style>` block uses a preprocessor language outside the supported SCSS, Sass, and Less set. |
|
|
92
|
-
| `rebuild-required` | A preprocessor entry was migrated; rebuild it to refresh its generated CSS. |
|
|
93
|
-
| `reference-only-css-module-consumer` | A reference-only (non-writable) source uses the CSS Module, so it is retained. |
|
|
94
|
-
| `retained-global-rule` | Global CSS is never deleted automatically. |
|
|
95
|
-
| `shadowed-scoped-rule` | Other package CSS also targets a class a Vue scoped rule matches, so the rule is retained to preserve the cascade. |
|
|
96
|
-
| `shared-preprocessor-source` | A Sass partial must be analyzed through every consuming entry, so it is retained. |
|
|
97
|
-
| `unproven-css-module-relationship` | A compound selector's element relationship could not be proven for every usage. |
|
|
98
|
-
| `unproven-script-reference` | An inline script names a CSS Module class, so the module is retained. |
|
|
99
|
-
| `unproven-shared-entry-flow` | A consumer flow could not be proven to load the shared Tailwind entry, so the stylesheet is retained. |
|
|
100
|
-
| `unproven-source-map` | A generated rule does not map uniquely to one authored source rule, so it is retained. |
|
|
101
|
-
| `unresolved-selector-target` | No exclusively supported `className` references were found for the rule. |
|
|
102
|
-
| `unscoped-style-block` | An unscoped Vue style does not have a closed private single-source usage surface, so it is retained. |
|
|
103
|
-
| `unsupported-animation` | The animation references keyframes that cannot be converted. |
|
|
104
|
-
| `unsupported-at-rule` | The rule contains or sits inside an at-rule outside the supported set. |
|
|
105
|
-
| `unsupported-container-query` | The `@container` condition has no Tailwind variant equivalent. |
|
|
106
|
-
| `unsupported-css-module-reference` | The CSS Module has an import or reference that cannot be migrated safely. |
|
|
107
|
-
| `unsupported-declaration` | A declaration is outside the supported property subset. |
|
|
108
|
-
| `unsupported-html-base` | A remote or unrepresentable base URL prevents safe stylesheet link resolution. |
|
|
109
|
-
| `unsupported-html-stylesheet-link` | Only local package stylesheet links are analyzed. |
|
|
110
|
-
| `unsupported-important` | `!important` declarations are not migrated. |
|
|
111
|
-
| `unsupported-link-media` | A stylesheet link or `@import` media condition cannot be represented safely. |
|
|
112
|
-
| `unsupported-media-query` | The `@media` condition has no Tailwind variant equivalent. |
|
|
113
|
-
| `unsupported-nested-at-rule` | A nested conditional at-rule could not be fully converted. |
|
|
114
|
-
| `unsupported-overlap` | Shorthand and longhand declarations overlap in a way that cannot be normalized. |
|
|
115
|
-
| `unsupported-rule-content` | The rule contains non-declaration content that cannot be converted. |
|
|
116
|
-
| `unsupported-selector` | The selector is outside the supported subset. |
|
|
117
|
-
| `unsupported-sfc-block` | A Vue SFC or one of its blocks cannot be analyzed safely, so it is retained. |
|
|
118
|
-
| `unsupported-starting-style` | The `@starting-style` condition could not be converted. |
|
|
119
|
-
| `unsupported-supports-query` | The `@supports` condition has no Tailwind variant equivalent. |
|
|
120
|
-
| `unsupported-value` | A declaration value cannot be represented as a Tailwind utility. |
|
|
121
|
-
| `unsupported-vue-version` | The project's Vue version is not Vue 3, so its SFCs are retained. |
|
|
122
|
-
|
|
123
|
-
See the [core RFC](./rfcs/css-to-tailwind-migration-cli.md) and [preprocessor/HTML RFC](./rfcs/preprocessor-and-html-migration.md) for the complete design and remaining scope.
|
|
124
|
-
|
|
125
|
-
## Testing the packaged CLI
|
|
126
|
-
|
|
127
|
-
Run `vp run test:snapshots` to build, pack, install, and test the published CLI shape against the cross-platform snapshot corpus. The command uses the npm registry and performs one shared install per test process. See [`crates/snapshots/README.md`](./crates/snapshots/README.md) for targeted runs and snapshot review commands.
|
|
128
|
-
|
|
129
|
-
## Testing browser ecosystem compatibility
|
|
130
|
-
|
|
131
|
-
Build the current-platform package and install Chromium before a focused browser run:
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
vp run build
|
|
135
|
-
vp run artifacts
|
|
136
|
-
vp exec playwright install chromium
|
|
137
|
-
vp run test:ecosystem --case react-vite-css
|
|
138
|
-
```
|
|
19
|
+
`pnpm dlx tw-migrate` and `yarn dlx tw-migrate` accept the same arguments.
|
|
139
20
|
|
|
140
|
-
|
|
21
|
+
**The CLI writes files by default.** Review the preview before applying. Unsupported or ambiguous rules stay in place with [warnings](https://github.com/jong-kyung/tw-migrate/blob/main/docs/warnings.md). Use `--tailwind-css path/to/globals.css` to select an entry when discovery is ambiguous.
|
|
141
22
|
|
|
142
|
-
|
|
23
|
+
## More
|
|
143
24
|
|
|
144
|
-
|
|
25
|
+
- [Supported behavior and design](https://github.com/jong-kyung/tw-migrate/tree/main/rfcs/)
|
|
26
|
+
- [Contributing and local development](https://github.com/jong-kyung/tw-migrate/blob/main/CONTRIBUTING.md)
|
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.
|
|
7
|
+
version: "0.7.1",
|
|
8
8
|
description: "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
|
|
9
9
|
keywords: [
|
|
10
10
|
"codemod",
|
|
@@ -36,16 +36,22 @@ const { version } = {
|
|
|
36
36
|
"artifacts": "node -e \"const fs=require('node:fs'); const files=fs.readdirSync('.').filter((file)=>file.startsWith('tw-migrate.')&&file.endsWith('.node')); if(files.length===0) throw new Error('no tw-migrate.*.node artifact in the package root; run build first'); for(const file of files) fs.copyFileSync(file, 'npm/'+file.slice('tw-migrate.'.length, -'.node'.length)+'/'+file)\"",
|
|
37
37
|
"test": "cargo test && pnpm build:debug && vp test",
|
|
38
38
|
"test:ecosystem": "node ecosystem-ci/run.ts",
|
|
39
|
+
"knip": "knip",
|
|
39
40
|
"typecheck": "tsc --noEmit",
|
|
40
41
|
"snapshots:prepare": "node -e \"const fs=require('node:fs'); for(const dir of fs.readdirSync('npm')) for(const file of fs.readdirSync('npm/'+dir)) if(file.endsWith('.node')) fs.rmSync('npm/'+dir+'/'+file)\" && pnpm build && pnpm artifacts",
|
|
41
42
|
"test:snapshots": "pnpm snapshots:prepare && cargo test -p tw-migrate-snapshots",
|
|
42
43
|
"prepare": "vp config"
|
|
43
44
|
},
|
|
44
|
-
dependencies: {
|
|
45
|
+
dependencies: {
|
|
46
|
+
"diff": "9.0.0",
|
|
47
|
+
"parse5": "8.0.1",
|
|
48
|
+
"zod": "4.5.4"
|
|
49
|
+
},
|
|
45
50
|
devDependencies: {
|
|
46
51
|
"@napi-rs/cli": "catalog:",
|
|
47
52
|
"@types/node": "catalog:",
|
|
48
53
|
"@vitest/browser-playwright": "catalog:",
|
|
54
|
+
"knip": "catalog:",
|
|
49
55
|
"less": "catalog:",
|
|
50
56
|
"playwright": "catalog:",
|
|
51
57
|
"sass": "catalog:",
|
|
@@ -75,7 +81,7 @@ const { version } = {
|
|
|
75
81
|
]
|
|
76
82
|
},
|
|
77
83
|
engines: { "node": ">=22.23.2" },
|
|
78
|
-
packageManager: "pnpm@11.
|
|
84
|
+
packageManager: "pnpm@11.24.0"
|
|
79
85
|
};
|
|
80
86
|
const usage = "Usage: tw-migrate [style-file] [options]";
|
|
81
87
|
const help = `${usage}
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
@@ -231,6 +229,10 @@ function sourcePathFromMap(source, sourceBase) {
|
|
|
231
229
|
}
|
|
232
230
|
//#endregion
|
|
233
231
|
//#region src/util/shared.ts
|
|
232
|
+
const projectPackageSchema = z.looseObject({
|
|
233
|
+
name: z.optional(z.string()),
|
|
234
|
+
private: z.optional(z.boolean())
|
|
235
|
+
});
|
|
234
236
|
const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
235
237
|
".html",
|
|
236
238
|
".vue",
|
|
@@ -282,6 +284,16 @@ async function rejectSymlinkTarget(path, root) {
|
|
|
282
284
|
if (current === root) break;
|
|
283
285
|
}
|
|
284
286
|
}
|
|
287
|
+
async function* readInBatches(paths) {
|
|
288
|
+
for (let offset = 0; offset < paths.length; offset += 32) {
|
|
289
|
+
const batch = paths.slice(offset, offset + 32);
|
|
290
|
+
const reads = await Promise.allSettled(batch.map((path) => readFile(path, "utf8")));
|
|
291
|
+
for (const [index, path] of batch.entries()) yield {
|
|
292
|
+
path,
|
|
293
|
+
read: reads[index]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
285
297
|
async function snapshotFile(snapshots, path) {
|
|
286
298
|
return recordSnapshot(snapshots, path, await readFile(path, "utf8"));
|
|
287
299
|
}
|
|
@@ -723,18 +735,129 @@ function locatedAttribute(source, location, parsedValue) {
|
|
|
723
735
|
};
|
|
724
736
|
}
|
|
725
737
|
//#endregion
|
|
726
|
-
//#region src/
|
|
727
|
-
function
|
|
728
|
-
return specifier === "tailwindcss" || specifier === "tailwindcss/utilities" || specifier === "tailwindcss/utilities.css";
|
|
738
|
+
//#region src/tailwind.ts
|
|
739
|
+
function findTailwindEntries(stylePaths, styleSources) {
|
|
740
|
+
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"));
|
|
741
|
+
}
|
|
742
|
+
const externalStylesheetUrl = z.url({ protocol: /^(https?|data)$/ });
|
|
743
|
+
const tailwindPackageSchema = z.object({ version: z.string() });
|
|
744
|
+
function isExternalStylesheet(specifier) {
|
|
745
|
+
return specifier.startsWith("//") || /^(?:https?:\/\/|data:)/i.test(specifier) && externalStylesheetUrl.safeParse(specifier).success;
|
|
746
|
+
}
|
|
747
|
+
function selectTailwindEntry(entries, configuredPath) {
|
|
748
|
+
if (configuredPath) return configuredPath;
|
|
749
|
+
if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
|
|
750
|
+
if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
|
|
751
|
+
return entries[0];
|
|
752
|
+
}
|
|
753
|
+
async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
|
|
754
|
+
const projectRequire = createRequire(join(packageRoot, "package.json"));
|
|
755
|
+
let packagePath;
|
|
756
|
+
try {
|
|
757
|
+
packagePath = projectRequire.resolve("tailwindcss/package.json");
|
|
758
|
+
} catch {
|
|
759
|
+
throw new Error("Tailwind v4 must be installed in the target project.");
|
|
760
|
+
}
|
|
761
|
+
const packageJson = tailwindPackageSchema.parse(JSON.parse(await readFile(packagePath, "utf8")));
|
|
762
|
+
if (!packageJson.version.startsWith("4.")) throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
|
|
763
|
+
const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
|
|
764
|
+
const css = await snapshotFile(snapshots, tailwindCss);
|
|
765
|
+
const base = dirname(tailwindCss);
|
|
766
|
+
const tailwindRoot = dirname(packagePath);
|
|
767
|
+
const loadModule = async (id, moduleBase) => {
|
|
768
|
+
const path = createRequire(join(moduleBase, "package.json")).resolve(id);
|
|
769
|
+
if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
|
|
770
|
+
const imported = await import(pathToFileURL(path).href);
|
|
771
|
+
return {
|
|
772
|
+
path,
|
|
773
|
+
base: dirname(path),
|
|
774
|
+
module: imported.default ?? imported
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
const loadStylesheet = async (id, sheetBase) => {
|
|
778
|
+
if (isExternalStylesheet(id)) return {
|
|
779
|
+
content: "",
|
|
780
|
+
base: sheetBase
|
|
781
|
+
};
|
|
782
|
+
let path;
|
|
783
|
+
if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
|
|
784
|
+
else if (id.startsWith("tailwindcss/")) {
|
|
785
|
+
const subpath = id.slice(12);
|
|
786
|
+
path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
|
|
787
|
+
} else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
|
|
788
|
+
else path = projectRequire.resolve(id);
|
|
789
|
+
return {
|
|
790
|
+
content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
|
|
791
|
+
base: dirname(path)
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
|
|
795
|
+
const graphSources = [];
|
|
796
|
+
const referenceTokens = /* @__PURE__ */ new Set();
|
|
797
|
+
const themeTokens = {
|
|
798
|
+
...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
|
|
799
|
+
...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources, referenceTokens)
|
|
800
|
+
};
|
|
801
|
+
graphSources.push({
|
|
802
|
+
path: tailwindCss,
|
|
803
|
+
source: css
|
|
804
|
+
});
|
|
805
|
+
const designSystem = await loadDesignSystem(css, {
|
|
806
|
+
base,
|
|
807
|
+
loadModule,
|
|
808
|
+
loadStylesheet
|
|
809
|
+
});
|
|
810
|
+
const loadWith = (replacement) => loadDesignSystem(replacement, {
|
|
811
|
+
base,
|
|
812
|
+
loadModule,
|
|
813
|
+
loadStylesheet
|
|
814
|
+
});
|
|
815
|
+
return {
|
|
816
|
+
designSystem,
|
|
817
|
+
css,
|
|
818
|
+
path: tailwindCss,
|
|
819
|
+
themeTokens,
|
|
820
|
+
referenceTokens,
|
|
821
|
+
graphSources,
|
|
822
|
+
loadWith
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function extractThemeTokens(key, css) {
|
|
826
|
+
return stylesheetAnalysis(key, css).themeTokens;
|
|
827
|
+
}
|
|
828
|
+
function importEntries(css) {
|
|
829
|
+
return (cssDirectives$1(css) ?? []).flatMap((directive) => directive !== null && typeof directive === "object" && "kind" in directive && directive.kind === "import" && "specifier" in directive && typeof directive.specifier === "string" ? [{
|
|
830
|
+
specifier: directive.specifier,
|
|
831
|
+
reference: "reference" in directive && directive.reference === true
|
|
832
|
+
}] : []);
|
|
833
|
+
}
|
|
834
|
+
async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, referenceTokens, inReference = false, seen = /* @__PURE__ */ new Set()) {
|
|
835
|
+
const tokens = {};
|
|
836
|
+
for (const { specifier: spec, reference } of importEntries(css)) {
|
|
837
|
+
if (isExternalStylesheet(spec)) continue;
|
|
838
|
+
const key = `${base}\0${spec}`;
|
|
839
|
+
if (seen.has(key)) continue;
|
|
840
|
+
seen.add(key);
|
|
841
|
+
const loaded = await loadStylesheet(spec, base);
|
|
842
|
+
if (spec !== "tailwindcss" && !spec.startsWith("tailwindcss/")) graphSources.push({
|
|
843
|
+
path: key,
|
|
844
|
+
source: loaded.content
|
|
845
|
+
});
|
|
846
|
+
Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, referenceTokens, inReference || reference, seen));
|
|
847
|
+
}
|
|
848
|
+
const own = extractThemeTokens(`${identity}.theme.css`, css);
|
|
849
|
+
if (inReference) for (const token of Object.keys(own)) referenceTokens.add(token);
|
|
850
|
+
return Object.assign(tokens, own);
|
|
729
851
|
}
|
|
730
|
-
function
|
|
731
|
-
|
|
852
|
+
function invalidCandidates(system, candidates) {
|
|
853
|
+
const generated = system.candidatesToCss(candidates);
|
|
854
|
+
return candidates.filter((_, index) => generated[index] === null);
|
|
732
855
|
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/plan/entry.ts
|
|
733
858
|
function tailwindEntryCatalog(styleSources, pathOwners) {
|
|
734
859
|
const catalog = /* @__PURE__ */ new Map();
|
|
735
|
-
for (const
|
|
736
|
-
if (extname(path) !== ".css") continue;
|
|
737
|
-
if (!hasTailwindImport(source)) continue;
|
|
860
|
+
for (const path of findTailwindEntries([...styleSources.keys()], styleSources)) {
|
|
738
861
|
const owner = pathOwners.get(path);
|
|
739
862
|
if (owner === void 0) continue;
|
|
740
863
|
const entries = catalog.get(owner) ?? [];
|
|
@@ -767,7 +890,7 @@ function entryGraphSheets(entry, styleSources) {
|
|
|
767
890
|
if (directive.kind !== "import") continue;
|
|
768
891
|
if (directive.specifier === null) return null;
|
|
769
892
|
const spec = directive.specifier;
|
|
770
|
-
if (directive.tailwind) continue;
|
|
893
|
+
if (directive.tailwind || isExternalStylesheet(spec)) continue;
|
|
771
894
|
if (!spec.startsWith(".") && !spec.startsWith("/")) {
|
|
772
895
|
let resolved;
|
|
773
896
|
try {
|
|
@@ -1211,115 +1334,6 @@ function htmlWarning(code, file, start, end, message) {
|
|
|
1211
1334
|
};
|
|
1212
1335
|
}
|
|
1213
1336
|
//#endregion
|
|
1214
|
-
//#region src/tailwind.ts
|
|
1215
|
-
function findTailwindEntries(stylePaths, styleSources) {
|
|
1216
|
-
return stylePaths.filter((path) => extname(path) === ".css" && importEntries(styleSources.get(path) ?? "").some(({ specifier }) => specifier === "tailwindcss" || specifier.startsWith("tailwindcss/")));
|
|
1217
|
-
}
|
|
1218
|
-
function selectTailwindEntry(entries, configuredPath) {
|
|
1219
|
-
if (configuredPath) return configuredPath;
|
|
1220
|
-
if (entries.length === 0) throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
|
|
1221
|
-
if (entries.length > 1) throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
|
|
1222
|
-
return entries[0];
|
|
1223
|
-
}
|
|
1224
|
-
async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
|
|
1225
|
-
const projectRequire = createRequire(join(packageRoot, "package.json"));
|
|
1226
|
-
let packagePath;
|
|
1227
|
-
try {
|
|
1228
|
-
packagePath = projectRequire.resolve("tailwindcss/package.json");
|
|
1229
|
-
} catch {
|
|
1230
|
-
throw new Error("Tailwind v4 must be installed in the target project.");
|
|
1231
|
-
}
|
|
1232
|
-
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
1233
|
-
if (!String(packageJson.version).startsWith("4.")) throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
|
|
1234
|
-
const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
|
|
1235
|
-
const css = await snapshotFile(snapshots, tailwindCss);
|
|
1236
|
-
const base = dirname(tailwindCss);
|
|
1237
|
-
const tailwindRoot = dirname(packagePath);
|
|
1238
|
-
const loadModule = async (id, moduleBase) => {
|
|
1239
|
-
const path = createRequire(join(moduleBase, "package.json")).resolve(id);
|
|
1240
|
-
if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
|
|
1241
|
-
const imported = await import(pathToFileURL(path).href);
|
|
1242
|
-
return {
|
|
1243
|
-
path,
|
|
1244
|
-
base: dirname(path),
|
|
1245
|
-
module: imported.default ?? imported
|
|
1246
|
-
};
|
|
1247
|
-
};
|
|
1248
|
-
const loadStylesheet = async (id, sheetBase) => {
|
|
1249
|
-
let path;
|
|
1250
|
-
if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
|
|
1251
|
-
else if (id.startsWith("tailwindcss/")) {
|
|
1252
|
-
const subpath = id.slice(12);
|
|
1253
|
-
path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
|
|
1254
|
-
} else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
|
|
1255
|
-
else path = projectRequire.resolve(id);
|
|
1256
|
-
return {
|
|
1257
|
-
content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
|
|
1258
|
-
base: dirname(path)
|
|
1259
|
-
};
|
|
1260
|
-
};
|
|
1261
|
-
const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
|
|
1262
|
-
const graphSources = [];
|
|
1263
|
-
const referenceTokens = /* @__PURE__ */ new Set();
|
|
1264
|
-
const themeTokens = {
|
|
1265
|
-
...extractThemeTokens("tailwind-default.theme.css", defaultTheme),
|
|
1266
|
-
...await extractThemeTokensFromGraph(tailwindCss, css, base, loadStylesheet, graphSources, referenceTokens)
|
|
1267
|
-
};
|
|
1268
|
-
graphSources.push({
|
|
1269
|
-
path: tailwindCss,
|
|
1270
|
-
source: css
|
|
1271
|
-
});
|
|
1272
|
-
const designSystem = await loadDesignSystem(css, {
|
|
1273
|
-
base,
|
|
1274
|
-
loadModule,
|
|
1275
|
-
loadStylesheet
|
|
1276
|
-
});
|
|
1277
|
-
const loadWith = (replacement) => loadDesignSystem(replacement, {
|
|
1278
|
-
base,
|
|
1279
|
-
loadModule,
|
|
1280
|
-
loadStylesheet
|
|
1281
|
-
});
|
|
1282
|
-
return {
|
|
1283
|
-
designSystem,
|
|
1284
|
-
css,
|
|
1285
|
-
path: tailwindCss,
|
|
1286
|
-
themeTokens,
|
|
1287
|
-
referenceTokens,
|
|
1288
|
-
graphSources,
|
|
1289
|
-
loadWith
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
function extractThemeTokens(key, css) {
|
|
1293
|
-
return stylesheetAnalysis(key, css).themeTokens;
|
|
1294
|
-
}
|
|
1295
|
-
function importEntries(css) {
|
|
1296
|
-
return (cssDirectives$1(css) ?? []).flatMap((directive) => directive !== null && typeof directive === "object" && "kind" in directive && directive.kind === "import" && "specifier" in directive && typeof directive.specifier === "string" ? [{
|
|
1297
|
-
specifier: directive.specifier,
|
|
1298
|
-
reference: "reference" in directive && directive.reference === true
|
|
1299
|
-
}] : []);
|
|
1300
|
-
}
|
|
1301
|
-
async function extractThemeTokensFromGraph(identity, css, base, loadStylesheet, graphSources, referenceTokens, inReference = false, seen = /* @__PURE__ */ new Set()) {
|
|
1302
|
-
const tokens = {};
|
|
1303
|
-
for (const { specifier: spec, reference } of importEntries(css)) {
|
|
1304
|
-
const key = `${base}\0${spec}`;
|
|
1305
|
-
if (seen.has(key)) continue;
|
|
1306
|
-
seen.add(key);
|
|
1307
|
-
const loaded = await loadStylesheet(spec, base);
|
|
1308
|
-
if (spec !== "tailwindcss" && !spec.startsWith("tailwindcss/")) graphSources.push({
|
|
1309
|
-
path: key,
|
|
1310
|
-
source: loaded.content
|
|
1311
|
-
});
|
|
1312
|
-
Object.assign(tokens, await extractThemeTokensFromGraph(key, loaded.content, loaded.base, loadStylesheet, graphSources, referenceTokens, inReference || reference, seen));
|
|
1313
|
-
}
|
|
1314
|
-
const own = extractThemeTokens(`${identity}.theme.css`, css);
|
|
1315
|
-
if (inReference) for (const token of Object.keys(own)) referenceTokens.add(token);
|
|
1316
|
-
return Object.assign(tokens, own);
|
|
1317
|
-
}
|
|
1318
|
-
function invalidCandidates(system, candidates) {
|
|
1319
|
-
const generated = system.candidatesToCss(candidates);
|
|
1320
|
-
return candidates.filter((_, index) => generated[index] === null);
|
|
1321
|
-
}
|
|
1322
|
-
//#endregion
|
|
1323
1337
|
//#region src/plan/canonicalize.ts
|
|
1324
1338
|
const caches = /* @__PURE__ */ new WeakMap();
|
|
1325
1339
|
function canonicalCandidate(system, candidate) {
|
|
@@ -1607,7 +1621,10 @@ function matchingFontTokens(stacks, normalizedStack) {
|
|
|
1607
1621
|
//#region src/plan/media.ts
|
|
1608
1622
|
const GENERATED_NAMESPACE = "twm-media";
|
|
1609
1623
|
/**
|
|
1610
|
-
* Resolve one fixed name per key across a complete entry group
|
|
1624
|
+
* Resolve one fixed name per key across a complete entry group: key to
|
|
1625
|
+
* resolved name, in sorted key order. Only `generated` entries emit a
|
|
1626
|
+
* definition; a key absent from the map could not escape existing owners
|
|
1627
|
+
* and keeps the arbitrary-variant fallback.
|
|
1611
1628
|
*
|
|
1612
1629
|
* Resolution follows the RFC order: a built-in candidate with a verified
|
|
1613
1630
|
* effective expansion, an existing breakpoint matched by exact value and
|
|
@@ -1633,7 +1650,6 @@ function resolveMediaNames(collections, themeTokens, probes, reservedNames = [])
|
|
|
1633
1650
|
};
|
|
1634
1651
|
const taken = (name) => claimed.has(name) || reserved.has(name) || authoredKeysByName.has(name) || probes.resolves(name);
|
|
1635
1652
|
const names = /* @__PURE__ */ new Map();
|
|
1636
|
-
const fallbacks = /* @__PURE__ */ new Set();
|
|
1637
1653
|
for (const component of components) {
|
|
1638
1654
|
const resolved = (name, kind) => {
|
|
1639
1655
|
claimed.add(name);
|
|
@@ -1652,25 +1668,18 @@ function resolveMediaNames(collections, themeTokens, probes, reservedNames = [])
|
|
|
1652
1668
|
}
|
|
1653
1669
|
const digestName = `${GENERATED_NAMESPACE}-${component.digest}`;
|
|
1654
1670
|
const candidates = component.readableName === null ? [digestName] : [component.readableName, digestName];
|
|
1655
|
-
let done = false;
|
|
1656
1671
|
for (const candidate of candidates) {
|
|
1657
1672
|
if (adoptableKey(candidate) === component.key && !claimed.has(candidate) && probes.expansionMatches(candidate, component.key)) {
|
|
1658
1673
|
resolved(candidate, "adopted");
|
|
1659
|
-
done = true;
|
|
1660
1674
|
break;
|
|
1661
1675
|
}
|
|
1662
1676
|
if (!taken(candidate)) {
|
|
1663
1677
|
resolved(candidate, "generated");
|
|
1664
|
-
done = true;
|
|
1665
1678
|
break;
|
|
1666
1679
|
}
|
|
1667
1680
|
}
|
|
1668
|
-
if (!done) fallbacks.add(component.key);
|
|
1669
1681
|
}
|
|
1670
|
-
return
|
|
1671
|
-
names,
|
|
1672
|
-
fallbacks
|
|
1673
|
-
};
|
|
1682
|
+
return names;
|
|
1674
1683
|
}
|
|
1675
1684
|
function mergeComponents(collections) {
|
|
1676
1685
|
const byKey = /* @__PURE__ */ new Map();
|
|
@@ -1751,7 +1760,7 @@ function planMediaExtraction(options) {
|
|
|
1751
1760
|
const resolution = resolveMediaNames(options.collections, options.tailwind.themeTokens, buildMediaProbes(options.tailwind), scannedVariantReservations([...options.scannedSources, ...options.tailwind.graphSources.map((graphSource) => graphSource.source)]));
|
|
1752
1761
|
const names = {};
|
|
1753
1762
|
const generated = [];
|
|
1754
|
-
for (const [key, resolved] of resolution
|
|
1763
|
+
for (const [key, resolved] of resolution) {
|
|
1755
1764
|
names[key] = resolved.name;
|
|
1756
1765
|
if (resolved.kind === "generated") generated.push({
|
|
1757
1766
|
key,
|
|
@@ -2604,7 +2613,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
|
|
|
2604
2613
|
})));
|
|
2605
2614
|
vueShadowModuleCss.push(...analysis.shadowModuleCssTexts, ...analysis.moduleBlocks.map((block) => block.analysisSource ?? block.content));
|
|
2606
2615
|
}
|
|
2607
|
-
const packageIsPrivate = JSON.parse(await snapshotFile(snapshots, join(packageRoot, "package.json"))).private === true;
|
|
2616
|
+
const packageIsPrivate = projectPackageSchema.parse(JSON.parse(await snapshotFile(snapshots, join(packageRoot, "package.json")))).private === true;
|
|
2608
2617
|
const files = /* @__PURE__ */ new Map();
|
|
2609
2618
|
const stylesheets = [];
|
|
2610
2619
|
const stylePaths = /* @__PURE__ */ new Set();
|
|
@@ -2815,16 +2824,15 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
|
|
|
2815
2824
|
//#endregion
|
|
2816
2825
|
//#region src/util/write.ts
|
|
2817
2826
|
async function verifySnapshots(snapshots) {
|
|
2818
|
-
const
|
|
2819
|
-
const
|
|
2820
|
-
for (const
|
|
2821
|
-
const read = reads[index];
|
|
2827
|
+
const expected = new Map(snapshots);
|
|
2828
|
+
const paths = [...expected.keys()].sort((left, right) => left.localeCompare(right));
|
|
2829
|
+
for await (const { path, read } of readInBatches(paths)) {
|
|
2822
2830
|
if (read.status === "rejected") {
|
|
2823
2831
|
const error = read.reason;
|
|
2824
2832
|
const detail = errorCode(error) ?? errorMessage(error);
|
|
2825
2833
|
throw new Error(`Source changed after planning: ${path} (${detail})`);
|
|
2826
2834
|
}
|
|
2827
|
-
if (read.value !==
|
|
2835
|
+
if (read.value !== expected.get(path)) throw new Error(`Source changed after planning: ${path}`);
|
|
2828
2836
|
}
|
|
2829
2837
|
}
|
|
2830
2838
|
async function writeChanges(changes, deletions) {
|
|
@@ -2888,7 +2896,20 @@ function indexWarningPositions(source, offsets, unicodeSeparatorsAreNewlines, fo
|
|
|
2888
2896
|
}
|
|
2889
2897
|
return positions;
|
|
2890
2898
|
}
|
|
2899
|
+
const optionPath = z.string().check(z.minLength(1), z.refine((path) => !path.includes("\0")));
|
|
2900
|
+
const migrateOptionsSchema = z.strictObject({
|
|
2901
|
+
styleFile: z.optional(optionPath),
|
|
2902
|
+
cwd: z.optional(optionPath),
|
|
2903
|
+
write: z.optional(z.boolean()),
|
|
2904
|
+
tailwindCss: z.optional(optionPath),
|
|
2905
|
+
workspaces: z.optional(z.boolean()),
|
|
2906
|
+
force: z.optional(z.boolean()),
|
|
2907
|
+
extractMediaQueries: z.optional(z.boolean())
|
|
2908
|
+
});
|
|
2891
2909
|
async function migrate(options = {}) {
|
|
2910
|
+
const parsedOptions = migrateOptionsSchema.safeParse(options);
|
|
2911
|
+
if (!parsedOptions.success) throw new TypeError(`Invalid migrate options: ${parsedOptions.error.message}`);
|
|
2912
|
+
options = parsedOptions.data;
|
|
2892
2913
|
if (options.styleFile && options.workspaces) throw new TypeError("styleFile cannot be combined with workspaces");
|
|
2893
2914
|
if (options.tailwindCss && options.workspaces) throw new TypeError("tailwindCss cannot be combined with workspaces");
|
|
2894
2915
|
const scope = await resolveScope(options);
|
|
@@ -2899,28 +2920,30 @@ async function migrate(options = {}) {
|
|
|
2899
2920
|
throw new Error(`Found leftover tw-migrate files from an interrupted run:\n${listed}\nRestore each ".<name>.tw-migrate-backup-*" file by renaming it back to "<name>", delete any remaining ".<name>.tw-migrate-*" staging files, then re-run.`);
|
|
2900
2921
|
}
|
|
2901
2922
|
const snapshots = /* @__PURE__ */ new Map();
|
|
2902
|
-
const
|
|
2903
|
-
const
|
|
2904
|
-
const [
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2923
|
+
const styleSources = /* @__PURE__ */ new Map();
|
|
2924
|
+
const sourceFiles = [];
|
|
2925
|
+
const inputPaths = [...scope.scannedPaths.filter((path) => isStylesheetPath(path) || SOURCE_EXTENSIONS.has(extname(path))), ...selectedPackages.map((packageRoot) => join(packageRoot, "package.json"))];
|
|
2926
|
+
for await (const { path, read } of readInBatches(inputPaths)) {
|
|
2927
|
+
if (read.status === "rejected") throw read.reason;
|
|
2928
|
+
const source = read.value;
|
|
2929
|
+
if (!SOURCE_EXTENSIONS.has(extname(path))) {
|
|
2930
|
+
recordSnapshot(snapshots, path, source);
|
|
2931
|
+
if (isStylesheetPath(path)) styleSources.set(path, source);
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
const mayReferenceModule = extname(path) !== ".html" || (() => {
|
|
2935
|
+
try {
|
|
2936
|
+
return parseHtmlSource(path, source).links.length > 0;
|
|
2937
|
+
} catch {
|
|
2938
|
+
return /<link\b/i.test(source);
|
|
2939
|
+
}
|
|
2940
|
+
})();
|
|
2941
|
+
if (!scope.targetable.has(path) && !mayReferenceModule) continue;
|
|
2942
|
+
sourceFiles.push({
|
|
2943
|
+
path,
|
|
2944
|
+
source: recordSnapshot(snapshots, path, source)
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2924
2947
|
if (explicitStyle && isStylesheetPath(explicitStyle) && !styleSources.has(explicitStyle)) styleSources.set(explicitStyle, await snapshotFile(snapshots, explicitStyle));
|
|
2925
2948
|
if (configuredEntry && !styleSources.has(configuredEntry)) styleSources.set(configuredEntry, await snapshotFile(snapshots, configuredEntry));
|
|
2926
2949
|
const vueStyleRanges = /* @__PURE__ */ new Map();
|
|
@@ -3104,10 +3127,7 @@ async function preparePackage(context, packageRoot) {
|
|
|
3104
3127
|
let packageJson = {};
|
|
3105
3128
|
try {
|
|
3106
3129
|
const rawPackageJson = snapshots.get(join(packageRoot, "package.json"));
|
|
3107
|
-
if (rawPackageJson !== void 0)
|
|
3108
|
-
const parsed = JSON.parse(rawPackageJson);
|
|
3109
|
-
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) packageJson = { ...parsed };
|
|
3110
|
-
}
|
|
3130
|
+
if (rawPackageJson !== void 0) packageJson = projectPackageSchema.parse(JSON.parse(rawPackageJson));
|
|
3111
3131
|
} catch (error) {
|
|
3112
3132
|
return recover(error);
|
|
3113
3133
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tw-migrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codemod",
|
|
@@ -38,18 +38,22 @@
|
|
|
38
38
|
"artifacts": "node -e \"const fs=require('node:fs'); const files=fs.readdirSync('.').filter((file)=>file.startsWith('tw-migrate.')&&file.endsWith('.node')); if(files.length===0) throw new Error('no tw-migrate.*.node artifact in the package root; run build first'); for(const file of files) fs.copyFileSync(file, 'npm/'+file.slice('tw-migrate.'.length, -'.node'.length)+'/'+file)\"",
|
|
39
39
|
"test": "cargo test && pnpm build:debug && vp test",
|
|
40
40
|
"test:ecosystem": "node ecosystem-ci/run.ts",
|
|
41
|
+
"knip": "knip",
|
|
41
42
|
"typecheck": "tsc --noEmit",
|
|
42
43
|
"snapshots:prepare": "node -e \"const fs=require('node:fs'); for(const dir of fs.readdirSync('npm')) for(const file of fs.readdirSync('npm/'+dir)) if(file.endsWith('.node')) fs.rmSync('npm/'+dir+'/'+file)\" && pnpm build && pnpm artifacts",
|
|
43
44
|
"test:snapshots": "pnpm snapshots:prepare && cargo test -p tw-migrate-snapshots",
|
|
44
45
|
"prepare": "vp config"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"
|
|
48
|
+
"diff": "9.0.0",
|
|
49
|
+
"parse5": "8.0.1",
|
|
50
|
+
"zod": "4.5.4"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
53
|
"@napi-rs/cli": "catalog:",
|
|
51
54
|
"@types/node": "catalog:",
|
|
52
55
|
"@vitest/browser-playwright": "catalog:",
|
|
56
|
+
"knip": "catalog:",
|
|
53
57
|
"less": "catalog:",
|
|
54
58
|
"playwright": "catalog:",
|
|
55
59
|
"sass": "catalog:",
|
|
@@ -62,11 +66,11 @@
|
|
|
62
66
|
"vue": "catalog:"
|
|
63
67
|
},
|
|
64
68
|
"optionalDependencies": {
|
|
65
|
-
"tw-migrate-darwin-arm64": "0.
|
|
66
|
-
"tw-migrate-darwin-x64": "0.
|
|
67
|
-
"tw-migrate-linux-arm64-gnu": "0.
|
|
68
|
-
"tw-migrate-linux-x64-gnu": "0.
|
|
69
|
-
"tw-migrate-win32-x64-msvc": "0.
|
|
69
|
+
"tw-migrate-darwin-arm64": "0.7.1",
|
|
70
|
+
"tw-migrate-darwin-x64": "0.7.1",
|
|
71
|
+
"tw-migrate-linux-arm64-gnu": "0.7.1",
|
|
72
|
+
"tw-migrate-linux-x64-gnu": "0.7.1",
|
|
73
|
+
"tw-migrate-win32-x64-msvc": "0.7.1"
|
|
70
74
|
},
|
|
71
75
|
"napi": {
|
|
72
76
|
"binaryName": "tw-migrate",
|
|
@@ -81,5 +85,5 @@
|
|
|
81
85
|
"engines": {
|
|
82
86
|
"node": ">=22.23.2"
|
|
83
87
|
},
|
|
84
|
-
"packageManager": "pnpm@11.
|
|
88
|
+
"packageManager": "pnpm@11.24.0"
|
|
85
89
|
}
|