@archie/devtools 0.0.41 → 0.0.42

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.
@@ -78,6 +78,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
78
78
  }
79
79
  return "";
80
80
  }
81
+ function isIntrinsicTagName(name) {
82
+ return t.isJSXIdentifier(name) && /^[a-z]/.test(name.name);
83
+ }
81
84
  function isHostLikeMemberExpressionName(name) {
82
85
  if (!t.isJSXMemberExpression(name)) return false;
83
86
  const propertyName = name.property.name;
@@ -168,6 +171,18 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
168
171
  }
169
172
  return hasAny ? t.objectExpression(props) : null;
170
173
  }
174
+ function describeChildrenText(opening) {
175
+ const elementPath = opening.parentPath;
176
+ if (!elementPath || !elementPath.isJSXElement()) return null;
177
+ const element = elementPath.node;
178
+ if (!element.closingElement) return null;
179
+ const children = element.children;
180
+ if (children.length === 0) return "";
181
+ if (children.length !== 1) return "__dynamic__";
182
+ const [only] = children;
183
+ if (!t.isJSXText(only)) return "__dynamic__";
184
+ return only.value.trim();
185
+ }
171
186
  function isMetaStripSpreadExpression(arg) {
172
187
  if (!t.isCallExpression(arg)) return false;
173
188
  if (!t.isArrowFunctionExpression(arg.callee)) return false;
@@ -275,6 +290,14 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
275
290
  }
276
291
  return map;
277
292
  }
293
+ function restParamName(pattern) {
294
+ for (const prop of pattern.properties) {
295
+ if (!t.isRestElement(prop)) continue;
296
+ if (t.isIdentifier(prop.argument)) return prop.argument.name;
297
+ return null;
298
+ }
299
+ return null;
300
+ }
278
301
  function resolvePropContext(path2) {
279
302
  let fn = path2.getFunctionParent();
280
303
  while (fn) {
@@ -283,7 +306,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
283
306
  return {
284
307
  componentScope: fn.scope,
285
308
  propMap: buildPropMap(first),
286
- componentName: getFunctionName(fn)
309
+ componentName: getFunctionName(fn),
310
+ restParam: restParamName(first),
311
+ componentPath: fn
287
312
  };
288
313
  }
289
314
  fn = fn.getFunctionParent();
@@ -403,12 +428,62 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
403
428
  const props = collectPropIdentifiers(expr, scope, ctx);
404
429
  return props.length > 0 ? { kind: "formatted", prop: props[0] } : null;
405
430
  }
