@archie/devtools 0.0.28 → 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`
@@ -139,10 +147,25 @@ import { types, PluginObj } from '@babel/core';
139
147
  * the duplicated test would double-evaluate it across SSR and hydration,
140
148
  * so the whole leaf is left unstamped instead;
141
149
  * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
142
- * member (a boolean-typed field driving two literal branches) → static
143
- * field named by `cond` — the FE's typed controls (D7, a different
144
- * mechanism, M3) read the boolean's true/false rendering from
145
- * `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;
146
169
  * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
147
170
  * name, `data-agent-field-state="unresolved"` (a positive "this is
148
171
  * row-scoped data, but which field is unknown" signal — never silent).
@@ -171,6 +194,13 @@ import { types, PluginObj } from '@babel/core';
171
194
  * that is itself slice-rooted gets discovered and stamped independently by
172
195
  * the same Program-wide `.map()` scan, with its own entity/row identity.
173
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
+ *
174
204
  * ── Failure posture ─────────────────────────────────────────────────────
175
205
  *
176
206
  * Every resolution step is an explicit allowlist; anything the resolver is
@@ -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`
@@ -139,10 +147,25 @@ import { types, PluginObj } from '@babel/core';
139
147
  * the duplicated test would double-evaluate it across SSR and hydration,
140
148
  * so the whole leaf is left unstamped instead;
141
149
  * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
142
- * member (a boolean-typed field driving two literal branches) → static
143
- * field named by `cond` — the FE's typed controls (D7, a different
144
- * mechanism, M3) read the boolean's true/false rendering from
145
- * `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;
146
169
  * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
147
170
  * name, `data-agent-field-state="unresolved"` (a positive "this is
148
171
  * row-scoped data, but which field is unknown" signal — never silent).
@@ -171,6 +194,13 @@ import { types, PluginObj } from '@babel/core';
171
194
  * that is itself slice-rooted gets discovered and stamped independently by
172
195
  * the same Program-wide `.map()` scan, with its own entity/row identity.
173
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
+ *
174
204
  * ── Failure posture ─────────────────────────────────────────────────────
175
205
  *
176
206
  * Every resolution step is an explicit allowlist; anything the resolver is
@@ -45,6 +45,7 @@ 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
51
  const { fileExclude = /node_modules/, forwardingComponents = [] } = opts;
@@ -92,44 +93,72 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
92
93
  if (t.isIdentifier(callee, { name: "useMemo" })) return true;
93
94
  return t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property, { name: "useMemo" });
94
95
  }
95
- function resolveSliceSource(expr, scope, hops, hasSort) {
96
- if (t.isCallExpression(expr)) {
97
- const callee = expr.callee;
98
- if (!t.isExpression(callee)) return null;
99
- if (isUseMemoCallee(callee)) {
100
- const [factory] = expr.arguments;
101
- if (!factory || !t.isArrowFunctionExpression(factory)) return null;
102
- if (!t.isExpression(factory.body)) return null;
103
- 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);
104
104
  }
105
- if (t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property)) {
106
- 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;
107
134
  if (!CHAIN_METHODS.has(method)) return null;
108
- if (!t.isExpression(callee.object)) return null;
109
- 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");
110
138
  }
111
139
  return null;
112
140
  }
113
- if (t.isMemberExpression(expr)) {
114
- if (!t.isIdentifier(expr.object, { name: SLICE_ROOT_NAME })) return null;
115
- if (!isPageSliceBinding(scope)) return null;
116
- if (!expr.computed && t.isIdentifier(expr.property)) {
117
- 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 };
118
147
  }
119
- if (expr.computed && t.isStringLiteral(expr.property)) {
120
- return { entity: expr.property.value, hasSort };
148
+ if (computed && t.isStringLiteral(property)) {
149
+ return { entity: property.value, hasSort };
121
150
  }
122
151
  return null;
123
152
  }
124
- if (t.isIdentifier(expr)) {
153
+ if (path.isIdentifier()) {
125
154
  if (hops >= MAX_SOURCE_CHAIN_HOPS) return null;
126
- const binding = scope.getBinding(expr.name);
155
+ const binding = path.scope.getBinding(path.node.name);
127
156
  if (!binding || !binding.constant) return null;
128
157
  const declarator = binding.path;
129
158
  if (!declarator.isVariableDeclarator()) return null;
130
- const init = declarator.node.init;
131
- if (!init || !t.isExpression(init)) return null;
132
- 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);
133
162
  }
