@component-compass/reference-graph 0.1.18 → 0.1.20
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/builder.d.ts +10 -4
- package/dist/builder.js +13 -9
- package/dist/builder.js.map +1 -1
- package/dist/engine/assert-never.d.ts +1 -1
- package/dist/engine/assert-never.js +1 -1
- package/dist/engine/component-shape.d.ts +18 -7
- package/dist/engine/component-shape.js +48 -18
- package/dist/engine/component-shape.js.map +1 -1
- package/dist/engine/helper-callers.d.ts +24 -14
- package/dist/engine/helper-callers.js +43 -70
- package/dist/engine/helper-callers.js.map +1 -1
- package/dist/engine/host-element.d.ts +37 -0
- package/dist/engine/host-element.js +55 -0
- package/dist/engine/host-element.js.map +1 -0
- package/dist/engine/index.d.ts +4 -4
- package/dist/engine/index.js +181 -91
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/library-stubs.js +1 -1
- package/dist/engine/member-identity.d.ts +9 -10
- package/dist/engine/member-identity.js +9 -10
- package/dist/engine/member-identity.js.map +1 -1
- package/dist/engine/re-export-chain.d.ts +1 -1
- package/dist/engine/re-export-chain.js +4 -3
- package/dist/engine/re-export-chain.js.map +1 -1
- package/dist/engine/registry.d.ts +43 -19
- package/dist/engine/registry.js +66 -50
- package/dist/engine/registry.js.map +1 -1
- package/dist/engine/resolve-reference.js +1 -1
- package/dist/engine/resolve-reference.js.map +1 -1
- package/dist/engine/resolve-type.d.ts +12 -0
- package/dist/engine/resolve-type.js +44 -52
- package/dist/engine/resolve-type.js.map +1 -1
- package/dist/engine/wrapper-folding.d.ts +22 -3
- package/dist/engine/wrapper-folding.js +146 -132
- package/dist/engine/wrapper-folding.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/file-graph.d.ts +29 -13
- package/dist/types/inferred-type.d.ts +2 -1
- package/package.json +2 -2
package/dist/engine/index.js
CHANGED
|
@@ -9,32 +9,38 @@ import { buildHelperCallers, findLocalDeclaration } from "./helper-callers.js";
|
|
|
9
9
|
import { resolveOwnerChain } from "./owner-resolution.js";
|
|
10
10
|
import { createCycleGuard } from "./cycle-detection.js";
|
|
11
11
|
import { unparsedMemberDefinition, effectiveExportName, residualMemberChain, compoundExportName } from "./member-identity.js";
|
|
12
|
-
import { buildComponentRegistry, excludeFoldedHolders, entryKey } from "./registry.js";
|
|
12
|
+
import { buildComponentRegistry, excludeFoldedHolders, excludeHostElementNames, entryKey } from "./registry.js";
|
|
13
|
+
import { hasOnlyHostElementNames, isHostElementName } from "./host-element.js";
|
|
14
|
+
import { firstDynamicImport } from "./component-shape.js";
|
|
13
15
|
/**
|
|
14
16
|
* JSX treats a bare, lowercase-initial element name as a host element
|
|
15
17
|
* (`div`, `svg`, `web-button`) — it never names a binding, so it is not an
|
|
16
18
|
* unresolved reference. A member chain (`motion.div`) or a capitalised name
|
|
17
|
-
* is a component reference and IS reported when unbound
|
|
19
|
+
* is a component reference and IS reported when unbound.
|
|
18
20
|
*/
|
|
19
21
|
function isHostElementRoot(ref) {
|
|
20
|
-
return ref.memberChain.length === 0 &&
|
|
22
|
+
return ref.memberChain.length === 0 && isHostElementName(ref.symbol);
|
|
21
23
|
}
|
|
22
24
|
export function resolve(graph, opts) {
|
|
23
25
|
const occurrences = [];
|
|
24
26
|
const argMap = createArgumentMap();
|
|
25
|
-
|
|
27
|
+
// The registry is the one judge of component-ness: owner
|
|
28
|
+
// classification asks it (through the host-element narrowing, so a
|
|
29
|
+
// lowercase member is a helper for ownership while the base registry still
|
|
30
|
+
// gates the argument-site holder loop below).
|
|
26
31
|
const registry = buildComponentRegistry(graph);
|
|
27
|
-
|
|
32
|
+
const helperIndex = buildHelperCallers(graph, excludeHostElementNames(registry, graph));
|
|
33
|
+
// THE admission rule (shape-only). A local
|
|
28
34
|
// react-component identity must be component-shaped; consumption is the
|
|
29
35
|
// usage itself (a JSX usage that resolves to a local declaration IS that
|
|
30
36
|
// declaration's consumption — the engine's own alias/union/dynamic-map/
|
|
31
37
|
// re-export/wrapper-fold resolution already did that work, so admission
|
|
32
38
|
// never re-derives it). Everything the registry does not type (externals,
|
|
33
39
|
// custom elements, vue-dialect declarations, workspace-member definitions
|
|
34
|
-
// outside the graph) is admitted
|
|
35
|
-
//
|
|
40
|
+
// outside the graph) is admitted — except a non-code file, which is never a
|
|
41
|
+
// component.
|
|
36
42
|
//
|
|
37
|
-
//
|
|
43
|
+
// A workspace-member path outside the graph (the bounded
|
|
38
44
|
// resolver pins it but never parses it — a lazy `import("./PhoneInput")`
|
|
39
45
|
// whose target file wasn't walked) is rejected only when its path HAS an
|
|
40
46
|
// extension that is provably not code (`hero.png`, `.SVG`). An
|
|
@@ -65,23 +71,23 @@ export function resolve(graph, opts) {
|
|
|
65
71
|
// O(ownership) `.find()` — matters on large files (hundreds of usages).
|
|
66
72
|
const composition = [];
|
|
67
73
|
// Finalise the occurrences emitted by one usage (the slice since
|
|
68
|
-
// `lengthBefore`): collapse
|
|
74
|
+
// `lengthBefore`): collapse duplicates, then stamp composition info +
|
|
69
75
|
// a `depth` sourced from the ownership entry. Called after each per-usage
|
|
70
76
|
// emit so helper-call fanout (multiple occurrences per usage) carries
|
|
71
77
|
// identical composition metadata across the fan.
|
|
72
78
|
//
|
|
73
|
-
//
|
|
79
|
+
// Dedup: resolveType's `Function` case flatMaps every `return`, so a
|
|
74
80
|
// referenced component whose body has N `return <JSX>` branches yields N
|
|
75
81
|
// folded terminals that all collapse to one identity — emitting N records
|
|
76
82
|
// sharing an `occurrenceId` (componentId + file + line + column + owner).
|
|
77
83
|
// Collapsing is scoped to THIS usage's slice, so genuinely-distinct
|
|
78
|
-
// cross-usage repeats that share an occurrenceId (e.g. a
|
|
84
|
+
// cross-usage repeats that share an occurrenceId (e.g. a prop-forward
|
|
79
85
|
// of one `const x = <Y/>` literal into two sibling call sites) are separate
|
|
80
86
|
// usages and never merged. Within one usage, a shared occurrenceId means
|
|
81
87
|
// same identity AND same owner — always a spurious duplicate. The
|
|
82
88
|
// `composition` sidecar isn't populated for this slice until the stamp loop
|
|
83
89
|
// below, so the compaction only touches `occurrences`.
|
|
84
|
-
// THE per-slice compaction seam (admission +
|
|
90
|
+
// THE per-slice compaction seam (admission + dedup). Every emission
|
|
85
91
|
// path — a usage's slice via `stampComposition`, a holder's argument-site
|
|
86
92
|
// slice below — routes through it; never re-derive admission or dedup at a
|
|
87
93
|
// push site.
|
|
@@ -134,7 +140,7 @@ export function resolve(graph, opts) {
|
|
|
134
140
|
if (!decl) {
|
|
135
141
|
if (!isHostElementRoot(usage.ref)) {
|
|
136
142
|
// No import record and no declaration in scope: the engine cannot
|
|
137
|
-
// attribute this usage. Surface it
|
|
143
|
+
// attribute this usage. Surface it instead of
|
|
138
144
|
// dropping it silently — `info`, because the usual cause is a
|
|
139
145
|
// global or macro-provided binding the scan cannot see.
|
|
140
146
|
opts?.collector?.emit({
|
|
@@ -156,9 +162,9 @@ export function resolve(graph, opts) {
|
|
|
156
162
|
const packageName = packageNameFromSpecifier(matchedImport.specifier);
|
|
157
163
|
// Resolve namespace-import members to their export name once, up front —
|
|
158
164
|
// every export-name / re-export lookup below keys on this, not the raw
|
|
159
|
-
// `imported
|
|
165
|
+
// `imported`. `via.import` keeps the raw `imported` (provenance).
|
|
160
166
|
const effectiveImported = effectiveExportName(matchedImport.imported, usage.ref.memberChain);
|
|
161
|
-
//
|
|
167
|
+
// Lookups below key on `effectiveImported` (the ROOT); the
|
|
162
168
|
// identity stamped on a fallback is the root plus the residual chain.
|
|
163
169
|
// A member the walker resolves to its own declaration never reaches a
|
|
164
170
|
// fallback, so this only names members with no declaration of their own.
|
|
@@ -169,7 +175,7 @@ export function resolve(graph, opts) {
|
|
|
169
175
|
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
170
176
|
if (!resolvedAbsTarget) {
|
|
171
177
|
// A JSX usage whose import failed to resolve is a dropped local
|
|
172
|
-
// reference — surface it instead of vanishing
|
|
178
|
+
// reference — surface it instead of vanishing.
|
|
173
179
|
opts?.collector?.emit({
|
|
174
180
|
code: "unresolved-import",
|
|
175
181
|
severity: "warning",
|
|
@@ -188,12 +194,14 @@ export function resolve(graph, opts) {
|
|
|
188
194
|
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
189
195
|
const folded = walkWithFolding(graph, fileGraph, refType, argMap);
|
|
190
196
|
const jsxTerminals = folded.filter((t) => t.type.kind === "JSX");
|
|
191
|
-
if (jsxTerminals.length === 0)
|
|
197
|
+
if (jsxTerminals.length === 0) {
|
|
198
|
+
emitLazyImportUnsupported(opts, filePath, usage.loc, refType);
|
|
192
199
|
continue;
|
|
200
|
+
}
|
|
193
201
|
// Determine the canonical source file + local export name. When the
|
|
194
202
|
// target is a re-export barrel (`export { X } from './source'`),
|
|
195
203
|
// follow the chain to the actual definition file so JSX edges
|
|
196
|
-
// attribute to the source rather than the barrel
|
|
204
|
+
// attribute to the source rather than the barrel. Without
|
|
197
205
|
// this, two consumers importing the same component through different
|
|
198
206
|
// re-export paths fragment across phantom identities.
|
|
199
207
|
const chained = followReExportChain(graph, resolvedTarget, effectiveImported);
|
|
@@ -228,7 +236,7 @@ export function resolve(graph, opts) {
|
|
|
228
236
|
source: { type: "local", filePath: canonicalFile },
|
|
229
237
|
};
|
|
230
238
|
if (viaOverride !== undefined) {
|
|
231
|
-
//
|
|
239
|
+
// Prop-forward: parser has pre-decided the owner — single-occurrence path.
|
|
232
240
|
const rawOwnerComponentId = ownerIdFromRef(graph, ownerRef, fileGraph.filePath);
|
|
233
241
|
for (const ft of jsxTerminals) {
|
|
234
242
|
const rawComponentId = componentIdFromIdentity(ft.identity, fallbackComponentId, fileGraph.dialect, {
|
|
@@ -297,7 +305,7 @@ export function resolve(graph, opts) {
|
|
|
297
305
|
: resolvedAbsTarget;
|
|
298
306
|
const targetInGraph = resolvedNormTarget !== null && graph.files.has(resolvedNormTarget);
|
|
299
307
|
if (targetInGraph) {
|
|
300
|
-
// Target file is in graph — workspace sibling
|
|
308
|
+
// Target file is in graph — workspace sibling. Resolve to check
|
|
301
309
|
// JSX type, fan out per terminal. The fallback identity is local so
|
|
302
310
|
// null-identity terminals still attribute to the sibling file rather
|
|
303
311
|
// than a phantom external stub. Terminal identities from deriveIdentity
|
|
@@ -305,8 +313,10 @@ export function resolve(graph, opts) {
|
|
|
305
313
|
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
306
314
|
const foldedTerminals = walkWithFolding(graph, fileGraph, refType, argMap);
|
|
307
315
|
const jsxTerminals = foldedTerminals.filter((t) => t.type.kind === "JSX");
|
|
308
|
-
if (jsxTerminals.length === 0)
|
|
316
|
+
if (jsxTerminals.length === 0) {
|
|
317
|
+
emitLazyImportUnsupported(opts, filePath, usage.loc, refType);
|
|
309
318
|
continue;
|
|
319
|
+
}
|
|
310
320
|
const siblingFallback = workspaceSiblingFallback(graph, resolvedNormTarget, matchedImport, effectiveImported, fileGraph.dialect);
|
|
311
321
|
const localFallback = siblingFallback !== undefined && siblingFallback.kind !== "custom-element"
|
|
312
322
|
? { ...siblingFallback, export: compoundExportName(siblingFallback.export, residual) }
|
|
@@ -316,7 +326,7 @@ export function resolve(graph, opts) {
|
|
|
316
326
|
}
|
|
317
327
|
}
|
|
318
328
|
else {
|
|
319
|
-
// Target not in graph. Workspace membership
|
|
329
|
+
// Target not in graph. Workspace membership: a
|
|
320
330
|
// resolved target owned by a workspace member is first-party — emit
|
|
321
331
|
// LOCAL, pinned to the definition file (bounded re-export resolution)
|
|
322
332
|
// so identity is scope-invariant between whole-repo and single-app
|
|
@@ -394,22 +404,22 @@ export function resolve(graph, opts) {
|
|
|
394
404
|
stampComposition(tagLengthBefore, "tag", i, tagDepth, tagParentUsageIdx);
|
|
395
405
|
}
|
|
396
406
|
}
|
|
397
|
-
// Argument-site seeding
|
|
407
|
+
// Argument-site seeding. A module-
|
|
398
408
|
// scope registry member whose value is a call — a wrapper product — is a
|
|
399
409
|
// *holder*. Its own fold decides everything: when the fold reaches JSX and
|
|
400
410
|
// every terminal carries neither an identity nor a `hoc-wrapper` hop, the
|
|
401
411
|
// holder is its own identity
|
|
402
|
-
// (
|
|
412
|
+
// (the callee was visible and built a new function) and credited
|
|
403
413
|
// none of its arguments, so each identifier argument of the OUTERMOST call
|
|
404
414
|
// that resolves to a component gets one occurrence at the argument's own
|
|
405
415
|
// position, owned by the holder. A fold that carried an identity credited
|
|
406
|
-
// an argument already
|
|
416
|
+
// an argument already and seeds nothing — a site is never
|
|
407
417
|
// counted twice. The registry gate keeps an unused, unexported holder from
|
|
408
418
|
// owning anything (no phantom owner by construction). Runs after the usage
|
|
409
419
|
// loops so a holder's slice never interleaves with a usage's; there is no
|
|
410
420
|
// composition sidecar entry because an argument site has no lexical JSX
|
|
411
421
|
// parent.
|
|
412
|
-
//
|
|
422
|
+
// The SAME fold answers membership. A holder whose every JSX
|
|
413
423
|
// terminal carries an identity has credited another identity for every
|
|
414
424
|
// render — it is the route to that identity, not a member. Collected here,
|
|
415
425
|
// applied once through `excludeFoldedHolders` at the return below.
|
|
@@ -431,6 +441,43 @@ export function resolve(graph, opts) {
|
|
|
431
441
|
compactSlice(lengthBefore);
|
|
432
442
|
}
|
|
433
443
|
}
|
|
444
|
+
// Hook argument seeding. A component passed to a hook call inside a
|
|
445
|
+
// top-level function body (`const [modal] = useModalTwoStatic(Modal)`) is
|
|
446
|
+
// used at that site: a hook's product is a value the caller interpolates,
|
|
447
|
+
// never a tag, so no render site will ever pay for the argument and the
|
|
448
|
+
// "fold credited it already" reasoning does not apply. Same argument rules
|
|
449
|
+
// and the same push seam as the holder loop; the owner is
|
|
450
|
+
// the enclosing declaration through `resolveOwnerChain`, so a helper hook
|
|
451
|
+
// fans the seed out to each caller with a `helper-call` hop. A hook is
|
|
452
|
+
// named by `HOOK_NAME`, React's own `isHookName` predicate (rules-of-hooks
|
|
453
|
+
// and the compiler) — there is no structural signal for "hook".
|
|
454
|
+
for (const [filePath, fileGraph] of graph.files) {
|
|
455
|
+
if (fileGraph.dialect === "vue")
|
|
456
|
+
continue;
|
|
457
|
+
for (const bodyCall of fileGraph.bodyCalls) {
|
|
458
|
+
if (!HOOK_NAME.test(bodyCall.callee.symbol) || bodyCall.args.length === 0)
|
|
459
|
+
continue;
|
|
460
|
+
const call = {
|
|
461
|
+
kind: "ReturnTypeOf",
|
|
462
|
+
callee: { kind: "TypeOf", ref: bodyCall.callee },
|
|
463
|
+
args: bodyCall.args,
|
|
464
|
+
};
|
|
465
|
+
const ownerRef = {
|
|
466
|
+
symbol: bodyCall.ownerSymbol,
|
|
467
|
+
scope: MODULE_SCOPE,
|
|
468
|
+
memberChain: [],
|
|
469
|
+
loc: { line: 0, column: 0 },
|
|
470
|
+
originFile: filePath,
|
|
471
|
+
};
|
|
472
|
+
const owners = () => resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard()).map((res) => ({
|
|
473
|
+
rawOwnerComponentId: ownerIdFromResolution(graph, res.ownerDecl),
|
|
474
|
+
viaPrefix: res.viaPrefix,
|
|
475
|
+
}));
|
|
476
|
+
const lengthBefore = occurrences.length;
|
|
477
|
+
pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts);
|
|
478
|
+
compactSlice(lengthBefore);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
434
481
|
// Pass 2: resolve `parentRef` from per-file `parentUsageIdx` ownership data.
|
|
435
482
|
// Pass 1 populated the `composition` sidecar with `(fileUsageIdx, kind)` per
|
|
436
483
|
// emitted occurrence, which lets us build a `(filePath, kind, fileUsageIdx)
|
|
@@ -455,7 +502,7 @@ export function resolve(graph, opts) {
|
|
|
455
502
|
if (parentIdx !== undefined)
|
|
456
503
|
occ.parentRef = parentIdx;
|
|
457
504
|
}
|
|
458
|
-
return { occurrences, registry: excludeFoldedHolders(registry, folded) };
|
|
505
|
+
return { occurrences, registry: excludeHostElementNames(excludeFoldedHolders(registry, folded), graph) };
|
|
459
506
|
}
|
|
460
507
|
/**
|
|
461
508
|
* Build a local-scope ComponentId for a symbol declared in `filePath`.
|
|
@@ -482,7 +529,7 @@ function dialectForFile(graph, filePath) {
|
|
|
482
529
|
return graph.files.get(filePath)?.dialect ?? "react";
|
|
483
530
|
}
|
|
484
531
|
/**
|
|
485
|
-
* Owner ComponentId for a parser-pre-decided owner reference (
|
|
532
|
+
* Owner ComponentId for a parser-pre-decided owner reference (the
|
|
486
533
|
* prop-forward path). `fallbackFile` is the consuming file, used when the ref
|
|
487
534
|
* carries no originFile.
|
|
488
535
|
*
|
|
@@ -513,17 +560,14 @@ function ownerIdFromResolution(graph, ownerDecl) {
|
|
|
513
560
|
* (e.g. `@shoelace/dist/components/button` → `modulePath: "dist/components/button"`)
|
|
514
561
|
* so per-subpath components stay distinct. Relative imports canonicalise to
|
|
515
562
|
* `identity.resolvedFile` (repo-relative POSIX from the moduleResolver) so
|
|
516
|
-
* `'./page'` and `'../page'` from different callers collapse to one identity
|
|
517
|
-
* (#109).
|
|
563
|
+
* `'./page'` and `'../page'` from different callers collapse to one identity.
|
|
518
564
|
*
|
|
519
565
|
* The component kind is derived from `fileDialect` via `componentKindFor` —
|
|
520
566
|
* `"vue"` dialect files emit `vue-component`, all others emit `react-component`.
|
|
521
|
-
* A future `manifestKindHint` parameter will let callers override the
|
|
522
|
-
* dialect-derived default when a manifest entry resolves to a concrete kind.
|
|
523
567
|
* Tag-usage paths emit `custom-element` directly and don't call this helper.
|
|
524
568
|
*
|
|
525
569
|
* When `ctx` is supplied, this is also the single seam that applies the
|
|
526
|
-
* external-leaf relabel
|
|
570
|
+
* external-leaf relabel — see `relabelExternalLeaf`. The specifier fed
|
|
527
571
|
* to the relabel hook is the folded terminal's OWN specifier when `identity`
|
|
528
572
|
* is `"imported"` (it may diverge from the consuming file's own import
|
|
529
573
|
* specifier, e.g. an in-graph barrel re-wrap), falling back to
|
|
@@ -572,7 +616,7 @@ function buildComponentId(identity, fallback, fileDialect) {
|
|
|
572
616
|
* Build the external `ComponentSource` for an import specifier. Stamps
|
|
573
617
|
* `modulePath` with the subpath after the package name when one is present,
|
|
574
618
|
* so subpath imports like `@shoelace/dist/components/button` stay distinct
|
|
575
|
-
* from `/icon
|
|
619
|
+
* from `/icon`. Bare package specs (`react`, `@org/pkg`) omit
|
|
576
620
|
* modulePath. Subpath derivation (including module-extension normalisation, so
|
|
577
621
|
* `react/button` and `react/button.js` collapse to one identity) lives in the
|
|
578
622
|
* `externalSubpath` seam.
|
|
@@ -585,7 +629,7 @@ function externalSource(pkg, specifier) {
|
|
|
585
629
|
}
|
|
586
630
|
/**
|
|
587
631
|
* Relabel an external react-component identity to the leaf package its
|
|
588
|
-
* re-export chain terminates in
|
|
632
|
+
* re-export chain terminates in. No-op unless the hook is supplied,
|
|
589
633
|
* the identity is an external react-component, and the walk crossed a
|
|
590
634
|
* package boundary. The relabeled modulePath routes through the
|
|
591
635
|
* `externalSource`/`externalSubpath` seam so extension normalisation stays
|
|
@@ -597,7 +641,7 @@ function relabelExternalLeaf(rawComponentId, filePath, specifier, resolveExterna
|
|
|
597
641
|
rawComponentId.source.type !== "external") {
|
|
598
642
|
return rawComponentId;
|
|
599
643
|
}
|
|
600
|
-
// A compound export
|
|
644
|
+
// A compound export names a member of the package's ROOT export;
|
|
601
645
|
// the leaf walk resolves the root and the member chain is re-joined onto
|
|
602
646
|
// whatever export the leaf calls it.
|
|
603
647
|
const { root, path } = parseCompoundExport(rawComponentId.export);
|
|
@@ -611,7 +655,7 @@ function relabelExternalLeaf(rawComponentId, filePath, specifier, resolveExterna
|
|
|
611
655
|
};
|
|
612
656
|
}
|
|
613
657
|
/**
|
|
614
|
-
* Build the local fallback ComponentId for a workspace-sibling import
|
|
658
|
+
* Build the local fallback ComponentId for a workspace-sibling import.
|
|
615
659
|
* When a bare-package import (`@org/foo`) resolves to a file in the parsed
|
|
616
660
|
* graph, the package is a workspace sibling — JSX edges should land on the
|
|
617
661
|
* sibling's actual file rather than a phantom external stub.
|
|
@@ -621,11 +665,11 @@ function relabelExternalLeaf(rawComponentId, filePath, specifier, resolveExterna
|
|
|
621
665
|
* default external fallback.
|
|
622
666
|
*/
|
|
623
667
|
function workspaceSiblingFallback(graph, resolvedTarget, matchedImport,
|
|
624
|
-
/** Namespace-member-resolved export name
|
|
668
|
+
/** Namespace-member-resolved export name; see `effectiveExportName`. */
|
|
625
669
|
effectiveImported, consumerDialect) {
|
|
626
670
|
if (!resolvedTarget)
|
|
627
671
|
return undefined;
|
|
628
|
-
// Follow re-exports
|
|
672
|
+
// Follow re-exports — workspace siblings can also use barrels.
|
|
629
673
|
const chained = followReExportChain(graph, resolvedTarget, effectiveImported);
|
|
630
674
|
const canonicalFile = chained?.file ?? resolvedTarget;
|
|
631
675
|
let exportName = chained?.localExport ?? effectiveImported;
|
|
@@ -670,8 +714,8 @@ function computeWraps(rawComponentId, wrapCallback) {
|
|
|
670
714
|
}
|
|
671
715
|
function pushOccurrence(occurrences, filePath, usage, matchedImport,
|
|
672
716
|
/** Export name for the fallback identity — the root export resolved via
|
|
673
|
-
* `effectiveExportName`
|
|
674
|
-
* `compoundExportName
|
|
717
|
+
* `effectiveExportName` joined with the residual member chain via
|
|
718
|
+
* `compoundExportName`. Distinct from `matchedImport.imported`,
|
|
675
719
|
* which stays raw for `via` provenance. */
|
|
676
720
|
effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, fallbackOverride, viaOverride) {
|
|
677
721
|
const fallback = fallbackOverride ?? {
|
|
@@ -694,7 +738,7 @@ effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opt
|
|
|
694
738
|
import: matchedImport.imported,
|
|
695
739
|
};
|
|
696
740
|
if (viaOverride !== undefined) {
|
|
697
|
-
//
|
|
741
|
+
// Prop-forward: parser has pre-decided the owner — single-occurrence path.
|
|
698
742
|
const rawOwnerComponentId = ownerIdFromRef(graph, ownerRef, filePath);
|
|
699
743
|
const viaChain = [viaOverride];
|
|
700
744
|
const provisionalId = serialiseComponentId(rawComponentId);
|
|
@@ -740,6 +784,27 @@ effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opt
|
|
|
740
784
|
});
|
|
741
785
|
}
|
|
742
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* A fold that reached no JSX while an import() sits in the walked value is a
|
|
789
|
+
* lazy shape the pipeline could not resolve to a component: a `.then` callback
|
|
790
|
+
* that does more than pick a property, or a target whose component shape could
|
|
791
|
+
* not be confirmed. Report it at the render site so the miss is visible, and
|
|
792
|
+
* name the file the `import()` is declared in when that is not the render file.
|
|
793
|
+
*/
|
|
794
|
+
function emitLazyImportUnsupported(opts, filePath, loc, value) {
|
|
795
|
+
const di = opts?.collector ? firstDynamicImport(value) : null;
|
|
796
|
+
if (!di)
|
|
797
|
+
return;
|
|
798
|
+
opts?.collector?.emit({
|
|
799
|
+
code: "lazy-import-unsupported",
|
|
800
|
+
severity: "warning",
|
|
801
|
+
filePath,
|
|
802
|
+
line: loc.line,
|
|
803
|
+
column: loc.column,
|
|
804
|
+
specifier: di.specifier,
|
|
805
|
+
detail: `the import() target could not be resolved to a component${di.originFile === filePath ? "" : ` (declared in ${di.originFile})`}`,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
743
808
|
/**
|
|
744
809
|
* Push local-scope occurrences for a symbol declared in the same file (no import).
|
|
745
810
|
* Walks the decl's value via walkWithFolding and fans out per JSX terminal.
|
|
@@ -747,21 +812,22 @@ effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opt
|
|
|
747
812
|
function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGraph, argMap, ownerRef, helperIndex, opts, viaOverride) {
|
|
748
813
|
// A member access on a same-file binding (`<LocalNS.Leaf/>`) walks the
|
|
749
814
|
// MEMBER, not the whole object — `resolveReference` applies the usage's
|
|
750
|
-
// member chain exactly as the import path already gets it (
|
|
751
|
-
//
|
|
815
|
+
// member chain exactly as the import path already gets it (the same
|
|
816
|
+
// static-MemberOf arm). A bare reference is unchanged:
|
|
752
817
|
// `decl.value` is what `resolveReference` returns for an empty chain.
|
|
753
818
|
const value = usage.ref.memberChain.length > 0 ? resolveReference(graph, fileGraph, usage.ref) : decl.value;
|
|
754
819
|
const folded = walkWithFolding(graph, fileGraph, value, argMap);
|
|
755
820
|
const jsxTerminals = folded.filter((t) => t.type.kind === "JSX");
|
|
756
821
|
// A reference that resolves to no JSX terminal — a function parameter
|
|
757
822
|
// (`<Component/>` inside a factory body), a hook-return destructure, or an
|
|
758
|
-
// unresolvable alias — emits
|
|
759
|
-
|
|
760
|
-
|
|
823
|
+
// unresolvable alias — emits no occurrence.
|
|
824
|
+
if (jsxTerminals.length === 0) {
|
|
825
|
+
emitLazyImportUnsupported(opts, filePath, usage.loc, value);
|
|
761
826
|
return;
|
|
827
|
+
}
|
|
762
828
|
const outerVia = viaOverride ?? { kind: "local-component" };
|
|
763
829
|
if (viaOverride !== undefined) {
|
|
764
|
-
//
|
|
830
|
+
// Prop-forward: parser has pre-decided the owner — single-occurrence path.
|
|
765
831
|
const rawOwnerComponentId = ownerIdFromRef(graph, ownerRef, fileGraph.filePath);
|
|
766
832
|
for (const ft of jsxTerminals) {
|
|
767
833
|
const fallback = {
|
|
@@ -828,11 +894,11 @@ function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGrap
|
|
|
828
894
|
}
|
|
829
895
|
}
|
|
830
896
|
/**
|
|
831
|
-
*
|
|
897
|
+
* A holder is folded away iff its fold reaches JSX and EVERY JSX
|
|
832
898
|
* terminal carries an identity — the fold credited someone else for every
|
|
833
899
|
* render. Any identity-less terminal means the holder is its own identity
|
|
834
|
-
* (
|
|
835
|
-
*
|
|
900
|
+
* (`memo(createField(Base))`, `forwardRef((p, r) => <b/>)`).
|
|
901
|
+
* The target is reported only when all terminals name ONE local member of
|
|
836
902
|
* the holder's own file; that member inherits `isDefault` in the seam.
|
|
837
903
|
* Returns `null` when the holder is not folded.
|
|
838
904
|
*/
|
|
@@ -848,8 +914,8 @@ function foldedHolder(holderJsx, filePath) {
|
|
|
848
914
|
return { target: sameFileSingle ? { filePath, export: first.export } : null };
|
|
849
915
|
}
|
|
850
916
|
/**
|
|
851
|
-
* Argument-site seeding for one holder
|
|
852
|
-
*
|
|
917
|
+
* Argument-site seeding for one holder over the fold the caller already ran
|
|
918
|
+
* — the rule is stated at the call site
|
|
853
919
|
* in `resolve`. Identity goes through `componentIdFromIdentity` (the
|
|
854
920
|
* external-leaf relabel seam) and the owner through `ownerIdFromRef`, exactly
|
|
855
921
|
* as every other push path; the binding is named through `argumentProvenance`,
|
|
@@ -857,9 +923,9 @@ function foldedHolder(holderJsx, filePath) {
|
|
|
857
923
|
* (admission + dedup).
|
|
858
924
|
*/
|
|
859
925
|
function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holderJsx, graph, fileGraph, argMap, opts) {
|
|
860
|
-
//
|
|
926
|
+
// The holder is its own identity iff its fold reaches JSX and no
|
|
861
927
|
// terminal carries an identity of its own OR a `hoc-wrapper` hop — the hop
|
|
862
|
-
// is the fold's own record that it credited an argument
|
|
928
|
+
// is the fold's own record that it credited an argument,
|
|
863
929
|
// even when that argument was anonymous (`forwardRef((p, r) => <b/>, x)`),
|
|
864
930
|
// so the holder is a wrapper of it and its other arguments are
|
|
865
931
|
// configuration, not wrapped components.
|
|
@@ -874,15 +940,42 @@ function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holder
|
|
|
874
940
|
loc: holder.loc,
|
|
875
941
|
originFile: filePath,
|
|
876
942
|
};
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
943
|
+
// The holder still seeds (its argument is a real usage), but a
|
|
944
|
+
// declaration that can never be rendered under any of its names is not a
|
|
945
|
+
// component and cannot own — the phantom-owner rule the registry gate
|
|
946
|
+
// above encodes, applied to the one owner this loop builds directly.
|
|
947
|
+
const rawOwnerComponentId = hasOnlyHostElementNames(holder.symbol, fileGraph)
|
|
948
|
+
? undefined
|
|
949
|
+
: ownerIdFromRef(graph, holderRef, filePath);
|
|
950
|
+
pushCallArgumentOccurrences(occurrences, filePath, call, () => [{ rawOwnerComponentId, viaPrefix: [] }], graph, fileGraph, argMap, opts);
|
|
951
|
+
}
|
|
952
|
+
/** React's own hook predicate (`isHookName` in rules-of-hooks and the compiler). */
|
|
953
|
+
const HOOK_NAME = /^use[A-Z0-9]/;
|
|
954
|
+
/**
|
|
955
|
+
* THE argument-site push seam: one `passed-as-argument` occurrence per
|
|
956
|
+
* (identifier argument that resolves to a component) × (owner). The
|
|
957
|
+
* module-scope holder loop and the hook loop in `resolve` both call it;
|
|
958
|
+
* neither re-derives the argument rules or the push.
|
|
959
|
+
*
|
|
960
|
+
* The outermost call's arguments only — an inner call's arguments configure
|
|
961
|
+
* the wrapper, they are not wrapped. Identifier arguments only, each
|
|
962
|
+
* resolved through the walker so aliases, re-exports and import-backed
|
|
963
|
+
* leaves take the same path a render site does; a null identity is skipped,
|
|
964
|
+
* never invented. An identifier that is a host-element name (`useSelector(pick, shallowEqual)`) can never be
|
|
965
|
+
* rendered as a component, whatever it resolves to — an external leaf has
|
|
966
|
+
* no shape to check and would otherwise synthesise a component out of any
|
|
967
|
+
* import; the same predicate the held-reference seam and the roster naming
|
|
968
|
+
* gate apply.
|
|
969
|
+
*
|
|
970
|
+
* `owners` is resolved on the first push and reused: most hook calls
|
|
971
|
+
* (`useState(x)`, `useEffect(fn, deps)`) never reach a push, and resolving
|
|
972
|
+
* the owner chain for each of them measured as roughly half of the hook
|
|
973
|
+
* loop's scan-time cost on a large consumer repo.
|
|
974
|
+
*/
|
|
975
|
+
function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts) {
|
|
976
|
+
let resolvedOwners;
|
|
884
977
|
call.args.forEach((arg, index) => {
|
|
885
|
-
if (arg.kind !== "TypeOf")
|
|
978
|
+
if (arg.kind !== "TypeOf" || isHostElementName(arg.ref.symbol))
|
|
886
979
|
return;
|
|
887
980
|
const outerVia = { kind: "passed-as-argument", index, ...argumentProvenance(call, arg, fileGraph) };
|
|
888
981
|
const argDecl = findLocalDeclaration(fileGraph, arg.ref);
|
|
@@ -898,22 +991,26 @@ function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holder
|
|
|
898
991
|
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
899
992
|
});
|
|
900
993
|
const wraps = computeWraps(rawComponentId, opts?.wrapCallback);
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
filePath,
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
994
|
+
resolvedOwners ??= owners();
|
|
995
|
+
for (const { rawOwnerComponentId, viaPrefix } of resolvedOwners) {
|
|
996
|
+
const viaChain = [...viaPrefix, outerVia, ...ft.viaTrail];
|
|
997
|
+
const occurrenceId = computeOccurrenceId(serialiseComponentId(rawComponentId), filePath, arg.ref.loc.line, arg.ref.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
998
|
+
occurrences.push({
|
|
999
|
+
rawComponentId,
|
|
1000
|
+
filePath,
|
|
1001
|
+
line: arg.ref.loc.line,
|
|
1002
|
+
column: arg.ref.loc.column,
|
|
1003
|
+
via: viaChain[0] ?? outerVia,
|
|
1004
|
+
viaChain,
|
|
1005
|
+
props: {},
|
|
1006
|
+
events: [],
|
|
1007
|
+
occurrenceId,
|
|
1008
|
+
depth: 0,
|
|
1009
|
+
...(argDecl !== null ? { definition: { line: argDecl.loc.line, column: argDecl.loc.column } } : {}),
|
|
1010
|
+
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
1011
|
+
...(wraps !== undefined ? { wraps } : {}),
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
917
1014
|
}
|
|
918
1015
|
});
|
|
919
1016
|
}
|
|
@@ -963,18 +1060,11 @@ function pushTagOccurrence(occurrences, filePath, usage, rawComponentId, ownerRe
|
|
|
963
1060
|
}
|
|
964
1061
|
}
|
|
965
1062
|
/**
|
|
966
|
-
* Decide the `kind` for a JSX-emitted ComponentId.
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
* caller supplies it).
|
|
972
|
-
* 2. Otherwise fall back to the file's dialect — `"vue"` files emit
|
|
973
|
-
* `"vue-component"`, all others emit `"react-component"`.
|
|
974
|
-
*
|
|
975
|
-
* `manifestKindHint` is wired through now so the future plumbing (parser-vue
|
|
976
|
-
* + CEM lookups in A2) is a single-site change rather than another caller
|
|
977
|
-
* sweep. Tag-usage paths (custom-element) don't call this helper.
|
|
1063
|
+
* Decide the `kind` for a JSX-emitted ComponentId. A concrete
|
|
1064
|
+
* `manifestKindHint` is used verbatim; otherwise the file's dialect decides —
|
|
1065
|
+
* `"vue"` files emit `"vue-component"`, all others `"react-component"`. No
|
|
1066
|
+
* caller supplies a hint today. Tag-usage paths (custom-element) don't call
|
|
1067
|
+
* this helper.
|
|
978
1068
|
*/
|
|
979
1069
|
function componentKindFor(fileDialect, manifestKindHint) {
|
|
980
1070
|
if (manifestKindHint !== undefined)
|