@archie/devtools 0.0.27 → 0.0.28

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.
@@ -72,6 +72,50 @@ import { types, PluginObj } from '@babel/core';
72
72
  * specific field OR several), because no row identity is at stake — only
73
73
  * further field leaves.
74
74
  *
75
+ * ── Forwarding-component allowlist descent (ARCH-1065 M-COV fix) ──────────
76
+ *
77
+ * D5.4 only ever sees THROUGH a component whose own source is available
78
+ * (module-local, co-located in the same file). Kit/imported components
79
+ * (`Card`, `CardContent`, `StatusBadge`, …) are opaque to the AST — the
80
+ * plugin has no body to project into — but many of them forward unrecognized
81
+ * JSX attributes onto their own root DOM element (typically via
82
+ * `{...rest}`), which makes them valid attribute CARRIERS even though
83
+ * they're not valid PROJECTION targets. `forwardingComponents` (a plugin
84
+ * option, see `CollectionBindingPluginOptions`) names exactly which ones.
85
+ *
86
+ * Two independent mechanisms use that allowlist:
87
+ *
88
+ * - (a2) ROW ROOT: when the `.map()` callback's own returned JSX element
89
+ * is an allowlisted component (`<Card key={r.id}>…</Card>` as the row
90
+ * root itself), the row-identity attributes stamp directly onto that
91
+ * component usage — NOT a projection into a separate definition, since
92
+ * there is none to project into. The field walk continues into the
93
+ * component's own JSX children with the SAME binding scope the row
94
+ * callback already established.
95
+ * - (c2-ii) LEAF: when a component usage directly hugs a field expression
96
+ * with no host element beneath it (`<StatusBadge>{w.status}</StatusBadge>`),
97
+ * the field attribute stamps onto the component itself IFF its name is
98
+ * allowlisted; otherwise the leaf is left unstamped — a leaf is never
99
+ * given an invented carrier.
100
+ *
101
+ * Separately, and regardless of allowlist membership at all — walking a
102
+ * component's own JSX children costs nothing, since no attribute lands
103
+ * anywhere until a valid carrier (host, or allowlisted leaf) is actually
104
+ * found — (a1) DESCENT: the field walk always continues INTO a structural
105
+ * child that is a component usage, as long as that usage has its own JSX
106
+ * children in source. `<Card><span>{w.name}</span></Card>` reaches the
107
+ * `<span>` (a host, stamped unconditionally) even though `Card` itself is
108
+ * never a stamp target unless allowlisted. This is what makes (c2-ii) safe
109
+ * to leave allowlist-gated: a wrapper with a host descendant never needs the
110
+ * wrapper itself to carry anything.
111
+ *
112
+ * The allowlist is ALWAYS an explicit option, never a built-in default list
113
+ * baked into this plugin — kit-manifest is the single source of truth for
114
+ * which components forward attributes (`forwardsDataAttributes`), and the
115
+ * kit build (`build-d.mjs`) is what threads that list in. An absent/empty
116
+ * option is exactly today's fail-closed behavior: zero component-carried
117
+ * stamps, byte-identical to the plugin before this mechanism existed.
118
+ *
75
119
  * ── Field resolution ────────────────────────────────────────────────────
76
120
  *
77
121
  * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
@@ -148,6 +192,27 @@ import { types, PluginObj } from '@babel/core';
148
192
  interface CollectionBindingPluginOptions {
149
193
  /** Skip files matching this pattern. @default /node_modules/ */
150
194
  fileExclude?: RegExp;
195
+ /**
196
+ * Component names known to forward unrecognized JSX attributes onto their
197
+ * own root DOM element (e.g. via `{...rest}`) — the exact set that makes
198
+ * a component usage a valid attribute CARRIER (row-root or leaf) even
199
+ * though it's opaque to the AST (see the "Forwarding-component allowlist
200
+ * descent" section above).
201
+ *
202
+ * Sourced from kit-manifest's `forwardsDataAttributes` flags by the kit
203
+ * build (`build-d.mjs` passes it) — this plugin NEVER hardcodes a default
204
+ * list here; that would be a shadow copy of kit-manifest's own truth, and
205
+ * would drift the moment a kit component's forwarding behavior changed
206
+ * without this plugin's knowledge. Synthetic lists are fine in tests, to
207
+ * keep the plugin testable standalone.
208
+ *
209
+ * Absent or empty = today's fail-closed behavior: no component ever
210
+ * carries a stamp, byte-identical to the plugin before this option
211
+ * existed.
212
+ *
213
+ * @default []
214
+ */
215
+ forwardingComponents?: string[];
151
216
  }
