@archie/devtools 0.0.28 → 0.0.30

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;
@@ -615,6 +615,122 @@
615
615
  }
616
616
  }
617
617
 
618
+ // ARCH-1065 M3 (v2.3.0) — the nearest data-agent-row-bearing element
619
+ // SELF-outward. This is `nearestRowId` above but returning the ELEMENT
620
+ // (not just its id), because QUERY_COLLECTION_ORDER needs the row
621
+ // root's sibling `data-agent-entity`/`data-agent-collection-order`
622
+ // frame too, not only the row id. Nested collections resolve correctly
623
+ // "for free": walking self-outward finds the INNERMOST row root first,
624
+ // i.e. the nearest enclosing collection of the target node.
625
+ function nearestRowRootElement(el) {
626
+ var node = el
627
+ for (var depth = 0; node && depth < MAX_ANCHOR_CHAIN; depth++) {
628
+ if (node.nodeType === 1 && typeof node.getAttribute === "function") {
629
+ var rowId = node.getAttribute("data-agent-row")
630
+ if (typeof rowId === "string" && rowId !== "") return node
631
+ }
632
+ node = node.parentElement
633
+ }
634
+ return null
635
+ }
636
+
637
+ // The first row-root element stamped with the given entity name,
638
+ // anywhere in the document. Used when QUERY_COLLECTION_ORDER is
639
+ // addressed by `entity` alone (no `selector`) — the entity-name
640
+ // targeting mode.
641
+ function findFirstRowRootForEntity(entity) {
642
+ if (typeof entity !== "string" || entity === "") return null
643
+ try {
644
+ return safeQuerySelector(
645
+ '[data-agent-entity="' +
646
+ escapeAttributeSelectorValue(entity) +
647
+ '"][data-agent-row]'
648
+ )
649
+ } catch (e) {
650
+ return null
651
+ }
652
+ }
653
+
654
+ // Every row root that belongs to the SAME collection instantiation as
655
+ // `rowRootEl`: the babel codemod (M2) stamps each `.map()` row's root
656
+ // element as a DIRECT child of whatever wraps the `.map()` call, so
657
+ // siblings sharing `data-agent-entity` under `rowRootEl`'s parent ARE
658
+ // the collection, in DOM order. Falls back to `[rowRootEl]` alone when
659
+ // no parent is reachable (defensive — should not happen for an
660
+ // attached element).
661
+ function collectionRowElements(rowRootEl, entity) {
662
+ var parent = rowRootEl.parentElement
663
+ if (!parent) return [rowRootEl]
664
+ var siblings = []
665
+ var kids = parent.children
666
+ for (var i = 0; i < kids.length; i++) {
667
+ var kid = kids[i]
668
+ if (
669
+ kid.nodeType === 1 &&
670
+ typeof kid.getAttribute === "function" &&
671
+ kid.getAttribute("data-agent-entity") === entity &&
672
+ kid.hasAttribute("data-agent-row")
673
+ ) {
674
+ siblings.push(kid)
675
+ }
676
+ }
677
+ return siblings.length > 0 ? siblings : [rowRootEl]
678
+ }
679
+
680
+ // ARCH-1065 M3 (v2.3.0) — {entity, order, rowIds, rowCount} for the
681
+ // collection enclosing a target node, read live off the DOM
682
+ // (data-agent-entity/-row/-collection-order, D18) and cross-checked
683
+ // against window.__slice[entity].length when the slice is available.
684
+ //
685
+ // `order` fails CLOSED ("derived") on anything but an explicit
686
+ // "stable" attribute value. babel-plugin-collection-binding stamps
687
+ // entity/row/collection-order ATOMICALLY — three addAttr calls back to
688
+ // back with no branch between them — and no other emitter of
689
+ // data-agent-row exists. So data-agent-row present WITHOUT
690
+ // data-agent-collection-order can only mean the stamping contract
691
+ // never ran for this element: the order is UNKNOWN, not "stable".
692
+ // Defaulting unknown to "stable" would let the FE permit a drag whose
693
+ // Save can't be honored (silent, user-visible corruption); defaulting
694
+ // to "derived" only disables reorder on markup we can't vouch for.
695
+ function queryCollectionOrder(rowRootEl) {
696
+ if (!rowRootEl) return null
697
+ var entity = rowRootEl.getAttribute("data-agent-entity")
698
+ if (typeof entity !== "string" || entity === "") return null
699
+
700
+ var orderAttr = rowRootEl.getAttribute("data-agent-collection-order")
701
+ var order = orderAttr === "stable" ? "stable" : "derived"
702
+
703
+ var rowElements = collectionRowElements(rowRootEl, entity)
704
+ var rowIds = []
705
+ for (var i = 0; i < rowElements.length; i++) {
706
+ var rowId = rowElements[i].getAttribute("data-agent-row")
707
+ if (typeof rowId === "string" && rowId !== "") rowIds.push(rowId)
708
+ }
709
+ var domRowCount = rowIds.length
710
+
711
+ var sliceRows = resolveSliceEntityRows(entity)
712
+ var sliceRowCount = sliceRows ? sliceRows.length : null
713
+
714
+ // `rowCount` is the A17 primary field — DOM population, the same
715
+ // rows the FE samples values from. `domRowCount`/`sliceRowCount` are
716
+ // the divergence-diagnostic pair (identical to rowCount when the
717
+ // slice agrees, or absent from parity entirely when the slice is
718
+ // unavailable). Known limit: two same-entity `.map()`s that happen
719
+ // to share one DOM parent merge into a single reported collection —
720
+ // rare by construction (siblings of a `.map()` output share a
721
+ // parent by definition), and there is no collection-INSTANCE stamp
722
+ // (only per-row entity/row/order) to disambiguate two side-by-side
723
+ // instantiations of the same entity.
724
+ return {
725
+ entity: entity,
726
+ order: order,
727
+ rowIds: rowIds,
728
+ rowCount: domRowCount,
729
+ domRowCount: domRowCount,
730
+ sliceRowCount: sliceRowCount,
731
+ }
732
+ }
733
+
618
734
  if (window._inspectorSharedTargetObserver) {
619
735
  try {
620
736
  window._inspectorSharedTargetObserver.disconnect()
@@ -1231,7 +1347,10 @@
1231
1347
  // `QUERY_SLICE_FIELD` in the message protocol, and
1232
1348
  // `capabilities.bindingContract` below. No existing field changed
1233
1349
  // shape, so `protocolVersion` stays 1.
1234
- version: "2.2.0",
1350
+ // 2.3.0 (ARCH-1065 A17-support): additive only — `QUERY_COLLECTION_ORDER`
1351
+ // in the message protocol and `capabilities.collectionOrderQuery`
1352
+ // below. No existing field changed shape, so `protocolVersion` stays 1.
1353
+ version: "2.3.0",
1235
1354
  protocolVersion: 1,
1236
1355
  capabilities: {
1237
1356
  // 2.1.1: function-valued __editorMeta props are invoked before any
@@ -1257,6 +1376,12 @@
1257
1376
  // before relying on either — a frozen pre-1065 inspector has
1258
1377
  // neither.
1259
1378
  collectionBinding: true,
1379
+ // ARCH-1065 A17-support (v2.3.0): `QUERY_COLLECTION_ORDER` resolves
1380
+ // the enclosing collection's order ("stable"/"derived") and row
1381
+ // ids/count for a target node, off the same data-agent-* grammar
1382
+ // as `collectionBinding`. Hosts MUST feature-detect on this flag —
1383
+ // a frozen pre-2.3.0 inspector has no handler for the message.
1384
+ collectionOrderQuery: true,
1260
1385
  // The BUILD-TIME contract version this specific page was stamped
1261
1386
  // with, forwarded verbatim from `<meta name="archie-binding-contract">`
1262
1387
  // when the kit build emitted one — null on any page built before
@@ -3881,6 +4006,58 @@
3881
4006
  field: queryField,
3882
4007
  result: queryResult,
3883
4008
  })
4009
+ } else if (event.data && event.data.type === "QUERY_COLLECTION_ORDER") {
4010
+ // ARCH-1065 M3 (v2.3.0) — {order, rowIds, rowCount} for the
4011
+ // collection enclosing a target node, so the FE can refuse
4012
+ // reorder-at-drag on "derived" collections and drive the
4013
+ // repetition-aware enum threshold off rowCount. Mirrors
4014
+ // QUERY_SLICE_FIELD's request/response envelope and graceful
4015
+ // (result: null) failure shape.
4016
+ //
4017
+ // Targeting, in priority order:
4018
+ // 1. `selector` — a CSS selector for ANY element inside the
4019
+ // collection (a row, or a leaf nested inside one); resolved
4020
+ // the same way other selector-addressed messages resolve one
4021
+ // (`findElementBySelector`), then walked up to the nearest
4022
+ // enclosing row root.
4023
+ // 2. `entity` alone (no selector) — the first row root in the
4024
+ // document stamped with that entity name.
4025
+ // 3. neither — mirrors QUERY_SLICE_FIELD's no-rowId fallback:
4026
+ // the currently selected (else hovered) element.
4027
+ var collectionQuerySelector =
4028
+ typeof event.data.selector === "string" && event.data.selector !== ""
4029
+ ? event.data.selector
4030
+ : null
4031
+ var collectionQueryEntity =
4032
+ typeof event.data.entity === "string" && event.data.entity !== ""
4033
+ ? event.data.entity
4034
+ : null
4035
+
4036
+ var collectionRowRoot = null
4037
+ if (collectionQuerySelector) {
4038
+ var collectionTargetEl = findElementBySelector(collectionQuerySelector)
4039
+ collectionRowRoot = collectionTargetEl
4040
+ ? nearestRowRootElement(collectionTargetEl)
4041
+ : null
4042
+ } else if (collectionQueryEntity) {
4043
+ collectionRowRoot = findFirstRowRootForEntity(collectionQueryEntity)
4044
+ } else {
4045
+ var collectionContextEl = getSelectedElement() || highlightedElement
4046
+ collectionRowRoot = collectionContextEl
4047
+ ? nearestRowRootElement(collectionContextEl)
4048
+ : null
4049
+ }
4050
+
4051
+ var collectionQueryResult = queryCollectionOrder(collectionRowRoot)
4052
+
4053
+ postMessageToParent({
4054
+ type: "COLLECTION_ORDER_RESULT",
4055
+ requestId: event.data.requestId,
4056
+ entity: collectionQueryResult
4057
+ ? collectionQueryResult.entity
4058
+ : collectionQueryEntity,
4059
+ result: collectionQueryResult,
4060
+ })
3884
4061
  }
3885
4062
  }
3886
4063
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archie/devtools",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "repository": {
6
6
  "type": "git",