tw-migrate 0.2.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 +12 -11
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +149 -0
- package/{index.d.ts → dist/index.d.ts} +22 -13
- package/dist/index.js +2275 -0
- package/package.json +24 -31
- package/bin/tw-migrate.js +0 -51
- package/html.js +0 -191
- package/index.js +0 -2225
- package/native.js +0 -41
- package/style-compiler.js +0 -140
- package/vue.js +0 -541
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",
|
|
@@ -24,56 +24,49 @@
|
|
|
24
24
|
"url": "git+https://github.com/jong-kyung/tw-migrate.git"
|
|
25
25
|
},
|
|
26
26
|
"bin": {
|
|
27
|
-
"tw-migrate": "bin
|
|
27
|
+
"tw-migrate": "dist/bin.js"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"
|
|
31
|
-
"html.js",
|
|
32
|
-
"index.js",
|
|
33
|
-
"index.d.ts",
|
|
34
|
-
"native.js",
|
|
35
|
-
"style-compiler.js",
|
|
36
|
-
"vue.js"
|
|
30
|
+
"dist"
|
|
37
31
|
],
|
|
38
32
|
"type": "module",
|
|
39
|
-
"main": "index.js",
|
|
40
|
-
"types": "index.d.ts",
|
|
33
|
+
"main": "dist/index.js",
|
|
34
|
+
"types": "dist/index.d.ts",
|
|
41
35
|
"scripts": {
|
|
42
|
-
"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",
|
|
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",
|
|
43
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",
|
|
44
|
-
"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)\"",
|
|
45
39
|
"test": "cargo test && pnpm build:debug && vp test",
|
|
46
40
|
"test:ecosystem": "node ecosystem-ci/run.ts",
|
|
47
|
-
"test:rust": "cargo test",
|
|
48
41
|
"typecheck": "tsc --noEmit",
|
|
49
42
|
"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",
|
|
50
43
|
"test:snapshots": "pnpm snapshots:prepare && cargo test -p tw-migrate-snapshots",
|
|
51
44
|
"prepare": "vp config"
|
|
52
45
|
},
|
|
53
46
|
"dependencies": {
|
|
54
|
-
"parse5": "
|
|
47
|
+
"parse5": "8.0.1"
|
|
55
48
|
},
|
|
56
49
|
"devDependencies": {
|
|
57
|
-
"@napi-rs/cli": "
|
|
58
|
-
"@types/node": "
|
|
50
|
+
"@napi-rs/cli": "catalog:",
|
|
51
|
+
"@types/node": "catalog:",
|
|
59
52
|
"@vitest/browser-playwright": "catalog:",
|
|
60
|
-
"less": "
|
|
61
|
-
"playwright": "
|
|
62
|
-
"sass": "
|
|
63
|
-
"tailwindcss": "
|
|
64
|
-
"typescript": "
|
|
65
|
-
"verdaccio": "
|
|
53
|
+
"less": "catalog:",
|
|
54
|
+
"playwright": "catalog:",
|
|
55
|
+
"sass": "catalog:",
|
|
56
|
+
"tailwindcss": "catalog:",
|
|
57
|
+
"typescript": "catalog:",
|
|
58
|
+
"verdaccio": "catalog:",
|
|
66
59
|
"vite": "catalog:",
|
|
67
60
|
"vite-plus": "catalog:",
|
|
68
61
|
"vitest": "catalog:",
|
|
69
|
-
"vue": "
|
|
62
|
+
"vue": "catalog:"
|
|
70
63
|
},
|
|
71
64
|
"optionalDependencies": {
|
|
72
|
-
"tw-migrate-darwin-arm64": "0.
|
|
73
|
-
"tw-migrate-darwin-x64": "0.
|
|
74
|
-
"tw-migrate-linux-arm64-gnu": "0.
|
|
75
|
-
"tw-migrate-linux-x64-gnu": "0.
|
|
76
|
-
"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"
|
|
77
70
|
},
|
|
78
71
|
"napi": {
|
|
79
72
|
"binaryName": "tw-migrate",
|
|
@@ -86,7 +79,7 @@
|
|
|
86
79
|
]
|
|
87
80
|
},
|
|
88
81
|
"engines": {
|
|
89
|
-
"node": ">=22.
|
|
82
|
+
"node": ">=22.23.2"
|
|
90
83
|
},
|
|
91
|
-
"packageManager": "pnpm@11.
|
|
84
|
+
"packageManager": "pnpm@11.20.0"
|
|
92
85
|
}
|
package/bin/tw-migrate.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { migrate } from "../index.js";
|
|
4
|
-
|
|
5
|
-
const usage =
|
|
6
|
-
"Usage: tw-migrate [style-file] [--tailwind-css <entry.css>] [--workspaces] [--force] [--write]";
|
|
7
|
-
|
|
8
|
-
async function main() {
|
|
9
|
-
const args = process.argv.slice(2);
|
|
10
|
-
let styleFile;
|
|
11
|
-
let tailwindCss;
|
|
12
|
-
let write = false;
|
|
13
|
-
let force = false;
|
|
14
|
-
let workspaces = false;
|
|
15
|
-
|
|
16
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
17
|
-
const argument = args[index];
|
|
18
|
-
if (argument === "--write") write = true;
|
|
19
|
-
else if (argument === "--force") force = true;
|
|
20
|
-
else if (argument === "--workspaces") workspaces = true;
|
|
21
|
-
else if (argument === "--tailwind-css") {
|
|
22
|
-
tailwindCss = args[++index];
|
|
23
|
-
if (!tailwindCss) throw new Error(`${usage}\n--tailwind-css requires a path.`);
|
|
24
|
-
} else if (argument === "--help" || argument === "-h") {
|
|
25
|
-
console.log(usage);
|
|
26
|
-
return;
|
|
27
|
-
} else if (argument.startsWith("-")) {
|
|
28
|
-
throw new Error(`Unknown option: ${argument}`);
|
|
29
|
-
} else if (!styleFile) styleFile = argument;
|
|
30
|
-
else throw new Error(`Unexpected argument: ${argument}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const report = await migrate({ styleFile, tailwindCss, write, force, workspaces });
|
|
34
|
-
if (report.diff) process.stdout.write(report.diff);
|
|
35
|
-
for (const warning of report.warnings) {
|
|
36
|
-
console.warn(
|
|
37
|
-
`warning[${warning.code}] ${warning.file}:${warning.start}-${warning.end} ${warning.message}`,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
for (const failure of report.failures) {
|
|
41
|
-
console.warn(`skipped[${failure.package}] ${failure.message}`);
|
|
42
|
-
}
|
|
43
|
-
console.log(
|
|
44
|
-
`${write ? "Applied" : "Previewed"} ${report.changedFiles.length} file(s); ${report.convertedRules} rule(s) converted, ${report.retainedRules} retained.`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
main().catch((error) => {
|
|
49
|
-
console.error(`tw-migrate: ${error.message}`);
|
|
50
|
-
process.exitCode = 1;
|
|
51
|
-
});
|
package/html.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { parse } from "parse5";
|
|
2
|
-
|
|
3
|
-
const RECOVERABLE_PARSE_ERRORS = new Set(["missing-doctype"]);
|
|
4
|
-
const TEMPLATE_MARKERS = /\{\{|\}\}|\$\{|<%|%>|<#|#>|\[%|%\]/;
|
|
5
|
-
|
|
6
|
-
export function parseHtmlSource(path, source) {
|
|
7
|
-
const errors = [];
|
|
8
|
-
const document = parse(source, {
|
|
9
|
-
sourceCodeLocationInfo: true,
|
|
10
|
-
onParseError(error) {
|
|
11
|
-
if (!RECOVERABLE_PARSE_ERRORS.has(error.code)) errors.push(error);
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
if (errors.length > 0) {
|
|
15
|
-
throw new Error(`Failed to parse ${path}: ${errors.map((error) => error.code).join(", ")}`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const links = [];
|
|
19
|
-
const bases = [];
|
|
20
|
-
const elements = [];
|
|
21
|
-
const dynamicAttributes = [];
|
|
22
|
-
const scriptTexts = [];
|
|
23
|
-
|
|
24
|
-
function visit(node) {
|
|
25
|
-
if (node.tagName === "script") {
|
|
26
|
-
for (const child of node.childNodes ?? []) {
|
|
27
|
-
if (child.nodeName === "#text" && child.value) scriptTexts.push(child.value);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (node.tagName) {
|
|
31
|
-
const attributes = new Map(node.attrs.map((attribute) => [attribute.name, attribute.value]));
|
|
32
|
-
const locations = node.sourceCodeLocation?.attrs;
|
|
33
|
-
if (locations) {
|
|
34
|
-
if (
|
|
35
|
-
node.tagName === "link" &&
|
|
36
|
-
stylesheetRel(attributes.get("rel")) &&
|
|
37
|
-
!attributes.has("disabled")
|
|
38
|
-
) {
|
|
39
|
-
const href = locatedAttribute(source, locations.href, attributes.get("href"));
|
|
40
|
-
const media = locatedAttribute(source, locations.media, attributes.get("media"));
|
|
41
|
-
const tag = node.sourceCodeLocation?.startTag ?? node.sourceCodeLocation;
|
|
42
|
-
if (href && tag) {
|
|
43
|
-
links.push({
|
|
44
|
-
href: href.value,
|
|
45
|
-
media: media?.value ?? "",
|
|
46
|
-
start: href.start,
|
|
47
|
-
end: href.end,
|
|
48
|
-
tagStart: tag.startOffset,
|
|
49
|
-
tagEnd: tag.endOffset,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (node.tagName === "base" && bases.length === 0) {
|
|
54
|
-
const href = locatedAttribute(source, locations.href, attributes.get("href"));
|
|
55
|
-
if (href)
|
|
56
|
-
bases.push({
|
|
57
|
-
href: href.value,
|
|
58
|
-
writable: href.writable,
|
|
59
|
-
start: href.start,
|
|
60
|
-
end: href.end,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let classAttribute = locatedAttribute(source, locations.class, attributes.get("class"));
|
|
65
|
-
const idAttribute = locatedAttribute(source, locations.id, attributes.get("id"));
|
|
66
|
-
// A class attribute that exists but cannot be located as a writable
|
|
67
|
-
// value (e.g. valueless `<main class>`) must not look absent, or the
|
|
68
|
-
// id-only branch would synthesize a duplicate class attribute.
|
|
69
|
-
const unparsedClass =
|
|
70
|
-
!classAttribute && idAttribute && locations.class
|
|
71
|
-
? { start: locations.class.startOffset, end: locations.class.endOffset }
|
|
72
|
-
: undefined;
|
|
73
|
-
const dynamic =
|
|
74
|
-
unparsedClass ??
|
|
75
|
-
[classAttribute, idAttribute].find(
|
|
76
|
-
(attribute) =>
|
|
77
|
-
attribute && (!attribute.writable || TEMPLATE_MARKERS.test(attribute.value)),
|
|
78
|
-
) ??
|
|
79
|
-
(classAttribute && !classAttribute.quoted ? classAttribute : undefined);
|
|
80
|
-
if (dynamic) {
|
|
81
|
-
dynamicAttributes.push({ start: dynamic.start, end: dynamic.end });
|
|
82
|
-
} else if (classAttribute || idAttribute) {
|
|
83
|
-
if (!classAttribute && idAttribute) {
|
|
84
|
-
const insertion = classInsertionOffset(source, node.sourceCodeLocation?.startTag);
|
|
85
|
-
if (insertion !== undefined) {
|
|
86
|
-
classAttribute = { value: "", start: insertion, end: insertion, synthetic: true };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
elements.push({ classAttribute, idAttribute });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for (const child of node.childNodes ?? []) visit(child);
|
|
95
|
-
if (node.content) visit(node.content);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
visit(document);
|
|
99
|
-
return {
|
|
100
|
-
...toByteOffsets(source, { links, bases, elements, dynamicAttributes }),
|
|
101
|
-
scriptText: scriptTexts.join("\n"),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Map UTF-16 string indices to UTF-8 byte offsets in one source pass, so
|
|
106
|
-
// large documents stay linear instead of rescanning the prefix per site.
|
|
107
|
-
export function utf8OffsetMap(source, indices) {
|
|
108
|
-
const sorted = [...new Set(indices)].sort((left, right) => left - right);
|
|
109
|
-
const map = new Map();
|
|
110
|
-
let lastIndex = 0;
|
|
111
|
-
let bytes = 0;
|
|
112
|
-
for (const index of sorted) {
|
|
113
|
-
bytes += Buffer.byteLength(source.slice(lastIndex, index));
|
|
114
|
-
lastIndex = index;
|
|
115
|
-
map.set(index, bytes);
|
|
116
|
-
}
|
|
117
|
-
return map;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function toByteOffsets(source, parsed) {
|
|
121
|
-
const offsets = utf8OffsetMap(source, [
|
|
122
|
-
...parsed.links.flatMap((link) => [link.start, link.end, link.tagStart, link.tagEnd]),
|
|
123
|
-
...parsed.bases.flatMap((base) => [base.start, base.end]),
|
|
124
|
-
...parsed.elements.flatMap((element) =>
|
|
125
|
-
[element.classAttribute, element.idAttribute]
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
.flatMap((value) => [value.start, value.end]),
|
|
128
|
-
),
|
|
129
|
-
...parsed.dynamicAttributes.flatMap((value) => [value.start, value.end]),
|
|
130
|
-
]);
|
|
131
|
-
const offset = (index) => offsets.get(index);
|
|
132
|
-
const attribute = (value) =>
|
|
133
|
-
value && { ...value, start: offset(value.start), end: offset(value.end) };
|
|
134
|
-
return {
|
|
135
|
-
links: parsed.links.map((link) => ({
|
|
136
|
-
...link,
|
|
137
|
-
start: offset(link.start),
|
|
138
|
-
end: offset(link.end),
|
|
139
|
-
tagStart: offset(link.tagStart),
|
|
140
|
-
tagEnd: offset(link.tagEnd),
|
|
141
|
-
})),
|
|
142
|
-
bases: parsed.bases.map(attribute),
|
|
143
|
-
elements: parsed.elements.map((element) => ({
|
|
144
|
-
classAttribute: attribute(element.classAttribute),
|
|
145
|
-
idAttribute: attribute(element.idAttribute),
|
|
146
|
-
})),
|
|
147
|
-
dynamicAttributes: parsed.dynamicAttributes.map(attribute),
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function classInsertionOffset(source, startTag) {
|
|
152
|
-
if (!startTag) return undefined;
|
|
153
|
-
let offset = startTag.endOffset - 1;
|
|
154
|
-
if (source[offset] !== ">") return undefined;
|
|
155
|
-
while (offset > startTag.startOffset && /\s/.test(source[offset - 1])) offset -= 1;
|
|
156
|
-
if (source[offset - 1] === "/") offset -= 1;
|
|
157
|
-
return offset;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function stylesheetRel(value = "") {
|
|
161
|
-
const tokens = value.toLowerCase().split(/\s+/);
|
|
162
|
-
return tokens.includes("stylesheet") && !tokens.includes("alternate");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function locatedAttribute(source, location, parsedValue) {
|
|
166
|
-
if (!location || parsedValue === undefined) return undefined;
|
|
167
|
-
const raw = source.slice(location.startOffset, location.endOffset);
|
|
168
|
-
const equals = raw.indexOf("=");
|
|
169
|
-
if (equals < 0) return undefined;
|
|
170
|
-
let start = equals + 1;
|
|
171
|
-
while (/\s/.test(raw[start] ?? "")) start += 1;
|
|
172
|
-
const quote = raw[start];
|
|
173
|
-
let end;
|
|
174
|
-
const quoted = quote === '"' || quote === "'";
|
|
175
|
-
if (quoted) {
|
|
176
|
-
start += 1;
|
|
177
|
-
end = raw.lastIndexOf(quote);
|
|
178
|
-
if (end < start) return undefined;
|
|
179
|
-
} else {
|
|
180
|
-
end = start;
|
|
181
|
-
while (end < raw.length && !/\s/.test(raw[end])) end += 1;
|
|
182
|
-
}
|
|
183
|
-
const value = raw.slice(start, end);
|
|
184
|
-
return {
|
|
185
|
-
value: value.includes("&") ? parsedValue : value,
|
|
186
|
-
quoted,
|
|
187
|
-
writable: !value.includes("&"),
|
|
188
|
-
start: location.startOffset + start,
|
|
189
|
-
end: location.startOffset + end,
|
|
190
|
-
};
|
|
191
|
-
}
|