@volter/editor-react 0.5.210 → 0.5.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contributions/component-contracts.service.ts +1 -1
- package/dist-node/serving.mjs +14770 -1629
- package/package.json +20 -9
- package/serving/component-prop-worker.ts +3 -19
- package/serving/index.ts +20 -8
- package/serving/source-diagnostics.ts +311 -0
- package/serving/ui-oid-plugin.ts +154 -775
- package/src/source/component-prop-types.ts +6 -62
- package/src/source/drei-detailed-names.ts +24 -0
- package/src/source/oid-transform.ts +166 -376
- package/src/source/plan-csf-story.ts +3 -26
- package/src/source/plan-extract-component.ts +15 -60
- package/src/source/plan-fork-component.ts +25 -92
- package/src/source/plan-named-style.ts +3 -17
- package/src/source/plan-source-edit.ts +9 -40
- package/src/source/r3f-contract-resolver.ts +9 -103
- package/src/source/r3f-physics-binding.ts +28 -186
- package/src/source/r3f-project-contracts.ts +13 -103
- package/src/source/relative-import-specifier.ts +3 -18
- package/src/source/reparent-guard.ts +16 -61
- package/src/source/source-edit-request.ts +2 -20
- package/src/source/ts-ast.ts +5 -95
- package/src/source/utility-class-support.ts +4 -32
- package/src/source/writer.ts +74 -326
- package/src/source/r3f-diagnostic-index.ts +0 -165
- package/src/source/r3f-environment-binding.ts +0 -106
- package/src/source/r3f-joint-binding.ts +0 -228
- package/src/source/r3f-lod-binding.ts +0 -81
- package/src/source/r3f-particle-binding.ts +0 -213
- package/src/source/syntactic-prop-specs.ts +0 -296
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/editor-react",
|
|
3
|
+
"publisher": "volter",
|
|
3
4
|
"author": "Volter AI, Inc.",
|
|
4
5
|
"license": "AGPL-3.0-only",
|
|
5
|
-
"version": "0.5.
|
|
6
|
+
"version": "0.5.212",
|
|
7
|
+
"engines": {
|
|
8
|
+
"vscode": "*"
|
|
9
|
+
},
|
|
6
10
|
"publishConfig": {
|
|
7
11
|
"access": "public"
|
|
8
12
|
},
|
|
@@ -31,19 +35,26 @@
|
|
|
31
35
|
"prepack": "npm run build",
|
|
32
36
|
"typecheck": "tsc -p tsconfig.json"
|
|
33
37
|
},
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
"contributes": {
|
|
39
|
+
"volter": {
|
|
40
|
+
"serving": [
|
|
41
|
+
"./dist-node/serving.mjs"
|
|
42
|
+
],
|
|
43
|
+
"services": [
|
|
44
|
+
{
|
|
45
|
+
"module": "./contributions/component-contracts.service.ts",
|
|
46
|
+
"sourceAnalyzer": "component-contracts"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
39
50
|
},
|
|
40
51
|
"dependencies": {
|
|
41
|
-
"@volter/sdk": "0.5.
|
|
42
|
-
"@volter/project": "0.5.
|
|
52
|
+
"@volter/sdk": "0.5.212",
|
|
53
|
+
"@volter/project": "0.5.212",
|
|
43
54
|
"typescript": "5.9.3"
|
|
44
55
|
},
|
|
45
56
|
"peerDependencies": {
|
|
46
|
-
"vite": "^6.4.3"
|
|
57
|
+
"vite": "^6.4.3 || ^7.0.0 || ^8.0.0"
|
|
47
58
|
},
|
|
48
59
|
"devDependencies": {
|
|
49
60
|
"esbuild": "0.25.12"
|
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `ComponentPropResolver` builds a full ts.Program on its first query. That is
|
|
5
|
-
* deliberately real TypeScript semantic analysis, but it is CPU-bound and
|
|
6
|
-
* takes seconds on a cold project. Running it from `setImmediate` does not
|
|
7
|
-
* make it asynchronous: it still freezes the one Node event loop serving the
|
|
8
|
-
* editor. This worker keeps one incremental resolver per project and returns
|
|
9
|
-
* only the ecosystem-native prop descriptions the OID index already carries.
|
|
10
|
-
*
|
|
11
|
-
* The `.ts` extensions below are LOAD-BEARING, not a style slip. `new
|
|
12
|
-
* Worker(new URL('./server/component-prop-worker.ts', …))` hands this file to
|
|
13
|
-
* Node directly: tsx's resolver hooks do not reach a worker thread, so the
|
|
14
|
-
* module is executed by Node's own type stripping, whose ESM resolver demands
|
|
15
|
-
* an explicit extension. Dropping them resolves under tsc and then dies at
|
|
16
|
-
* runtime with ERR_MODULE_NOT_FOUND the first time the editor asks for
|
|
17
|
-
* declared props. `tsconfig.server.json` sets `allowImportingTsExtensions`
|
|
18
|
-
* for exactly this reason.
|
|
19
|
-
*/
|
|
1
|
+
/** Resolve declared TypeScript props away from the Vite HTTP event loop.
|
|
2
|
+
* Keep one incremental ComponentPropResolver per project and return specs for
|
|
3
|
+
* each requested definition or callsite. */
|
|
20
4
|
import { parentPort } from 'node:worker_threads';
|
|
21
5
|
import { ComponentPropResolver } from '../src/source/component-prop-types';
|
|
22
6
|
import type { ComponentPropSpec } from '@volter/sdk/source-authoring';
|
package/serving/index.ts
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@volter/editor-react`'s server half (`package.json#volter.serving`, the project-serving door in
|
|
2
|
+
* `@volter/editor-react`'s server half (`package.json#contributes.volter.serving`, the project-serving door in
|
|
3
3
|
* `@volter/sdk/session/project-serving`): the JSX identity stamp and the `/__ui-source/*`
|
|
4
4
|
* authoring routes over a project's React and React Three Fiber source, and the source answers
|
|
5
|
-
* the kit's region decision, validation and Content index ask of a JSX lane.
|
|
5
|
+
* the kit's region decision, validation and Content index ask of a JSX lane. It registers the `dom`
|
|
6
|
+
* substrate, the `dom` surface's default row.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { ProjectServingModule } from '@volter/sdk/session/project-serving';
|
|
9
|
-
import { analyzeR3fComponentContracts, builtinR3fContractsForSource
|
|
10
|
-
import {
|
|
10
|
+
import { analyzeR3fComponentContracts, builtinR3fContractsForSource } from '../src/source/oid-transform';
|
|
11
|
+
import { r3fSourceDiagnostics } from './source-diagnostics';
|
|
11
12
|
import { uiOidPlugin } from './ui-oid-plugin';
|
|
12
13
|
|
|
13
14
|
export const servingPlugins: ProjectServingModule['servingPlugins'] = (services) => [uiOidPlugin(services)];
|
|
14
15
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
export const sourceDiagnostics: ProjectServingModule['sourceDiagnostics'] = (code, file, options) =>
|
|
18
|
-
r3fAuthoringDiagnostics(code, file, options);
|
|
16
|
+
export const sourceDiagnostics: ProjectServingModule['sourceDiagnostics'] = (code, file, decision) =>
|
|
17
|
+
r3fSourceDiagnostics(code, file, decision);
|
|
19
18
|
|
|
20
19
|
export const componentContracts: ProjectServingModule['componentContracts'] = (source, path) =>
|
|
21
20
|
analyzeR3fComponentContracts(source, path, builtinR3fContractsForSource(source, path));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The `dom` substrate: the `dom` surface's default row, authored in the `jsx` dialect through the
|
|
24
|
+
* source-prop and structural anchors.
|
|
25
|
+
*/
|
|
26
|
+
export const substrates: ProjectServingModule['substrates'] = [
|
|
27
|
+
{
|
|
28
|
+
substrate: 'dom',
|
|
29
|
+
surface: 'dom',
|
|
30
|
+
dialects: ['jsx'],
|
|
31
|
+
default: { dialect: 'jsx', anchors: ['source-prop', 'source-structure'] },
|
|
32
|
+
},
|
|
33
|
+
];
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authoring diagnostics this integration answers through the serving door's `sourceDiagnostics`
|
|
3
|
+
* (`@volter/sdk/session/project-serving`): what a module's own source says against the kit's region
|
|
4
|
+
* decision for it. The kit decides which region owns a file and stamps from that alone; these diagnostics
|
|
5
|
+
* read the file's dialect evidence (`sourceDialectEvidence`) and say where it disagrees with the decision,
|
|
6
|
+
* at the volume its consequence deserves.
|
|
7
|
+
*
|
|
8
|
+
* - `OID001`: the file is inside the project, but no region reaches it and none declares it, so its
|
|
9
|
+
* attribute is the `data-oid` default. Graded by what its own host elements are.
|
|
10
|
+
* - `OID003`: the file's rendered HOST ELEMENTS contradict the region that owns it. The region still
|
|
11
|
+
* decides the attribute, and the disagreement is said out loud so the author can fix the declaration
|
|
12
|
+
* (or the file).
|
|
13
|
+
* - `OID004`: the file renders BOTH R3F-only and DOM-only intrinsics. A per-FILE attribute cannot
|
|
14
|
+
* serve both dialects, so no answer is correct; this is reported, never guessed around.
|
|
15
|
+
* - `R3F002`-`R3F005`: the R3F authoring conventions (`r3f-project-contracts.ts`), for a module the kit
|
|
16
|
+
* validates on save that a region places on `stage`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { AuthoringDiagnostic } from '@volter/sdk/source-authoring';
|
|
20
|
+
import type { SourceRegionDecision } from '@volter/sdk/session/project-serving';
|
|
21
|
+
import { ADAPTER_MODULE_FILENAME } from '@volter/project/adapter/adapter-module';
|
|
22
|
+
import { type SourceDialectEvidence, sourceDialectEvidence, sourceProvesR3f } from '../src/source/oid-transform';
|
|
23
|
+
import { r3fAuthoringDiagnostics } from '../src/source/r3f-project-contracts';
|
|
24
|
+
|
|
25
|
+
/** The label the kit prints these diagnostics under. */
|
|
26
|
+
const SOURCE = 'R3F authoring';
|
|
27
|
+
|
|
28
|
+
interface StampDiagnostic {
|
|
29
|
+
readonly code: string;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Portable CSF is a design-time document. A component-only story commonly
|
|
34
|
+
* renders `<Story />` or a provider decorator and is reached from no runtime
|
|
35
|
+
* root, but importing it from one or giving it a manifest root would be exactly
|
|
36
|
+
* the wrong repair. The preview lane qualifies its rendered output
|
|
37
|
+
* independently, while selectable nodes retain the OIDs stamped in the
|
|
38
|
+
* prefab/component source they came from. */
|
|
39
|
+
function isPortableStoryModule(file: string): boolean {
|
|
40
|
+
return /\.stories\.[jt]sx?$/i.test(file);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function evidenceSummary(evidence: SourceDialectEvidence): string {
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
if (evidence.reconcilerImport) parts.push('imports the R3F reconciler');
|
|
46
|
+
if (evidence.r3fOnlyTags.length > 0) {
|
|
47
|
+
parts.push(`renders R3F-only <${evidence.r3fOnlyTags.slice(0, 4).join('>, <')}>`);
|
|
48
|
+
}
|
|
49
|
+
if (evidence.domOnlyTags.length > 0) {
|
|
50
|
+
parts.push(`renders DOM-only <${evidence.domOnlyTags.slice(0, 4).join('>, <')}>`);
|
|
51
|
+
}
|
|
52
|
+
return parts.length > 0 ? parts.join('; ') : 'carries no dialect evidence of its own';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True for DOM-only host output. These files need no missing-region
|
|
56
|
+
* warning because the default data-oid stamp is correct. */
|
|
57
|
+
function rendersOnlyDomHosts(evidence: SourceDialectEvidence): boolean {
|
|
58
|
+
return evidence.domOnlyTags.length > 0 && evidence.r3fOnlyTags.length === 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True for R3F-only host output. A DOM stamp would pierce a nonexistent
|
|
62
|
+
* object.data field, so diagnostics require the owning stage declaration. */
|
|
63
|
+
function rendersOnlyR3fHosts(evidence: SourceDialectEvidence): boolean {
|
|
64
|
+
return evidence.r3fOnlyTags.length > 0 && evidence.domOnlyTags.length === 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* THE FATAL DIRECTION of declared-vs-measured drift: a `dom`/`canvas` region
|
|
69
|
+
* owning a file whose host elements are THREE objects — see
|
|
70
|
+
* {@link rendersOnlyR3fHosts} for what `data-oid` does to one of those.
|
|
71
|
+
*
|
|
72
|
+
* Reported, and the region still stamps: which region owns a file is the project's
|
|
73
|
+
* to state, and the include glob is where it states it. `why` names how the
|
|
74
|
+
* non-`stage` answer came to be believed, so the diagnostic reads as one
|
|
75
|
+
* sentence.
|
|
76
|
+
*/
|
|
77
|
+
function r3fHostDriftDiagnostic(
|
|
78
|
+
absFile: string,
|
|
79
|
+
why: string,
|
|
80
|
+
evidence: SourceDialectEvidence,
|
|
81
|
+
rootsNote: string,
|
|
82
|
+
): StampDiagnostic {
|
|
83
|
+
return {
|
|
84
|
+
code: 'OID003',
|
|
85
|
+
message:
|
|
86
|
+
`${absFile} ${why}, but the file itself ${evidenceSummary(evidence)} and renders no DOM ` +
|
|
87
|
+
'element at all — its host elements are THREE objects, and `data-oid` on one of those is ' +
|
|
88
|
+
'not a degraded stamp: react-three-fiber pierces the dash, writes `object.data` on the ' +
|
|
89
|
+
'first apply and THROWS on the next one (`R3F: Cannot set "data-oid"`), so this file will ' +
|
|
90
|
+
'break the running project, not merely fail to select. If it really is part of a ' +
|
|
91
|
+
`\`dom\`/\`canvas\` region, move its R3F elements out of it; otherwise give it a \`stage\` ` +
|
|
92
|
+
`root of its own, or declare it onto the \`stage\` surface with an \`include\` glob (or a ` +
|
|
93
|
+
`\`mounts\` entry, if this root mounts \`stage\` beside another surface) in ` +
|
|
94
|
+
`${ADAPTER_MODULE_FILENAME}. Roots considered: ${rootsNote}.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** How the diagnostic names the way a `stage` surface came to be believed. */
|
|
99
|
+
function whyOf(region: SourceRegionDecision['region']): string {
|
|
100
|
+
if (region.via === 'declared-include') {
|
|
101
|
+
return `is declared onto the \`${region.regionId}\` region's \`stage\` surface in ${ADAPTER_MODULE_FILENAME}`;
|
|
102
|
+
}
|
|
103
|
+
if (region.via === 'root-entry')
|
|
104
|
+
return 'is declared as a `stage` root entry in volter.project.json';
|
|
105
|
+
return 'is reached only from a `stage` root';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** OID001's extra clause about what the defaulted file's own bytes say — the
|
|
109
|
+
* consequence, graded. A file whose HOST elements are THREE objects gets the
|
|
110
|
+
* `data-oid` default and will THROW inside fiber on the second apply (see
|
|
111
|
+
* {@link rendersOnlyR3fHosts}), so its clause says so; a file that merely
|
|
112
|
+
* IMPORTS the reconciler has nothing for a wrong stamp to land on and only
|
|
113
|
+
* fails to select. */
|
|
114
|
+
function unresolvableClause(evidence: SourceDialectEvidence): string {
|
|
115
|
+
if (rendersOnlyR3fHosts(evidence)) {
|
|
116
|
+
return (
|
|
117
|
+
` — and this file ${evidenceSummary(evidence)}, so the \`data-oid\` default lands on THREE ` +
|
|
118
|
+
'objects: react-three-fiber pierces the dash, writes `object.data` on the first apply and ' +
|
|
119
|
+
'THROWS on the next one. Declaring this file is not optional cleanup — the running project ' +
|
|
120
|
+
'breaks without it'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return sourceProvesR3f(evidence)
|
|
124
|
+
? ` — and this file ${evidenceSummary(evidence)}, so its selection will NOT resolve`
|
|
125
|
+
: '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** What the file's own source says against the region decision for it. */
|
|
129
|
+
function stampDiagnostics(
|
|
130
|
+
absFile: string,
|
|
131
|
+
evidence: SourceDialectEvidence,
|
|
132
|
+
decision: SourceRegionDecision,
|
|
133
|
+
): StampDiagnostic[] {
|
|
134
|
+
const { region, project } = decision;
|
|
135
|
+
const roots = project?.roots ?? '(none)';
|
|
136
|
+
const diagnostics: StampDiagnostic[] = [];
|
|
137
|
+
|
|
138
|
+
if (evidence.r3fOnlyTags.length > 0 && evidence.domOnlyTags.length > 0) {
|
|
139
|
+
diagnostics.push({
|
|
140
|
+
code: 'OID004',
|
|
141
|
+
message:
|
|
142
|
+
`${absFile} renders BOTH R3F-only (<${evidence.r3fOnlyTags.slice(0, 3).join('>, <')}>) ` +
|
|
143
|
+
`and DOM-only (<${evidence.domOnlyTags.slice(0, 3).join('>, <')}>) elements. The editor ` +
|
|
144
|
+
'stamps ONE source-id attribute per file (`userData-oid` for the R3F reconciler, ' +
|
|
145
|
+
'`data-oid` for react-dom), so selection can only resolve for one of the two dialects ' +
|
|
146
|
+
'in this file. Split the DOM part (e.g. the contents of a drei <Html>) into its own ' +
|
|
147
|
+
'module to make both halves selectable.',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (region.surface !== undefined) {
|
|
152
|
+
// DRIFT, REPORTED AND NOT ACTED ON: a `stage` region owning a file whose
|
|
153
|
+
// HOST ELEMENTS are DOM elements. The region still stamps — it is the
|
|
154
|
+
// project's own statement about which surface this file renders on, and a
|
|
155
|
+
// stamp derived from the file's bytes instead would make the declaration
|
|
156
|
+
// untestable, and a wrong declaration would stay invisible. What
|
|
157
|
+
// the author sees is the disagreement itself, plus the two declarations
|
|
158
|
+
// that resolve it.
|
|
159
|
+
if (region.surface === 'stage' && rendersOnlyDomHosts(evidence)) {
|
|
160
|
+
diagnostics.push({
|
|
161
|
+
code: 'OID003',
|
|
162
|
+
message:
|
|
163
|
+
`${absFile} ${whyOf(region)}, but the file itself ${evidenceSummary(evidence)} and ` +
|
|
164
|
+
'renders no R3F element at all — its host elements are DOM elements, so the ' +
|
|
165
|
+
'`userData-oid` stamped from that region is an unrecognized React prop on every one of ' +
|
|
166
|
+
'them and selection resolves for neither dialect. This is the normal shape for ' +
|
|
167
|
+
'HUD/overlay modules that import `@react-three/fiber` only for `addEffect`/`useFrame`: ' +
|
|
168
|
+
`declare them onto this root's \`dom\` surface with a \`mounts\` entry in ` +
|
|
169
|
+
`${ADAPTER_MODULE_FILENAME}, or give them a \`dom\` root of their own in ` +
|
|
170
|
+
'volter.project.json. If this file really does author R3F content, render an R3F element ' +
|
|
171
|
+
`in it. Roots considered: ${roots}.`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// DRIFT, REPORTED AND NOT ACTED ON — the FATAL direction: a non-`stage`
|
|
175
|
+
// region owning a file whose HOST ELEMENTS are THREE objects. The region
|
|
176
|
+
// still stamps, for the same reason it does above. What differs is the
|
|
177
|
+
// consequence, and the diagnostic says it (`r3fHostDriftDiagnostic`).
|
|
178
|
+
if (region.surface !== 'stage' && rendersOnlyR3fHosts(evidence)) {
|
|
179
|
+
diagnostics.push(
|
|
180
|
+
r3fHostDriftDiagnostic(
|
|
181
|
+
absFile,
|
|
182
|
+
`belongs to a \`${region.surface}\` region (${region.via})`,
|
|
183
|
+
evidence,
|
|
184
|
+
roots,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// DRIFT, REPORTED AND NOT ACTED ON: a non-`stage` region reaching a file
|
|
189
|
+
// that IMPORTS the R3F reconciler but renders no host element of either
|
|
190
|
+
// dialect. Import reach is not render reach — a `dom` root CAN render an
|
|
191
|
+
// R3F `<Canvas>`, and its children are reached through the graph — so this
|
|
192
|
+
// is a real, nameable disagreement between the declaration and the source,
|
|
193
|
+
// with nothing in the file for a wrong stamp to land on.
|
|
194
|
+
if (
|
|
195
|
+
region.surface !== 'stage' &&
|
|
196
|
+
evidence.reconcilerImport &&
|
|
197
|
+
evidence.r3fOnlyTags.length === 0 &&
|
|
198
|
+
evidence.domOnlyTags.length === 0
|
|
199
|
+
) {
|
|
200
|
+
diagnostics.push({
|
|
201
|
+
code: 'OID003',
|
|
202
|
+
message:
|
|
203
|
+
`${absFile} belongs to a \`${region.surface}\` region (${region.via}), but the file ` +
|
|
204
|
+
`itself ${evidenceSummary(evidence)} — import reach is not render reach (a ` +
|
|
205
|
+
`\`${region.surface}\` root can render an R3F <Canvas>). \`data-oid\` is stamped from ` +
|
|
206
|
+
'the region, so any R3F element here will not resolve to a selection. Give this file a ' +
|
|
207
|
+
`\`stage\` root of its own, or declare it onto the \`stage\` surface with an \`include\` ` +
|
|
208
|
+
`glob (or a \`mounts\` entry) in ${ADAPTER_MODULE_FILENAME}. Roots considered: ${roots}.`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return diagnostics;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
region.ambiguity !== 'differing-surfaces' &&
|
|
216
|
+
project &&
|
|
217
|
+
// "No region reaches it" is only a FINDING at a stamp, where the served
|
|
218
|
+
// module graph was consulted; at a save it is just ignorance, and claiming
|
|
219
|
+
// it would warn about every non-entry file in the project.
|
|
220
|
+
decision.at === 'stamp' &&
|
|
221
|
+
// The noise gate — see `rendersOnlyDomHosts`: an unplaced file whose host
|
|
222
|
+
// elements are DOM already carries the attribute it needs, because the
|
|
223
|
+
// default IS `data-oid`.
|
|
224
|
+
!rendersOnlyDomHosts(evidence) &&
|
|
225
|
+
// The story exemption is a NOISE gate too, and it stops at the fatal case:
|
|
226
|
+
// an unplaced story whose own host elements are THREE objects takes the
|
|
227
|
+
// `data-oid` default onto them and throws inside fiber on the second mount
|
|
228
|
+
// (see `rendersOnlyR3fHosts`). Silence about a crash is not noise reduction.
|
|
229
|
+
(!isPortableStoryModule(absFile) || rendersOnlyR3fHosts(evidence))
|
|
230
|
+
) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
code: 'OID001',
|
|
233
|
+
message:
|
|
234
|
+
`${absFile} is inside the project at ${project.manifest} but ` +
|
|
235
|
+
'no region reaches it and none declares it, so the source-id attribute it carries is a ' +
|
|
236
|
+
`DEFAULT, not a decision${unresolvableClause(evidence)}. Import it from a region entry (directly or ` +
|
|
237
|
+
'transitively), give it a root, or declare it via a region `include` glob in ' +
|
|
238
|
+
`${ADAPTER_MODULE_FILENAME}. Roots considered: ${project.roots}.`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return diagnostics;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The validation-time leftover the stamp diagnostics do not cover: a root entry
|
|
246
|
+
* that IMPORTS the reconciler while rendering no host element of EITHER
|
|
247
|
+
* dialect. A `dom`/`canvas` entry that imports fiber for `addEffect`/`useFrame`
|
|
248
|
+
* and renders DOM elements has no R3F element to fail to resolve; one that
|
|
249
|
+
* renders THREE objects is the fatal-direction drift, already reported.
|
|
250
|
+
*/
|
|
251
|
+
function entryConflict(
|
|
252
|
+
absFile: string,
|
|
253
|
+
evidence: SourceDialectEvidence,
|
|
254
|
+
decision: SourceRegionDecision,
|
|
255
|
+
): StampDiagnostic | null {
|
|
256
|
+
const direct = decision.project?.entrySurface;
|
|
257
|
+
if (
|
|
258
|
+
direct === undefined ||
|
|
259
|
+
direct === 'stage' ||
|
|
260
|
+
!evidence.reconcilerImport ||
|
|
261
|
+
evidence.r3fOnlyTags.length > 0 ||
|
|
262
|
+
evidence.domOnlyTags.length > 0
|
|
263
|
+
) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
code: 'OID003',
|
|
268
|
+
message:
|
|
269
|
+
`${absFile} is declared as a \`${direct}\` root entry in volter.project.json, but the file ` +
|
|
270
|
+
`${evidenceSummary(evidence)}. The manifest wins (\`data-oid\` is stamped), so any R3F ` +
|
|
271
|
+
'element here will not resolve to a selection. Declare a `stage` root for the R3F part.',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The door's `sourceDiagnostics`. At a module's stamp (`decision.at === 'stamp'`), the
|
|
277
|
+
* diagnostics about the stamp. When the kit validates a saved module (`'save'`),
|
|
278
|
+
* the R3F authoring conventions for a module a region places on `stage`, then
|
|
279
|
+
* the stamp diagnostics the file's bytes and the manifest settle between them:
|
|
280
|
+
* `OID004` and `OID003`, plus the root-entry leftover.
|
|
281
|
+
*/
|
|
282
|
+
export function r3fSourceDiagnostics(
|
|
283
|
+
code: string,
|
|
284
|
+
file: string,
|
|
285
|
+
decision: SourceRegionDecision,
|
|
286
|
+
): AuthoringDiagnostic[] {
|
|
287
|
+
const evidence = sourceDialectEvidence(code);
|
|
288
|
+
const labelled = (diagnostic: StampDiagnostic): AuthoringDiagnostic => ({
|
|
289
|
+
code: diagnostic.code,
|
|
290
|
+
message: diagnostic.message,
|
|
291
|
+
source: SOURCE,
|
|
292
|
+
});
|
|
293
|
+
if (decision.at === 'stamp') return stampDiagnostics(file, evidence, decision).map(labelled);
|
|
294
|
+
const conventions = r3fAuthoringDiagnostics(code, file, {
|
|
295
|
+
knownStageSurface: decision.region.surface === 'stage',
|
|
296
|
+
}).map(
|
|
297
|
+
(diagnostic): AuthoringDiagnostic => ({
|
|
298
|
+
code: diagnostic.code,
|
|
299
|
+
message: diagnostic.message,
|
|
300
|
+
line: diagnostic.line,
|
|
301
|
+
col: diagnostic.col,
|
|
302
|
+
source: SOURCE,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
const conflict = entryConflict(file, evidence, decision);
|
|
306
|
+
return [
|
|
307
|
+
...conventions,
|
|
308
|
+
...stampDiagnostics(file, evidence, decision).map(labelled),
|
|
309
|
+
...(conflict ? [labelled(conflict)] : []),
|
|
310
|
+
];
|
|
311
|
+
}
|