@archie/devtools 0.0.27 → 0.0.29

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.
@@ -21,8 +21,12 @@ import { types, PluginObj } from '@babel/core';
21
21
  * data-agent-row {String(param?.id)}, on the row root
22
22
  * data-agent-field static string OR a runtime expression
23
23
  * (??/ternary field selection, D5.5)
24
- * data-agent-field-state "mixed" | "unresolved"
25
- * data-agent-field-parts comma-separated field list (mixed leaves)
24
+ * data-agent-field-state "mixed" | "unresolved" | "rendered"
25
+ * ("rendered": a formatter-CALL-wrapped
26
+ * value — the DOM only ever holds the
27
+ * FORMATTED string, ARCH-1065 M-COV FIX)
28
+ * data-agent-field-parts comma-separated field list (mixed leaves,
29
+ * or the single field of a "rendered" leaf)
26
30
  * data-agent-field-slot text-node index of the dynamic part
27
31
  * (label+value leaves, D6)
28
32
  * data-agent-collection-order "stable" | "derived" (D18), on the row root
@@ -35,8 +39,12 @@ import { types, PluginObj } from '@babel/core';
35
39
  * `.filter()/.slice()/.sort()/.map()` chain calls, through single-assignment
36
40
  * (`const`, never reassigned) local-variable hops (bounded to
37
41
  * `MAX_SOURCE_CHAIN_HOPS`), and through a `useMemo(() => …, deps)` /
38
- * `React.useMemo(() => …, deps)` unwrap (the common `() => expr` shape only —
39
- * see `resolveSliceSource`). A `.sort(` ANYWHERE in the chain marks the
42
+ * `React.useMemo(() => …, deps)` unwrap — EITHER the arrow-EXPRESSION-body
43
+ * shape (`() => expr`) OR a block body with exactly ONE return statement
44
+ * (`() => { …; return expr; }`, ARCH-1065 M-COV FIX, devtools 0.0.29); a
45
+ * block body with zero or 2+ returns (ambiguous control flow) is left
46
+ * unresolved, never guessed at (see `resolveSliceSource` /
47
+ * `findSingleReturnArgument`). A `.sort(` ANYWHERE in the chain marks the
40
48
  * collection `data-agent-collection-order="derived"`; otherwise `"stable"`
41
49
  * (D18). The identifier root must be exactly `slice` — this mirrors the
42
50
  * runtime's `window.__slice` (M3); the codemod does not trace how a `slice`
@@ -72,6 +80,50 @@ import { types, PluginObj } from '@babel/core';
72
80
  * specific field OR several), because no row identity is at stake — only
73
81
  * further field leaves.
74
82
  *
83
+ * ── Forwarding-component allowlist descent (ARCH-1065 M-COV fix) ──────────
84
+ *
85
+ * D5.4 only ever sees THROUGH a component whose own source is available
86
+ * (module-local, co-located in the same file). Kit/imported components
87
+ * (`Card`, `CardContent`, `StatusBadge`, …) are opaque to the AST — the
88
+ * plugin has no body to project into — but many of them forward unrecognized
89
+ * JSX attributes onto their own root DOM element (typically via
90
+ * `{...rest}`), which makes them valid attribute CARRIERS even though
91
+ * they're not valid PROJECTION targets. `forwardingComponents` (a plugin
92
+ * option, see `CollectionBindingPluginOptions`) names exactly which ones.
93
+ *
94
+ * Two independent mechanisms use that allowlist:
95
+ *
96
+ * - (a2) ROW ROOT: when the `.map()` callback's own returned JSX element
97
+ * is an allowlisted component (`<Card key={r.id}>…</Card>` as the row
98
+ * root itself), the row-identity attributes stamp directly onto that
99
+ * component usage — NOT a projection into a separate definition, since
100
+ * there is none to project into. The field walk continues into the
101
+ * component's own JSX children with the SAME binding scope the row
102
+ * callback already established.
103
+ * - (c2-ii) LEAF: when a component usage directly hugs a field expression
104
+ * with no host element beneath it (`<StatusBadge>{w.status}</StatusBadge>`),
105
+ * the field attribute stamps onto the component itself IFF its name is
106
+ * allowlisted; otherwise the leaf is left unstamped — a leaf is never
107
+ * given an invented carrier.
108
+ *
109
+ * Separately, and regardless of allowlist membership at all — walking a
110
+ * component's own JSX children costs nothing, since no attribute lands
111
+ * anywhere until a valid carrier (host, or allowlisted leaf) is actually
112
+ * found — (a1) DESCENT: the field walk always continues INTO a structural
113
+ * child that is a component usage, as long as that usage has its own JSX
114
+ * children in source. `<Card><span>{w.name}</span></Card>` reaches the
115
+ * `<span>` (a host, stamped unconditionally) even though `Card` itself is
116
+ * never a stamp target unless allowlisted. This is what makes (c2-ii) safe
117
+ * to leave allowlist-gated: a wrapper with a host descendant never needs the
118
+ * wrapper itself to carry anything.
119
+ *
120
+ * The allowlist is ALWAYS an explicit option, never a built-in default list
121
+ * baked into this plugin — kit-manifest is the single source of truth for
122
+ * which components forward attributes (`forwardsDataAttributes`), and the
123
+ * kit build (`build-d.mjs`) is what threads that list in. An absent/empty
124
+ * option is exactly today's fail-closed behavior: zero component-carried
125
+ * stamps, byte-identical to the plugin before this mechanism existed.
126
+ *
75
127
  * ── Field resolution ────────────────────────────────────────────────────
76
128
  *
77
129
  * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
@@ -95,10 +147,25 @@ import { types, PluginObj } from '@babel/core';
95
147
  * the duplicated test would double-evaluate it across SSR and hydration,
96
148
  * so the whole leaf is left unstamped instead;
97
149
  * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
