tw-migrate 0.3.0 → 0.4.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 +2 -1
- package/dist/bin.js +9 -6
- package/dist/index.d.ts +5 -0
- package/dist/index.js +196 -108
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ node /path/to/tw-migrate/src/bin.ts
|
|
|
47
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
|
|
48
48
|
- direct CSS Module members, static template literals, and static expression literals
|
|
49
49
|
- global `className` and `id` literals
|
|
50
|
+
- 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
|
|
50
51
|
- multi-compound CSS Module selectors whose element relationships are proven from the JSX graph
|
|
51
52
|
- common state pseudo-classes, global arbitrary descendant variants, and conditional at-rules (`@media`, `@supports`, `@container`, `@starting-style`)
|
|
52
53
|
- the tier-1 property mapping families with shorthand/longhand normalization
|
|
@@ -68,7 +69,7 @@ Everything outside this subset is retained and reported with one of the warning
|
|
|
68
69
|
| `computed-css-module-reference` | A computed CSS Module access cannot be verified, so the module is retained. |
|
|
69
70
|
| `cross-package-stylesheet-link` | A linked stylesheet is owned by another package, so it is not analyzed outside workspace mode. |
|
|
70
71
|
| `css-module-composes` | The rule uses or is targeted by `composes`, so it is retained. |
|
|
71
|
-
| `dynamic-class-name` | A `className` value is dynamic, so
|
|
72
|
+
| `dynamic-class-name` | A `className` value or expression result is dynamic, so that class site cannot be migrated. |
|
|
72
73
|
| `dynamic-html-attribute` | An HTML attribute is not a safely writable quoted literal, so the element cannot be migrated. |
|
|
73
74
|
| `dynamic-template-class` | A dynamic class binding makes a Vue template's class set unprovable, so its scoped rules are retained. |
|
|
74
75
|
| `existing-tailwind-conflict` | A generated utility may conflict with a Tailwind class already on the element. |
|
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.4.0",
|
|
8
8
|
description: "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
|
|
9
9
|
keywords: [
|
|
10
10
|
"codemod",
|
|
@@ -33,7 +33,7 @@ const { version } = {
|
|
|
33
33
|
scripts: {
|
|
34
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
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": "
|
|
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
39
|
"typecheck": "tsc --noEmit",
|
|
@@ -41,7 +41,7 @@ const { version } = {
|
|
|
41
41
|
"test:snapshots": "pnpm snapshots:prepare && cargo test -p tw-migrate-snapshots",
|
|
42
42
|
"prepare": "vp config"
|
|
43
43
|
},
|
|
44
|
-
dependencies: { "parse5": "
|
|
44
|
+
dependencies: { "parse5": "8.0.1" },
|
|
45
45
|
devDependencies: {
|
|
46
46
|
"@napi-rs/cli": "catalog:",
|
|
47
47
|
"@types/node": "catalog:",
|
|
@@ -74,8 +74,8 @@ const { version } = {
|
|
|
74
74
|
"x86_64-pc-windows-msvc"
|
|
75
75
|
]
|
|
76
76
|
},
|
|
77
|
-
engines: { "node": ">=22.
|
|
78
|
-
packageManager: "pnpm@11.
|
|
77
|
+
engines: { "node": ">=22.23.2" },
|
|
78
|
+
packageManager: "pnpm@11.20.0"
|
|
79
79
|
};
|
|
80
80
|
const usage = "Usage: tw-migrate [style-file] [options]";
|
|
81
81
|
const help = `${usage}
|
|
@@ -134,7 +134,10 @@ async function main() {
|
|
|
134
134
|
workspaces
|
|
135
135
|
});
|
|
136
136
|
if (report.diff) process.stdout.write(report.diff);
|
|
137
|
-
for (const warning of report.warnings)
|
|
137
|
+
for (const warning of report.warnings) {
|
|
138
|
+
const location = warning.line ? `${warning.file}:${warning.line}:${warning.column}-${warning.endLine}:${warning.endColumn}` : warning.file;
|
|
139
|
+
console.warn(styleText("yellow", `warning[${warning.code}] ${location} ${warning.message}`, { stream: process.stderr }));
|
|
140
|
+
}
|
|
138
141
|
for (const failure of report.failures) console.warn(styleText("yellow", `skipped[${failure.package}] ${failure.message}`, { stream: process.stderr }));
|
|
139
142
|
console.log(`${write ? "Applied" : "Previewed"} ${report.changedFiles.length} file(s); ${report.convertedRules} rule(s) converted, ${report.retainedRules} retained.`);
|
|
140
143
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ interface MigrationWarning {
|
|
|
13
13
|
/** Byte offsets into the authored file, or (0, 0) when no unique mapping exists. */
|
|
14
14
|
start: number;
|
|
15
15
|
end: number;
|
|
16
|
+
/** 1-based UTF-16 positions, omitted when no unique authored mapping exists. */
|
|
17
|
+
line?: number;
|
|
18
|
+
column?: number;
|
|
19
|
+
endLine?: number;
|
|
20
|
+
endColumn?: number;
|
|
16
21
|
message: string;
|
|
17
22
|
}
|
|
18
23
|
interface RuleReport {
|
package/dist/index.js
CHANGED
|
@@ -19,14 +19,13 @@ function unifiedDiff(path, before, after) {
|
|
|
19
19
|
}
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/parser/html.ts
|
|
22
|
-
const RECOVERABLE_PARSE_ERRORS = /* @__PURE__ */ new Set(["missing-doctype"]);
|
|
23
22
|
const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
|
|
24
23
|
function parseHtmlSource(path, source) {
|
|
25
24
|
const errors = [];
|
|
26
25
|
const document = parse(source, {
|
|
27
26
|
sourceCodeLocationInfo: true,
|
|
28
27
|
onParseError(error) {
|
|
29
|
-
if (
|
|
28
|
+
if (error.code !== "missing-doctype") errors.push(error);
|
|
30
29
|
}
|
|
31
30
|
});
|
|
32
31
|
if (errors.length > 0) throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
|
|
@@ -78,13 +77,16 @@ function parseHtmlSource(path, source) {
|
|
|
78
77
|
});
|
|
79
78
|
else if (classAttribute || idAttribute) {
|
|
80
79
|
if (!classAttribute && idAttribute) {
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
const startTag = node.sourceCodeLocation?.startTag;
|
|
81
|
+
if (startTag && source[startTag.endOffset - 1] === ">") {
|
|
82
|
+
const insertion = classInsertionOffset(source, startTag.startOffset, startTag.endOffset - 1);
|
|
83
|
+
classAttribute = {
|
|
84
|
+
value: "",
|
|
85
|
+
start: insertion,
|
|
86
|
+
end: insertion,
|
|
87
|
+
synthetic: true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
91
|
elements.push({
|
|
90
92
|
classAttribute,
|
|
@@ -159,12 +161,10 @@ function toByteOffsets(source, parsed) {
|
|
|
159
161
|
dynamicAttributes: parsed.dynamicAttributes.map((value) => attribute(value))
|
|
160
162
|
};
|
|
161
163
|
}
|
|
162
|
-
function classInsertionOffset
|
|
163
|
-
|
|
164
|
-
let offset = startTag.endOffset - 1;
|
|
165
|
-
if (source[offset] !== ">") return void 0;
|
|
166
|
-
while (offset > startTag.startOffset && /\s/.test(source[offset - 1] ?? "")) offset -= 1;
|
|
164
|
+
function classInsertionOffset(source, floor, tagEnd) {
|
|
165
|
+
let offset = tagEnd;
|
|
167
166
|
if (source[offset - 1] === "/") offset -= 1;
|
|
167
|
+
while (offset > floor && /\s/.test(source[offset - 1] ?? "")) offset -= 1;
|
|
168
168
|
return offset;
|
|
169
169
|
}
|
|
170
170
|
function stylesheetRel(value = "") {
|
|
@@ -240,11 +240,13 @@ async function loadProjectModule(packageRoot, name, errorMessage) {
|
|
|
240
240
|
const imported = await import(pathToFileURL(modulePath).href);
|
|
241
241
|
return imported.default ?? imported;
|
|
242
242
|
}
|
|
243
|
+
const missingCompilerMessage = (name) => `${name} must be installed in the target project.`;
|
|
244
|
+
const MISSING_STYLE_COMPILER_MESSAGES = /* @__PURE__ */ new Set([missingCompilerMessage("Sass"), missingCompilerMessage("Less")]);
|
|
243
245
|
function loadProjectSass(packageRoot) {
|
|
244
|
-
return loadProjectModule(packageRoot, "sass", "Sass
|
|
246
|
+
return loadProjectModule(packageRoot, "sass", missingCompilerMessage("Sass"));
|
|
245
247
|
}
|
|
246
248
|
function loadProjectLess(packageRoot) {
|
|
247
|
-
return loadProjectModule(packageRoot, "less", "Less
|
|
249
|
+
return loadProjectModule(packageRoot, "less", missingCompilerMessage("Less"));
|
|
248
250
|
}
|
|
249
251
|
async function compileStyleEntry(compilers, packageRoot, entryPath, source, options = {}) {
|
|
250
252
|
if (isSassPath(entryPath)) return compileSassEntry(compilers.sass ??= await loadProjectSass(packageRoot), entryPath, source, options);
|
|
@@ -356,6 +358,9 @@ const RECOVERABLE_INPUT_ERROR = "TW_MIGRATE_RECOVERABLE_INPUT:";
|
|
|
356
358
|
function errorCode(error) {
|
|
357
359
|
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
358
360
|
}
|
|
361
|
+
function errorMessage(error) {
|
|
362
|
+
return error instanceof Error ? error.message : String(error);
|
|
363
|
+
}
|
|
359
364
|
function isWithin(root, path) {
|
|
360
365
|
return path === root || path.startsWith(`${root}${sep}`);
|
|
361
366
|
}
|
|
@@ -404,21 +409,20 @@ function recordSnapshot(snapshots, path, source) {
|
|
|
404
409
|
return source;
|
|
405
410
|
}
|
|
406
411
|
function packageFailure(workspaceRoot, packageRoot, error) {
|
|
407
|
-
const message =
|
|
412
|
+
const message = errorMessage(error);
|
|
408
413
|
return {
|
|
409
414
|
package: normalizedRelativePath(workspaceRoot, packageRoot) || ".",
|
|
410
415
|
message: message.startsWith("TW_MIGRATE_RECOVERABLE_INPUT:") ? message.slice(29) : message
|
|
411
416
|
};
|
|
412
417
|
}
|
|
413
418
|
function isRecoverablePlanningError(error) {
|
|
414
|
-
return (error
|
|
419
|
+
return errorMessage(error).startsWith(RECOVERABLE_INPUT_ERROR);
|
|
415
420
|
}
|
|
416
421
|
function isMissingStyleCompilerError(error) {
|
|
417
|
-
|
|
418
|
-
return /^(?:Sass|Less) must be installed in the target project\.$/.test(message);
|
|
422
|
+
return MISSING_STYLE_COMPILER_MESSAGES.has(errorMessage(error));
|
|
419
423
|
}
|
|
420
424
|
function isIntegrityError(error) {
|
|
421
|
-
return (error
|
|
425
|
+
return errorMessage(error).startsWith("Source changed during planning:");
|
|
422
426
|
}
|
|
423
427
|
function normalizedRelativePath(root, path) {
|
|
424
428
|
return relative(root, path).split(sep).join("/");
|
|
@@ -528,7 +532,7 @@ async function resolveScope(options) {
|
|
|
528
532
|
const currentPackage = await findPackageRoot(cwd);
|
|
529
533
|
const gitRoot = await findGitRoot(currentPackage);
|
|
530
534
|
const workspaceRoot = gitRoot && !await isIgnoredByGit(gitRoot, currentPackage) ? gitRoot : currentPackage;
|
|
531
|
-
const allPaths = await
|
|
535
|
+
const allPaths = workspaceRoot === gitRoot ? await discoverGitFiles(workspaceRoot) : await collectFiles(workspaceRoot, isRelevantDiscoveredFile);
|
|
532
536
|
const scannedPaths = workspaceRoot === gitRoot ? [.../* @__PURE__ */ new Set([...allPaths, ...await collectFiles(workspaceRoot, isRelevantDiscoveredFile)])] : [...allPaths];
|
|
533
537
|
const explicitStyle = options.styleFile ? resolve(cwd, options.styleFile) : void 0;
|
|
534
538
|
const configuredEntry = options.tailwindCss ? resolve(cwd, options.tailwindCss) : void 0;
|
|
@@ -603,8 +607,7 @@ async function isIgnoredByGit(gitRoot, path) {
|
|
|
603
607
|
return false;
|
|
604
608
|
}
|
|
605
609
|
}
|
|
606
|
-
async function
|
|
607
|
-
if (!useGit) return collectFiles(root, isRelevantDiscoveredFile);
|
|
610
|
+
async function discoverGitFiles(root) {
|
|
608
611
|
const { stdout } = await run("git", [
|
|
609
612
|
"ls-files",
|
|
610
613
|
"-co",
|
|
@@ -645,7 +648,6 @@ async function collectFiles(root, include) {
|
|
|
645
648
|
const files = [];
|
|
646
649
|
async function visit(directory) {
|
|
647
650
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
648
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
649
651
|
for (const entry of entries) {
|
|
650
652
|
const path = join(directory, entry.name);
|
|
651
653
|
if (entry.isDirectory()) {
|
|
@@ -885,8 +887,30 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
|
|
|
885
887
|
const { __unstable__loadDesignSystem: loadDesignSystem } = await loadProjectModule(packageRoot, "tailwindcss", "Tailwind v4 must be installed in the target project.");
|
|
886
888
|
const css = await snapshotFile(snapshots, tailwindCss);
|
|
887
889
|
const base = dirname(tailwindCss);
|
|
888
|
-
const
|
|
889
|
-
const
|
|
890
|
+
const tailwindRoot = dirname(packagePath);
|
|
891
|
+
const loadModule = async (id, moduleBase) => {
|
|
892
|
+
const path = createRequire(join(moduleBase, "package.json")).resolve(id);
|
|
893
|
+
if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
|
|
894
|
+
const imported = await import(pathToFileURL(path).href);
|
|
895
|
+
return {
|
|
896
|
+
path,
|
|
897
|
+
base: dirname(path),
|
|
898
|
+
module: imported.default ?? imported
|
|
899
|
+
};
|
|
900
|
+
};
|
|
901
|
+
const loadStylesheet = async (id, sheetBase) => {
|
|
902
|
+
let path;
|
|
903
|
+
if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
|
|
904
|
+
else if (id.startsWith("tailwindcss/")) {
|
|
905
|
+
const subpath = id.slice(12);
|
|
906
|
+
path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
|
|
907
|
+
} else if (id.startsWith(".") || isAbsolute(id)) path = resolve(sheetBase, id);
|
|
908
|
+
else path = projectRequire.resolve(id);
|
|
909
|
+
return {
|
|
910
|
+
content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
|
|
911
|
+
base: dirname(path)
|
|
912
|
+
};
|
|
913
|
+
};
|
|
890
914
|
const themeTokens = {
|
|
891
915
|
...extractThemeTokens(await readFile(join(dirname(packagePath), "theme.css"), "utf8")),
|
|
892
916
|
...await extractThemeTokensFromGraph(css, base, loadStylesheet)
|
|
@@ -922,34 +946,6 @@ function invalidCandidates(tailwind, candidates) {
|
|
|
922
946
|
const generated = tailwind.designSystem.candidatesToCss(candidates);
|
|
923
947
|
return candidates.filter((_, index) => generated[index] === null);
|
|
924
948
|
}
|
|
925
|
-
function createModuleLoader(snapshots, workspaceRoot) {
|
|
926
|
-
return async (id, base) => {
|
|
927
|
-
const path = createRequire(join(base, "package.json")).resolve(id);
|
|
928
|
-
if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
|
|
929
|
-
const imported = await import(pathToFileURL(path).href);
|
|
930
|
-
return {
|
|
931
|
-
path,
|
|
932
|
-
base: dirname(path),
|
|
933
|
-
module: imported.default ?? imported
|
|
934
|
-
};
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
function createStylesheetLoader(projectRequire, tailwindPackagePath, snapshots, workspaceRoot) {
|
|
938
|
-
const tailwindRoot = dirname(tailwindPackagePath);
|
|
939
|
-
return async (id, base) => {
|
|
940
|
-
let path;
|
|
941
|
-
if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
|
|
942
|
-
else if (id.startsWith("tailwindcss/")) {
|
|
943
|
-
const subpath = id.slice(12);
|
|
944
|
-
path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
|
|
945
|
-
} else if (id.startsWith(".") || isAbsolute(id)) path = resolve(base, id);
|
|
946
|
-
else path = projectRequire.resolve(id);
|
|
947
|
-
return {
|
|
948
|
-
content: isProjectInput(workspaceRoot, path) ? await snapshotFile(snapshots, path) : await readFile(path, "utf8"),
|
|
949
|
-
base: dirname(path)
|
|
950
|
-
};
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
949
|
//#endregion
|
|
954
950
|
//#region src/parser/vue.ts
|
|
955
951
|
const ESCAPE_SELECTOR = /(?:::v-|:)(?:deep|global|slotted)\(([^)]*)\)/g;
|
|
@@ -967,6 +963,13 @@ const SUPPORTED_STYLE_LANGUAGES = /* @__PURE__ */ new Set([
|
|
|
967
963
|
"sass",
|
|
968
964
|
"less"
|
|
969
965
|
]);
|
|
966
|
+
const SUPPORTED_SCRIPT_LANGUAGES = /* @__PURE__ */ new Set([
|
|
967
|
+
void 0,
|
|
968
|
+
"js",
|
|
969
|
+
"jsx",
|
|
970
|
+
"ts",
|
|
971
|
+
"tsx"
|
|
972
|
+
]);
|
|
970
973
|
const NODE_ELEMENT = 1;
|
|
971
974
|
const NODE_TEXT = 2;
|
|
972
975
|
const NODE_INTERPOLATION = 5;
|
|
@@ -997,29 +1000,48 @@ function analyzeVueSource(compiler, path, source) {
|
|
|
997
1000
|
message
|
|
998
1001
|
});
|
|
999
1002
|
const { descriptor, errors } = compiler.parse(source, { filename: path });
|
|
1003
|
+
const styleRanges = descriptor.styles.map((style) => ({
|
|
1004
|
+
start: style.loc.start.offset,
|
|
1005
|
+
end: style.loc.end.offset
|
|
1006
|
+
}));
|
|
1007
|
+
const byteRanges = (offset) => ({
|
|
1008
|
+
warnings: warnings.map((warning) => ({
|
|
1009
|
+
...warning,
|
|
1010
|
+
start: offset(warning.start),
|
|
1011
|
+
end: offset(warning.end)
|
|
1012
|
+
})),
|
|
1013
|
+
styleRanges: styleRanges.map((range) => ({
|
|
1014
|
+
start: offset(range.start),
|
|
1015
|
+
end: offset(range.end)
|
|
1016
|
+
}))
|
|
1017
|
+
});
|
|
1018
|
+
const retained = () => {
|
|
1019
|
+
const offsets = utf8OffsetMap(source, [...warnings, ...styleRanges].flatMap((range) => [range.start, range.end]));
|
|
1020
|
+
return {
|
|
1021
|
+
retained: true,
|
|
1022
|
+
...byteRanges(offsetLookup(offsets))
|
|
1023
|
+
};
|
|
1024
|
+
};
|
|
1000
1025
|
if (errors.length > 0) {
|
|
1001
1026
|
const offset = errors[0]?.loc?.start?.offset ?? 0;
|
|
1002
1027
|
warn("unsupported-sfc-block", offset, offset, `The SFC could not be parsed: ${errors[0].message}`);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1028
|
+
if (errors[0]?.loc?.start?.offset === 0) Object.assign(warnings[0], {
|
|
1029
|
+
line: 1,
|
|
1030
|
+
column: 1,
|
|
1031
|
+
endLine: 1,
|
|
1032
|
+
endColumn: 1
|
|
1033
|
+
});
|
|
1034
|
+
return retained();
|
|
1007
1035
|
}
|
|
1008
1036
|
const template = descriptor.template;
|
|
1009
1037
|
if (!template || template.lang || template.src) {
|
|
1010
1038
|
warn("unsupported-sfc-block", 0, 0, !template ? "The SFC has no template block, so its class usage cannot be analyzed." : "Only the default template language with inline content is analyzed.");
|
|
1011
|
-
return
|
|
1012
|
-
warnings: toByteWarnings(source, warnings),
|
|
1013
|
-
retained: true
|
|
1014
|
-
};
|
|
1039
|
+
return retained();
|
|
1015
1040
|
}
|
|
1016
1041
|
if (descriptor.customBlocks.length > 0) {
|
|
1017
1042
|
const block = descriptor.customBlocks[0];
|
|
1018
1043
|
warn("unsupported-sfc-block", block.loc.start.offset, block.loc.end.offset, `The custom <${block.type}> block is not analyzed.`);
|
|
1019
|
-
return
|
|
1020
|
-
warnings: toByteWarnings(source, warnings),
|
|
1021
|
-
retained: true
|
|
1022
|
-
};
|
|
1044
|
+
return retained();
|
|
1023
1045
|
}
|
|
1024
1046
|
const blocks = [];
|
|
1025
1047
|
const unscopedBlocks = [];
|
|
@@ -1119,13 +1141,7 @@ function analyzeVueSource(compiler, path, source) {
|
|
|
1119
1141
|
let scriptImportsUnverifiable = false;
|
|
1120
1142
|
const styleImports = [descriptor.script, descriptor.scriptSetup].flatMap((script) => {
|
|
1121
1143
|
if (!script) return [];
|
|
1122
|
-
if (script.src || !
|
|
1123
|
-
void 0,
|
|
1124
|
-
"js",
|
|
1125
|
-
"jsx",
|
|
1126
|
-
"ts",
|
|
1127
|
-
"tsx"
|
|
1128
|
-
].includes(script.lang)) {
|
|
1144
|
+
if (script.src || !SUPPORTED_SCRIPT_LANGUAGES.has(script.lang)) {
|
|
1129
1145
|
scriptImportsUnverifiable = true;
|
|
1130
1146
|
return [];
|
|
1131
1147
|
}
|
|
@@ -1137,19 +1153,14 @@ function analyzeVueSource(compiler, path, source) {
|
|
|
1137
1153
|
}
|
|
1138
1154
|
});
|
|
1139
1155
|
let componentImports = [];
|
|
1140
|
-
if (descriptor.scriptSetup && !descriptor.scriptSetup.src &&
|
|
1141
|
-
void 0,
|
|
1142
|
-
"js",
|
|
1143
|
-
"jsx",
|
|
1144
|
-
"ts",
|
|
1145
|
-
"tsx"
|
|
1146
|
-
].includes(descriptor.scriptSetup.lang)) try {
|
|
1156
|
+
if (descriptor.scriptSetup && !descriptor.scriptSetup.src && SUPPORTED_SCRIPT_LANGUAGES.has(descriptor.scriptSetup.lang)) try {
|
|
1147
1157
|
componentImports = staticImportBindings(`${path}.${descriptor.scriptSetup.lang ?? "js"}`, descriptor.scriptSetup.content);
|
|
1148
1158
|
} catch {
|
|
1149
1159
|
componentImports = [];
|
|
1150
1160
|
}
|
|
1151
1161
|
const offsets = utf8OffsetMap(source, [
|
|
1152
1162
|
...warnings.flatMap((warning) => [warning.start, warning.end]),
|
|
1163
|
+
...styleRanges.flatMap((range) => [range.start, range.end]),
|
|
1153
1164
|
...[
|
|
1154
1165
|
...blocks,
|
|
1155
1166
|
...unscopedBlocks,
|
|
@@ -1194,11 +1205,7 @@ function analyzeVueSource(compiler, path, source) {
|
|
|
1194
1205
|
}
|
|
1195
1206
|
});
|
|
1196
1207
|
return {
|
|
1197
|
-
|
|
1198
|
-
...warning,
|
|
1199
|
-
start: offset(warning.start),
|
|
1200
|
-
end: offset(warning.end)
|
|
1201
|
-
})),
|
|
1208
|
+
...byteRanges(offset),
|
|
1202
1209
|
blocks: blocks.map(toByteBlock),
|
|
1203
1210
|
unscopedBlocks: unscopedBlocks.map(toByteBlock),
|
|
1204
1211
|
moduleBlocks: moduleBlocks.map(toByteBlock),
|
|
@@ -1309,7 +1316,7 @@ function templateSite(source, node, bindingClasses, classOpaque, state) {
|
|
|
1309
1316
|
}
|
|
1310
1317
|
const matchClasses = bindingClasses.length ? [...classAttribute?.value.split(/[\t\n\f\r ]+/).filter(Boolean) ?? [], ...bindingClasses] : void 0;
|
|
1311
1318
|
if (!classAttribute && (bindingClasses.length > 0 || !classOpaque && (node.tagType === TAG_COMPONENT || idAttribute))) {
|
|
1312
|
-
const insertion =
|
|
1319
|
+
const insertion = nodeClassInsertionOffset(source, node);
|
|
1313
1320
|
if (insertion === void 0) state.dynamic = true;
|
|
1314
1321
|
else classAttribute = {
|
|
1315
1322
|
value: "",
|
|
@@ -1340,22 +1347,11 @@ function literalAttribute(source, node, name) {
|
|
|
1340
1347
|
end: end - 1
|
|
1341
1348
|
};
|
|
1342
1349
|
}
|
|
1343
|
-
function
|
|
1350
|
+
function nodeClassInsertionOffset(source, node) {
|
|
1344
1351
|
const propsEnd = Math.max(node.loc.start.offset + 1 + node.tag.length, ...(node.props ?? []).map((prop) => prop.loc.end.offset));
|
|
1345
1352
|
const tagEnd = source.indexOf(">", propsEnd);
|
|
1346
1353
|
if (tagEnd < 0) return void 0;
|
|
1347
|
-
|
|
1348
|
-
if (source[offset - 1] === "/") offset -= 1;
|
|
1349
|
-
while (offset > node.loc.start.offset && /\s/.test(source[offset - 1])) offset -= 1;
|
|
1350
|
-
return offset;
|
|
1351
|
-
}
|
|
1352
|
-
function toByteWarnings(source, warnings) {
|
|
1353
|
-
const offsets = utf8OffsetMap(source, warnings.flatMap((warning) => [warning.start, warning.end]));
|
|
1354
|
-
return warnings.map((warning) => ({
|
|
1355
|
-
...warning,
|
|
1356
|
-
start: offsets.get(warning.start) ?? warning.start,
|
|
1357
|
-
end: offsets.get(warning.end) ?? warning.end
|
|
1358
|
-
}));
|
|
1354
|
+
return classInsertionOffset(source, node.loc.start.offset, tagEnd);
|
|
1359
1355
|
}
|
|
1360
1356
|
//#endregion
|
|
1361
1357
|
//#region src/plan/vue.ts
|
|
@@ -1473,6 +1469,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
|
|
|
1473
1469
|
const none = {
|
|
1474
1470
|
files: /* @__PURE__ */ new Map(),
|
|
1475
1471
|
stylesheets: [],
|
|
1472
|
+
styleRanges: /* @__PURE__ */ new Map(),
|
|
1476
1473
|
stylePaths: /* @__PURE__ */ new Set(),
|
|
1477
1474
|
unscopedPaths: /* @__PURE__ */ new Set(),
|
|
1478
1475
|
warnings: [],
|
|
@@ -1768,6 +1765,7 @@ async function preparePackageVue({ packageRoot, styleCompilers, sourceFiles, sty
|
|
|
1768
1765
|
return {
|
|
1769
1766
|
files,
|
|
1770
1767
|
stylesheets,
|
|
1768
|
+
styleRanges: new Map([...analyses].map(([path, analysis]) => [path, analysis.styleRanges])),
|
|
1771
1769
|
stylePaths,
|
|
1772
1770
|
unscopedPaths,
|
|
1773
1771
|
warnings,
|
|
@@ -1783,7 +1781,7 @@ async function verifySnapshots(snapshots) {
|
|
|
1783
1781
|
const read = reads[index];
|
|
1784
1782
|
if (read.status === "rejected") {
|
|
1785
1783
|
const error = read.reason;
|
|
1786
|
-
const detail = errorCode(error) ?? (error
|
|
1784
|
+
const detail = errorCode(error) ?? errorMessage(error);
|
|
1787
1785
|
throw new Error(`Source changed after planning: ${path} (${detail})`);
|
|
1788
1786
|
}
|
|
1789
1787
|
if (read.value !== before) throw new Error(`Source changed after planning: ${path}`);
|
|
@@ -1818,6 +1816,38 @@ async function writeChanges(changes, deletions) {
|
|
|
1818
1816
|
}
|
|
1819
1817
|
//#endregion
|
|
1820
1818
|
//#region src/index.ts
|
|
1819
|
+
function indexWarningPositions(source, offsets, unicodeSeparatorsAreNewlines, formFeedIsNewline, initial = {
|
|
1820
|
+
line: 1,
|
|
1821
|
+
column: 1
|
|
1822
|
+
}) {
|
|
1823
|
+
const targets = [...offsets].sort((left, right) => left - right);
|
|
1824
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1825
|
+
let target = 0;
|
|
1826
|
+
let byte = 0;
|
|
1827
|
+
let { line, column } = initial;
|
|
1828
|
+
let previousWasCarriageReturn = false;
|
|
1829
|
+
const record = () => {
|
|
1830
|
+
while (targets[target] === byte) {
|
|
1831
|
+
positions.set(byte, {
|
|
1832
|
+
line,
|
|
1833
|
+
column
|
|
1834
|
+
});
|
|
1835
|
+
target += 1;
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
record();
|
|
1839
|
+
for (const character of source) {
|
|
1840
|
+
byte += Buffer.byteLength(character);
|
|
1841
|
+
while ((targets[target] ?? Infinity) < byte) target += 1;
|
|
1842
|
+
if (character === "\r" || character === "\n" && !previousWasCarriageReturn || unicodeSeparatorsAreNewlines && (character === "\u2028" || character === "\u2029") || formFeedIsNewline && character === "\f") {
|
|
1843
|
+
line += 1;
|
|
1844
|
+
column = 1;
|
|
1845
|
+
} else if (character !== "\n") column += character.length;
|
|
1846
|
+
previousWasCarriageReturn = character === "\r";
|
|
1847
|
+
record();
|
|
1848
|
+
}
|
|
1849
|
+
return positions;
|
|
1850
|
+
}
|
|
1821
1851
|
async function migrate(options = {}) {
|
|
1822
1852
|
if (options.styleFile && options.workspaces) throw new TypeError("styleFile cannot be combined with workspaces");
|
|
1823
1853
|
if (options.tailwindCss && options.workspaces) throw new TypeError("tailwindCss cannot be combined with workspaces");
|
|
@@ -1847,13 +1877,15 @@ async function migrate(options = {}) {
|
|
|
1847
1877
|
const sourceFiles = sourceCandidates.flatMap((file) => file ? [file] : []);
|
|
1848
1878
|
if (explicitStyle && isStylesheetPath(explicitStyle) && !styleSources.has(explicitStyle)) styleSources.set(explicitStyle, await snapshotFile(snapshots, explicitStyle));
|
|
1849
1879
|
if (configuredEntry && !styleSources.has(configuredEntry)) styleSources.set(configuredEntry, await snapshotFile(snapshots, configuredEntry));
|
|
1880
|
+
const vueStyleRanges = /* @__PURE__ */ new Map();
|
|
1850
1881
|
const context = {
|
|
1851
1882
|
...scope,
|
|
1852
1883
|
options,
|
|
1853
1884
|
snapshots,
|
|
1854
1885
|
styleSources,
|
|
1855
1886
|
sourceFiles,
|
|
1856
|
-
styleDependents: indexStylesheetDependents(styleSources)
|
|
1887
|
+
styleDependents: indexStylesheetDependents(styleSources),
|
|
1888
|
+
vueStyleRanges
|
|
1857
1889
|
};
|
|
1858
1890
|
const failures = [];
|
|
1859
1891
|
const plans = [];
|
|
@@ -1880,6 +1912,42 @@ async function migrate(options = {}) {
|
|
|
1880
1912
|
await writeChanges(changed, deleted);
|
|
1881
1913
|
}
|
|
1882
1914
|
warnings.sort((left, right) => left.file.localeCompare(right.file) || left.start - right.start || left.end - right.end || left.code.localeCompare(right.code));
|
|
1915
|
+
const warningOffsets = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (const warning of warnings) {
|
|
1917
|
+
if (warning.start === 0 && warning.end === 0) continue;
|
|
1918
|
+
const offsets = warningOffsets.get(warning.file) ?? /* @__PURE__ */ new Set();
|
|
1919
|
+
offsets.add(warning.start).add(warning.end);
|
|
1920
|
+
warningOffsets.set(warning.file, offsets);
|
|
1921
|
+
}
|
|
1922
|
+
const warningPositions = new Map([...warningOffsets].flatMap(([file, offsets]) => {
|
|
1923
|
+
const source = snapshots.get(file);
|
|
1924
|
+
if (!source) return [];
|
|
1925
|
+
const extension = extname(file);
|
|
1926
|
+
const javascriptSource = SOURCE_EXTENSIONS.has(extension) && extension !== ".html" && extension !== ".vue";
|
|
1927
|
+
const styleRanges = vueStyleRanges.get(file) ?? [];
|
|
1928
|
+
const defaultOffsets = new Set(offsets);
|
|
1929
|
+
for (const range of styleRanges) defaultOffsets.add(range.start);
|
|
1930
|
+
const defaultPositions = indexWarningPositions(source, defaultOffsets, javascriptSource, isStylesheetPath(file));
|
|
1931
|
+
const bytes = Buffer.from(source);
|
|
1932
|
+
const sortedOffsets = [...offsets].sort((left, right) => left - right);
|
|
1933
|
+
const stylePositions = /* @__PURE__ */ new Map();
|
|
1934
|
+
let offsetIndex = 0;
|
|
1935
|
+
for (const range of styleRanges) {
|
|
1936
|
+
while ((sortedOffsets[offsetIndex] ?? Infinity) < range.start) offsetIndex += 1;
|
|
1937
|
+
const relativeOffsets = /* @__PURE__ */ new Set();
|
|
1938
|
+
while ((sortedOffsets[offsetIndex] ?? Infinity) <= range.end) {
|
|
1939
|
+
relativeOffsets.add(sortedOffsets[offsetIndex] - range.start);
|
|
1940
|
+
offsetIndex += 1;
|
|
1941
|
+
}
|
|
1942
|
+
const initial = defaultPositions.get(range.start);
|
|
1943
|
+
if (!initial) continue;
|
|
1944
|
+
for (const [offset, position] of indexWarningPositions(bytes.subarray(range.start, range.end).toString(), relativeOffsets, false, true, initial)) stylePositions.set(range.start + offset, position);
|
|
1945
|
+
}
|
|
1946
|
+
return [[file, {
|
|
1947
|
+
default: defaultPositions,
|
|
1948
|
+
style: stylePositions
|
|
1949
|
+
}]];
|
|
1950
|
+
}));
|
|
1883
1951
|
failures.sort((left, right) => left.package.localeCompare(right.package));
|
|
1884
1952
|
return {
|
|
1885
1953
|
changedFiles,
|
|
@@ -1891,15 +1959,29 @@ async function migrate(options = {}) {
|
|
|
1891
1959
|
file: normalizedRelativePath(cwd, rule.file)
|
|
1892
1960
|
})),
|
|
1893
1961
|
candidates: [...candidates].sort(),
|
|
1894
|
-
warnings: warnings.map((warning) =>
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1962
|
+
warnings: warnings.map((warning) => {
|
|
1963
|
+
const indexed = warningPositions.get(warning.file);
|
|
1964
|
+
const styleFrom = indexed?.style.get(warning.start);
|
|
1965
|
+
const styleTo = indexed?.style.get(warning.end);
|
|
1966
|
+
const located = warning.start !== 0 || warning.end !== 0;
|
|
1967
|
+
const from = located ? styleFrom ?? indexed?.default.get(warning.start) : void 0;
|
|
1968
|
+
const to = located ? styleFrom && styleTo ? styleTo : indexed?.default.get(warning.end) : void 0;
|
|
1969
|
+
return {
|
|
1970
|
+
...warning,
|
|
1971
|
+
...from && to ? {
|
|
1972
|
+
line: from.line,
|
|
1973
|
+
column: from.column,
|
|
1974
|
+
endLine: to.line,
|
|
1975
|
+
endColumn: to.column
|
|
1976
|
+
} : {},
|
|
1977
|
+
file: normalizedRelativePath(cwd, warning.file)
|
|
1978
|
+
};
|
|
1979
|
+
}),
|
|
1898
1980
|
failures
|
|
1899
1981
|
};
|
|
1900
1982
|
}
|
|
1901
1983
|
async function planPackage(context, packageRoot) {
|
|
1902
|
-
const { options, snapshots, workspaceRoot, explicitStyle, configuredEntry, styleSources, sourceFiles, styleDependents, pathOwners, targetable, selectedPackages, scannedPaths } = context;
|
|
1984
|
+
const { options, snapshots, workspaceRoot, explicitStyle, configuredEntry, styleSources, sourceFiles, styleDependents, pathOwners, targetable, selectedPackages, scannedPaths, vueStyleRanges } = context;
|
|
1903
1985
|
const recover = (error, fatal = false) => {
|
|
1904
1986
|
if (!options.force || fatal) throw error;
|
|
1905
1987
|
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
@@ -1936,6 +2018,12 @@ async function planPackage(context, packageRoot) {
|
|
|
1936
2018
|
} catch (error) {
|
|
1937
2019
|
return recover(error, isIntegrityError(error));
|
|
1938
2020
|
}
|
|
2021
|
+
for (const [path, preparedRanges] of preparedVue.styleRanges) {
|
|
2022
|
+
const ranges = vueStyleRanges.get(path) ?? [];
|
|
2023
|
+
ranges.push(...preparedRanges);
|
|
2024
|
+
ranges.sort((left, right) => left.start - right.start);
|
|
2025
|
+
vueStyleRanges.set(path, ranges);
|
|
2026
|
+
}
|
|
1939
2027
|
if (explicitStyle && extname(explicitStyle) === ".vue" && preparedVue.stylesheets.length === 0 && ![...preparedVue.stylePaths].some((path) => !isStylesheetModule(path))) return vueWarningsOnlyResult(preparedVue);
|
|
1940
2028
|
const packageSources = [...sourceFiles.filter((file) => extname(file.path) !== ".html").map((file) => preparedVue.files.get(file.path) ?? file), ...preparedHtml.files];
|
|
1941
2029
|
const ownedStyles = [...styleSources.keys()].filter((path) => pathOwners.get(path) === packageRoot && (targetable.has(path) || preparedHtml.stylePaths.has(path) || preparedVue.stylePaths.has(path)));
|
|
@@ -2167,7 +2255,7 @@ function mergePlans(plans, originals) {
|
|
|
2167
2255
|
retainedRules += plan.retainedRules;
|
|
2168
2256
|
rules.push(...plan.rules);
|
|
2169
2257
|
for (const warning of plan.warnings) {
|
|
2170
|
-
const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}`;
|
|
2258
|
+
const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}\0${warning.message}`;
|
|
2171
2259
|
if (seenWarnings.has(key)) continue;
|
|
2172
2260
|
seenWarnings.add(key);
|
|
2173
2261
|
warnings.push(warning);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tw-migrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Preview and migrate React, Vue, and HTML CSS to Tailwind utilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codemod",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"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",
|
|
37
37
|
"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",
|
|
38
|
-
"artifacts": "
|
|
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
41
|
"typecheck": "tsc --noEmit",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepare": "vp config"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"parse5": "
|
|
47
|
+
"parse5": "8.0.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@napi-rs/cli": "catalog:",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"vue": "catalog:"
|
|
63
63
|
},
|
|
64
64
|
"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.
|
|
65
|
+
"tw-migrate-darwin-arm64": "0.4.0",
|
|
66
|
+
"tw-migrate-darwin-x64": "0.4.0",
|
|
67
|
+
"tw-migrate-linux-arm64-gnu": "0.4.0",
|
|
68
|
+
"tw-migrate-linux-x64-gnu": "0.4.0",
|
|
69
|
+
"tw-migrate-win32-x64-msvc": "0.4.0"
|
|
70
70
|
},
|
|
71
71
|
"napi": {
|
|
72
72
|
"binaryName": "tw-migrate",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
]
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
|
-
"node": ">=22.
|
|
82
|
+
"node": ">=22.23.2"
|
|
83
83
|
},
|
|
84
|
-
"packageManager": "pnpm@11.
|
|
84
|
+
"packageManager": "pnpm@11.20.0"
|
|
85
85
|
}
|