@volter/editor-core 0.5.57
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/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- package/vite-plugin-ui-oid.ts +2106 -0
|
@@ -0,0 +1,1845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OID instrumentation (C1/C2) — the visual-edit "stamp every JSX element with a
|
|
3
|
+
* stable source id" trick, ported to vgai using the TypeScript compiler API
|
|
4
|
+
* (vgai has `typescript`; it does NOT have @babel/*). Hand-authored React UI
|
|
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
|
+
*/
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
import {
|
|
16
|
+
environmentBindingsByElement,
|
|
17
|
+
type R3fEnvironmentBinding,
|
|
18
|
+
} from './r3f-environment-binding';
|
|
19
|
+
import { jointBindingsByElement, type R3fJointBinding } from './r3f-joint-binding';
|
|
20
|
+
import { lodBindingsByElement, type R3fLodBinding } from './r3f-lod-binding';
|
|
21
|
+
import { particleBindingsByElement, type R3fParticleBinding } from './r3f-particle-binding';
|
|
22
|
+
import { physicsBindingsByElement, type R3fPhysicsBinding } from './r3f-physics-binding';
|
|
23
|
+
import { parseAuthoringTsx, refIdentifier } from './ts-ast';
|
|
24
|
+
|
|
25
|
+
export type {
|
|
26
|
+
R3fEnvironmentBinding,
|
|
27
|
+
R3fEnvironmentNumberBinding,
|
|
28
|
+
R3fEnvironmentStringBinding,
|
|
29
|
+
} from './r3f-environment-binding';
|
|
30
|
+
export type {
|
|
31
|
+
R3fJointBinding,
|
|
32
|
+
R3fJointHook,
|
|
33
|
+
R3fJointLiteral,
|
|
34
|
+
R3fJointLiteralRange,
|
|
35
|
+
} from './r3f-joint-binding';
|
|
36
|
+
export type { R3fLodBinding, R3fLodNumberBinding } from './r3f-lod-binding';
|
|
37
|
+
export type {
|
|
38
|
+
QuarksEmitterShape,
|
|
39
|
+
R3fParticleBinding,
|
|
40
|
+
R3fParticleNumberBinding,
|
|
41
|
+
} from './r3f-particle-binding';
|
|
42
|
+
export type { PhysicsChannel, R3fPhysicsBinding } from './r3f-physics-binding';
|
|
43
|
+
|
|
44
|
+
export interface OidEntry {
|
|
45
|
+
file: string;
|
|
46
|
+
line: number; // 1-based
|
|
47
|
+
col: number; // 0-based (matches ts char)
|
|
48
|
+
component: string | null;
|
|
49
|
+
tag: string;
|
|
50
|
+
/**
|
|
51
|
+
* R3F-only, source-proven authoring contract for a LOCAL custom component
|
|
52
|
+
* callsite. `undefined` means the component definition is not in this file
|
|
53
|
+
* (usually an import), so the editor must retain its conservative fallback.
|
|
54
|
+
*
|
|
55
|
+
* A single native root plus forwarded standard group props is ordinary R3F,
|
|
56
|
+
* not a vgai wrapper. Recording it here lets the editor safely ADD an absent
|
|
57
|
+
* transform prop at the callsite: the source analyser has proven where that
|
|
58
|
+
* prop lands. An empty `transformProps` list is an equally useful result —
|
|
59
|
+
* the local component exists, but exposes no writable spatial channel.
|
|
60
|
+
*/
|
|
61
|
+
r3fAuthoring?: R3fComponentContract;
|
|
62
|
+
/**
|
|
63
|
+
* The props the tag's component DECLARES — resolved server-side through the
|
|
64
|
+
* TypeScript type checker (`component-prop-types.ts`) and shipped here so
|
|
65
|
+
* the editor can inspect a node by its declared surface rather than by the
|
|
66
|
+
* attributes someone happened to write. Absent for a native tag, or when the
|
|
67
|
+
* definition could not be resolved; the editor then falls back to reading
|
|
68
|
+
* the authored attributes alone.
|
|
69
|
+
*/
|
|
70
|
+
props?: ComponentPropSpec[];
|
|
71
|
+
/**
|
|
72
|
+
* True once the server-side declared-prop resolver has ANSWERED for this
|
|
73
|
+
* tag — present even when the answer was "no props". Distinguishes "the
|
|
74
|
+
* declaration rung has not landed yet" (the resolver's cold ts.Program
|
|
75
|
+
* takes seconds and enrichment is async) from "the resolver looked and
|
|
76
|
+
* found nothing": the inspector's no-declaration warning is honest only in
|
|
77
|
+
* the second state. Measured on racing-game: the doctor's sweep inspected
|
|
78
|
+
* before the cold program answered, the fallback warned once-per-key, and
|
|
79
|
+
* the session ledger held six "No declaration covers" rows for props the
|
|
80
|
+
* resolver demonstrably types (`allowSleep:boolean`, `broadphase:enum`, …).
|
|
81
|
+
*/
|
|
82
|
+
propsResolved?: true;
|
|
83
|
+
/** Props physically authored on this JSX callsite. Unlike {@link props},
|
|
84
|
+
* which describes the component declaration, this is the byte-level answer
|
|
85
|
+
* needed by instance editing: an override exists only when the attribute is
|
|
86
|
+
* present here, and applying it to the component is safe only when its source
|
|
87
|
+
* value is a literal. */
|
|
88
|
+
authoredProps?: Array<{
|
|
89
|
+
name: string;
|
|
90
|
+
valueText: string;
|
|
91
|
+
literal: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* The R3F authorability diagnostics that pertain to THIS element, attached
|
|
95
|
+
* server-side by `currentOidIndex` so they ride the exact same
|
|
96
|
+
* `/__ui-source/index` payload (and therefore the exact same freshness) as
|
|
97
|
+
* every other source-derived field on this entry. Absent when there are
|
|
98
|
+
* none.
|
|
99
|
+
*
|
|
100
|
+
* Two kinds land here, both selected by `fileDiagnosticJoin`:
|
|
101
|
+
* the diagnostic recorded AT this element (an R3F004 "has no name" on this
|
|
102
|
+
* very callsite), and the ones recorded against the component whose body
|
|
103
|
+
* lexically contains it (R3F002/R3F003/R3F005 — `component` matches). The
|
|
104
|
+
* second is what carries a definition-side warning out to the rows that
|
|
105
|
+
* INSTANTIATE that component: an instance's boundary object also carries its
|
|
106
|
+
* definition-root oid, so the client reads both entries. See
|
|
107
|
+
* `r3f-diagnostic-index.ts`.
|
|
108
|
+
*/
|
|
109
|
+
diagnostics?: R3fAuthoringDiagnostic[];
|
|
110
|
+
/**
|
|
111
|
+
* The oid of the JSX element that lexically ENCLOSES this one, in the same
|
|
112
|
+
* file. Recorded because a wrapper tag does not always become its own node:
|
|
113
|
+
* `<RigidBody>` from `@react-three/rapier` never forwards the editor's stamp
|
|
114
|
+
* to the Object3D it renders, so it has no live object to select — yet its
|
|
115
|
+
* props (`type`, `gravityScale`, colliders) are exactly the configuration an
|
|
116
|
+
* author expects to find on the thing inside it. This chain is what lets the
|
|
117
|
+
* inspector attribute a collapsed wrapper's props to the node it wraps.
|
|
118
|
+
*/
|
|
119
|
+
parentOid?: string;
|
|
120
|
+
/**
|
|
121
|
+
* The PHYSICS BODY that owns this element's transform, when a simulation
|
|
122
|
+
* binding attached one to its ref (`r3f-physics-binding.ts`).
|
|
123
|
+
*
|
|
124
|
+
* Present ⇒ this element's own `position`/`rotation`/`scale` props are dead
|
|
125
|
+
* on arrival: the body writes the object's matrix every frame from ITS OWN
|
|
126
|
+
* spawn, so a write here produces source that contradicts the running world.
|
|
127
|
+
* Every writer reads it as a refusal that can name the binding, and the
|
|
128
|
+
* component contract reads its `forwarded` channels as real forwarding —
|
|
129
|
+
* which is what routes the write to the callsite literal the hook reads.
|
|
130
|
+
*/
|
|
131
|
+
physicsBinding?: R3fPhysicsBinding;
|
|
132
|
+
/** Native Rapier joint hooks that name this `<RigidBody ref={…}>` as one
|
|
133
|
+
* endpoint. Hook parameters remain the project's TSX source of truth. */
|
|
134
|
+
jointBindings?: readonly R3fJointBinding[];
|
|
135
|
+
/** Native three.quarks shape construction bound to this emitter primitive. */
|
|
136
|
+
particleBinding?: R3fParticleBinding;
|
|
137
|
+
/** Drei Detailed is a source projection of the native THREE.LOD it creates. */
|
|
138
|
+
lodBinding?: R3fLodBinding;
|
|
139
|
+
/** Native Fiber scene attachment (`background` or `fog`). */
|
|
140
|
+
environmentBinding?: R3fEnvironmentBinding;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* One R3F authoring-convention warning.
|
|
145
|
+
*
|
|
146
|
+
* DECLARED HERE, not in its producer. `r3f-project-contracts.ts` computes
|
|
147
|
+
* these (and re-exports this type for its existing importers), but that module
|
|
148
|
+
* imports `node:fs` — while this one is deliberately dependency-free so a
|
|
149
|
+
* PAGE can import it. Since a diagnostic now travels ON an `OidEntry` (see its
|
|
150
|
+
* `diagnostics` field), the TYPE has to live on the browser-safe side of that
|
|
151
|
+
* line even though the ANALYSIS does not.
|
|
152
|
+
*/
|
|
153
|
+
export interface R3fAuthoringDiagnostic {
|
|
154
|
+
code: 'R3F002' | 'R3F003' | 'R3F004' | 'R3F005';
|
|
155
|
+
severity: 'warning';
|
|
156
|
+
file: string;
|
|
157
|
+
/** 1-based, and computed from `node.getStart()` — the SAME position basis
|
|
158
|
+
* `OidEntry.line`/`col` use, which is what makes an exact positional join
|
|
159
|
+
* between the two sound rather than approximate. */
|
|
160
|
+
line: number;
|
|
161
|
+
col: number;
|
|
162
|
+
/** The component the warning is ABOUT: the definition it names (R3F002/3/5)
|
|
163
|
+
* or the tag at the offending callsite (R3F004). */
|
|
164
|
+
component: string;
|
|
165
|
+
message: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A prop as DECLARED by a component, independent of any callsite. Lives here
|
|
170
|
+
* beside `OidEntry` because it travels on it; it is PRODUCED by
|
|
171
|
+
* `component-prop-types.ts` (server-side, needs a `ts.Program`) and CONSUMED
|
|
172
|
+
* by the editor's authoring adapters.
|
|
173
|
+
*/
|
|
174
|
+
export interface ComponentPropSpec {
|
|
175
|
+
name: string;
|
|
176
|
+
/** Inspector widget implied by the declared type; `null` when no widget
|
|
177
|
+
* honestly represents it (the consumer falls back to the attribute text). */
|
|
178
|
+
type: 'string' | 'number' | 'boolean' | 'enum' | 'vec3' | 'json' | null;
|
|
179
|
+
/** Allowed values, for a union of string literals. */
|
|
180
|
+
options?: Array<string | number>;
|
|
181
|
+
/** Declared `?:`, or given a default by the component's destructuring. */
|
|
182
|
+
optional: boolean;
|
|
183
|
+
/** The default's source text (`1.55`, `'raider'`, `defaultSpawn()`). */
|
|
184
|
+
defaultText?: string;
|
|
185
|
+
/** The default as a value, when the default is a literal. */
|
|
186
|
+
defaultValue?: string | number | boolean | number[];
|
|
187
|
+
/** The prop's jsdoc, as one line. */
|
|
188
|
+
doc?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function literalJsxExpression(expression: ts.Expression | undefined): boolean {
|
|
192
|
+
if (!expression) return false;
|
|
193
|
+
if (
|
|
194
|
+
ts.isStringLiteral(expression) ||
|
|
195
|
+
ts.isNumericLiteral(expression) ||
|
|
196
|
+
expression.kind === ts.SyntaxKind.TrueKeyword ||
|
|
197
|
+
expression.kind === ts.SyntaxKind.FalseKeyword
|
|
198
|
+
) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (!ts.isArrayLiteralExpression(expression)) return false;
|
|
202
|
+
return expression.elements.every(
|
|
203
|
+
(element) =>
|
|
204
|
+
ts.isNumericLiteral(element) ||
|
|
205
|
+
(ts.isPrefixUnaryExpression(element) &&
|
|
206
|
+
element.operator === ts.SyntaxKind.MinusToken &&
|
|
207
|
+
ts.isNumericLiteral(element.operand)),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function authoredPropsOf(
|
|
212
|
+
node: ts.JsxOpeningElement | ts.JsxSelfClosingElement,
|
|
213
|
+
sourceFile: ts.SourceFile,
|
|
214
|
+
): NonNullable<OidEntry['authoredProps']> {
|
|
215
|
+
return node.attributes.properties.flatMap((property) => {
|
|
216
|
+
if (!ts.isJsxAttribute(property)) return [];
|
|
217
|
+
const name = property.name.getText(sourceFile);
|
|
218
|
+
const initializer = property.initializer;
|
|
219
|
+
if (!initializer) return [{ name, valueText: 'true', literal: true }];
|
|
220
|
+
if (ts.isStringLiteral(initializer)) {
|
|
221
|
+
return [{ name, valueText: initializer.getText(sourceFile), literal: true }];
|
|
222
|
+
}
|
|
223
|
+
if (!ts.isJsxExpression(initializer)) return [];
|
|
224
|
+
return [
|
|
225
|
+
{
|
|
226
|
+
name,
|
|
227
|
+
valueText: initializer.expression?.getText(sourceFile) ?? '',
|
|
228
|
+
literal: literalJsxExpression(initializer.expression),
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Persistent oid store: signature -> oid (kept stable across re-transforms). */
|
|
235
|
+
export class OidStore {
|
|
236
|
+
private bySig = new Map<string, string>();
|
|
237
|
+
private counter = 0;
|
|
238
|
+
readonly index = new Map<string, OidEntry>();
|
|
239
|
+
|
|
240
|
+
getOrCreate(sig: string): string {
|
|
241
|
+
let oid = this.bySig.get(sig);
|
|
242
|
+
if (!oid) {
|
|
243
|
+
this.counter += 1;
|
|
244
|
+
oid = `o${this.counter.toString(36)}${hash(sig).toString(36)}`;
|
|
245
|
+
this.bySig.set(sig, oid);
|
|
246
|
+
}
|
|
247
|
+
return oid;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Clear index entries for a file before re-transforming it. */
|
|
251
|
+
clearFile(file: string): void {
|
|
252
|
+
for (const [oid, e] of this.index) if (e.file === file) this.index.delete(oid);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function hash(s: string): number {
|
|
257
|
+
let h = 0;
|
|
258
|
+
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
259
|
+
return Math.abs(h);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface SourceEdit {
|
|
263
|
+
pos: number;
|
|
264
|
+
end?: number;
|
|
265
|
+
text: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export type R3fTransformProp = 'position' | 'rotation' | 'scale';
|
|
269
|
+
|
|
270
|
+
export interface R3fComponentContract {
|
|
271
|
+
root: 'single' | 'multiple' | 'non-spatial' | 'unknown';
|
|
272
|
+
rootTag?: string;
|
|
273
|
+
transformProps: R3fTransformProp[];
|
|
274
|
+
/** The callsite's native `visible` prop is proven to reach the single
|
|
275
|
+
* Object3D root. Omitted is deliberately false/unknown, so an editor never
|
|
276
|
+
* writes a decorative prop that the component silently ignores. */
|
|
277
|
+
visibleProp?: true;
|
|
278
|
+
/**
|
|
279
|
+
* The component's own source says the SIMULATION owns this transform: it
|
|
280
|
+
* neither accepts nor spreads a transform prop, and it writes its root's
|
|
281
|
+
* `position`/`rotation`/`quaternion`/`scale` through a ref every frame
|
|
282
|
+
* inside `useFrame`.
|
|
283
|
+
*
|
|
284
|
+
* Present (and only ever `true`) when both halves hold, so a contract that
|
|
285
|
+
* predates the distinction compares equal to one that has no opinion.
|
|
286
|
+
*
|
|
287
|
+
* This is a DECLARATION OF NON-AUTHORABILITY, never of authorability:
|
|
288
|
+
* `transformProps` stays empty, so every writer path
|
|
289
|
+
* (`transformEditability`) still refuses the edit — with an honest reason
|
|
290
|
+
* instead of "does not forward position". What it buys is silence from the
|
|
291
|
+
* convention diagnostics whose premise it falsifies (R3F002 asks the
|
|
292
|
+
* component to forward an authored transform the simulation would overwrite
|
|
293
|
+
* on the next tick; R3F005 warns that runtime motion fights an editor
|
|
294
|
+
* transform that cannot exist here).
|
|
295
|
+
*/
|
|
296
|
+
simulationOwnedTransform?: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* The subset of {@link transformProps} this component forwards to a PHYSICS
|
|
299
|
+
* BODY BINDING rather than to its native root — the channels whose callsite
|
|
300
|
+
* literal is read by a `useBox(…)`-family hook (`r3f-physics-binding.ts`).
|
|
301
|
+
*
|
|
302
|
+
* The write is exactly as real as any other forwarded prop, which is why
|
|
303
|
+
* these are IN `transformProps`; what this field adds is WHY it is real, and
|
|
304
|
+
* that fact has a consequence no other prop has: a body re-spawns from this
|
|
305
|
+
* literal and then owns the node's matrix, so the authored value must survive
|
|
306
|
+
* the re-settle after a remount. That is the `physics-binding`
|
|
307
|
+
* {@link WriteAnchorKind}'s own contract, and this is the source fact the
|
|
308
|
+
* planner reads to classify an anchor as one.
|
|
309
|
+
*
|
|
310
|
+
* Absent (never `[]`) when nothing is forwarded to a binding, so a contract
|
|
311
|
+
* that predates the distinction compares equal to one that has no opinion —
|
|
312
|
+
* the same rule `simulationOwnedTransform` above follows, and one the
|
|
313
|
+
* fixed-point loop in `collectR3fComponentContracts` depends on.
|
|
314
|
+
*/
|
|
315
|
+
bodyForwarded?: R3fTransformProp[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const R3F_TRANSFORM_PROPS = ['position', 'rotation', 'scale'] as const;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Contracts for components vgai does NOT own, keyed by package specifier and
|
|
322
|
+
* export name. The project resolver reads a local component's contract out of
|
|
323
|
+
* its own source; an installed package has no source to walk from the browser
|
|
324
|
+
* tier and no reason to expect one, so the few whose native root is part of the
|
|
325
|
+
* package's documented public API are recorded here instead.
|
|
326
|
+
*
|
|
327
|
+
* Deliberately tiny and hand-audited. Everything absent from this table stays
|
|
328
|
+
* conservative (`root: 'unknown'`), because guessing at a dependency's scene
|
|
329
|
+
* ownership is how an editor writes a prop the library silently ignores.
|
|
330
|
+
*
|
|
331
|
+
* `@react-three/rapier`'s `<RigidBody>` renders one host `object3D` and passes
|
|
332
|
+
* `position`/`rotation`/`scale` straight to it — the body's initial transform is
|
|
333
|
+
* read back off that object, and rapier scales the colliders it owns by the
|
|
334
|
+
* object's world scale — so a transform written at the callsite lands exactly
|
|
335
|
+
* where the editor claims it does.
|
|
336
|
+
*
|
|
337
|
+
* `rootTag` is `group`, not `object3D`, because the tag is proved against the
|
|
338
|
+
* CALLER's props type: `typeAllowsStandardRootProps` looks for the literal text
|
|
339
|
+
* `ThreeElements['<rootTag>']`, and a component forwarding to a RigidBody types
|
|
340
|
+
* its props as `ThreeElements['group'] & …` — the ordinary R3F spelling for "a
|
|
341
|
+
* spatial container". `object3D` would match no real caller.
|
|
342
|
+
*/
|
|
343
|
+
const BUILTIN_R3F_CONTRACTS: Readonly<
|
|
344
|
+
Record<string, Readonly<Record<string, R3fComponentContract>>>
|
|
345
|
+
> = {
|
|
346
|
+
'@react-three/rapier': {
|
|
347
|
+
RigidBody: { root: 'single', rootTag: 'group', transformProps: [...R3F_TRANSFORM_PROPS] },
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Package prop types that are themselves a proved native-root prop surface.
|
|
353
|
+
* Kept beside the component contract because both facts come from the same
|
|
354
|
+
* hand-audited public API. This lets ordinary wrappers preserve the package's
|
|
355
|
+
* own type (`Omit<RigidBodyProps, ...>`) without pretending every imported
|
|
356
|
+
* type is spatial.
|
|
357
|
+
*/
|
|
358
|
+
const BUILTIN_R3F_ROOT_PROP_TYPES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
359
|
+
'@react-three/rapier': { RigidBodyProps: 'group' },
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/** Local imported type binding → native root tag it proves. */
|
|
363
|
+
function builtinRootPropTypes(sf: ts.SourceFile): Map<string, string> {
|
|
364
|
+
const bound = new Map<string, string>();
|
|
365
|
+
for (const statement of sf.statements) {
|
|
366
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const known = BUILTIN_R3F_ROOT_PROP_TYPES[statement.moduleSpecifier.text];
|
|
370
|
+
const bindings = statement.importClause?.namedBindings;
|
|
371
|
+
if (!known || !bindings || !ts.isNamedImports(bindings)) continue;
|
|
372
|
+
for (const element of bindings.elements) {
|
|
373
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
374
|
+
const rootTag = known[importedName];
|
|
375
|
+
if (rootTag) bound.set(element.name.text, rootTag);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return bound;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** True for a contract that came from the table above rather than from source.
|
|
382
|
+
* Identity, not shape: a project-local definition that SHADOWS an imported
|
|
383
|
+
* binding produces its own freshly analysed object, so it is never mistaken
|
|
384
|
+
* for the package's. */
|
|
385
|
+
export function isBuiltinR3fContract(contract: R3fComponentContract | undefined): boolean {
|
|
386
|
+
if (!contract) return false;
|
|
387
|
+
return Object.values(BUILTIN_R3F_CONTRACTS).some((exports) =>
|
|
388
|
+
Object.values(exports).includes(contract),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The built-in contract for one named export of an external package, if any. */
|
|
393
|
+
export function builtinR3fContract(
|
|
394
|
+
specifier: string,
|
|
395
|
+
exportName: string,
|
|
396
|
+
): R3fComponentContract | undefined {
|
|
397
|
+
return BUILTIN_R3F_CONTRACTS[specifier]?.[exportName];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The local bindings ONE import declaration takes from the built-in table.
|
|
401
|
+
* Keyed by the LOCAL name, so `import { RigidBody as RB }` registers `RB`. */
|
|
402
|
+
export function builtinR3fContractsOfImport(
|
|
403
|
+
declaration: ts.ImportDeclaration,
|
|
404
|
+
): Map<string, R3fComponentContract> {
|
|
405
|
+
const bound = new Map<string, R3fComponentContract>();
|
|
406
|
+
if (!ts.isStringLiteral(declaration.moduleSpecifier)) return bound;
|
|
407
|
+
const bindings = declaration.importClause?.namedBindings;
|
|
408
|
+
if (!bindings || !ts.isNamedImports(bindings)) return bound;
|
|
409
|
+
for (const element of bindings.elements) {
|
|
410
|
+
const contract = builtinR3fContract(
|
|
411
|
+
declaration.moduleSpecifier.text,
|
|
412
|
+
element.propertyName?.text ?? element.name.text,
|
|
413
|
+
);
|
|
414
|
+
if (contract) bound.set(element.name.text, contract);
|
|
415
|
+
}
|
|
416
|
+
return bound;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Every built-in contract one module's imports bring into scope. The browser
|
|
420
|
+
* tier has no filesystem to resolve project-local imports from, so this is the
|
|
421
|
+
* whole of the contract knowledge available to it. */
|
|
422
|
+
export function builtinR3fContractsForSource(
|
|
423
|
+
code: string,
|
|
424
|
+
file: string,
|
|
425
|
+
): Map<string, R3fComponentContract> {
|
|
426
|
+
const sf = parseAuthoringTsx(file, code);
|
|
427
|
+
const contracts = new Map<string, R3fComponentContract>();
|
|
428
|
+
for (const statement of sf.statements) {
|
|
429
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
430
|
+
for (const [name, contract] of builtinR3fContractsOfImport(statement)) {
|
|
431
|
+
contracts.set(name, contract);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return contracts;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
438
|
+
let current = expression;
|
|
439
|
+
while (
|
|
440
|
+
ts.isParenthesizedExpression(current) ||
|
|
441
|
+
ts.isAsExpression(current) ||
|
|
442
|
+
ts.isSatisfiesExpression(current)
|
|
443
|
+
) {
|
|
444
|
+
current = current.expression;
|
|
445
|
+
}
|
|
446
|
+
return current;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function returnedExpression(body: ts.ConciseBody): ts.Expression | undefined {
|
|
450
|
+
if (!ts.isBlock(body)) return unwrapExpression(body);
|
|
451
|
+
const returns = body.statements.filter(ts.isReturnStatement);
|
|
452
|
+
if (returns.length !== 1 || !returns[0]?.expression) return undefined;
|
|
453
|
+
return unwrapExpression(returns[0].expression);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* A type alias and the file it was written in.
|
|
458
|
+
*
|
|
459
|
+
* These travel together because `ts.Node.getText(sf)` reads the node's
|
|
460
|
+
* position range out of the SOURCE TEXT it is handed: give it another file's
|
|
461
|
+
* `SourceFile` and it returns whatever characters happen to sit at those
|
|
462
|
+
* offsets. An alias resolved across a module boundary is exactly that case.
|
|
463
|
+
*/
|
|
464
|
+
export interface R3fTypeAlias {
|
|
465
|
+
readonly type: ts.TypeNode;
|
|
466
|
+
readonly sf: ts.SourceFile;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** The `export type X = …` declarations of one module, for importers. */
|
|
470
|
+
export function exportedR3fTypeAliases(sf: ts.SourceFile): Map<string, R3fTypeAlias> {
|
|
471
|
+
const aliases = new Map<string, R3fTypeAlias>();
|
|
472
|
+
for (const statement of sf.statements) {
|
|
473
|
+
if (!ts.isTypeAliasDeclaration(statement)) continue;
|
|
474
|
+
if (!ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
475
|
+
aliases.set(statement.name.text, { type: statement.type, sf });
|
|
476
|
+
}
|
|
477
|
+
return aliases;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function textIncludesBuiltinRootProps(
|
|
481
|
+
text: string,
|
|
482
|
+
rootTag: string,
|
|
483
|
+
builtinPropTypes: ReadonlyMap<string, string>,
|
|
484
|
+
): boolean {
|
|
485
|
+
for (const [name, knownRootTag] of builtinPropTypes) {
|
|
486
|
+
if (knownRootTag === rootTag && new RegExp(`\\b${name}\\b`).test(text)) return true;
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function typeAllowsStandardRootProps(
|
|
492
|
+
type: ts.TypeNode | undefined,
|
|
493
|
+
rootTag: string,
|
|
494
|
+
sf: ts.SourceFile,
|
|
495
|
+
typeAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
496
|
+
builtinPropTypes: ReadonlyMap<string, string>,
|
|
497
|
+
seen = new Set<string>(),
|
|
498
|
+
): boolean {
|
|
499
|
+
if (!type) return false;
|
|
500
|
+
const text = type.getText(sf);
|
|
501
|
+
const escapedRootTag = rootTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
502
|
+
if (new RegExp(`\\bThreeElements\\s*\\[\\s*['"]${escapedRootTag}['"]\\s*\\]`).test(text))
|
|
503
|
+
return true;
|
|
504
|
+
if (textIncludesBuiltinRootProps(text, rootTag, builtinPropTypes)) return true;
|
|
505
|
+
if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
|
|
506
|
+
const name = type.typeName.text;
|
|
507
|
+
if (seen.has(name)) return false;
|
|
508
|
+
const alias = typeAliases.get(name);
|
|
509
|
+
if (!alias) return false;
|
|
510
|
+
seen.add(name);
|
|
511
|
+
return typeAllowsStandardRootProps(
|
|
512
|
+
alias.type,
|
|
513
|
+
rootTag,
|
|
514
|
+
alias.sf,
|
|
515
|
+
typeAliases,
|
|
516
|
+
builtinPropTypes,
|
|
517
|
+
seen,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
for (const [name, alias] of typeAliases) {
|
|
521
|
+
if (seen.has(name) || !new RegExp(`\\b${name}\\b`).test(text)) continue;
|
|
522
|
+
const nextSeen = new Set(seen).add(name);
|
|
523
|
+
if (
|
|
524
|
+
typeAllowsStandardRootProps(
|
|
525
|
+
alias.type,
|
|
526
|
+
rootTag,
|
|
527
|
+
alias.sf,
|
|
528
|
+
typeAliases,
|
|
529
|
+
builtinPropTypes,
|
|
530
|
+
nextSeen,
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function spatialRootOpening(expression: ts.Expression): ts.JsxOpeningLikeElement | undefined {
|
|
539
|
+
if (ts.isJsxSelfClosingElement(expression)) return expression;
|
|
540
|
+
if (!ts.isJsxElement(expression)) return undefined;
|
|
541
|
+
return expression.openingElement;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
type FragmentRootCardinality = 0 | 1 | 'multiple' | 'unknown';
|
|
545
|
+
|
|
546
|
+
function combineFragmentRootCardinality(
|
|
547
|
+
left: FragmentRootCardinality,
|
|
548
|
+
right: FragmentRootCardinality,
|
|
549
|
+
): FragmentRootCardinality {
|
|
550
|
+
if (left === 'multiple' || right === 'multiple') return 'multiple';
|
|
551
|
+
if (left === 'unknown' || right === 'unknown') return 'unknown';
|
|
552
|
+
return left + right >= 2 ? 'multiple' : ((left + right) as 0 | 1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function jsxRootCardinality(
|
|
556
|
+
element: ts.JsxElement | ts.JsxSelfClosingElement,
|
|
557
|
+
sf: ts.SourceFile,
|
|
558
|
+
knownContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
559
|
+
): FragmentRootCardinality {
|
|
560
|
+
const opening = ts.isJsxElement(element) ? element.openingElement : element;
|
|
561
|
+
const tag = opening.tagName.getText(sf);
|
|
562
|
+
if (!/^[A-Z]/.test(tag)) return isNonSpatialNativeTag(tag) ? 0 : 1;
|
|
563
|
+
const delegated = knownContracts.get(tag);
|
|
564
|
+
if (!delegated || delegated.root === 'unknown') return 'unknown';
|
|
565
|
+
if (delegated.root === 'non-spatial') return 0;
|
|
566
|
+
return delegated.root === 'multiple' ? 'multiple' : 1;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function mappedRootCardinality(
|
|
570
|
+
expression: ts.CallExpression,
|
|
571
|
+
sf: ts.SourceFile,
|
|
572
|
+
knownContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
573
|
+
): FragmentRootCardinality | undefined {
|
|
574
|
+
if (
|
|
575
|
+
!ts.isPropertyAccessExpression(expression.expression) ||
|
|
576
|
+
expression.expression.name.text !== 'map'
|
|
577
|
+
) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
const callback = expression.arguments[0];
|
|
581
|
+
if (!callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback))) {
|
|
582
|
+
return 'unknown';
|
|
583
|
+
}
|
|
584
|
+
const returned = returnedExpression(callback.body);
|
|
585
|
+
if (!returned) return 'unknown';
|
|
586
|
+
const item = expressionRootCardinality(returned, sf, knownContracts);
|
|
587
|
+
// A map can emit any number of its item. It remains non-spatial only when
|
|
588
|
+
// the item is proved non-spatial; every spatial item makes a plural result
|
|
589
|
+
// possible, which is precisely the non-integral component shape R3F003
|
|
590
|
+
// reports.
|
|
591
|
+
return item === 0 ? 0 : item === 'unknown' ? 'unknown' : 'multiple';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function expressionRootCardinality(
|
|
595
|
+
expression: ts.Expression,
|
|
596
|
+
sf: ts.SourceFile,
|
|
597
|
+
knownContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
598
|
+
): FragmentRootCardinality {
|
|
599
|
+
const unwrapped = unwrapExpression(expression);
|
|
600
|
+
if (unwrapped.kind === ts.SyntaxKind.NullKeyword) return 0;
|
|
601
|
+
if (unwrapped.kind === ts.SyntaxKind.FalseKeyword) return 0;
|
|
602
|
+
if (ts.isJsxFragment(unwrapped)) {
|
|
603
|
+
return fragmentRootCardinality(unwrapped, sf, knownContracts);
|
|
604
|
+
}
|
|
605
|
+
if (ts.isJsxElement(unwrapped) || ts.isJsxSelfClosingElement(unwrapped)) {
|
|
606
|
+
return jsxRootCardinality(unwrapped, sf, knownContracts);
|
|
607
|
+
}
|
|
608
|
+
if (ts.isCallExpression(unwrapped)) {
|
|
609
|
+
return mappedRootCardinality(unwrapped, sf, knownContracts) ?? 'unknown';
|
|
610
|
+
}
|
|
611
|
+
return 'unknown';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function jsxChildRootCardinality(
|
|
615
|
+
child: ts.JsxChild,
|
|
616
|
+
sf: ts.SourceFile,
|
|
617
|
+
knownContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
618
|
+
): FragmentRootCardinality {
|
|
619
|
+
if (ts.isJsxText(child)) return child.text.trim() === '' ? 0 : 'unknown';
|
|
620
|
+
if (ts.isJsxExpression(child)) {
|
|
621
|
+
return child.expression ? expressionRootCardinality(child.expression, sf, knownContracts) : 0;
|
|
622
|
+
}
|
|
623
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
624
|
+
return jsxRootCardinality(child, sf, knownContracts);
|
|
625
|
+
}
|
|
626
|
+
if (ts.isJsxFragment(child)) return fragmentRootCardinality(child, sf, knownContracts);
|
|
627
|
+
return 'unknown';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function fragmentRootCardinality(
|
|
631
|
+
fragment: ts.JsxFragment,
|
|
632
|
+
sf: ts.SourceFile,
|
|
633
|
+
knownContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
634
|
+
): FragmentRootCardinality {
|
|
635
|
+
let result: FragmentRootCardinality = 0;
|
|
636
|
+
for (const child of fragment.children) {
|
|
637
|
+
result = combineFragmentRootCardinality(
|
|
638
|
+
result,
|
|
639
|
+
jsxChildRootCardinality(child, sf, knownContracts),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function fragmentComponentContract(
|
|
646
|
+
fragment: ts.JsxFragment,
|
|
647
|
+
sf: ts.SourceFile,
|
|
648
|
+
knownContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
649
|
+
): R3fComponentContract {
|
|
650
|
+
const cardinality = fragmentRootCardinality(fragment, sf, knownContracts);
|
|
651
|
+
if (cardinality === 0) return { root: 'non-spatial', transformProps: [] };
|
|
652
|
+
if (cardinality === 'multiple') return { root: 'multiple', transformProps: [] };
|
|
653
|
+
// A fragment with one proved spatial child, or an unresolved child, has no
|
|
654
|
+
// fragment object of its own through which transform props can be proved to
|
|
655
|
+
// flow. Keep it conservative without falsely claiming several roots.
|
|
656
|
+
return { root: 'unknown', transformProps: [] };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Native R3F tags that are NOT `Object3D`s.
|
|
661
|
+
*
|
|
662
|
+
* R3F's host namespace is all of three's constructors, not just the scene
|
|
663
|
+
* graph: materials, geometries and buffer attributes are elements too, and
|
|
664
|
+
* they mount by `attach` onto a parent rather than by taking a place in the
|
|
665
|
+
* hierarchy. They have no `position`, no `rotation`, no `scale`, and they are
|
|
666
|
+
* never a hierarchy row.
|
|
667
|
+
*
|
|
668
|
+
* A component whose single root is one of these is a perfectly ordinary R3F
|
|
669
|
+
* idiom — a shared material helper returning `<meshStandardMaterial …/>` is
|
|
670
|
+
* about as common as R3F code gets — and it must not be read as an
|
|
671
|
+
* unauthorable spatial instance. R3F002 would demand it forward transform
|
|
672
|
+
* channels the element does not have (there is no `position` prop on a
|
|
673
|
+
* material to accept), and R3F004 would ask for a `name` "so its instance is
|
|
674
|
+
* recognizable in the hierarchy" when nothing about it ever appears there.
|
|
675
|
+
* Both are unactionable by construction, which is precisely the failure the
|
|
676
|
+
* `simulationOwnedTransform` and keyed-list-item carve-outs already exist to
|
|
677
|
+
* prevent elsewhere in these rules.
|
|
678
|
+
*
|
|
679
|
+
* Recognized by suffix because three's naming is regular here and the set is
|
|
680
|
+
* open — every release adds materials and geometries, and an exhaustive list
|
|
681
|
+
* would silently rot into false positives again. The three exact names are the
|
|
682
|
+
* scene-level attachables that carry no suffix.
|
|
683
|
+
*/
|
|
684
|
+
function isNonSpatialNativeTag(tag: string): boolean {
|
|
685
|
+
if (/^[A-Z]/.test(tag)) return false;
|
|
686
|
+
return (
|
|
687
|
+
/(Material|Geometry|Attribute)$/.test(tag) ||
|
|
688
|
+
tag === 'fog' ||
|
|
689
|
+
tag === 'fogExp2' ||
|
|
690
|
+
tag === 'color'
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
interface R3fPropForwarding {
|
|
695
|
+
standardRootProps: boolean;
|
|
696
|
+
wholePropsName?: string;
|
|
697
|
+
restPropsName?: string;
|
|
698
|
+
localBindingByProp: Map<R3fObjectProp, string>;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
type R3fObjectProp = R3fTransformProp | 'visible';
|
|
702
|
+
|
|
703
|
+
function isR3fTransformProp(value: string): value is R3fTransformProp {
|
|
704
|
+
return (R3F_TRANSFORM_PROPS as readonly string[]).includes(value);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function isR3fObjectProp(value: string): value is R3fObjectProp {
|
|
708
|
+
return isR3fTransformProp(value) || value === 'visible';
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function propForwarding(
|
|
712
|
+
first: ts.ParameterDeclaration | undefined,
|
|
713
|
+
rootTag: string,
|
|
714
|
+
sf: ts.SourceFile,
|
|
715
|
+
typeAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
716
|
+
builtinPropTypes: ReadonlyMap<string, string>,
|
|
717
|
+
): R3fPropForwarding {
|
|
718
|
+
const result: R3fPropForwarding = {
|
|
719
|
+
standardRootProps: typeAllowsStandardRootProps(
|
|
720
|
+
first?.type,
|
|
721
|
+
rootTag,
|
|
722
|
+
sf,
|
|
723
|
+
typeAliases,
|
|
724
|
+
builtinPropTypes,
|
|
725
|
+
),
|
|
726
|
+
localBindingByProp: new Map(),
|
|
727
|
+
};
|
|
728
|
+
if (!first) return result;
|
|
729
|
+
if (ts.isIdentifier(first.name)) {
|
|
730
|
+
result.wholePropsName = first.name.text;
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
if (!ts.isObjectBindingPattern(first.name)) return result;
|
|
734
|
+
for (const element of first.name.elements) {
|
|
735
|
+
if (element.dotDotDotToken && ts.isIdentifier(element.name)) {
|
|
736
|
+
result.restPropsName = element.name.text;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
740
|
+
const authoredName = element.propertyName?.getText(sf) ?? element.name.text;
|
|
741
|
+
if (isR3fObjectProp(authoredName)) {
|
|
742
|
+
result.localBindingByProp.set(authoredName, element.name.text);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return result;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Is this parameter's type spelled AS another component's props — the guard
|
|
750
|
+
* wrapper's `Parameters<typeof Body>[0]` / `ComponentProps<typeof Body>`?
|
|
751
|
+
*
|
|
752
|
+
* Deliberately a spelling test and not a type check: this analyzer holds no
|
|
753
|
+
* checker, and the one thing it needs to know is that the wrapper's props ARE
|
|
754
|
+
* the delegate's, which the spelling states outright.
|
|
755
|
+
*/
|
|
756
|
+
function typeIsDelegateProps(
|
|
757
|
+
type: ts.TypeNode | undefined,
|
|
758
|
+
delegateTag: string,
|
|
759
|
+
sf: ts.SourceFile,
|
|
760
|
+
): boolean {
|
|
761
|
+
if (!type) return false;
|
|
762
|
+
const text = type.getText(sf).replace(/\s+/g, '');
|
|
763
|
+
const tag = delegateTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
764
|
+
return new RegExp(
|
|
765
|
+
`^(React\\.)?(Parameters<typeof${tag}>\\[0\\]|ComponentProps(WithoutRef|WithRef)?<typeof${tag}>)`,
|
|
766
|
+
).test(text);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function spreadForwardsAll(
|
|
770
|
+
property: ts.JsxSpreadAttribute,
|
|
771
|
+
forwarding: R3fPropForwarding,
|
|
772
|
+
): boolean {
|
|
773
|
+
if (!forwarding.standardRootProps || !ts.isIdentifier(property.expression)) return false;
|
|
774
|
+
return (
|
|
775
|
+
property.expression.text === forwarding.wholePropsName ||
|
|
776
|
+
property.expression.text === forwarding.restPropsName
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function attributeForwardsObjectProp(
|
|
781
|
+
property: ts.JsxAttribute,
|
|
782
|
+
forwarding: R3fPropForwarding,
|
|
783
|
+
sf: ts.SourceFile,
|
|
784
|
+
): R3fObjectProp | undefined {
|
|
785
|
+
const propName = property.name.getText(sf);
|
|
786
|
+
if (!isR3fObjectProp(propName)) return undefined;
|
|
787
|
+
if (!property.initializer || !ts.isJsxExpression(property.initializer)) return undefined;
|
|
788
|
+
const value = property.initializer.expression;
|
|
789
|
+
if (!value) return undefined;
|
|
790
|
+
const localName = forwarding.localBindingByProp.get(propName);
|
|
791
|
+
if (localName && ts.isIdentifier(value) && value.text === localName) return propName;
|
|
792
|
+
if (
|
|
793
|
+
forwarding.wholePropsName &&
|
|
794
|
+
ts.isPropertyAccessExpression(value) &&
|
|
795
|
+
ts.isIdentifier(value.expression) &&
|
|
796
|
+
value.expression.text === forwarding.wholePropsName &&
|
|
797
|
+
value.name.text === propName
|
|
798
|
+
) {
|
|
799
|
+
return propName;
|
|
800
|
+
}
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function forwardedObjectProps(
|
|
805
|
+
opening: ts.JsxOpeningLikeElement,
|
|
806
|
+
forwarding: R3fPropForwarding,
|
|
807
|
+
sf: ts.SourceFile,
|
|
808
|
+
): R3fObjectProp[] {
|
|
809
|
+
const forwarded = new Set<R3fObjectProp>();
|
|
810
|
+
for (const property of opening.attributes.properties) {
|
|
811
|
+
if (ts.isJsxSpreadAttribute(property)) {
|
|
812
|
+
if (spreadForwardsAll(property, forwarding)) {
|
|
813
|
+
for (const channel of R3F_TRANSFORM_PROPS) forwarded.add(channel);
|
|
814
|
+
forwarded.add('visible');
|
|
815
|
+
}
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const prop = attributeForwardsObjectProp(property, forwarding, sf);
|
|
819
|
+
if (prop) forwarded.add(prop);
|
|
820
|
+
}
|
|
821
|
+
const ordered: readonly R3fObjectProp[] = [...R3F_TRANSFORM_PROPS, 'visible'];
|
|
822
|
+
return ordered.filter((prop) => forwarded.has(prop));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* A ref whose `.current` transform is written inside a `useFrame` body — the
|
|
827
|
+
* source-visible mark of runtime-driven motion.
|
|
828
|
+
*
|
|
829
|
+
* Optional chaining is part of the pattern, not an edge case: `useRef<
|
|
830
|
+
* THREE.Group>(null)` types `.current` as nullable, so `body.current?.position
|
|
831
|
+
* .copy(…)` is the ordinary spelling and `body.current.position.copy(…)` the
|
|
832
|
+
* non-null-asserted one. Both must read the same.
|
|
833
|
+
*/
|
|
834
|
+
const RUNTIME_MOTION_MUTATION_RE =
|
|
835
|
+
/\b([A-Za-z_$][\w$]*)\.current\??\.(?:position|rotation|quaternion|scale)(?:\??\.[xyzw]+)?\s*(?:[+\-*/]?=|\??\.(?:set|setScalar|copy|add|sub|multiply|lerp|slerp|fromArray)\s*\()/g;
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* The refs any `useFrame` callback in `scope` drives every frame.
|
|
839
|
+
*
|
|
840
|
+
* Deliberately a regex over the callback's text rather than a resolved
|
|
841
|
+
* dataflow analysis — the same honesty budget the rest of this module's source
|
|
842
|
+
* evidence holds to, and the single derivation BOTH readers use (the contract
|
|
843
|
+
* below, and R3F005 in `r3f-project-contracts.ts`) so the two can never
|
|
844
|
+
* disagree about whether one component drives its own root.
|
|
845
|
+
*/
|
|
846
|
+
export function runtimeMotionRefs(scope: ts.Node, sf: ts.SourceFile): Set<string> {
|
|
847
|
+
const refs = new Set<string>();
|
|
848
|
+
const visit = (node: ts.Node): void => {
|
|
849
|
+
if (ts.isCallExpression(node) && /(^|\.)useFrame$/.test(node.expression.getText(sf))) {
|
|
850
|
+
for (const match of node.getText(sf).matchAll(RUNTIME_MOTION_MUTATION_RE)) {
|
|
851
|
+
refs.add(match[1]!);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
ts.forEachChild(node, visit);
|
|
855
|
+
};
|
|
856
|
+
visit(scope);
|
|
857
|
+
return refs;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** `{ simulationOwnedTransform: true }` when this root's transform is written
|
|
861
|
+
* by the runtime and by nothing else — see the field's own doc. Only when
|
|
862
|
+
* NOTHING is forwarded: a component that forwards a channel and then animates
|
|
863
|
+
* it is the genuine conflict R3F005 exists for, not a simulation-owned
|
|
864
|
+
* instance. */
|
|
865
|
+
function simulationOwnership(
|
|
866
|
+
opening: ts.JsxOpeningLikeElement,
|
|
867
|
+
transformProps: readonly R3fTransformProp[],
|
|
868
|
+
motionRefs: ReadonlySet<string>,
|
|
869
|
+
): { simulationOwnedTransform?: true } {
|
|
870
|
+
if (transformProps.length > 0) return {};
|
|
871
|
+
const rootRef = refIdentifier(opening);
|
|
872
|
+
return rootRef && motionRefs.has(rootRef) ? { simulationOwnedTransform: true } : {};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function componentContract(
|
|
876
|
+
parameters: ts.NodeArray<ts.ParameterDeclaration>,
|
|
877
|
+
body: ts.ConciseBody,
|
|
878
|
+
sf: ts.SourceFile,
|
|
879
|
+
typeAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
880
|
+
builtinPropTypes: ReadonlyMap<string, string>,
|
|
881
|
+
knownContracts: ReadonlyMap<string, R3fComponentContract>,
|
|
882
|
+
motionRefs: ReadonlySet<string>,
|
|
883
|
+
physicsBindings: ReadonlyMap<ts.JsxOpeningLikeElement, R3fPhysicsBinding>,
|
|
884
|
+
): R3fComponentContract {
|
|
885
|
+
const expression = returnedExpression(body);
|
|
886
|
+
if (!expression) return { root: 'unknown', transformProps: [] };
|
|
887
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword)
|
|
888
|
+
return { root: 'non-spatial', transformProps: [] };
|
|
889
|
+
if (ts.isJsxFragment(expression))
|
|
890
|
+
return fragmentComponentContract(expression, sf, knownContracts);
|
|
891
|
+
if (!ts.isJsxElement(expression) && !ts.isJsxSelfClosingElement(expression)) {
|
|
892
|
+
return { root: 'unknown', transformProps: [] };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const opening = spatialRootOpening(expression);
|
|
896
|
+
if (!opening) return { root: 'unknown', transformProps: [] };
|
|
897
|
+
const rootTag = opening.tagName.getText(sf);
|
|
898
|
+
// A material/geometry/attribute root is not a spatial instance at all — see
|
|
899
|
+
// `isNonSpatialNativeTag`. Classified here rather than suppressed at each
|
|
900
|
+
// rule, so every rule that keys off `root === 'single'` gets it at once.
|
|
901
|
+
if (isNonSpatialNativeTag(rootTag)) return { root: 'non-spatial', transformProps: [] };
|
|
902
|
+
const delegated = /^[A-Z]/.test(rootTag) ? knownContracts.get(rootTag) : undefined;
|
|
903
|
+
if (/^[A-Z]/.test(rootTag) && !delegated) return { root: 'unknown', transformProps: [] };
|
|
904
|
+
if (delegated && delegated.root !== 'single') return delegated;
|
|
905
|
+
const nativeRootTag = delegated?.rootTag ?? rootTag;
|
|
906
|
+
// A channel this component hands to the PHYSICS BINDING that places its root
|
|
907
|
+
// is forwarded exactly as truly as one written onto the root's own tag — the
|
|
908
|
+
// body is what puts the object in the world, so the callsite literal the hook
|
|
909
|
+
// reads IS this instance's placement (`r3f-physics-binding.ts`). Without this
|
|
910
|
+
// the contract said `Ramp` "does not forward position to its native root"
|
|
911
|
+
// and refused `<Ramp position={[2, -1, 168.55]} />`, the one literal in the
|
|
912
|
+
// whole chain that actually places the ramp.
|
|
913
|
+
const rootBinding = physicsBindings.get(opening);
|
|
914
|
+
// A channel the component's OWN `useFrame` drives through the body's api is
|
|
915
|
+
// NOT forwarded in any sense a write can rely on: the hook may read the
|
|
916
|
+
// callsite literal at spawn, but the per-frame driver has the last word and
|
|
917
|
+
// the authored value is dead on the next tick (racing-game's `Train` — a
|
|
918
|
+
// Kinematic body re-posed from its animated group every frame; the doctor's
|
|
919
|
+
// settle leg measured the authored spawn discarded, digit for digit). Those
|
|
920
|
+
// channels leave the forwarded set here, and the component is classified
|
|
921
|
+
// simulation-owned below, so a callsite write refuses with the sentence
|
|
922
|
+
// that names the real owner instead of persisting a literal that lies.
|
|
923
|
+
const apiDriven = (rootBinding?.apiDriven ?? []) as readonly string[];
|
|
924
|
+
const toBinding = ((rootBinding?.forwarded ?? []) as readonly string[]).filter(
|
|
925
|
+
(channel) => !apiDriven.includes(channel),
|
|
926
|
+
);
|
|
927
|
+
const declared = propForwarding(parameters[0], nativeRootTag, sf, typeAliases, builtinPropTypes);
|
|
928
|
+
// A GUARD WRAPPER spells its props AS the component it delegates to —
|
|
929
|
+
// `props: Parameters<typeof WeaponModelBody>[0]` — and spreads them whole.
|
|
930
|
+
// `typeAllowsStandardRootProps` reads type TEXT and aliases, so that spelling
|
|
931
|
+
// reads as "accepts no root props" and the wrapper was reported as forwarding
|
|
932
|
+
// nothing: first-person's `WeaponModel` (the hook-free discriminator check in
|
|
933
|
+
// front of `WeaponModelBody` that its own header documents, bought by runhuman
|
|
934
|
+
// pass 97) drew a permanent R3F002 telling the author to forward
|
|
935
|
+
// position/rotation/scale it already forwards, all three, through `{...props}`.
|
|
936
|
+
// The delegate's contract is resolved just above, so a wrapper whose props are
|
|
937
|
+
// SPELLED as the delegate's accepts exactly what the delegate accepts.
|
|
938
|
+
const forwarding =
|
|
939
|
+
!declared.standardRootProps &&
|
|
940
|
+
delegated &&
|
|
941
|
+
typeIsDelegateProps(parameters[0]?.type, rootTag, sf)
|
|
942
|
+
? { ...declared, standardRootProps: true }
|
|
943
|
+
: declared;
|
|
944
|
+
const toTag = forwardedObjectProps(opening, forwarding, sf);
|
|
945
|
+
const ownForwarding = R3F_TRANSFORM_PROPS.filter(
|
|
946
|
+
(channel) => toTag.includes(channel) || toBinding.includes(channel),
|
|
947
|
+
);
|
|
948
|
+
const transformProps = delegated
|
|
949
|
+
? ownForwarding.filter((channel) => delegated.transformProps.includes(channel))
|
|
950
|
+
: ownForwarding;
|
|
951
|
+
const forwardsVisible =
|
|
952
|
+
toTag.includes('visible') && (!delegated || delegated.visibleProp === true);
|
|
953
|
+
|
|
954
|
+
// A built-in package contract may only ADD authorability. A component that
|
|
955
|
+
// returns an external component while forwarding no transform channel to it
|
|
956
|
+
// is exactly as opaque as it was before the table existed — reading the
|
|
957
|
+
// package's root as its own would claim a native root the component never
|
|
958
|
+
// routes anything to, and turn a silent unknown into a "does not forward"
|
|
959
|
+
// warning about source the author cannot act on.
|
|
960
|
+
if (isBuiltinR3fContract(delegated) && transformProps.length === 0) {
|
|
961
|
+
return { root: 'unknown', transformProps: [] };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// WHICH of the forwarded channels a BODY reads, kept because the answer
|
|
965
|
+
// changes what an authored write has to survive (see `bodyForwarded`).
|
|
966
|
+
const bodyForwarded = transformProps.filter((channel) => toBinding.includes(channel));
|
|
967
|
+
|
|
968
|
+
return {
|
|
969
|
+
root: 'single',
|
|
970
|
+
rootTag: nativeRootTag,
|
|
971
|
+
transformProps,
|
|
972
|
+
...(bodyForwarded.length > 0 ? { bodyForwarded } : {}),
|
|
973
|
+
...(forwardsVisible ? { visibleProp: true as const } : {}),
|
|
974
|
+
...simulationOwnership(opening, transformProps, motionRefs),
|
|
975
|
+
// The api-driven case (see `apiDriven` above): the component drives its
|
|
976
|
+
// body every frame, so its instances are placed by the simulation exactly
|
|
977
|
+
// as `runtimeMotionRefs`'s direct-ref case is.
|
|
978
|
+
...(apiDriven.length > 0 ? { simulationOwnedTransform: true as const } : {}),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
interface R3fComponentDefinition {
|
|
983
|
+
name: string;
|
|
984
|
+
parameters: ts.NodeArray<ts.ParameterDeclaration>;
|
|
985
|
+
body: ts.ConciseBody;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function componentDefinitions(statement: ts.Statement): R3fComponentDefinition[] {
|
|
989
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
|
|
990
|
+
return /^[A-Z]/.test(statement.name.text)
|
|
991
|
+
? [{ name: statement.name.text, parameters: statement.parameters, body: statement.body }]
|
|
992
|
+
: [];
|
|
993
|
+
}
|
|
994
|
+
if (!ts.isVariableStatement(statement)) return [];
|
|
995
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
996
|
+
if (
|
|
997
|
+
!ts.isIdentifier(declaration.name) ||
|
|
998
|
+
!/^[A-Z]/.test(declaration.name.text) ||
|
|
999
|
+
!declaration.initializer ||
|
|
1000
|
+
(!ts.isArrowFunction(declaration.initializer) &&
|
|
1001
|
+
!ts.isFunctionExpression(declaration.initializer))
|
|
1002
|
+
) {
|
|
1003
|
+
return [];
|
|
1004
|
+
}
|
|
1005
|
+
return [
|
|
1006
|
+
{
|
|
1007
|
+
name: declaration.name.text,
|
|
1008
|
+
parameters: declaration.initializer.parameters,
|
|
1009
|
+
body: declaration.initializer.body,
|
|
1010
|
+
},
|
|
1011
|
+
];
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function collectR3fComponentContracts(
|
|
1016
|
+
sf: ts.SourceFile,
|
|
1017
|
+
importedContracts: ReadonlyMap<string, R3fComponentContract> = new Map(),
|
|
1018
|
+
importedTypeAliases: ReadonlyMap<string, R3fTypeAlias> = new Map(),
|
|
1019
|
+
): Map<string, R3fComponentContract> {
|
|
1020
|
+
// Imported first, so a local declaration of the same name shadows it exactly
|
|
1021
|
+
// as JavaScript lexical scope does — the same rule `importedContracts`
|
|
1022
|
+
// already follows for components.
|
|
1023
|
+
const aliases = new Map<string, R3fTypeAlias>(importedTypeAliases);
|
|
1024
|
+
for (const statement of sf.statements) {
|
|
1025
|
+
if (ts.isTypeAliasDeclaration(statement))
|
|
1026
|
+
aliases.set(statement.name.text, { type: statement.type, sf });
|
|
1027
|
+
}
|
|
1028
|
+
const builtins = builtinRootPropTypes(sf);
|
|
1029
|
+
|
|
1030
|
+
const definitions = sf.statements.flatMap(componentDefinitions);
|
|
1031
|
+
const motionRefs = runtimeMotionRefs(sf, sf);
|
|
1032
|
+
const physicsBindings = physicsBindingsByElement(sf);
|
|
1033
|
+
const contracts = new Map(importedContracts);
|
|
1034
|
+
// Components commonly delegate their native root to another local component.
|
|
1035
|
+
// Resolve that ordinary React composition to a fixed point rather than
|
|
1036
|
+
// requiring definition order or an authoring-specific wrapper.
|
|
1037
|
+
for (let pass = 0; pass <= definitions.length; pass += 1) {
|
|
1038
|
+
let changed = false;
|
|
1039
|
+
for (const definition of definitions) {
|
|
1040
|
+
const next = componentContract(
|
|
1041
|
+
definition.parameters,
|
|
1042
|
+
definition.body,
|
|
1043
|
+
sf,
|
|
1044
|
+
aliases,
|
|
1045
|
+
builtins,
|
|
1046
|
+
contracts,
|
|
1047
|
+
motionRefs,
|
|
1048
|
+
physicsBindings,
|
|
1049
|
+
);
|
|
1050
|
+
const previous = contracts.get(definition.name);
|
|
1051
|
+
if (JSON.stringify(previous) !== JSON.stringify(next)) {
|
|
1052
|
+
contracts.set(definition.name, next);
|
|
1053
|
+
changed = true;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (!changed) break;
|
|
1057
|
+
}
|
|
1058
|
+
return contracts;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** Analyze the component bindings visible in one R3F module. Imported
|
|
1062
|
+
* bindings are supplied by the project resolver; local definitions override
|
|
1063
|
+
* them exactly as JavaScript lexical scope does. */
|
|
1064
|
+
export function analyzeR3fComponentContracts(
|
|
1065
|
+
code: string,
|
|
1066
|
+
file: string,
|
|
1067
|
+
importedContracts: ReadonlyMap<string, R3fComponentContract> = new Map(),
|
|
1068
|
+
importedTypeAliases: ReadonlyMap<string, R3fTypeAlias> = new Map(),
|
|
1069
|
+
): Map<string, R3fComponentContract> {
|
|
1070
|
+
const sf = parseAuthoringTsx(file, code);
|
|
1071
|
+
return collectR3fComponentContracts(sf, importedContracts, importedTypeAliases);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Mirrors `@volter/editor-project/adapter/adapter-surface`'s `AdapterSurface` vocabulary
|
|
1076
|
+
* (`'three' | 'canvas' | 'dom'`) as a bare literal union rather than
|
|
1077
|
+
* importing it: this module is deliberately dependency-free (see the module
|
|
1078
|
+
* doc comment above `OidEntry`) so a page with no bundler alias resolution can
|
|
1079
|
+
* import it too. Every caller
|
|
1080
|
+
* that already carries the real `AdapterSurface` (e.g. `binding-resolver.ts`)
|
|
1081
|
+
* may pass it here directly; the two types are structurally identical.
|
|
1082
|
+
*/
|
|
1083
|
+
export type DeclaredRootSurface = 'three' | 'canvas' | 'dom';
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Intrinsic (lowercase-initial) JSX tags that exist ONLY in react-three-fiber's
|
|
1087
|
+
* reconciler — there is no HTML or SVG element with any of these names.
|
|
1088
|
+
*
|
|
1089
|
+
* DIAGNOSTIC EVIDENCE ONLY. This does not classify a file's dialect and must
|
|
1090
|
+
* not be made to: which region a file belongs to is decided by declaration and
|
|
1091
|
+
* import reach (`file-region-resolver.ts`), because that is a fact the game's
|
|
1092
|
+
* author states or the host EXECUTES, while a tag list is a guess about a file
|
|
1093
|
+
* whose imports it cannot see. What the scan is still for is telling an author
|
|
1094
|
+
* what the editor OBSERVED: the `OID003`/`OID004` messages in
|
|
1095
|
+
* `server/project-root-surface.ts` name the tags a file renders, and `OID004` —
|
|
1096
|
+
* a file rendering both dialects' intrinsics, which one attribute cannot serve
|
|
1097
|
+
* — is nothing BUT this observation.
|
|
1098
|
+
*
|
|
1099
|
+
* Deliberately CONSERVATIVE: every entry is checked against the HTML and SVG
|
|
1100
|
+
* element sets, so nothing here can be a DOM tag. `line`, `points`, `sprite`,
|
|
1101
|
+
* `audio`, `color` and `fog` are all real R3F intrinsics but are EXCLUDED from
|
|
1102
|
+
* this list where SVG/HTML also spells them (`<line>`, `<audio>`) — the list is
|
|
1103
|
+
* evidence, not a catalogue, and a wrong entry would flip a genuine DOM file.
|
|
1104
|
+
* `[a-z][A-Za-z]*(Geometry|Material)` covers the whole geometry/material family
|
|
1105
|
+
* (`boxGeometry`, `meshStandardMaterial`, `shaderMaterial`, …) without naming
|
|
1106
|
+
* each one; the leading lowercase class is what keeps it from matching a
|
|
1107
|
+
* CUSTOM component (`<MyMaterial>`), which is a callsite, not an intrinsic.
|
|
1108
|
+
*/
|
|
1109
|
+
const R3F_ONLY_INTRINSIC_RE =
|
|
1110
|
+
/<(mesh|instancedMesh|batchedMesh|skinnedMesh|group|primitive|lineSegments|bufferGeometry|bufferAttribute|instancedBufferAttribute|[a-z][A-Za-z]*Geometry|[a-z][A-Za-z]*Material|ambientLight|directionalLight|pointLight|spotLight|hemisphereLight|rectAreaLight|perspectiveCamera|orthographicCamera|axesHelper|gridHelper|boxHelper)[\s/>]/g;
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* The mirror image: intrinsic tags that exist ONLY in the react-dom
|
|
1114
|
+
* reconciler.
|
|
1115
|
+
*
|
|
1116
|
+
* DIAGNOSTIC EVIDENCE ONLY, on the same terms as the R3F list above: it
|
|
1117
|
+
* classifies no file. What a file renders is reported (`OID003`/`OID004` in
|
|
1118
|
+
* `server/project-root-surface.ts`); which region a file BELONGS to is a
|
|
1119
|
+
* declaration or a reach, never a property of its bytes. Excludes every name R3F also spells as an intrinsic
|
|
1120
|
+
* (`line`, `points`, `sprite`, `audio`, `color`, `fog`, `path`, `circle`), so
|
|
1121
|
+
* a hit here is unambiguous.
|
|
1122
|
+
*/
|
|
1123
|
+
const DOM_ONLY_INTRINSIC_RE =
|
|
1124
|
+
/<(div|span|p|button|section|header|footer|nav|main|aside|form|input|textarea|select|option|label|table|thead|tbody|tr|td|th|img|iframe|br|hr|strong|em|pre|code|canvas|ul|ol|li|h[1-6])[\s/>]/g;
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* What ONE file's own bytes say about which reconciler its JSX targets.
|
|
1128
|
+
*
|
|
1129
|
+
* DIAGNOSTIC EVIDENCE ONLY — it decides no attribute anywhere. What a file
|
|
1130
|
+
* renders is REPORTED (`server/project-root-surface.ts`'s `OID003`/`OID004`);
|
|
1131
|
+
* which region it belongs to is a declaration or a reach, never a property of
|
|
1132
|
+
* its bytes.
|
|
1133
|
+
*
|
|
1134
|
+
* Deliberately a cheap regex scan, not an AST walk, because it runs in a
|
|
1135
|
+
* `transform` hook on every project `.tsx`. COMMENTS are
|
|
1136
|
+
* stripped first (see {@link stripCommentsForScan}) so prose that merely
|
|
1137
|
+
* NAMES a tag cannot count as rendering it; a mention inside a string still
|
|
1138
|
+
* matches, which is deliberate — import specifiers ARE strings.
|
|
1139
|
+
*/
|
|
1140
|
+
export interface SourceDialectEvidence {
|
|
1141
|
+
/** Imports the R3F reconciler, or the upstreamed `@vgai/game-runtime/world3d-react` bridge. */
|
|
1142
|
+
reconcilerImport: boolean;
|
|
1143
|
+
/** Distinct R3F-only intrinsic tags this file renders. */
|
|
1144
|
+
r3fOnlyTags: string[];
|
|
1145
|
+
/** Distinct DOM-only intrinsic tags this file renders. */
|
|
1146
|
+
domOnlyTags: string[];
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function distinctTags(code: string, pattern: RegExp): string[] {
|
|
1150
|
+
const found = new Set<string>();
|
|
1151
|
+
for (const match of code.matchAll(pattern)) if (match[1]) found.add(match[1]);
|
|
1152
|
+
return [...found];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Drop line and block comments before the dialect scan, copying string
|
|
1157
|
+
* literals through untouched. The shipped case that bought this: the starter
|
|
1158
|
+
* dev-tools JSDoc names its command shape `cheat.<group>.<label>` — `group`
|
|
1159
|
+
* is an R3F-only intrinsic and `label` a DOM-only one, so ONE comment line
|
|
1160
|
+
* made a fresh scaffold boot with a dialect-mixing diagnostic about a file
|
|
1161
|
+
* with no JSX in it. A diagnostic channel agents are told to trust cannot
|
|
1162
|
+
* cry wolf over prose.
|
|
1163
|
+
*
|
|
1164
|
+
* Single pass, no AST — the transform-hook perf bar stands. Known residual
|
|
1165
|
+
* imprecision, accepted: a regex literal containing `//` or `/*` is misread
|
|
1166
|
+
* as a comment and drops the rest of itself — which degrades toward "no
|
|
1167
|
+
* evidence" (the safe default), never toward a fabricated tag.
|
|
1168
|
+
*/
|
|
1169
|
+
function stripCommentsForScan(code: string): string {
|
|
1170
|
+
const kept: string[] = [];
|
|
1171
|
+
let i = 0;
|
|
1172
|
+
while (i < code.length) {
|
|
1173
|
+
const ch = code[i]!;
|
|
1174
|
+
const next = code[i + 1];
|
|
1175
|
+
if (ch === '/' && (next === '/' || next === '*')) {
|
|
1176
|
+
// A space, not nothing: gluing a block comment's neighbors could mint
|
|
1177
|
+
// a token that was never in the source.
|
|
1178
|
+
if (next === '*') kept.push(' ');
|
|
1179
|
+
i = commentEnd(code, i, next);
|
|
1180
|
+
} else if (ch === "'" || ch === '"' || ch === '`') {
|
|
1181
|
+
const end = stringLiteralEnd(code, i);
|
|
1182
|
+
kept.push(code.slice(i, end));
|
|
1183
|
+
i = end;
|
|
1184
|
+
} else {
|
|
1185
|
+
kept.push(ch);
|
|
1186
|
+
i += 1;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return kept.join('');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** End index (exclusive) of the comment opening at `start` — a `//` comment
|
|
1193
|
+
* stops BEFORE its newline (the newline stays in the scanned text). */
|
|
1194
|
+
function commentEnd(code: string, start: number, kind: '/' | '*'): number {
|
|
1195
|
+
if (kind === '/') {
|
|
1196
|
+
const newline = code.indexOf('\n', start + 2);
|
|
1197
|
+
return newline === -1 ? code.length : newline;
|
|
1198
|
+
}
|
|
1199
|
+
const close = code.indexOf('*/', start + 2);
|
|
1200
|
+
return close === -1 ? code.length : close + 2;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** End index (exclusive) of the string literal opening at `start`, honoring
|
|
1204
|
+
* backslash escapes; an unterminated literal runs to the end of the file. */
|
|
1205
|
+
function stringLiteralEnd(code: string, start: number): number {
|
|
1206
|
+
const quote = code[start]!;
|
|
1207
|
+
let i = start + 1;
|
|
1208
|
+
while (i < code.length) {
|
|
1209
|
+
const ch = code[i]!;
|
|
1210
|
+
if (ch === '\\') i += 2;
|
|
1211
|
+
else if (ch === quote) return i + 1;
|
|
1212
|
+
else i += 1;
|
|
1213
|
+
}
|
|
1214
|
+
return code.length;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** Scan one file's source for self-contained evidence of its JSX dialect. */
|
|
1218
|
+
export function sourceDialectEvidence(code: string): SourceDialectEvidence {
|
|
1219
|
+
const scanned = stripCommentsForScan(code);
|
|
1220
|
+
return {
|
|
1221
|
+
reconcilerImport:
|
|
1222
|
+
scanned.includes('@react-three/fiber') ||
|
|
1223
|
+
scanned.includes('@vgai/game-runtime/world3d-react'),
|
|
1224
|
+
r3fOnlyTags: distinctTags(scanned, R3F_ONLY_INTRINSIC_RE),
|
|
1225
|
+
domOnlyTags: distinctTags(scanned, DOM_ONLY_INTRINSIC_RE),
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** True when a file's own bytes prove it authors for the R3F reconciler. */
|
|
1230
|
+
export function sourceProvesR3f(evidence: SourceDialectEvidence): boolean {
|
|
1231
|
+
return evidence.reconcilerImport || evidence.r3fOnlyTags.length > 0;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* THE SOURCE-ID ATTRIBUTE for one file, decided by its REGION and nothing else.
|
|
1236
|
+
*
|
|
1237
|
+
* 1. **The region (authoritative).** `surface` is the answer
|
|
1238
|
+
* `file-region-resolver.ts` gave for THIS file — a declaration the game
|
|
1239
|
+
* made (a region `include` glob, a region `mounts` entry, or the manifest
|
|
1240
|
+
* root whose `entry` this file is) or the region whose import closure
|
|
1241
|
+
* reaches it. `'three'` stamps `userData-oid`; `'canvas'`/`'dom'` stamp
|
|
1242
|
+
* `data-oid`. It is resolved from OUTSIDE the file, exactly where the fact
|
|
1243
|
+
* lives, which is what makes it right for a world whose entry
|
|
1244
|
+
* default-exports a component and imports NEITHER `@react-three/fiber` nor
|
|
1245
|
+
* `@vgai/game-runtime/world3d-react` (legal: R3F's global JSX-intrinsics augmentation,
|
|
1246
|
+
* or an entry composed entirely of already-typed child components, needs no
|
|
1247
|
+
* import in THIS file). Every call site threads it from a context that
|
|
1248
|
+
* resolved it per file: `binding-resolver.ts`'s per-surface resolvers, the
|
|
1249
|
+
* browser bundle's own per-file resolution
|
|
1250
|
+
* (`browser-transpile.ts`), and the dev server's
|
|
1251
|
+
* `server/project-root-surface.ts` — shared with `classifyProjectHotUpdate`,
|
|
1252
|
+
* the HMR half of this same decision, so the two cannot drift apart by hand.
|
|
1253
|
+
*
|
|
1254
|
+
* 2. **The DEFAULT.** When `surface` is `undefined` — no region placed the
|
|
1255
|
+
* file (an ad-hoc file with no `vgai.project.json` above it, or a module no
|
|
1256
|
+
* region's import closure reaches and none declares) — the answer is
|
|
1257
|
+
* `data-oid`, the documented default, and the CALLER says so out loud
|
|
1258
|
+
* (`server/project-root-surface.ts`'s `OID001`/`OID002`). There is
|
|
1259
|
+
* deliberately no source-text rung: a probe over a file's own bytes answered
|
|
1260
|
+
* for a file it could not place, silently, and being wrong that way is
|
|
1261
|
+
* indistinguishable from being right. Zero inference (ARCHITECTURE-CORE
|
|
1262
|
+
* §The editor protocol) makes the fix a declaration — an `include` glob or
|
|
1263
|
+
* a `mounts` entry in `vgai.adapter.ts` — not a better heuristic.
|
|
1264
|
+
*
|
|
1265
|
+
* Nothing about a file's own bytes outranks the region — not even a file that
|
|
1266
|
+
* renders THREE host elements while its region says `dom`. That disagreement is
|
|
1267
|
+
* FATAL rather than noisy (`data-oid` on a THREE object pierces inside fiber,
|
|
1268
|
+
* writes `object.data`, and throws on the next apply), which is exactly why it
|
|
1269
|
+
* is reported at full volume as declared-vs-measured DRIFT
|
|
1270
|
+
* (`server/project-root-surface.ts`'s `OID003`) and repaired by fixing the
|
|
1271
|
+
* declaration. A stamp derived from the bytes instead would make the
|
|
1272
|
+
* declaration untestable, which is how a wrong one stays invisible.
|
|
1273
|
+
*
|
|
1274
|
+
* Lives HERE, beside the transform it parameterizes, rather than in
|
|
1275
|
+
* `vite-plugin-ui-oid.ts` (which re-exports it for its existing callers):
|
|
1276
|
+
* a caller in a page reaches the same `transformSource`, and that path cannot
|
|
1277
|
+
* import a `node:fs`-bearing module.
|
|
1278
|
+
*/
|
|
1279
|
+
export function oidAttributeForSurface(
|
|
1280
|
+
declaredSurface?: DeclaredRootSurface,
|
|
1281
|
+
): 'data-oid' | 'userData-oid' {
|
|
1282
|
+
return declaredSurface === 'three' ? 'userData-oid' : 'data-oid';
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** Options for {@link transformSource}. */
|
|
1286
|
+
export interface TransformSourceOptions {
|
|
1287
|
+
/**
|
|
1288
|
+
* The attribute name stamped on every JSX element (default `data-oid`).
|
|
1289
|
+
* R3F-dialect files (spike W1) pass `userData-oid` instead:
|
|
1290
|
+
* react-three-fiber's dashed-prop piercing writes `object.userData.oid`
|
|
1291
|
+
* on the constructed Object3D at reconcile time — zero runtime cost, and
|
|
1292
|
+
* the editor's R3F authoring adapter reads it back off the live scene
|
|
1293
|
+
* graph. The OID→{file,line,col} index is attribute-agnostic (keyed by
|
|
1294
|
+
* oid only), so every `/__ui-source/*` endpoint works unchanged for
|
|
1295
|
+
* either dialect.
|
|
1296
|
+
*/
|
|
1297
|
+
attribute?: string;
|
|
1298
|
+
/** Contracts for ordinary project-local imported component bindings. */
|
|
1299
|
+
importedR3fContracts?: ReadonlyMap<string, R3fComponentContract>;
|
|
1300
|
+
/** The prop-shape type aliases those same imports bring into scope. A
|
|
1301
|
+
* project that writes its canonical prop shape once and imports it is
|
|
1302
|
+
* otherwise read as forwarding nothing. */
|
|
1303
|
+
importedR3fTypeAliases?: ReadonlyMap<string, R3fTypeAlias>;
|
|
1304
|
+
/** The file is mounted by @pixi/react. Local component callsites propagate
|
|
1305
|
+
* their OID onto the component's first native Pixi host, exactly as R3F
|
|
1306
|
+
* does with userData, but through ordinary data-* constructor fields. */
|
|
1307
|
+
canvasComponents?: boolean;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* The local names bound to drei's `<Html>` in this file — the ONE seam where a
|
|
1312
|
+
* three-surface file renders through **react-dom** instead of the R3F
|
|
1313
|
+
* reconciler. `<Html>` creates a real `<div>`, mounts a `createRoot` on it and
|
|
1314
|
+
* renders its children there, so every host element inside it is a DOM element
|
|
1315
|
+
* and the R3F-pierced identity props this transform stamps on host elements
|
|
1316
|
+
* (`userData-oid`, `userData-authoringInstance`, `userData-authoringLabel`,
|
|
1317
|
+
* and the `userData={{}}` seed) have nowhere to land.
|
|
1318
|
+
*
|
|
1319
|
+
* Measured: the starter template's own `BotHud` — screen-pinned dev chrome
|
|
1320
|
+
* built on `<Html>`, the documented way to put DOM in a Three world — logged
|
|
1321
|
+
* FOUR `React does not recognize the \`userData-…\` prop on a DOM element`
|
|
1322
|
+
* errors on every bot run, one per stamped prop. Inside an `<Html>` subtree we
|
|
1323
|
+
* therefore stamp `data-oid`, byte-for-byte what a `dom` root gets.
|
|
1324
|
+
*
|
|
1325
|
+
* Lexical by design: this reads the subtree written in THIS file, which is the
|
|
1326
|
+
* whole extent the transform can see. A component defined elsewhere and
|
|
1327
|
+
* rendered inside `<Html>` is transformed by its own pass against its own
|
|
1328
|
+
* declared surface.
|
|
1329
|
+
*/
|
|
1330
|
+
function dreiHtmlLocalNames(sf: ts.SourceFile): Set<string> {
|
|
1331
|
+
const names = new Set<string>();
|
|
1332
|
+
for (const statement of sf.statements) {
|
|
1333
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1337
|
+
if (specifier !== '@react-three/drei' && !specifier.startsWith('@react-three/drei/')) continue;
|
|
1338
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1339
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
1340
|
+
for (const element of bindings.elements) {
|
|
1341
|
+
if ((element.propertyName?.text ?? element.name.text) === 'Html')
|
|
1342
|
+
names.add(element.name.text);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return names;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Local JSX names bound by an import from an INSTALLED PACKAGE — every
|
|
1350
|
+
* specifier that is not relative (`./x`, `../x`), which is exactly the line
|
|
1351
|
+
* `r3f-project-contracts.ts`'s `resolveRelativeModule` already draws between
|
|
1352
|
+
* "source this editor walks" and "a dependency it never guesses about".
|
|
1353
|
+
*
|
|
1354
|
+
* Why the callsite stamp must skip them: `__vgaiOid`/`__vgaiLabel` are a
|
|
1355
|
+
* CONTRACT BETWEEN TWO HALVES OF THIS TRANSFORM. The callsite half writes
|
|
1356
|
+
* them; the only thing that ever reads them is the callee half —
|
|
1357
|
+
* `componentInstanceExpressions` destructuring them out of the component's own
|
|
1358
|
+
* parameters and re-emitting them as `userData-authoringInstance` on that
|
|
1359
|
+
* component's host elements. Nothing anywhere reads them off a React instance
|
|
1360
|
+
* or a DOM node. A package module is never served through this transform, so
|
|
1361
|
+
* the callee half never runs for it: the props have no reader by construction,
|
|
1362
|
+
* and the only thing they can still do is escape.
|
|
1363
|
+
*
|
|
1364
|
+
* And they do. Measured on the vendored `racing-game` ingest: `App.tsx` is a
|
|
1365
|
+
* MIXED file (an R3F `Scene` and a DOM `Hud` in one module, so the file's one
|
|
1366
|
+
* dialect is `userData-oid`), and its `<Canvas>` — `@react-three/fiber`'s own,
|
|
1367
|
+
* which spreads its rest props onto the `<div>` it wraps the canvas in — put
|
|
1368
|
+
* `__vgaiOid` and `__vgaiLabel` straight into the DOM, for two permanent
|
|
1369
|
+
* `React does not recognize the \`__vgaiOid\` prop on a DOM element` errors on
|
|
1370
|
+
* every mount of a real game. Nothing was lost by not stamping it, because
|
|
1371
|
+
* nothing was reading it.
|
|
1372
|
+
*
|
|
1373
|
+
* The one package component that DOES carry editor identity earns it through
|
|
1374
|
+
* the hand-audited contract table instead (`BUILTIN_R3F_CONTRACTS`), which
|
|
1375
|
+
* describes the native root the package forwards to — a claim about a specific
|
|
1376
|
+
* package's public API, never a guess made at a callsite.
|
|
1377
|
+
*/
|
|
1378
|
+
function packageImportedLocalNames(sf: ts.SourceFile): Set<string> {
|
|
1379
|
+
const names = new Set<string>();
|
|
1380
|
+
for (const statement of sf.statements) {
|
|
1381
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (statement.moduleSpecifier.text.startsWith('.')) continue;
|
|
1385
|
+
const clause = statement.importClause;
|
|
1386
|
+
if (!clause) continue;
|
|
1387
|
+
if (clause.name) names.add(clause.name.text);
|
|
1388
|
+
const bindings = clause.namedBindings;
|
|
1389
|
+
if (!bindings) continue;
|
|
1390
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
1391
|
+
names.add(bindings.name.text);
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
for (const element of bindings.elements) names.add(element.name.text);
|
|
1395
|
+
}
|
|
1396
|
+
return names;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/** Drei cameras forward these props to their native Three camera. */
|
|
1400
|
+
function dreiCameraLocalNames(sf: ts.SourceFile): Set<string> {
|
|
1401
|
+
const names = new Set<string>();
|
|
1402
|
+
const cameras = ['PerspectiveCamera', 'OrthographicCamera'];
|
|
1403
|
+
for (const statement of sf.statements) {
|
|
1404
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
1405
|
+
continue;
|
|
1406
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1407
|
+
if (
|
|
1408
|
+
specifier !== '@react-three/drei' &&
|
|
1409
|
+
!cameras.some((name) => specifier === `@react-three/drei/core/${name}`)
|
|
1410
|
+
)
|
|
1411
|
+
continue;
|
|
1412
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1413
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
1414
|
+
for (const camera of cameras) names.add(`${bindings.name.text}.${camera}`);
|
|
1415
|
+
} else if (bindings && ts.isNamedImports(bindings)) {
|
|
1416
|
+
for (const element of bindings.elements) {
|
|
1417
|
+
if (cameras.includes(element.propertyName?.text ?? element.name.text))
|
|
1418
|
+
names.add(element.name.text);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return names;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Component-shaped JSX names bound by a function PARAMETER — the second way a
|
|
1427
|
+
* tag's callee is unknowable at transform time, and the same hazard as
|
|
1428
|
+
* {@link packageImportedLocalNames} for exactly the same reason.
|
|
1429
|
+
*
|
|
1430
|
+
* `__vgaiOid`/`__vgaiLabel` are a contract between two halves of this
|
|
1431
|
+
* transform: the callsite half writes them, the callee half destructures them
|
|
1432
|
+
* out of the component's own parameters. A tag bound to a parameter names
|
|
1433
|
+
* WHATEVER the caller passed, so this module cannot know whether the callee
|
|
1434
|
+
* half ever ran for it — and where it did not, the props' only remaining
|
|
1435
|
+
* behaviour is to escape into the DOM through any rest-prop spread.
|
|
1436
|
+
*
|
|
1437
|
+
* MEASURED on the vendored `racing-game` ingest, against the SERVED modules:
|
|
1438
|
+
* `useToggle.tsx`'s `<ToggledComponent {...props} />` (its `ToggledComponent`
|
|
1439
|
+
* is a parameter — drei's `Stats`/`OrbitControls`, cannon's `Debug`, or a
|
|
1440
|
+
* project component, depending on the call) and every R3F `*.stories.tsx`
|
|
1441
|
+
* decorator's `<Story />`. Two standing
|
|
1442
|
+
* `React does not recognize the \`__vgaiOid\` prop on a DOM element` errors on
|
|
1443
|
+
* every run of the acceptance portfolio.
|
|
1444
|
+
*
|
|
1445
|
+
* Nothing is lost by not stamping, and identity actually IMPROVES: a forwarder
|
|
1446
|
+
* spreads the caller's own props first, so the oid the REAL callsite wrote
|
|
1447
|
+
* (`<ToggledMap/>` in `App.tsx`) now reaches the wrapped component instead of
|
|
1448
|
+
* being overwritten by a literal naming the forwarder's own line.
|
|
1449
|
+
*
|
|
1450
|
+
* Scope is the whole file rather than lexical scope on purpose: a name that is
|
|
1451
|
+
* a parameter ANYWHERE in a module is not a stable component identity to stamp
|
|
1452
|
+
* against, and a per-scope walk would buy precision this decision does not use.
|
|
1453
|
+
*/
|
|
1454
|
+
function parameterBoundLocalNames(sf: ts.SourceFile): Set<string> {
|
|
1455
|
+
const names = new Set<string>();
|
|
1456
|
+
const addBinding = (name: ts.BindingName): void => {
|
|
1457
|
+
if (ts.isIdentifier(name)) {
|
|
1458
|
+
if (/^[A-Z]/.test(name.text)) names.add(name.text);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
for (const element of name.elements) {
|
|
1462
|
+
if (ts.isBindingElement(element)) addBinding(element.name);
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
const visit = (node: ts.Node): void => {
|
|
1466
|
+
if (ts.isFunctionLike(node)) {
|
|
1467
|
+
for (const parameter of node.parameters) addBinding(parameter.name);
|
|
1468
|
+
}
|
|
1469
|
+
ts.forEachChild(node, visit);
|
|
1470
|
+
};
|
|
1471
|
+
visit(sf);
|
|
1472
|
+
return names;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/** JSX spellings that resolve to React's transparent Fragment. A Fragment is
|
|
1476
|
+
* composition, not a host node: it accepts only key/children, has no runtime
|
|
1477
|
+
* element to select, and must never receive an OID prop. */
|
|
1478
|
+
function reactFragmentNames(sf: ts.SourceFile): Set<string> {
|
|
1479
|
+
const names = new Set(['Fragment', 'React.Fragment']);
|
|
1480
|
+
for (const statement of sf.statements) {
|
|
1481
|
+
if (
|
|
1482
|
+
!ts.isImportDeclaration(statement) ||
|
|
1483
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
1484
|
+
statement.moduleSpecifier.text !== 'react'
|
|
1485
|
+
) {
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
const clause = statement.importClause;
|
|
1489
|
+
if (clause?.name) names.add(`${clause.name.text}.Fragment`);
|
|
1490
|
+
const bindings = clause?.namedBindings;
|
|
1491
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
1492
|
+
names.add(`${bindings.name.text}.Fragment`);
|
|
1493
|
+
} else if (bindings && ts.isNamedImports(bindings)) {
|
|
1494
|
+
for (const element of bindings.elements) {
|
|
1495
|
+
if ((element.propertyName?.text ?? element.name.text) === 'Fragment') {
|
|
1496
|
+
names.add(element.name.text);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return names;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Transform source: insert `data-oid="…"` (or `opts.attribute="…"`) on every
|
|
1506
|
+
* JSX element and register the index. In the R3F dialect, a custom component
|
|
1507
|
+
* callsite receives editor-only props whose values are propagated onto the
|
|
1508
|
+
* component's existing host Object3Ds. Every transformed project component,
|
|
1509
|
+
* on every surface, consumes those props at its parameter boundary so a
|
|
1510
|
+
* cross-surface component or ordinary `{...props}` spread cannot forward them
|
|
1511
|
+
* into game DOM. That preserves per-instance source identity without inserting
|
|
1512
|
+
* a wrapper into the native tree. Returns the transformed code + index entries.
|
|
1513
|
+
*/
|
|
1514
|
+
export function transformSource(
|
|
1515
|
+
code: string,
|
|
1516
|
+
file: string,
|
|
1517
|
+
store: OidStore,
|
|
1518
|
+
opts?: TransformSourceOptions,
|
|
1519
|
+
): { code: string; entries: OidEntry[] } {
|
|
1520
|
+
const sf = parseAuthoringTsx(file, code);
|
|
1521
|
+
store.clearFile(file);
|
|
1522
|
+
const attribute = opts?.attribute ?? 'data-oid';
|
|
1523
|
+
const projectsComponentInstances =
|
|
1524
|
+
attribute === 'userData-oid' || opts?.canvasComponents === true;
|
|
1525
|
+
const sourceEdits: SourceEdit[] = [];
|
|
1526
|
+
const entries: OidEntry[] = [];
|
|
1527
|
+
const r3fComponentContracts =
|
|
1528
|
+
attribute === 'userData-oid'
|
|
1529
|
+
? collectR3fComponentContracts(sf, opts?.importedR3fContracts, opts?.importedR3fTypeAliases)
|
|
1530
|
+
: new Map();
|
|
1531
|
+
const physicsBindings =
|
|
1532
|
+
attribute === 'userData-oid'
|
|
1533
|
+
? physicsBindingsByElement(sf)
|
|
1534
|
+
: new Map<ts.JsxOpeningLikeElement, R3fPhysicsBinding>();
|
|
1535
|
+
const jointBindings =
|
|
1536
|
+
attribute === 'userData-oid'
|
|
1537
|
+
? jointBindingsByElement(sf)
|
|
1538
|
+
: new Map<ts.JsxOpeningLikeElement, readonly R3fJointBinding[]>();
|
|
1539
|
+
const particleBindings =
|
|
1540
|
+
attribute === 'userData-oid'
|
|
1541
|
+
? particleBindingsByElement(sf)
|
|
1542
|
+
: new Map<ts.JsxOpeningLikeElement, R3fParticleBinding>();
|
|
1543
|
+
const lodBindings =
|
|
1544
|
+
attribute === 'userData-oid'
|
|
1545
|
+
? lodBindingsByElement(sf)
|
|
1546
|
+
: new Map<ts.JsxOpeningLikeElement, R3fLodBinding>();
|
|
1547
|
+
const environmentBindings =
|
|
1548
|
+
attribute === 'userData-oid'
|
|
1549
|
+
? environmentBindingsByElement(sf)
|
|
1550
|
+
: new Map<ts.JsxOpeningLikeElement, R3fEnvironmentBinding>();
|
|
1551
|
+
const componentStack: Array<{
|
|
1552
|
+
name: string;
|
|
1553
|
+
instanceExpression: string;
|
|
1554
|
+
labelExpression: string;
|
|
1555
|
+
}> = [];
|
|
1556
|
+
// per (component:tag) occurrence counters for stable signatures
|
|
1557
|
+
const occ = new Map<string, number>();
|
|
1558
|
+
|
|
1559
|
+
const componentInstanceExpressions = (
|
|
1560
|
+
parameters: ts.NodeArray<ts.ParameterDeclaration>,
|
|
1561
|
+
): { instanceExpression: string; labelExpression: string } => {
|
|
1562
|
+
const first = parameters[0];
|
|
1563
|
+
if (!first) {
|
|
1564
|
+
// Default the whole binding. Isolation/preview/bake call a stamped
|
|
1565
|
+
// export as a factory (`MainScene()` / `WOOD()`) with no args; a
|
|
1566
|
+
// required destructure of `undefined` is `Cannot destructure property
|
|
1567
|
+
// '__vgaiOid'`. React still passes `{}` at a real callsite.
|
|
1568
|
+
sourceEdits.push({ pos: parameters.pos, text: '{ __vgaiOid, __vgaiLabel } = {}' });
|
|
1569
|
+
return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
|
|
1570
|
+
}
|
|
1571
|
+
if (ts.isObjectBindingPattern(first.name)) {
|
|
1572
|
+
sourceEdits.push({
|
|
1573
|
+
pos: first.name.getStart(sf) + 1,
|
|
1574
|
+
text: '__vgaiOid, __vgaiLabel, ',
|
|
1575
|
+
});
|
|
1576
|
+
// After the WHOLE parameter, including a type annotation:
|
|
1577
|
+
// `({ name, ...props }: T)` must become `({ __vgaiOid, …, name, ...props }: T = {})`.
|
|
1578
|
+
// Inserting at the binding's end produced `} = {}: T`, which is not
|
|
1579
|
+
// TypeScript and broke every typed scene export the look verb imports.
|
|
1580
|
+
if (!first.initializer) {
|
|
1581
|
+
sourceEdits.push({ pos: first.getEnd(), text: ' = {}' });
|
|
1582
|
+
}
|
|
1583
|
+
return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
|
|
1584
|
+
}
|
|
1585
|
+
if (ts.isIdentifier(first.name) && !first.dotDotDotToken) {
|
|
1586
|
+
// Preserve the authored identifier as the REST binding. Code below the
|
|
1587
|
+
// parameter continues to read `props`, but it now sees only game props:
|
|
1588
|
+
// `function Card(props: Props)` becomes
|
|
1589
|
+
// `function Card({ __vgaiOid, __vgaiLabel, ...props }: Props)`.
|
|
1590
|
+
// Remove an optional marker while replacing the identifier; a binding
|
|
1591
|
+
// pattern cannot itself be optional, so its equivalent is a `{}` default.
|
|
1592
|
+
sourceEdits.push({
|
|
1593
|
+
pos: first.name.getStart(sf),
|
|
1594
|
+
end: first.questionToken?.end ?? first.name.getEnd(),
|
|
1595
|
+
text: `{ __vgaiOid, __vgaiLabel, ...${first.name.text} }`,
|
|
1596
|
+
});
|
|
1597
|
+
if (!first.initializer) {
|
|
1598
|
+
sourceEdits.push({ pos: first.getEnd(), text: ' = {}' });
|
|
1599
|
+
}
|
|
1600
|
+
return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
|
|
1601
|
+
}
|
|
1602
|
+
return { instanceExpression: '__vgaiOid', labelExpression: '__vgaiLabel' };
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
const pushComponent = (
|
|
1606
|
+
name: string | undefined,
|
|
1607
|
+
parameters: ts.NodeArray<ts.ParameterDeclaration> | undefined,
|
|
1608
|
+
): boolean => {
|
|
1609
|
+
if (!name || !/^[A-Z]/.test(name) || !parameters) return false;
|
|
1610
|
+
// Component membership participates in the stable OID signature on every
|
|
1611
|
+
// JSX dialect. Three/Canvas callsites inject the transport props, while
|
|
1612
|
+
// every project component consumes them: a component declared on a DOM
|
|
1613
|
+
// mount can therefore be called safely from a Three-region module without
|
|
1614
|
+
// leaking editor metadata through its own rest props.
|
|
1615
|
+
componentStack.push({ name, ...componentInstanceExpressions(parameters) });
|
|
1616
|
+
return true;
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
/** Local names of drei's `<Html>`, and how deep inside one we currently are —
|
|
1620
|
+
* see {@link dreiHtmlLocalNames}. Only meaningful in the R3F dialect; a
|
|
1621
|
+
* `data-oid` file already stamps what a DOM subtree wants. */
|
|
1622
|
+
const dreiHtmlNames = attribute === 'userData-oid' ? dreiHtmlLocalNames(sf) : new Set<string>();
|
|
1623
|
+
const dreiCameraNames =
|
|
1624
|
+
attribute === 'userData-oid' ? dreiCameraLocalNames(sf) : new Set<string>();
|
|
1625
|
+
/** Tags bound to an installed package — see {@link packageImportedLocalNames}.
|
|
1626
|
+
* Only meaningful in the R3F dialect: `data-oid` is a legal DOM attribute
|
|
1627
|
+
* and a package that spreads it renders it harmlessly. */
|
|
1628
|
+
const packageNames = projectsComponentInstances
|
|
1629
|
+
? packageImportedLocalNames(sf)
|
|
1630
|
+
: new Set<string>();
|
|
1631
|
+
/** Tags bound to a function parameter — see {@link parameterBoundLocalNames}.
|
|
1632
|
+
* Same dialect gating and same reason as `packageNames`: only the transport
|
|
1633
|
+
* props can escape, and `data-oid` is a legal DOM attribute. */
|
|
1634
|
+
const parameterNames = projectsComponentInstances
|
|
1635
|
+
? parameterBoundLocalNames(sf)
|
|
1636
|
+
: new Set<string>();
|
|
1637
|
+
const fragmentNames = reactFragmentNames(sf);
|
|
1638
|
+
let domSubtreeDepth = 0;
|
|
1639
|
+
|
|
1640
|
+
/** Oids of the JSX elements lexically enclosing the node being visited. */
|
|
1641
|
+
const jsxStack: string[] = [];
|
|
1642
|
+
/** Opening tag -> its oid, so the enclosing `JsxElement` can push it before
|
|
1643
|
+
* descending into the children that tag wraps. */
|
|
1644
|
+
const openedOid = new Map<ts.JsxOpeningElement, string>();
|
|
1645
|
+
|
|
1646
|
+
const visit = (node: ts.Node): void => {
|
|
1647
|
+
let pushed = false;
|
|
1648
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
1649
|
+
pushed = pushComponent(node.name.text, node.parameters);
|
|
1650
|
+
else if (
|
|
1651
|
+
ts.isVariableDeclaration(node) &&
|
|
1652
|
+
ts.isIdentifier(node.name) &&
|
|
1653
|
+
node.initializer &&
|
|
1654
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
|
|
1655
|
+
) {
|
|
1656
|
+
pushed = pushComponent(node.name.text, node.initializer.parameters);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1660
|
+
const tag = node.tagName.getText(sf);
|
|
1661
|
+
if (fragmentNames.has(tag)) {
|
|
1662
|
+
ts.forEachChild(node, visit);
|
|
1663
|
+
if (pushed) componentStack.pop();
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const componentContext = componentStack[componentStack.length - 1];
|
|
1667
|
+
const component = componentContext?.name ?? null;
|
|
1668
|
+
const sigBase = `${file}:${component}:${tag}`;
|
|
1669
|
+
const n = occ.get(sigBase) ?? 0;
|
|
1670
|
+
occ.set(sigBase, n + 1);
|
|
1671
|
+
const sig = `${sigBase}:${n}`;
|
|
1672
|
+
const oid = store.getOrCreate(sig);
|
|
1673
|
+
// Inside a drei `<Html>` the reconciler is react-dom, not fiber — see
|
|
1674
|
+
// `dreiHtmlLocalNames`. Everything below reads this, not `attribute`.
|
|
1675
|
+
const elementAttribute = domSubtreeDepth > 0 ? 'data-oid' : attribute;
|
|
1676
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
1677
|
+
const localContract =
|
|
1678
|
+
elementAttribute === 'userData-oid' && /^[A-Z]/.test(tag)
|
|
1679
|
+
? r3fComponentContracts.get(tag)
|
|
1680
|
+
: undefined;
|
|
1681
|
+
const entry: OidEntry = {
|
|
1682
|
+
file,
|
|
1683
|
+
line: line + 1,
|
|
1684
|
+
col: character,
|
|
1685
|
+
component,
|
|
1686
|
+
tag,
|
|
1687
|
+
authoredProps: authoredPropsOf(node, sf),
|
|
1688
|
+
...(localContract ? { r3fAuthoring: localContract } : {}),
|
|
1689
|
+
...(physicsBindings.has(node) ? { physicsBinding: physicsBindings.get(node)! } : {}),
|
|
1690
|
+
...(jointBindings.has(node) ? { jointBindings: jointBindings.get(node)! } : {}),
|
|
1691
|
+
...(particleBindings.has(node) ? { particleBinding: particleBindings.get(node)! } : {}),
|
|
1692
|
+
...(lodBindings.has(node) ? { lodBinding: lodBindings.get(node)! } : {}),
|
|
1693
|
+
...(environmentBindings.has(node)
|
|
1694
|
+
? { environmentBinding: environmentBindings.get(node)! }
|
|
1695
|
+
: {}),
|
|
1696
|
+
...(jsxStack.length > 0 ? { parentOid: jsxStack[jsxStack.length - 1]! } : {}),
|
|
1697
|
+
};
|
|
1698
|
+
store.index.set(oid, entry);
|
|
1699
|
+
entries.push(entry);
|
|
1700
|
+
if (ts.isJsxOpeningElement(node)) openedOid.set(node, oid);
|
|
1701
|
+
const isProjectCustomComponent =
|
|
1702
|
+
(elementAttribute === 'userData-oid' || opts?.canvasComponents === true) &&
|
|
1703
|
+
/^[A-Z]/.test(tag);
|
|
1704
|
+
if (isProjectCustomComponent) {
|
|
1705
|
+
const element = ts.isJsxOpeningElement(node) ? node.parent : node;
|
|
1706
|
+
if (ts.isJsxElement(element) || ts.isJsxSelfClosingElement(element)) {
|
|
1707
|
+
// An installed package's module never passes through this transform,
|
|
1708
|
+
// so the callee half of the `__vgaiOid` contract never runs for it and
|
|
1709
|
+
// the props can only escape (into the DOM, via any package component
|
|
1710
|
+
// that spreads its rest props onto a host element). The callsite is
|
|
1711
|
+
// still INDEXED above — the source entry, its parent and its contract
|
|
1712
|
+
// are unchanged; only the unreadable runtime prop is withheld. See
|
|
1713
|
+
// {@link packageImportedLocalNames}.
|
|
1714
|
+
if (packageNames.has(tag.split('.')[0] ?? tag)) {
|
|
1715
|
+
// Detailed and the two Drei cameras spread their remaining props
|
|
1716
|
+
// onto a native Three host, where the ordinary R3F OID belongs.
|
|
1717
|
+
if (lodBindings.has(node) || dreiCameraNames.has(tag)) {
|
|
1718
|
+
sourceEdits.push({ pos: node.attributes.end, text: ` userData-oid="${oid}"` });
|
|
1719
|
+
}
|
|
1720
|
+
ts.forEachChild(node, visit);
|
|
1721
|
+
if (pushed) componentStack.pop();
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
// A tag bound to a PARAMETER names whatever the caller passed, so the
|
|
1725
|
+
// callee half may never have run for it either — same withholding,
|
|
1726
|
+
// same reason, and the callsite stays indexed. See
|
|
1727
|
+
// {@link parameterBoundLocalNames}.
|
|
1728
|
+
if (parameterNames.has(tag.split('.')[0] ?? tag)) {
|
|
1729
|
+
ts.forEachChild(node, visit);
|
|
1730
|
+
if (pushed) componentStack.pop();
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const authoredName = node.attributes.properties.find(
|
|
1734
|
+
(property): property is ts.JsxAttribute =>
|
|
1735
|
+
ts.isJsxAttribute(property) && property.name.getText(sf) === 'name',
|
|
1736
|
+
);
|
|
1737
|
+
const label =
|
|
1738
|
+
authoredName?.initializer && ts.isStringLiteral(authoredName.initializer)
|
|
1739
|
+
? authoredName.initializer.text
|
|
1740
|
+
: tag;
|
|
1741
|
+
const labelAttribute =
|
|
1742
|
+
authoredName?.initializer && ts.isJsxExpression(authoredName.initializer)
|
|
1743
|
+
? `__vgaiLabel={${authoredName.initializer.expression?.getText(sf) ?? JSON.stringify(tag)}}`
|
|
1744
|
+
: `__vgaiLabel=${JSON.stringify(label)}`;
|
|
1745
|
+
sourceEdits.push({
|
|
1746
|
+
pos: node.attributes.end,
|
|
1747
|
+
text: ` __vgaiOid="${oid}" ${labelAttribute}`,
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
} else {
|
|
1751
|
+
// Host elements carry their identity directly.
|
|
1752
|
+
// Append after authored attributes. In R3F, an authored `userData={{…}}`
|
|
1753
|
+
// replaces the whole object at its position in prop order; putting the
|
|
1754
|
+
// pierced editor keys last prevents that prop from erasing identity.
|
|
1755
|
+
sourceEdits.push({ pos: node.attributes.end, text: ` ${elementAttribute}="${oid}"` });
|
|
1756
|
+
if (elementAttribute === 'userData-oid' && componentContext) {
|
|
1757
|
+
sourceEdits.push({
|
|
1758
|
+
pos: node.attributes.end,
|
|
1759
|
+
text:
|
|
1760
|
+
` userData-authoringInstance={${componentContext.instanceExpression}}` +
|
|
1761
|
+
` userData-authoringLabel={${componentContext.labelExpression}}` +
|
|
1762
|
+
` userData-authoringComponent=${JSON.stringify(componentContext.name)}`,
|
|
1763
|
+
});
|
|
1764
|
+
} else if (opts?.canvasComponents === true && componentContext) {
|
|
1765
|
+
sourceEdits.push({
|
|
1766
|
+
pos: node.attributes.end,
|
|
1767
|
+
text:
|
|
1768
|
+
` data-authoring-instance={${componentContext.instanceExpression}}` +
|
|
1769
|
+
` data-authoring-label={${componentContext.labelExpression}}` +
|
|
1770
|
+
` data-authoring-component=${JSON.stringify(componentContext.name)}`,
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
if (
|
|
1774
|
+
elementAttribute === 'userData-oid' &&
|
|
1775
|
+
// NEVER on `<primitive>`. Its object is the GAME's, constructed before
|
|
1776
|
+
// React ever sees it, and `userData={{}}` REPLACES that object's own
|
|
1777
|
+
// `userData` wholesale — measured: a root carrying
|
|
1778
|
+
// `markComponentRoot(node, 'Stage')` from its constructor came out of
|
|
1779
|
+
// mount with nothing but the three editor keys, so every hierarchy
|
|
1780
|
+
// mark a class-owned root sets was destroyed by the editor's own
|
|
1781
|
+
// serve-time transform. An adopted object always HAS a `userData`
|
|
1782
|
+
// (three sets one on every Object3D/geometry/material), which is
|
|
1783
|
+
// precisely why the initializer below is unnecessary here and
|
|
1784
|
+
// destructive. That initializer's real subjects are the resources R3F
|
|
1785
|
+
// itself constructs.
|
|
1786
|
+
tag !== 'primitive' &&
|
|
1787
|
+
!node.attributes.properties.some(
|
|
1788
|
+
(property) => ts.isJsxAttribute(property) && property.name.getText(sf) === 'userData',
|
|
1789
|
+
)
|
|
1790
|
+
) {
|
|
1791
|
+
// Some valid R3F host resources (for example BufferAttribute) do not
|
|
1792
|
+
// start with a `userData` object. Current Fiber deliberately refuses
|
|
1793
|
+
// to pierce `userData-*` through an undefined parent, so initialize
|
|
1794
|
+
// it before the editor metadata props are applied.
|
|
1795
|
+
sourceEdits.push({ pos: node.attributes.end, text: ' userData={{}}' });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// A `<Tag>…</Tag>` pair encloses its children, but in the AST the opening
|
|
1801
|
+
// tag is their SIBLING — so walk it FIRST (which mints its oid), then push
|
|
1802
|
+
// that oid while descending into the children it wraps. Doing this in the
|
|
1803
|
+
// generic `forEachChild` tail would read the oid before it exists.
|
|
1804
|
+
if (ts.isJsxElement(node)) {
|
|
1805
|
+
visit(node.openingElement);
|
|
1806
|
+
const enclosingOid = openedOid.get(node.openingElement);
|
|
1807
|
+
if (enclosingOid !== undefined) jsxStack.push(enclosingOid);
|
|
1808
|
+
// `<Html>` ITSELF stays an R3F callsite (drei mounts a `<group>` for it);
|
|
1809
|
+
// only what it wraps is DOM.
|
|
1810
|
+
const opensDomSubtree = dreiHtmlNames.has(node.openingElement.tagName.getText(sf));
|
|
1811
|
+
if (opensDomSubtree) domSubtreeDepth++;
|
|
1812
|
+
for (const child of node.children) visit(child);
|
|
1813
|
+
if (opensDomSubtree) domSubtreeDepth--;
|
|
1814
|
+
if (enclosingOid !== undefined) jsxStack.pop();
|
|
1815
|
+
visit(node.closingElement);
|
|
1816
|
+
if (pushed) componentStack.pop();
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
ts.forEachChild(node, visit);
|
|
1821
|
+
if (pushed) componentStack.pop();
|
|
1822
|
+
};
|
|
1823
|
+
visit(sf);
|
|
1824
|
+
|
|
1825
|
+
// Apply edits back-to-front so every offset stays relative to source bytes.
|
|
1826
|
+
sourceEdits.sort((a, b) => b.pos - a.pos);
|
|
1827
|
+
let out = code;
|
|
1828
|
+
for (const edit of sourceEdits) {
|
|
1829
|
+
out = out.slice(0, edit.pos) + edit.text + out.slice(edit.end ?? edit.pos);
|
|
1830
|
+
}
|
|
1831
|
+
return { code: out, entries };
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/** 1-based line + 0-based col -> absolute char offset in `code`. */
|
|
1835
|
+
export function lineColToOffset(code: string, line: number, col: number): number {
|
|
1836
|
+
let offset = 0;
|
|
1837
|
+
let cur = 1;
|
|
1838
|
+
while (cur < line) {
|
|
1839
|
+
const nl = code.indexOf('\n', offset);
|
|
1840
|
+
if (nl === -1) return code.length;
|
|
1841
|
+
offset = nl + 1;
|
|
1842
|
+
cur += 1;
|
|
1843
|
+
}
|
|
1844
|
+
return offset + col;
|
|
1845
|
+
}
|