tw-migrate 0.1.0 → 0.3.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
@@ -1,22 +1,22 @@
1
1
  # tw-migrate
2
2
 
3
- Preview and migrate static React/Next.js stylesheet references to Tailwind v4 utilities.
3
+ Migrate static React/Next.js, Vue 3 SFC, and HTML stylesheet references to Tailwind v4 utilities.
4
4
 
5
5
  ## Usage
6
6
 
7
7
  Run it in the package you want to migrate — no install step:
8
8
 
9
9
  ```bash
10
- npx tw-migrate # Preview the current package
11
- npx tw-migrate --write # Migrate the current package
12
- npx tw-migrate path/to/Button.module.scss # Preview one stylesheet
13
- npx tw-migrate path/to/Button.module.scss --write # Migrate one stylesheet
14
- npx tw-migrate --workspaces --write # Migrate every package
10
+ npx tw-migrate # Migrate the current package
11
+ npx tw-migrate --dry-run # Preview the current package
12
+ npx tw-migrate path/to/Button.module.scss # Migrate one stylesheet
13
+ npx tw-migrate path/to/Button.module.scss --dry-run # Preview one stylesheet
14
+ npx tw-migrate --workspaces # Migrate every package
15
15
  ```
16
16
 
17
17
  `pnpm dlx tw-migrate` and `yarn dlx tw-migrate` take the same arguments.
18
18
 
19
- The CLI previews changes by default. 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.
19
+ 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.
20
20
 
21
21
  ## Run from a clone
22
22
 
@@ -25,14 +25,14 @@ The migration planner is a native addon, so a clone needs one build before the C
25
25
  ```bash
26
26
  vp install # or: pnpm install
27
27
  vp run build:debug # compiles the addon for your platform
28
- node bin/tw-migrate.js --help
28
+ node src/bin.ts --help
29
29
  ```
30
30
 
31
- Then use `node bin/tw-migrate.js` 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:
31
+ 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:
32
32
 
33
33
  ```bash
34
34
  cd ../some-app
35
- node /path/to/tw-migrate/bin/tw-migrate.js --write
35
+ node /path/to/tw-migrate/src/bin.ts
36
36
  ```
37
37
 
38
38
  ## Current support
@@ -41,6 +41,10 @@ node /path/to/tw-migrate/bin/tw-migrate.js --write
41
41
  - SCSS/Sass/Less values evaluated with the target project's installed compiler; ambiguous mixin and partial origins are retained
42
42
  - `.js`, `.jsx`, `.ts`, and `.tsx` source files
43
43
  - static `.html` literal `class`/`id` attributes scoped by local external stylesheet links (link-level `print` media supported; other link media conditions are retained)
44
+ - 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
45
+ - 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
46
+ - unscoped Vue styles migrate only in private single-source packages where their global usage is closed; broader unscoped surfaces retain with warnings
47
+ - Vue `<style module>` blocks migrate against direct `:class="$style.x"` member bindings; any other `$style`/`useCssModule` appearance or a named module retains the block
44
48
  - direct CSS Module members, static template literals, and static expression literals
45
49
  - global `className` and `id` literals
46
50
  - multi-compound CSS Module selectors whose element relationships are proven from the JSX graph
@@ -60,23 +64,29 @@ Everything outside this subset is retained and reported with one of the warning
60
64
  | `aliased-css-module-reference` | A CSS Module class is aliased to a local binding, so the module is retained. |
61
65
  | `batch-stylesheet-conflict` | Utilities generated from different stylesheets conflict on the same JSX element, so the contributing rule is retained. |
62
66
  | `candidate-compilation-failure` | A generated candidate did not compile under the project's Tailwind entry, so its rule is retained. |
67
+ | `component-class-target` | A child component's root element can carry classes a Vue scoped rule matches, so the rule is retained. |
63
68
  | `computed-css-module-reference` | A computed CSS Module access cannot be verified, so the module is retained. |
64
69
  | `cross-package-stylesheet-link` | A linked stylesheet is owned by another package, so it is not analyzed outside workspace mode. |
65
70
  | `css-module-composes` | The rule uses or is targeted by `composes`, so it is retained. |
66
71
  | `dynamic-class-name` | A `className` value is dynamic, so the element cannot be migrated. |
