@volter/editor-react 0.5.66

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.
Files changed (33) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +4 -0
  3. package/contributions/component-contracts.service.ts +18 -0
  4. package/dist-node/component-prop-worker.mjs +335 -0
  5. package/dist-node/serving.mjs +5842 -0
  6. package/package.json +51 -0
  7. package/serving/component-prop-worker.ts +72 -0
  8. package/serving/index.ts +21 -0
  9. package/serving/ui-oid-plugin.ts +1844 -0
  10. package/src/source/component-prop-types.ts +517 -0
  11. package/src/source/ensure-import.ts +132 -0
  12. package/src/source/oid-transform.ts +1667 -0
  13. package/src/source/plan-csf-story.ts +196 -0
  14. package/src/source/plan-extract-component.ts +421 -0
  15. package/src/source/plan-fork-component.ts +621 -0
  16. package/src/source/plan-named-style.ts +95 -0
  17. package/src/source/plan-source-edit.ts +353 -0
  18. package/src/source/r3f-contract-resolver.ts +327 -0
  19. package/src/source/r3f-diagnostic-index.ts +165 -0
  20. package/src/source/r3f-environment-binding.ts +106 -0
  21. package/src/source/r3f-joint-binding.ts +228 -0
  22. package/src/source/r3f-lod-binding.ts +81 -0
  23. package/src/source/r3f-particle-binding.ts +213 -0
  24. package/src/source/r3f-physics-binding.ts +454 -0
  25. package/src/source/r3f-project-contracts.ts +444 -0
  26. package/src/source/relative-import-specifier.ts +34 -0
  27. package/src/source/reparent-guard.ts +531 -0
  28. package/src/source/source-edit-request.ts +22 -0
  29. package/src/source/syntactic-prop-specs.ts +296 -0
  30. package/src/source/ts-ast.ts +171 -0
  31. package/src/source/utility-class-support.ts +169 -0
  32. package/src/source/write-component-default.ts +133 -0
  33. package/src/source/writer.ts +1982 -0
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The component-contract analyzer, registered for the browser (a `workspace.service`
3
+ * contribution): what the kit's Content index reads when it indexes a project from storage,
4
+ * with no session server to ask (`@volter/editor-sdk/source-analysis`). The session's server
5
+ * registers the same analyzer through this package's `vgai.serving` module.
6
+ */
7
+
8
+ import { registerComponentContractAnalyzer } from '@volter/editor-sdk/source-analysis';
9
+ import { analyzeR3fComponentContracts, builtinR3fContractsForSource } from '../src/source/oid-transform';
10
+
11
+ export const point = 'workspace.service';
12
+
13
+ export function start(): () => void {
14
+ registerComponentContractAnalyzer((source, path) =>
15
+ analyzeR3fComponentContracts(source, path, builtinR3fContractsForSource(source, path)),
16
+ );
17
+ return () => registerComponentContractAnalyzer(null);
18
+ }
@@ -0,0 +1,335 @@
1
+ // serving/component-prop-worker.ts
2
+ import { parentPort } from "node:worker_threads";
3
+
4
+ // src/source/component-prop-types.ts
5
+ import { statSync } from "node:fs";
6
+ import { dirname, resolve } from "node:path";
7
+ import ts from "typescript";
8
+ function componentParameterOf(declaration) {
9
+ if (ts.isFunctionDeclaration(declaration)) return declaration.parameters[0] ?? null;
10
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
11
+ const init = declaration.initializer;
12
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
13
+ return init.parameters[0] ?? null;
14
+ }
15
+ return null;
16
+ }
17
+ function componentParameter(sf, componentName) {
18
+ let found = null;
19
+ const visit = (node) => {
20
+ if (found) return;
21
+ const name = ts.isFunctionDeclaration(node) ? node.name?.text : ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) ? node.name.text : void 0;
22
+ if (name === componentName) found = componentParameterOf(node);
23
+ if (!found) ts.forEachChild(node, visit);
24
+ };
25
+ visit(sf);
26
+ return found;
27
+ }
28
+ function jsxTag(sf, tagName) {
29
+ let found = null;
30
+ const visit = (node) => {
31
+ if (found) return;
32
+ if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName) && node.tagName.text === tagName) {
33
+ found = node;
34
+ return;
35
+ }
36
+ ts.forEachChild(node, visit);
37
+ };
38
+ visit(sf);
39
+ return found;
40
+ }
41
+ function componentDeclaration(identifier, checker) {
42
+ let symbol = checker.getSymbolAtLocation(identifier);
43
+ for (let hop = 0; symbol && symbol.flags & ts.SymbolFlags.Alias && hop < 8; hop += 1) {
44
+ const next = checker.getAliasedSymbol(symbol);
45
+ if (next === symbol) break;
46
+ symbol = next;
47
+ }
48
+ return symbol?.valueDeclaration ?? symbol?.declarations?.[0] ?? null;
49
+ }
50
+ function contextlessPropsType(declaration, checker) {
51
+ const parameter = componentParameterOf(declaration);
52
+ return parameter?.type ? checker.getTypeAtLocation(parameter.type) : null;
53
+ }
54
+ var PROJECT = "(project)";
55
+ var NODE_MODULES_PKG_RE = /node_modules\/((?:@[^/]+\/)?[^/]+)\//;
56
+ function packageOfFile(fileName) {
57
+ return NODE_MODULES_PKG_RE.exec(fileName)?.[1] ?? PROJECT;
58
+ }
59
+ function packageOfDeclaration(node) {
60
+ return packageOfFile(node.getSourceFile().fileName);
61
+ }
62
+ function ownProps(propsType, homePackage) {
63
+ const own = [];
64
+ for (const symbol of propsType.getProperties()) {
65
+ const declaration = symbol.declarations?.find(
66
+ (candidate) => packageOfDeclaration(candidate) === homePackage
67
+ );
68
+ if (declaration) own.push({ symbol, declaration });
69
+ }
70
+ return own;
71
+ }
72
+ function widgetForType(type, checker) {
73
+ const bare = checker.getNonNullableType(type);
74
+ if (bare.flags & ts.TypeFlags.BooleanLike) return { type: "boolean" };
75
+ if (bare.isUnion()) {
76
+ const members = bare.types.filter((member) => !(member.flags & ts.TypeFlags.Undefined));
77
+ if (members.length && members.every((member) => member.isStringLiteral())) {
78
+ return {
79
+ type: "enum",
80
+ options: members.map((member) => member.value)
81
+ };
82
+ }
83
+ if (members.length && members.every((member) => member.isNumberLiteral())) {
84
+ return {
85
+ type: "enum",
86
+ options: members.map((member) => member.value)
87
+ };
88
+ }
89
+ if (members.length === 1) return widgetForType(members[0], checker);
90
+ return { type: null };
91
+ }
92
+ if (bare.flags & ts.TypeFlags.StringLike) return { type: "string" };
93
+ if (bare.flags & ts.TypeFlags.NumberLike) return { type: "number" };
94
+ const sequenceLength = numericSequenceLength(bare, checker);
95
+ if (sequenceLength === 3) return { type: "vec3" };
96
+ if (sequenceLength !== null) return { type: "json" };
97
+ return { type: null };
98
+ }
99
+ function numericSequenceLength(type, checker) {
100
+ const isNumeric = (member) => Boolean(checker.getNonNullableType(member).flags & ts.TypeFlags.NumberLike);
101
+ if (checker.isTupleType(type)) {
102
+ const args = checker.getTypeArguments(type);
103
+ return args.length > 0 && args.every(isNumeric) ? args.length : null;
104
+ }
105
+ if (checker.isArrayType(type)) {
106
+ const element = checker.getTypeArguments(type)[0];
107
+ return element && isNumeric(element) ? -1 : null;
108
+ }
109
+ return null;
110
+ }
111
+ function literalValue(node) {
112
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
113
+ if (ts.isNumericLiteral(node)) return Number(node.text);
114
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
115
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
116
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
117
+ const operand = literalValue(node.operand);
118
+ return typeof operand === "number" ? -operand : void 0;
119
+ }
120
+ if (ts.isArrayLiteralExpression(node)) {
121
+ const values = node.elements.map(literalValue);
122
+ return values.every((value) => typeof value === "number") ? values : void 0;
123
+ }
124
+ if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node))
125
+ return literalValue(node.expression);
126
+ return void 0;
127
+ }
128
+ function destructuring(parameter, sf) {
129
+ const order = [];
130
+ const defaults = /* @__PURE__ */ new Map();
131
+ if (!ts.isObjectBindingPattern(parameter.name)) return { order, defaults };
132
+ for (const element of parameter.name.elements) {
133
+ if (element.dotDotDotToken) continue;
134
+ const name = element.propertyName?.getText(sf) ?? element.name.getText(sf);
135
+ order.push(name);
136
+ if (element.initializer) defaults.set(name, element.initializer);
137
+ }
138
+ return { order, defaults };
139
+ }
140
+ function docOf(symbol, checker) {
141
+ const prose = ts.displayPartsToString(symbol.getDocumentationComment(checker)).replace(/\s+/g, " ").trim();
142
+ return prose || void 0;
143
+ }
144
+ function declaredSpec(symbol, declaration, checker, defaults) {
145
+ const name = symbol.getName();
146
+ const widget = widgetForType(checker.getTypeOfSymbolAtLocation(symbol, declaration), checker);
147
+ const doc = docOf(symbol, checker);
148
+ return {
149
+ name,
150
+ type: widget.type,
151
+ ...widget.options ? { options: widget.options } : {},
152
+ optional: Boolean(symbol.flags & ts.SymbolFlags.Optional) || defaults.has(name),
153
+ ...doc ? { doc } : {}
154
+ };
155
+ }
156
+ function withDefault(spec, initializer, sf) {
157
+ if (!initializer) return spec;
158
+ const value = literalValue(initializer);
159
+ return {
160
+ ...spec,
161
+ defaultText: initializer.getText(sf),
162
+ ...value === void 0 ? {} : { defaultValue: value }
163
+ };
164
+ }
165
+ function orderedByDestructuring(specs, order) {
166
+ const ordered = [];
167
+ for (const name of order) {
168
+ const spec = specs.get(name);
169
+ if (spec) {
170
+ ordered.push(spec);
171
+ specs.delete(name);
172
+ }
173
+ }
174
+ return [...ordered, ...specs.values()];
175
+ }
176
+ var ComponentPropResolver = class {
177
+ service;
178
+ rootFiles;
179
+ projectRoot;
180
+ constructor(projectRoot) {
181
+ this.projectRoot = projectRoot;
182
+ const configPath = ts.findConfigFile(projectRoot, ts.sys.fileExists, "tsconfig.json");
183
+ const parsed = configPath ? ts.parseJsonConfigFileContent(
184
+ ts.readConfigFile(configPath, ts.sys.readFile).config,
185
+ ts.sys,
186
+ dirname(configPath)
187
+ ) : { fileNames: [], options: {} };
188
+ this.rootFiles = parsed.fileNames;
189
+ const options = {
190
+ ...parsed.options,
191
+ // Types are all this asks for; emitting or reporting errors is not its job.
192
+ noEmit: true,
193
+ skipLibCheck: true,
194
+ jsx: parsed.options.jsx ?? ts.JsxEmit.ReactJSX
195
+ };
196
+ const host = {
197
+ getScriptFileNames: () => this.rootFiles,
198
+ getScriptVersion: (fileName) => this.versionOf(fileName),
199
+ getScriptSnapshot: (fileName) => {
200
+ const text = ts.sys.readFile(fileName);
201
+ return text === void 0 ? void 0 : ts.ScriptSnapshot.fromString(text);
202
+ },
203
+ getCurrentDirectory: () => projectRoot,
204
+ getCompilationSettings: () => options,
205
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
206
+ fileExists: ts.sys.fileExists,
207
+ readFile: ts.sys.readFile,
208
+ readDirectory: ts.sys.readDirectory,
209
+ directoryExists: ts.sys.directoryExists,
210
+ getDirectories: ts.sys.getDirectories,
211
+ ...ts.sys.realpath ? { realpath: ts.sys.realpath } : {}
212
+ };
213
+ this.service = ts.createLanguageService(host, ts.createDocumentRegistry());
214
+ }
215
+ /** Only project sources are re-stamped: a dependency's `.d.ts` does not
216
+ * change under a running editor, and stat-ing thousands of them per query
217
+ * would cost more than the answer is worth. */
218
+ versionOf(fileName) {
219
+ if (fileName.includes("/node_modules/")) return "1";
220
+ if (!fileName.startsWith(this.projectRoot)) return "1";
221
+ try {
222
+ return String(statSync(fileName).mtimeMs);
223
+ } catch {
224
+ return "0";
225
+ }
226
+ }
227
+ /** Make a file known to the program even if the tsconfig did not list it. */
228
+ ensureRoot(file) {
229
+ const absolute = resolve(this.projectRoot, file);
230
+ if (!this.rootFiles.includes(absolute)) this.rootFiles.push(absolute);
231
+ }
232
+ /**
233
+ * The props `componentName` declares, as defined in `file`. Returns an empty
234
+ * list when the component or its props type cannot be found — never a guess.
235
+ */
236
+ propsFor(file, componentName) {
237
+ const context = this.context(file);
238
+ if (!context) return [];
239
+ const parameter = componentParameter(context.sf, componentName);
240
+ if (!parameter?.type) return [];
241
+ return this.specsFor(
242
+ context.checker.getTypeAtLocation(parameter.type),
243
+ packageOfDeclaration(parameter),
244
+ parameter,
245
+ context.checker
246
+ );
247
+ }
248
+ /**
249
+ * The props behind a JSX tag USED in `callsiteFile` — `<Enemy …>` in a world
250
+ * whose `Enemy` is imported from anywhere. The tag identifier is resolved
251
+ * through the checker, so aliased imports, re-exports and barrel files all
252
+ * land on the real definition without a module-resolution guess here.
253
+ */
254
+ propsForTag(callsiteFile, tagName) {
255
+ const context = this.context(callsiteFile);
256
+ if (!context) return [];
257
+ const tag = jsxTag(context.sf, tagName);
258
+ if (!tag) return [];
259
+ const declaration = componentDeclaration(tag.tagName, context.checker);
260
+ if (!declaration) return [];
261
+ const propsType = context.checker.getContextualType(tag.attributes) ?? contextlessPropsType(declaration, context.checker);
262
+ if (!propsType) return [];
263
+ const parameter = componentParameterOf(declaration);
264
+ return this.specsFor(propsType, packageOfDeclaration(declaration), parameter, context.checker);
265
+ }
266
+ context(file) {
267
+ this.ensureRoot(file);
268
+ const program = this.service.getProgram();
269
+ const sf = program?.getSourceFile(resolve(this.projectRoot, file));
270
+ return program && sf ? { sf, checker: program.getTypeChecker() } : null;
271
+ }
272
+ specsFor(propsType, homePackage, parameter, checker) {
273
+ const sf = parameter?.getSourceFile();
274
+ const { order, defaults } = parameter ? destructuring(parameter, parameter.getSourceFile()) : { order: [], defaults: /* @__PURE__ */ new Map() };
275
+ const specs = /* @__PURE__ */ new Map();
276
+ for (const { symbol, declaration } of ownProps(propsType, homePackage)) {
277
+ specs.set(symbol.getName(), declaredSpec(symbol, declaration, checker, defaults));
278
+ }
279
+ for (const name of order) {
280
+ if (specs.has(name)) continue;
281
+ const symbol = propsType.getProperty(name);
282
+ if (!symbol) {
283
+ specs.set(name, { name, type: null, optional: defaults.has(name) });
284
+ continue;
285
+ }
286
+ const widget = widgetForType(checker.getTypeOfSymbol(symbol), checker);
287
+ const doc = docOf(symbol, checker);
288
+ specs.set(name, {
289
+ name,
290
+ type: widget.type,
291
+ ...widget.options ? { options: widget.options } : {},
292
+ optional: Boolean(symbol.flags & ts.SymbolFlags.Optional) || defaults.has(name),
293
+ ...doc ? { doc } : {}
294
+ });
295
+ }
296
+ if (sf) {
297
+ for (const [name, spec] of specs) specs.set(name, withDefault(spec, defaults.get(name), sf));
298
+ }
299
+ return orderedByDestructuring(specs, order);
300
+ }
301
+ dispose() {
302
+ this.service.dispose();
303
+ }
304
+ };
305
+
306
+ // serving/component-prop-worker.ts
307
+ var resolvers = /* @__PURE__ */ new Map();
308
+ function resolverFor(projectRoot) {
309
+ const existing = resolvers.get(projectRoot);
310
+ if (existing) return existing;
311
+ const resolver = new ComponentPropResolver(projectRoot);
312
+ resolvers.set(projectRoot, resolver);
313
+ return resolver;
314
+ }
315
+ if (parentPort === null) {
316
+ throw new Error(
317
+ "component-prop-worker is a worker-thread entry point: it must be spawned with `new Worker(...)` and never imported on the main thread."
318
+ );
319
+ }
320
+ var port = parentPort;
321
+ port.on("message", (message) => {
322
+ try {
323
+ const resolver = resolverFor(message.projectRoot);
324
+ const props = {};
325
+ for (const request of message.requests) {
326
+ props[request.key] = request.definition ? resolver.propsFor(request.file, request.tag) : resolver.propsForTag(request.file, request.tag);
327
+ }
328
+ port.postMessage({ id: message.id, props });
329
+ } catch (error) {
330
+ port.postMessage({
331
+ id: message.id,
332
+ error: error instanceof Error ? error.message : String(error)
333
+ });
334
+ }
335
+ });