@volter/editor-react 0.5.211 → 0.5.212
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/contributions/component-contracts.service.ts +1 -1
- package/dist-node/serving.mjs +14770 -1629
- package/package.json +20 -9
- package/serving/component-prop-worker.ts +3 -19
- package/serving/index.ts +20 -8
- package/serving/source-diagnostics.ts +311 -0
- package/serving/ui-oid-plugin.ts +154 -775
- package/src/source/component-prop-types.ts +6 -62
- package/src/source/drei-detailed-names.ts +24 -0
- package/src/source/oid-transform.ts +166 -376
- package/src/source/plan-csf-story.ts +3 -26
- package/src/source/plan-extract-component.ts +15 -60
- package/src/source/plan-fork-component.ts +25 -92
- package/src/source/plan-named-style.ts +3 -17
- package/src/source/plan-source-edit.ts +9 -40
- package/src/source/r3f-contract-resolver.ts +9 -103
- package/src/source/r3f-physics-binding.ts +28 -186
- package/src/source/r3f-project-contracts.ts +13 -103
- package/src/source/relative-import-specifier.ts +3 -18
- package/src/source/reparent-guard.ts +16 -61
- package/src/source/source-edit-request.ts +2 -20
- package/src/source/ts-ast.ts +5 -95
- package/src/source/utility-class-support.ts +4 -32
- package/src/source/writer.ts +74 -326
- package/src/source/r3f-diagnostic-index.ts +0 -165
- package/src/source/r3f-environment-binding.ts +0 -106
- package/src/source/r3f-joint-binding.ts +0 -228
- package/src/source/r3f-lod-binding.ts +0 -81
- package/src/source/r3f-particle-binding.ts +0 -213
- package/src/source/syntactic-prop-specs.ts +0 -296
|
@@ -1,49 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* component source is transformed at build/dev time to carry a `data-oid` on every
|
|
6
|
-
* JSX element, and an OID -> {file,line,col,component,tag} index is built so the
|
|
7
|
-
* editor can map a clicked element back to its exact source location.
|
|
8
|
-
*
|
|
9
|
-
* Stability (C1): unlike visual-edit's line:col key (which shifts every id below an
|
|
10
|
-
* edit), we key by a CONTENT signature `component:tag:nthOccurrence`, so an edit on
|
|
11
|
-
* an unrelated line keeps existing ids stable. The `data-oid` lives ONLY in the
|
|
12
|
-
* transformed output, never on disk (the writer edits the original source instead).
|
|
13
|
-
*/
|
|
1
|
+
/** Instrument authored JSX with source IDs using the TypeScript compiler API.
|
|
2
|
+
* IDs key file, component, tag, and occurrence, so unrelated line edits keep
|
|
3
|
+
* their identities. Stamps exist only in transform output; source writes use
|
|
4
|
+
* the original files and the recorded index locations. */
|
|
14
5
|
import { isMemberLiteral, memberKey } from './prop-member-writer';
|
|
15
6
|
import ts from 'typescript';
|
|
16
|
-
import {
|
|
17
|
-
environmentBindingsByElement,
|
|
18
|
-
type R3fEnvironmentBinding,
|
|
19
|
-
} from './r3f-environment-binding';
|
|
20
|
-
import { jointBindingsByElement, type R3fJointBinding } from './r3f-joint-binding';
|
|
21
|
-
import { lodBindingsByElement, type R3fLodBinding } from './r3f-lod-binding';
|
|
22
|
-
import { particleBindingsByElement, type R3fParticleBinding } from './r3f-particle-binding';
|
|
7
|
+
import { dreiDetailedLocalNames } from './drei-detailed-names';
|
|
23
8
|
import { physicsBindingsByElement, type R3fPhysicsBinding } from './r3f-physics-binding';
|
|
24
9
|
import { parseAuthoringTsx, refIdentifier } from './ts-ast';
|
|
25
10
|
|
|
26
|
-
import type { DeclaredRootSurface, OidEntry
|
|
27
|
-
export type { ComponentPropSpec, DeclaredRootSurface
|
|
28
|
-
|
|
29
|
-
export type {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
11
|
+
import type { ComponentContract, DeclaredRootSurface, OidEntry as SourceOidEntry } from '@volter/sdk/source-authoring';
|
|
12
|
+
export type { ComponentPropSpec, DeclaredRootSurface } from '@volter/sdk/source-authoring';
|
|
13
|
+
|
|
14
|
+
export type { OidEntry } from '@volter/sdk/source-authoring';
|
|
15
|
+
type OidEntry = SourceOidEntry;
|
|
16
|
+
|
|
17
|
+
/** R3F authoring-convention warning returned by the source diagnostics service. */
|
|
18
|
+
export interface R3fAuthoringDiagnostic {
|
|
19
|
+
code: 'R3F002' | 'R3F003' | 'R3F004' | 'R3F005';
|
|
20
|
+
severity: 'warning';
|
|
21
|
+
file: string;
|
|
22
|
+
/** 1-based source position computed from node.getStart(). */
|
|
23
|
+
line: number;
|
|
24
|
+
col: number;
|
|
25
|
+
/** The component the warning is ABOUT: the definition it names (R3F002/3/5)
|
|
26
|
+
* or the tag at the offending callsite (R3F004). */
|
|
27
|
+
component: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
type R3fTransformProp = 'position' | 'rotation' | 'scale';
|
|
31
|
+
/** The R3F authoring contract of a component definition: the kit's `ComponentContract` and what R3F reads
|
|
32
|
+
* beside it. */
|
|
33
|
+
export interface R3fComponentContract extends ComponentContract {
|
|
34
|
+
root: 'single' | 'multiple' | 'non-spatial' | 'unknown';
|
|
35
|
+
rootTag?: string;
|
|
36
|
+
transformProps: R3fTransformProp[];
|
|
37
|
+
/** The callsite's native `visible` prop is proven to reach the single
|
|
38
|
+
* Object3D root. Omitted is deliberately false/unknown, so an editor never
|
|
39
|
+
* writes a decorative prop that the component silently ignores. */
|
|
40
|
+
visibleProp?: true;
|
|
41
|
+
/** The component drives its root transform through a ref in useFrame and
|
|
42
|
+
* accepts no authored transform. Suppress forwarding and motion-conflict
|
|
43
|
+
* diagnostics for that simulation-owned transform. */
|
|
44
|
+
simulationOwnedTransform?: boolean;
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* What ONE file's own bytes say about which reconciler its JSX targets.
|
|
49
|
+
*
|
|
50
|
+
* DIAGNOSTIC EVIDENCE ONLY — it decides no attribute anywhere. What a file
|
|
51
|
+
* renders is REPORTED (`serving/source-diagnostics.ts`'s `OID003`/`OID004`);
|
|
52
|
+
* which region it belongs to is a declaration or a reach, never a property of
|
|
53
|
+
* its bytes.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately a cheap regex scan, not an AST walk, because it runs in a
|
|
56
|
+
* `transform` hook on every project `.tsx`. COMMENTS are
|
|
57
|
+
* stripped first (see {@link stripCommentsForScan}) so prose that merely
|
|
58
|
+
* NAMES a tag cannot count as rendering it; a mention inside a string still
|
|
59
|
+
* matches, which is deliberate — import specifiers ARE strings.
|
|
60
|
+
*/
|
|
61
|
+
export interface SourceDialectEvidence {
|
|
62
|
+
/** Scanned source includes the R3F package specifier. */
|
|
63
|
+
reconcilerImport: boolean;
|
|
64
|
+
/** Distinct tags observed by the R3F-only JSX scan. */
|
|
65
|
+
r3fOnlyTags: string[];
|
|
66
|
+
/** Distinct tags observed by the DOM-only JSX scan. */
|
|
67
|
+
domOnlyTags: string[];
|
|
68
|
+
}
|
|
47
69
|
|
|
48
70
|
/** A number as the writer reads one: `3`, `-3`, `+0.5` (the writer's `NUMBER_LITERAL_SOURCE`). */
|
|
49
71
|
function signedNumericLiteral(expression: ts.Expression): boolean {
|
|
@@ -202,7 +224,7 @@ const BUILTIN_R3F_CONTRACTS: Readonly<
|
|
|
202
224
|
* Package prop types that are themselves a proved native-root prop surface.
|
|
203
225
|
* Kept beside the component contract because both facts come from the same
|
|
204
226
|
* hand-audited public API. This lets ordinary wrappers preserve the package's
|
|
205
|
-
* own type (`Omit<RigidBodyProps
|
|
227
|
+
* own type (`Omit<RigidBodyProps,...>`) without pretending every imported
|
|
206
228
|
* type is spatial.
|
|
207
229
|
*/
|
|
208
230
|
const BUILTIN_R3F_ROOT_PROP_TYPES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
@@ -299,8 +321,8 @@ function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
|
299
321
|
function returnedExpression(body: ts.ConciseBody): ts.Expression | undefined {
|
|
300
322
|
if (!ts.isBlock(body)) return unwrapExpression(body);
|
|
301
323
|
const returns = body.statements.filter(ts.isReturnStatement);
|
|
302
|
-
if (returns.length !== 1 || !returns[0]
|
|
303
|
-
return unwrapExpression(returns[0]
|
|
324
|
+
if (returns.length !== 1 || !returns[0]!.expression) return undefined;
|
|
325
|
+
return unwrapExpression(returns[0]!.expression);
|
|
304
326
|
}
|
|
305
327
|
|
|
306
328
|
/**
|
|
@@ -385,10 +407,10 @@ function typeAllowsStandardRootProps(
|
|
|
385
407
|
return false;
|
|
386
408
|
}
|
|
387
409
|
|
|
388
|
-
function spatialRootOpening(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return expression.openingElement;
|
|
410
|
+
function spatialRootOpening(
|
|
411
|
+
expression: ts.JsxElement | ts.JsxSelfClosingElement,
|
|
412
|
+
): ts.JsxOpeningLikeElement {
|
|
413
|
+
return ts.isJsxSelfClosingElement(expression) ? expression : expression.openingElement;
|
|
392
414
|
}
|
|
393
415
|
|
|
394
416
|
type FragmentRootCardinality = 0 | 1 | 'multiple' | 'unknown';
|
|
@@ -473,8 +495,7 @@ function jsxChildRootCardinality(
|
|
|
473
495
|
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
474
496
|
return jsxRootCardinality(child, sf, knownContracts);
|
|
475
497
|
}
|
|
476
|
-
|
|
477
|
-
return 'unknown';
|
|
498
|
+
return fragmentRootCardinality(child, sf, knownContracts);
|
|
478
499
|
}
|
|
479
500
|
|
|
480
501
|
function fragmentRootCardinality(
|
|
@@ -527,9 +548,8 @@ function fragmentComponentContract(
|
|
|
527
548
|
* prevent elsewhere in these rules.
|
|
528
549
|
*
|
|
529
550
|
* Recognized by suffix because three's naming is regular here and the set is
|
|
530
|
-
* open
|
|
531
|
-
*
|
|
532
|
-
* scene-level attachables that carry no suffix.
|
|
551
|
+
* open. The three exact names are the scene-level attachables that carry no
|
|
552
|
+
* suffix.
|
|
533
553
|
*/
|
|
534
554
|
function isNonSpatialNativeTag(tag: string): boolean {
|
|
535
555
|
if (/^[A-Z]/.test(tag)) return false;
|
|
@@ -678,7 +698,7 @@ function forwardedObjectProps(
|
|
|
678
698
|
*
|
|
679
699
|
* Optional chaining is part of the pattern, not an edge case: `useRef<
|
|
680
700
|
* THREE.Group>(null)` types `.current` as nullable, so `body.current?.position
|
|
681
|
-
|
|
701
|
+
*.copy(…)` is the ordinary spelling and `body.current.position.copy(…)` the
|
|
682
702
|
* non-null-asserted one. Both must read the same.
|
|
683
703
|
*/
|
|
684
704
|
const RUNTIME_MOTION_MUTATION_RE =
|
|
@@ -743,7 +763,6 @@ function componentContract(
|
|
|
743
763
|
}
|
|
744
764
|
|
|
745
765
|
const opening = spatialRootOpening(expression);
|
|
746
|
-
if (!opening) return { root: 'unknown', transformProps: [] };
|
|
747
766
|
const rootTag = opening.tagName.getText(sf);
|
|
748
767
|
// A material/geometry/attribute root is not a spatial instance at all — see
|
|
749
768
|
// `isNonSpatialNativeTag`. Classified here rather than suppressed at each
|
|
@@ -753,38 +772,15 @@ function componentContract(
|
|
|
753
772
|
if (/^[A-Z]/.test(rootTag) && !delegated) return { root: 'unknown', transformProps: [] };
|
|
754
773
|
if (delegated && delegated.root !== 'single') return delegated;
|
|
755
774
|
const nativeRootTag = delegated?.rootTag ?? rootTag;
|
|
756
|
-
//
|
|
757
|
-
// is forwarded exactly as truly as one written onto the root's own tag — the
|
|
758
|
-
// body is what puts the object in the world, so the callsite literal the hook
|
|
759
|
-
// reads IS this instance's placement (`r3f-physics-binding.ts`). Without this
|
|
760
|
-
// the contract said `Ramp` "does not forward position to its native root"
|
|
761
|
-
// and refused `<Ramp position={[2, -1, 168.55]} />`, the one literal in the
|
|
762
|
-
// whole chain that actually places the ramp.
|
|
775
|
+
// Physics spawn channels forwarded from component props contribute to its contract.
|
|
763
776
|
const rootBinding = physicsBindings.get(opening);
|
|
764
|
-
//
|
|
765
|
-
// NOT forwarded in any sense a write can rely on: the hook may read the
|
|
766
|
-
// callsite literal at spawn, but the per-frame driver has the last word and
|
|
767
|
-
// the authored value is dead on the next tick (racing-game's `Train` — a
|
|
768
|
-
// Kinematic body re-posed from its animated group every frame; the doctor's
|
|
769
|
-
// settle leg measured the authored spawn discarded, digit for digit). Those
|
|
770
|
-
// channels leave the forwarded set here, and the component is classified
|
|
771
|
-
// simulation-owned below, so a callsite write refuses with the sentence
|
|
772
|
-
// that names the real owner instead of persisting a literal that lies.
|
|
777
|
+
// Exclude channels driven each frame through the body API from writable forwarding.
|
|
773
778
|
const apiDriven = (rootBinding?.apiDriven ?? []) as readonly string[];
|
|
774
779
|
const toBinding = ((rootBinding?.forwarded ?? []) as readonly string[]).filter(
|
|
775
780
|
(channel) => !apiDriven.includes(channel),
|
|
776
781
|
);
|
|
777
782
|
const declared = propForwarding(parameters[0], nativeRootTag, sf, typeAliases, builtinPropTypes);
|
|
778
|
-
// A
|
|
779
|
-
// `props: Parameters<typeof WeaponModelBody>[0]` — and spreads them whole.
|
|
780
|
-
// `typeAllowsStandardRootProps` reads type TEXT and aliases, so that spelling
|
|
781
|
-
// reads as "accepts no root props" and the wrapper was reported as forwarding
|
|
782
|
-
// nothing: first-person's `WeaponModel` (the hook-free discriminator check in
|
|
783
|
-
// front of `WeaponModelBody` that its own header documents, bought by runhuman
|
|
784
|
-
// pass 97) drew a permanent R3F002 telling the author to forward
|
|
785
|
-
// position/rotation/scale it already forwards, all three, through `{...props}`.
|
|
786
|
-
// The delegate's contract is resolved just above, so a wrapper whose props are
|
|
787
|
-
// SPELLED as the delegate's accepts exactly what the delegate accepts.
|
|
783
|
+
// A wrapper typed from its delegate accepts the delegate's root props.
|
|
788
784
|
const forwarding =
|
|
789
785
|
!declared.standardRootProps &&
|
|
790
786
|
delegated &&
|
|
@@ -801,25 +797,16 @@ function componentContract(
|
|
|
801
797
|
const forwardsVisible =
|
|
802
798
|
toTag.includes('visible') && (!delegated || delegated.visibleProp === true);
|
|
803
799
|
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
// is exactly as opaque as it was before the table existed — reading the
|
|
807
|
-
// package's root as its own would claim a native root the component never
|
|
808
|
-
// routes anything to, and turn a silent unknown into a "does not forward"
|
|
809
|
-
// warning about source the author cannot act on.
|
|
800
|
+
// An external root contributes authorability only through forwarded channels.
|
|
801
|
+
// Keep wrappers opaque when they expose none of the package transform props.
|
|
810
802
|
if (isBuiltinR3fContract(delegated) && transformProps.length === 0) {
|
|
811
803
|
return { root: 'unknown', transformProps: [] };
|
|
812
804
|
}
|
|
813
805
|
|
|
814
|
-
// WHICH of the forwarded channels a BODY reads, kept because the answer
|
|
815
|
-
// changes what an authored write has to survive (see `bodyForwarded`).
|
|
816
|
-
const bodyForwarded = transformProps.filter((channel) => toBinding.includes(channel));
|
|
817
|
-
|
|
818
806
|
return {
|
|
819
807
|
root: 'single',
|
|
820
808
|
rootTag: nativeRootTag,
|
|
821
809
|
transformProps,
|
|
822
|
-
...(bodyForwarded.length > 0 ? { bodyForwarded } : {}),
|
|
823
810
|
...(forwardsVisible ? { visibleProp: true as const } : {}),
|
|
824
811
|
...simulationOwnership(opening, transformProps, motionRefs),
|
|
825
812
|
// The api-driven case (see `apiDriven` above): the component drives its
|
|
@@ -864,8 +851,8 @@ function componentDefinitions(statement: ts.Statement): R3fComponentDefinition[]
|
|
|
864
851
|
|
|
865
852
|
function collectR3fComponentContracts(
|
|
866
853
|
sf: ts.SourceFile,
|
|
867
|
-
importedContracts: ReadonlyMap<string, R3fComponentContract
|
|
868
|
-
importedTypeAliases: ReadonlyMap<string, R3fTypeAlias
|
|
854
|
+
importedContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
855
|
+
importedTypeAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
869
856
|
): Map<string, R3fComponentContract> {
|
|
870
857
|
// Imported first, so a local declaration of the same name shadows it exactly
|
|
871
858
|
// as JavaScript lexical scope does — the same rule `importedContracts`
|
|
@@ -931,7 +918,7 @@ export function analyzeR3fComponentContracts(
|
|
|
931
918
|
* author states or the host EXECUTES, while a tag list is a guess about a file
|
|
932
919
|
* whose imports it cannot see. What the scan is still for is telling an author
|
|
933
920
|
* what the editor OBSERVED: the `OID003`/`OID004` messages in
|
|
934
|
-
* `
|
|
921
|
+
* `serving/source-diagnostics.ts` name the tags a file renders, and `OID004` —
|
|
935
922
|
* a file rendering both dialects' intrinsics, which one attribute cannot serve
|
|
936
923
|
* — is nothing BUT this observation.
|
|
937
924
|
*
|
|
@@ -939,7 +926,7 @@ export function analyzeR3fComponentContracts(
|
|
|
939
926
|
* element sets, so nothing here can be a DOM tag. `line`, `points`, `sprite`,
|
|
940
927
|
* `audio`, `color` and `fog` are all real R3F intrinsics but are EXCLUDED from
|
|
941
928
|
* this list where SVG/HTML also spells them (`<line>`, `<audio>`) — the list is
|
|
942
|
-
* evidence, not a catalogue
|
|
929
|
+
* evidence, not a catalogue; a wrong entry would falsely report R3F evidence in DOM source.
|
|
943
930
|
* `[a-z][A-Za-z]*(Geometry|Material)` covers the whole geometry/material family
|
|
944
931
|
* (`boxGeometry`, `meshStandardMaterial`, `shaderMaterial`, …) without naming
|
|
945
932
|
* each one; the leading lowercase class is what keeps it from matching a
|
|
@@ -954,7 +941,7 @@ const R3F_ONLY_INTRINSIC_RE =
|
|
|
954
941
|
*
|
|
955
942
|
* DIAGNOSTIC EVIDENCE ONLY, on the same terms as the R3F list above: it
|
|
956
943
|
* classifies no file. What a file renders is reported (`OID003`/`OID004` in
|
|
957
|
-
* `
|
|
944
|
+
* `serving/source-diagnostics.ts`); which region a file BELONGS to is a
|
|
958
945
|
* declaration or a reach, never a property of its bytes. Excludes every name R3F also spells as an intrinsic
|
|
959
946
|
* (`line`, `points`, `sprite`, `audio`, `color`, `fog`, `path`, `circle`), so
|
|
960
947
|
* a hit here is unambiguous.
|
|
@@ -964,24 +951,13 @@ const DOM_ONLY_INTRINSIC_RE =
|
|
|
964
951
|
|
|
965
952
|
function distinctTags(code: string, pattern: RegExp): string[] {
|
|
966
953
|
const found = new Set<string>();
|
|
967
|
-
for (const match of code.matchAll(pattern))
|
|
954
|
+
for (const match of code.matchAll(pattern)) found.add(match[1]!);
|
|
968
955
|
return [...found];
|
|
969
956
|
}
|
|
970
957
|
|
|
971
|
-
/**
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
* dev-tools JSDoc names its command shape `cheat.<group>.<label>` — `group`
|
|
975
|
-
* is an R3F-only intrinsic and `label` a DOM-only one, so ONE comment line
|
|
976
|
-
* made a fresh scaffold boot with a dialect-mixing diagnostic about a file
|
|
977
|
-
* with no JSX in it. A diagnostic channel agents are told to trust cannot
|
|
978
|
-
* cry wolf over prose.
|
|
979
|
-
*
|
|
980
|
-
* Single pass, no AST — the transform-hook perf bar stands. Known residual
|
|
981
|
-
* imprecision, accepted: a regex literal containing `//` or `/*` is misread
|
|
982
|
-
* as a comment and drops the rest of itself — which degrades toward "no
|
|
983
|
-
* evidence" (the safe default), never toward a fabricated tag.
|
|
984
|
-
*/
|
|
958
|
+
/** Strip comments before dialect scanning and preserve quoted string literals.
|
|
959
|
+
* This bounded scan treats comment markers inside regex literals as comments,
|
|
960
|
+
* which can discard evidence but cannot fabricate a rendered tag. */
|
|
985
961
|
function stripCommentsForScan(code: string): string {
|
|
986
962
|
const kept: string[] = [];
|
|
987
963
|
let i = 0;
|
|
@@ -1041,60 +1017,18 @@ export function sourceDialectEvidence(code: string): SourceDialectEvidence {
|
|
|
1041
1017
|
};
|
|
1042
1018
|
}
|
|
1043
1019
|
|
|
1044
|
-
/**
|
|
1020
|
+
/** Whether the scan finds R3F import or intrinsic-tag evidence in this file. */
|
|
1045
1021
|
export function sourceProvesR3f(evidence: SourceDialectEvidence): boolean {
|
|
1046
1022
|
return evidence.reconcilerImport || evidence.r3fOnlyTags.length > 0;
|
|
1047
1023
|
}
|
|
1048
1024
|
|
|
1049
|
-
/**
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
* 1. **The region (authoritative).** `surface` is the answer
|
|
1053
|
-
* `file-region-resolver.ts` gave for THIS file — a declaration the game
|
|
1054
|
-
* made (a region `include` glob, a region `mounts` entry, or the manifest
|
|
1055
|
-
* root whose `entry` this file is) or the region whose import closure
|
|
1056
|
-
* reaches it. `'three'` stamps `userData-oid`; `'canvas'`/`'dom'` stamp
|
|
1057
|
-
* `data-oid`. It is resolved from OUTSIDE the file, exactly where the fact
|
|
1058
|
-
* lives, which is what makes it right for a world whose entry
|
|
1059
|
-
* default-exports a component and does not import `@react-three/fiber`
|
|
1060
|
-
* (legal: R3F's global JSX-intrinsics augmentation,
|
|
1061
|
-
* or an entry composed entirely of already-typed child components, needs no
|
|
1062
|
-
* import in THIS file). Every call site threads it from a context that
|
|
1063
|
-
* resolved it per file: `binding-resolver.ts`'s per-surface resolvers, the
|
|
1064
|
-
* browser bundle's own per-file resolution
|
|
1065
|
-
* (`browser-transpile.ts`), and the dev server's
|
|
1066
|
-
* `server/project-root-surface.ts` — shared with `classifyProjectHotUpdate`,
|
|
1067
|
-
* the HMR half of this same decision, so the two cannot drift apart by hand.
|
|
1068
|
-
*
|
|
1069
|
-
* 2. **The DEFAULT.** When `surface` is `undefined` — no region placed the
|
|
1070
|
-
* file (an ad-hoc file with no `volter.project.json` above it, or a module no
|
|
1071
|
-
* region's import closure reaches and none declares) — the answer is
|
|
1072
|
-
* `data-oid`, the documented default, and the CALLER says so out loud
|
|
1073
|
-
* (`server/project-root-surface.ts`'s `OID001`/`OID002`). There is
|
|
1074
|
-
* deliberately no source-text rung: a probe over a file's own bytes answered
|
|
1075
|
-
* for a file it could not place, silently, and being wrong that way is
|
|
1076
|
-
* indistinguishable from being right. Zero inference (ARCHITECTURE-CORE
|
|
1077
|
-
* §The editor protocol) makes the fix a declaration — an `include` glob or
|
|
1078
|
-
* a `mounts` entry in `editor/volter.adapter.ts` — not a better heuristic.
|
|
1079
|
-
*
|
|
1080
|
-
* Nothing about a file's own bytes outranks the region — not even a file that
|
|
1081
|
-
* renders THREE host elements while its region says `dom`. That disagreement is
|
|
1082
|
-
* FATAL rather than noisy (`data-oid` on a THREE object pierces inside fiber,
|
|
1083
|
-
* writes `object.data`, and throws on the next apply), which is exactly why it
|
|
1084
|
-
* is reported at full volume as declared-vs-measured DRIFT
|
|
1085
|
-
* (`server/project-root-surface.ts`'s `OID003`) and repaired by fixing the
|
|
1086
|
-
* declaration. A stamp derived from the bytes instead would make the
|
|
1087
|
-
* declaration untestable, which is how a wrong one stays invisible.
|
|
1088
|
-
*
|
|
1089
|
-
* Lives HERE, beside the transform it parameterizes, rather than in
|
|
1090
|
-
* `vite-plugin-ui-oid.ts` (which re-exports it for its existing callers):
|
|
1091
|
-
* a caller in a page reaches the same `transformSource`, and that path cannot
|
|
1092
|
-
* import a `node:fs`-bearing module.
|
|
1093
|
-
*/
|
|
1025
|
+
/** Choose the source-ID attribute from the owning region: stage uses
|
|
1026
|
+
* userData-oid; DOM, canvas, and unplaced files use data-oid. Source dialect
|
|
1027
|
+
* diagnostics report disagreement without overriding the region declaration. */
|
|
1094
1028
|
export function oidAttributeForSurface(
|
|
1095
1029
|
declaredSurface?: DeclaredRootSurface,
|
|
1096
1030
|
): 'data-oid' | 'userData-oid' {
|
|
1097
|
-
return declaredSurface === '
|
|
1031
|
+
return declaredSurface === 'stage' ? 'userData-oid' : 'data-oid';
|
|
1098
1032
|
}
|
|
1099
1033
|
|
|
1100
1034
|
/**
|
|
@@ -1142,49 +1076,18 @@ const SEEDED_USER_DATA_TAGS = new Set<string>([
|
|
|
1142
1076
|
|
|
1143
1077
|
/** Options for {@link transformSource}. */
|
|
1144
1078
|
export interface TransformSourceOptions {
|
|
1145
|
-
/**
|
|
1146
|
-
*
|
|
1147
|
-
* R3F-dialect files (spike W1) pass `userData-oid` instead:
|
|
1148
|
-
* react-three-fiber's dashed-prop piercing writes `object.userData.oid`
|
|
1149
|
-
* on the constructed Object3D at reconcile time — zero runtime cost, and
|
|
1150
|
-
* the editor's R3F authoring adapter reads it back off the live scene
|
|
1151
|
-
* graph. The OID→{file,line,col} index is attribute-agnostic (keyed by
|
|
1152
|
-
* oid only), so every `/__ui-source/*` endpoint works unchanged for
|
|
1153
|
-
* either dialect.
|
|
1154
|
-
*/
|
|
1079
|
+
/** Attribute stamped on JSX elements. R3F uses userData-oid to place the
|
|
1080
|
+
* identity on the constructed Three object; DOM uses data-oid. */
|
|
1155
1081
|
attribute?: string;
|
|
1156
|
-
/** Contracts for ordinary project-local imported component bindings. */
|
|
1157
|
-
importedR3fContracts?: ReadonlyMap<string, R3fComponentContract>;
|
|
1158
|
-
/** The prop-shape type aliases those same imports bring into scope. A
|
|
1159
|
-
* project that writes its canonical prop shape once and imports it is
|
|
1160
|
-
* otherwise read as forwarding nothing. */
|
|
1161
|
-
importedR3fTypeAliases?: ReadonlyMap<string, R3fTypeAlias>;
|
|
1162
1082
|
/** The file is mounted by @pixi/react. Local component callsites propagate
|
|
1163
1083
|
* their OID onto the component's first native Pixi host, exactly as R3F
|
|
1164
1084
|
* does with userData, but through ordinary data-* constructor fields. */
|
|
1165
1085
|
canvasComponents?: boolean;
|
|
1166
1086
|
}
|
|
1167
1087
|
|
|
1168
|
-
/**
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
* reconciler. `<Html>` creates a real `<div>`, mounts a `createRoot` on it and
|
|
1172
|
-
* renders its children there, so every host element inside it is a DOM element
|
|
1173
|
-
* and the R3F-pierced identity props this transform stamps on host elements
|
|
1174
|
-
* (`userData-oid`, `userData-authoringInstance`, `userData-authoringLabel`,
|
|
1175
|
-
* and the `userData={{}}` seed) have nowhere to land.
|
|
1176
|
-
*
|
|
1177
|
-
* Measured: the starter template's own `BotHud` — screen-pinned dev chrome
|
|
1178
|
-
* built on `<Html>`, the documented way to put DOM in a Three world — logged
|
|
1179
|
-
* FOUR `React does not recognize the \`userData-…\` prop on a DOM element`
|
|
1180
|
-
* errors on every bot run, one per stamped prop. Inside an `<Html>` subtree we
|
|
1181
|
-
* therefore stamp `data-oid`, byte-for-byte what a `dom` root gets.
|
|
1182
|
-
*
|
|
1183
|
-
* Lexical by design: this reads the subtree written in THIS file, which is the
|
|
1184
|
-
* whole extent the transform can see. A component defined elsewhere and
|
|
1185
|
-
* rendered inside `<Html>` is transformed by its own pass against its own
|
|
1186
|
-
* declared surface.
|
|
1187
|
-
*/
|
|
1088
|
+
/** Local JSX names imported as Drei Html. Its descendants render through
|
|
1089
|
+
* react-dom and receive data-oid rather than R3F identity props. The decision
|
|
1090
|
+
* is lexical to this file; imported components receive their own transform. */
|
|
1188
1091
|
function dreiHtmlLocalNames(sf: ts.SourceFile): Set<string> {
|
|
1189
1092
|
const names = new Set<string>();
|
|
1190
1093
|
for (const statement of sf.statements) {
|
|
@@ -1203,36 +1106,10 @@ function dreiHtmlLocalNames(sf: ts.SourceFile): Set<string> {
|
|
|
1203
1106
|
return names;
|
|
1204
1107
|
}
|
|
1205
1108
|
|
|
1206
|
-
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
* "source this editor walks" and "a dependency it never guesses about".
|
|
1211
|
-
*
|
|
1212
|
-
* Why the callsite stamp must skip them: `__volterOid`/`__volterLabel` are a
|
|
1213
|
-
* CONTRACT BETWEEN TWO HALVES OF THIS TRANSFORM. The callsite half writes
|
|
1214
|
-
* them; the only thing that ever reads them is the callee half —
|
|
1215
|
-
* `componentInstanceExpressions` destructuring them out of the component's own
|
|
1216
|
-
* parameters and re-emitting them as `userData-authoringInstance` on that
|
|
1217
|
-
* component's host elements. Nothing anywhere reads them off a React instance
|
|
1218
|
-
* or a DOM node. A package module is never served through this transform, so
|
|
1219
|
-
* the callee half never runs for it: the props have no reader by construction,
|
|
1220
|
-
* and the only thing they can still do is escape.
|
|
1221
|
-
*
|
|
1222
|
-
* And they do. Measured on the vendored `racing-game` ingest: `App.tsx` is a
|
|
1223
|
-
* MIXED file (an R3F `Scene` and a DOM `Hud` in one module, so the file's one
|
|
1224
|
-
* dialect is `userData-oid`), and its `<Canvas>` — `@react-three/fiber`'s own,
|
|
1225
|
-
* which spreads its rest props onto the `<div>` it wraps the canvas in — put
|
|
1226
|
-
* `__volterOid` and `__volterLabel` straight into the DOM, for two permanent
|
|
1227
|
-
* `React does not recognize the \`__volterOid\` prop on a DOM element` errors on
|
|
1228
|
-
* every mount of a real game. Nothing was lost by not stamping it, because
|
|
1229
|
-
* nothing was reading it.
|
|
1230
|
-
*
|
|
1231
|
-
* The one package component that DOES carry editor identity earns it through
|
|
1232
|
-
* the hand-audited contract table instead (`BUILTIN_R3F_CONTRACTS`), which
|
|
1233
|
-
* describes the native root the package forwards to — a claim about a specific
|
|
1234
|
-
* package's public API, never a guess made at a callsite.
|
|
1235
|
-
*/
|
|
1109
|
+
/** Local names imported from installed packages. Those modules do not run
|
|
1110
|
+
* the callee half of this transform, so private component-instance props
|
|
1111
|
+
* would have no reader and could escape through rest-prop spreads. Known
|
|
1112
|
+
* native Three wrappers receive host OID stamps at their own handling site. */
|
|
1236
1113
|
function packageImportedLocalNames(sf: ts.SourceFile): Set<string> {
|
|
1237
1114
|
const names = new Set<string>();
|
|
1238
1115
|
for (const statement of sf.statements) {
|
|
@@ -1280,35 +1157,10 @@ function dreiCameraLocalNames(sf: ts.SourceFile): Set<string> {
|
|
|
1280
1157
|
return names;
|
|
1281
1158
|
}
|
|
1282
1159
|
|
|
1283
|
-
/**
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1288
|
-
* `__volterOid`/`__volterLabel` are a contract between two halves of this
|
|
1289
|
-
* transform: the callsite half writes them, the callee half destructures them
|
|
1290
|
-
* out of the component's own parameters. A tag bound to a parameter names
|
|
1291
|
-
* WHATEVER the caller passed, so this module cannot know whether the callee
|
|
1292
|
-
* half ever ran for it — and where it did not, the props' only remaining
|
|
1293
|
-
* behaviour is to escape into the DOM through any rest-prop spread.
|
|
1294
|
-
*
|
|
1295
|
-
* MEASURED on the vendored `racing-game` ingest, against the SERVED modules:
|
|
1296
|
-
* `useToggle.tsx`'s `<ToggledComponent {...props} />` (its `ToggledComponent`
|
|
1297
|
-
* is a parameter — drei's `Stats`/`OrbitControls`, cannon's `Debug`, or a
|
|
1298
|
-
* project component, depending on the call) and every R3F `*.stories.tsx`
|
|
1299
|
-
* decorator's `<Story />`. Two standing
|
|
1300
|
-
* `React does not recognize the \`__volterOid\` prop on a DOM element` errors on
|
|
1301
|
-
* every run of the acceptance portfolio.
|
|
1302
|
-
*
|
|
1303
|
-
* Nothing is lost by not stamping, and identity actually IMPROVES: a forwarder
|
|
1304
|
-
* spreads the caller's own props first, so the oid the REAL callsite wrote
|
|
1305
|
-
* (`<ToggledMap/>` in `App.tsx`) now reaches the wrapped component instead of
|
|
1306
|
-
* being overwritten by a literal naming the forwarder's own line.
|
|
1307
|
-
*
|
|
1308
|
-
* Scope is the whole file rather than lexical scope on purpose: a name that is
|
|
1309
|
-
* a parameter ANYWHERE in a module is not a stable component identity to stamp
|
|
1310
|
-
* against, and a per-scope walk would buy precision this decision does not use.
|
|
1311
|
-
*/
|
|
1160
|
+
/** Component-shaped names bound by function parameters. Their callees are
|
|
1161
|
+
* unknown to this transform, so preserve caller identity rather than adding
|
|
1162
|
+
* private instance props that may escape to DOM hosts. Names are collected
|
|
1163
|
+
* across the whole file, without resolving individual lexical scopes. */
|
|
1312
1164
|
function parameterBoundLocalNames(sf: ts.SourceFile): Set<string> {
|
|
1313
1165
|
const names = new Set<string>();
|
|
1314
1166
|
const addBinding = (name: ts.BindingName): void => {
|
|
@@ -1359,16 +1211,6 @@ function reactFragmentNames(sf: ts.SourceFile): Set<string> {
|
|
|
1359
1211
|
return names;
|
|
1360
1212
|
}
|
|
1361
1213
|
|
|
1362
|
-
/**
|
|
1363
|
-
* Transform source: insert `data-oid="…"` (or `opts.attribute="…"`) on every
|
|
1364
|
-
* JSX element and register the index. In the R3F dialect, a custom component
|
|
1365
|
-
* callsite receives editor-only props whose values are propagated onto the
|
|
1366
|
-
* component's existing host Object3Ds. Every transformed project component,
|
|
1367
|
-
* on every surface, consumes those props at its parameter boundary so a
|
|
1368
|
-
* cross-surface component or ordinary `{...props}` spread cannot forward them
|
|
1369
|
-
* into game DOM. That preserves per-instance source identity without inserting
|
|
1370
|
-
* a wrapper into the native tree. Returns the transformed code + index entries.
|
|
1371
|
-
*/
|
|
1372
1214
|
/** Whether any JSX (an element, a self-closing element or a fragment) appears under `node`. */
|
|
1373
1215
|
function containsJsx(node: ts.Node): boolean {
|
|
1374
1216
|
let found = false;
|
|
@@ -1384,6 +1226,16 @@ function containsJsx(node: ts.Node): boolean {
|
|
|
1384
1226
|
return found;
|
|
1385
1227
|
}
|
|
1386
1228
|
|
|
1229
|
+
/**
|
|
1230
|
+
* Transform source: insert `data-oid="…"` (or `opts.attribute="…"`) on every
|
|
1231
|
+
* JSX element and register the index. In the R3F dialect, a custom component
|
|
1232
|
+
* callsite receives editor-only props whose values are propagated onto the
|
|
1233
|
+
* component's existing host Object3Ds. Object- and identifier-bound props
|
|
1234
|
+
* consume those fields at the parameter boundary, so ordinary `{...props}`
|
|
1235
|
+
* spreads do not forward them into project DOM. That preserves per-instance
|
|
1236
|
+
* source identity without inserting a wrapper into the native tree. Returns
|
|
1237
|
+
* the transformed code + index entries.
|
|
1238
|
+
*/
|
|
1387
1239
|
export function transformSource(
|
|
1388
1240
|
code: string,
|
|
1389
1241
|
file: string,
|
|
@@ -1397,30 +1249,7 @@ export function transformSource(
|
|
|
1397
1249
|
attribute === 'userData-oid' || opts?.canvasComponents === true;
|
|
1398
1250
|
const sourceEdits: SourceEdit[] = [];
|
|
1399
1251
|
const entries: OidEntry[] = [];
|
|
1400
|
-
const
|
|
1401
|
-
attribute === 'userData-oid'
|
|
1402
|
-
? collectR3fComponentContracts(sf, opts?.importedR3fContracts, opts?.importedR3fTypeAliases)
|
|
1403
|
-
: new Map();
|
|
1404
|
-
const physicsBindings =
|
|
1405
|
-
attribute === 'userData-oid'
|
|
1406
|
-
? physicsBindingsByElement(sf)
|
|
1407
|
-
: new Map<ts.JsxOpeningLikeElement, R3fPhysicsBinding>();
|
|
1408
|
-
const jointBindings =
|
|
1409
|
-
attribute === 'userData-oid'
|
|
1410
|
-
? jointBindingsByElement(sf)
|
|
1411
|
-
: new Map<ts.JsxOpeningLikeElement, readonly R3fJointBinding[]>();
|
|
1412
|
-
const particleBindings =
|
|
1413
|
-
attribute === 'userData-oid'
|
|
1414
|
-
? particleBindingsByElement(sf)
|
|
1415
|
-
: new Map<ts.JsxOpeningLikeElement, R3fParticleBinding>();
|
|
1416
|
-
const lodBindings =
|
|
1417
|
-
attribute === 'userData-oid'
|
|
1418
|
-
? lodBindingsByElement(sf)
|
|
1419
|
-
: new Map<ts.JsxOpeningLikeElement, R3fLodBinding>();
|
|
1420
|
-
const environmentBindings =
|
|
1421
|
-
attribute === 'userData-oid'
|
|
1422
|
-
? environmentBindingsByElement(sf)
|
|
1423
|
-
: new Map<ts.JsxOpeningLikeElement, R3fEnvironmentBinding>();
|
|
1252
|
+
const detailedNames = attribute === 'userData-oid' ? dreiDetailedLocalNames(sf) : new Set<string>();
|
|
1424
1253
|
const componentStack: Array<{
|
|
1425
1254
|
name: string;
|
|
1426
1255
|
instanceExpression: string;
|
|
@@ -1447,9 +1276,7 @@ export function transformSource(
|
|
|
1447
1276
|
text: '__volterOid, __volterLabel, ',
|
|
1448
1277
|
});
|
|
1449
1278
|
// After the WHOLE parameter, including a type annotation:
|
|
1450
|
-
// `({ name
|
|
1451
|
-
// Inserting at the binding's end produced `} = {}: T`, which is not
|
|
1452
|
-
// TypeScript and broke every typed scene export the look verb imports.
|
|
1279
|
+
// `({ name,...props }: T)` must become `({ __volterOid, …, name,...props }: T = {})`.
|
|
1453
1280
|
if (!first.initializer) {
|
|
1454
1281
|
sourceEdits.push({ pos: first.getEnd(), text: ' = {}' });
|
|
1455
1282
|
}
|
|
@@ -1459,7 +1286,7 @@ export function transformSource(
|
|
|
1459
1286
|
// Preserve the authored identifier as the REST binding. Code below the
|
|
1460
1287
|
// parameter continues to read `props`, but it now sees only game props:
|
|
1461
1288
|
// `function Card(props: Props)` becomes
|
|
1462
|
-
// `function Card({ __volterOid, __volterLabel
|
|
1289
|
+
// `function Card({ __volterOid, __volterLabel,...props }: Props)`.
|
|
1463
1290
|
// Remove an optional marker while replacing the identifier; a binding
|
|
1464
1291
|
// pattern cannot itself be optional, so its equivalent is a `{}` default.
|
|
1465
1292
|
sourceEdits.push({
|
|
@@ -1476,21 +1303,17 @@ export function transformSource(
|
|
|
1476
1303
|
};
|
|
1477
1304
|
|
|
1478
1305
|
const pushComponent = (
|
|
1479
|
-
name: string
|
|
1480
|
-
parameters: ts.NodeArray<ts.ParameterDeclaration
|
|
1306
|
+
name: string,
|
|
1307
|
+
parameters: ts.NodeArray<ts.ParameterDeclaration>,
|
|
1481
1308
|
body: ts.Node,
|
|
1482
1309
|
): boolean => {
|
|
1483
|
-
if (
|
|
1484
|
-
//
|
|
1485
|
-
// helper like `const PATCH = (key) => ({ key, … })` had its parameter rewritten into props
|
|
1486
|
-
// destructuring and received an object of its string's characters. Nothing in it is stamped,
|
|
1487
|
-
// and it consumes no transport props, so it is left as written.
|
|
1310
|
+
if (!/^[A-Z]/.test(name)) return false;
|
|
1311
|
+
// Only functions containing JSX consume the component transport props.
|
|
1488
1312
|
if (!containsJsx(body)) return false;
|
|
1489
1313
|
// Component membership participates in the stable OID signature on every
|
|
1490
1314
|
// JSX dialect. Three/Canvas callsites inject the transport props, while
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
// leaking editor metadata through its own rest props.
|
|
1315
|
+
// supported props bindings consume them before spreading ordinary props
|
|
1316
|
+
// into a DOM host.
|
|
1494
1317
|
componentStack.push({ name, ...componentInstanceExpressions(parameters) });
|
|
1495
1318
|
return true;
|
|
1496
1319
|
};
|
|
@@ -1539,7 +1362,6 @@ export function transformSource(
|
|
|
1539
1362
|
const tag = node.tagName.getText(sf);
|
|
1540
1363
|
if (fragmentNames.has(tag)) {
|
|
1541
1364
|
ts.forEachChild(node, visit);
|
|
1542
|
-
if (pushed) componentStack.pop();
|
|
1543
1365
|
return;
|
|
1544
1366
|
}
|
|
1545
1367
|
const componentContext = componentStack[componentStack.length - 1];
|
|
@@ -1553,10 +1375,6 @@ export function transformSource(
|
|
|
1553
1375
|
// `dreiHtmlLocalNames`. Everything below reads this, not `attribute`.
|
|
1554
1376
|
const elementAttribute = domSubtreeDepth > 0 ? 'data-oid' : attribute;
|
|
1555
1377
|
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
1556
|
-
const localContract =
|
|
1557
|
-
elementAttribute === 'userData-oid' && /^[A-Z]/.test(tag)
|
|
1558
|
-
? r3fComponentContracts.get(tag)
|
|
1559
|
-
: undefined;
|
|
1560
1378
|
const entry: OidEntry = {
|
|
1561
1379
|
file,
|
|
1562
1380
|
line: line + 1,
|
|
@@ -1564,14 +1382,6 @@ export function transformSource(
|
|
|
1564
1382
|
component,
|
|
1565
1383
|
tag,
|
|
1566
1384
|
authoredProps: authoredPropsOf(node, sf),
|
|
1567
|
-
...(localContract ? { r3fAuthoring: localContract } : {}),
|
|
1568
|
-
...(physicsBindings.has(node) ? { physicsBinding: physicsBindings.get(node)! } : {}),
|
|
1569
|
-
...(jointBindings.has(node) ? { jointBindings: jointBindings.get(node)! } : {}),
|
|
1570
|
-
...(particleBindings.has(node) ? { particleBinding: particleBindings.get(node)! } : {}),
|
|
1571
|
-
...(lodBindings.has(node) ? { lodBinding: lodBindings.get(node)! } : {}),
|
|
1572
|
-
...(environmentBindings.has(node)
|
|
1573
|
-
? { environmentBinding: environmentBindings.get(node)! }
|
|
1574
|
-
: {}),
|
|
1575
1385
|
...(jsxStack.length > 0 ? { parentOid: jsxStack[jsxStack.length - 1]! } : {}),
|
|
1576
1386
|
};
|
|
1577
1387
|
store.index.set(oid, entry);
|
|
@@ -1581,51 +1391,47 @@ export function transformSource(
|
|
|
1581
1391
|
(elementAttribute === 'userData-oid' || opts?.canvasComponents === true) &&
|
|
1582
1392
|
/^[A-Z]/.test(tag);
|
|
1583
1393
|
if (isProjectCustomComponent) {
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
sourceEdits.push({ pos: node.attributes.end, text: ` userData-oid="${oid}"` });
|
|
1598
|
-
}
|
|
1599
|
-
ts.forEachChild(node, visit);
|
|
1600
|
-
if (pushed) componentStack.pop();
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
// A tag bound to a PARAMETER names whatever the caller passed, so the
|
|
1604
|
-
// callee half may never have run for it either — same withholding,
|
|
1605
|
-
// same reason, and the callsite stays indexed. See
|
|
1606
|
-
// {@link parameterBoundLocalNames}.
|
|
1607
|
-
if (parameterNames.has(tag.split('.')[0] ?? tag)) {
|
|
1608
|
-
ts.forEachChild(node, visit);
|
|
1609
|
-
if (pushed) componentStack.pop();
|
|
1610
|
-
return;
|
|
1394
|
+
const tagRoot = tag.split('.')[0]!;
|
|
1395
|
+
// An installed package's module never passes through this transform,
|
|
1396
|
+
// so the callee half of the `__volterOid` contract never runs for it and
|
|
1397
|
+
// the props can only escape (into the DOM, via any package component
|
|
1398
|
+
// that spreads its rest props onto a host element). The callsite is
|
|
1399
|
+
// still INDEXED above — the source entry, its parent and its contract
|
|
1400
|
+
// are unchanged; only the unreadable runtime prop is withheld. See
|
|
1401
|
+
// {@link packageImportedLocalNames}.
|
|
1402
|
+
if (packageNames.has(tagRoot)) {
|
|
1403
|
+
// Detailed and the two Drei cameras spread their remaining props
|
|
1404
|
+
// onto a native Three host, where the ordinary R3F OID belongs.
|
|
1405
|
+
if (detailedNames.has(tag) || dreiCameraNames.has(tag)) {
|
|
1406
|
+
sourceEdits.push({ pos: node.attributes.end, text: ` userData-oid="${oid}"` });
|
|
1611
1407
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
? `__volterLabel={${authoredName.initializer.expression?.getText(sf) ?? JSON.stringify(tag)}}`
|
|
1623
|
-
: `__volterLabel=${JSON.stringify(label)}`;
|
|
1624
|
-
sourceEdits.push({
|
|
1625
|
-
pos: node.attributes.end,
|
|
1626
|
-
text: ` __volterOid="${oid}" ${labelAttribute}`,
|
|
1627
|
-
});
|
|
1408
|
+
ts.forEachChild(node, visit);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
// A tag bound to a PARAMETER names whatever the caller passed, so the
|
|
1412
|
+
// callee half may never have run for it either — same withholding,
|
|
1413
|
+
// same reason, and the callsite stays indexed. See
|
|
1414
|
+
// {@link parameterBoundLocalNames}.
|
|
1415
|
+
if (parameterNames.has(tagRoot)) {
|
|
1416
|
+
ts.forEachChild(node, visit);
|
|
1417
|
+
return;
|
|
1628
1418
|
}
|
|
1419
|
+
const authoredName = node.attributes.properties.find(
|
|
1420
|
+
(property): property is ts.JsxAttribute =>
|
|
1421
|
+
ts.isJsxAttribute(property) && property.name.getText(sf) === 'name',
|
|
1422
|
+
);
|
|
1423
|
+
const label =
|
|
1424
|
+
authoredName?.initializer && ts.isStringLiteral(authoredName.initializer)
|
|
1425
|
+
? authoredName.initializer.text
|
|
1426
|
+
: tag;
|
|
1427
|
+
const labelAttribute =
|
|
1428
|
+
authoredName?.initializer && ts.isJsxExpression(authoredName.initializer)
|
|
1429
|
+
? `__volterLabel={${authoredName.initializer.expression?.getText(sf) ?? JSON.stringify(tag)}}`
|
|
1430
|
+
: `__volterLabel=${JSON.stringify(label)}`;
|
|
1431
|
+
sourceEdits.push({
|
|
1432
|
+
pos: node.attributes.end,
|
|
1433
|
+
text: ` __volterOid="${oid}" ${labelAttribute}`,
|
|
1434
|
+
});
|
|
1629
1435
|
} else {
|
|
1630
1436
|
// Host elements carry their identity directly.
|
|
1631
1437
|
// Append after authored attributes. In R3F, an authored `userData={{…}}`
|
|
@@ -1651,24 +1457,9 @@ export function transformSource(
|
|
|
1651
1457
|
}
|
|
1652
1458
|
if (
|
|
1653
1459
|
elementAttribute === 'userData-oid' &&
|
|
1654
|
-
//
|
|
1655
|
-
// React ever sees it, and `userData={{}}` REPLACES that object's own
|
|
1656
|
-
// `userData` wholesale — measured: a root carrying
|
|
1657
|
-
// `markComponentRoot(node, 'Stage')` from its constructor came out of
|
|
1658
|
-
// mount with nothing but the three editor keys, so every hierarchy
|
|
1659
|
-
// mark a class-owned root sets was destroyed by the editor's own
|
|
1660
|
-
// serve-time transform. An adopted object always HAS a `userData`
|
|
1661
|
-
// (three sets one on every Object3D/geometry/material), which is
|
|
1662
|
-
// precisely why the initializer below is unnecessary here and
|
|
1663
|
-
// destructive. That initializer's real subjects are the resources R3F
|
|
1664
|
-
// itself constructs.
|
|
1460
|
+
// Primitive objects already own userData. Do not replace it when adopting an object.
|
|
1665
1461
|
tag !== 'primitive' &&
|
|
1666
|
-
//
|
|
1667
|
-
// seed is a new `{}` on every render, and Fiber REPLACES `userData`
|
|
1668
|
-
// whenever that prop changes: seeded on an Object3D, a material or a
|
|
1669
|
-
// custom host, it wiped what the game kept there on each rerender
|
|
1670
|
-
// (measured: a mesh's `userData.probe`, set once on mount, gone after
|
|
1671
|
-
// 13 renders, leaving only the editor's keys).
|
|
1462
|
+
// Seed userData only on host types that do not construct their own object.
|
|
1672
1463
|
SEEDED_USER_DATA_TAGS.has(tag) &&
|
|
1673
1464
|
!node.attributes.properties.some(
|
|
1674
1465
|
(property) => ts.isJsxAttribute(property) && property.name.getText(sf) === 'userData',
|
|
@@ -1699,7 +1490,6 @@ export function transformSource(
|
|
|
1699
1490
|
if (opensDomSubtree) domSubtreeDepth--;
|
|
1700
1491
|
if (enclosingOid !== undefined) jsxStack.pop();
|
|
1701
1492
|
visit(node.closingElement);
|
|
1702
|
-
if (pushed) componentStack.pop();
|
|
1703
1493
|
return;
|
|
1704
1494
|
}
|
|
1705
1495
|
|