tw-migrate 0.1.0-alpha.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/native.js ADDED
@@ -0,0 +1,34 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const targets = {
5
+ 'darwin-arm64': 'darwin-arm64',
6
+ 'darwin-x64': 'darwin-x64',
7
+ 'linux-arm64': 'linux-arm64-gnu',
8
+ 'linux-x64': 'linux-x64-gnu',
9
+ 'win32-x64': 'win32-x64-msvc',
10
+ };
11
+ const target = targets[`${process.platform}-${process.arch}`];
12
+
13
+ if (!target) throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
14
+
15
+ let binding;
16
+ for (const load of [
17
+ () => require(`./tw-migrate.${target}.node`),
18
+ () => require(`tw-migrate-${target}`),
19
+ ]) {
20
+ try {
21
+ binding = load();
22
+ break;
23
+ } catch (error) {
24
+ if (error.code !== 'MODULE_NOT_FOUND') throw error;
25
+ }
26
+ }
27
+
28
+ if (!binding) {
29
+ throw new Error(`No tw-migrate native addon was found for ${target}. Reinstall the package or build it locally.`);
30
+ }
31
+
32
+ export const decodeSourceMap = binding.decodeSourceMap;
33
+ export const planBatchMigration = binding.planBatchMigration;
34
+ export const validateCss = binding.validateCss;
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "tw-migrate",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Preview and migrate React CSS to Tailwind utilities",
5
+ "keywords": [
6
+ "codemod",
7
+ "css",
8
+ "css-modules",
9
+ "migration",
10
+ "napi-rs",
11
+ "react",
12
+ "tailwind",
13
+ "tailwindcss"
14
+ ],
15
+ "homepage": "https://github.com/jong-kyung/tw-migrate#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/jong-kyung/tw-migrate/issues"
18
+ },
19
+ "license": "MIT",
20
+ "author": "JongKyung Lee",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/jong-kyung/tw-migrate.git"
24
+ },
25
+ "bin": {
26
+ "tw-migrate": "bin/tw-migrate.js"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "html.js",
31
+ "index.js",
32
+ "index.d.ts",
33
+ "native.js",
34
+ "rfcs",
35
+ "style-compiler.js"
36
+ ],
37
+ "type": "module",
38
+ "main": "index.js",
39
+ "types": "index.d.ts",
40
+ "scripts": {
41
+ "build": "napi build --manifest-path crates/tw_migrate/Cargo.toml --package-json-path package.json --output-dir . --platform --release --no-js --dts binding.d.ts",
42
+ "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",
43
+ "artifacts": "napi artifacts --output-dir . --npm-dir npm",
44
+ "test": "cargo test && pnpm build:debug && node --test",
45
+ "test:ecosystem": "node ecosystem-ci/run.ts",
46
+ "test:rust": "cargo test",
47
+ "typecheck": "tsc --noEmit",
48
+ "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",
49
+ "test:snapshots": "pnpm snapshots:prepare && cargo test -p tw-migrate-snapshots",
50
+ "prepare": "vp config"
51
+ },
52
+ "dependencies": {
53
+ "parse5": "7.3.0"
54
+ },
55
+ "devDependencies": {
56
+ "@napi-rs/cli": "3.7.3",
57
+ "@types/node": "22.20.1",
58
+ "@vitest/browser-playwright": "catalog:",
59
+ "less": "4.7.0",
60
+ "playwright": "1.61.1",
61
+ "sass": "1.101.3",
62
+ "tailwindcss": "4.3.3",
63
+ "typescript": "7.0.2",
64
+ "verdaccio": "6.8.0",
65
+ "vite": "catalog:",
66
+ "vite-plus": "catalog:",
67
+ "vitest": "catalog:"
68
+ },
69
+ "optionalDependencies": {
70
+ "tw-migrate-darwin-arm64": "0.1.0-alpha.0",
71
+ "tw-migrate-darwin-x64": "0.1.0-alpha.0",
72
+ "tw-migrate-linux-arm64-gnu": "0.1.0-alpha.0",
73
+ "tw-migrate-linux-x64-gnu": "0.1.0-alpha.0",
74
+ "tw-migrate-win32-x64-msvc": "0.1.0-alpha.0"
75
+ },
76
+ "napi": {
77
+ "binaryName": "tw-migrate",
78
+ "targets": [
79
+ "aarch64-apple-darwin",
80
+ "x86_64-apple-darwin",
81
+ "aarch64-unknown-linux-gnu",
82
+ "x86_64-unknown-linux-gnu",
83
+ "x86_64-pc-windows-msvc"
84
+ ]
85
+ },
86
+ "engines": {
87
+ "node": ">=22.18.0"
88
+ },
89
+ "packageManager": "pnpm@11.15.1"
90
+ }
@@ -0,0 +1,297 @@
1
+ # RFC: Batch CSS Migration
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Summary
8
+
9
+ `tw-migrate` currently requires one CSS file per invocation. This RFC extends it so an invocation without a CSS path discovers and migrates every eligible CSS file in the current package. A supplied CSS path continues to scope conversion to that file. Workspace-wide migration is explicit through `--workspaces`.
10
+
11
+ The batch operation reads the project once, builds one deterministic edit plan, validates the complete result, and applies it atomically. Independent per-CSS migrations are not written one after another because they can edit the same JavaScript or Tailwind entry from stale source.
12
+
13
+ ## Goals
14
+
15
+ 1. Migrate every eligible `.css` file in the current package when no CSS path is supplied.
16
+ 2. Preserve single-file migration when a CSS path is supplied.
17
+ 3. Find every supported JavaScript/TypeScript reference to each selected stylesheet.
18
+ 4. Group migration by the nearest package boundary and that package's Tailwind v4 entry.
19
+ 5. Support an explicit `--workspaces` mode for all packages below the workspace root.
20
+ 6. Respect `.gitignore` during automatic discovery.
21
+ 7. Produce one deterministic preview and one validated write transaction.
22
+ 8. Retain only conflicting rules while continuing independent conversions.
23
+ 9. Allow `--force` to skip failed package groups without weakening edit, Tailwind, or write validation.
24
+ 10. Establish a measurable baseline before adding parallel parsing.
25
+
26
+ ## Non-Goals
27
+
28
+ 1. SCSS, Sass, and Less parsing or migration in this release.
29
+ 2. Emotion or styled-components discovery.
30
+ 3. Selecting migration scope from a JavaScript or TypeScript positional path.
31
+ 4. Directory, glob, or include/exclude configuration beyond current package and workspace scope.
32
+ 5. Guessing ownership across package boundaries when a package has no unique Tailwind entry.
33
+ 6. Parallel analysis in the initial implementation.
34
+ 7. Automatic deletion of global CSS rules.
35
+
36
+ Preprocessor extensions must not be treated as CSS merely because they are discovered. They will be added only with parsers and source-editing rules that preserve their original syntax.
37
+
38
+ ## User Experience
39
+
40
+ ### CLI
41
+
42
+ ```bash
43
+ # Preview every eligible CSS file in the current package.
44
+ tw-migrate
45
+
46
+ # Preview one CSS file while scanning all relevant source references.
47
+ tw-migrate src/components/Button.module.css
48
+
49
+ # Apply the current package migration.
50
+ tw-migrate --write
51
+
52
+ # Apply every discovered workspace package.
53
+ tw-migrate --workspaces --write
54
+
55
+ # Skip recoverably failed package groups and apply successful groups.
56
+ tw-migrate --workspaces --write --force
57
+
58
+ # Resolve a package with an ambiguous Tailwind entry.
59
+ tw-migrate --tailwind-css src/app/globals.css --write
60
+ ```
61
+
62
+ The command accepts at most one positional CSS file. `--workspaces` cannot be combined with a positional CSS file because the positional path already defines the requested scope.
63
+
64
+ Preview remains the default. `--force` has no effect on unsupported rules that are already represented as warnings; those rules remain unchanged in every mode.
65
+
66
+ ### Node.js API
67
+
68
+ ```ts
69
+ interface MigrateOptions {
70
+ cssFile?: string;
71
+ cwd?: string;
72
+ write?: boolean;
73
+ tailwindCss?: string;
74
+ workspaces?: boolean;
75
+ force?: boolean;
76
+ }
77
+ ```
78
+
79
+ `migrate()` with no `cssFile` processes the current package. `workspaces: true` processes all package groups below the workspace root. Supplying both `cssFile` and `workspaces: true` is an error.
80
+
81
+ The existing aggregate report fields remain. Package failures skipped by `force` are returned separately:
82
+
83
+ ```ts
84
+ interface MigrationFailure {
85
+ package: string;
86
+ message: string;
87
+ }
88
+
89
+ interface MigrationReport {
90
+ changedFiles: string[];
91
+ diff: string;
92
+ convertedRules: number;
93
+ retainedRules: number;
94
+ rules: RuleReport[];
95
+ candidates: string[];
96
+ warnings: MigrationWarning[];
97
+ failures: MigrationFailure[];
98
+ }
99
+ ```
100
+
101
+ ## Scope Discovery
102
+
103
+ ### Package Boundaries
104
+
105
+ The current package is the nearest ancestor containing `package.json`. The workspace root is the Git root when available, otherwise the current package root.
106
+
107
+ In default mode only the current package contributes migration targets. In `--workspaces` mode, package directories below the workspace root are discovered from non-ignored `package.json` files. Every selected CSS file belongs to its nearest package boundary.
108
+
109
+ A package group must resolve exactly one Tailwind v4 CSS entry unless `tailwindCss` explicitly selects one for a single-package invocation. Tailwind entry files are inputs and possible edit destinations, never migration targets.
110
+
111
+ A package with no Tailwind entry or multiple entries fails before planning. In default mode this prevents every write. With `--force`, the package group is skipped.
112
+
113
+ ### CSS Targets
114
+
115
+ Automatic discovery selects `.css` files within the requested package groups after excluding:
116
+
117
+ - Tailwind entry files;
118
+ - `.git`, `.next`, `build`, `dist`, and `node_modules`;
119
+ - paths ignored by Git.
120
+
121
+ A positional CSS path overrides ignore rules and selects exactly that file. It must end in `.css` and belong to the current package.
122
+
123
+ ### Source References
124
+
125
+ Migration targets and writable sources are scoped separately:
126
+
127
+ - default mode writes CSS and supported JS/TS files in the current package;
128
+ - workspace mode writes files in all selected package groups;
129
+ - safety analysis scans supported JS/TS and CSS references throughout the workspace root.
130
+
131
+ If a selected CSS Module has a consumer outside the writable scope, the affected module rule and import are retained with a warning. The external consumer is not modified. This permits safe current-package migration without silently expanding its write scope.
132
+
133
+ ## Batch Architecture
134
+
135
+ ```text
136
+ resolve scope
137
+ -> discover non-ignored files once
138
+ -> snapshot CSS, JS/TS, package manifests, and Tailwind entries
139
+ -> group targets by package and Tailwind entry
140
+ -> plan every group against the immutable snapshot
141
+ -> merge edits by absolute path and original byte span
142
+ -> retain cross-stylesheet conflicts
143
+ -> validate edited syntax and Tailwind candidates
144
+ -> verify inputs still match the snapshot
145
+ -> print preview or atomically write
146
+ ```
147
+
148
+ ### One Snapshot
149
+
150
+ Every relevant file is read once before planning. The planner never reads a source file after another CSS plan has modified it. All edits target the same immutable source snapshot.
151
+
152
+ Before writing, every destination is read again and compared with its snapshot. A concurrent user or tool edit aborts the operation instead of being overwritten.
153
+
154
+ ### One Combined Plan
155
+
156
+ The native planner receives all CSS targets for a package group together. It resolves all CSS Module bindings and global selector candidates before producing JS edits. This avoids these unsafe patterns:
157
+
158
+ - two CSS files replacing the same `className` from different source versions;
159
+ - one CSS plan deleting an import needed by another stylesheet;
160
+ - multiple plans independently appending to the same Tailwind entry;
161
+ - the last file write silently discarding an earlier migration.
162
+
163
+ Edits are sorted deterministically by path and descending byte offset. Overlapping edits remaining after conflict handling are an internal planning error and abort the requested operation.
164
+
165
+ ### Cross-Stylesheet Conflicts
166
+
167
+ If different selected stylesheets generate utilities that affect the same Tailwind property and variant on the same JSX element, class attribute order cannot preserve the original cascade.
168
+
169
+ The planner therefore:
170
+
171
+ 1. identifies every contributing CSS rule;
172
+ 2. retains those rules in their original stylesheets;
173
+ 3. leaves their JS references unchanged;
174
+ 4. emits a stable warning naming the conflicting candidates;
175
+ 5. continues planning unrelated rules.
176
+
177
+ A retained conflict is a warning, not a package failure.
178
+
179
+ ### Tailwind Validation
180
+
181
+ Each package group loads its Tailwind installation and design system once. All candidates in that group are validated against that group's prefix, theme, config, and plugins.
182
+
183
+ A candidate validation failure is an integrity failure, not a recoverable unsupported rule. It aborts the complete requested operation even with `--force` because it indicates that the generated edit plan cannot be trusted.
184
+
185
+ ## Failure and Transaction Policy
186
+
187
+ ### Default
188
+
189
+ Without `--force`, the requested scope is one transaction. Discovery, parsing, planning, candidate validation, source validation, or pre-write snapshot failure causes no file changes.
190
+
191
+ ### Force
192
+
193
+ `--force` changes only package-group admission. A group with a recoverable discovery or parse failure is reported and excluded. Successful groups are then merged and written through one transaction.
194
+
195
+ `--force` never ignores:
196
+
197
+ - overlapping final edit ranges;
198
+ - invalid generated JavaScript, TypeScript, or CSS;
199
+ - invalid Tailwind candidates;
200
+ - changed inputs detected before commit;
201
+ - staging, rename, or rollback failures.
202
+
203
+ The package group is the minimum failure-isolation unit. CSS-file-level isolation is deferred because files in a group can share JS destinations and a Tailwind entry.
204
+
205
+ ### Atomic Write
206
+
207
+ All final files are staged before any destination is replaced. Existing files are renamed to backups, staged files are installed, deletions remain represented by backups, and backups are removed only after the complete operation succeeds. A failure restores every available backup.
208
+
209
+ File permissions are preserved. Leftover staging or backup files continue to block a later run until the user restores or removes them.
210
+
211
+ ## Git Ignore Behavior
212
+
213
+ Automatic discovery uses Git's ignore semantics when the workspace is a Git repository. Explicit CSS paths bypass ignore filtering. Outside Git, the fixed ignored-directory list still applies.
214
+
215
+ No new ignore-pattern parser or glob dependency is introduced for the initial implementation.
216
+
217
+ ## Determinism
218
+
219
+ Package groups, CSS targets, source files, warnings, candidates, and output operations are sorted by normalized path and stable secondary keys. Repeated previews from the same snapshot must be byte-for-byte identical.
220
+
221
+ The preview and write paths consume the same final plan. A successful second invocation must produce no additional changes.
222
+
223
+ ## Parallelism
224
+
225
+ Initial implementation is sequential after eliminating repeated project scans and repeated Tailwind loads. This provides the performance baseline and keeps the planner deterministic.
226
+
227
+ If benchmarks show CSS parsing is material, a later implementation may use a bounded worker pool for immutable CSS reads, parsing, and candidate extraction. Module resolution, edit merging, conflict handling, validation, and commit remain coordinated stages. Running complete per-CSS migrations concurrently is explicitly disallowed.
228
+
229
+ ## Testing Strategy
230
+
231
+ ### Discovery
232
+
233
+ - no positional path discovers all non-ignored CSS in the current package;
234
+ - a positional ignored CSS file is still processed;
235
+ - Tailwind entries and fixed generated directories are excluded;
236
+ - `--workspaces` discovers nested package groups;
237
+ - default mode does not write another package;
238
+ - workspace references outside writable scope prevent unsafe module cleanup.
239
+
240
+ ### Grouping
241
+
242
+ - one package entry is selected automatically;
243
+ - missing and duplicate entries fail deterministically;
244
+ - workspace packages use their own Tailwind installations and entries;
245
+ - a shared package without a unique entry is skipped only with `--force`.
246
+
247
+ ### Planning
248
+
249
+ - two CSS Modules can update different references in one JS file without lost edits;
250
+ - multiple selected rules can update one static `className`;
251
+ - cross-stylesheet utility conflicts retain only contributing rules;
252
+ - imports and CSS Module files are deleted only after all batch references are safe;
253
+ - candidates and warnings are deterministically ordered.
254
+
255
+ ### Transactions
256
+
257
+ - preview writes nothing;
258
+ - default failure writes nothing;
259
+ - `--force` excludes a failed package group and applies successful groups;
260
+ - candidate or edit-integrity failure aborts even with `--force`;
261
+ - a source changed after planning aborts the write;
262
+ - an interrupted write restores prior content;
263
+ - a second run is a no-op.
264
+
265
+ ## Implementation Sequence
266
+
267
+ 1. Extend CLI and API options while preserving single-file behavior.
268
+ 2. Add package-aware, Git-ignore-aware discovery and Tailwind grouping.
269
+ 3. Replace the single-CSS native request with a package batch request.
270
+ 4. Resolve all batch CSS Module and global candidates before planning source edits.
271
+ 5. Add cross-stylesheet conflict retention and combined edit validation.
272
+ 6. Aggregate package reports and implement `--force` package admission.
273
+ 7. Add pre-write snapshot checks and retain the existing atomic writer.
274
+ 8. Benchmark representative repositories before deciding whether to parallelize parsing.
275
+
276
+ ## Success Criteria
277
+
278
+ 1. `tw-migrate` previews every eligible CSS target in the current package.
279
+ 2. `tw-migrate <file>` preserves exact single-file scope.
280
+ 3. `--workspaces` processes package groups with their own Tailwind entries.
281
+ 4. No batch edit can overwrite another edit produced from a different source version.
282
+ 5. Cross-stylesheet conflicts retain their contributing rules without blocking independent work.
283
+ 6. Default writes are all-or-nothing for the requested scope.
284
+ 7. `--force` skips only recoverably failed package groups.
285
+ 8. Automatic discovery respects Git ignore rules.
286
+ 9. External workspace references prevent unsafe CSS Module deletion.
287
+ 10. Preview, write, and repeated-run behavior are deterministic.
288
+
289
+ ## Deferred Work
290
+
291
+ - SCSS, Sass, and Less parsers and source-preserving edits;
292
+ - Emotion and styled-components discovery from JS/TS inputs;
293
+ - JavaScript positional scope;
294
+ - configurable include/exclude globs;
295
+ - CSS-file-level failure isolation;
296
+ - dependency-graph ownership for shared CSS across multiple Tailwind applications;
297
+ - bounded parallel CSS parsing after benchmarks justify it.