98
- * member (a boolean-typed field driving two literal branches) → static
99
- * field named by `cond` — the FE's typed controls (D7, a different
100
- * mechanism, M3) read the boolean's true/false rendering from
101
- * `window.__slice`, not from this attribute;
150
+ * member (a boolean-typed field driving two literal branches), OR a
151
+ * CALL-FREE comparison (`===`/`!==`/`==`/`!=`/`</<=`/`>`/`>=`) naming a
152
+ * field on either side (`r.status === 'Active' ? 'Pause' : 'Enable'`,
153
+ * D5.5 widening, ARCH-1065 M-COV FIX) → static field named by the
154
+ * resolved test field — the FE's typed controls (D7, a different
155
+ * mechanism, M3) read the boolean's/status's true/false or vocabulary
156
+ * rendering from `window.__slice`, not from this attribute. Nothing
157
+ * about the test is ever duplicated into the DOM here (only the field
158
+ * NAME is emitted), so there is no C3 double-evaluation concern for this
159
+ * shape — `containsEvaluation(test)` is still checked defensively;
160
+ * - a formatter CALL with exactly one argument that is itself a bare
161
+ * member/optional-member field (`Kit.date(r.updated_at)`,
162
+ * `money(auction.currentPrice)`, ARCH-1065 M-COV FIX) → the resolved
163
+ * field name, PLUS `data-agent-field-state="rendered"` and
164
+ * `data-agent-field-parts` (the same field name) — the DOM only ever
165
+ * holds the FORMATTED string, never the raw value, so this is marked
166
+ * the same "honest, non-invertible" way a `??`/boolean fallback is (D8);
167
+ * a multi-arg call, or a call whose sole argument isn't a bare field
168
+ * member, is left unstamped rather than guessed at;
102
169
  * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
103
170
  * name, `data-agent-field-state="unresolved"` (a positive "this is
104
171
  * row-scoped data, but which field is unknown" signal — never silent).
@@ -127,6 +194,13 @@ import { types, PluginObj } from '@babel/core';
127
194
  * that is itself slice-rooted gets discovered and stamped independently by
128
195
  * the same Program-wide `.map()` scan, with its own entity/row identity.
129
196
  *
197
+ * A JSXExpressionContainer wrapping `cond && <JSX/>` / `cond || <JSX/>` / a
198
+ * ternary IS a structural child, though (ARCH-1065 M-COV FIX, devtools
199
+ * 0.0.29, see `collectConditionalJsxBranchPaths`): the walker descends into
200
+ * whichever branch(es) are themselves a JSXElement, with the SAME scope —
201
+ * purely structural, the guard/test is never duplicated or stamped, only the
202
+ * JSX it conditionally renders is walked for further fields.
203
+ *
130
204
  * ── Failure posture ─────────────────────────────────────────────────────
131
205
  *
132
206
  * Every resolution step is an explicit allowlist; anything the resolver is
@@ -148,6 +222,27 @@ import { types, PluginObj } from '@babel/core';
148
222
  interface CollectionBindingPluginOptions {
149
223
  /** Skip files matching this pattern. @default /node_modules/ */
150
224
  fileExclude?: RegExp;
225
+ /**
226
+ * Component names known to forward unrecognized JSX attributes onto their
227
+ * own root DOM element (e.g. via `{...rest}`) — the exact set that makes
228
+ * a component usage a valid attribute CARRIER (row-root or leaf) even
229
+ * though it's opaque to the AST (see the "Forwarding-component allowlist
230
+ * descent" section above).
231
+ *
232
+ * Sourced from kit-manifest's `forwardsDataAttributes` flags by the kit
233
+ * build (`build-d.mjs` passes it) — this plugin NEVER hardcodes a default
234
+ * list here; that would be a shadow copy of kit-manifest's own truth, and
235
+ * would drift the moment a kit component's forwarding behavior changed
236
+ * without this plugin's knowledge. Synthetic lists are fine in tests, to
237
+ * keep the plugin testable standalone.
238
+ *
239
+ * Absent or empty = today's fail-closed behavior: no component ever
240
+ * carries a stamp, byte-identical to the plugin before this option
241
+ * existed.
242
+ *
243
+ * @default []
244
+ */
245
+ forwardingComponents?: string[];
151
246
  }
152
247
  declare function babelPluginCollectionBinding({ types: t }: {
153
248
  types: typeof types;
@@ -21,8 +21,12 @@ import { types, PluginObj } from '@babel/core';
21
21
  * data-agent-row {String(param?.id)}, on the row root
22
22
  * data-agent-field static string OR a runtime expression
23
23
  * (??/ternary field selection, D5.5)
24
- * data-agent-field-state "mixed" | "unresolved"
25
- * data-agent-field-parts comma-separated field list (mixed leaves)
24
+ * data-agent-field-state "mixed" | "unresolved" | "rendered"
25
+ * ("rendered": a formatter-CALL-wrapped
26
+ * value — the DOM only ever holds the
27
+ * FORMATTED string, ARCH-1065 M-COV FIX)
28
+ * data-agent-field-parts comma-separated field list (mixed leaves,
29
+ * or the single field of a "rendered" leaf)
26
30
  * data-agent-field-slot text-node index of the dynamic part
27
31
  * (label+value leaves, D6)
28
32
  * data-agent-collection-order "stable" | "derived" (D18), on the row root
@@ -35,8 +39,12 @@ import { types, PluginObj } from '@babel/core';
35
39
  * `.filter()/.slice()/.sort()/.map()` chain calls, through single-assignment
36
40
  * (`const`, never reassigned) local-variable hops (bounded to
37
41
  * `MAX_SOURCE_CHAIN_HOPS`), and through a `useMemo(() => …, deps)` /
38
- * `React.useMemo(() => …, deps)` unwrap (the common `() => expr` shape only —
39
- * see `resolveSliceSource`). A `.sort(` ANYWHERE in the chain marks the
42
+ * `React.useMemo(() => …, deps)` unwrap — EITHER the arrow-EXPRESSION-body
43
+ * shape (`() => expr`) OR a block body with exactly ONE return statement
44
+ * (`() => { …; return expr; }`, ARCH-1065 M-COV FIX, devtools 0.0.29); a
45
+ * block body with zero or 2+ returns (ambiguous control flow) is left
46
+ * unresolved, never guessed at (see `resolveSliceSource` /
47
+ * `findSingleReturnArgument`). A `.sort(` ANYWHERE in the chain marks the
40
48
  * collection `data-agent-collection-order="derived"`; otherwise `"stable"`
41
49
  * (D18). The identifier root must be exactly `slice` — this mirrors the
42
50
  * runtime's `window.__slice` (M3); the codemod does not trace how a `slice`
@@ -72,6 +80,50 @@ import { types, PluginObj } from '@babel/core';
72
80
  * specific field OR several), because no row identity is at stake — only
73
81
  * further field leaves.
74
82
  *
83
+ * ── Forwarding-component allowlist descent (ARCH-1065 M-COV fix) ──────────
84
+ *
85
+ * D5.4 only ever sees THROUGH a component whose own source is available
86
+ * (module-local, co-located in the same file). Kit/imported components
87
+ * (`Card`, `CardContent`, `StatusBadge`, …) are opaque to the AST — the
88
+ * plugin has no body to project into — but many of them forward unrecognized
89
+ * JSX attributes onto their own root DOM element (typically via
90
+ * `{...rest}`), which makes them valid attribute CARRIERS even though
91
+ * they're not valid PROJECTION targets. `forwardingComponents` (a plugin
92
+ * option, see `CollectionBindingPluginOptions`) names exactly which ones.
93
+ *
94
+ * Two independent mechanisms use that allowlist:
95
+ *
96
+ * - (a2) ROW ROOT: when the `.map()` callback's own returned JSX element
97
+ * is an allowlisted component (`<Card key={r.id}>…</Card>` as the row
98
+ * root itself), the row-identity attributes stamp directly onto that
99
+ * component usage — NOT a projection into a separate definition, since
100
+ * there is none to project into. The field walk continues into the
101
+ * component's own JSX children with the SAME binding scope the row
102
+ * callback already established.
103
+ * - (c2-ii) LEAF: when a component usage directly hugs a field expression
104
+ * with no host element beneath it (`<StatusBadge>{w.status}</StatusBadge>`),
105
+ * the field attribute stamps onto the component itself IFF its name is
106
+ * allowlisted; otherwise the leaf is left unstamped — a leaf is never
107
+ * given an invented carrier.
108
+ *
109
+ * Separately, and regardless of allowlist membership at all — walking a
110
+ * component's own JSX children costs nothing, since no attribute lands
111
+ * anywhere until a valid carrier (host, or allowlisted leaf) is actually
112
+ * found — (a1) DESCENT: the field walk always continues INTO a structural
113
+ * child that is a component usage, as long as that usage has its own JSX
114
+ * children in source. `<Card><span>{w.name}</span></Card>` reaches the
115
+ * `<span>` (a host, stamped unconditionally) even though `Card` itself is
116
+ * never a stamp target unless allowlisted. This is what makes (c2-ii) safe
117
+ * to leave allowlist-gated: a wrapper with a host descendant never needs the
118
+ * wrapper itself to carry anything.
119
+ *
120
+ * The allowlist is ALWAYS an explicit option, never a built-in default list
121
+ * baked into this plugin — kit-manifest is the single source of truth for
122
+ * which components forward attributes (`forwardsDataAttributes`), and the
123
+ * kit build (`build-d.mjs`) is what threads that list in. An absent/empty
124
+ * option is exactly today's fail-closed behavior: zero component-carried
125
+ * stamps, byte-identical to the plugin before this mechanism existed.
126
+ *
75
127
  * ── Field resolution ────────────────────────────────────────────────────
76
128
  *
77
129
  * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
@@ -95,10 +147,25 @@ import { types, PluginObj } from '@babel/core';
95
147
  * the duplicated test would double-evaluate it across SSR and hydration,
96
148
  * so the whole leaf is left unstamped instead;
97
149
  * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
98
- * member (a boolean-typed field driving two literal branches) → static
99
- * field named by `cond` — the FE's typed controls (D7, a different
100
- * mechanism, M3) read the boolean's true/false rendering from
101
- * `window.__slice`, not from this attribute;
150
+ * member (a boolean-typed field driving two literal branches), OR a
151
+ * CALL-FREE comparison (`===`/`!==`/`==`/`!=`/`</<=`/`>`/`>=`) naming a
152
+ * field on either side (`r.status === 'Active' ? 'Pause' : 'Enable'`,
153
+ * D5.5 widening, ARCH-1065 M-COV FIX) → static field named by the
154
+ * resolved test field — the FE's typed controls (D7, a different
155
+ * mechanism, M3) read the boolean's/status's true/false or vocabulary
156
+ * rendering from `window.__slice`, not from this attribute. Nothing
157
+ * about the test is ever duplicated into the DOM here (only the field
158
+ * NAME is emitted), so there is no C3 double-evaluation concern for this
159
+ * shape — `containsEvaluation(test)` is still checked defensively;
160
+ * - a formatter CALL with exactly one argument that is itself a bare
161
+ * member/optional-member field (`Kit.date(r.updated_at)`,
162
+ * `money(auction.currentPrice)`, ARCH-1065 M-COV FIX) → the resolved
163
+ * field name, PLUS `data-agent-field-state="rendered"` and
164
+ * `data-agent-field-parts` (the same field name) — the DOM only ever
165
+ * holds the FORMATTED string, never the raw value, so this is marked
166
+ * the same "honest, non-invertible" way a `??`/boolean fallback is (D8);
167
+ * a multi-arg call, or a call whose sole argument isn't a bare field
168
+ * member, is left unstamped rather than guessed at;
102
169
  * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