152
217
  declare function babelPluginCollectionBinding({ types: t }: {
153
218
  types: typeof types;
@@ -72,6 +72,50 @@ import { types, PluginObj } from '@babel/core';
72
72
  * specific field OR several), because no row identity is at stake — only
73
73
  * further field leaves.
74
74
  *
75
+ * ── Forwarding-component allowlist descent (ARCH-1065 M-COV fix) ──────────
76
+ *
77
+ * D5.4 only ever sees THROUGH a component whose own source is available
78
+ * (module-local, co-located in the same file). Kit/imported components
79
+ * (`Card`, `CardContent`, `StatusBadge`, …) are opaque to the AST — the
80
+ * plugin has no body to project into — but many of them forward unrecognized
81
+ * JSX attributes onto their own root DOM element (typically via
82
+ * `{...rest}`), which makes them valid attribute CARRIERS even though
83
+ * they're not valid PROJECTION targets. `forwardingComponents` (a plugin
84
+ * option, see `CollectionBindingPluginOptions`) names exactly which ones.
85
+ *
86
+ * Two independent mechanisms use that allowlist:
87
+ *
88
+ * - (a2) ROW ROOT: when the `.map()` callback's own returned JSX element
89
+ * is an allowlisted component (`<Card key={r.id}>…</Card>` as the row
90
+ * root itself), the row-identity attributes stamp directly onto that
91
+ * component usage — NOT a projection into a separate definition, since
92
+ * there is none to project into. The field walk continues into the
93
+ * component's own JSX children with the SAME binding scope the row
94
+ * callback already established.
95
+ * - (c2-ii) LEAF: when a component usage directly hugs a field expression
96
+ * with no host element beneath it (`<StatusBadge>{w.status}</StatusBadge>`),
97
+ * the field attribute stamps onto the component itself IFF its name is
98
+ * allowlisted; otherwise the leaf is left unstamped — a leaf is never
99
+ * given an invented carrier.
100
+ *
101
+ * Separately, and regardless of allowlist membership at all — walking a
102
+ * component's own JSX children costs nothing, since no attribute lands
103
+ * anywhere until a valid carrier (host, or allowlisted leaf) is actually
104
+ * found — (a1) DESCENT: the field walk always continues INTO a structural
105
+ * child that is a component usage, as long as that usage has its own JSX
106
+ * children in source. `<Card><span>{w.name}</span></Card>` reaches the
107
+ * `<span>` (a host, stamped unconditionally) even though `Card` itself is
108
+ * never a stamp target unless allowlisted. This is what makes (c2-ii) safe
109
+ * to leave allowlist-gated: a wrapper with a host descendant never needs the
110
+ * wrapper itself to carry anything.
111
+ *
112
+ * The allowlist is ALWAYS an explicit option, never a built-in default list
113
+ * baked into this plugin — kit-manifest is the single source of truth for
114
+ * which components forward attributes (`forwardsDataAttributes`), and the
115
+ * kit build (`build-d.mjs`) is what threads that list in. An absent/empty
116
+ * option is exactly today's fail-closed behavior: zero component-carried
117
+ * stamps, byte-identical to the plugin before this mechanism existed.
118
+ *
75
119
  * ── Field resolution ────────────────────────────────────────────────────
76
120
  *
77
121
  * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