134
163
  return null;
135
164
  }
@@ -267,11 +296,27 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
267
296
  const field = fieldNameForBranch(expr.left, scope);
268
297
  return field ? { kind: "static", field } : null;
269
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
+ }
270
314
  function classifyConditional(expr, scope) {
271
315
  const { test, consequent, alternate } = expr;
272
316
  if (!t.isExpression(test) || !t.isExpression(consequent) || !t.isExpression(alternate)) return null;
273
317
  if (isStaticLiteralExpr(consequent) && isStaticLiteralExpr(alternate)) {
274
- const testField = fieldNameForBranch(test, scope);
318
+ if (containsEvaluation(test)) return null;
319
+ const testField = fieldNameForLiteralBranchTest(test, scope);
275
320
  return testField ? { kind: "static", field: testField } : null;
276
321
  }
277
322
  const consField = fieldNameForBranch(consequent, scope);
@@ -284,6 +329,14 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
284
329
  expr: t.conditionalExpression(t.cloneNode(test), t.stringLiteral(consField), t.stringLiteral(altField))
285
330
  };
286
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
+ }
287
340
  function classifyFieldExpr(expr, scope) {
288
341
  if (t.isIdentifier(expr)) {
289
342
  const binding = scope.identifiers.get(expr.name);
@@ -295,6 +348,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
295
348
  if (t.isLogicalExpression(expr) && expr.operator === "??") return classifyNullishChain(expr, scope);
296
349
  if (t.isLogicalExpression(expr) && expr.operator === "||") return classifySimpleLogicalOr(expr, scope);
297
350
  if (t.isConditionalExpression(expr)) return classifyConditional(expr, scope);
351
+ if (t.isCallExpression(expr)) return classifyFormatterCall(expr, scope);
298
352
  return null;
299
353
  }
300
354
  function applyFieldVerdict(elementPath, verdict) {
@@ -307,6 +361,12 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
307
361
  addAttr(elementPath, ATTR_FIELD, t.stringLiteral(verdict.field));
308
362
  return;
309
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
+ }
310
370
  addAttr(elementPath, ATTR_FIELD, t.jsxExpressionContainer(verdict.expr));
311
371
  }
312
372
  function meaningfulChildPaths(elementPath) {
@@ -485,13 +545,42 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
485
545
  if (!innerReturn) return null;
486
546
  return resolveFieldProjection(innerReturn, { identifiers }, depth + 1);
487
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
+ }
488
565
  function walkFieldsWithinRow(elementPath, scope) {
489
566
  const childPaths = meaningfulChildPaths(elementPath);
490
- 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));
491
574
  if (hasElementChild) {
492
575
  for (const childPath of childPaths) {
493
- if (!childPath.isJSXElement()) continue;
494
- 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
+ }
495
584
  }
496
585
  return;
497
586
  }
@@ -525,7 +614,11 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
525
614
  if (!t.isMemberExpression(callee) || callee.computed) return;
526
615
  if (!t.isIdentifier(callee.property, { name: "map" })) return;
527
616
  if (!t.isExpression(callee.object)) return;
528
- 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);
529
622
  if (!source) return;
530
623
  const [callback] = node.arguments;
531
624
  if (!callback || !t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) return;
@@ -14,6 +14,7 @@ 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
20
  const { fileExclude = /node_modules/, forwardingComponents = [] } = opts;
@@ -61,44 +62,72 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
61
62
  if (t.isIdentifier(callee, { name: "useMemo" })) return true;
62
63
  return t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property, { name: "useMemo" });
63
64
  }
64
- function resolveSliceSource(expr, scope, hops, hasSort) {
65
- if (t.isCallExpression(expr)) {
66
- const callee = expr.callee;
67
- if (!t.isExpression(callee)) return null;
68
- if (isUseMemoCallee(callee)) {
69
- const [factory] = expr.arguments;
70
- if (!factory || !t.isArrowFunctionExpression(factory)) return null;
71
- if (!t.isExpression(factory.body)) return null;
72
- 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);
73
73
  }
74
- if (t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property)) {
75
- 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;
76
103
  if (!CHAIN_METHODS.has(method)) return null;
77
- if (!t.isExpression(callee.object)) return null;
78
- 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");
79
107
  }
80
108
  return null;
81
109
  }
