@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
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/** Source address for a native three.quarks `ParticleSystem` emitter shape. */
|
|
2
|
-
|
|
3
|
-
import ts from 'typescript';
|
|
4
|
-
import { enclosingScope, numericLiteral } from './ts-ast';
|
|
5
|
-
|
|
6
|
-
import type { QuarksEmitterShape, R3fParticleBinding, R3fParticleNumberBinding } from '@volter/sdk/source-authoring';
|
|
7
|
-
export type { QuarksEmitterShape, R3fParticleBinding, R3fParticleNumberBinding } from '@volter/sdk/source-authoring';
|
|
8
|
-
|
|
9
|
-
const SHAPES: Record<string, QuarksEmitterShape> = {
|
|
10
|
-
PointEmitter: 'point',
|
|
11
|
-
SphereEmitter: 'sphere',
|
|
12
|
-
HemisphereEmitter: 'hemisphere',
|
|
13
|
-
ConeEmitter: 'cone',
|
|
14
|
-
CircleEmitter: 'circle',
|
|
15
|
-
DonutEmitter: 'donut',
|
|
16
|
-
RectangleEmitter: 'rectangle',
|
|
17
|
-
GridEmitter: 'grid',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
interface Imports {
|
|
21
|
-
readonly particleSystems: Set<string>;
|
|
22
|
-
readonly shapes: Map<string, { imported: string; shape: QuarksEmitterShape }>;
|
|
23
|
-
readonly useMemo: Set<string>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function namedImports(statement: ts.Statement, module: string): readonly ts.ImportSpecifier[] {
|
|
27
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
if (statement.moduleSpecifier.text !== module) return [];
|
|
31
|
-
const bindings = statement.importClause?.namedBindings;
|
|
32
|
-
return bindings && ts.isNamedImports(bindings) ? bindings.elements : [];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function importsOf(sf: ts.SourceFile): Imports {
|
|
36
|
-
const particleSystems = new Set<string>();
|
|
37
|
-
const shapes = new Map<string, { imported: string; shape: QuarksEmitterShape }>();
|
|
38
|
-
const useMemo = new Set<string>();
|
|
39
|
-
for (const statement of sf.statements) {
|
|
40
|
-
for (const element of namedImports(statement, 'three.quarks')) {
|
|
41
|
-
const imported = element.propertyName?.text ?? element.name.text;
|
|
42
|
-
if (imported === 'ParticleSystem') particleSystems.add(element.name.text);
|
|
43
|
-
const shape = SHAPES[imported];
|
|
44
|
-
if (shape) shapes.set(element.name.text, { imported, shape });
|
|
45
|
-
}
|
|
46
|
-
for (const element of namedImports(statement, 'quarks.core')) {
|
|
47
|
-
const imported = element.propertyName?.text ?? element.name.text;
|
|
48
|
-
const shape = SHAPES[imported];
|
|
49
|
-
if (shape) shapes.set(element.name.text, { imported, shape });
|
|
50
|
-
}
|
|
51
|
-
for (const element of namedImports(statement, 'react')) {
|
|
52
|
-
const imported = element.propertyName?.text ?? element.name.text;
|
|
53
|
-
if (imported === 'useMemo') useMemo.add(element.name.text);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return { particleSystems, shapes, useMemo };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function returnedExpression(
|
|
60
|
-
fn: ts.ArrowFunction | ts.FunctionExpression,
|
|
61
|
-
): ts.Expression | undefined {
|
|
62
|
-
if (!ts.isBlock(fn.body)) return fn.body;
|
|
63
|
-
return fn.body.statements.find(ts.isReturnStatement)?.expression;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function particleSystemExpression(
|
|
67
|
-
expression: ts.Expression,
|
|
68
|
-
imported: Imports,
|
|
69
|
-
): ts.NewExpression | undefined {
|
|
70
|
-
if (
|
|
71
|
-
ts.isNewExpression(expression) &&
|
|
72
|
-
ts.isIdentifier(expression.expression) &&
|
|
73
|
-
imported.particleSystems.has(expression.expression.text)
|
|
74
|
-
) {
|
|
75
|
-
return expression;
|
|
76
|
-
}
|
|
77
|
-
if (
|
|
78
|
-
!ts.isCallExpression(expression) ||
|
|
79
|
-
!ts.isIdentifier(expression.expression) ||
|
|
80
|
-
!imported.useMemo.has(expression.expression.text)
|
|
81
|
-
) {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
const factory = expression.arguments[0];
|
|
85
|
-
if (!factory || (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory)))
|
|
86
|
-
return undefined;
|
|
87
|
-
const returned = returnedExpression(factory);
|
|
88
|
-
return returned ? particleSystemExpression(returned, imported) : undefined;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function propertyName(name: ts.PropertyName, sf: ts.SourceFile): string {
|
|
92
|
-
return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)
|
|
93
|
-
? name.text
|
|
94
|
-
: name.getText(sf);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function property(
|
|
98
|
-
object: ts.ObjectLiteralExpression,
|
|
99
|
-
name: string,
|
|
100
|
-
sf: ts.SourceFile,
|
|
101
|
-
): ts.PropertyAssignment | undefined {
|
|
102
|
-
return object.properties.find(
|
|
103
|
-
(candidate): candidate is ts.PropertyAssignment =>
|
|
104
|
-
ts.isPropertyAssignment(candidate) && propertyName(candidate.name, sf) === name,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function bindingOf(
|
|
109
|
-
system: string,
|
|
110
|
-
declaration: ts.VariableDeclaration,
|
|
111
|
-
sf: ts.SourceFile,
|
|
112
|
-
imported: Imports,
|
|
113
|
-
): R3fParticleBinding | undefined {
|
|
114
|
-
const initializer = declaration.initializer;
|
|
115
|
-
if (!initializer) return undefined;
|
|
116
|
-
const construction = particleSystemExpression(initializer, imported);
|
|
117
|
-
const options = construction?.arguments?.[0];
|
|
118
|
-
if (!options || !ts.isObjectLiteralExpression(options)) return undefined;
|
|
119
|
-
const shapeProperty = property(options, 'shape', sf);
|
|
120
|
-
const shapeExpression = shapeProperty?.initializer;
|
|
121
|
-
if (
|
|
122
|
-
!shapeExpression ||
|
|
123
|
-
!ts.isNewExpression(shapeExpression) ||
|
|
124
|
-
!ts.isIdentifier(shapeExpression.expression)
|
|
125
|
-
) {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
const shape = imported.shapes.get(shapeExpression.expression.text);
|
|
129
|
-
if (!shape) return undefined;
|
|
130
|
-
const parameters = shapeExpression.arguments?.[0];
|
|
131
|
-
const fields: Record<string, R3fParticleNumberBinding> = {};
|
|
132
|
-
if (parameters && ts.isObjectLiteralExpression(parameters)) {
|
|
133
|
-
for (const candidate of parameters.properties) {
|
|
134
|
-
if (!ts.isPropertyAssignment(candidate)) continue;
|
|
135
|
-
const value = numericLiteral(candidate.initializer);
|
|
136
|
-
if (value === undefined) continue;
|
|
137
|
-
fields[propertyName(candidate.name, sf)] = {
|
|
138
|
-
value,
|
|
139
|
-
start: candidate.initializer.getStart(sf),
|
|
140
|
-
end: candidate.initializer.getEnd(),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const { line, character } = sf.getLineAndCharacterOfPosition(shapeExpression.getStart(sf));
|
|
145
|
-
return {
|
|
146
|
-
system,
|
|
147
|
-
shape: shape.shape,
|
|
148
|
-
constructor: shape.imported,
|
|
149
|
-
line: line + 1,
|
|
150
|
-
col: character,
|
|
151
|
-
fields,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function primitiveSystem(element: ts.JsxOpeningLikeElement): string | undefined {
|
|
156
|
-
if (element.tagName.getText() !== 'primitive') return undefined;
|
|
157
|
-
const object = element.attributes.properties.find(
|
|
158
|
-
(candidate): candidate is ts.JsxAttribute =>
|
|
159
|
-
ts.isJsxAttribute(candidate) && candidate.name.getText() === 'object',
|
|
160
|
-
);
|
|
161
|
-
const expression =
|
|
162
|
-
object?.initializer && ts.isJsxExpression(object.initializer)
|
|
163
|
-
? object.initializer.expression
|
|
164
|
-
: undefined;
|
|
165
|
-
return expression &&
|
|
166
|
-
ts.isPropertyAccessExpression(expression) &&
|
|
167
|
-
ts.isIdentifier(expression.expression) &&
|
|
168
|
-
expression.name.text === 'emitter'
|
|
169
|
-
? expression.expression.text
|
|
170
|
-
: undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Native particle bindings keyed by `<primitive object={system.emitter}>`. */
|
|
174
|
-
export function particleBindingsByElement(
|
|
175
|
-
sf: ts.SourceFile,
|
|
176
|
-
): Map<ts.JsxOpeningLikeElement, R3fParticleBinding> {
|
|
177
|
-
const imported = importsOf(sf);
|
|
178
|
-
const byScope = new Map<ts.FunctionLikeDeclaration | null, Map<string, R3fParticleBinding>>();
|
|
179
|
-
const collectSystems = (node: ts.Node): void => {
|
|
180
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
181
|
-
const binding = bindingOf(node.name.text, node, sf, imported);
|
|
182
|
-
if (binding) {
|
|
183
|
-
const scope = enclosingScope(node);
|
|
184
|
-
const systems = byScope.get(scope) ?? new Map();
|
|
185
|
-
systems.set(node.name.text, binding);
|
|
186
|
-
byScope.set(scope, systems);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
ts.forEachChild(node, collectSystems);
|
|
190
|
-
};
|
|
191
|
-
collectSystems(sf);
|
|
192
|
-
|
|
193
|
-
const result = new Map<ts.JsxOpeningLikeElement, R3fParticleBinding>();
|
|
194
|
-
const resolve = (node: ts.Node, system: string): R3fParticleBinding | undefined => {
|
|
195
|
-
let scope: ts.FunctionLikeDeclaration | null = enclosingScope(node);
|
|
196
|
-
for (;;) {
|
|
197
|
-
const binding = byScope.get(scope)?.get(system);
|
|
198
|
-
if (binding) return binding;
|
|
199
|
-
if (scope === null) return undefined;
|
|
200
|
-
scope = enclosingScope(scope);
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
const join = (node: ts.Node): void => {
|
|
204
|
-
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
205
|
-
const system = primitiveSystem(node);
|
|
206
|
-
const binding = system ? resolve(node, system) : undefined;
|
|
207
|
-
if (binding) result.set(node, binding);
|
|
208
|
-
}
|
|
209
|
-
ts.forEachChild(node, join);
|
|
210
|
-
};
|
|
211
|
-
join(sf);
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SYNTACTIC declared-prop specs — the checker-free half of the declared-props
|
|
3
|
-
* story, for the tier that cannot build a `ts.Program`.
|
|
4
|
-
*
|
|
5
|
-
* The dev server resolves a component's prop surface semantically
|
|
6
|
-
* (`component-prop-types.ts`: a real `ts.LanguageService`, so
|
|
7
|
-
* `variant: EnemyVariant` becomes `'raider' | 'sharpshooter'` with no grammar
|
|
8
|
-
* here). The hosted editor has no filesystem to host a program, and until this
|
|
9
|
-
* module it shipped NO specs at all — which silently disabled everything
|
|
10
|
-
* downstream that gates on a spec: the Prefab Instance card's override
|
|
11
|
-
* detection (`isOverride` requires `spec.optional`) and Apply-to-Component
|
|
12
|
-
* (requires `spec.defaultValue`) read as "Overrides (0) — Using component
|
|
13
|
-
* defaults" while the authored divergence sat in plain sight.
|
|
14
|
-
*
|
|
15
|
-
* What syntax alone CAN answer, this answers, by the same WHICH-props rule the
|
|
16
|
-
* checker module documents: the props that belong to a component are the
|
|
17
|
-
* members the author literally wrote in its props type —
|
|
18
|
-
* `Omit<ThreeElements['mesh'], …> & { color?: string }` contributes only
|
|
19
|
-
* `{ color?: string }`; the forwarded native surface is not the author's.
|
|
20
|
-
* Destructured parameter defaults (`{ color = '#c8dce1' }`) supply
|
|
21
|
-
* `optional`/`defaultText`/`defaultValue` — exactly the fields the override
|
|
22
|
-
* and apply gates read, and `writeComponentDefault` rewrites. Types the
|
|
23
|
-
* grammar cannot prove stay `null`, the spec's documented "no honest widget"
|
|
24
|
-
* state; they are never guessed from a default's shape.
|
|
25
|
-
*
|
|
26
|
-
* A component with no authored members but WITH destructured defaults still
|
|
27
|
-
* gets specs for those defaulted names — the spec docstring's own rule
|
|
28
|
-
* ("Declared `?:`, or given a default by the component's destructuring").
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import ts from 'typescript';
|
|
32
|
-
import type { ComponentPropSpec, R3fTypeAlias } from './oid-transform';
|
|
33
|
-
|
|
34
|
-
interface AuthoredMember {
|
|
35
|
-
optional: boolean;
|
|
36
|
-
type: ComponentPropSpec['type'];
|
|
37
|
-
options?: Array<string | number>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** `'a' | 'b'` → enum options; `[number, number, number]` → vec3; keywords →
|
|
41
|
-
* their widget; everything else → `null` (no honest widget from syntax). */
|
|
42
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one bounded type-syntax dispatcher, not application control flow.
|
|
43
|
-
function widgetTypeOf(node: ts.TypeNode | undefined): {
|
|
44
|
-
type: ComponentPropSpec['type'];
|
|
45
|
-
options?: Array<string | number>;
|
|
46
|
-
} {
|
|
47
|
-
if (!node) return { type: null };
|
|
48
|
-
switch (node.kind) {
|
|
49
|
-
case ts.SyntaxKind.StringKeyword:
|
|
50
|
-
return { type: 'string' };
|
|
51
|
-
case ts.SyntaxKind.NumberKeyword:
|
|
52
|
-
return { type: 'number' };
|
|
53
|
-
case ts.SyntaxKind.BooleanKeyword:
|
|
54
|
-
return { type: 'boolean' };
|
|
55
|
-
default:
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
if (ts.isUnionTypeNode(node)) {
|
|
59
|
-
const options: Array<string | number> = [];
|
|
60
|
-
for (const member of node.types) {
|
|
61
|
-
if (!ts.isLiteralTypeNode(member)) return { type: null };
|
|
62
|
-
if (ts.isStringLiteral(member.literal)) options.push(member.literal.text);
|
|
63
|
-
else if (ts.isNumericLiteral(member.literal)) options.push(Number(member.literal.text));
|
|
64
|
-
else return { type: null };
|
|
65
|
-
}
|
|
66
|
-
return options.length > 0 ? { type: 'enum', options } : { type: null };
|
|
67
|
-
}
|
|
68
|
-
if (
|
|
69
|
-
ts.isTupleTypeNode(node) &&
|
|
70
|
-
node.elements.length === 3 &&
|
|
71
|
-
node.elements.every((element) => element.kind === ts.SyntaxKind.NumberKeyword)
|
|
72
|
-
) {
|
|
73
|
-
return { type: 'vec3' };
|
|
74
|
-
}
|
|
75
|
-
return { type: null };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** A member's type through the alias it names — `kind: WeaponKind` where
|
|
79
|
-
* `WeaponKind` is `'pistol' | 'rifle' | 'grenade'` declared in this file or
|
|
80
|
-
* imported from another the resolver supplied. Without this the member read
|
|
81
|
-
* `type: null` and a required enum prop had no first option to offer a drop
|
|
82
|
-
* (`<WeaponPickup>` rendered nothing until `kind` was set — runhuman pass
|
|
83
|
-
* 123; the door built for it answered null on preview-b56). One hop, in the
|
|
84
|
-
* file the alias was written in. */
|
|
85
|
-
function memberTypeNode(
|
|
86
|
-
node: ts.TypeNode | undefined,
|
|
87
|
-
sf: ts.SourceFile,
|
|
88
|
-
importedAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
89
|
-
): ts.TypeNode | undefined {
|
|
90
|
-
if (!node || !ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) return node;
|
|
91
|
-
if (node.typeArguments && node.typeArguments.length > 0) return node;
|
|
92
|
-
const name = node.typeName.text;
|
|
93
|
-
const local = localTypeDeclaration(sf, name);
|
|
94
|
-
if (local) return ts.isTypeAliasDeclaration(local) ? local.type : node;
|
|
95
|
-
const imported = importedAliases.get(name);
|
|
96
|
-
return imported ? imported.type : node;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function collectMembers(
|
|
100
|
-
members: ts.NodeArray<ts.TypeElement>,
|
|
101
|
-
into: Map<string, AuthoredMember>,
|
|
102
|
-
sf: ts.SourceFile,
|
|
103
|
-
importedAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
104
|
-
): void {
|
|
105
|
-
for (const member of members) {
|
|
106
|
-
if (!ts.isPropertySignature(member)) continue;
|
|
107
|
-
const name =
|
|
108
|
-
ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) ? member.name.text : null;
|
|
109
|
-
if (!name) continue;
|
|
110
|
-
into.set(name, {
|
|
111
|
-
optional: member.questionToken !== undefined,
|
|
112
|
-
...widgetTypeOf(memberTypeNode(member.type, sf, importedAliases)),
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** A named type declared in this same source file, or null. */
|
|
118
|
-
function localTypeDeclaration(
|
|
119
|
-
sf: ts.SourceFile,
|
|
120
|
-
name: string,
|
|
121
|
-
): ts.TypeAliasDeclaration | ts.InterfaceDeclaration | null {
|
|
122
|
-
for (const statement of sf.statements) {
|
|
123
|
-
if (
|
|
124
|
-
(ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) &&
|
|
125
|
-
statement.name.text === name
|
|
126
|
-
) {
|
|
127
|
-
return statement;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* The authored members of a props type node. Follows intersections, same-file
|
|
135
|
-
* aliases/interfaces, and imported aliases the resolver supplied; a reference
|
|
136
|
-
* that resolves to nothing project-authored (`Omit<…>`, `ThreeElements[…]`)
|
|
137
|
-
* contributes nothing — the checker module's WHICH-props rule.
|
|
138
|
-
*/
|
|
139
|
-
function membersOfTypeNode(
|
|
140
|
-
node: ts.TypeNode,
|
|
141
|
-
sf: ts.SourceFile,
|
|
142
|
-
importedAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
143
|
-
into: Map<string, AuthoredMember>,
|
|
144
|
-
depth: number,
|
|
145
|
-
): void {
|
|
146
|
-
if (depth > 4) return;
|
|
147
|
-
if (ts.isTypeLiteralNode(node)) {
|
|
148
|
-
collectMembers(node.members, into, sf, importedAliases);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (ts.isIntersectionTypeNode(node)) {
|
|
152
|
-
for (const part of node.types) {
|
|
153
|
-
membersOfTypeNode(part, sf, importedAliases, into, depth + 1);
|
|
154
|
-
}
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) return;
|
|
158
|
-
const name = node.typeName.text;
|
|
159
|
-
const local = localTypeDeclaration(sf, name);
|
|
160
|
-
if (local) {
|
|
161
|
-
if (ts.isTypeAliasDeclaration(local)) {
|
|
162
|
-
membersOfTypeNode(local.type, sf, importedAliases, into, depth + 1);
|
|
163
|
-
} else {
|
|
164
|
-
collectMembers(local.members, into, sf, importedAliases);
|
|
165
|
-
}
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const imported = importedAliases.get(name);
|
|
169
|
-
// The alias's own references resolve in the file it was WRITTEN in — the
|
|
170
|
-
// pairing `R3fTypeAlias` exists to carry.
|
|
171
|
-
if (imported) membersOfTypeNode(imported.type, imported.sf, new Map(), into, depth + 1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one bounded literal-syntax dispatcher, not application control flow.
|
|
175
|
-
function defaultOf(
|
|
176
|
-
init: ts.Expression,
|
|
177
|
-
sf: ts.SourceFile,
|
|
178
|
-
): { defaultText: string; defaultValue?: string | number | boolean | number[] } {
|
|
179
|
-
const defaultText = init.getText(sf);
|
|
180
|
-
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
181
|
-
return { defaultText, defaultValue: init.text };
|
|
182
|
-
}
|
|
183
|
-
if (ts.isNumericLiteral(init)) return { defaultText, defaultValue: Number(init.text) };
|
|
184
|
-
if (
|
|
185
|
-
ts.isPrefixUnaryExpression(init) &&
|
|
186
|
-
init.operator === ts.SyntaxKind.MinusToken &&
|
|
187
|
-
ts.isNumericLiteral(init.operand)
|
|
188
|
-
) {
|
|
189
|
-
return { defaultText, defaultValue: -Number(init.operand.text) };
|
|
190
|
-
}
|
|
191
|
-
if (init.kind === ts.SyntaxKind.TrueKeyword) return { defaultText, defaultValue: true };
|
|
192
|
-
if (init.kind === ts.SyntaxKind.FalseKeyword) return { defaultText, defaultValue: false };
|
|
193
|
-
if (ts.isArrayLiteralExpression(init)) {
|
|
194
|
-
const numbers: number[] = [];
|
|
195
|
-
for (const element of init.elements) {
|
|
196
|
-
if (ts.isNumericLiteral(element)) numbers.push(Number(element.text));
|
|
197
|
-
else if (
|
|
198
|
-
ts.isPrefixUnaryExpression(element) &&
|
|
199
|
-
element.operator === ts.SyntaxKind.MinusToken &&
|
|
200
|
-
ts.isNumericLiteral(element.operand)
|
|
201
|
-
) {
|
|
202
|
-
numbers.push(-Number(element.operand.text));
|
|
203
|
-
} else return { defaultText };
|
|
204
|
-
}
|
|
205
|
-
return { defaultText, defaultValue: numbers };
|
|
206
|
-
}
|
|
207
|
-
return { defaultText };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** The props parameter of a component declaration, however it is spelled. */
|
|
211
|
-
function componentParameterOf(node: ts.Node): {
|
|
212
|
-
name: string;
|
|
213
|
-
parameter: ts.ParameterDeclaration;
|
|
214
|
-
} | null {
|
|
215
|
-
if (ts.isFunctionDeclaration(node) && node.name && node.parameters[0]) {
|
|
216
|
-
return { name: node.name.text, parameter: node.parameters[0] };
|
|
217
|
-
}
|
|
218
|
-
if (
|
|
219
|
-
ts.isVariableDeclaration(node) &&
|
|
220
|
-
ts.isIdentifier(node.name) &&
|
|
221
|
-
node.initializer &&
|
|
222
|
-
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
|
223
|
-
node.initializer.parameters[0]
|
|
224
|
-
) {
|
|
225
|
-
return { name: node.name.text, parameter: node.initializer.parameters[0] };
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Every top-level component in `sf` that declares a prop surface syntax can
|
|
232
|
-
* read, by local name. `importedAliases` is the resolver's answer for the
|
|
233
|
-
* file's `import type { X } from './shared'` bindings.
|
|
234
|
-
*/
|
|
235
|
-
export function localComponentPropSpecs(
|
|
236
|
-
sf: ts.SourceFile,
|
|
237
|
-
importedAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
238
|
-
): Map<string, ComponentPropSpec[]> {
|
|
239
|
-
const found = new Map<string, ComponentPropSpec[]>();
|
|
240
|
-
const visit = (node: ts.Node): void => {
|
|
241
|
-
const component = componentParameterOf(node);
|
|
242
|
-
if (component && /^[A-Z]/.test(component.name) && !found.has(component.name)) {
|
|
243
|
-
const specs = specsOfParameter(component.parameter, sf, importedAliases);
|
|
244
|
-
if (specs.length > 0) found.set(component.name, specs);
|
|
245
|
-
}
|
|
246
|
-
if (ts.isSourceFile(node) || ts.isVariableStatement(node) || ts.isVariableDeclarationList(node))
|
|
247
|
-
ts.forEachChild(node, visit);
|
|
248
|
-
};
|
|
249
|
-
visit(sf);
|
|
250
|
-
return found;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one bounded pass keeps the destructure-default and type-member halves of a parameter together.
|
|
254
|
-
function specsOfParameter(
|
|
255
|
-
parameter: ts.ParameterDeclaration,
|
|
256
|
-
sf: ts.SourceFile,
|
|
257
|
-
importedAliases: ReadonlyMap<string, R3fTypeAlias>,
|
|
258
|
-
): ComponentPropSpec[] {
|
|
259
|
-
const defaults = new Map<
|
|
260
|
-
string,
|
|
261
|
-
{ defaultText: string; defaultValue?: string | number | boolean | number[] }
|
|
262
|
-
>();
|
|
263
|
-
if (ts.isObjectBindingPattern(parameter.name)) {
|
|
264
|
-
for (const element of parameter.name.elements) {
|
|
265
|
-
if (element.dotDotDotToken) continue;
|
|
266
|
-
const authored = element.propertyName ?? element.name;
|
|
267
|
-
if (!ts.isIdentifier(authored) || !element.initializer) continue;
|
|
268
|
-
defaults.set(authored.text, defaultOf(element.initializer, sf));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
const members = new Map<string, AuthoredMember>();
|
|
272
|
-
if (parameter.type) membersOfTypeNode(parameter.type, sf, importedAliases, members, 0);
|
|
273
|
-
if (members.size > 0) {
|
|
274
|
-
const specs: ComponentPropSpec[] = [];
|
|
275
|
-
for (const [name, member] of members) {
|
|
276
|
-
const withDefault = defaults.get(name);
|
|
277
|
-
specs.push({
|
|
278
|
-
name,
|
|
279
|
-
type: member.type,
|
|
280
|
-
...(member.options ? { options: member.options } : {}),
|
|
281
|
-
optional: member.optional || withDefault !== undefined,
|
|
282
|
-
...(withDefault ?? {}),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return specs;
|
|
286
|
-
}
|
|
287
|
-
// No authored members syntax can see: the destructured defaults are still
|
|
288
|
-
// the author's declaration, and they carry every field the override and
|
|
289
|
-
// apply gates read.
|
|
290
|
-
return [...defaults].map(([name, withDefault]) => ({
|
|
291
|
-
name,
|
|
292
|
-
type: null,
|
|
293
|
-
optional: true,
|
|
294
|
-
...withDefault,
|
|
295
|
-
}));
|
|
296
|
-
}
|