67
72
  | `dynamic-html-attribute` | An HTML attribute is not a safely writable quoted literal, so the element cannot be migrated. |
73
+ | `dynamic-template-class` | A dynamic class binding makes a Vue template's class set unprovable, so its scoped rules are retained. |
68
74
  | `existing-tailwind-conflict` | A generated utility may conflict with a Tailwind class already on the element. |
69
75
  | `inferred-preprocessor-source` | A linked CSS file was matched to a uniquely named preprocessor source file. |
70
76
  | `module-utilities-conflict` | Utilities generated from different module classes on one element overlap, so their rules are retained. |
71
77
  | `non-classname-css-module-reference` | A CSS Module class is used outside a supported `className`, so the module is retained. |
78
+ | `open-root-fallthrough` | A parent component can merge classes onto a Vue SFC's single root element, so its scoped rules are retained. |
79
+ | `preprocessor-style-block` | A Vue `<style>` block uses a preprocessor language outside the supported SCSS, Sass, and Less set. |
72
80
  | `rebuild-required` | A preprocessor entry was migrated; rebuild it to refresh its generated CSS. |
73
81
  | `reference-only-css-module-consumer` | A reference-only (non-writable) source uses the CSS Module, so it is retained. |
74
82
  | `retained-global-rule` | Global CSS is never deleted automatically. |
83
+ | `shadowed-scoped-rule` | Other package CSS also targets a class a Vue scoped rule matches, so the rule is retained to preserve the cascade. |
75
84
  | `shared-preprocessor-source` | A Sass partial must be analyzed through every consuming entry, so it is retained. |
76
85
  | `unproven-css-module-relationship` | A compound selector's element relationship could not be proven for every usage. |
77
86
  | `unproven-script-reference` | An inline script names a CSS Module class, so the module is retained. |
78
87
  | `unproven-source-map` | A generated rule does not map uniquely to one authored source rule, so it is retained. |
79
88
  | `unresolved-selector-target` | No exclusively supported `className` references were found for the rule. |
89
+ | `unscoped-style-block` | An unscoped Vue style does not have a closed private single-source usage surface, so it is retained. |
80
90
  | `unsupported-animation` | The animation references keyframes that cannot be converted. |
81
91
  | `unsupported-at-rule` | The rule contains or sits inside an at-rule outside the supported set. |
82
92
  | `unsupported-container-query` | The `@container` condition has no Tailwind variant equivalent. |
@@ -91,9 +101,11 @@ Everything outside this subset is retained and reported with one of the warning
91
101
  | `unsupported-overlap` | Shorthand and longhand declarations overlap in a way that cannot be normalized. |
92
102
  | `unsupported-rule-content` | The rule contains non-declaration content that cannot be converted. |
93
103
  | `unsupported-selector` | The selector is outside the supported subset. |
104
+ | `unsupported-sfc-block` | A Vue SFC or one of its blocks cannot be analyzed safely, so it is retained. |
94
105
  | `unsupported-starting-style` | The `@starting-style` condition could not be converted. |
95
106
  | `unsupported-supports-query` | The `@supports` condition has no Tailwind variant equivalent. |
96
107
  | `unsupported-value` | A declaration value cannot be represented as a Tailwind utility. |
108
+ | `unsupported-vue-version` | The project's Vue version is not Vue 3, so its SFCs are retained. |
97
109
 
98
110
  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.
