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