@nuvio/ast-engine 0.1.0 → 0.5.1
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/dist/index.d.ts +19 -2
- package/dist/index.js +384 -61
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { PatchOp } from '@nuvio/shared';
|
|
2
2
|
|
|
3
|
+
type ClassNameMode = "literal-only" | "cn-basic";
|
|
4
|
+
type Breakpoint = "base" | "sm" | "md" | "lg" | "xl";
|
|
5
|
+
type BreakpointBuckets = {
|
|
6
|
+
base: string[];
|
|
7
|
+
sm: string[];
|
|
8
|
+
md: string[];
|
|
9
|
+
lg: string[];
|
|
10
|
+
xl: string[];
|
|
11
|
+
passthrough: string[];
|
|
12
|
+
};
|
|
13
|
+
declare function parseClassNameByBreakpoint(className: string): BreakpointBuckets;
|
|
14
|
+
declare function mergeAtBreakpoint(className: string, fragment: string, activeBreakpoint: Breakpoint): string;
|
|
15
|
+
/** Strip allowlisted utilities from the active breakpoint (inverse of mergeAtBreakpoint). */
|
|
16
|
+
declare function removeAtBreakpoint(className: string, fragment: string, activeBreakpoint: Breakpoint): string;
|
|
3
17
|
type ApplyPatchToSourceResult = {
|
|
4
18
|
ok: true;
|
|
5
19
|
source: string;
|
|
@@ -12,7 +26,10 @@ type ApplyPatchToSourceResult = {
|
|
|
12
26
|
/**
|
|
13
27
|
* Apply Phase 2 patch operations to TSX/JSX source for a single `data-nuvio-id` host.
|
|
14
28
|
*/
|
|
15
|
-
declare function applyPatchToSource(source: string, filePath: string, hostId: string, ops: readonly PatchOp[]
|
|
29
|
+
declare function applyPatchToSource(source: string, filePath: string, hostId: string, ops: readonly PatchOp[], options?: {
|
|
30
|
+
classNameMode?: ClassNameMode;
|
|
31
|
+
activeBreakpoint?: Breakpoint;
|
|
32
|
+
}): Promise<ApplyPatchToSourceResult>;
|
|
16
33
|
|
|
17
34
|
/**
|
|
18
35
|
* Phase 2 — conservative Tailwind v3-style allowlist for `mergeTailwindClassName`.
|
|
@@ -20,4 +37,4 @@ declare function applyPatchToSource(source: string, filePath: string, hostId: st
|
|
|
20
37
|
*/
|
|
21
38
|
declare function validateTailwindFragment(fragment: string): void;
|
|
22
39
|
|
|
23
|
-
export { type ApplyPatchToSourceResult, applyPatchToSource, validateTailwindFragment };
|
|
40
|
+
export { type ApplyPatchToSourceResult, applyPatchToSource, mergeAtBreakpoint, parseClassNameByBreakpoint, removeAtBreakpoint, validateTailwindFragment };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/apply-patch.ts
|
|
2
|
-
import { createRequire } from "module";
|
|
2
|
+
import { createRequire as createRequire2 } from "module";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { parse } from "@babel/parser";
|
|
5
|
-
import * as
|
|
4
|
+
import { parse as parse2 } from "@babel/parser";
|
|
5
|
+
import * as t2 from "@babel/types";
|
|
6
6
|
import prettier from "prettier";
|
|
7
7
|
import { twMerge } from "tailwind-merge";
|
|
8
8
|
|
|
@@ -17,8 +17,13 @@ var BG_COLOR_OPACITY = /^bg-(slate|sky|neutral)-(800|900|950)\/(50|75|80)$/;
|
|
|
17
17
|
var ROUNDED = /^rounded$|^rounded-(none|sm|md|lg|xl|2xl|3xl|full)$/;
|
|
18
18
|
var LAYOUT = /^(flex|inline-flex|block|inline|inline-block|grid|inline-grid|hidden|contents)$/;
|
|
19
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 GRID_COLS = /^grid-cols-(1|2|3|4|5|6|7|8|9|10|11|12)$/;
|
|
20
21
|
var BORDER_W = /^border(-(0|2|4|8))?$/;
|
|
22
|
+
var BORDER_COLOR = /^border-(inherit|current|transparent|black|white|(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))$/;
|
|
23
|
+
var RING = /^ring(-(0|1|2|4|8))?$/;
|
|
24
|
+
var RING_COLOR = /^ring-(inherit|current|transparent|black|white|(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))$/;
|
|
21
25
|
var TEXT_ALIGN = /^text-(left|center|right|justify|start|end)$/;
|
|
26
|
+
var TRACKING = /^tracking-(tighter|tight|normal|wide|wider|widest)$/;
|
|
22
27
|
var OPACITY = /^opacity-(0|5|10|15|20|25|30|40|50|60|70|75|80|90|95|100)$/;
|
|
23
28
|
var SHADOW = /^shadow$|^shadow-(sm|md|lg|xl|2xl|inner|none)$/;
|
|
24
29
|
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)$/;
|
|
@@ -31,33 +36,136 @@ function normalizeTailwindToken(raw) {
|
|
|
31
36
|
function validateTailwindFragment(fragment) {
|
|
32
37
|
const tokens = fragment.trim().split(/\s+/).filter(Boolean);
|
|
33
38
|
for (const raw of tokens) {
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
39
|
+
const t3 = normalizeTailwindToken(raw);
|
|
40
|
+
if (!t3) {
|
|
36
41
|
continue;
|
|
37
42
|
}
|
|
38
|
-
if (SPACING.test(
|
|
43
|
+
if (SPACING.test(t3) || TEXT_SIZE.test(t3) || FONT_WEIGHT.test(t3) || LEADING.test(t3) || COLOR_SOLID.test(t3) || COLOR_SCALE.test(t3) || BG_COLOR_OPACITY.test(t3) || ROUNDED.test(t3) || LAYOUT.test(t3) || FLEX.test(t3) || GRID_COLS.test(t3) || BORDER_W.test(t3) || BORDER_COLOR.test(t3) || RING.test(t3) || RING_COLOR.test(t3) || TEXT_ALIGN.test(t3) || TRACKING.test(t3) || OPACITY.test(t3) || SHADOW.test(t3) || W_WIDTH.test(t3) || H_HEIGHT.test(t3) || MAX_W.test(t3) || MIN_H.test(t3)) {
|
|
39
44
|
continue;
|
|
40
45
|
}
|
|
41
|
-
throw new Error(`Unknown or disallowed Tailwind utility: ${
|
|
46
|
+
throw new Error(`Unknown or disallowed Tailwind utility: ${t3}`);
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
// src/
|
|
50
|
+
// src/set-table-data-field.ts
|
|
51
|
+
import { parse } from "@babel/parser";
|
|
52
|
+
import * as t from "@babel/types";
|
|
53
|
+
import { createRequire } from "module";
|
|
46
54
|
var require2 = createRequire(import.meta.url);
|
|
47
55
|
var traverse = require2("@babel/traverse").default;
|
|
48
|
-
|
|
56
|
+
function applySetTableDataField(ast, arrayName, rowKey, field, value) {
|
|
57
|
+
let updated = false;
|
|
58
|
+
traverse(ast, {
|
|
59
|
+
VariableDeclarator(path2) {
|
|
60
|
+
if (updated || !t.isIdentifier(path2.node.id) || path2.node.id.name !== arrayName) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!t.isArrayExpression(path2.node.init)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const el of path2.node.init.elements) {
|
|
67
|
+
if (!el || !t.isObjectExpression(el)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let idValue = null;
|
|
71
|
+
for (const prop of el.properties) {
|
|
72
|
+
if (!t.isObjectProperty(prop) || !t.isIdentifier(prop.key)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (prop.key.name === "id" && t.isNumericLiteral(prop.value)) {
|
|
76
|
+
idValue = String(prop.value.value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (idValue !== rowKey) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const prop of el.properties) {
|
|
83
|
+
if (!t.isObjectProperty(prop) || !t.isIdentifier(prop.key)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (prop.key.name !== field) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (t.isStringLiteral(prop.value)) {
|
|
90
|
+
prop.value.value = value;
|
|
91
|
+
updated = true;
|
|
92
|
+
path2.stop();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!updated) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`No ${arrayName} entry with id=${rowKey} and string field "${field}" found in source`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/apply-patch.ts
|
|
107
|
+
var require3 = createRequire2(import.meta.url);
|
|
108
|
+
var traverse2 = require3("@babel/traverse").default;
|
|
109
|
+
var generate = require3("@babel/generator").default;
|
|
110
|
+
function extractRowKeysFromSource(ast) {
|
|
111
|
+
const keys = [];
|
|
112
|
+
traverse2(ast, {
|
|
113
|
+
VariableDeclarator(path2) {
|
|
114
|
+
if (!t2.isIdentifier(path2.node.id) || path2.node.id.name !== "tableData") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!t2.isArrayExpression(path2.node.init)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const el of path2.node.init.elements) {
|
|
121
|
+
if (!el || !t2.isObjectExpression(el)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const prop of el.properties) {
|
|
125
|
+
if (t2.isObjectProperty(prop) && t2.isIdentifier(prop.key, { name: "id" }) && t2.isNumericLiteral(prop.value)) {
|
|
126
|
+
keys.push(String(prop.value.value));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return keys;
|
|
133
|
+
}
|
|
134
|
+
function templateLiteralMatchesHostId(attr, hostId, rowKeys) {
|
|
135
|
+
if (!t2.isJSXExpressionContainer(attr.value)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const expr = attr.value.expression;
|
|
139
|
+
if (!t2.isTemplateLiteral(expr) || expr.expressions.length !== 1) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const ex = expr.expressions[0];
|
|
143
|
+
const mapOk = t2.isIdentifier(ex, { name: "product" }) || t2.isIdentifier(ex, { name: "item" }) || t2.isMemberExpression(ex) && !ex.computed && t2.isIdentifier(ex.object, { name: "product" }) && t2.isIdentifier(ex.property, { name: "id" });
|
|
144
|
+
if (!mapOk) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const prefix = expr.quasis[0]?.value.cooked ?? "";
|
|
148
|
+
const suffix = expr.quasis[1]?.value.cooked ?? "";
|
|
149
|
+
return rowKeys.some((key) => `${prefix}${key}${suffix}` === hostId);
|
|
150
|
+
}
|
|
49
151
|
function findHostOpening(ast, hostId) {
|
|
152
|
+
const rowKeys = extractRowKeysFromSource(ast);
|
|
50
153
|
let found = null;
|
|
51
|
-
|
|
154
|
+
traverse2(ast, {
|
|
52
155
|
JSXOpeningElement(path2) {
|
|
53
156
|
for (const attr of path2.node.attributes) {
|
|
54
|
-
if (!
|
|
157
|
+
if (!t2.isJSXAttribute(attr)) {
|
|
55
158
|
continue;
|
|
56
159
|
}
|
|
57
|
-
if (!
|
|
160
|
+
if (!t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
58
161
|
continue;
|
|
59
162
|
}
|
|
60
|
-
if (
|
|
163
|
+
if (t2.isStringLiteral(attr.value) && attr.value.value === hostId) {
|
|
164
|
+
found = path2;
|
|
165
|
+
path2.stop();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (rowKeys.length > 0 && templateLiteralMatchesHostId(attr, hostId, rowKeys)) {
|
|
61
169
|
found = path2;
|
|
62
170
|
path2.stop();
|
|
63
171
|
return;
|
|
@@ -75,41 +183,189 @@ function applySetText(openingPath, text) {
|
|
|
75
183
|
const jsx = parent;
|
|
76
184
|
const { children } = jsx.node;
|
|
77
185
|
if (children.length === 0) {
|
|
78
|
-
jsx.node.children = [
|
|
186
|
+
jsx.node.children = [t2.jsxText(text)];
|
|
79
187
|
return;
|
|
80
188
|
}
|
|
81
|
-
if (children.length === 1 &&
|
|
189
|
+
if (children.length === 1 && t2.isJSXText(children[0])) {
|
|
82
190
|
children[0].value = text;
|
|
83
191
|
return;
|
|
84
192
|
}
|
|
85
|
-
if (children.length === 1 &&
|
|
193
|
+
if (children.length === 1 && t2.isJSXExpressionContainer(children[0]) && t2.isStringLiteral(children[0].expression)) {
|
|
86
194
|
children[0].expression.value = text;
|
|
87
195
|
return;
|
|
88
196
|
}
|
|
89
|
-
jsx.node.children = [
|
|
197
|
+
jsx.node.children = [t2.jsxText(text)];
|
|
198
|
+
}
|
|
199
|
+
function emptyBreakpointBuckets() {
|
|
200
|
+
return {
|
|
201
|
+
base: [],
|
|
202
|
+
sm: [],
|
|
203
|
+
md: [],
|
|
204
|
+
lg: [],
|
|
205
|
+
xl: [],
|
|
206
|
+
passthrough: []
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function classifyTokenByBreakpoint(token) {
|
|
210
|
+
if (!token.includes(":")) {
|
|
211
|
+
return { bp: "base", value: token };
|
|
212
|
+
}
|
|
213
|
+
const m = token.match(/^(sm|md|lg|xl):(.*)$/);
|
|
214
|
+
if (!m) {
|
|
215
|
+
return { bp: "passthrough", value: token };
|
|
216
|
+
}
|
|
217
|
+
if (!m[2] || m[2].includes(":")) {
|
|
218
|
+
return { bp: "passthrough", value: token };
|
|
219
|
+
}
|
|
220
|
+
return { bp: m[1], value: m[2] };
|
|
221
|
+
}
|
|
222
|
+
function parseClassNameByBreakpoint(className) {
|
|
223
|
+
const buckets = emptyBreakpointBuckets();
|
|
224
|
+
const tokens = className.trim().split(/\s+/).filter(Boolean);
|
|
225
|
+
for (const tok of tokens) {
|
|
226
|
+
const parsed = classifyTokenByBreakpoint(tok);
|
|
227
|
+
if (parsed.bp === "passthrough") {
|
|
228
|
+
buckets.passthrough.push(parsed.value);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
buckets[parsed.bp].push(parsed.value);
|
|
232
|
+
}
|
|
233
|
+
return buckets;
|
|
234
|
+
}
|
|
235
|
+
function prefixTokenForBreakpoint(token, bp) {
|
|
236
|
+
return bp === "base" ? token : `${bp}:${token}`;
|
|
237
|
+
}
|
|
238
|
+
var BREAKPOINT_ORDER = ["base", "sm", "md", "lg", "xl"];
|
|
239
|
+
function serializeClassNameFromBuckets(buckets) {
|
|
240
|
+
const out = [];
|
|
241
|
+
const mergedBase = buckets.base.join(" ").trim();
|
|
242
|
+
if (mergedBase) {
|
|
243
|
+
out.push(mergedBase);
|
|
244
|
+
}
|
|
245
|
+
for (const bp of BREAKPOINT_ORDER.slice(1)) {
|
|
246
|
+
const merged = buckets[bp].join(" ").trim();
|
|
247
|
+
if (merged) {
|
|
248
|
+
out.push(
|
|
249
|
+
...merged.split(/\s+/).filter(Boolean).map((t3) => prefixTokenForBreakpoint(t3, bp))
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
out.push(...buckets.passthrough);
|
|
254
|
+
return out.join(" ").trim();
|
|
255
|
+
}
|
|
256
|
+
function flattenTokensAtBreakpointFromBuckets(buckets, activeBreakpoint) {
|
|
257
|
+
const idx = BREAKPOINT_ORDER.indexOf(activeBreakpoint);
|
|
258
|
+
const out = [];
|
|
259
|
+
for (const tok of buckets.passthrough) {
|
|
260
|
+
if (/^dark:/.test(tok)) {
|
|
261
|
+
out.push(tok.replace(/^dark:/, ""));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (let i = 0; i <= idx; i++) {
|
|
265
|
+
out.push(...buckets[BREAKPOINT_ORDER[i]]);
|
|
266
|
+
}
|
|
267
|
+
for (const tok of buckets.passthrough) {
|
|
268
|
+
if (!/^dark:/.test(tok)) {
|
|
269
|
+
out.push(tok);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function mergeAtBreakpoint(className, fragment, activeBreakpoint) {
|
|
275
|
+
const buckets = parseClassNameByBreakpoint(className);
|
|
276
|
+
const incomingBuckets = emptyBreakpointBuckets();
|
|
277
|
+
for (const tok of fragment.trim().split(/\s+/).filter(Boolean)) {
|
|
278
|
+
const parsed = classifyTokenByBreakpoint(tok);
|
|
279
|
+
if (parsed.bp === "passthrough") {
|
|
280
|
+
incomingBuckets.passthrough.push(parsed.value);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const targetBp = parsed.bp === "base" ? activeBreakpoint : parsed.bp;
|
|
284
|
+
incomingBuckets[targetBp].push(parsed.value);
|
|
285
|
+
}
|
|
286
|
+
const mergedBase = twMerge(buckets.base.join(" "), incomingBuckets.base.join(" ")).trim();
|
|
287
|
+
const mergedSm = twMerge(buckets.sm.join(" "), incomingBuckets.sm.join(" ")).trim();
|
|
288
|
+
const mergedMd = twMerge(buckets.md.join(" "), incomingBuckets.md.join(" ")).trim();
|
|
289
|
+
const mergedLg = twMerge(buckets.lg.join(" "), incomingBuckets.lg.join(" ")).trim();
|
|
290
|
+
const mergedXl = twMerge(buckets.xl.join(" "), incomingBuckets.xl.join(" ")).trim();
|
|
291
|
+
return serializeClassNameFromBuckets({
|
|
292
|
+
base: mergedBase ? mergedBase.split(/\s+/).filter(Boolean) : [],
|
|
293
|
+
sm: mergedSm ? mergedSm.split(/\s+/).filter(Boolean) : [],
|
|
294
|
+
md: mergedMd ? mergedMd.split(/\s+/).filter(Boolean) : [],
|
|
295
|
+
lg: mergedLg ? mergedLg.split(/\s+/).filter(Boolean) : [],
|
|
296
|
+
xl: mergedXl ? mergedXl.split(/\s+/).filter(Boolean) : [],
|
|
297
|
+
passthrough: [...buckets.passthrough, ...incomingBuckets.passthrough]
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function removeAtBreakpoint(className, fragment, activeBreakpoint) {
|
|
301
|
+
const buckets = parseClassNameByBreakpoint(className);
|
|
302
|
+
const toRemove = fragment.trim().split(/\s+/).filter(Boolean);
|
|
303
|
+
if (toRemove.length === 0) {
|
|
304
|
+
return className.trim();
|
|
305
|
+
}
|
|
306
|
+
const idx = BREAKPOINT_ORDER.indexOf(activeBreakpoint);
|
|
307
|
+
for (const tok of toRemove) {
|
|
308
|
+
buckets[activeBreakpoint] = buckets[activeBreakpoint].filter((t3) => t3 !== tok);
|
|
309
|
+
const stillPresent = flattenTokensAtBreakpointFromBuckets(buckets, activeBreakpoint).includes(tok);
|
|
310
|
+
if (stillPresent) {
|
|
311
|
+
for (let i = idx; i >= 0; i--) {
|
|
312
|
+
const bp = BREAKPOINT_ORDER[i];
|
|
313
|
+
if (buckets[bp].includes(tok)) {
|
|
314
|
+
buckets[bp] = buckets[bp].filter((t3) => t3 !== tok);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return serializeClassNameFromBuckets(buckets);
|
|
90
321
|
}
|
|
91
|
-
function
|
|
322
|
+
function getClassNameBinding(opening, classNameMode) {
|
|
92
323
|
for (const attr of opening.attributes) {
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
95
|
-
|
|
324
|
+
if (t2.isJSXAttribute(attr) && t2.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
325
|
+
if (t2.isStringLiteral(attr.value)) {
|
|
326
|
+
const literal = attr.value;
|
|
327
|
+
return {
|
|
328
|
+
read: () => literal.value,
|
|
329
|
+
write: (next) => {
|
|
330
|
+
attr.value = t2.stringLiteral(next);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (classNameMode === "cn-basic" && t2.isJSXExpressionContainer(attr.value) && t2.isCallExpression(attr.value.expression) && (t2.isIdentifier(attr.value.expression.callee) && (attr.value.expression.callee.name === "cn" || attr.value.expression.callee.name === "clsx") || false)) {
|
|
335
|
+
const call = attr.value.expression;
|
|
336
|
+
if (call.arguments.every((arg) => t2.isStringLiteral(arg))) {
|
|
337
|
+
return {
|
|
338
|
+
read: () => call.arguments.map((arg) => arg.value).join(" "),
|
|
339
|
+
write: (next) => {
|
|
340
|
+
call.arguments = [t2.stringLiteral(next)];
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
96
344
|
}
|
|
97
345
|
return null;
|
|
98
346
|
}
|
|
99
347
|
}
|
|
100
|
-
return
|
|
348
|
+
return {
|
|
349
|
+
read: () => "",
|
|
350
|
+
write: (next) => {
|
|
351
|
+
opening.attributes.push(
|
|
352
|
+
t2.jsxAttribute(t2.jsxIdentifier("className"), t2.stringLiteral(next))
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
101
356
|
}
|
|
102
357
|
function parentSupportsLayoutMoves(parentOpening) {
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
358
|
+
const binding = getClassNameBinding(parentOpening, "literal-only");
|
|
359
|
+
if (!binding) {
|
|
105
360
|
return false;
|
|
106
361
|
}
|
|
362
|
+
const cls = binding.read();
|
|
107
363
|
return /\b(flex|inline-flex|grid|inline-grid)\b/.test(cls) || /\b(flex-|grid-)/.test(cls);
|
|
108
364
|
}
|
|
109
365
|
function collectJsxElementChildIndices(parent) {
|
|
110
366
|
const indices = [];
|
|
111
367
|
parent.children.forEach((child, i) => {
|
|
112
|
-
if (
|
|
368
|
+
if (t2.isJSXElement(child)) {
|
|
113
369
|
indices.push(i);
|
|
114
370
|
}
|
|
115
371
|
});
|
|
@@ -151,34 +407,34 @@ function applyMoveSibling(openingPath, direction) {
|
|
|
151
407
|
parent.children[hostIndex] = parent.children[swapIndex];
|
|
152
408
|
parent.children[swapIndex] = hostNode;
|
|
153
409
|
}
|
|
154
|
-
function applySetHidden(openingPath, hidden) {
|
|
410
|
+
function applySetHidden(openingPath, hidden, classNameMode, activeBreakpoint) {
|
|
155
411
|
if (hidden) {
|
|
156
|
-
applyMergeClassName(openingPath, "hidden");
|
|
412
|
+
applyMergeClassName(openingPath, "hidden", classNameMode, activeBreakpoint);
|
|
157
413
|
return;
|
|
158
414
|
}
|
|
159
415
|
const opening = openingPath.node;
|
|
160
416
|
let clsAttr;
|
|
161
417
|
for (const attr of opening.attributes) {
|
|
162
|
-
if (
|
|
418
|
+
if (t2.isJSXAttribute(attr) && t2.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
163
419
|
clsAttr = attr;
|
|
164
420
|
break;
|
|
165
421
|
}
|
|
166
422
|
}
|
|
167
|
-
if (!clsAttr || !
|
|
423
|
+
if (!clsAttr || !t2.isStringLiteral(clsAttr.value)) {
|
|
168
424
|
return;
|
|
169
425
|
}
|
|
170
426
|
const tokens = clsAttr.value.value.split(/\s+/).filter((tok) => tok && tok !== "hidden");
|
|
171
|
-
clsAttr.value =
|
|
427
|
+
clsAttr.value = t2.stringLiteral(twMerge(tokens.join(" ")));
|
|
172
428
|
}
|
|
173
429
|
function collectNuvioIds(ast) {
|
|
174
430
|
const ids = /* @__PURE__ */ new Set();
|
|
175
|
-
|
|
431
|
+
traverse2(ast, {
|
|
176
432
|
JSXOpeningElement(path2) {
|
|
177
433
|
for (const attr of path2.node.attributes) {
|
|
178
|
-
if (!
|
|
434
|
+
if (!t2.isJSXAttribute(attr) || !t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
179
435
|
continue;
|
|
180
436
|
}
|
|
181
|
-
if (
|
|
437
|
+
if (t2.isStringLiteral(attr.value)) {
|
|
182
438
|
ids.add(attr.value.value);
|
|
183
439
|
}
|
|
184
440
|
}
|
|
@@ -188,17 +444,41 @@ function collectNuvioIds(ast) {
|
|
|
188
444
|
}
|
|
189
445
|
function setNuvioIdOnOpening(opening, id) {
|
|
190
446
|
for (const attr of opening.attributes) {
|
|
191
|
-
if (
|
|
192
|
-
if (
|
|
447
|
+
if (t2.isJSXAttribute(attr) && t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
448
|
+
if (t2.isStringLiteral(attr.value)) {
|
|
193
449
|
attr.value.value = id;
|
|
194
450
|
return;
|
|
195
451
|
}
|
|
196
452
|
}
|
|
197
453
|
}
|
|
198
454
|
opening.attributes.push(
|
|
199
|
-
|
|
455
|
+
t2.jsxAttribute(t2.jsxIdentifier("data-nuvio-id"), t2.stringLiteral(id))
|
|
200
456
|
);
|
|
201
457
|
}
|
|
458
|
+
function remapDescendantNuvioIds(element, taken) {
|
|
459
|
+
const stack = [];
|
|
460
|
+
for (const child of element.children) {
|
|
461
|
+
if (t2.isJSXElement(child)) {
|
|
462
|
+
stack.push(child);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
while (stack.length > 0) {
|
|
466
|
+
const el = stack.pop();
|
|
467
|
+
for (const attr of el.openingElement.attributes) {
|
|
468
|
+
if (!t2.isJSXAttribute(attr) || !t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" }) || !t2.isStringLiteral(attr.value)) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const nextId = uniqueDuplicateId(attr.value.value, taken);
|
|
472
|
+
attr.value.value = nextId;
|
|
473
|
+
taken.add(nextId);
|
|
474
|
+
}
|
|
475
|
+
for (const child of el.children) {
|
|
476
|
+
if (t2.isJSXElement(child)) {
|
|
477
|
+
stack.push(child);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
202
482
|
function uniqueDuplicateId(baseId, taken) {
|
|
203
483
|
const candidate = `${baseId}.copy`;
|
|
204
484
|
if (!taken.has(candidate)) {
|
|
@@ -221,11 +501,13 @@ function applyDuplicateHost(ast, openingPath, hostId) {
|
|
|
221
501
|
}
|
|
222
502
|
const taken = collectNuvioIds(ast);
|
|
223
503
|
const newId = uniqueDuplicateId(hostId, taken);
|
|
224
|
-
const clone =
|
|
225
|
-
if (!
|
|
504
|
+
const clone = t2.cloneNode(hostPath.node, true);
|
|
505
|
+
if (!t2.isJSXElement(clone)) {
|
|
226
506
|
throw new Error("Failed to clone host element");
|
|
227
507
|
}
|
|
228
508
|
setNuvioIdOnOpening(clone.openingElement, newId);
|
|
509
|
+
taken.add(newId);
|
|
510
|
+
remapDescendantNuvioIds(clone, taken);
|
|
229
511
|
const parent = parentPath.node;
|
|
230
512
|
const hostIndex = parent.children.indexOf(hostPath.node);
|
|
231
513
|
if (hostIndex < 0) {
|
|
@@ -234,32 +516,36 @@ function applyDuplicateHost(ast, openingPath, hostId) {
|
|
|
234
516
|
parent.children.splice(hostIndex + 1, 0, clone);
|
|
235
517
|
return newId;
|
|
236
518
|
}
|
|
237
|
-
function applyMergeClassName(openingPath, fragment) {
|
|
519
|
+
function applyMergeClassName(openingPath, fragment, classNameMode, activeBreakpoint) {
|
|
238
520
|
validateTailwindFragment(fragment);
|
|
239
521
|
const opening = openingPath.node;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (!clsAttr) {
|
|
248
|
-
opening.attributes.push(
|
|
249
|
-
t.jsxAttribute(t.jsxIdentifier("className"), t.stringLiteral(fragment.trim()))
|
|
522
|
+
const binding = getClassNameBinding(opening, classNameMode);
|
|
523
|
+
if (!binding) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
classNameMode === "cn-basic" ? "className must be a string literal or simple cn()/clsx() string list in cn-basic mode" : "className must be a string literal for Phase 2 patches"
|
|
250
526
|
);
|
|
251
|
-
return;
|
|
252
527
|
}
|
|
253
|
-
|
|
254
|
-
|
|
528
|
+
const current = binding.read();
|
|
529
|
+
binding.write(mergeAtBreakpoint(current, fragment.trim(), activeBreakpoint));
|
|
530
|
+
}
|
|
531
|
+
function applyRemoveClassName(openingPath, fragment, classNameMode, activeBreakpoint) {
|
|
532
|
+
validateTailwindFragment(fragment);
|
|
533
|
+
const opening = openingPath.node;
|
|
534
|
+
const binding = getClassNameBinding(opening, classNameMode);
|
|
535
|
+
if (!binding) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
classNameMode === "cn-basic" ? "className must be a string literal or simple cn()/clsx() string list in cn-basic mode" : "className must be a string literal for Phase 2 patches"
|
|
538
|
+
);
|
|
255
539
|
}
|
|
256
|
-
const current =
|
|
257
|
-
|
|
540
|
+
const current = binding.read();
|
|
541
|
+
binding.write(removeAtBreakpoint(current, fragment.trim(), activeBreakpoint));
|
|
258
542
|
}
|
|
259
|
-
async function applyPatchToSource(source, filePath, hostId, ops) {
|
|
543
|
+
async function applyPatchToSource(source, filePath, hostId, ops, options) {
|
|
544
|
+
const classNameMode = options?.classNameMode ?? "literal-only";
|
|
545
|
+
const activeBreakpoint = options?.activeBreakpoint ?? "base";
|
|
260
546
|
let ast;
|
|
261
547
|
try {
|
|
262
|
-
ast =
|
|
548
|
+
ast = parse2(source, {
|
|
263
549
|
sourceType: "module",
|
|
264
550
|
plugins: ["typescript", "jsx"],
|
|
265
551
|
sourceFilename: filePath
|
|
@@ -267,22 +553,52 @@ async function applyPatchToSource(source, filePath, hostId, ops) {
|
|
|
267
553
|
} catch (e) {
|
|
268
554
|
return { ok: false, code: "parse_error", message: String(e) };
|
|
269
555
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
556
|
+
const tableDataOps = ops.filter((o) => o.kind === "setTableDataField");
|
|
557
|
+
const hostOps = ops.filter((o) => o.kind !== "setTableDataField");
|
|
558
|
+
const openingPath = hostOps.length > 0 ? findHostOpening(ast, hostId) : tableDataOps.length > 0 ? null : findHostOpening(ast, hostId);
|
|
559
|
+
if (hostOps.length > 0 && !openingPath) {
|
|
560
|
+
return { ok: false, code: "host_not_found", message: `No JSX host with data-nuvio-id="${hostId}"` };
|
|
561
|
+
}
|
|
562
|
+
if (ops.length === 0) {
|
|
563
|
+
return { ok: false, code: "patch_rejected", message: "No patch operations" };
|
|
564
|
+
}
|
|
565
|
+
if (hostOps.length === 0 && tableDataOps.length === 0) {
|
|
272
566
|
return { ok: false, code: "host_not_found", message: `No JSX host with data-nuvio-id="${hostId}"` };
|
|
273
567
|
}
|
|
274
568
|
let duplicateNewId;
|
|
275
569
|
try {
|
|
276
570
|
for (const op of ops) {
|
|
277
|
-
if (op.kind === "
|
|
571
|
+
if (op.kind === "setTableDataField") {
|
|
572
|
+
applySetTableDataField(ast, op.arrayName, op.rowKey, op.field, op.value);
|
|
573
|
+
} else if (op.kind === "setText") {
|
|
574
|
+
if (!openingPath) {
|
|
575
|
+
throw new Error("setText requires a JSX host");
|
|
576
|
+
}
|
|
278
577
|
applySetText(openingPath, op.text);
|
|
279
578
|
} else if (op.kind === "mergeTailwindClassName") {
|
|
280
|
-
|
|
579
|
+
if (!openingPath) {
|
|
580
|
+
throw new Error("mergeTailwindClassName requires a JSX host");
|
|
581
|
+
}
|
|
582
|
+
applyMergeClassName(openingPath, op.classNameFragment, classNameMode, activeBreakpoint);
|
|
583
|
+
} else if (op.kind === "removeTailwindClassName") {
|
|
584
|
+
if (!openingPath) {
|
|
585
|
+
throw new Error("removeTailwindClassName requires a JSX host");
|
|
586
|
+
}
|
|
587
|
+
applyRemoveClassName(openingPath, op.classNameFragment, classNameMode, activeBreakpoint);
|
|
281
588
|
} else if (op.kind === "moveSibling") {
|
|
589
|
+
if (!openingPath) {
|
|
590
|
+
throw new Error("moveSibling requires a JSX host");
|
|
591
|
+
}
|
|
282
592
|
applyMoveSibling(openingPath, op.direction);
|
|
283
593
|
} else if (op.kind === "setHidden") {
|
|
284
|
-
|
|
594
|
+
if (!openingPath) {
|
|
595
|
+
throw new Error("setHidden requires a JSX host");
|
|
596
|
+
}
|
|
597
|
+
applySetHidden(openingPath, op.hidden, classNameMode, activeBreakpoint);
|
|
285
598
|
} else if (op.kind === "duplicateHost") {
|
|
599
|
+
if (!openingPath) {
|
|
600
|
+
throw new Error("duplicateHost requires a JSX host");
|
|
601
|
+
}
|
|
286
602
|
duplicateNewId = applyDuplicateHost(ast, openingPath, hostId);
|
|
287
603
|
}
|
|
288
604
|
}
|
|
@@ -311,12 +627,16 @@ async function applyPatchToSource(source, filePath, hostId, ops) {
|
|
|
311
627
|
return `set text (${op.text.length} char${op.text.length === 1 ? "" : "s"})`;
|
|
312
628
|
case "mergeTailwindClassName":
|
|
313
629
|
return `merge className (${op.classNameFragment.trim()})`;
|
|
630
|
+
case "removeTailwindClassName":
|
|
631
|
+
return `remove className (${op.classNameFragment.trim()})`;
|
|
314
632
|
case "moveSibling":
|
|
315
633
|
return `move sibling ${op.direction}`;
|
|
316
634
|
case "setHidden":
|
|
317
635
|
return op.hidden ? "hide element" : "show element";
|
|
318
636
|
case "duplicateHost":
|
|
319
637
|
return duplicateNewId ? `duplicate host \u2192 ${duplicateNewId}` : "duplicate host";
|
|
638
|
+
case "setTableDataField":
|
|
639
|
+
return `update ${op.arrayName}[${op.rowKey}].${op.field}`;
|
|
320
640
|
}
|
|
321
641
|
});
|
|
322
642
|
const diffSummary = `${base}: ${opBits.join("; ")}`;
|
|
@@ -324,5 +644,8 @@ async function applyPatchToSource(source, filePath, hostId, ops) {
|
|
|
324
644
|
}
|
|
325
645
|
export {
|
|
326
646
|
applyPatchToSource,
|
|
647
|
+
mergeAtBreakpoint,
|
|
648
|
+
parseClassNameByBreakpoint,
|
|
649
|
+
removeAtBreakpoint,
|
|
327
650
|
validateTailwindFragment
|
|
328
651
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuvio/ast-engine",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Nuvio AST patch engine: parse TSX/JSX, apply whitelist Tailwind merges and text edits, format with Prettier.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@babel/types": "^7.26.9",
|
|
42
42
|
"prettier": "^3.5.1",
|
|
43
43
|
"tailwind-merge": "^2.6.0",
|
|
44
|
-
"@nuvio/shared": "0.1
|
|
44
|
+
"@nuvio/shared": "0.5.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/babel__traverse": "^7.20.7",
|