431
+ function unwrapSanitizedSpread(arg) {
432
+ if (!t.isCallExpression(arg)) return arg;
433
+ if (!isMetaStripSpreadExpression(arg)) return arg;
434
+ if (arg.arguments.length !== 1) return arg;
435
+ const [payload] = arg.arguments;
436
+ if (!t.isObjectExpression(payload) || payload.properties.length !== 1) return arg;
437
+ const [only] = payload.properties;
438
+ if (!t.isSpreadElement(only)) return arg;
439
+ return only.argument;
440
+ }
441
+ function forwardsOwnerRestChildren(opening, ctx) {
442
+ if (ctx.restParam === null) return false;
443
+ const elementPath = opening.parentPath;
444
+ if (!elementPath || !elementPath.isJSXElement()) return false;
445
+ if (meaningfulChildren(t, elementPath.node.children).length > 0) return false;
446
+ let spreadsRest = 0;
447
+ for (const attr of opening.node.attributes) {
448
+ if (t.isJSXAttribute(attr)) {
449
+ if (t.isJSXIdentifier(attr.name, { name: "children" })) return false;
450
+ continue;
451
+ }
452
+ if (!t.isJSXSpreadAttribute(attr)) continue;
453
+ const payload = unwrapSanitizedSpread(attr.argument);
454
+ if (!t.isIdentifier(payload, { name: ctx.restParam })) return false;
455
+ spreadsRest += 1;
456
+ }
457
+ if (spreadsRest !== 1) return false;
458
+ const binding = opening.scope.getBinding(ctx.restParam);
459
+ if (!binding) return false;
460
+ if (binding.scope !== ctx.componentScope) return false;
461
+ return binding.constant;
462
+ }
463
+ const restForwarderCounts = /* @__PURE__ */ new WeakMap();
464
+ function countRestForwarders(ctx) {
465
+ const cached = restForwarderCounts.get(ctx.componentPath.node);
466
+ if (cached !== void 0) return cached;
467
+ let count = 0;
468
+ ctx.componentPath.traverse({
469
+ JSXOpeningElement(inner) {
470
+ if (forwardsOwnerRestChildren(inner, ctx)) count += 1;
471
+ }
472
+ });
473
+ restForwarderCounts.set(ctx.componentPath.node, count);
474
+ return count;
475
+ }
406
476
  function collectPropBindings(path2, attrs) {
407
477
  const ctx = resolvePropContext(path2);
408
- if (!ctx || ctx.propMap.size === 0) return { bindings: [], owner: null };
478
+ if (!ctx || ctx.propMap.size === 0 && ctx.restParam === null) {
479
+ return { bindings: [], owner: null };
480
+ }
409
481
  if (ctx.componentName === null) return { bindings: [], owner: null };
410
482
  const scope = path2.scope;
411
483
  const bindings = [];
484
+ if (forwardsOwnerRestChildren(path2, ctx) && countRestForwarders(ctx) === 1) {
485
+ bindings.push({ target: "children", prop: "children", kind: "direct" });
486
+ }
412
487
  const elementPath = path2.parentPath;
413
488
  if (elementPath && elementPath.isJSXElement()) {
414
489
  const children = meaningfulChildren(t, elementPath.node.children);
@@ -510,7 +585,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
510
585
  return;
511
586
  }
512
587
  let rootName = null;
513
- if (t.isJSXIdentifier(path2.node.name)) {
588
+ if (isIntrinsicTagName(path2.node.name)) {
589
+ rootName = null;
590
+ } else if (t.isJSXIdentifier(path2.node.name)) {
514
591
  rootName = path2.node.name.name;
515
592
  } else if (t.isJSXMemberExpression(path2.node.name)) {
516
593
  let cur = path2.node.name;
@@ -572,6 +649,15 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
572
649
  t.objectProperty(t.identifier("slot"), t.stringLiteral(slot))
573
650
  );
574
651
  }
652
+ if (!isHostElement) {
653
+ const childrenText = describeChildrenText(path2);
654
+ metaObj.properties.push(
655
+ t.objectProperty(
656
+ t.identifier("childrenText"),
657
+ childrenText === null ? t.nullLiteral() : t.stringLiteral(childrenText)
658
+ )
659
+ );
660
+ }
575
661
  if (bindings.length > 0) {
576
662
  metaObj.properties.push(
577
663
  t.objectProperty(t.identifier("propBindings"), propBindingsToAst(bindings))
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  meaningfulChildren
3
- } from "./chunk-6QB35SLQ.mjs";
3
+ } from "./chunk-O3UMTM4N.mjs";
4
4
 
5
5
  // src/babel-plugin-collection-binding.ts
6
6
  import { VISITOR_KEYS } from "@babel/types";
package/dist/index.d.mts CHANGED
@@ -12,6 +12,7 @@ import { types, PluginObj } from '@babel/core';
12
12
  * nodeId, file, line, col, componentName, staticProps, movable, parentNodeId
13
13
  * + (ARCH-883 F1) slot, propBindings, propOwner
14
14
  * + (ARCH-883-KOI) origin
15
+ * + (ARCH-1444) childrenText — non-host elements only
15
16
  *
16
17
  * At runtime the Fiber bridge reads `memoizedProps.__editorMeta` to resolve
17
18
  * element identity without any DOM attributes.
@@ -169,6 +170,17 @@ declare function babelPluginEditorMeta({ types: t }: {
169
170
  * `id`/`htmlFor`/`name`/`type`/`role`/`slot`) are downgraded to `formatted` by the
170
171
  * PRODUCER, because no usage value could make them a text edit. The consumer does
171
172
  * not need to re-derive that list; it only needs the allowlist above.
173
+ *
174
+ * ARCH-1443: `{ target: 'children', prop: 'children', kind: 'direct' }` is emitted
175
+ * for an element that forwards its component's props REST and renders no children
176
+ * of its own — the shape EVERY ejected shadcn-style primitive has
177
+ * (`({ className, ...p }) => <h3 className={…} {...p} />`). Nothing about the
178
+ * allowlist changes: the usage value of `children` is the JSX children written at
179
+ * the usage site (`<CardTitle>Cómo funciona</CardTitle>`), and the same
180
+ * usage-value law applies to it — plain text is writable, a nested element or a
181
+ * computed expression is a gap. The producer refuses whenever more than one
182
+ * element in the component forwards the rest, since one `children` prop cannot
183
+ * name which of them an edit is about.
172
184
  */
173
185
  interface PropBinding {
174
186
  /**
@@ -216,6 +228,38 @@ interface EditorMeta {
216
228
  componentName: string;
217
229
  /** Safely serializable literal prop values. Dynamic expressions are "__dynamic__". */
218
230
  staticProps: Record<string, unknown> | null;
231
+ /**
232
+ * ARCH-1444 — the SHAPE of this element's JSX children, and their text when
233
+ * that text is plain. `staticProps` is built from ATTRIBUTES only, so without
234
+ * this field the `children` prop is the one binding target whose value at the
235
+ * usage a consumer cannot see — and the usage-value law above ("plain text is
236
+ * writable, a nested element or a computed expression is a gap") is stated in
237
+ * terms of exactly that value.
238
+ *
239
+ * `null` — the element is SELF-CLOSING: no children container at
240
+ * all. NOT the same as `''` (`<X></X>`), which can take a
241
+ * text write; `<X />` cannot without a structural rewrite.
242
+ * `''` — an empty children container.
243
+ * `'__dynamic__'` — anything that is not exactly one JSXText child: an
244
+ * expression container, a nested element, a comment, or
245
+ * several children. Same sentinel as `staticProps`.
246
+ * any other string— the sole JSXText child, TRIMMED (JSX itself trims
247
+ * leading/trailing whitespace-with-newlines, so the
248
+ * trimmed form is what renders). A newline that SURVIVES
249
+ * the trim is real multi-line content: a consumer that
250
+ * writes by splicing must refuse it, because replacing it
251
+ * with one line deletes source lines and invalidates every
252
+ * anchor below.
253
+ *
254
+ * Emitted for NON-HOST elements only — a usage is always a component tag, and
255
+ * stamping every `<div>` would be weight for an answer nothing asks.
256
+ *
257
+ * ABSENT (`undefined`) on a host element and on every bundle built before this
258
+ * change. Absence is NOT "no children": it means the page cannot answer the
259
+ * question, and a consumer must gap the edit rather than assume (the same law
260
+ * `origin`'s docstring states at length).
261
+ */
262
+ childrenText?: string | null;
219
263
  /**
220
264
  * Whether the JSX node is in a reorderable position (2+ siblings).
221
265
  *
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ import { types, PluginObj } from '@babel/core';
12
12
  * nodeId, file, line, col, componentName, staticProps, movable, parentNodeId
13
13
  * + (ARCH-883 F1) slot, propBindings, propOwner
14
14
  * + (ARCH-883-KOI) origin
15
+ * + (ARCH-1444) childrenText — non-host elements only
15
16
  *
16
17
  * At runtime the Fiber bridge reads `memoizedProps.__editorMeta` to resolve
17
18
  * element identity without any DOM attributes.
@@ -169,6 +170,17 @@ declare function babelPluginEditorMeta({ types: t }: {
169
170
  * `id`/`htmlFor`/`name`/`type`/`role`/`slot`) are downgraded to `formatted` by the
170
171
  * PRODUCER, because no usage value could make them a text edit. The consumer does
171
172
  * not need to re-derive that list; it only needs the allowlist above.
173
+ *
174
+ * ARCH-1443: `{ target: 'children', prop: 'children', kind: 'direct' }` is emitted
175
+ * for an element that forwards its component's props REST and renders no children
176
+ * of its own — the shape EVERY ejected shadcn-style primitive has
177
+ * (`({ className, ...p }) => <h3 className={…} {...p} />`). Nothing about the
178
+ * allowlist changes: the usage value of `children` is the JSX children written at
179
+ * the usage site (`<CardTitle>Cómo funciona</CardTitle>`), and the same
180
+ * usage-value law applies to it — plain text is writable, a nested element or a
181
+ * computed expression is a gap. The producer refuses whenever more than one
182
+ * element in the component forwards the rest, since one `children` prop cannot
183
+ * name which of them an edit is about.
172
184
  */
173
185
  interface PropBinding {
174
186
  /**
@@ -216,6 +228,38 @@ interface EditorMeta {
216
228
  componentName: string;
217
229
  /** Safely serializable literal prop values. Dynamic expressions are "__dynamic__". */
218
230
  staticProps: Record<string, unknown> | null;
231
+ /**
232
+ * ARCH-1444 — the SHAPE of this element's JSX children, and their text when
233
+ * that text is plain. `staticProps` is built from ATTRIBUTES only, so without
234
+ * this field the `children` prop is the one binding target whose value at the
235
+ * usage a consumer cannot see — and the usage-value law above ("plain text is
236
+ * writable, a nested element or a computed expression is a gap") is stated in
237
+ * terms of exactly that value.
238
+ *
239
+ * `null` — the element is SELF-CLOSING: no children container at
240
+ * all. NOT the same as `''` (`<X></X>`), which can take a
241
+ * text write; `<X />` cannot without a structural rewrite.
242
+ * `''` — an empty children container.
243
+ * `'__dynamic__'` — anything that is not exactly one JSXText child: an
244
+ * expression container, a nested element, a comment, or
245
+ * several children. Same sentinel as `staticProps`.
246
+ * any other string— the sole JSXText child, TRIMMED (JSX itself trims
247
+ * leading/trailing whitespace-with-newlines, so the
248
+ * trimmed form is what renders). A newline that SURVIVES
249
+ * the trim is real multi-line content: a consumer that
250
+ * writes by splicing must refuse it, because replacing it
251
+ * with one line deletes source lines and invalidates every
252
+ * anchor below.
253
+ *
254
+ * Emitted for NON-HOST elements only — a usage is always a component tag, and
255
+ * stamping every `<div>` would be weight for an answer nothing asks.
256
+ *
257
+ * ABSENT (`undefined`) on a host element and on every bundle built before this
258
+ * change. Absence is NOT "no children": it means the page cannot answer the
259
+ * question, and a consumer must gap the edit rather than assume (the same law
260
+ * `origin`'s docstring states at length).
261
+ */
262
+ childrenText?: string | null;
219
263
  /**
220
264
  * Whether the JSX node is in a reorderable position (2+ siblings).
221
265
  *
package/dist/index.js CHANGED
@@ -277,6 +277,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
277
277
  }
278
278
  return "";
279
279
  }
280
+ function isIntrinsicTagName(name) {
281
+ return t.isJSXIdentifier(name) && /^[a-z]/.test(name.name);
282
+ }
280
283
  function isHostLikeMemberExpressionName(name) {
281
284
  if (!t.isJSXMemberExpression(name)) return false;
282
285
  const propertyName = name.property.name;
@@ -367,6 +370,18 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
367
370
  }
368
371
  return hasAny ? t.objectExpression(props) : null;
369
372
  }
373
+ function describeChildrenText(opening) {
374
+ const elementPath = opening.parentPath;
375
+ if (!elementPath || !elementPath.isJSXElement()) return null;
376
+ const element = elementPath.node;
377
+ if (!element.closingElement) return null;
378
+ const children = element.children;
379
+ if (children.length === 0) return "";
380
+ if (children.length !== 1) return "__dynamic__";
381
+ const [only] = children;
382
+ if (!t.isJSXText(only)) return "__dynamic__";
383
+ return only.value.trim();
384
+ }
370
385
  function isMetaStripSpreadExpression(arg) {
371
386
  if (!t.isCallExpression(arg)) return false;
372
387
  if (!t.isArrowFunctionExpression(arg.callee)) return false;
@@ -474,6 +489,14 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
474
489
  }
475
490
  return map;
476
491
  }
492
+ function restParamName(pattern) {
493
+ for (const prop of pattern.properties) {
494
+ if (!t.isRestElement(prop)) continue;
495
+ if (t.isIdentifier(prop.argument)) return prop.argument.name;
496
+ return null;
497
+ }
498
+ return null;
499
+ }
477
500
  function resolvePropContext(path) {
478
501
  let fn = path.getFunctionParent();
479
502
  while (fn) {
@@ -482,7 +505,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
482
505
  return {
483
506
  componentScope: fn.scope,
484
507
  propMap: buildPropMap(first),
485
- componentName: getFunctionName(fn)
508
+ componentName: getFunctionName(fn),
509
+ restParam: restParamName(first),
510
+ componentPath: fn
486
511
  };
487
512
  }
488
513
  fn = fn.getFunctionParent();
@@ -602,12 +627,62 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
602
627
  const props = collectPropIdentifiers(expr, scope, ctx);
603
628
  return props.length > 0 ? { kind: "formatted", prop: props[0] } : null;
604
629
  }
630
+ function unwrapSanitizedSpread(arg) {
631
+ if (!t.isCallExpression(arg)) return arg;
632
+ if (!isMetaStripSpreadExpression(arg)) return arg;
633
+ if (arg.arguments.length !== 1) return arg;
634
+ const [payload] = arg.arguments;
635
+ if (!t.isObjectExpression(payload) || payload.properties.length !== 1) return arg;
636
+ const [only] = payload.properties;
637
+ if (!t.isSpreadElement(only)) return arg;
638
+ return only.argument;
639
+ }
640
+ function forwardsOwnerRestChildren(opening, ctx) {
641
+ if (ctx.restParam === null) return false;
642
+ const elementPath = opening.parentPath;
643
+ if (!elementPath || !elementPath.isJSXElement()) return false;
644
+ if (meaningfulChildren(t, elementPath.node.children).length > 0) return false;
645
+ let spreadsRest = 0;
646
+ for (const attr of opening.node.attributes) {
647
+ if (t.isJSXAttribute(attr)) {
648
+ if (t.isJSXIdentifier(attr.name, { name: "children" })) return false;
649
+ continue;
650
+ }
651
+ if (!t.isJSXSpreadAttribute(attr)) continue;
652
+ const payload = unwrapSanitizedSpread(attr.argument);
653
+ if (!t.isIdentifier(payload, { name: ctx.restParam })) return false;
654
+ spreadsRest += 1;
655
+ }
656
+ if (spreadsRest !== 1) return false;
657
+ const binding = opening.scope.getBinding(ctx.restParam);
658
+ if (!binding) return false;
659
+ if (binding.scope !== ctx.componentScope) return false;
660
+ return binding.constant;
661
+ }
662
+ const restForwarderCounts = /* @__PURE__ */ new WeakMap();
663
+ function countRestForwarders(ctx) {
664
+ const cached = restForwarderCounts.get(ctx.componentPath.node);
665
+ if (cached !== void 0) return cached;
666
+ let count = 0;
667
+ ctx.componentPath.traverse({
668
+ JSXOpeningElement(inner) {
669
+ if (forwardsOwnerRestChildren(inner, ctx)) count += 1;
670
+ }
671
+ });
672
+ restForwarderCounts.set(ctx.componentPath.node, count);
673
+ return count;
674
+ }
605
675
  function collectPropBindings(path, attrs) {
606
676
  const ctx = resolvePropContext(path);
607
- if (!ctx || ctx.propMap.size === 0) return { bindings: [], owner: null };
677
+ if (!ctx || ctx.propMap.size === 0 && ctx.restParam === null) {
678
+ return { bindings: [], owner: null };
679
+ }
608
680
  if (ctx.componentName === null) return { bindings: [], owner: null };
609
681
  const scope = path.scope;
610
682
  const bindings = [];
683
+ if (forwardsOwnerRestChildren(path, ctx) && countRestForwarders(ctx) === 1) {
684
+ bindings.push({ target: "children", prop: "children", kind: "direct" });
685
+ }
611
686
  const elementPath = path.parentPath;
612
687
  if (elementPath && elementPath.isJSXElement()) {
613
688
  const children = meaningfulChildren(t, elementPath.node.children);
@@ -709,7 +784,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
709
784
  return;
710
785
  }
711
786
  let rootName = null;
712
- if (t.isJSXIdentifier(path.node.name)) {
787
+ if (isIntrinsicTagName(path.node.name)) {
788
+ rootName = null;
789
+ } else if (t.isJSXIdentifier(path.node.name)) {
713
790
  rootName = path.node.name.name;
714
791
  } else if (t.isJSXMemberExpression(path.node.name)) {
715
792
  let cur = path.node.name;
@@ -771,6 +848,15 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
771
848
  t.objectProperty(t.identifier("slot"), t.stringLiteral(slot))
772
849
  );
773
850
  }
851
+ if (!isHostElement) {
852
+ const childrenText = describeChildrenText(path);
853
+ metaObj.properties.push(
854
+ t.objectProperty(
855
+ t.identifier("childrenText"),
856
+ childrenText === null ? t.nullLiteral() : t.stringLiteral(childrenText)
857
+ )
858
+ );
859
+ }
774
860
  if (bindings.length > 0) {
775
861
  metaObj.properties.push(
776
862
  t.objectProperty(t.identifier("propBindings"), propBindingsToAst(bindings))
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  __dirname,
3
3
  babelPluginEditorMeta
4
- } from "./chunk-6QB35SLQ.mjs";
4
+ } from "./chunk-O3UMTM4N.mjs";
5
5
 
6
6
  // src/index.ts
7
7
  import { readFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archie/devtools",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "repository": {
6
6
  "type": "git",