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