@@ -148,6 +192,27 @@ import { types, PluginObj } from '@babel/core';
148
192
  interface CollectionBindingPluginOptions {
149
193
  /** Skip files matching this pattern. @default /node_modules/ */
150
194
  fileExclude?: RegExp;
195
+ /**
196
+ * Component names known to forward unrecognized JSX attributes onto their
197
+ * own root DOM element (e.g. via `{...rest}`) — the exact set that makes
198
+ * a component usage a valid attribute CARRIER (row-root or leaf) even
199
+ * though it's opaque to the AST (see the "Forwarding-component allowlist
200
+ * descent" section above).
201
+ *
202
+ * Sourced from kit-manifest's `forwardsDataAttributes` flags by the kit
203
+ * build (`build-d.mjs` passes it) — this plugin NEVER hardcodes a default
204
+ * list here; that would be a shadow copy of kit-manifest's own truth, and
205
+ * would drift the moment a kit component's forwarding behavior changed
206
+ * without this plugin's knowledge. Synthetic lists are fine in tests, to
207
+ * keep the plugin testable standalone.
208
+ *
209
+ * Absent or empty = today's fail-closed behavior: no component ever
210
+ * carries a stamp, byte-identical to the plugin before this option
211
+ * existed.
212
+ *
213
+ * @default []
214
+ */
215
+ forwardingComponents?: string[];
151
216
  }
