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/LICENSE +21 -0
- package/README.md +121 -0
- package/bin/tw-migrate.js +46 -0
- package/html.js +153 -0
- package/index.d.ts +46 -0
- package/index.js +1254 -0
- package/native.js +34 -0
- package/package.json +90 -0
- package/rfcs/batch-css-migration.md +297 -0
- package/rfcs/browser-ecosystem-e2e.md +338 -0
- package/rfcs/css-to-tailwind-migration-cli.md +521 -0
- package/rfcs/preprocessor-and-html-migration.md +497 -0
- package/style-compiler.js +125 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# RFC: Preprocessor and Static HTML Migration
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
`tw-migrate` currently discovers plain CSS and rewrites JavaScript/TypeScript JSX consumers. This RFC extends the migration pipeline to authored `.scss`, `.sass`, and `.less` stylesheets and static `.html` consumers while preserving the existing conservative safety model.
|
|
10
|
+
|
|
11
|
+
Preprocessor support means that the target project's own Sass or Less compiler evaluates the language's real semantics. It does not mean every generated declaration is automatically editable. Authored preprocessor source is changed only when generated CSS can be mapped back to one unambiguous original source range and the edit remains safe for every entry and consumer that shares it. Ambiguous constructs remain unchanged with warnings.
|
|
12
|
+
|
|
13
|
+
Static HTML support is limited to literal `class` and `id` attributes reached through local external stylesheet links. It does not introduce support for template languages, inline styles, or runtime DOM analysis.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
1. Accept `.css`, `.scss`, `.sass`, and `.less` authored stylesheet targets.
|
|
18
|
+
2. Evaluate SCSS/Sass/Less with the compiler installed in the target package.
|
|
19
|
+
3. Preserve the original stylesheet language and all unrelated source bytes.
|
|
20
|
+
4. Convert compiler-resolved values to exact Tailwind theme utilities or arbitrary values.
|
|
21
|
+
5. Support `.module.scss`, `.module.sass`, and `.module.less` in existing JS/TS JSX flows.
|
|
22
|
+
6. Analyze only preprocessor entries reachable from supported consumers; treat imported partials as dependencies rather than standalone entries.
|
|
23
|
+
7. Edit a shared partial only after every reachable entry and consumer proves the same edit safe.
|
|
24
|
+
8. Support literal `class` and `id` attributes in static `.html` files.
|
|
25
|
+
9. Scope HTML matching through local `<link rel="stylesheet">` and stylesheet import graphs.
|
|
26
|
+
10. Connect linked generated CSS to one unique same-stem preprocessor entry without requiring a prior build.
|
|
27
|
+
11. Remove a fully migrated CSS Module and each writable direct HTML link that consumed it.
|
|
28
|
+
12. Preserve the current package-batch snapshot, conflict, validation, and atomic-write guarantees.
|
|
29
|
+
13. Preserve package-level `--force` failure isolation.
|
|
30
|
+
|
|
31
|
+
## Non-Goals
|
|
32
|
+
|
|
33
|
+
1. Vue, Svelte, Astro, Angular, PHP, ERB, or other template languages.
|
|
34
|
+
2. HTML `<style>` blocks or `style` attributes.
|
|
35
|
+
3. Template expressions, bound attributes, or runtime-generated classes in `.html` files.
|
|
36
|
+
4. Automatic loading or execution of Vite, Webpack, PostCSS, or framework configuration.
|
|
37
|
+
5. Custom Sass importers, Sass plugins, Less plugins, or build-tool aliases.
|
|
38
|
+
6. Bundling Sass or Less with `tw-migrate`.
|
|
39
|
+
7. Replacing authored preprocessor files with generated CSS.
|
|
40
|
+
8. Editing generated CSS or generated source maps.
|
|
41
|
+
9. Automatic deletion of global stylesheet rules.
|
|
42
|
+
10. Guessing generated CSS-to-preprocessor relationships when no unique same-stem entry exists.
|
|
43
|
+
11. Automatic promotion of Sass/Less variables into Tailwind `@theme` variables.
|
|
44
|
+
12. A guarantee that every valid preprocessor construct is automatically migrated.
|
|
45
|
+
|
|
46
|
+
## Terminology
|
|
47
|
+
|
|
48
|
+
- **Authored stylesheet**: a user-owned `.css`, `.scss`, `.sass`, or `.less` source file.
|
|
49
|
+
- **Entry**: a stylesheet directly consumed by JS/TS, directly selected with `styleFile`, directly linked from HTML, or identified from linked generated CSS by a unique same-stem filename.
|
|
50
|
+
- **Partial**: a stylesheet loaded by an entry through CSS import, Sass `@use`/`@forward`/`@import`, or Less import semantics.
|
|
51
|
+
- **Compiled context**: one entry's generated CSS, source map, loaded-source graph, and accumulated consumer condition such as link media.
|
|
52
|
+
- **Provenance**: evidence connecting a generated CSS rule or declaration to one authored file and source span.
|
|
53
|
+
- **Safe edit**: a non-overlapping authored-source edit with unique provenance that remains valid in every consuming compiled context.
|
|
54
|
+
|
|
55
|
+
## Public Contract
|
|
56
|
+
|
|
57
|
+
### CLI
|
|
58
|
+
|
|
59
|
+
The positional argument becomes a stylesheet path rather than a CSS-only path:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Preview all reachable stylesheets in the current package.
|
|
63
|
+
tw-migrate
|
|
64
|
+
|
|
65
|
+
# Preview one authored stylesheet.
|
|
66
|
+
tw-migrate src/components/Button.module.scss
|
|
67
|
+
|
|
68
|
+
# Migrate a package and its static HTML consumers.
|
|
69
|
+
tw-migrate --write
|
|
70
|
+
|
|
71
|
+
# Tailwind's entry remains plain CSS.
|
|
72
|
+
tw-migrate src/styles/site.less --tailwind-css src/tailwind.css --write
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The usage text changes from `[css-file]` to `[style-file]`.
|
|
76
|
+
|
|
77
|
+
### Node.js API
|
|
78
|
+
|
|
79
|
+
`cssFile` is removed and replaced immediately by `styleFile`. No compatibility alias is provided.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
interface MigrateOptions {
|
|
83
|
+
styleFile?: string;
|
|
84
|
+
cwd?: string;
|
|
85
|
+
write?: boolean;
|
|
86
|
+
tailwindCss?: string;
|
|
87
|
+
workspaces?: boolean;
|
|
88
|
+
force?: boolean;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`tailwindCss` continues to accept only `.css` files.
|
|
93
|
+
|
|
94
|
+
### Reports
|
|
95
|
+
|
|
96
|
+
The existing report shape remains. Preprocessor-specific state is represented through stable warnings rather than a new top-level field.
|
|
97
|
+
|
|
98
|
+
A changed authored preprocessor source produces one warning per affected entry:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
{
|
|
102
|
+
code: 'rebuild-required',
|
|
103
|
+
file: 'src/styles/site.scss',
|
|
104
|
+
start: 0,
|
|
105
|
+
end: 0,
|
|
106
|
+
message: 'Rebuild this preprocessor entry to refresh its generated CSS.'
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Generated CSS and source maps are never listed as changed files unless they are independent authored CSS migration targets.
|
|
111
|
+
|
|
112
|
+
## Supported Inputs
|
|
113
|
+
|
|
114
|
+
### Stylesheets
|
|
115
|
+
|
|
116
|
+
| Extension | Parser syntax | Semantic evaluator |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `.css` | `Syntax::Css` | none |
|
|
119
|
+
| `.scss` | `Syntax::Scss` | target package's `sass` |
|
|
120
|
+
| `.sass` | `Syntax::Sass` | target package's `sass` |
|
|
121
|
+
| `.less` | `Syntax::Less` | target package's `less` |
|
|
122
|
+
|
|
123
|
+
CSS Modules are recognized by `.module.css`, `.module.scss`, `.module.sass`, and `.module.less`.
|
|
124
|
+
|
|
125
|
+
### Consumers
|
|
126
|
+
|
|
127
|
+
Existing JS/TS extensions remain supported. `.html` is added as a static consumer format.
|
|
128
|
+
|
|
129
|
+
HTML migration reads only:
|
|
130
|
+
|
|
131
|
+
- local `<link rel="stylesheet" href="…">` references;
|
|
132
|
+
- literal `class` attributes;
|
|
133
|
+
- literal `id` attributes;
|
|
134
|
+
- supported `media` conditions attached to stylesheet links.
|
|
135
|
+
|
|
136
|
+
Remote, protocol-relative, `data:`, and dynamically generated links are ignored with warnings when they block a migration.
|
|
137
|
+
|
|
138
|
+
## Architecture
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
resolve package/workspace scope
|
|
142
|
+
-> snapshot relevant authored styles, JS/TS, HTML, manifests
|
|
143
|
+
-> discover consumer-to-entry relationships
|
|
144
|
+
-> load project Sass/Less compiler when required
|
|
145
|
+
-> compile each reachable preprocessor entry with source maps
|
|
146
|
+
-> build entry/partial/link/import graphs
|
|
147
|
+
-> parse compiled CSS for candidates
|
|
148
|
+
-> prove generated-to-authored provenance
|
|
149
|
+
-> plan JS/TS and HTML consumer edits together
|
|
150
|
+
-> retain cross-stylesheet conflicts
|
|
151
|
+
-> admit only all-consumer-safe authored edits
|
|
152
|
+
-> validate candidates, syntax, recompilation, and snapshots
|
|
153
|
+
-> preview or atomically write one transaction
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Responsibility Split
|
|
157
|
+
|
|
158
|
+
The Node layer owns:
|
|
159
|
+
|
|
160
|
+
- filesystem discovery and immutable snapshots;
|
|
161
|
+
- package ownership;
|
|
162
|
+
- project-local Sass/Less loading;
|
|
163
|
+
- compilation and compiler-loaded dependency discovery;
|
|
164
|
+
- source-map normalization;
|
|
165
|
+
- HTML link and stylesheet entry graph construction;
|
|
166
|
+
- Tailwind loading and candidate validation;
|
|
167
|
+
- final merged preview/write transaction.
|
|
168
|
+
|
|
169
|
+
The Rust layer owns:
|
|
170
|
+
|
|
171
|
+
- dialect-aware stylesheet AST parsing;
|
|
172
|
+
- selector and declaration planning;
|
|
173
|
+
- generated rule provenance consumption;
|
|
174
|
+
- JS/TS semantic rewriting;
|
|
175
|
+
- static HTML attribute rewriting;
|
|
176
|
+
- cross-stylesheet conflict handling;
|
|
177
|
+
- non-overlapping source edits and syntax validation.
|
|
178
|
+
|
|
179
|
+
A small private Node module should isolate compiler adapters and source-map normalization from `index.js`. Sass and Less are loaded with `createRequire()` rooted at the target package, following the existing Tailwind-loading pattern.
|
|
180
|
+
|
|
181
|
+
## Discovery and Graph Rules
|
|
182
|
+
|
|
183
|
+
### Entry Admission
|
|
184
|
+
|
|
185
|
+
Automatic migration does not treat every preprocessor file as an independent entry. Entries are admitted only when reached from:
|
|
186
|
+
|
|
187
|
+
1. a supported JS/TS stylesheet import;
|
|
188
|
+
2. a local HTML stylesheet link;
|
|
189
|
+
3. the unique non-partial preprocessor file whose stem matches a linked CSS filename;
|
|
190
|
+
4. another admitted stylesheet's import graph where that stylesheet is itself an executable entry; or
|
|
191
|
+
5. an explicit `styleFile` selection.
|
|
192
|
+
|
|
193
|
+
An explicitly selected partial is evaluated through every discovered entry that consumes it. If no complete consuming entry set can be established, it is retained rather than compiled as an invented entry.
|
|
194
|
+
|
|
195
|
+
### Compiler-Reported Dependencies
|
|
196
|
+
|
|
197
|
+
Sass and Less compiler-loaded URLs/files are authoritative for entry-to-partial edges. A conservative static import scan may suppress filename inference, but it never admits dependencies or replaces the compiler-loaded graph.
|
|
198
|
+
|
|
199
|
+
Only standard compiler resolution is enabled. Build-tool aliases, custom importers, and plugins are not loaded. An unresolved compiler dependency is a recoverable package input failure.
|
|
200
|
+
|
|
201
|
+
### HTML Relationships
|
|
202
|
+
|
|
203
|
+
A static HTML page is affected only by styles reachable through its local stylesheet links and transitive local CSS imports.
|
|
204
|
+
|
|
205
|
+
HTML migration does not inspect source maps emitted by prior project builds. Whether the linked CSS exists or not, it connects the link to a preprocessor entry only when exactly one non-partial file in the package has the same filename stem. The inferred relationship emits `inferred-preprocessor-source`; zero or multiple matches are not inferred. Once admitted, the project compiler's in-memory source map remains required to prove generated-to-authored edit spans.
|
|
206
|
+
|
|
207
|
+
Root-relative links are resolved against the package root. Query strings and fragments are excluded from filesystem resolution but preserved in HTML. Remote or virtual URLs are not writable graph edges.
|
|
208
|
+
|
|
209
|
+
When every rule and every JS/TS and static HTML reference to a CSS Module is migrated, each writable direct HTML `<link>` for that module is removed with the now-unused module dependency. A reference-only HTML file, dynamic attribute, unsupported media condition, retained rule, or otherwise unproven consumer retains both the module and its link. Transitive links are not removed; their stylesheet import edge retains the dependency instead.
|
|
210
|
+
|
|
211
|
+
### Shared Partials
|
|
212
|
+
|
|
213
|
+
One authored partial may occur in several compiled contexts. A proposed edit is admitted only when:
|
|
214
|
+
|
|
215
|
+
1. every reachable entry containing the source range was compiled;
|
|
216
|
+
2. every affected writable and reference-only consumer was analyzed;
|
|
217
|
+
3. every context proposes the same authored path, span, and replacement;
|
|
218
|
+
4. no context retains the rule or class dependency;
|
|
219
|
+
5. the edit introduces no cross-context Tailwind conflict; and
|
|
220
|
+
6. every affected entry recompiles after the edit.
|
|
221
|
+
|
|
222
|
+
Otherwise the source remains unchanged and the affected rule is reported as retained.
|
|
223
|
+
|
|
224
|
+
## Preprocessor Compilation
|
|
225
|
+
|
|
226
|
+
### Compiler Ownership
|
|
227
|
+
|
|
228
|
+
`tw-migrate` does not bundle Sass or Less. It resolves `sass` or `less` from the target package. The project therefore controls the compiler version used by its own migration.
|
|
229
|
+
|
|
230
|
+
If a required compiler cannot be resolved:
|
|
231
|
+
|
|
232
|
+
- normal mode fails the package operation before writing;
|
|
233
|
+
- workspace `--force` records a package failure and continues with admitted package groups.
|
|
234
|
+
|
|
235
|
+
### Analysis-Only Output
|
|
236
|
+
|
|
237
|
+
Compiler-generated CSS and maps are ephemeral planning inputs. They are never substituted for authored source.
|
|
238
|
+
|
|
239
|
+
Compiler results must include:
|
|
240
|
+
|
|
241
|
+
- generated CSS;
|
|
242
|
+
- a source map when authored edits depend on generated values;
|
|
243
|
+
- the entry path;
|
|
244
|
+
- normalized loaded source paths;
|
|
245
|
+
- enough origin information to identify writable project sources.
|
|
246
|
+
|
|
247
|
+
Virtual, external, dependency-owned, or unsnapshotted sources are analysis-only and cannot receive edits.
|
|
248
|
+
|
|
249
|
+
### Resolved Values
|
|
250
|
+
|
|
251
|
+
A generated declaration value may produce:
|
|
252
|
+
|
|
253
|
+
1. an exact utility from the target Tailwind theme;
|
|
254
|
+
2. a validated arbitrary value; or
|
|
255
|
+
3. an unsupported-declaration warning.
|
|
256
|
+
|
|
257
|
+
For example:
|
|
258
|
+
|
|
259
|
+
```scss
|
|
260
|
+
$space: 13px;
|
|
261
|
+
.card { padding: $space; }
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
may produce `p-[13px]`. The variable definition is not automatically removed, even if it becomes unused.
|
|
265
|
+
|
|
266
|
+
## Provenance and Source Editing
|
|
267
|
+
|
|
268
|
+
Generated CSS spans and authored source spans are distinct types in the planning contract. A generated span must never be applied directly to an authored preprocessor source.
|
|
269
|
+
|
|
270
|
+
A preprocessor declaration or rule is editable only when source-map and AST evidence resolves it to one contiguous authored span. The following are retained:
|
|
271
|
+
|
|
272
|
+
- missing or split mappings;
|
|
273
|
+
- mappings spanning multiple authored files;
|
|
274
|
+
- interpolation-generated selectors;
|
|
275
|
+
- declarations generated by ambiguous mixin/function expansion;
|
|
276
|
+
- Less inline JavaScript;
|
|
277
|
+
- generated or virtual sources;
|
|
278
|
+
- differing edits proposed by separate entries;
|
|
279
|
+
- edits whose post-edit compilation cannot be verified.
|
|
280
|
+
|
|
281
|
+
Edits are applied as byte-range splices to original source. The tool does not print or serialize a whole stylesheet AST.
|
|
282
|
+
|
|
283
|
+
After tentative authored edits, every affected entry is recompiled. The final generated CSS must parse, the expected removed or retained behavior must hold, and candidate validation must still succeed before the edit enters the transaction.
|
|
284
|
+
|
|
285
|
+
## Static HTML Rewriting
|
|
286
|
+
|
|
287
|
+
HTML uses a span-bearing parser. Regular expressions are not used to rewrite attributes.
|
|
288
|
+
|
|
289
|
+
A `class` or `id` attribute is writable only when:
|
|
290
|
+
|
|
291
|
+
- the parser provides an original byte location;
|
|
292
|
+
- its value is a plain literal;
|
|
293
|
+
- it contains no recognized template or binding syntax;
|
|
294
|
+
- the element is reached by a proven linked stylesheet context; and
|
|
295
|
+
- the planned candidates do not conflict with another linked stylesheet.
|
|
296
|
+
|
|
297
|
+
The implementation edits only the attribute value span and preserves quote style, tag formatting, comments, entities, and unrelated bytes.
|
|
298
|
+
|
|
299
|
+
The following remain unchanged:
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
<div class="card {{ state }}"></div>
|
|
303
|
+
<div :class="state"></div>
|
|
304
|
+
<div class="card" style="padding: 1rem"></div> <!-- style is ignored -->
|
|
305
|
+
<style>.card { padding: 1rem }</style> <!-- block is ignored -->
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`<template>` descendants containing plain literal HTML are eligible. Script contents are never scanned for class strings.
|
|
309
|
+
|
|
310
|
+
### Link Media
|
|
311
|
+
|
|
312
|
+
A stylesheet link without `media`, or with `media="all"`, applies normally. Supported conditions are converted to the same Tailwind variants used for equivalent CSS `@media` conditions and are stacked outside rule-level variants.
|
|
313
|
+
|
|
314
|
+
If a link media condition cannot be represented exactly, that link context does not produce consumer edits and emits a warning. The condition is never ignored.
|
|
315
|
+
|
|
316
|
+
## Global Rules and CSS Modules
|
|
317
|
+
|
|
318
|
+
Global stylesheet rules are never deleted, regardless of whether their consumer is JSX or HTML and regardless of source language. Utilities may be appended to proven consumers, and the existing `retained-global-rule` warning remains.
|
|
319
|
+
|
|
320
|
+
Preprocessor CSS Module cleanup follows existing module safety rules plus provenance and shared-partial proof. A module reference or rule is removed only when every declaration, source origin, imported dependency, and consumer is safe. Otherwise both the source rule and required module reference remain.
|
|
321
|
+
|
|
322
|
+
## Validation and Transaction Policy
|
|
323
|
+
|
|
324
|
+
All existing batch invariants remain mandatory:
|
|
325
|
+
|
|
326
|
+
1. relevant inputs are snapshotted before planning;
|
|
327
|
+
2. all package edits target one immutable snapshot;
|
|
328
|
+
3. cross-stylesheet conflicts are resolved before source edits;
|
|
329
|
+
4. every Tailwind candidate compiles in the target package;
|
|
330
|
+
5. edited JS/TS, HTML, CSS, SCSS, Sass, and Less inputs reparse;
|
|
331
|
+
6. affected preprocessor entries recompile;
|
|
332
|
+
7. overlapping or disagreeing authored edits abort planning;
|
|
333
|
+
8. snapshots are checked immediately before writing;
|
|
334
|
+
9. all files are staged before any destination is replaced;
|
|
335
|
+
10. rollback restores every available backup after a failed write;
|
|
336
|
+
11. a successful second run produces no diff.
|
|
337
|
+
|
|
338
|
+
### Failure Classification
|
|
339
|
+
|
|
340
|
+
Recoverable package input failures include:
|
|
341
|
+
|
|
342
|
+
- missing project Sass/Less compiler;
|
|
343
|
+
- invalid initial preprocessor syntax;
|
|
344
|
+
- standard import resolution failure;
|
|
345
|
+
- invalid initial writable HTML input;
|
|
346
|
+
- unsupported source type.
|
|
347
|
+
|
|
348
|
+
Warnings retain individual constructs when a file remains analyzable, including ambiguous provenance, dynamic HTML attributes, unsupported link media, and shared partials without complete edit proof.
|
|
349
|
+
|
|
350
|
+
Candidate failures, generated edit parse failures, post-edit compiler failures, source-map/edit-integrity errors, changed snapshots, staging errors, and rollback failures remain fatal even with `--force`.
|
|
351
|
+
|
|
352
|
+
## Diagnostics
|
|
353
|
+
|
|
354
|
+
New stable warning categories include:
|
|
355
|
+
|
|
356
|
+
- `rebuild-required`;
|
|
357
|
+
- `unproven-source-map`;
|
|
358
|
+
- `shared-preprocessor-source`;
|
|
359
|
+
- `dynamic-html-attribute`;
|
|
360
|
+
- `unproven-script-reference`;
|
|
361
|
+
- `unsupported-html-base`;
|
|
362
|
+
- `unsupported-html-stylesheet-link`;
|
|
363
|
+
- `unsupported-link-media`;
|
|
364
|
+
- `cross-package-stylesheet-link`;
|
|
365
|
+
- `inferred-preprocessor-source`.
|
|
366
|
+
|
|
367
|
+
Warnings retain the existing `{code, file, start, end, message}` shape and deterministic ordering.
|
|
368
|
+
|
|
369
|
+
## Implementation Phases
|
|
370
|
+
|
|
371
|
+
### Phase 1: Generalized Stylesheet Contract
|
|
372
|
+
|
|
373
|
+
- Rename `cssFile` to `styleFile` in CLI, API, types, and errors.
|
|
374
|
+
- Add shared stylesheet extension/syntax/module helpers.
|
|
375
|
+
- Pass explicit syntax and module metadata into Rust.
|
|
376
|
+
- Parse and validate `.css`, `.scss`, `.sass`, and `.less` with Oxc.
|
|
377
|
+
- Keep Tailwind entry discovery and validation CSS-only.
|
|
378
|
+
- Add regression fixtures for discovery, explicit selection, modules, and failures.
|
|
379
|
+
|
|
380
|
+
Phase 1 does not claim evaluated variables or source-map-backed cleanup; constructs without direct safe values remain unchanged.
|
|
381
|
+
|
|
382
|
+
### Phase 2: SCSS and Sass Semantics
|
|
383
|
+
|
|
384
|
+
- Load target-package `sass`.
|
|
385
|
+
- Compile admitted entries with source maps and loaded source reporting.
|
|
386
|
+
- Build Sass entry/partial graphs.
|
|
387
|
+
- Resolve generated values and prove authored provenance.
|
|
388
|
+
- Recompile all affected entries after tentative edits.
|
|
389
|
+
- Emit deterministic rebuild warnings.
|
|
390
|
+
|
|
391
|
+
### Phase 3: Less Semantics
|
|
392
|
+
|
|
393
|
+
- Load target-package `less`.
|
|
394
|
+
- Add Less entry/import graph and source-map normalization.
|
|
395
|
+
- Apply the same provenance, shared-source, recompilation, and warning rules.
|
|
396
|
+
|
|
397
|
+
### Phase 4: Static HTML Consumers
|
|
398
|
+
|
|
399
|
+
- Add span-bearing static HTML parsing and attribute edits.
|
|
400
|
+
- Build local link/import and same-stem filename relationships.
|
|
401
|
+
- Add supported link-media variants.
|
|
402
|
+
- Route HTML matches through batch conflict handling.
|
|
403
|
+
- Preserve global rules and reject dynamic/template attributes.
|
|
404
|
+
|
|
405
|
+
Each phase is independently testable and may merge separately. Public documentation must describe only phases that are actually released.
|
|
406
|
+
|
|
407
|
+
## Testing Strategy
|
|
408
|
+
|
|
409
|
+
### Contract and Discovery
|
|
410
|
+
|
|
411
|
+
- `styleFile` accepts all supported extensions.
|
|
412
|
+
- legacy `cssFile` does not select a target.
|
|
413
|
+
- `styleFile` cannot be combined with `workspaces`.
|
|
414
|
+
- `tailwindCss` rejects non-CSS entries.
|
|
415
|
+
- automatic discovery admits entries but not standalone partials.
|
|
416
|
+
- ignored automatic files remain analysis-only; explicit targets preserve existing override behavior.
|
|
417
|
+
|
|
418
|
+
### Dialects and Modules
|
|
419
|
+
|
|
420
|
+
- plain CSS behavior remains byte-for-byte compatible.
|
|
421
|
+
- `.module.scss`, `.module.sass`, and `.module.less` resolve existing static JSX module forms.
|
|
422
|
+
- syntax selection is correct for all four extensions.
|
|
423
|
+
- malformed inputs follow default and `--force` package behavior.
|
|
424
|
+
|
|
425
|
+
### Semantic Evaluation
|
|
426
|
+
|
|
427
|
+
- Sass and Less variables resolve to exact or arbitrary utilities.
|
|
428
|
+
- nested selectors and supported conditions produce equivalent variants.
|
|
429
|
+
- mixin, function, interpolation, guard, and generated-selector cases migrate only with unique provenance.
|
|
430
|
+
- compiler absence and standard import failure are deterministic.
|
|
431
|
+
- project compiler resolution is package-relative.
|
|
432
|
+
|
|
433
|
+
### Graph and Provenance
|
|
434
|
+
|
|
435
|
+
- imported partials are not independently compiled as entries.
|
|
436
|
+
- extensionless, underscore, index, `@use`, `@forward`, and Less import graphs use compiler-reported dependencies.
|
|
437
|
+
- shared partials edit only after all entry contexts agree.
|
|
438
|
+
- missing, split, virtual, and conflicting compiler maps retain source.
|
|
439
|
+
- post-edit recompilation failure writes nothing.
|
|
440
|
+
- changed preprocessor sources emit one rebuild warning per affected entry.
|
|
441
|
+
|
|
442
|
+
### HTML
|
|
443
|
+
|
|
444
|
+
- local linked CSS updates literal class and id attributes.
|
|
445
|
+
- unlinked pages and unrelated styles remain unchanged.
|
|
446
|
+
- transitive local CSS imports are honored.
|
|
447
|
+
- linked generated CSS reaches a preprocessor source only through one unique same-stem filename, including before the first build and regardless of prior build maps.
|
|
448
|
+
- supported link media stacks variants; unsupported media retains.
|
|
449
|
+
- quote style and unrelated HTML bytes remain unchanged.
|
|
450
|
+
- template-looking values, bound attributes, inline styles, style blocks, scripts, and remote links are not rewritten.
|
|
451
|
+
- linked cross-stylesheet conflicts retain contributing rules and consumer edits.
|
|
452
|
+
- fully migrated CSS Modules remove direct writable HTML links, including HTML-only and inferred-preprocessor consumption.
|
|
453
|
+
- reference-only HTML, entity-bearing links, dynamic attributes, and unsupported link media cannot leave a dangling link.
|
|
454
|
+
|
|
455
|
+
### Transaction and Regression
|
|
456
|
+
|
|
457
|
+
- preview writes nothing.
|
|
458
|
+
- write uses one package transaction.
|
|
459
|
+
- workspace `--force` skips only recoverable package groups.
|
|
460
|
+
- snapshot, candidate, overlap, post-edit compile, and write failures abort.
|
|
461
|
+
- interrupted writes restore HTML and partial files.
|
|
462
|
+
- all existing CSS/JS/TS tests remain green.
|
|
463
|
+
- a successful second invocation produces zero diff.
|
|
464
|
+
|
|
465
|
+
## Success Criteria
|
|
466
|
+
|
|
467
|
+
1. The CLI and Node API accept the released stylesheet extensions through `styleFile`.
|
|
468
|
+
2. Tailwind entries remain `.css` and candidate validation uses the target package's Tailwind v4 installation.
|
|
469
|
+
3. Preprocessor values are evaluated by the target package's own compiler.
|
|
470
|
+
4. No generated CSS span is ever applied to authored preprocessor source.
|
|
471
|
+
5. Ambiguous mappings and incomplete consumer graphs retain source with warnings.
|
|
472
|
+
6. Shared partial edits are safe across every reachable entry and consumer.
|
|
473
|
+
7. Static HTML edits are link-scoped, literal-only, and byte-local.
|
|
474
|
+
8. Global rules are never automatically deleted.
|
|
475
|
+
9. Generated CSS is not edited; changed preprocessor entries produce rebuild warnings.
|
|
476
|
+
10. A CSS Module is unlinked from static HTML only when every rule and consumer is safely migrated.
|
|
477
|
+
11. Existing snapshot, conflict, atomic write, rollback, `--force`, determinism, and idempotence guarantees remain intact.
|
|
478
|
+
|
|
479
|
+
## Accepted Trade-offs
|
|
480
|
+
|
|
481
|
+
1. Full language evaluation produces conservative conversion coverage; valid constructs may remain when source provenance is ambiguous.
|
|
482
|
+
2. Projects must already install the compiler required by their authored language.
|
|
483
|
+
3. Custom build resolution is unsupported unless the standard compiler can resolve the graph independently.
|
|
484
|
+
4. HTML-to-preprocessor relationships ignore prior build maps, use a warned same-stem filename inference, and skip ambiguous matches.
|
|
485
|
+
5. Generated CSS may remain absent or stale until the project rebuilds it.
|
|
486
|
+
6. Global migrations intentionally duplicate validated utilities while retaining authored global rules.
|
|
487
|
+
7. The immediate `cssFile` to `styleFile` rename is a breaking API change.
|
|
488
|
+
|
|
489
|
+
## Deferred Work
|
|
490
|
+
|
|
491
|
+
- Explicit custom load paths or alias mapping.
|
|
492
|
+
- Optional build-script execution after write.
|
|
493
|
+
- Additional HTML-like template languages.
|
|
494
|
+
- Inline style migration.
|
|
495
|
+
- Automatic `@theme` extraction from preprocessor variables.
|
|
496
|
+
- Generated artifact refresh.
|
|
497
|
+
- Configurable global-rule cleanup after runtime evidence.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { decodeSourceMap } from './native.js';
|
|
7
|
+
|
|
8
|
+
const SASS_EXTENSIONS = new Set(['.scss', '.sass']);
|
|
9
|
+
|
|
10
|
+
export function isSassPath(path) {
|
|
11
|
+
return SASS_EXTENSIONS.has(extname(path));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isPreprocessorPath(path) {
|
|
15
|
+
return isSassPath(path) || extname(path) === '.less';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function loadProjectModule(packageRoot, name, errorMessage) {
|
|
19
|
+
const projectRequire = createRequire(join(packageRoot, 'package.json'));
|
|
20
|
+
let modulePath;
|
|
21
|
+
try {
|
|
22
|
+
modulePath = projectRequire.resolve(name);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(errorMessage);
|
|
25
|
+
}
|
|
26
|
+
const imported = await import(pathToFileURL(modulePath));
|
|
27
|
+
return imported.default ?? imported;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function loadProjectSass(packageRoot) {
|
|
31
|
+
return loadProjectModule(packageRoot, 'sass', 'Sass must be installed in the target project.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadProjectLess(packageRoot) {
|
|
35
|
+
return loadProjectModule(packageRoot, 'less', 'Less must be installed in the target project.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function compileSassEntry(sass, entryPath, source) {
|
|
39
|
+
const options = {
|
|
40
|
+
sourceMap: true,
|
|
41
|
+
sourceMapIncludeSources: true,
|
|
42
|
+
};
|
|
43
|
+
const result = source === undefined
|
|
44
|
+
? await sass.compileAsync(entryPath, options)
|
|
45
|
+
: await sass.compileStringAsync(source, {
|
|
46
|
+
...options,
|
|
47
|
+
url: pathToFileURL(entryPath),
|
|
48
|
+
syntax: extname(entryPath) === '.sass' ? 'indented' : 'scss',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!result.sourceMap) throw new Error(`Sass did not produce a source map for ${entryPath}`);
|
|
52
|
+
const loadedPaths = result.loadedUrls
|
|
53
|
+
.filter((url) => url.protocol === 'file:')
|
|
54
|
+
.map(fileURLToPath);
|
|
55
|
+
const mappings = sourceMappings(result.sourceMap);
|
|
56
|
+
const normalizedPaths = await normalizeEntryPaths(
|
|
57
|
+
[...loadedPaths, ...mappings.map((mapping) => mapping.sourcePath)],
|
|
58
|
+
entryPath,
|
|
59
|
+
);
|
|
60
|
+
return {
|
|
61
|
+
css: result.css,
|
|
62
|
+
loadedPaths: loadedPaths.map((path) => normalizedPaths.get(path)),
|
|
63
|
+
sourceMappings: mappings.map((mapping) => ({
|
|
64
|
+
...mapping,
|
|
65
|
+
sourcePath: normalizedPaths.get(mapping.sourcePath),
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function normalizeEntryPaths(paths, entryPath) {
|
|
71
|
+
const canonicalEntryPath = await realpath(entryPath);
|
|
72
|
+
return new Map(await Promise.all([...new Set(paths)].map(async (path) => {
|
|
73
|
+
if (path === entryPath) return [path, path];
|
|
74
|
+
try {
|
|
75
|
+
return [path, await realpath(path) === canonicalEntryPath ? entryPath : path];
|
|
76
|
+
} catch {
|
|
77
|
+
return [path, path];
|
|
78
|
+
}
|
|
79
|
+
})));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function compileLessEntry(less, entryPath, source) {
|
|
83
|
+
const result = await less.render(source, {
|
|
84
|
+
filename: entryPath,
|
|
85
|
+
sourceMap: {
|
|
86
|
+
outputSourceFiles: true,
|
|
87
|
+
sourceMapBasepath: dirname(entryPath),
|
|
88
|
+
sourceMapOutputFilename: `${basename(entryPath, '.less')}.css`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
css: result.css,
|
|
93
|
+
loadedPaths: result.imports.map((path) => isAbsolute(path) ? path : resolve(dirname(entryPath), path)),
|
|
94
|
+
sourceMappings: result.map ? sourceMappings(JSON.parse(result.map), dirname(entryPath)) : [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function sourceMappings(sourceMap, sourceBase) {
|
|
99
|
+
return JSON.parse(decodeSourceMap(JSON.stringify(sourceMap))).flatMap(({ source, ...mapping }) => {
|
|
100
|
+
const sourcePath = sourcePathFromMap(sourceReferenceFromMap(source, sourceMap.sourceRoot), sourceBase);
|
|
101
|
+
return sourcePath ? [{ ...mapping, sourcePath }] : [];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sourceReferenceFromMap(source, sourceRoot) {
|
|
106
|
+
if (!sourceRoot) return source;
|
|
107
|
+
if (isAbsolute(sourceRoot)) return resolve(sourceRoot, source);
|
|
108
|
+
try {
|
|
109
|
+
const root = new URL(sourceRoot);
|
|
110
|
+
root.pathname = `${root.pathname.replace(/\/+$/, '')}/`;
|
|
111
|
+
return new URL(source, root).href;
|
|
112
|
+
} catch {
|
|
113
|
+
return join(sourceRoot, source);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sourcePathFromMap(source, sourceBase) {
|
|
118
|
+
if (isAbsolute(source)) return resolve(source);
|
|
119
|
+
try {
|
|
120
|
+
const url = new URL(source);
|
|
121
|
+
return url.protocol === 'file:' ? resolve(fileURLToPath(url)) : undefined;
|
|
122
|
+
} catch {
|
|
123
|
+
return sourceBase ? resolve(sourceBase, source) : undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|