@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.
- package/dist/collection-binding.d.mts +38 -8
- package/dist/collection-binding.d.ts +38 -8
- package/dist/collection-binding.js +123 -30
- package/dist/collection-binding.mjs +123 -30
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
|
39
|
-
*
|
|
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)
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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
|
-
*
|
|
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
|
|
39
|
-
*
|
|
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)
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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 (
|
|
114
|
-
|
|
115
|
-
if (!
|
|
116
|
-
if (!
|
|
117
|
-
|
|
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 (
|
|
120
|
-
return { entity:
|
|
148
|
+
if (computed && t.isStringLiteral(property)) {
|
|
149
|
+
return { entity: property.value, hasSort };
|
|
121
150
|
}
|
|
122
151
|
return null;
|
|
123
152
|
}
|
|
124
|
-
if (
|
|
153
|
+
if (path.isIdentifier()) {
|
|
125
154
|
if (hops >= MAX_SOURCE_CHAIN_HOPS) return null;
|
|
126
|
-
const binding = scope.getBinding(
|
|
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
|
|
131
|
-
if (!
|
|
132
|
-
return resolveSliceSource(
|
|
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
|
-
|
|
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
|
|
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 (
|
|
494
|
-
|
|
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
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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 (
|
|
83
|
-
|
|
84
|
-
if (!
|
|
85
|
-
if (!
|
|
86
|
-
|
|
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 (
|
|
89
|
-
return { entity:
|
|
117
|
+
if (computed && t.isStringLiteral(property)) {
|
|
118
|
+
return { entity: property.value, hasSort };
|
|
90
119
|
}
|
|
91
120
|
return null;
|
|
92
121
|
}
|
|
93
|
-
if (
|
|
122
|
+
if (path.isIdentifier()) {
|
|
94
123
|
if (hops >= MAX_SOURCE_CHAIN_HOPS) return null;
|
|
95
|
-
const binding = scope.getBinding(
|
|
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
|
|
100
|
-
if (!
|
|
101
|
-
return resolveSliceSource(
|
|
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
|
-
|
|
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
|
|
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 (
|
|
463
|
-
|
|
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
|
|
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;
|