82
- if (t.isMemberExpression(expr)) {
83
- if (!t.isIdentifier(expr.object, { name: SLICE_ROOT_NAME })) return null;
84
- if (!isPageSliceBinding(scope)) return null;
85
- if (!expr.computed && t.isIdentifier(expr.property)) {
86
- 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 };
87
116
  }
88
- if (expr.computed && t.isStringLiteral(expr.property)) {
89
- return { entity: expr.property.value, hasSort };
117
+ if (computed && t.isStringLiteral(property)) {
118
+ return { entity: property.value, hasSort };
90
119
  }
91
120
  return null;
92
121
  }
93
- if (t.isIdentifier(expr)) {
122
+ if (path.isIdentifier()) {
94
123
  if (hops >= MAX_SOURCE_CHAIN_HOPS) return null;
95
- const binding = scope.getBinding(expr.name);
124
+ const binding = path.scope.getBinding(path.node.name);
96
125
  if (!binding || !binding.constant) return null;
97
126
  const declarator = binding.path;
98
127
  if (!declarator.isVariableDeclarator()) return null;
99
- const init = declarator.node.init;
100
- if (!init || !t.isExpression(init)) return null;
101
- 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);
102
131
  }
103
132
  return null;
104
133
  }
@@ -236,11 +265,27 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
236
265
  const field = fieldNameForBranch(expr.left, scope);
237
266
  return field ? { kind: "static", field } : null;
238
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
+ }
239
283
  function classifyConditional(expr, scope) {
240
284
  const { test, consequent, alternate } = expr;
241
285
  if (!t.isExpression(test) || !t.isExpression(consequent) || !t.isExpression(alternate)) return null;
242
286
  if (isStaticLiteralExpr(consequent) && isStaticLiteralExpr(alternate)) {
243
- const testField = fieldNameForBranch(test, scope);
287
+ if (containsEvaluation(test)) return null;
288
+ const testField = fieldNameForLiteralBranchTest(test, scope);
244
289
  return testField ? { kind: "static", field: testField } : null;
245
290
  }
246
291
  const consField = fieldNameForBranch(consequent, scope);
@@ -253,6 +298,14 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
253
298
  expr: t.conditionalExpression(t.cloneNode(test), t.stringLiteral(consField), t.stringLiteral(altField))
254
299
  };
255
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
+ }
256
309
  function classifyFieldExpr(expr, scope) {
257
310
  if (t.isIdentifier(expr)) {
258
311
  const binding = scope.identifiers.get(expr.name);
@@ -264,6 +317,7 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
264
317
  if (t.isLogicalExpression(expr) && expr.operator === "??") return classifyNullishChain(expr, scope);
265
318
  if (t.isLogicalExpression(expr) && expr.operator === "||") return classifySimpleLogicalOr(expr, scope);
266
319
  if (t.isConditionalExpression(expr)) return classifyConditional(expr, scope);
320
+ if (t.isCallExpression(expr)) return classifyFormatterCall(expr, scope);
267
321
  return null;
268
322
  }
269
323
  function applyFieldVerdict(elementPath, verdict) {
@@ -276,6 +330,12 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
276
330
  addAttr(elementPath, ATTR_FIELD, t.stringLiteral(verdict.field));
277
331
  return;
278
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
+ }
279
339
  addAttr(elementPath, ATTR_FIELD, t.jsxExpressionContainer(verdict.expr));
280
340
  }
281
341
  function meaningfulChildPaths(elementPath) {
@@ -454,13 +514,42 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
454
514
  if (!innerReturn) return null;
455
515
  return resolveFieldProjection(innerReturn, { identifiers }, depth + 1);
456
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
+ }
457
534
  function walkFieldsWithinRow(elementPath, scope) {
458
535
  const childPaths = meaningfulChildPaths(elementPath);
459
- 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));
460
543
  if (hasElementChild) {
461
544
  for (const childPath of childPaths) {
462
- if (!childPath.isJSXElement()) continue;
463
- 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
+ }
464
553
  }
465
554
  return;
466
555
  }
@@ -494,7 +583,11 @@ function babelPluginCollectionBinding({ types: t }, opts = {}) {
494
583
  if (!t.isMemberExpression(callee) || callee.computed) return;
495
584
  if (!t.isIdentifier(callee.property, { name: "map" })) return;
496
585
  if (!t.isExpression(callee.object)) return;
497
- 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);
498
591
  if (!source) return;
499
592
  const [callback] = node.arguments;
500
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.28",
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",