152
217
  declare function babelPluginCollectionBinding({ types: t }: {
153
218
  types: typeof types;
@@ -47,7 +47,8 @@ var CHAIN_METHODS = /* @__PURE__ */ new Set(["filter", "slice", "sort", "map"]);
47
47
  var MAX_SOURCE_CHAIN_HOPS = 4;
48
48
  var MAX_PROJECTION_DEPTH = 3;
49
49
  function babelPluginCollectionBinding({ types: t }, opts = {}) {
50
- const { fileExclude = /node_modules/ } = opts;
50
+ const { fileExclude = /node_modules/, forwardingComponents = [] } = opts;
51
+ const forwardingComponentSet = new Set(forwardingComponents);
51
52
  function hasOwnAttr(elementPath, name) {
52
53
  return elementPath.node.openingElement.attributes.some(
53
54
  (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name })
@@ -75,6 +76,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
75
76
  function isLowercaseTag(tag) {
76
77
  return tag.length > 0 && tag[0] === tag[0].toLowerCase();
77
78
  }
79
+ function canCarryFieldStamp(elementPath) {
80
+ const tag = getTagName(elementPath.node.openingElement);
81
+ return isLowercaseTag(tag) || forwardingComponentSet.has(tag);
82
+ }
78
83
  function getAttrName(attr) {
79
84
  return t.isJSXIdentifier(attr.name) ? attr.name.name : `${attr.name.namespace.name}:${attr.name.name.name}`;
80
85
  }
@@ -324,11 +329,13 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
324
329
  }
325
330
  function stampPureLeaf(elementPath, expr, scope) {
326
331
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
332
+ if (!canCarryFieldStamp(elementPath)) return;
327
333
  if (t.isJSXEmptyExpression(expr) || !t.isExpression(expr)) return;
328
334
  applyFieldVerdict(elementPath, classifyFieldExpr(expr, scope));
329
335
  }
330
336
  function stampLabelValueOrMixed(elementPath, childPaths, scope) {
331
337
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
338
+ if (!canCarryFieldStamp(elementPath)) return;
332
339
  const hasInvisibleSibling = childPaths.some(
333
340
  (p) => p.isJSXExpressionContainer() && t.isExpression(p.node.expression) && isInvisibleRenderExpr(p.node.expression)
334
341
  );
@@ -432,6 +439,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
432
439
  function resolveRowRoot(jsxPath, scope, depth) {
433
440
  const tag = getTagName(jsxPath.node.openingElement);
434
441
  if (isLowercaseTag(tag)) return { hostPath: jsxPath, scope };
442
+ if (forwardingComponentSet.has(tag)) return { hostPath: jsxPath, scope };
435
443
  if (depth >= MAX_PROJECTION_DEPTH) return null;
436
444
  const fnPath = resolveComponentDefinition(tag, jsxPath);
437
445
  if (!fnPath) return null;
@@ -503,6 +511,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
503
511
  walkFieldsWithinRow(childPath, scope);
504
512
  return;
505
513
  }
514
+ if (meaningfulChildPaths(childPath).length > 0) {
515
+ walkFieldsWithinRow(childPath, scope);
516
+ return;
517
+ }
506
518
  const projected = resolveFieldProjection(childPath, scope, 0);
507
519
  if (!projected) return;
508
520
  walkFieldsWithinRow(projected.hostPath, projected.scope);
@@ -16,7 +16,8 @@ var CHAIN_METHODS = /* @__PURE__ */ new Set(["filter", "slice", "sort", "map"]);
16
16
  var MAX_SOURCE_CHAIN_HOPS = 4;
17
17
  var MAX_PROJECTION_DEPTH = 3;
18
18
  function babelPluginCollectionBinding({ types: t }, opts = {}) {
19
- const { fileExclude = /node_modules/ } = opts;
19
+ const { fileExclude = /node_modules/, forwardingComponents = [] } = opts;
20
+ const forwardingComponentSet = new Set(forwardingComponents);
20
21
  function hasOwnAttr(elementPath, name) {
21
22
  return elementPath.node.openingElement.attributes.some(
22
23
  (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name })
@@ -44,6 +45,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
44
45
  function isLowercaseTag(tag) {
45
46
  return tag.length > 0 && tag[0] === tag[0].toLowerCase();
46
47
  }
48
+ function canCarryFieldStamp(elementPath) {
49
+ const tag = getTagName(elementPath.node.openingElement);
50
+ return isLowercaseTag(tag) || forwardingComponentSet.has(tag);
51
+ }
47
52
  function getAttrName(attr) {
48
53
  return t.isJSXIdentifier(attr.name) ? attr.name.name : `${attr.name.namespace.name}:${attr.name.name.name}`;
49
54
  }
@@ -293,11 +298,13 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
293
298
  }
294
299
  function stampPureLeaf(elementPath, expr, scope) {
295
300
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
301
+ if (!canCarryFieldStamp(elementPath)) return;
296
302
  if (t.isJSXEmptyExpression(expr) || !t.isExpression(expr)) return;
297
303
  applyFieldVerdict(elementPath, classifyFieldExpr(expr, scope));
298
304
  }
299
305
  function stampLabelValueOrMixed(elementPath, childPaths, scope) {
300
306
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
307
+ if (!canCarryFieldStamp(elementPath)) return;
301
308
  const hasInvisibleSibling = childPaths.some(
302
309
  (p) => p.isJSXExpressionContainer() && t.isExpression(p.node.expression) && isInvisibleRenderExpr(p.node.expression)
303
310
  );
@@ -401,6 +408,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
401
408
  function resolveRowRoot(jsxPath, scope, depth) {
402
409
  const tag = getTagName(jsxPath.node.openingElement);
403
410
  if (isLowercaseTag(tag)) return { hostPath: jsxPath, scope };
411
+ if (forwardingComponentSet.has(tag)) return { hostPath: jsxPath, scope };
404
412
  if (depth >= MAX_PROJECTION_DEPTH) return null;
405
413
  const fnPath = resolveComponentDefinition(tag, jsxPath);
406
414
  if (!fnPath) return null;
@@ -472,6 +480,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
472
480
  walkFieldsWithinRow(childPath, scope);
473
481
  return;
474
482
  }
483
+ if (meaningfulChildPaths(childPath).length > 0) {
484
+ walkFieldsWithinRow(childPath, scope);
485
+ return;
486
+ }
475
487
  const projected = resolveFieldProjection(childPath, scope, 0);
476
488
  if (!projected) return;
477
489
  walkFieldsWithinRow(projected.hostPath, projected.scope);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archie/devtools",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "repository": {
6
6
  "type": "git",