@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 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 t2 from "@babel/types";
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 t3 = normalizeTailwindToken(raw);
40
- if (!t3) {
39
+ const t6 = normalizeTailwindToken(raw);
40
+ if (!t6) {
41
41
  continue;
42
42
  }
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)) {
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: ${t3}`);
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 (!t2.isIdentifier(path2.node.id) || path2.node.id.name !== "tableData") {
387
+ if (!t4.isIdentifier(path2.node.id) || path2.node.id.name !== "tableData") {
115
388
  return;
116
389
  }
117
- if (!t2.isArrayExpression(path2.node.init)) {
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 || !t2.isObjectExpression(el)) {
394
+ if (!el || !t4.isObjectExpression(el)) {
122
395
  continue;
123
396
  }
124
397
  for (const prop of el.properties) {
125
- if (t2.isObjectProperty(prop) && t2.isIdentifier(prop.key, { name: "id" }) && t2.isNumericLiteral(prop.value)) {
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 (!t2.isJSXExpressionContainer(attr.value)) {
408
+ if (!t4.isJSXExpressionContainer(attr.value)) {
136
409
  return false;
137
410
  }
138
411
  const expr = attr.value.expression;
139
- if (!t2.isTemplateLiteral(expr) || expr.expressions.length !== 1) {
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 = 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" });
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 (!t2.isJSXAttribute(attr)) {
430
+ if (!t4.isJSXAttribute(attr)) {
158
431
  continue;
159
432
  }
160
- if (!t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
433
+ if (!t4.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
161
434
  continue;
162
435
  }
163
- if (t2.isStringLiteral(attr.value) && attr.value.value === hostId) {
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 = [t2.jsxText(text)];
459
+ jsx.node.children = [t4.jsxText(text)];
187
460
  return;
188
461
  }
189
- if (children.length === 1 && t2.isJSXText(children[0])) {
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 && t2.isJSXExpressionContainer(children[0]) && t2.isStringLiteral(children[0].expression)) {
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 = [t2.jsxText(text)];
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((t3) => prefixTokenForBreakpoint(t3, bp))
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((t3) => t3 !== tok);
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((t3) => t3 !== tok);
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 (t2.isJSXElement(child)) {
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 (t2.isJSXAttribute(attr) && t2.isJSXIdentifier(attr.name, { name: "className" })) {
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 || !t2.isStringLiteral(clsAttr.value)) {
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 = t2.stringLiteral(twMerge(tokens.join(" ")));
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 (!t2.isJSXAttribute(attr) || !t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
672
+ if (!t4.isJSXAttribute(attr) || !t4.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
435
673
  continue;
436
674
  }
437
- if (t2.isStringLiteral(attr.value)) {
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 (t2.isJSXAttribute(attr) && t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
448
- if (t2.isStringLiteral(attr.value)) {
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
- t2.jsxAttribute(t2.jsxIdentifier("data-nuvio-id"), t2.stringLiteral(id))
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 (t2.isJSXElement(child)) {
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 (!t2.isJSXAttribute(attr) || !t2.isJSXIdentifier(attr.name, { name: "data-nuvio-id" }) || !t2.isStringLiteral(attr.value)) {
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 (t2.isJSXElement(child)) {
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 = t2.cloneNode(hostPath.node, true);
505
- if (!t2.isJSXElement(clone)) {
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.5.4",
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.5.4"
44
+ "@nuvio/shared": "1.0.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/babel__traverse": "^7.20.7",