@archie/devtools 0.0.41 → 0.0.43
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/{chunk-6QB35SLQ.mjs → chunk-P5XMHBFS.mjs} +249 -3
- package/dist/collection-binding.mjs +1 -1
- package/dist/index.d.mts +199 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +249 -3
- package/dist/index.mjs +1 -1
- package/fixtures/prop-source.fixture.json +310 -0
- package/package.json +3 -2
|
@@ -78,6 +78,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
return "";
|
|
80
80
|
}
|
|
81
|
+
function isIntrinsicTagName(name) {
|
|
82
|
+
return t.isJSXIdentifier(name) && /^[a-z]/.test(name.name);
|
|
83
|
+
}
|
|
81
84
|
function isHostLikeMemberExpressionName(name) {
|
|
82
85
|
if (!t.isJSXMemberExpression(name)) return false;
|
|
83
86
|
const propertyName = name.property.name;
|
|
@@ -168,6 +171,158 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
168
171
|
}
|
|
169
172
|
return hasAny ? t.objectExpression(props) : null;
|
|
170
173
|
}
|
|
174
|
+
function describeChildrenText(opening) {
|
|
175
|
+
const elementPath = opening.parentPath;
|
|
176
|
+
if (!elementPath || !elementPath.isJSXElement()) return null;
|
|
177
|
+
const element = elementPath.node;
|
|
178
|
+
if (!element.closingElement) return null;
|
|
179
|
+
const children = element.children;
|
|
180
|
+
if (children.length === 0) return "";
|
|
181
|
+
if (children.length !== 1) return "__dynamic__";
|
|
182
|
+
const [only] = children;
|
|
183
|
+
if (!t.isJSXText(only)) return "__dynamic__";
|
|
184
|
+
return only.value.trim();
|
|
185
|
+
}
|
|
186
|
+
function literalInitText(init) {
|
|
187
|
+
if (t.isStringLiteral(init)) return init.value;
|
|
188
|
+
if (t.isTemplateLiteral(init) && init.expressions.length === 0) {
|
|
189
|
+
const [quasi] = init.quasis;
|
|
190
|
+
if (!quasi) return null;
|
|
191
|
+
return quasi.value.cooked ?? quasi.value.raw;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function describeLocalConstSource(name, binding) {
|
|
196
|
+
const declarator = binding.path;
|
|
197
|
+
if (!declarator.isVariableDeclarator()) return null;
|
|
198
|
+
const declaration = declarator.parentPath;
|
|
199
|
+
if (!declaration?.isVariableDeclaration()) return null;
|
|
200
|
+
if (declaration.node.kind !== "const" && declaration.node.kind !== "let") return null;
|
|
201
|
+
const init = declarator.node.init;
|
|
202
|
+
if (!init) return null;
|
|
203
|
+
const literal = literalInitText(init);
|
|
204
|
+
if (literal === null) return null;
|
|
205
|
+
const loc = init.loc?.start;
|
|
206
|
+
if (!loc) return null;
|
|
207
|
+
return {
|
|
208
|
+
kind: "local-const",
|
|
209
|
+
name,
|
|
210
|
+
// The init lives in the SAME file being transformed by construction —
|
|
211
|
+
// `scope.getBinding` never crosses a file boundary — so the caller's
|
|
212
|
+
// own `file` (computed once per visited node) is correct here too.
|
|
213
|
+
file: currentFile,
|
|
214
|
+
line: loc.line,
|
|
215
|
+
col: loc.column + 1,
|
|
216
|
+
literal,
|
|
217
|
+
// FE MINOR-5: how many places in the FILE read this const — every
|
|
218
|
+
// reference, not only the one this call is describing. `refs > 1`
|
|
219
|
+
// means one initializer feeds more than one rendered element, so a
|
|
220
|
+
// consumer must warn/refuse rather than treat a rewrite as scoped to
|
|
221
|
+
// just this usage (see `PropSource`'s docstring for the full contract).
|
|
222
|
+
refs: binding.referencePaths.length
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function resolvableSameFileBinding(name, scope) {
|
|
226
|
+
const binding = scope.getBinding(name);
|
|
227
|
+
if (!binding) return null;
|
|
228
|
+
if (!binding.constant) return null;
|
|
229
|
+
const bp = binding.path;
|
|
230
|
+
if (bp.isImportSpecifier() || bp.isImportDefaultSpecifier() || bp.isImportNamespaceSpecifier()) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return binding;
|
|
234
|
+
}
|
|
235
|
+
function memberChain(expr) {
|
|
236
|
+
const path2 = [];
|
|
237
|
+
let current = expr;
|
|
238
|
+
while (t.isMemberExpression(current)) {
|
|
239
|
+
if (current.computed) return null;
|
|
240
|
+
if (!t.isIdentifier(current.property)) return null;
|
|
241
|
+
path2.unshift(current.property.name);
|
|
242
|
+
current = current.object;
|
|
243
|
+
}
|
|
244
|
+
if (!t.isIdentifier(current)) return null;
|
|
245
|
+
return { root: current.name, path: path2 };
|
|
246
|
+
}
|
|
247
|
+
let currentFile = "";
|
|
248
|
+
function describeSource(expr, scope) {
|
|
249
|
+
if (t.isIdentifier(expr)) {
|
|
250
|
+
const binding = resolvableSameFileBinding(expr.name, scope);
|
|
251
|
+
if (!binding) return { kind: "other" };
|
|
252
|
+
const local = describeLocalConstSource(expr.name, binding);
|
|
253
|
+
if (local) return local;
|
|
254
|
+
return { kind: "identifier", name: expr.name };
|
|
255
|
+
}
|
|
256
|
+
if (t.isMemberExpression(expr) && !expr.computed) {
|
|
257
|
+
const chain = memberChain(expr);
|
|
258
|
+
if (chain && resolvableSameFileBinding(chain.root, scope)) {
|
|
259
|
+
return { kind: "member", root: chain.root, path: chain.path };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { kind: "other" };
|
|
263
|
+
}
|
|
264
|
+
function isStaticPropsLiteral(expr) {
|
|
265
|
+
return t.isStringLiteral(expr) || t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) || t.isNullLiteral(expr);
|
|
266
|
+
}
|
|
267
|
+
function describeChildrenSource(opening) {
|
|
268
|
+
const elementPath = opening.parentPath;
|
|
269
|
+
if (!elementPath || !elementPath.isJSXElement()) return null;
|
|
270
|
+
const children = elementPath.node.children;
|
|
271
|
+
if (children.length !== 1) return null;
|
|
272
|
+
const [only] = children;
|
|
273
|
+
if (!t.isJSXExpressionContainer(only)) return null;
|
|
274
|
+
if (t.isJSXEmptyExpression(only.expression)) return null;
|
|
275
|
+
return describeSource(only.expression, opening.scope);
|
|
276
|
+
}
|
|
277
|
+
function collectPropSources(attrs, scope) {
|
|
278
|
+
const entries = [];
|
|
279
|
+
for (const attr of attrs) {
|
|
280
|
+
if (!t.isJSXAttribute(attr)) continue;
|
|
281
|
+
const name = t.isJSXIdentifier(attr.name) ? attr.name.name : `${attr.name.namespace.name}:${attr.name.name.name}`;
|
|
282
|
+
if (NON_BINDING_ATTRS.has(name)) continue;
|
|
283
|
+
const value = attr.value;
|
|
284
|
+
if (!t.isJSXExpressionContainer(value)) continue;
|
|
285
|
+
const expression = value.expression;
|
|
286
|
+
if (t.isJSXEmptyExpression(expression)) continue;
|
|
287
|
+
if (isStaticPropsLiteral(expression)) continue;
|
|
288
|
+
entries.push([name, describeSource(expression, scope)]);
|
|
289
|
+
}
|
|
290
|
+
return entries;
|
|
291
|
+
}
|
|
292
|
+
function propSourceToAst(source) {
|
|
293
|
+
switch (source.kind) {
|
|
294
|
+
case "identifier":
|
|
295
|
+
return t.objectExpression([
|
|
296
|
+
t.objectProperty(t.identifier("kind"), t.stringLiteral("identifier")),
|
|
297
|
+
t.objectProperty(t.identifier("name"), t.stringLiteral(source.name))
|
|
298
|
+
]);
|
|
299
|
+
case "local-const":
|
|
300
|
+
return t.objectExpression([
|
|
301
|
+
t.objectProperty(t.identifier("kind"), t.stringLiteral("local-const")),
|
|
302
|
+
t.objectProperty(t.identifier("name"), t.stringLiteral(source.name)),
|
|
303
|
+
t.objectProperty(t.identifier("file"), t.stringLiteral(source.file)),
|
|
304
|
+
t.objectProperty(t.identifier("line"), t.numericLiteral(source.line)),
|
|
305
|
+
t.objectProperty(t.identifier("col"), t.numericLiteral(source.col)),
|
|
306
|
+
t.objectProperty(
|
|
307
|
+
t.identifier("literal"),
|
|
308
|
+
source.literal === null ? t.nullLiteral() : t.stringLiteral(source.literal)
|
|
309
|
+
),
|
|
310
|
+
t.objectProperty(t.identifier("refs"), t.numericLiteral(source.refs))
|
|
311
|
+
]);
|
|
312
|
+
case "member":
|
|
313
|
+
return t.objectExpression([
|
|
314
|
+
t.objectProperty(t.identifier("kind"), t.stringLiteral("member")),
|
|
315
|
+
t.objectProperty(t.identifier("root"), t.stringLiteral(source.root)),
|
|
316
|
+
t.objectProperty(
|
|
317
|
+
t.identifier("path"),
|
|
318
|
+
t.arrayExpression(source.path.map((segment) => t.stringLiteral(segment)))
|
|
319
|
+
)
|
|
320
|
+
]);
|
|
321
|
+
case "other":
|
|
322
|
+
default:
|
|
323
|
+
return t.objectExpression([t.objectProperty(t.identifier("kind"), t.stringLiteral("other"))]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
171
326
|
function isMetaStripSpreadExpression(arg) {
|
|
172
327
|
if (!t.isCallExpression(arg)) return false;
|
|
173
328
|
if (!t.isArrowFunctionExpression(arg.callee)) return false;
|
|
@@ -275,6 +430,14 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
275
430
|
}
|
|
276
431
|
return map;
|
|
277
432
|
}
|
|
433
|
+
function restParamName(pattern) {
|
|
434
|
+
for (const prop of pattern.properties) {
|
|
435
|
+
if (!t.isRestElement(prop)) continue;
|
|
436
|
+
if (t.isIdentifier(prop.argument)) return prop.argument.name;
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
278
441
|
function resolvePropContext(path2) {
|
|
279
442
|
let fn = path2.getFunctionParent();
|
|
280
443
|
while (fn) {
|
|
@@ -283,7 +446,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
283
446
|
return {
|
|
284
447
|
componentScope: fn.scope,
|
|
285
448
|
propMap: buildPropMap(first),
|
|
286
|
-
componentName: getFunctionName(fn)
|
|
449
|
+
componentName: getFunctionName(fn),
|
|
450
|
+
restParam: restParamName(first),
|
|
451
|
+
componentPath: fn
|
|
287
452
|
};
|
|
288
453
|
}
|
|
289
454
|
fn = fn.getFunctionParent();
|
|
@@ -403,12 +568,62 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
403
568
|
const props = collectPropIdentifiers(expr, scope, ctx);
|
|
404
569
|
return props.length > 0 ? { kind: "formatted", prop: props[0] } : null;
|
|
405
570
|
}
|
|
571
|
+
function unwrapSanitizedSpread(arg) {
|
|
572
|
+
if (!t.isCallExpression(arg)) return arg;
|
|
573
|
+
if (!isMetaStripSpreadExpression(arg)) return arg;
|
|
574
|
+
if (arg.arguments.length !== 1) return arg;
|
|
575
|
+
const [payload] = arg.arguments;
|
|
576
|
+
if (!t.isObjectExpression(payload) || payload.properties.length !== 1) return arg;
|
|
577
|
+
const [only] = payload.properties;
|
|
578
|
+
if (!t.isSpreadElement(only)) return arg;
|
|
579
|
+
return only.argument;
|
|
580
|
+
}
|
|
581
|
+
function forwardsOwnerRestChildren(opening, ctx) {
|
|
582
|
+
if (ctx.restParam === null) return false;
|
|
583
|
+
const elementPath = opening.parentPath;
|
|
584
|
+
if (!elementPath || !elementPath.isJSXElement()) return false;
|
|
585
|
+
if (meaningfulChildren(t, elementPath.node.children).length > 0) return false;
|
|
586
|
+
let spreadsRest = 0;
|
|
587
|
+
for (const attr of opening.node.attributes) {
|
|
588
|
+
if (t.isJSXAttribute(attr)) {
|
|
589
|
+
if (t.isJSXIdentifier(attr.name, { name: "children" })) return false;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (!t.isJSXSpreadAttribute(attr)) continue;
|
|
593
|
+
const payload = unwrapSanitizedSpread(attr.argument);
|
|
594
|
+
if (!t.isIdentifier(payload, { name: ctx.restParam })) return false;
|
|
595
|
+
spreadsRest += 1;
|
|
596
|
+
}
|
|
597
|
+
if (spreadsRest !== 1) return false;
|
|
598
|
+
const binding = opening.scope.getBinding(ctx.restParam);
|
|
599
|
+
if (!binding) return false;
|
|
600
|
+
if (binding.scope !== ctx.componentScope) return false;
|
|
601
|
+
return binding.constant;
|
|
602
|
+
}
|
|
603
|
+
const restForwarderCounts = /* @__PURE__ */ new WeakMap();
|
|
604
|
+
function countRestForwarders(ctx) {
|
|
605
|
+
const cached = restForwarderCounts.get(ctx.componentPath.node);
|
|
606
|
+
if (cached !== void 0) return cached;
|
|
607
|
+
let count = 0;
|
|
608
|
+
ctx.componentPath.traverse({
|
|
609
|
+
JSXOpeningElement(inner) {
|
|
610
|
+
if (forwardsOwnerRestChildren(inner, ctx)) count += 1;
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
restForwarderCounts.set(ctx.componentPath.node, count);
|
|
614
|
+
return count;
|
|
615
|
+
}
|
|
406
616
|
function collectPropBindings(path2, attrs) {
|
|
407
617
|
const ctx = resolvePropContext(path2);
|
|
408
|
-
if (!ctx || ctx.propMap.size === 0
|
|
618
|
+
if (!ctx || ctx.propMap.size === 0 && ctx.restParam === null) {
|
|
619
|
+
return { bindings: [], owner: null };
|
|
620
|
+
}
|
|
409
621
|
if (ctx.componentName === null) return { bindings: [], owner: null };
|
|
410
622
|
const scope = path2.scope;
|
|
411
623
|
const bindings = [];
|
|
624
|
+
if (forwardsOwnerRestChildren(path2, ctx) && countRestForwarders(ctx) === 1) {
|
|
625
|
+
bindings.push({ target: "children", prop: "children", kind: "direct" });
|
|
626
|
+
}
|
|
412
627
|
const elementPath = path2.parentPath;
|
|
413
628
|
if (elementPath && elementPath.isJSXElement()) {
|
|
414
629
|
const children = meaningfulChildren(t, elementPath.node.children);
|
|
@@ -499,6 +714,7 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
499
714
|
if (!loc) return;
|
|
500
715
|
const root = state.file.opts.root ?? "";
|
|
501
716
|
const file = root ? nodePath.relative(root, filename) : filename;
|
|
717
|
+
currentFile = file;
|
|
502
718
|
const line = loc.line;
|
|
503
719
|
const col = loc.column + 1;
|
|
504
720
|
const componentName = getJSXTagName(path2.node);
|
|
@@ -510,7 +726,9 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
510
726
|
return;
|
|
511
727
|
}
|
|
512
728
|
let rootName = null;
|
|
513
|
-
if (
|
|
729
|
+
if (isIntrinsicTagName(path2.node.name)) {
|
|
730
|
+
rootName = null;
|
|
731
|
+
} else if (t.isJSXIdentifier(path2.node.name)) {
|
|
514
732
|
rootName = path2.node.name.name;
|
|
515
733
|
} else if (t.isJSXMemberExpression(path2.node.name)) {
|
|
516
734
|
let cur = path2.node.name;
|
|
@@ -572,6 +790,21 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
572
790
|
t.objectProperty(t.identifier("slot"), t.stringLiteral(slot))
|
|
573
791
|
);
|
|
574
792
|
}
|
|
793
|
+
if (!isHostElement) {
|
|
794
|
+
const childrenText = describeChildrenText(path2);
|
|
795
|
+
metaObj.properties.push(
|
|
796
|
+
t.objectProperty(
|
|
797
|
+
t.identifier("childrenText"),
|
|
798
|
+
childrenText === null ? t.nullLiteral() : t.stringLiteral(childrenText)
|
|
799
|
+
)
|
|
800
|
+
);
|
|
801
|
+
const childrenSource = describeChildrenSource(path2);
|
|
802
|
+
if (childrenSource) {
|
|
803
|
+
metaObj.properties.push(
|
|
804
|
+
t.objectProperty(t.identifier("childrenSource"), propSourceToAst(childrenSource))
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
575
808
|
if (bindings.length > 0) {
|
|
576
809
|
metaObj.properties.push(
|
|
577
810
|
t.objectProperty(t.identifier("propBindings"), propBindingsToAst(bindings))
|
|
@@ -583,6 +816,19 @@ function babelPluginEditorMeta({ types: t }, opts = {}) {
|
|
|
583
816
|
)
|
|
584
817
|
);
|
|
585
818
|
}
|
|
819
|
+
const propSourceEntries = collectPropSources(path2.node.attributes, path2.scope);
|
|
820
|
+
if (propSourceEntries.length > 0) {
|
|
821
|
+
metaObj.properties.push(
|
|
822
|
+
t.objectProperty(
|
|
823
|
+
t.identifier("propSources"),
|
|
824
|
+
t.objectExpression(
|
|
825
|
+
propSourceEntries.map(
|
|
826
|
+
([name, source]) => t.objectProperty(t.stringLiteral(name), propSourceToAst(source))
|
|
827
|
+
)
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
586
832
|
if (isHostElement && includeHostSourceRef) {
|
|
587
833
|
path2.node.attributes.push(
|
|
588
834
|
t.jsxAttribute(
|
package/dist/index.d.mts
CHANGED
|
@@ -12,6 +12,10 @@ import { types, PluginObj } from '@babel/core';
|
|
|
12
12
|
* nodeId, file, line, col, componentName, staticProps, movable, parentNodeId
|
|
13
13
|
* + (ARCH-883 F1) slot, propBindings, propOwner
|
|
14
14
|
* + (ARCH-883-KOI) origin
|
|
15
|
+
* + (ARCH-1444) childrenText — non-host elements only
|
|
16
|
+
* + (ARCH-1450) childrenSource, propSources — same-file identifier tracing
|
|
17
|
+
* for values `propBindings` cannot reach (no destructured-prop component
|
|
18
|
+
* in scope, or a value that never touches a prop at all)
|
|
15
19
|
*
|
|
16
20
|
* At runtime the Fiber bridge reads `memoizedProps.__editorMeta` to resolve
|
|
17
21
|
* element identity without any DOM attributes.
|
|
@@ -169,6 +173,17 @@ declare function babelPluginEditorMeta({ types: t }: {
|
|
|
169
173
|
* `id`/`htmlFor`/`name`/`type`/`role`/`slot`) are downgraded to `formatted` by the
|
|
170
174
|
* PRODUCER, because no usage value could make them a text edit. The consumer does
|
|
171
175
|
* not need to re-derive that list; it only needs the allowlist above.
|
|
176
|
+
*
|
|
177
|
+
* ARCH-1443: `{ target: 'children', prop: 'children', kind: 'direct' }` is emitted
|
|
178
|
+
* for an element that forwards its component's props REST and renders no children
|
|
179
|
+
* of its own — the shape EVERY ejected shadcn-style primitive has
|
|
180
|
+
* (`({ className, ...p }) => <h3 className={…} {...p} />`). Nothing about the
|
|
181
|
+
* allowlist changes: the usage value of `children` is the JSX children written at
|
|
182
|
+
* the usage site (`<CardTitle>Cómo funciona</CardTitle>`), and the same
|
|
183
|
+
* usage-value law applies to it — plain text is writable, a nested element or a
|
|
184
|
+
* computed expression is a gap. The producer refuses whenever more than one
|
|
185
|
+
* element in the component forwards the rest, since one `children` prop cannot
|
|
186
|
+
* name which of them an edit is about.
|
|
172
187
|
*/
|
|
173
188
|
interface PropBinding {
|
|
174
189
|
/**
|
|
@@ -204,6 +219,134 @@ interface PropBinding {
|
|
|
204
219
|
*/
|
|
205
220
|
kind: 'direct' | 'derived' | 'formatted';
|
|
206
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* ARCH-1450 — where a rendered EXPRESSION traces to in the SAME file, for the
|
|
224
|
+
* one case `propBindings` structurally cannot answer: a value that never
|
|
225
|
+
* touches a component prop at all.
|
|
226
|
+
*
|
|
227
|
+
* `propBindings`/`propOwner` trace an expression to a PROP of the enclosing
|
|
228
|
+
* (module-level, destructured) component — that machinery is silent whenever
|
|
229
|
+
* there is no such component (a screen's `export default function X()` with
|
|
230
|
+
* no destructured params) or whenever the expression bottoms out in a plain
|
|
231
|
+
* local rather than a prop (`const title = "Contenido"; … <Section
|
|
232
|
+
* title={title}>`). In both cases `collectPropBindings` returns `{bindings:
|
|
233
|
+
* [], owner: null}` and the identifier name never crosses the postMessage
|
|
234
|
+
* boundary in any form — the FE's `usage-prop-resolver` has nothing to walk.
|
|
235
|
+
* `childrenSource`/`propSources` close exactly that gap, independently of
|
|
236
|
+
* whether a `PropBinding` was also emitted for the same target: the two
|
|
237
|
+
* fields answer different questions ("is this a prop, and is it a safe
|
|
238
|
+
* rewrite" vs "what source-level thing does this identifier name") and a
|
|
239
|
+
* consumer may need either, or both, depending on what it already knows.
|
|
240
|
+
*
|
|
241
|
+
* THE FOUR ANSWERS:
|
|
242
|
+
*
|
|
243
|
+
* `identifier` — the expression is a bare identifier whose binding
|
|
244
|
+
* resolves (constant, same file, not an import) but is
|
|
245
|
+
* not a literal-init local — a destructured prop used
|
|
246
|
+
* directly, a plain function parameter, or a local
|
|
247
|
+
* `const`/`let` whose initializer is not itself a plain
|
|
248
|
+
* string. `name` is the LOCAL identifier as written at
|
|
249
|
+
* this usage — NOT necessarily the prop name `propBindings`
|
|
250
|
+
* would report for a renamed destructure
|
|
251
|
+
* (`{ headline: h }` reports `h` here, `headline` there).
|
|
252
|
+
* Consumers that need the true prop name still read
|
|
253
|
+
* `propBindings`; this field only says "this traces to a
|
|
254
|
+
* resolvable local named X", which is enough to tell a
|
|
255
|
+
* prop/parameter apart from a dead end.
|
|
256
|
+
* `local-const` — the identifier resolves to a `const`/`let` declared in
|
|
257
|
+
* THIS SAME FILE (by construction — scope resolution never
|
|
258
|
+
* crosses a file boundary) whose initializer is a
|
|
259
|
+
* `StringLiteral` or a `TemplateLiteral` with NO
|
|
260
|
+
* interpolations. `literal` is that string — the exact
|
|
261
|
+
* text a consumer would show/replace. `file`/`line`/`col`
|
|
262
|
+
* point at the INITIALIZER, not the identifier's usage —
|
|
263
|
+
* that is where a rewrite must land: replacing the
|
|
264
|
+
* identifier's occurrence would rename a variable, not
|
|
265
|
+
* change what it renders wherever else it is used.
|
|
266
|
+
* `refs` (FE MINOR-5) is `binding.referencePaths.length` —
|
|
267
|
+
* how many places in the file READ this const, counting
|
|
268
|
+
* every usage, not only the one this `PropSource` sits on.
|
|
269
|
+
* `refs > 1` means the SAME initializer feeds more than
|
|
270
|
+
* one rendered element (`const title = "X"; <A
|
|
271
|
+
* title={title}/><B title={title}/>` — both usages report
|
|
272
|
+
* `refs: 2`): rewriting the initializer changes what BOTH
|
|
273
|
+
* elements show, so a consumer must warn or refuse rather
|
|
274
|
+
* than treat the write as scoped to the one element the
|
|
275
|
+
* edit was made on. `refs === 1` is the only shape safe to
|
|
276
|
+
* treat as a single-target rewrite.
|
|
277
|
+
* `member` — a non-computed member-expression chain
|
|
278
|
+
* (`props.title`, `item.address.city`) whose ROOT resolves
|
|
279
|
+
* the same way `identifier`'s binding does (constant, same
|
|
280
|
+
* file, not an import). `root` is the base identifier;
|
|
281
|
+
* `path` is the property chain beyond it, in order
|
|
282
|
+
* (`['address', 'city']` for `item.address.city`).
|
|
283
|
+
* `other` — the honest gap marker, same role `formatted` plays for
|
|
284
|
+
* `PropBinding.kind` and `'__dynamic__'` plays for
|
|
285
|
+
* `childrenText`: an expression the plugin looked at and
|
|
286
|
+
* declined to trace — a computed member (`obj[key]`), a
|
|
287
|
+
* call, a template WITH interpolations, a conditional, an
|
|
288
|
+
* identifier with no binding (a free/global read), a
|
|
289
|
+
* binding that IS reassigned somewhere in the file, or one
|
|
290
|
+
* that comes from an `import`. NEVER inferred past any of
|
|
291
|
+
* those — a wrong trace is strictly worse than an honest
|
|
292
|
+
* gap.
|
|
293
|
+
*
|
|
294
|
+
* SCOPE — deliberately NARROW, unlike `childrenText`/`origin`: both fields
|
|
295
|
+
* are OMITTED (not stamped `other`) whenever there is nothing to say, the
|
|
296
|
+
* same convention `propBindings`/`propOwner`/`slot` already use ("emitted
|
|
297
|
+
* only when it carries information"). `childrenSource` is only computed when
|
|
298
|
+
* the element's children are EXACTLY one `JSXExpressionContainer` (the case
|
|
299
|
+
* that makes `childrenText === '__dynamic__'` for the single most common
|
|
300
|
+
* reason — `<h1>{title}</h1>` — and therefore the case where a source trace
|
|
301
|
+
* is actually useful); self-closing elements, plain-text children and
|
|
302
|
+
* mixed/multi children never get it. `propSources` only carries entries for
|
|
303
|
+
* attributes whose value is a `JSXExpressionContainer` wrapping something
|
|
304
|
+
* OTHER than a literal `extractStaticProps` already captures exactly
|
|
305
|
+
* (`staticProps` already has the true value for `title={"Static"}`; adding a
|
|
306
|
+
* `propSources.title: {kind:'other'}` next to it would look like a gap where
|
|
307
|
+
* there is none). Both fields are ABSENT — not `null`/`{}` — on every bundle
|
|
308
|
+
* built before ARCH-1450, the same absence-means-"cannot answer" law every
|
|
309
|
+
* other additive field in this file states.
|
|
310
|
+
*
|
|
311
|
+
* Emitted for BOTH host and non-host elements for `propSources` (attribute
|
|
312
|
+
* resolution does not depend on the element being a component usage — a host
|
|
313
|
+
* `<div title={title}>` traces exactly the same way `staticProps` treats host
|
|
314
|
+
* and component attributes identically). `childrenSource` follows
|
|
315
|
+
* `childrenText`'s own host exemption: a host element's children are the
|
|
316
|
+
* screen's own literal JSX, not a component usage, so there is no target for
|
|
317
|
+
* an editor-driven rewrite to route through in the first place.
|
|
318
|
+
*
|
|
319
|
+
* CROSS-REPO CONTRACT (ARCH-1450 CR MINOR-1): `usage-prop-resolver.ts`
|
|
320
|
+
* (archie-app) is the only consumer, and the two repos share no fixture of
|
|
321
|
+
* their own — a rename here (e.g. `literal` -> `value`) would leave both
|
|
322
|
+
* suites green while every real generated project broke. The published
|
|
323
|
+
* package ships `fixtures/prop-source.fixture.json` (see `files` in
|
|
324
|
+
* `package.json`) specifically so archie-app's tests can assert against the
|
|
325
|
+
* REAL emitted shape instead of a hand-typed copy of it. That file is
|
|
326
|
+
* GENERATED, never hand-edited — see `src/prop-source-fixture.generate.test.ts`,
|
|
327
|
+
* which regenerates it from the live plugin and fails loud the moment the
|
|
328
|
+
* committed copy drifts. Regenerate with `UPDATE_FIXTURES=1 npx vitest run
|
|
329
|
+
* src/prop-source-fixture.generate.test.ts`, review the diff, and bump
|
|
330
|
+
* archie-app's copy in the same change.
|
|
331
|
+
*/
|
|
332
|
+
type PropSource = {
|
|
333
|
+
kind: 'identifier';
|
|
334
|
+
name: string;
|
|
335
|
+
} | {
|
|
336
|
+
kind: 'local-const';
|
|
337
|
+
name: string;
|
|
338
|
+
file: string;
|
|
339
|
+
line: number;
|
|
340
|
+
col: number;
|
|
341
|
+
literal: string | null;
|
|
342
|
+
refs: number;
|
|
343
|
+
} | {
|
|
344
|
+
kind: 'member';
|
|
345
|
+
root: string;
|
|
346
|
+
path: string[];
|
|
347
|
+
} | {
|
|
348
|
+
kind: 'other';
|
|
349
|
+
};
|
|
207
350
|
/** Metadata injected into every JSX element by babel-plugin-editor-meta. */
|
|
208
351
|
interface EditorMeta {
|
|
209
352
|
/** Stable identifier: "file:line:col:componentName" */
|
|
@@ -216,6 +359,38 @@ interface EditorMeta {
|
|
|
216
359
|
componentName: string;
|
|
217
360
|
/** Safely serializable literal prop values. Dynamic expressions are "__dynamic__". */
|
|
218
361
|
staticProps: Record<string, unknown> | null;
|
|
362
|
+
/**
|
|
363
|
+
* ARCH-1444 — the SHAPE of this element's JSX children, and their text when
|
|
364
|
+
* that text is plain. `staticProps` is built from ATTRIBUTES only, so without
|
|
365
|
+
* this field the `children` prop is the one binding target whose value at the
|
|
366
|
+
* usage a consumer cannot see — and the usage-value law above ("plain text is
|
|
367
|
+
* writable, a nested element or a computed expression is a gap") is stated in
|
|
368
|
+
* terms of exactly that value.
|
|
369
|
+
*
|
|
370
|
+
* `null` — the element is SELF-CLOSING: no children container at
|
|
371
|
+
* all. NOT the same as `''` (`<X></X>`), which can take a
|
|
372
|
+
* text write; `<X />` cannot without a structural rewrite.
|
|
373
|
+
* `''` — an empty children container.
|
|
374
|
+
* `'__dynamic__'` — anything that is not exactly one JSXText child: an
|
|
375
|
+
* expression container, a nested element, a comment, or
|
|
376
|
+
* several children. Same sentinel as `staticProps`.
|
|
377
|
+
* any other string— the sole JSXText child, TRIMMED (JSX itself trims
|
|
378
|
+
* leading/trailing whitespace-with-newlines, so the
|
|
379
|
+
* trimmed form is what renders). A newline that SURVIVES
|
|
380
|
+
* the trim is real multi-line content: a consumer that
|
|
381
|
+
* writes by splicing must refuse it, because replacing it
|
|
382
|
+
* with one line deletes source lines and invalidates every
|
|
383
|
+
* anchor below.
|
|
384
|
+
*
|
|
385
|
+
* Emitted for NON-HOST elements only — a usage is always a component tag, and
|
|
386
|
+
* stamping every `<div>` would be weight for an answer nothing asks.
|
|
387
|
+
*
|
|
388
|
+
* ABSENT (`undefined`) on a host element and on every bundle built before this
|
|
389
|
+
* change. Absence is NOT "no children": it means the page cannot answer the
|
|
390
|
+
* question, and a consumer must gap the edit rather than assume (the same law
|
|
391
|
+
* `origin`'s docstring states at length).
|
|
392
|
+
*/
|
|
393
|
+
childrenText?: string | null;
|
|
219
394
|
/**
|
|
220
395
|
* Whether the JSX node is in a reorderable position (2+ siblings).
|
|
221
396
|
*
|
|
@@ -354,6 +529,30 @@ interface EditorMeta {
|
|
|
354
529
|
* not the usage's, and routing those would write the wrong prop. Mismatch = gap.
|
|
355
530
|
*/
|
|
356
531
|
propOwner?: string | null;
|
|
532
|
+
/**
|
|
533
|
+
* ARCH-1450 — source trace for this element's SOLE children expression
|
|
534
|
+
* (`<h1>{title}</h1>`), independently of whether `propBindings` also has a
|
|
535
|
+
* `target: 'children'` entry for the same node. See `PropSource`'s own
|
|
536
|
+
* docstring for the full contract (the four kinds, the narrow emission
|
|
537
|
+
* scope, why it exists alongside `propBindings`).
|
|
538
|
+
*
|
|
539
|
+
* Absent when the element is self-closing, has plain-text children, has
|
|
540
|
+
* more than one child, or is a HOST element — see `PropSource`'s docstring.
|
|
541
|
+
* Absent on every bundle built before ARCH-1450.
|
|
542
|
+
*/
|
|
543
|
+
childrenSource?: PropSource;
|
|
544
|
+
/**
|
|
545
|
+
* ARCH-1450 — source trace for each ATTRIBUTE whose value is a
|
|
546
|
+
* `JSXExpressionContainer`, keyed by the attribute name (never `attr:`
|
|
547
|
+
* prefixed — this is not `PropBinding.target`). See `PropSource`'s own
|
|
548
|
+
* docstring for the full contract.
|
|
549
|
+
*
|
|
550
|
+
* Emitted only when non-empty — read it as `meta.propSources ?? {}`. An
|
|
551
|
+
* attribute whose expression is a literal already exact in `staticProps`
|
|
552
|
+
* never gets an entry here (nothing to trace beyond what `staticProps`
|
|
553
|
+
* already says). Absent on every bundle built before ARCH-1450.
|
|
554
|
+
*/
|
|
555
|
+
propSources?: Record<string, PropSource>;
|
|
357
556
|
}
|
|
358
557
|
|
|
359
558
|
interface ArchieDevToolsOptions {
|