@nuvio/ast-engine 0.1.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 +5 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +328 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Nuvio contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# `@nuvio/ast-engine`
|
|
2
|
+
|
|
3
|
+
Parses TSX/JSX, locates `data-nuvio-id` hosts, applies patch ops (`setText`, `mergeTailwindClassName`, `moveSibling`, `setHidden`, `duplicateHost`) with whitelist + `tailwind-merge`, formats with Prettier. Golden tests in `src/apply-patch.test.ts`.
|
|
4
|
+
|
|
5
|
+
See the [Nuvio README](../../README.md) and [CHANGELOG](../../CHANGELOG.md).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PatchOp } from '@nuvio/shared';
|
|
2
|
+
|
|
3
|
+
type ApplyPatchToSourceResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
source: string;
|
|
6
|
+
diffSummary: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Apply Phase 2 patch operations to TSX/JSX source for a single `data-nuvio-id` host.
|
|
14
|
+
*/
|
|
15
|
+
declare function applyPatchToSource(source: string, filePath: string, hostId: string, ops: readonly PatchOp[]): Promise<ApplyPatchToSourceResult>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Phase 2 — conservative Tailwind v3-style allowlist for `mergeTailwindClassName`.
|
|
19
|
+
* Expand intentionally; unknown tokens are rejected before `tailwind-merge`.
|
|
20
|
+
*/
|
|
21
|
+
declare function validateTailwindFragment(fragment: string): void;
|
|
22
|
+
|
|
23
|
+
export { type ApplyPatchToSourceResult, applyPatchToSource, validateTailwindFragment };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// src/apply-patch.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { parse } from "@babel/parser";
|
|
5
|
+
import * as t from "@babel/types";
|
|
6
|
+
import prettier from "prettier";
|
|
7
|
+
import { twMerge } from "tailwind-merge";
|
|
8
|
+
|
|
9
|
+
// src/tailwind-whitelist.ts
|
|
10
|
+
var SPACING = /^(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(0|px|auto|0\.5|1|1\.5|2|2\.5|3|3\.5|4|5|6|7|8|9|10|11|12|14|16|20|24|28|32|36|40|44|48|52|56|60|64|72|80|96)$/;
|
|
11
|
+
var TEXT_SIZE = /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/;
|
|
12
|
+
var FONT_WEIGHT = /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/;
|
|
13
|
+
var LEADING = /^leading-(none|tight|snug|normal|relaxed|loose|[0-9]+)$/;
|
|
14
|
+
var COLOR_SOLID = /^(text|bg|border)-(inherit|current|transparent|black|white)$/;
|
|
15
|
+
var COLOR_SCALE = /^(text|bg|border)-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)$/;
|
|
16
|
+
var BG_COLOR_OPACITY = /^bg-(slate|sky|neutral)-(800|900|950)\/(50|75|80)$/;
|
|
17
|
+
var ROUNDED = /^rounded$|^rounded-(none|sm|md|lg|xl|2xl|3xl|full)$/;
|
|
18
|
+
var LAYOUT = /^(flex|inline-flex|block|inline|inline-block|grid|inline-grid|hidden|contents)$/;
|
|
19
|
+
var FLEX = /^(flex-row|flex-col|flex-wrap|flex-1|grow|shrink|basis-0|items-(start|end|center|baseline|stretch)|justify-(start|end|center|between|around|evenly))$/;
|
|
20
|
+
var BORDER_W = /^border(-(0|2|4|8))?$/;
|
|
21
|
+
var TEXT_ALIGN = /^text-(left|center|right|justify|start|end)$/;
|
|
22
|
+
var OPACITY = /^opacity-(0|5|10|15|20|25|30|40|50|60|70|75|80|90|95|100)$/;
|
|
23
|
+
var SHADOW = /^shadow$|^shadow-(sm|md|lg|xl|2xl|inner|none)$/;
|
|
24
|
+
var W_WIDTH = /^w-(auto|full|screen|min|max|fit|px|0|0\.5|1|1\.5|2|2\.5|3|3\.5|4|5|6|7|8|9|10|11|12|14|16|20|24|28|32|36|40|44|48|52|56|60|64|72|80|96|1\/2|1\/3|2\/3|1\/4|3\/4)$/;
|
|
25
|
+
var H_HEIGHT = /^h-(auto|full|screen|min|max|fit|px|0|0\.5|1|1\.5|2|2\.5|3|3\.5|4|5|6|7|8|9|10|11|12|14|16|20|24|28|32|36|40|44|48|52|56|60|64|72|80|96)$/;
|
|
26
|
+
var MAX_W = /^max-w-(none|xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|full|min|max|fit|prose)$/;
|
|
27
|
+
var MIN_H = /^min-h-(0|full|screen|min|max|fit|px|0\.5|1|1\.5|2|2\.5|3|3\.5|4|5|6|7|8|9|10|11|12|14|16|20|24|28|32|36|40|44|48|52|56|60|64|72|80|96)$/;
|
|
28
|
+
function normalizeTailwindToken(raw) {
|
|
29
|
+
return raw.trim().replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/[\u00AD\u2010\u2011\u2012\u2013\u2014\u2212]/g, "-");
|
|
30
|
+
}
|
|
31
|
+
function validateTailwindFragment(fragment) {
|
|
32
|
+
const tokens = fragment.trim().split(/\s+/).filter(Boolean);
|
|
33
|
+
for (const raw of tokens) {
|
|
34
|
+
const t2 = normalizeTailwindToken(raw);
|
|
35
|
+
if (!t2) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (SPACING.test(t2) || TEXT_SIZE.test(t2) || FONT_WEIGHT.test(t2) || LEADING.test(t2) || COLOR_SOLID.test(t2) || COLOR_SCALE.test(t2) || BG_COLOR_OPACITY.test(t2) || ROUNDED.test(t2) || LAYOUT.test(t2) || FLEX.test(t2) || BORDER_W.test(t2) || TEXT_ALIGN.test(t2) || OPACITY.test(t2) || SHADOW.test(t2) || W_WIDTH.test(t2) || H_HEIGHT.test(t2) || MAX_W.test(t2) || MIN_H.test(t2)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unknown or disallowed Tailwind utility: ${t2}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/apply-patch.ts
|
|
46
|
+
var require2 = createRequire(import.meta.url);
|
|
47
|
+
var traverse = require2("@babel/traverse").default;
|
|
48
|
+
var generate = require2("@babel/generator").default;
|
|
49
|
+
function findHostOpening(ast, hostId) {
|
|
50
|
+
let found = null;
|
|
51
|
+
traverse(ast, {
|
|
52
|
+
JSXOpeningElement(path2) {
|
|
53
|
+
for (const attr of path2.node.attributes) {
|
|
54
|
+
if (!t.isJSXAttribute(attr)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!t.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (t.isStringLiteral(attr.value) && attr.value.value === hostId) {
|
|
61
|
+
found = path2;
|
|
62
|
+
path2.stop();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
function applySetText(openingPath, text) {
|
|
71
|
+
const parent = openingPath.parentPath;
|
|
72
|
+
if (!parent?.isJSXElement()) {
|
|
73
|
+
throw new Error("Host is not a JSX element");
|
|
74
|
+
}
|
|
75
|
+
const jsx = parent;
|
|
76
|
+
const { children } = jsx.node;
|
|
77
|
+
if (children.length === 0) {
|
|
78
|
+
jsx.node.children = [t.jsxText(text)];
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (children.length === 1 && t.isJSXText(children[0])) {
|
|
82
|
+
children[0].value = text;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (children.length === 1 && t.isJSXExpressionContainer(children[0]) && t.isStringLiteral(children[0].expression)) {
|
|
86
|
+
children[0].expression.value = text;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
jsx.node.children = [t.jsxText(text)];
|
|
90
|
+
}
|
|
91
|
+
function readStringLiteralClassName(opening) {
|
|
92
|
+
for (const attr of opening.attributes) {
|
|
93
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
94
|
+
if (t.isStringLiteral(attr.value)) {
|
|
95
|
+
return attr.value.value;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
function parentSupportsLayoutMoves(parentOpening) {
|
|
103
|
+
const cls = readStringLiteralClassName(parentOpening);
|
|
104
|
+
if (cls === null) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return /\b(flex|inline-flex|grid|inline-grid)\b/.test(cls) || /\b(flex-|grid-)/.test(cls);
|
|
108
|
+
}
|
|
109
|
+
function collectJsxElementChildIndices(parent) {
|
|
110
|
+
const indices = [];
|
|
111
|
+
parent.children.forEach((child, i) => {
|
|
112
|
+
if (t.isJSXElement(child)) {
|
|
113
|
+
indices.push(i);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return indices;
|
|
117
|
+
}
|
|
118
|
+
function applyMoveSibling(openingPath, direction) {
|
|
119
|
+
const hostPath = openingPath.parentPath;
|
|
120
|
+
if (!hostPath?.isJSXElement()) {
|
|
121
|
+
throw new Error("Host is not a JSX element");
|
|
122
|
+
}
|
|
123
|
+
const parentPath = hostPath.parentPath;
|
|
124
|
+
if (!parentPath?.isJSXElement()) {
|
|
125
|
+
throw new Error("Move requires a JSX element parent (same flex/grid container)");
|
|
126
|
+
}
|
|
127
|
+
const parentOpening = parentPath.node.openingElement;
|
|
128
|
+
if (!parentSupportsLayoutMoves(parentOpening)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Parent must use flex or grid layout (string-literal className with flex/grid utilities)"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const parent = parentPath.node;
|
|
134
|
+
const jsxIndices = collectJsxElementChildIndices(parent);
|
|
135
|
+
const hostIndex = parent.children.indexOf(hostPath.node);
|
|
136
|
+
if (hostIndex < 0) {
|
|
137
|
+
throw new Error("Host not found in parent children");
|
|
138
|
+
}
|
|
139
|
+
const pos = jsxIndices.indexOf(hostIndex);
|
|
140
|
+
if (pos < 0) {
|
|
141
|
+
throw new Error("Host must be a direct JSX element child of the layout parent");
|
|
142
|
+
}
|
|
143
|
+
if (direction === "up" && pos === 0) {
|
|
144
|
+
throw new Error("Already the first sibling");
|
|
145
|
+
}
|
|
146
|
+
if (direction === "down" && pos === jsxIndices.length - 1) {
|
|
147
|
+
throw new Error("Already the last sibling");
|
|
148
|
+
}
|
|
149
|
+
const swapIndex = direction === "up" ? jsxIndices[pos - 1] : jsxIndices[pos + 1];
|
|
150
|
+
const hostNode = parent.children[hostIndex];
|
|
151
|
+
parent.children[hostIndex] = parent.children[swapIndex];
|
|
152
|
+
parent.children[swapIndex] = hostNode;
|
|
153
|
+
}
|
|
154
|
+
function applySetHidden(openingPath, hidden) {
|
|
155
|
+
if (hidden) {
|
|
156
|
+
applyMergeClassName(openingPath, "hidden");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const opening = openingPath.node;
|
|
160
|
+
let clsAttr;
|
|
161
|
+
for (const attr of opening.attributes) {
|
|
162
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
163
|
+
clsAttr = attr;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!clsAttr || !t.isStringLiteral(clsAttr.value)) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const tokens = clsAttr.value.value.split(/\s+/).filter((tok) => tok && tok !== "hidden");
|
|
171
|
+
clsAttr.value = t.stringLiteral(twMerge(tokens.join(" ")));
|
|
172
|
+
}
|
|
173
|
+
function collectNuvioIds(ast) {
|
|
174
|
+
const ids = /* @__PURE__ */ new Set();
|
|
175
|
+
traverse(ast, {
|
|
176
|
+
JSXOpeningElement(path2) {
|
|
177
|
+
for (const attr of path2.node.attributes) {
|
|
178
|
+
if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (t.isStringLiteral(attr.value)) {
|
|
182
|
+
ids.add(attr.value.value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return ids;
|
|
188
|
+
}
|
|
189
|
+
function setNuvioIdOnOpening(opening, id) {
|
|
190
|
+
for (const attr of opening.attributes) {
|
|
191
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
192
|
+
if (t.isStringLiteral(attr.value)) {
|
|
193
|
+
attr.value.value = id;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
opening.attributes.push(
|
|
199
|
+
t.jsxAttribute(t.jsxIdentifier("data-nuvio-id"), t.stringLiteral(id))
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
function uniqueDuplicateId(baseId, taken) {
|
|
203
|
+
const candidate = `${baseId}.copy`;
|
|
204
|
+
if (!taken.has(candidate)) {
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
207
|
+
let n = 2;
|
|
208
|
+
while (taken.has(`${baseId}.copy${n}`)) {
|
|
209
|
+
n += 1;
|
|
210
|
+
}
|
|
211
|
+
return `${baseId}.copy${n}`;
|
|
212
|
+
}
|
|
213
|
+
function applyDuplicateHost(ast, openingPath, hostId) {
|
|
214
|
+
const hostPath = openingPath.parentPath;
|
|
215
|
+
if (!hostPath?.isJSXElement()) {
|
|
216
|
+
throw new Error("Host is not a JSX element");
|
|
217
|
+
}
|
|
218
|
+
const parentPath = hostPath.parentPath;
|
|
219
|
+
if (!parentPath?.isJSXElement()) {
|
|
220
|
+
throw new Error("Duplicate requires a JSX element parent");
|
|
221
|
+
}
|
|
222
|
+
const taken = collectNuvioIds(ast);
|
|
223
|
+
const newId = uniqueDuplicateId(hostId, taken);
|
|
224
|
+
const clone = t.cloneNode(hostPath.node, true);
|
|
225
|
+
if (!t.isJSXElement(clone)) {
|
|
226
|
+
throw new Error("Failed to clone host element");
|
|
227
|
+
}
|
|
228
|
+
setNuvioIdOnOpening(clone.openingElement, newId);
|
|
229
|
+
const parent = parentPath.node;
|
|
230
|
+
const hostIndex = parent.children.indexOf(hostPath.node);
|
|
231
|
+
if (hostIndex < 0) {
|
|
232
|
+
throw new Error("Host not found in parent children");
|
|
233
|
+
}
|
|
234
|
+
parent.children.splice(hostIndex + 1, 0, clone);
|
|
235
|
+
return newId;
|
|
236
|
+
}
|
|
237
|
+
function applyMergeClassName(openingPath, fragment) {
|
|
238
|
+
validateTailwindFragment(fragment);
|
|
239
|
+
const opening = openingPath.node;
|
|
240
|
+
let clsAttr;
|
|
241
|
+
for (const attr of opening.attributes) {
|
|
242
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
243
|
+
clsAttr = attr;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!clsAttr) {
|
|
248
|
+
opening.attributes.push(
|
|
249
|
+
t.jsxAttribute(t.jsxIdentifier("className"), t.stringLiteral(fragment.trim()))
|
|
250
|
+
);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!t.isStringLiteral(clsAttr.value)) {
|
|
254
|
+
throw new Error("className must be a string literal for Phase 2 patches");
|
|
255
|
+
}
|
|
256
|
+
const current = clsAttr.value.value;
|
|
257
|
+
clsAttr.value = t.stringLiteral(twMerge(current, fragment.trim()));
|
|
258
|
+
}
|
|
259
|
+
async function applyPatchToSource(source, filePath, hostId, ops) {
|
|
260
|
+
let ast;
|
|
261
|
+
try {
|
|
262
|
+
ast = parse(source, {
|
|
263
|
+
sourceType: "module",
|
|
264
|
+
plugins: ["typescript", "jsx"],
|
|
265
|
+
sourceFilename: filePath
|
|
266
|
+
});
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return { ok: false, code: "parse_error", message: String(e) };
|
|
269
|
+
}
|
|
270
|
+
const openingPath = findHostOpening(ast, hostId);
|
|
271
|
+
if (!openingPath) {
|
|
272
|
+
return { ok: false, code: "host_not_found", message: `No JSX host with data-nuvio-id="${hostId}"` };
|
|
273
|
+
}
|
|
274
|
+
let duplicateNewId;
|
|
275
|
+
try {
|
|
276
|
+
for (const op of ops) {
|
|
277
|
+
if (op.kind === "setText") {
|
|
278
|
+
applySetText(openingPath, op.text);
|
|
279
|
+
} else if (op.kind === "mergeTailwindClassName") {
|
|
280
|
+
applyMergeClassName(openingPath, op.classNameFragment);
|
|
281
|
+
} else if (op.kind === "moveSibling") {
|
|
282
|
+
applyMoveSibling(openingPath, op.direction);
|
|
283
|
+
} else if (op.kind === "setHidden") {
|
|
284
|
+
applySetHidden(openingPath, op.hidden);
|
|
285
|
+
} else if (op.kind === "duplicateHost") {
|
|
286
|
+
duplicateNewId = applyDuplicateHost(ast, openingPath, hostId);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch (e) {
|
|
290
|
+
return { ok: false, code: "patch_rejected", message: String(e) };
|
|
291
|
+
}
|
|
292
|
+
let raw;
|
|
293
|
+
try {
|
|
294
|
+
raw = generate(ast, { retainLines: false, comments: true }).code;
|
|
295
|
+
} catch (e) {
|
|
296
|
+
return { ok: false, code: "generate_error", message: String(e) };
|
|
297
|
+
}
|
|
298
|
+
let formatted;
|
|
299
|
+
try {
|
|
300
|
+
formatted = await prettier.format(raw, {
|
|
301
|
+
parser: "typescript",
|
|
302
|
+
filepath: filePath
|
|
303
|
+
});
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return { ok: false, code: "format_error", message: String(e) };
|
|
306
|
+
}
|
|
307
|
+
const base = path.basename(filePath);
|
|
308
|
+
const opBits = ops.map((op) => {
|
|
309
|
+
switch (op.kind) {
|
|
310
|
+
case "setText":
|
|
311
|
+
return `set text (${op.text.length} char${op.text.length === 1 ? "" : "s"})`;
|
|
312
|
+
case "mergeTailwindClassName":
|
|
313
|
+
return `merge className (${op.classNameFragment.trim()})`;
|
|
314
|
+
case "moveSibling":
|
|
315
|
+
return `move sibling ${op.direction}`;
|
|
316
|
+
case "setHidden":
|
|
317
|
+
return op.hidden ? "hide element" : "show element";
|
|
318
|
+
case "duplicateHost":
|
|
319
|
+
return duplicateNewId ? `duplicate host \u2192 ${duplicateNewId}` : "duplicate host";
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const diffSummary = `${base}: ${opBits.join("; ")}`;
|
|
323
|
+
return { ok: true, source: formatted, diffSummary };
|
|
324
|
+
}
|
|
325
|
+
export {
|
|
326
|
+
applyPatchToSource,
|
|
327
|
+
validateTailwindFragment
|
|
328
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nuvio/ast-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Nuvio AST patch engine: parse TSX/JSX, apply whitelist Tailwind merges and text edits, format with Prettier.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ehah/Nuvio.git",
|
|
9
|
+
"directory": "packages/ast-engine"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"nuvio",
|
|
13
|
+
"ast",
|
|
14
|
+
"tsx",
|
|
15
|
+
"tailwind",
|
|
16
|
+
"babel"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@babel/generator": "^7.26.9",
|
|
39
|
+
"@babel/parser": "^7.26.9",
|
|
40
|
+
"@babel/traverse": "^7.26.9",
|
|
41
|
+
"@babel/types": "^7.26.9",
|
|
42
|
+
"prettier": "^3.5.1",
|
|
43
|
+
"tailwind-merge": "^2.6.0",
|
|
44
|
+
"@nuvio/shared": "0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/babel__traverse": "^7.20.7",
|
|
48
|
+
"@types/node": "^22.13.5",
|
|
49
|
+
"tsup": "^8.4.0",
|
|
50
|
+
"typescript": "^5.7.3",
|
|
51
|
+
"vitest": "^3.0.6"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
55
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
56
|
+
"test": "vitest run"
|
|
57
|
+
}
|
|
58
|
+
}
|