@nuvio/ast-engine 0.5.5 → 1.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/dist/index.d.ts +21 -2
- package/dist/index.js +437 -77
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { PatchOp } from '@nuvio/shared';
|
|
2
|
+
export { NUVIO_ID_PATTERN, isValidNuvioId, suggestNuvioId } from '@nuvio/shared';
|
|
3
|
+
import { JSXOpeningElement } from '@babel/types';
|
|
4
|
+
|
|
5
|
+
type ClassNameMode = "literal-only" | "cn-basic" | "cn-conditional" | "classnames-static" | "unsupported";
|
|
6
|
+
declare function classifyHostClassNameMode(opening: JSXOpeningElement): ClassNameMode;
|
|
7
|
+
declare function readFlattenedClassName(opening: JSXOpeningElement, mode: ClassNameMode): string | undefined;
|
|
2
8
|
|
|
3
|
-
type ClassNameMode = "literal-only" | "cn-basic";
|
|
4
9
|
type Breakpoint = "base" | "sm" | "md" | "lg" | "xl";
|
|
5
10
|
type BreakpointBuckets = {
|
|
6
11
|
base: string[];
|
|
@@ -31,10 +36,24 @@ declare function applyPatchToSource(source: string, filePath: string, hostId: st
|
|
|
31
36
|
activeBreakpoint?: Breakpoint;
|
|
32
37
|
}): Promise<ApplyPatchToSourceResult>;
|
|
33
38
|
|
|
39
|
+
type InsertNuvioIdResult = {
|
|
40
|
+
ok: true;
|
|
41
|
+
source: string;
|
|
42
|
+
id: string;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
code: string;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Insert `data-nuvio-id` on the JSX opening element at the given 1-based line / 0-based column.
|
|
50
|
+
*/
|
|
51
|
+
declare function insertDataNuvioIdAtLocation(source: string, filePath: string, line: number, column: number, id: string): Promise<InsertNuvioIdResult>;
|
|
52
|
+
|
|
34
53
|
/**
|
|
35
54
|
* Phase 2 — conservative Tailwind v3-style allowlist for `mergeTailwindClassName`.
|
|
36
55
|
* Expand intentionally; unknown tokens are rejected before `tailwind-merge`.
|
|
37
56
|
*/
|
|
38
57
|
declare function validateTailwindFragment(fragment: string): void;
|
|
39
58
|
|
|
40
|
-
export { type ApplyPatchToSourceResult, applyPatchToSource, mergeAtBreakpoint, parseClassNameByBreakpoint, removeAtBreakpoint, validateTailwindFragment };
|
|
59
|
+
export { type ApplyPatchToSourceResult, type ClassNameMode, type InsertNuvioIdResult, applyPatchToSource, classifyHostClassNameMode, insertDataNuvioIdAtLocation, mergeAtBreakpoint, parseClassNameByBreakpoint, readFlattenedClassName, removeAtBreakpoint, validateTailwindFragment };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire as createRequire2 } from "module";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { parse as parse2 } from "@babel/parser";
|
|
5
|
-
import * as
|
|
5
|
+
import * as t4 from "@babel/types";
|
|
6
6
|
import prettier from "prettier";
|
|
7
7
|
import { twMerge } from "tailwind-merge";
|
|
8
8
|
|
|
@@ -26,6 +26,7 @@ var TEXT_ALIGN = /^text-(left|center|right|justify|start|end)$/;
|
|
|
26
26
|
var TRACKING = /^tracking-(tighter|tight|normal|wide|wider|widest)$/;
|
|
27
27
|
var OPACITY = /^opacity-(0|5|10|15|20|25|30|40|50|60|70|75|80|90|95|100)$/;
|
|
28
28
|
var SHADOW = /^shadow$|^shadow-(sm|md|lg|xl|2xl|inner|none)$/;
|
|
29
|
+
var HOVER_COLOR_SCALE = /^hover:(bg|border)-(slate|gray|blue|green|purple|rose)-(50|100|200|300|400|500|600|700|800)$/;
|
|
29
30
|
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)$/;
|
|
30
31
|
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)$/;
|
|
31
32
|
var MAX_W = /^max-w-(none|xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|full|min|max|fit|prose)$/;
|
|
@@ -36,14 +37,14 @@ function normalizeTailwindToken(raw) {
|
|
|
36
37
|
function validateTailwindFragment(fragment) {
|
|
37
38
|
const tokens = fragment.trim().split(/\s+/).filter(Boolean);
|
|
38
39
|
for (const raw of tokens) {
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
40
|
+
const t6 = normalizeTailwindToken(raw);
|
|
41
|
+
if (!t6) {
|
|
41
42
|
continue;
|
|
42
43
|
}
|
|
43
|
-
if (SPACING.test(
|
|
44
|
+
if (SPACING.test(t6) || TEXT_SIZE.test(t6) || FONT_WEIGHT.test(t6) || LEADING.test(t6) || COLOR_SOLID.test(t6) || COLOR_SCALE.test(t6) || BG_COLOR_OPACITY.test(t6) || ROUNDED.test(t6) || LAYOUT.test(t6) || FLEX.test(t6) || GRID_COLS.test(t6) || BORDER_W.test(t6) || BORDER_COLOR.test(t6) || RING.test(t6) || RING_COLOR.test(t6) || TEXT_ALIGN.test(t6) || TRACKING.test(t6) || OPACITY.test(t6) || SHADOW.test(t6) || HOVER_COLOR_SCALE.test(t6) || W_WIDTH.test(t6) || H_HEIGHT.test(t6) || MAX_W.test(t6) || MIN_H.test(t6)) {
|
|
44
45
|
continue;
|
|
45
46
|
}
|
|
46
|
-
throw new Error(`Unknown or disallowed Tailwind utility: ${
|
|
47
|
+
throw new Error(`Unknown or disallowed Tailwind utility: ${t6}`);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -103,6 +104,279 @@ function applySetTableDataField(ast, arrayName, rowKey, field, value) {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// src/classname-binding.ts
|
|
108
|
+
import * as t3 from "@babel/types";
|
|
109
|
+
|
|
110
|
+
// src/classname-mode.ts
|
|
111
|
+
import * as t2 from "@babel/types";
|
|
112
|
+
var CN_CALLEES = /* @__PURE__ */ new Set(["cn", "clsx"]);
|
|
113
|
+
var CLASSNAMES_CALLEES = /* @__PURE__ */ new Set(["classnames", "clsx", "cn"]);
|
|
114
|
+
function isCnCallee(call) {
|
|
115
|
+
return t2.isIdentifier(call.callee) && CN_CALLEES.has(call.callee.name);
|
|
116
|
+
}
|
|
117
|
+
function isClassnamesCallee(call) {
|
|
118
|
+
return t2.isIdentifier(call.callee) && CLASSNAMES_CALLEES.has(call.callee.name);
|
|
119
|
+
}
|
|
120
|
+
function isExpressionArg(arg) {
|
|
121
|
+
return t2.isExpression(arg);
|
|
122
|
+
}
|
|
123
|
+
function isConditionalTailwindArg(arg) {
|
|
124
|
+
return t2.isLogicalExpression(arg) && arg.operator === "&&" && t2.isStringLiteral(arg.right);
|
|
125
|
+
}
|
|
126
|
+
function isStaticObjectArg(arg) {
|
|
127
|
+
if (!t2.isObjectExpression(arg)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return arg.properties.every((prop) => {
|
|
131
|
+
if (!t2.isObjectProperty(prop) || prop.computed) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const keyOk = t2.isIdentifier(prop.key) || t2.isStringLiteral(prop.key);
|
|
135
|
+
if (!keyOk) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return !t2.isStringLiteral(prop.value);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function objectArgClassKeys(obj) {
|
|
142
|
+
const keys = [];
|
|
143
|
+
for (const prop of obj.properties) {
|
|
144
|
+
if (!t2.isObjectProperty(prop)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (t2.isIdentifier(prop.key)) {
|
|
148
|
+
keys.push(prop.key.name);
|
|
149
|
+
} else if (t2.isStringLiteral(prop.key)) {
|
|
150
|
+
keys.push(prop.key.value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return keys;
|
|
154
|
+
}
|
|
155
|
+
function classifyClassNameCall(call) {
|
|
156
|
+
const args = call.arguments.filter(isExpressionArg);
|
|
157
|
+
if (args.length === 0) {
|
|
158
|
+
return "unsupported";
|
|
159
|
+
}
|
|
160
|
+
const hasObject = args.some((a) => isStaticObjectArg(a));
|
|
161
|
+
if (hasObject && isClassnamesCallee(call)) {
|
|
162
|
+
if (args.every((a) => t2.isStringLiteral(a) || isStaticObjectArg(a))) {
|
|
163
|
+
return "classnames-static";
|
|
164
|
+
}
|
|
165
|
+
return "unsupported";
|
|
166
|
+
}
|
|
167
|
+
if (!isCnCallee(call)) {
|
|
168
|
+
return "unsupported";
|
|
169
|
+
}
|
|
170
|
+
if (args.every((a) => t2.isStringLiteral(a))) {
|
|
171
|
+
return "cn-basic";
|
|
172
|
+
}
|
|
173
|
+
if (args.every((a) => t2.isStringLiteral(a) || isConditionalTailwindArg(a))) {
|
|
174
|
+
return "cn-conditional";
|
|
175
|
+
}
|
|
176
|
+
return "unsupported";
|
|
177
|
+
}
|
|
178
|
+
function classifyHostClassNameMode(opening) {
|
|
179
|
+
for (const attr of opening.attributes) {
|
|
180
|
+
if (!t2.isJSXAttribute(attr) || !t2.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (t2.isStringLiteral(attr.value)) {
|
|
184
|
+
return "literal-only";
|
|
185
|
+
}
|
|
186
|
+
if (t2.isJSXExpressionContainer(attr.value) && t2.isCallExpression(attr.value.expression)) {
|
|
187
|
+
return classifyClassNameCall(attr.value.expression);
|
|
188
|
+
}
|
|
189
|
+
return "unsupported";
|
|
190
|
+
}
|
|
191
|
+
return "literal-only";
|
|
192
|
+
}
|
|
193
|
+
function readFlattenedClassName(opening, mode) {
|
|
194
|
+
for (const attr of opening.attributes) {
|
|
195
|
+
if (!t2.isJSXAttribute(attr) || !t2.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (t2.isStringLiteral(attr.value)) {
|
|
199
|
+
return attr.value.value;
|
|
200
|
+
}
|
|
201
|
+
if (!t2.isJSXExpressionContainer(attr.value) || !t2.isCallExpression(attr.value.expression)) {
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
const call = attr.value.expression;
|
|
205
|
+
const parts = [];
|
|
206
|
+
for (const arg of call.arguments.filter(isExpressionArg)) {
|
|
207
|
+
if (t2.isStringLiteral(arg)) {
|
|
208
|
+
parts.push(arg.value);
|
|
209
|
+
} else if (isConditionalTailwindArg(arg) && t2.isStringLiteral(arg.right)) {
|
|
210
|
+
parts.push(arg.right.value);
|
|
211
|
+
} else if (isStaticObjectArg(arg)) {
|
|
212
|
+
parts.push(...objectArgClassKeys(arg));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (parts.length === 0) {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
if (mode === "literal-only" || mode === "cn-basic" || mode === "cn-conditional" || mode === "classnames-static") {
|
|
219
|
+
return parts.join(" ");
|
|
220
|
+
}
|
|
221
|
+
return void 0;
|
|
222
|
+
}
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/classname-binding.ts
|
|
227
|
+
function isExpressionArg2(arg) {
|
|
228
|
+
return t3.isExpression(arg);
|
|
229
|
+
}
|
|
230
|
+
function isStaticObjectArg2(arg) {
|
|
231
|
+
if (!t3.isObjectExpression(arg)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return arg.properties.every((prop) => {
|
|
235
|
+
if (!t3.isObjectProperty(prop) || prop.computed) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return (t3.isIdentifier(prop.key) || t3.isStringLiteral(prop.key)) && !t3.isStringLiteral(prop.value);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function readCallFlattened(call) {
|
|
242
|
+
const parts = [];
|
|
243
|
+
for (const arg of call.arguments.filter(isExpressionArg2)) {
|
|
244
|
+
if (t3.isStringLiteral(arg)) {
|
|
245
|
+
parts.push(arg.value);
|
|
246
|
+
} else if (t3.isLogicalExpression(arg) && arg.operator === "&&" && t3.isStringLiteral(arg.right)) {
|
|
247
|
+
parts.push(arg.right.value);
|
|
248
|
+
} else if (isStaticObjectArg2(arg)) {
|
|
249
|
+
for (const prop of arg.properties) {
|
|
250
|
+
if (!t3.isObjectProperty(prop)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (t3.isIdentifier(prop.key)) {
|
|
254
|
+
parts.push(prop.key.name);
|
|
255
|
+
} else if (t3.isStringLiteral(prop.key)) {
|
|
256
|
+
parts.push(prop.key.value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return parts.join(" ");
|
|
262
|
+
}
|
|
263
|
+
function writeCnBasic(call, next) {
|
|
264
|
+
call.arguments = [t3.stringLiteral(next)];
|
|
265
|
+
}
|
|
266
|
+
function writeCnConditional(call, next) {
|
|
267
|
+
const newTokenList = next.trim().split(/\s+/).filter(Boolean);
|
|
268
|
+
const conditionalArgs = [];
|
|
269
|
+
const used = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const arg of call.arguments.filter(isExpressionArg2)) {
|
|
271
|
+
if (t3.isLogicalExpression(arg) && arg.operator === "&&" && t3.isStringLiteral(arg.right)) {
|
|
272
|
+
const oldTokens = arg.right.value.split(/\s+/).filter(Boolean);
|
|
273
|
+
const kept = newTokenList.filter((tok) => oldTokens.includes(tok));
|
|
274
|
+
for (const tok of kept) {
|
|
275
|
+
used.add(tok);
|
|
276
|
+
}
|
|
277
|
+
arg.right = t3.stringLiteral(kept.join(" "));
|
|
278
|
+
conditionalArgs.push(arg);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const baseTokens = newTokenList.filter((tok) => !used.has(tok));
|
|
282
|
+
const baseStr = baseTokens.join(" ");
|
|
283
|
+
const literalArgs = call.arguments.filter(
|
|
284
|
+
(a) => isExpressionArg2(a) && t3.isStringLiteral(a)
|
|
285
|
+
);
|
|
286
|
+
if (literalArgs.length > 0) {
|
|
287
|
+
literalArgs[0].value = baseStr;
|
|
288
|
+
call.arguments = [literalArgs[0], ...conditionalArgs];
|
|
289
|
+
} else {
|
|
290
|
+
call.arguments = [t3.stringLiteral(baseStr), ...conditionalArgs];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function writeClassnamesStatic(call, next) {
|
|
294
|
+
const newTokenList = next.trim().split(/\s+/).filter(Boolean);
|
|
295
|
+
const objectArgs = call.arguments.filter(
|
|
296
|
+
(a) => isExpressionArg2(a) && isStaticObjectArg2(a)
|
|
297
|
+
);
|
|
298
|
+
const objectKeys = /* @__PURE__ */ new Set();
|
|
299
|
+
for (const obj of objectArgs) {
|
|
300
|
+
if (!isStaticObjectArg2(obj)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
for (const prop of obj.properties) {
|
|
304
|
+
if (!t3.isObjectProperty(prop)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (t3.isIdentifier(prop.key)) {
|
|
308
|
+
objectKeys.add(prop.key.name);
|
|
309
|
+
} else if (t3.isStringLiteral(prop.key)) {
|
|
310
|
+
objectKeys.add(prop.key.value);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const baseTokens = newTokenList.filter((tok) => !objectKeys.has(tok));
|
|
315
|
+
const baseStr = baseTokens.join(" ");
|
|
316
|
+
const nonObjectArgs = call.arguments.filter(
|
|
317
|
+
(a) => isExpressionArg2(a) && !isStaticObjectArg2(a) && t3.isStringLiteral(a)
|
|
318
|
+
);
|
|
319
|
+
if (nonObjectArgs.length > 0) {
|
|
320
|
+
nonObjectArgs[0].value = baseStr;
|
|
321
|
+
call.arguments = [nonObjectArgs[0], ...objectArgs];
|
|
322
|
+
} else if (baseStr.length > 0) {
|
|
323
|
+
call.arguments = [t3.stringLiteral(baseStr), ...objectArgs];
|
|
324
|
+
} else {
|
|
325
|
+
call.arguments = [...objectArgs];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function getClassNameBinding(opening, classNameMode) {
|
|
329
|
+
for (const attr of opening.attributes) {
|
|
330
|
+
if (!t3.isJSXAttribute(attr) || !t3.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (t3.isStringLiteral(attr.value)) {
|
|
334
|
+
const literal = attr.value;
|
|
335
|
+
return {
|
|
336
|
+
read: () => literal.value,
|
|
337
|
+
write: (next) => {
|
|
338
|
+
attr.value = t3.stringLiteral(next);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (!t3.isJSXExpressionContainer(attr.value) || !t3.isCallExpression(attr.value.expression)) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const call = attr.value.expression;
|
|
346
|
+
const detected = classifyClassNameCall(call);
|
|
347
|
+
if (classNameMode === "cn-basic" && detected === "cn-basic") {
|
|
348
|
+
return {
|
|
349
|
+
read: () => readCallFlattened(call),
|
|
350
|
+
write: (next) => writeCnBasic(call, next)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (classNameMode === "cn-conditional" && detected === "cn-conditional") {
|
|
354
|
+
return {
|
|
355
|
+
read: () => readCallFlattened(call),
|
|
356
|
+
write: (next) => writeCnConditional(call, next)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (classNameMode === "classnames-static" && detected === "classnames-static") {
|
|
360
|
+
return {
|
|
361
|
+
read: () => readCallFlattened(call),
|
|
362
|
+
write: (next) => writeClassnamesStatic(call, next)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (classNameMode === "cn-basic" && detected === "cn-conditional") {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
read: () => "",
|
|
372
|
+
write: (next) => {
|
|
373
|
+
opening.attributes.push(
|
|
374
|
+
t3.jsxAttribute(t3.jsxIdentifier("className"), t3.stringLiteral(next))
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
106
380
|
// src/apply-patch.ts
|
|
107
381
|
var require3 = createRequire2(import.meta.url);
|
|
108
382
|
var traverse2 = require3("@babel/traverse").default;
|
|
@@ -111,18 +385,18 @@ function extractRowKeysFromSource(ast) {
|
|
|
111
385
|
const keys = [];
|
|
112
386
|
traverse2(ast, {
|
|
113
387
|
VariableDeclarator(path2) {
|
|
114
|
-
if (!
|
|
388
|
+
if (!t4.isIdentifier(path2.node.id) || path2.node.id.name !== "tableData") {
|
|
115
389
|
return;
|
|
116
390
|
}
|
|
117
|
-
if (!
|
|
391
|
+
if (!t4.isArrayExpression(path2.node.init)) {
|
|
118
392
|
return;
|
|
119
393
|
}
|
|
120
394
|
for (const el of path2.node.init.elements) {
|
|
121
|
-
if (!el || !
|
|
395
|
+
if (!el || !t4.isObjectExpression(el)) {
|
|
122
396
|
continue;
|
|
123
397
|
}
|
|
124
398
|
for (const prop of el.properties) {
|
|
125
|
-
if (
|
|
399
|
+
if (t4.isObjectProperty(prop) && t4.isIdentifier(prop.key, { name: "id" }) && t4.isNumericLiteral(prop.value)) {
|
|
126
400
|
keys.push(String(prop.value.value));
|
|
127
401
|
}
|
|
128
402
|
}
|
|
@@ -132,15 +406,15 @@ function extractRowKeysFromSource(ast) {
|
|
|
132
406
|
return keys;
|
|
133
407
|
}
|
|
134
408
|
function templateLiteralMatchesHostId(attr, hostId, rowKeys) {
|
|
135
|
-
if (!
|
|
409
|
+
if (!t4.isJSXExpressionContainer(attr.value)) {
|
|
136
410
|
return false;
|
|
137
411
|
}
|
|
138
412
|
const expr = attr.value.expression;
|
|
139
|
-
if (!
|
|
413
|
+
if (!t4.isTemplateLiteral(expr) || expr.expressions.length !== 1) {
|
|
140
414
|
return false;
|
|
141
415
|
}
|
|
142
416
|
const ex = expr.expressions[0];
|
|
143
|
-
const mapOk =
|
|
417
|
+
const mapOk = t4.isIdentifier(ex, { name: "product" }) || t4.isIdentifier(ex, { name: "item" }) || t4.isMemberExpression(ex) && !ex.computed && t4.isIdentifier(ex.object, { name: "product" }) && t4.isIdentifier(ex.property, { name: "id" });
|
|
144
418
|
if (!mapOk) {
|
|
145
419
|
return false;
|
|
146
420
|
}
|
|
@@ -154,13 +428,13 @@ function findHostOpening(ast, hostId) {
|
|
|
154
428
|
traverse2(ast, {
|
|
155
429
|
JSXOpeningElement(path2) {
|
|
156
430
|
for (const attr of path2.node.attributes) {
|
|
157
|
-
if (!
|
|
431
|
+
if (!t4.isJSXAttribute(attr)) {
|
|
158
432
|
continue;
|
|
159
433
|
}
|
|
160
|
-
if (!
|
|
434
|
+
if (!t4.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
161
435
|
continue;
|
|
162
436
|
}
|
|
163
|
-
if (
|
|
437
|
+
if (t4.isStringLiteral(attr.value) && attr.value.value === hostId) {
|
|
164
438
|
found = path2;
|
|
165
439
|
path2.stop();
|
|
166
440
|
return;
|
|
@@ -183,18 +457,18 @@ function applySetText(openingPath, text) {
|
|
|
183
457
|
const jsx = parent;
|
|
184
458
|
const { children } = jsx.node;
|
|
185
459
|
if (children.length === 0) {
|
|
186
|
-
jsx.node.children = [
|
|
460
|
+
jsx.node.children = [t4.jsxText(text)];
|
|
187
461
|
return;
|
|
188
462
|
}
|
|
189
|
-
if (children.length === 1 &&
|
|
463
|
+
if (children.length === 1 && t4.isJSXText(children[0])) {
|
|
190
464
|
children[0].value = text;
|
|
191
465
|
return;
|
|
192
466
|
}
|
|
193
|
-
if (children.length === 1 &&
|
|
467
|
+
if (children.length === 1 && t4.isJSXExpressionContainer(children[0]) && t4.isStringLiteral(children[0].expression)) {
|
|
194
468
|
children[0].expression.value = text;
|
|
195
469
|
return;
|
|
196
470
|
}
|
|
197
|
-
jsx.node.children = [
|
|
471
|
+
jsx.node.children = [t4.jsxText(text)];
|
|
198
472
|
}
|
|
199
473
|
function emptyBreakpointBuckets() {
|
|
200
474
|
return {
|
|
@@ -246,7 +520,7 @@ function serializeClassNameFromBuckets(buckets) {
|
|
|
246
520
|
const merged = buckets[bp].join(" ").trim();
|
|
247
521
|
if (merged) {
|
|
248
522
|
out.push(
|
|
249
|
-
...merged.split(/\s+/).filter(Boolean).map((
|
|
523
|
+
...merged.split(/\s+/).filter(Boolean).map((t6) => prefixTokenForBreakpoint(t6, bp))
|
|
250
524
|
);
|
|
251
525
|
}
|
|
252
526
|
}
|
|
@@ -305,13 +579,13 @@ function removeAtBreakpoint(className, fragment, activeBreakpoint) {
|
|
|
305
579
|
}
|
|
306
580
|
const idx = BREAKPOINT_ORDER.indexOf(activeBreakpoint);
|
|
307
581
|
for (const tok of toRemove) {
|
|
308
|
-
buckets[activeBreakpoint] = buckets[activeBreakpoint].filter((
|
|
582
|
+
buckets[activeBreakpoint] = buckets[activeBreakpoint].filter((t6) => t6 !== tok);
|
|
309
583
|
const stillPresent = flattenTokensAtBreakpointFromBuckets(buckets, activeBreakpoint).includes(tok);
|
|
310
584
|
if (stillPresent) {
|
|
311
585
|
for (let i = idx; i >= 0; i--) {
|
|
312
586
|
const bp = BREAKPOINT_ORDER[i];
|
|
313
587
|
if (buckets[bp].includes(tok)) {
|
|
314
|
-
buckets[bp] = buckets[bp].filter((
|
|
588
|
+
buckets[bp] = buckets[bp].filter((t6) => t6 !== tok);
|
|
315
589
|
break;
|
|
316
590
|
}
|
|
317
591
|
}
|
|
@@ -319,41 +593,6 @@ function removeAtBreakpoint(className, fragment, activeBreakpoint) {
|
|
|
319
593
|
}
|
|
320
594
|
return serializeClassNameFromBuckets(buckets);
|
|
321
595
|
}
|
|
322
|
-
function getClassNameBinding(opening, classNameMode) {
|
|
323
|
-
for (const attr of opening.attributes) {
|
|
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
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
read: () => "",
|
|
350
|
-
write: (next) => {
|
|
351
|
-
opening.attributes.push(
|
|
352
|
-
t2.jsxAttribute(t2.jsxIdentifier("className"), t2.stringLiteral(next))
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
596
|
function parentSupportsLayoutMoves(parentOpening) {
|
|
358
597
|
const binding = getClassNameBinding(parentOpening, "literal-only");
|
|
359
598
|
if (!binding) {
|
|
@@ -365,7 +604,7 @@ function parentSupportsLayoutMoves(parentOpening) {
|
|
|
365
604
|
function collectJsxElementChildIndices(parent) {
|
|
366
605
|
const indices = [];
|
|
367
606
|
parent.children.forEach((child, i) => {
|
|
368
|
-
if (
|
|
607
|
+
if (t4.isJSXElement(child)) {
|
|
369
608
|
indices.push(i);
|
|
370
609
|
}
|
|
371
610
|
});
|
|
@@ -415,26 +654,26 @@ function applySetHidden(openingPath, hidden, classNameMode, activeBreakpoint) {
|
|
|
415
654
|
const opening = openingPath.node;
|
|
416
655
|
let clsAttr;
|
|
417
656
|
for (const attr of opening.attributes) {
|
|
418
|
-
if (
|
|
657
|
+
if (t4.isJSXAttribute(attr) && t4.isJSXIdentifier(attr.name, { name: "className" })) {
|
|
419
658
|
clsAttr = attr;
|
|
420
659
|
break;
|
|
421
660
|
}
|
|
422
661
|
}
|
|
423
|
-
if (!clsAttr || !
|
|
662
|
+
if (!clsAttr || !t4.isStringLiteral(clsAttr.value)) {
|
|
424
663
|
return;
|
|
425
664
|
}
|
|
426
665
|
const tokens = clsAttr.value.value.split(/\s+/).filter((tok) => tok && tok !== "hidden");
|
|
427
|
-
clsAttr.value =
|
|
666
|
+
clsAttr.value = t4.stringLiteral(twMerge(tokens.join(" ")));
|
|
428
667
|
}
|
|
429
668
|
function collectNuvioIds(ast) {
|
|
430
669
|
const ids = /* @__PURE__ */ new Set();
|
|
431
670
|
traverse2(ast, {
|
|
432
671
|
JSXOpeningElement(path2) {
|
|
433
672
|
for (const attr of path2.node.attributes) {
|
|
434
|
-
if (!
|
|
673
|
+
if (!t4.isJSXAttribute(attr) || !t4.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
435
674
|
continue;
|
|
436
675
|
}
|
|
437
|
-
if (
|
|
676
|
+
if (t4.isStringLiteral(attr.value)) {
|
|
438
677
|
ids.add(attr.value.value);
|
|
439
678
|
}
|
|
440
679
|
}
|
|
@@ -444,28 +683,28 @@ function collectNuvioIds(ast) {
|
|
|
444
683
|
}
|
|
445
684
|
function setNuvioIdOnOpening(opening, id) {
|
|
446
685
|
for (const attr of opening.attributes) {
|
|
447
|
-
if (
|
|
448
|
-
if (
|
|
686
|
+
if (t4.isJSXAttribute(attr) && t4.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
|
|
687
|
+
if (t4.isStringLiteral(attr.value)) {
|
|
449
688
|
attr.value.value = id;
|
|
450
689
|
return;
|
|
451
690
|
}
|
|
452
691
|
}
|
|
453
692
|
}
|
|
454
693
|
opening.attributes.push(
|
|
455
|
-
|
|
694
|
+
t4.jsxAttribute(t4.jsxIdentifier("data-nuvio-id"), t4.stringLiteral(id))
|
|
456
695
|
);
|
|
457
696
|
}
|
|
458
697
|
function remapDescendantNuvioIds(element, taken) {
|
|
459
698
|
const stack = [];
|
|
460
699
|
for (const child of element.children) {
|
|
461
|
-
if (
|
|
700
|
+
if (t4.isJSXElement(child)) {
|
|
462
701
|
stack.push(child);
|
|
463
702
|
}
|
|
464
703
|
}
|
|
465
704
|
while (stack.length > 0) {
|
|
466
705
|
const el = stack.pop();
|
|
467
706
|
for (const attr of el.openingElement.attributes) {
|
|
468
|
-
if (!
|
|
707
|
+
if (!t4.isJSXAttribute(attr) || !t4.isJSXIdentifier(attr.name, { name: "data-nuvio-id" }) || !t4.isStringLiteral(attr.value)) {
|
|
469
708
|
continue;
|
|
470
709
|
}
|
|
471
710
|
const nextId = uniqueDuplicateId(attr.value.value, taken);
|
|
@@ -473,7 +712,7 @@ function remapDescendantNuvioIds(element, taken) {
|
|
|
473
712
|
taken.add(nextId);
|
|
474
713
|
}
|
|
475
714
|
for (const child of el.children) {
|
|
476
|
-
if (
|
|
715
|
+
if (t4.isJSXElement(child)) {
|
|
477
716
|
stack.push(child);
|
|
478
717
|
}
|
|
479
718
|
}
|
|
@@ -501,8 +740,8 @@ function applyDuplicateHost(ast, openingPath, hostId) {
|
|
|
501
740
|
}
|
|
502
741
|
const taken = collectNuvioIds(ast);
|
|
503
742
|
const newId = uniqueDuplicateId(hostId, taken);
|
|
504
|
-
const clone =
|
|
505
|
-
if (!
|
|
743
|
+
const clone = t4.cloneNode(hostPath.node, true);
|
|
744
|
+
if (!t4.isJSXElement(clone)) {
|
|
506
745
|
throw new Error("Failed to clone host element");
|
|
507
746
|
}
|
|
508
747
|
setNuvioIdOnOpening(clone.openingElement, newId);
|
|
@@ -521,21 +760,29 @@ function applyMergeClassName(openingPath, fragment, classNameMode, activeBreakpo
|
|
|
521
760
|
const opening = openingPath.node;
|
|
522
761
|
const binding = getClassNameBinding(opening, classNameMode);
|
|
523
762
|
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"
|
|
526
|
-
);
|
|
763
|
+
throw new Error(classNamePatchErrorMessage(classNameMode));
|
|
527
764
|
}
|
|
528
765
|
const current = binding.read();
|
|
529
766
|
binding.write(mergeAtBreakpoint(current, fragment.trim(), activeBreakpoint));
|
|
530
767
|
}
|
|
768
|
+
function classNamePatchErrorMessage(mode) {
|
|
769
|
+
switch (mode) {
|
|
770
|
+
case "cn-conditional":
|
|
771
|
+
return 'className must be cn()/clsx() with string literals and condition && "token" branches';
|
|
772
|
+
case "classnames-static":
|
|
773
|
+
return "className must be classnames()/clsx() with string literals and a static object map";
|
|
774
|
+
case "cn-basic":
|
|
775
|
+
return "className must be a string literal or simple cn()/clsx() string list";
|
|
776
|
+
default:
|
|
777
|
+
return "className must be a string literal for Phase 2 patches";
|
|
778
|
+
}
|
|
779
|
+
}
|
|
531
780
|
function applyRemoveClassName(openingPath, fragment, classNameMode, activeBreakpoint) {
|
|
532
781
|
validateTailwindFragment(fragment);
|
|
533
782
|
const opening = openingPath.node;
|
|
534
783
|
const binding = getClassNameBinding(opening, classNameMode);
|
|
535
784
|
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
|
-
);
|
|
785
|
+
throw new Error(classNamePatchErrorMessage(classNameMode));
|
|
539
786
|
}
|
|
540
787
|
const current = binding.read();
|
|
541
788
|
binding.write(removeAtBreakpoint(current, fragment.trim(), activeBreakpoint));
|
|
@@ -642,10 +889,123 @@ async function applyPatchToSource(source, filePath, hostId, ops, options) {
|
|
|
642
889
|
const diffSummary = `${base}: ${opBits.join("; ")}`;
|
|
643
890
|
return { ok: true, source: formatted, diffSummary };
|
|
644
891
|
}
|
|
892
|
+
|
|
893
|
+
// src/insert-nuvio-id.ts
|
|
894
|
+
import { createRequire as createRequire3 } from "module";
|
|
895
|
+
import { parse as parse3 } from "@babel/parser";
|
|
896
|
+
import * as t5 from "@babel/types";
|
|
897
|
+
import prettier2 from "prettier";
|
|
898
|
+
|
|
899
|
+
// src/nuvio-id.ts
|
|
900
|
+
import {
|
|
901
|
+
isValidNuvioId,
|
|
902
|
+
NUVIO_ID_PATTERN,
|
|
903
|
+
suggestNuvioId
|
|
904
|
+
} from "@nuvio/shared";
|
|
905
|
+
|
|
906
|
+
// src/insert-nuvio-id.ts
|
|
907
|
+
var require4 = createRequire3(import.meta.url);
|
|
908
|
+
var traverse3 = require4("@babel/traverse").default;
|
|
909
|
+
var generate2 = require4("@babel/generator").default;
|
|
910
|
+
function hasNuvioIdAttr(opening) {
|
|
911
|
+
return opening.attributes.some(
|
|
912
|
+
(attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
function findOpeningAtLocation(ast, line, column) {
|
|
916
|
+
const onLine = [];
|
|
917
|
+
traverse3(ast, {
|
|
918
|
+
JSXOpeningElement(path2) {
|
|
919
|
+
const loc = path2.node.loc?.start;
|
|
920
|
+
if (!loc || loc.line !== line) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
onLine.push({ path: path2, column: loc.column });
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
if (onLine.length === 0) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
const exact = onLine.find((entry) => entry.column === column);
|
|
930
|
+
if (exact) {
|
|
931
|
+
return exact.path;
|
|
932
|
+
}
|
|
933
|
+
if (onLine.length === 1) {
|
|
934
|
+
return onLine[0].path;
|
|
935
|
+
}
|
|
936
|
+
onLine.sort(
|
|
937
|
+
(a, b) => Math.abs(a.column - column) - Math.abs(b.column - column)
|
|
938
|
+
);
|
|
939
|
+
return onLine[0].path;
|
|
940
|
+
}
|
|
941
|
+
async function formatSource(source, filePath) {
|
|
942
|
+
try {
|
|
943
|
+
return await prettier2.format(source, {
|
|
944
|
+
filepath: filePath,
|
|
945
|
+
parser: "typescript"
|
|
946
|
+
});
|
|
947
|
+
} catch {
|
|
948
|
+
return source;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
async function insertDataNuvioIdAtLocation(source, filePath, line, column, id) {
|
|
952
|
+
if (!isValidNuvioId(id)) {
|
|
953
|
+
return {
|
|
954
|
+
ok: false,
|
|
955
|
+
code: "invalid_id",
|
|
956
|
+
message: "Id must be segmented lowercase (e.g. page.title)"
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
let ast;
|
|
960
|
+
try {
|
|
961
|
+
ast = parse3(source, {
|
|
962
|
+
sourceType: "module",
|
|
963
|
+
plugins: ["typescript", "jsx"],
|
|
964
|
+
sourceFilename: filePath
|
|
965
|
+
});
|
|
966
|
+
} catch (e) {
|
|
967
|
+
return { ok: false, code: "parse_error", message: String(e) };
|
|
968
|
+
}
|
|
969
|
+
const openingPath = findOpeningAtLocation(ast, line, column);
|
|
970
|
+
if (!openingPath) {
|
|
971
|
+
return {
|
|
972
|
+
ok: false,
|
|
973
|
+
code: "node_not_found",
|
|
974
|
+
message: "No JSX element at that source location"
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const opening = openingPath.node;
|
|
978
|
+
if (hasNuvioIdAttr(opening)) {
|
|
979
|
+
return {
|
|
980
|
+
ok: false,
|
|
981
|
+
code: "already_tagged",
|
|
982
|
+
message: "Element already has data-nuvio-id"
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (opening.name.type !== "JSXIdentifier") {
|
|
986
|
+
return {
|
|
987
|
+
ok: false,
|
|
988
|
+
code: "unsupported_tag",
|
|
989
|
+
message: "Cannot tag dynamic JSX member expressions"
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
opening.attributes.push(
|
|
993
|
+
t5.jsxAttribute(t5.jsxIdentifier("data-nuvio-id"), t5.stringLiteral(id))
|
|
994
|
+
);
|
|
995
|
+
const generated = generate2(ast, { retainLines: true }).code;
|
|
996
|
+
const formatted = await formatSource(generated, filePath);
|
|
997
|
+
return { ok: true, source: formatted, id };
|
|
998
|
+
}
|
|
645
999
|
export {
|
|
1000
|
+
NUVIO_ID_PATTERN,
|
|
646
1001
|
applyPatchToSource,
|
|
1002
|
+
classifyHostClassNameMode,
|
|
1003
|
+
insertDataNuvioIdAtLocation,
|
|
1004
|
+
isValidNuvioId,
|
|
647
1005
|
mergeAtBreakpoint,
|
|
648
1006
|
parseClassNameByBreakpoint,
|
|
1007
|
+
readFlattenedClassName,
|
|
649
1008
|
removeAtBreakpoint,
|
|
1009
|
+
suggestNuvioId,
|
|
650
1010
|
validateTailwindFragment
|
|
651
1011
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuvio/ast-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
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": "
|
|
44
|
+
"@nuvio/shared": "1.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/babel__traverse": "^7.20.7",
|