99
111
 
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/bin.js ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ import { styleText } from "node:util";
3
+ //#endregion
4
+ //#region src/bin.ts
5
+ const { version } = {
6
+ name: "tw-migrate",
7
+ version: "0.3.0",
8
+ description: "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
9
+ keywords: [
10
+ "codemod",
11
+ "css",
12
+ "css-modules",
13
+ "migration",
14
+ "napi-rs",
15
+ "react",
16
+ "tailwind",
17
+ "tailwindcss",
18
+ "vue"
19
+ ],
20
+ homepage: "https://github.com/jong-kyung/tw-migrate#readme",
21
+ bugs: { "url": "https://github.com/jong-kyung/tw-migrate/issues" },
22
+ license: "MIT",
23
+ author: "JongKyung Lee",
24
+ repository: {
25
+ "type": "git",
26
+ "url": "git+https://github.com/jong-kyung/tw-migrate.git"
27
+ },
28
+ bin: { "tw-migrate": "dist/bin.js" },
29
+ files: ["dist"],
30
+ type: "module",
31
+ main: "dist/index.js",
32
+ types: "dist/index.d.ts",
33
+ scripts: {
34
+ "build": "vp pack && napi build --manifest-path crates/tw_migrate/Cargo.toml --package-json-path package.json --output-dir . --platform --release --no-js --dts binding.d.ts",
35
+ "build:debug": "napi build --manifest-path crates/tw_migrate/Cargo.toml --package-json-path package.json --output-dir . --platform --no-js --dts binding.d.ts",
36
+ "artifacts": "napi artifacts --output-dir . --npm-dir npm",
37
+ "test": "cargo test && pnpm build:debug && vp test",
38
+ "test:ecosystem": "node ecosystem-ci/run.ts",
39
+ "typecheck": "tsc --noEmit",
40
+ "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
+ "test:snapshots": "pnpm snapshots:prepare && cargo test -p tw-migrate-snapshots",
42
+ "prepare": "vp config"
43
+ },
44
+ dependencies: { "parse5": "7.3.0" },
45
+ devDependencies: {
46
+ "@napi-rs/cli": "catalog:",
47
+ "@types/node": "catalog:",
48
+ "@vitest/browser-playwright": "catalog:",
49
+ "less": "catalog:",
50
+ "playwright": "catalog:",
51
+ "sass": "catalog:",
52
+ "tailwindcss": "catalog:",
53
+ "typescript": "catalog:",
54
+ "verdaccio": "catalog:",
55
+ "vite": "catalog:",
56
+ "vite-plus": "catalog:",
57
+ "vitest": "catalog:",
58
+ "vue": "catalog:"
59
+ },
60
+ optionalDependencies: {
61
+ "tw-migrate-darwin-arm64": "workspace:*",
62
+ "tw-migrate-darwin-x64": "workspace:*",
63
+ "tw-migrate-linux-arm64-gnu": "workspace:*",
64
+ "tw-migrate-linux-x64-gnu": "workspace:*",
65
+ "tw-migrate-win32-x64-msvc": "workspace:*"
66
+ },
67
+ napi: {
68
+ "binaryName": "tw-migrate",
69
+ "targets": [
70
+ "aarch64-apple-darwin",
71
+ "x86_64-apple-darwin",
72
+ "aarch64-unknown-linux-gnu",
73
+ "x86_64-unknown-linux-gnu",
74
+ "x86_64-pc-windows-msvc"
75
+ ]
76
+ },
77
+ engines: { "node": ">=22.18.0" },
78
+ packageManager: "pnpm@11.15.1"
79
+ };
80
+ const usage = "Usage: tw-migrate [style-file] [options]";
81
+ const help = `${usage}
82
+
83
+ Arguments:
84
+ [style-file] Migrate one stylesheet instead of the current package.
85
+ Cannot be used with --workspaces.
86
+
87
+ Options:
88
+ --tailwind-css <entry.css> Use this Tailwind CSS entry when the current package
89
+ has multiple entries. Cannot be used with --workspaces.
90
+ --workspaces Migrate every package in the workspace.
91
+ --force Skip packages with recoverable discovery or input errors.
92
+ --dry-run Preview changes without applying them.
93
+ -h, --help Show this help message.
94
+ -v, --version Show the version number.
95
+
96
+ Examples:
97
+ tw-migrate
98
+ tw-migrate --dry-run
99
+ tw-migrate path/to/Button.module.scss
100
+ tw-migrate --workspaces`;
101
+ async function main() {
102
+ const args = process.argv.slice(2);
103
+ if (args.includes("--help") || args.includes("-h")) {
104
+ console.log(help);
105
+ return;
106
+ }
107
+ if (args.includes("--version") || args.includes("-v")) {
108
+ console.log(`tw-migrate ${version}`);
109
+ return;
110
+ }
111
+ let styleFile;
112
+ let tailwindCss;
113
+ let write = true;
114
+ let force = false;
115
+ let workspaces = false;
116
+ for (let index = 0; index < args.length; index += 1) {
117
+ const argument = args[index];
118
+ if (argument === "--dry-run") write = false;
119
+ else if (argument === "--force") force = true;
120
+ else if (argument === "--workspaces") workspaces = true;
121
+ else if (argument === "--tailwind-css") {
122
+ tailwindCss = args[++index];
123
+ if (!tailwindCss) throw new Error(`${usage}\n--tailwind-css requires a path.`);
124
+ } else if (argument.startsWith("-")) throw new Error(`Unknown option: ${argument}`);
125
+ else if (!styleFile) styleFile = argument;
126
+ else throw new Error(`Unexpected argument: ${argument}`);
127
+ }
128
+ const { migrate } = await import("./index.js");
129
+ const report = await migrate({
130
+ styleFile,
131
+ tailwindCss,
132
+ write,
133
+ force,
134
+ workspaces
135
+ });
136
+ if (report.diff) process.stdout.write(report.diff);
137
+ for (const warning of report.warnings) console.warn(styleText("yellow", `warning[${warning.code}] ${warning.file}:${warning.start}-${warning.end} ${warning.message}`, { stream: process.stderr }));
138
+ for (const failure of report.failures) console.warn(styleText("yellow", `skipped[${failure.package}] ${failure.message}`, { stream: process.stderr }));
139
+ console.log(`${write ? "Applied" : "Previewed"} ${report.changedFiles.length} file(s); ${report.convertedRules} rule(s) converted, ${report.retainedRules} retained.`);
140
+ }
141
+ main().catch((error) => {
142
+ console.error(styleText("red", `tw-migrate: ${error.message}`, { stream: process.stderr }));
143
+ process.exitCode = 1;
144
+ });
145
+ //#endregion
146
+ export {};
@@ -1,4 +1,5 @@
1
- export interface MigrateOptions {
1
+ //#region src/types.d.ts
2
+ interface MigrateOptions {
2
3
  styleFile?: string;
3
4
  cwd?: string;
4
5
  write?: boolean;
@@ -6,8 +7,7 @@ export interface MigrateOptions {
6
7
  workspaces?: boolean;
7
8
  force?: boolean;
8
9
  }
9
-
10
- export interface MigrationWarning {
10
+ interface MigrationWarning {
11
11
  code: string;
12
12
  file: string;
13
13
  /** Byte offsets into the authored file, or (0, 0) when no unique mapping exists. */
@@ -15,24 +15,21 @@ export interface MigrationWarning {
15
15
  end: number;
16
16
  message: string;
17
17
  }
18
-
19
- export interface RuleReport {
18
+ interface RuleReport {
20
19
  selector: string;
21
- status: 'converted' | 'retained';
20
+ status: "converted" | "retained";
22
21
  candidates: string[];
23
22
  file: string;
24
23
  /** Rule span in the analysis source (compiled CSS for preprocessor stylesheets). */
25
- ruleId: { start: number; end: number };
24
+ ruleId: RuleSpan;
26
25
  /** Rule span in the authored file, or (0, 0) when no unique mapping exists. */
27
- authoredSpan: { start: number; end: number };
26
+ authoredSpan: RuleSpan;
28
27
  }
29
-
30
- export interface MigrationFailure {
28
+ interface MigrationFailure {
31
29
  package: string;
32
30
  message: string;
33
31
  }
34
-
35
- export interface MigrationReport {
32
+ interface MigrationReport {
36
33
  changedFiles: string[];
37
34
  diff: string;
38
35
  convertedRules: number;
@@ -42,5 +39,12 @@ export interface MigrationReport {
42
39
  warnings: MigrationWarning[];
43
40
  failures: MigrationFailure[];
44
41
  }
45
-
46
- export function migrate(options?: MigrateOptions): Promise<MigrationReport>;
42
+ interface RuleSpan {
43
+ start: number;
44
+ end: number;
45
+ }
46
+ //#endregion
47
+ //#region src/index.d.ts
48
+ declare function migrate(options?: MigrateOptions): Promise<MigrationReport>;
49
+ //#endregion
50
+ export { type MigrateOptions, type MigrationFailure, type MigrationReport, type MigrationWarning, type RuleReport, migrate };