103
170
  * name, `data-agent-field-state="unresolved"` (a positive "this is
104
171
  * row-scoped data, but which field is unknown" signal — never silent).
@@ -127,6 +194,13 @@ import { types, PluginObj } from '@babel/core';
127
194
  * that is itself slice-rooted gets discovered and stamped independently by
128
195
  * the same Program-wide `.map()` scan, with its own entity/row identity.
129
196
  *
197
+ * A JSXExpressionContainer wrapping `cond && <JSX/>` / `cond || <JSX/>` / a
198
+ * ternary IS a structural child, though (ARCH-1065 M-COV FIX, devtools
199
+ * 0.0.29, see `collectConditionalJsxBranchPaths`): the walker descends into
200
+ * whichever branch(es) are themselves a JSXElement, with the SAME scope —
201
+ * purely structural, the guard/test is never duplicated or stamped, only the
202
+ * JSX it conditionally renders is walked for further fields.
203
+ *
130
204
  * ── Failure posture ─────────────────────────────────────────────────────
131
205
  *
132
206
  * Every resolution step is an explicit allowlist; anything the resolver is
@@ -148,6 +222,27 @@ import { types, PluginObj } from '@babel/core';
148
222
  interface CollectionBindingPluginOptions {
149
223
  /** Skip files matching this pattern. @default /node_modules/ */
150
224
  fileExclude?: RegExp;
225
+ /**
226
+ * Component names known to forward unrecognized JSX attributes onto their
227
+ * own root DOM element (e.g. via `{...rest}`) — the exact set that makes
228
+ * a component usage a valid attribute CARRIER (row-root or leaf) even
229
+ * though it's opaque to the AST (see the "Forwarding-component allowlist
230
+ * descent" section above).
231
+ *
232
+ * Sourced from kit-manifest's `forwardsDataAttributes` flags by the kit
233
+ * build (`build-d.mjs` passes it) — this plugin NEVER hardcodes a default
234
+ * list here; that would be a shadow copy of kit-manifest's own truth, and
235
+ * would drift the moment a kit component's forwarding behavior changed
236
+ * without this plugin's knowledge. Synthetic lists are fine in tests, to
237
+ * keep the plugin testable standalone.
238
+ *
239
+ * Absent or empty = today's fail-closed behavior: no component ever
240
+ * carries a stamp, byte-identical to the plugin before this option
241
+ * existed.
242
+ *
243
+ * @default []
244
+ */
245
+ forwardingComponents?: string[];
151
246
  }
152
247
  declare function babelPluginCollectionBinding({ types: t }: {
153
248
  types: typeof types;
@@ -45,9 +45,11 @@ var ATTR_COLLECTION_ORDER = "data-agent-collection-order";
45
45
  var SLICE_ROOT_NAME = "slice";
46
46
  var CHAIN_METHODS = /* @__PURE__ */ new Set(["filter", "slice", "sort", "map"]);
47
47
  var MAX_SOURCE_CHAIN_HOPS = 4;
48
+ var COMPARISON_OPERATORS = /* @__PURE__ */ new Set(["==", "!=", "===", "!==", "<", "<=", ">", ">="]);
48
49
  var MAX_PROJECTION_DEPTH = 3;
49
50
  function babelPluginCollectionBinding({ types: t }, opts = {}) {
50
- const { fileExclude = /node_modules/ } = opts;
51
+ const { fileExclude = /node_modules/, forwardingComponents = [] } = opts;
52
+ const forwardingComponentSet = new Set(forwardingComponents);
51
53
  function hasOwnAttr(elementPath, name) {
52
54
  return elementPath.node.openingElement.attributes.some(
53
55
  (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name })
@@ -75,6 +77,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
75
77
  function isLowercaseTag(tag) {
76
78
  return tag.length > 0 && tag[0] === tag[0].toLowerCase();
77
79
  }
80
+ function canCarryFieldStamp(elementPath) {
81
+ const tag = getTagName(elementPath.node.openingElement);
82
+ return isLowercaseTag(tag) || forwardingComponentSet.has(tag);
83
+ }
78
84
  function getAttrName(attr) {
79
85
  return t.isJSXIdentifier(attr.name) ? attr.name.name : `${attr.name.namespace.name}:${attr.name.name.name}`;
80
86
  }
@@ -87,44 +93,72 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
87
93
  if (t.isIdentifier(callee, { name: "useMemo" })) return true;
88
94
  return t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property, { name: "useMemo" });
89
95
  }
90
- function resolveSliceSource(expr, scope, hops, hasSort) {
91
- if (t.isCallExpression(expr)) {
92
- const callee = expr.callee;
93
- if (!t.isExpression(callee)) return null;
94
- if (isUseMemoCallee(callee)) {
95
- const [factory] = expr.arguments;
96
- if (!factory || !t.isArrowFunctionExpression(factory)) return null;
97
- if (!t.isExpression(factory.body)) return null;
98
- return resolveSliceSource(factory.body, scope, hops, hasSort);
96
+ function findSingleReturnArgument(blockPath) {
97
+ const returnPaths = [];
98
+ blockPath.traverse({
99
+ Function(inner) {
100
+ inner.skip();
101
+ },
102
+ ReturnStatement(rp) {
103
+ returnPaths.push(rp);
99
104
  }
100
- if (t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property)) {
101
- const method = callee.property.name;
105
+ });
106
+ if (returnPaths.length !== 1) return null;
107
+ const argPath = returnPaths[0].get("argument");
108
+ if (Array.isArray(argPath) || !argPath.node || !argPath.isExpression()) return null;
109
+ return argPath;
110
+ }
111
+ function resolveSliceSource(path, hops, hasSort) {
112
+ if (path.isCallExpression()) {
113
+ const calleePath = path.get("callee");
114
+ if (Array.isArray(calleePath) || !calleePath.isExpression()) return null;
115
+ if (isUseMemoCallee(calleePath.node)) {
116
+ const argPaths = path.get("arguments");
117
+ const factoryPath = argPaths[0];
118
+ if (!factoryPath || !factoryPath.isArrowFunctionExpression()) return null;
119
+ const bodyPath = factoryPath.get("body");
120
+ if (Array.isArray(bodyPath)) return null;
121
+ if (bodyPath.isExpression()) {
122
+ return resolveSliceSource(bodyPath, hops, hasSort);
123
+ }
124
+ if (bodyPath.isBlockStatement()) {
125
+ const returnArgPath = findSingleReturnArgument(bodyPath);
126
+ if (!returnArgPath) return null;
127
+ return resolveSliceSource(returnArgPath, hops, hasSort);
128
+ }
129
+ return null;
130
+ }
131
+ const calleeNode = calleePath.node;
132
+ if (t.isMemberExpression(calleeNode) && !calleeNode.computed && t.isIdentifier(calleeNode.property)) {
133
+ const method = calleeNode.property.name;
102
134
  if (!CHAIN_METHODS.has(method)) return null;
103
- if (!t.isExpression(callee.object)) return null;
104
- return resolveSliceSource(callee.object, scope, hops, hasSort || method === "sort");
135
+ const objectPath = calleePath.get("object");
136
+ if (Array.isArray(objectPath) || !objectPath.isExpression()) return null;
137
+ return resolveSliceSource(objectPath, hops, hasSort || method === "sort");
105
138
  }
106
139
  return null;
107
140
  }
108
- if (t.isMemberExpression(expr)) {
109
- if (!t.isIdentifier(expr.object, { name: SLICE_ROOT_NAME })) return null;
110
- if (!isPageSliceBinding(scope)) return null;
111
- if (!expr.computed && t.isIdentifier(expr.property)) {
112
- return { entity: expr.property.name, hasSort };
141
+ if (path.isMemberExpression()) {
142
+ const { object, property, computed } = path.node;
143
+ if (!t.isIdentifier(object, { name: SLICE_ROOT_NAME })) return null;
144
+ if (!isPageSliceBinding(path.scope)) return null;
145
+ if (!computed && t.isIdentifier(property)) {
146
+ return { entity: property.name, hasSort };
113
147
  }
114
- if (expr.computed && t.isStringLiteral(expr.property)) {
115
- return { entity: expr.property.value, hasSort };
148
+ if (computed && t.isStringLiteral(property)) {
149
+ return { entity: property.value, hasSort };
116
150
  }
117
151
  return null;
118
152
  }
119
- if (t.isIdentifier(expr)) {
153
+ if (path.isIdentifier()) {
120
154
  if (hops >= MAX_SOURCE_CHAIN_HOPS) return null;
121
- const binding = scope.getBinding(expr.name);
155
+ const binding = path.scope.getBinding(path.node.name);
122
156
  if (!binding || !binding.constant) return null;
123
157
  const declarator = binding.path;
124
158
  if (!declarator.isVariableDeclarator()) return null;
125
- const init = declarator.node.init;
126
- if (!init || !t.isExpression(init)) return null;
127
- return resolveSliceSource(init, declarator.scope, hops + 1, hasSort);
159
+ const initPath = declarator.get("init");
160
+ if (Array.isArray(initPath) || !initPath.node || !initPath.isExpression()) return null;
161
+ return resolveSliceSource(initPath, hops + 1, hasSort);
128
162
  }
129
163
  return null;
130
164
  }
@@ -262,11 +296,27 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
262
296
  const field = fieldNameForBranch(expr.left, scope);
263
297
  return field ? { kind: "static", field } : null;
264
298
  }
299
+ function fieldNameForLiteralBranchTest(expr, scope) {
300
+ const direct = fieldNameForBranch(expr, scope);
301
+ if (direct) return direct;
302
+ if (t.isBinaryExpression(expr) && COMPARISON_OPERATORS.has(expr.operator)) {
303
+ if (t.isExpression(expr.left)) {
304
+ const leftField = fieldNameForBranch(expr.left, scope);
305
+ if (leftField) return leftField;
306
+ }
307
+ if (t.isExpression(expr.right)) {
308
+ const rightField = fieldNameForBranch(expr.right, scope);
309
+ if (rightField) return rightField;
310
+ }
311
+ }
312
+ return null;
313
+ }
265
314
  function classifyConditional(expr, scope) {
266
315
  const { test, consequent, alternate } = expr;
267
316
  if (!t.isExpression(test) || !t.isExpression(consequent) || !t.isExpression(alternate)) return null;
268
317
  if (isStaticLiteralExpr(consequent) && isStaticLiteralExpr(alternate)) {
269
- const testField = fieldNameForBranch(test, scope);
318
+ if (containsEvaluation(test)) return null;
319
+ const testField = fieldNameForLiteralBranchTest(test, scope);
270
320
  return testField ? { kind: "static", field: testField } : null;
271
321
  }
272
322
  const consField = fieldNameForBranch(consequent, scope);
@@ -279,6 +329,14 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
279
329
  expr: t.conditionalExpression(t.cloneNode(test), t.stringLiteral(consField), t.stringLiteral(altField))
280
330
  };
281
331
  }
332
+ function classifyFormatterCall(expr, scope) {
333
+ if (expr.arguments.length !== 1) return null;
334
+ const [arg] = expr.arguments;
335
+ if (!t.isMemberExpression(arg) && !t.isOptionalMemberExpression(arg)) return null;
336
+ const verdict = classifyMemberField(arg, scope);
337
+ if (!verdict || verdict.kind !== "static") return null;
338
+ return { kind: "formatted", field: verdict.field };
339
+ }
282
340
  function classifyFieldExpr(expr, scope) {
283
341
  if (t.isIdentifier(expr)) {
284
342
  const binding = scope.identifiers.get(expr.name);
@@ -290,6 +348,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
290
348
  if (t.isLogicalExpression(expr) && expr.operator === "??") return classifyNullishChain(expr, scope);
291
349
  if (t.isLogicalExpression(expr) && expr.operator === "||") return classifySimpleLogicalOr(expr, scope);
292
350
  if (t.isConditionalExpression(expr)) return classifyConditional(expr, scope);
351
+ if (t.isCallExpression(expr)) return classifyFormatterCall(expr, scope);
293
352
  return null;
294
353
  }
295
354
  function applyFieldVerdict(elementPath, verdict) {
@@ -302,6 +361,12 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
302
361
  addAttr(elementPath, ATTR_FIELD, t.stringLiteral(verdict.field));
303
362
  return;
304
363
  }
364
+ if (verdict.kind === "formatted") {
365
+ addAttr(elementPath, ATTR_FIELD, t.stringLiteral(verdict.field));
366
+ addAttr(elementPath, ATTR_FIELD_STATE, t.stringLiteral("rendered"));
367
+ addAttr(elementPath, ATTR_FIELD_PARTS, t.stringLiteral(verdict.field));
368
+ return;
369
+ }
305
370
  addAttr(elementPath, ATTR_FIELD, t.jsxExpressionContainer(verdict.expr));
306
371
  }
307
372
  function meaningfulChildPaths(elementPath) {
@@ -324,11 +389,13 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
324
389
  }
325
390
  function stampPureLeaf(elementPath, expr, scope) {
326
391
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
392
+ if (!canCarryFieldStamp(elementPath)) return;
327
393
  if (t.isJSXEmptyExpression(expr) || !t.isExpression(expr)) return;
328
394
  applyFieldVerdict(elementPath, classifyFieldExpr(expr, scope));
329
395
  }
330
396
  function stampLabelValueOrMixed(elementPath, childPaths, scope) {
331
397
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
398
+ if (!canCarryFieldStamp(elementPath)) return;
332
399
  const hasInvisibleSibling = childPaths.some(
333
400
  (p) => p.isJSXExpressionContainer() && t.isExpression(p.node.expression) && isInvisibleRenderExpr(p.node.expression)
334
401
  );
@@ -432,6 +499,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
432
499
  function resolveRowRoot(jsxPath, scope, depth) {
433
500
  const tag = getTagName(jsxPath.node.openingElement);
434
501
  if (isLowercaseTag(tag)) return { hostPath: jsxPath, scope };
502
+ if (forwardingComponentSet.has(tag)) return { hostPath: jsxPath, scope };
435
503
  if (depth >= MAX_PROJECTION_DEPTH) return null;
436
504
  const fnPath = resolveComponentDefinition(tag, jsxPath);
437
505
  if (!fnPath) return null;
@@ -477,13 +545,42 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
477
545
  if (!innerReturn) return null;
478
546
  return resolveFieldProjection(innerReturn, { identifiers }, depth + 1);
479
547
  }
548
+ function collectConditionalJsxBranchPaths(containerPath) {
549
+ const exprPath = containerPath.get("expression");
550
+ if (Array.isArray(exprPath)) return [];
551
+ if (exprPath.isLogicalExpression() && (exprPath.node.operator === "&&" || exprPath.node.operator === "||")) {
552
+ const rightPath = exprPath.get("right");
553
+ return !Array.isArray(rightPath) && rightPath.isJSXElement() ? [rightPath] : [];
554
+ }
555
+ if (exprPath.isConditionalExpression()) {
556
+ const branches = [];
557
+ const consPath = exprPath.get("consequent");
558
+ const altPath = exprPath.get("alternate");
559
+ if (!Array.isArray(consPath) && consPath.isJSXElement()) branches.push(consPath);
560
+ if (!Array.isArray(altPath) && altPath.isJSXElement()) branches.push(altPath);
561
+ return branches;
562
+ }
563
+ return [];
564
+ }
480
565
  function walkFieldsWithinRow(elementPath, scope) {
481
566
  const childPaths = meaningfulChildPaths(elementPath);
482
- const hasElementChild = childPaths.some((p) => p.isJSXElement());
567
+ const conditionalBranchesByChild = /* @__PURE__ */ new Map();
568
+ for (const p of childPaths) {
569
+ if (!p.isJSXExpressionContainer()) continue;
570
+ const branches = collectConditionalJsxBranchPaths(p);
571
+ if (branches.length > 0) conditionalBranchesByChild.set(p, branches);
572
+ }
573
+ const hasElementChild = childPaths.some((p) => p.isJSXElement() || conditionalBranchesByChild.has(p));
483
574
  if (hasElementChild) {
484
575
  for (const childPath of childPaths) {
485
- if (!childPath.isJSXElement()) continue;
486
- handleStructuralChild(childPath, scope);
576
+ if (childPath.isJSXElement()) {
577
+ handleStructuralChild(childPath, scope);
578
+ continue;
579
+ }
580
+ const branches = conditionalBranchesByChild.get(childPath);
581
+ if (branches) {
582
+ for (const branchPath of branches) handleStructuralChild(branchPath, scope);
583
+ }
487
584
  }
488
585
  return;
489
586
  }
@@ -503,6 +600,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
503
600
  walkFieldsWithinRow(childPath, scope);
504
601
  return;
505
602
  }
603
+ if (meaningfulChildPaths(childPath).length > 0) {
604
+ walkFieldsWithinRow(childPath, scope);
605
+ return;
606
+ }
506
607
  const projected = resolveFieldProjection(childPath, scope, 0);
507
608
  if (!projected) return;
508
609
  walkFieldsWithinRow(projected.hostPath, projected.scope);
@@ -513,7 +614,11 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
513
614
  if (!t.isMemberExpression(callee) || callee.computed) return;
514
615
  if (!t.isIdentifier(callee.property, { name: "map" })) return;
515
616
  if (!t.isExpression(callee.object)) return;
516
- const source = resolveSliceSource(callee.object, callPath.scope, 0, false);
617
+ const calleePath = callPath.get("callee");
618
+ if (Array.isArray(calleePath) || !calleePath.isMemberExpression()) return;
619
+ const objectPath = calleePath.get("object");
620
+ if (Array.isArray(objectPath) || !objectPath.isExpression()) return;
621
+ const source = resolveSliceSource(objectPath, 0, false);
517
622
  if (!source) return;
518
623
  const [callback] = node.arguments;
519
624
  if (!callback || !t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) return;
@@ -14,9 +14,11 @@ var ATTR_COLLECTION_ORDER = "data-agent-collection-order";
14
14
  var SLICE_ROOT_NAME = "slice";
15
15
  var CHAIN_METHODS = /* @__PURE__ */ new Set(["filter", "slice", "sort", "map"]);
16
16
  var MAX_SOURCE_CHAIN_HOPS = 4;
17
+ var COMPARISON_OPERATORS = /* @__PURE__ */ new Set(["==", "!=", "===", "!==", "<", "<=", ">", ">="]);
17
18
  var MAX_PROJECTION_DEPTH = 3;
18
19
  function babelPluginCollectionBinding({ types: t }, opts = {}) {
19
- const { fileExclude = /node_modules/ } = opts;
20
+ const { fileExclude = /node_modules/, forwardingComponents = [] } = opts;
21
+ const forwardingComponentSet = new Set(forwardingComponents);
20
22
  function hasOwnAttr(elementPath, name) {
21
23
  return elementPath.node.openingElement.attributes.some(
22
24
  (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name })
@@ -44,6 +46,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
44
46
  function isLowercaseTag(tag) {
45
47
  return tag.length > 0 && tag[0] === tag[0].toLowerCase();
46
48
  }
49
+ function canCarryFieldStamp(elementPath) {
50
+ const tag = getTagName(elementPath.node.openingElement);
51
+ return isLowercaseTag(tag) || forwardingComponentSet.has(tag);
52
+ }
47
53
  function getAttrName(attr) {
48
54
  return t.isJSXIdentifier(attr.name) ? attr.name.name : `${attr.name.namespace.name}:${attr.name.name.name}`;
49
55
  }
@@ -56,44 +62,72 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
56
62
  if (t.isIdentifier(callee, { name: "useMemo" })) return true;
57
63
  return t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property, { name: "useMemo" });
58
64
  }
59
- function resolveSliceSource(expr, scope, hops, hasSort) {
60
- if (t.isCallExpression(expr)) {
61
- const callee = expr.callee;
62
- if (!t.isExpression(callee)) return null;
63
- if (isUseMemoCallee(callee)) {
64
- const [factory] = expr.arguments;
65
- if (!factory || !t.isArrowFunctionExpression(factory)) return null;
66
- if (!t.isExpression(factory.body)) return null;
67
- return resolveSliceSource(factory.body, scope, hops, hasSort);
65
+ function findSingleReturnArgument(blockPath) {
66
+ const returnPaths = [];
67
+ blockPath.traverse({
68
+ Function(inner) {
69
+ inner.skip();
70
+ },
71
+ ReturnStatement(rp) {
72
+ returnPaths.push(rp);
68
73
  }
69
- if (t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property)) {
70
- const method = callee.property.name;
74
+ });
75
+ if (returnPaths.length !== 1) return null;
76
+ const argPath = returnPaths[0].get("argument");
77
+ if (Array.isArray(argPath) || !argPath.node || !argPath.isExpression()) return null;
78
+ return argPath;
79
+ }
80
+ function resolveSliceSource(path, hops, hasSort) {
81
+ if (path.isCallExpression()) {
82
+ const calleePath = path.get("callee");
83
+ if (Array.isArray(calleePath) || !calleePath.isExpression()) return null;
84
+ if (isUseMemoCallee(calleePath.node)) {
85
+ const argPaths = path.get("arguments");
86
+ const factoryPath = argPaths[0];
87
+ if (!factoryPath || !factoryPath.isArrowFunctionExpression()) return null;
88
+ const bodyPath = factoryPath.get("body");
89
+ if (Array.isArray(bodyPath)) return null;
90
+ if (bodyPath.isExpression()) {
91
+ return resolveSliceSource(bodyPath, hops, hasSort);
92
+ }
93
+ if (bodyPath.isBlockStatement()) {
94
+ const returnArgPath = findSingleReturnArgument(bodyPath);
95
+ if (!returnArgPath) return null;
96
+ return resolveSliceSource(returnArgPath, hops, hasSort);
97
+ }
98
+ return null;
99
+ }
100
+ const calleeNode = calleePath.node;
101
+ if (t.isMemberExpression(calleeNode) && !calleeNode.computed && t.isIdentifier(calleeNode.property)) {
102
+ const method = calleeNode.property.name;
71
103
  if (!CHAIN_METHODS.has(method)) return null;
72
- if (!t.isExpression(callee.object)) return null;
73
- return resolveSliceSource(callee.object, scope, hops, hasSort || method === "sort");
104
+ const objectPath = calleePath.get("object");
105
+ if (Array.isArray(objectPath) || !objectPath.isExpression()) return null;
106
+ return resolveSliceSource(objectPath, hops, hasSort || method === "sort");
74
107
  }
75
108
  return null;
76
109
  }
77
- if (t.isMemberExpression(expr)) {
78
- if (!t.isIdentifier(expr.object, { name: SLICE_ROOT_NAME })) return null;
79
- if (!isPageSliceBinding(scope)) return null;
80
- if (!expr.computed && t.isIdentifier(expr.property)) {
81
- return { entity: expr.property.name, hasSort };
110
+ if (path.isMemberExpression()) {
111
+ const { object, property, computed } = path.node;
112
+ if (!t.isIdentifier(object, { name: SLICE_ROOT_NAME })) return null;
113
+ if (!isPageSliceBinding(path.scope)) return null;
114
+ if (!computed && t.isIdentifier(property)) {
115
+ return { entity: property.name, hasSort };
82
116
  }
83
- if (expr.computed && t.isStringLiteral(expr.property)) {
84
- return { entity: expr.property.value, hasSort };
117
+ if (computed && t.isStringLiteral(property)) {
118
+ return { entity: property.value, hasSort };
85
119
  }
86
120
  return null;
87
121
  }
88
- if (t.isIdentifier(expr)) {
122
+ if (path.isIdentifier()) {
89
123
  if (hops >= MAX_SOURCE_CHAIN_HOPS) return null;
90
- const binding = scope.getBinding(expr.name);
124
+ const binding = path.scope.getBinding(path.node.name);
91
125
  if (!binding || !binding.constant) return null;
92
126
  const declarator = binding.path;
93
127
  if (!declarator.isVariableDeclarator()) return null;
94
- const init = declarator.node.init;
95
- if (!init || !t.isExpression(init)) return null;
96
- return resolveSliceSource(init, declarator.scope, hops + 1, hasSort);
128
+ const initPath = declarator.get("init");
129
+ if (Array.isArray(initPath) || !initPath.node || !initPath.isExpression()) return null;
130
+ return resolveSliceSource(initPath, hops + 1, hasSort);
97
131
  }
98
132
  return null;
99
133
  }
@@ -231,11 +265,27 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
231
265
  const field = fieldNameForBranch(expr.left, scope);
232
266
  return field ? { kind: "static", field } : null;
233
267
  }
268
+ function fieldNameForLiteralBranchTest(expr, scope) {
269
+ const direct = fieldNameForBranch(expr, scope);
270
+ if (direct) return direct;
271
+ if (t.isBinaryExpression(expr) && COMPARISON_OPERATORS.has(expr.operator)) {
272
+ if (t.isExpression(expr.left)) {
273
+ const leftField = fieldNameForBranch(expr.left, scope);
274
+ if (leftField) return leftField;
275
+ }
276
+ if (t.isExpression(expr.right)) {
277
+ const rightField = fieldNameForBranch(expr.right, scope);
278
+ if (rightField) return rightField;
279
+ }
280
+ }
281
+ return null;
282
+ }
234
283
  function classifyConditional(expr, scope) {
235
284
  const { test, consequent, alternate } = expr;
236
285
  if (!t.isExpression(test) || !t.isExpression(consequent) || !t.isExpression(alternate)) return null;
237
286
  if (isStaticLiteralExpr(consequent) && isStaticLiteralExpr(alternate)) {
238
- const testField = fieldNameForBranch(test, scope);
287
+ if (containsEvaluation(test)) return null;
288
+ const testField = fieldNameForLiteralBranchTest(test, scope);
239
289
  return testField ? { kind: "static", field: testField } : null;
240
290
  }
241
291
  const consField = fieldNameForBranch(consequent, scope);
@@ -248,6 +298,14 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
248
298
  expr: t.conditionalExpression(t.cloneNode(test), t.stringLiteral(consField), t.stringLiteral(altField))
249
299
  };
250
300
  }
301
+ function classifyFormatterCall(expr, scope) {
302
+ if (expr.arguments.length !== 1) return null;
303
+ const [arg] = expr.arguments;
304
+ if (!t.isMemberExpression(arg) && !t.isOptionalMemberExpression(arg)) return null;
305
+ const verdict = classifyMemberField(arg, scope);
306
+ if (!verdict || verdict.kind !== "static") return null;
307
+ return { kind: "formatted", field: verdict.field };
308
+ }
251
309
  function classifyFieldExpr(expr, scope) {
252
310
  if (t.isIdentifier(expr)) {
253
311
  const binding = scope.identifiers.get(expr.name);
@@ -259,6 +317,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
259
317
  if (t.isLogicalExpression(expr) && expr.operator === "??") return classifyNullishChain(expr, scope);
260
318
  if (t.isLogicalExpression(expr) && expr.operator === "||") return classifySimpleLogicalOr(expr, scope);
261
319
  if (t.isConditionalExpression(expr)) return classifyConditional(expr, scope);
320
+ if (t.isCallExpression(expr)) return classifyFormatterCall(expr, scope);
262
321
  return null;
263
322
  }
264
323
  function applyFieldVerdict(elementPath, verdict) {
@@ -271,6 +330,12 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
271
330
  addAttr(elementPath, ATTR_FIELD, t.stringLiteral(verdict.field));
272
331
  return;
273
332
  }
333
+ if (verdict.kind === "formatted") {
334
+ addAttr(elementPath, ATTR_FIELD, t.stringLiteral(verdict.field));
335
+ addAttr(elementPath, ATTR_FIELD_STATE, t.stringLiteral("rendered"));
336
+ addAttr(elementPath, ATTR_FIELD_PARTS, t.stringLiteral(verdict.field));
337
+ return;
338
+ }
274
339
  addAttr(elementPath, ATTR_FIELD, t.jsxExpressionContainer(verdict.expr));
275
340
  }
276
341
  function meaningfulChildPaths(elementPath) {
@@ -293,11 +358,13 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
293
358
  }
294
359
  function stampPureLeaf(elementPath, expr, scope) {
295
360
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
361
+ if (!canCarryFieldStamp(elementPath)) return;
296
362
  if (t.isJSXEmptyExpression(expr) || !t.isExpression(expr)) return;
297
363
  applyFieldVerdict(elementPath, classifyFieldExpr(expr, scope));
298
364
  }
299
365
  function stampLabelValueOrMixed(elementPath, childPaths, scope) {
300
366
  if (hasOwnAttr(elementPath, ATTR_FIELD) || hasOwnAttr(elementPath, ATTR_FIELD_STATE)) return;
367
+ if (!canCarryFieldStamp(elementPath)) return;
301
368
  const hasInvisibleSibling = childPaths.some(
302
369
  (p) => p.isJSXExpressionContainer() && t.isExpression(p.node.expression) && isInvisibleRenderExpr(p.node.expression)
303
370
  );
@@ -401,6 +468,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
401
468
  function resolveRowRoot(jsxPath, scope, depth) {
402
469
  const tag = getTagName(jsxPath.node.openingElement);
403
470
  if (isLowercaseTag(tag)) return { hostPath: jsxPath, scope };
471
+ if (forwardingComponentSet.has(tag)) return { hostPath: jsxPath, scope };
404
472
  if (depth >= MAX_PROJECTION_DEPTH) return null;
405
473
  const fnPath = resolveComponentDefinition(tag, jsxPath);
406
474
  if (!fnPath) return null;
@@ -446,13 +514,42 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
446
514
  if (!innerReturn) return null;
447
515
  return resolveFieldProjection(innerReturn, { identifiers }, depth + 1);
448
516
  }
517
+ function collectConditionalJsxBranchPaths(containerPath) {
518
+ const exprPath = containerPath.get("expression");
519
+ if (Array.isArray(exprPath)) return [];
520
+ if (exprPath.isLogicalExpression() && (exprPath.node.operator === "&&" || exprPath.node.operator === "||")) {
521
+ const rightPath = exprPath.get("right");
522
+ return !Array.isArray(rightPath) && rightPath.isJSXElement() ? [rightPath] : [];
523
+ }
524
+ if (exprPath.isConditionalExpression()) {
525
+ const branches = [];
526
+ const consPath = exprPath.get("consequent");
527
+ const altPath = exprPath.get("alternate");
528
+ if (!Array.isArray(consPath) && consPath.isJSXElement()) branches.push(consPath);
529
+ if (!Array.isArray(altPath) && altPath.isJSXElement()) branches.push(altPath);
530
+ return branches;
531
+ }
532
+ return [];
533
+ }
449
534
  function walkFieldsWithinRow(elementPath, scope) {
450
535
  const childPaths = meaningfulChildPaths(elementPath);
451
- const hasElementChild = childPaths.some((p) => p.isJSXElement());
536
+ const conditionalBranchesByChild = /* @__PURE__ */ new Map();
537
+ for (const p of childPaths) {
538
+ if (!p.isJSXExpressionContainer()) continue;
539
+ const branches = collectConditionalJsxBranchPaths(p);
540
+ if (branches.length > 0) conditionalBranchesByChild.set(p, branches);
541
+ }
542
+ const hasElementChild = childPaths.some((p) => p.isJSXElement() || conditionalBranchesByChild.has(p));
452
543
  if (hasElementChild) {
453
544
  for (const childPath of childPaths) {
454
- if (!childPath.isJSXElement()) continue;
455
- handleStructuralChild(childPath, scope);
545
+ if (childPath.isJSXElement()) {
546
+ handleStructuralChild(childPath, scope);
547
+ continue;
548
+ }
549
+ const branches = conditionalBranchesByChild.get(childPath);
550
+ if (branches) {
551
+ for (const branchPath of branches) handleStructuralChild(branchPath, scope);
552
+ }
456
553
  }
457
554
  return;
458
555
  }
@@ -472,6 +569,10 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
472
569
  walkFieldsWithinRow(childPath, scope);
473
570
  return;
474
571
  }
572
+ if (meaningfulChildPaths(childPath).length > 0) {
573
+ walkFieldsWithinRow(childPath, scope);
574
+ return;
575
+ }
475
576
  const projected = resolveFieldProjection(childPath, scope, 0);
476
577
  if (!projected) return;
477
578
  walkFieldsWithinRow(projected.hostPath, projected.scope);
@@ -482,7 +583,11 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
482
583
  if (!t.isMemberExpression(callee) || callee.computed) return;
483
584
  if (!t.isIdentifier(callee.property, { name: "map" })) return;
484
585
  if (!t.isExpression(callee.object)) return;
485
- const source = resolveSliceSource(callee.object, callPath.scope, 0, false);
586
+ const calleePath = callPath.get("callee");
587
+ if (Array.isArray(calleePath) || !calleePath.isMemberExpression()) return;
588
+ const objectPath = calleePath.get("object");
589
+ if (Array.isArray(objectPath) || !objectPath.isExpression()) return;
590
+ const source = resolveSliceSource(objectPath, 0, false);
486
591
  if (!source) return;
487
592
  const [callback] = node.arguments;
488
593
  if (!callback || !t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archie/devtools",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "repository": {
6
6
  "type": "git",