@defold-typescript/transpiler 0.36.0 → 0.38.0
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/dist/index.d.ts +1 -1
- package/dist/index.js +101 -10
- package/dist/output-paths.d.ts +1 -0
- package/dist/scene-address-declaration.d.ts +7 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/message-dispatch-lowering.ts +35 -4
- package/src/output-paths.ts +24 -0
- package/src/scene-address-declaration.ts +84 -7
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type { ExtensionDependency, LibraryIncludedEntries, LibrarySceneDocument,
|
|
|
13
13
|
export { archiveWrapperOf, isContainedResourcePath, LIBRARY_DEPENDENCY_ROOT, libraryIncludedEntries, readGameProjectDependencies, readLibrarySceneDocuments, } from "./library-dependencies";
|
|
14
14
|
export { findMainEntryFactoryImports } from "./main-entry-factory-imports";
|
|
15
15
|
export type { BuildConfig, ScriptKind, SourceOutputKind } from "./output-paths";
|
|
16
|
-
export { computeOutputRel, DEFAULT_INCLUDE, parseBuildConfig, requirePathForRel, SCRIPT_SUFFIX_BY_KIND, stripIncludeBase, } from "./output-paths";
|
|
16
|
+
export { computeOutputRel, DEFAULT_INCLUDE, parseBuildConfig, projectRelativeBase, requirePathForRel, SCRIPT_SUFFIX_BY_KIND, stripIncludeBase, } from "./output-paths";
|
|
17
17
|
export { getProgramDiagnostics } from "./program-diagnostics";
|
|
18
18
|
export { isDefignoredPath, SCAFFOLDED_DEFIGNORE_LINES } from "./project-resources";
|
|
19
19
|
export { rewriteEmittedRequires } from "./require-rewrite";
|
package/dist/index.js
CHANGED
|
@@ -56,7 +56,7 @@ function resolvesToDispatchExport(callee, checker) {
|
|
|
56
56
|
}
|
|
57
57
|
return declaration.getSourceFile().fileName.includes(DISPATCH_MODULE);
|
|
58
58
|
}
|
|
59
|
-
function handlerName(property) {
|
|
59
|
+
function handlerName(property, checker) {
|
|
60
60
|
const name = property.name;
|
|
61
61
|
if (name === undefined) {
|
|
62
62
|
return;
|
|
@@ -64,8 +64,29 @@ function handlerName(property) {
|
|
|
64
64
|
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
|
65
65
|
return name.text;
|
|
66
66
|
}
|
|
67
|
+
if (ts.isNumericLiteral(name)) {
|
|
68
|
+
return String(Number(name.text));
|
|
69
|
+
}
|
|
70
|
+
if (ts.isComputedPropertyName(name)) {
|
|
71
|
+
const type = checker.getTypeAtLocation(name.expression);
|
|
72
|
+
if (type.isStringLiteral() || type.isNumberLiteral()) {
|
|
73
|
+
return String(type.value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
67
76
|
return;
|
|
68
77
|
}
|
|
78
|
+
function unlowerableHandler(node, text) {
|
|
79
|
+
const file = node.getSourceFile();
|
|
80
|
+
const start = node.getStart(file);
|
|
81
|
+
return {
|
|
82
|
+
file,
|
|
83
|
+
start,
|
|
84
|
+
length: node.getEnd() - start,
|
|
85
|
+
category: ts.DiagnosticCategory.Error,
|
|
86
|
+
code: 0,
|
|
87
|
+
messageText: `${DISPATCH_NAME} cannot lower \`${text}\`: pass an object literal whose keys are an identifier, a quoted or numeric literal, or a computed literal constant, each with its handler written inline.`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
69
90
|
function handlerFunction(property) {
|
|
70
91
|
if (ts.isMethodDeclaration(property) && property.body !== undefined) {
|
|
71
92
|
return property;
|
|
@@ -104,8 +125,11 @@ function handlerBody(fn, context) {
|
|
|
104
125
|
var messageDispatchLoweringPlugin = {
|
|
105
126
|
visitors: {
|
|
106
127
|
[ts.SyntaxKind.CallExpression]: (node, context) => {
|
|
128
|
+
if (!resolvesToDispatchExport(node.expression, context.checker)) {
|
|
129
|
+
return context.superTransformExpression(node);
|
|
130
|
+
}
|
|
107
131
|
const [handlersArg] = node.arguments;
|
|
108
|
-
if (node.arguments.length === 1 && handlersArg !== undefined && ts.isObjectLiteralExpression(handlersArg)
|
|
132
|
+
if (node.arguments.length === 1 && handlersArg !== undefined && ts.isObjectLiteralExpression(handlersArg)) {
|
|
109
133
|
let chain;
|
|
110
134
|
const properties = [...handlersArg.properties];
|
|
111
135
|
for (let i = properties.length - 1;i >= 0; i--) {
|
|
@@ -113,9 +137,11 @@ var messageDispatchLoweringPlugin = {
|
|
|
113
137
|
if (property === undefined) {
|
|
114
138
|
continue;
|
|
115
139
|
}
|
|
116
|
-
const name = handlerName(property);
|
|
140
|
+
const name = handlerName(property, context.checker);
|
|
117
141
|
const fn = handlerFunction(property);
|
|
118
142
|
if (name === undefined || fn === undefined) {
|
|
143
|
+
const offending = property.name ?? property;
|
|
144
|
+
context.diagnostics.push(unlowerableHandler(property, offending.getText()));
|
|
119
145
|
continue;
|
|
120
146
|
}
|
|
121
147
|
const condition = createBinaryExpression(createIdentifier("message_id"), createCallExpression(createIdentifier("hash"), [createStringLiteral(name)]), SyntaxKind2.EqualityOperator);
|
|
@@ -130,6 +156,8 @@ var messageDispatchLoweringPlugin = {
|
|
|
130
156
|
];
|
|
131
157
|
return createFunctionExpression(createBlock(chain === undefined ? [] : [chain]), params, undefined, NodeFlags.Declaration, node);
|
|
132
158
|
}
|
|
159
|
+
const offending = handlersArg ?? node;
|
|
160
|
+
context.diagnostics.push(unlowerableHandler(offending, offending.getText()));
|
|
133
161
|
return context.superTransformExpression(node);
|
|
134
162
|
}
|
|
135
163
|
}
|
|
@@ -1993,6 +2021,17 @@ function stripIncludeBase(pattern) {
|
|
|
1993
2021
|
const lastSlash = upToWildcard.lastIndexOf("/");
|
|
1994
2022
|
return lastSlash === -1 ? "" : upToWildcard.slice(0, lastSlash + 1);
|
|
1995
2023
|
}
|
|
2024
|
+
var ABSOLUTE_SPELLING_RE = /^(\/|\\\\|[A-Za-z]:[\\/])/;
|
|
2025
|
+
function projectRelativeBase(pattern) {
|
|
2026
|
+
if (ABSOLUTE_SPELLING_RE.test(pattern)) {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
const base = path.posix.normalize(stripIncludeBase(pattern.split("\\").join("/")));
|
|
2030
|
+
if (base === ".." || base.startsWith("../")) {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
return base === "." || base === "./" ? "" : base;
|
|
2034
|
+
}
|
|
1996
2035
|
function relUnderOutDir(rel, config) {
|
|
1997
2036
|
const { outDir, include } = config;
|
|
1998
2037
|
if (outDir === undefined || outDir === "" || outDir === ".") {
|
|
@@ -2421,20 +2460,71 @@ function componentAddressesOf(documents, paths) {
|
|
|
2421
2460
|
}
|
|
2422
2461
|
return keys;
|
|
2423
2462
|
}
|
|
2424
|
-
function
|
|
2425
|
-
|
|
2463
|
+
function bareScriptsOf(documents, paths) {
|
|
2464
|
+
const agreed = new Map;
|
|
2465
|
+
const record = (id, resource) => {
|
|
2466
|
+
if (!agreed.has(id))
|
|
2467
|
+
agreed.set(id, resource);
|
|
2468
|
+
else if (agreed.get(id) !== resource)
|
|
2469
|
+
agreed.set(id, undefined);
|
|
2470
|
+
};
|
|
2471
|
+
for (const [path, ids] of paths.componentsOf) {
|
|
2472
|
+
const resources = paths.componentResourcesOf.get(path);
|
|
2473
|
+
for (const id of ids)
|
|
2474
|
+
record(id, resources?.get(id));
|
|
2475
|
+
}
|
|
2476
|
+
for (const [displayPath, text] of documents) {
|
|
2477
|
+
let declarations;
|
|
2478
|
+
try {
|
|
2479
|
+
declarations = collectComponentDeclarations(parseSceneTextFormat(text), displayPath, []);
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
if (!(error instanceof SceneTextFormatError))
|
|
2482
|
+
throw error;
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
for (const id of declarations.ids)
|
|
2486
|
+
record(id, declarations.resources.get(id));
|
|
2487
|
+
}
|
|
2488
|
+
return agreed;
|
|
2489
|
+
}
|
|
2490
|
+
function componentValuesOf(documents, paths, scriptModules) {
|
|
2491
|
+
const values = new Map;
|
|
2492
|
+
const moduleOf = (resource) => resource === undefined ? undefined : scriptModules.get(resource);
|
|
2493
|
+
const keys = componentAddressesOf(documents, paths);
|
|
2494
|
+
if (scriptModules.size === 0) {
|
|
2495
|
+
for (const key of keys)
|
|
2496
|
+
values.set(key, undefined);
|
|
2497
|
+
return values;
|
|
2498
|
+
}
|
|
2499
|
+
const bare = bareScriptsOf(documents, paths);
|
|
2500
|
+
for (const key of keys) {
|
|
2501
|
+
const hash = key.indexOf("#");
|
|
2502
|
+
const path = key.slice(0, hash);
|
|
2503
|
+
const id = key.slice(hash + 1);
|
|
2504
|
+
values.set(key, moduleOf(path === "" ? bare.get(id) : paths.componentResourcesOf.get(path)?.get(id)));
|
|
2505
|
+
}
|
|
2506
|
+
return values;
|
|
2507
|
+
}
|
|
2508
|
+
function bodyOf(values) {
|
|
2509
|
+
if (values.size === 0)
|
|
2426
2510
|
return "{}";
|
|
2427
|
-
const members =
|
|
2428
|
-
`
|
|
2511
|
+
const members = [...values].map(([key, specifier]) => {
|
|
2512
|
+
const value = specifier === undefined ? "true" : `typeof import(${JSON.stringify(specifier)}).default`;
|
|
2513
|
+
return ` ${JSON.stringify(key)}: ${value};
|
|
2514
|
+
`;
|
|
2515
|
+
}).sort().join("");
|
|
2429
2516
|
return `{
|
|
2430
2517
|
${members} }`;
|
|
2431
2518
|
}
|
|
2432
|
-
function
|
|
2519
|
+
function trueValues(keys) {
|
|
2520
|
+
return new Map([...keys].map((key) => [key, undefined]));
|
|
2521
|
+
}
|
|
2522
|
+
function buildSceneAddressDeclaration(documents, roles, scriptModules = new Map) {
|
|
2433
2523
|
const index = buildSceneObjectPathIndex(documents, roles);
|
|
2434
|
-
const components =
|
|
2524
|
+
const components = componentValuesOf(documents, index, scriptModules);
|
|
2435
2525
|
return `${BANNER}
|
|
2436
2526
|
declare global {
|
|
2437
|
-
interface SceneGameObjectAddresses ${bodyOf(
|
|
2527
|
+
interface SceneGameObjectAddresses ${bodyOf(trueValues(index.paths))}
|
|
2438
2528
|
|
|
2439
2529
|
interface SceneComponentAddresses ${bodyOf(components)}
|
|
2440
2530
|
}
|
|
@@ -3901,6 +3991,7 @@ export {
|
|
|
3901
3991
|
listProjectResourcePaths,
|
|
3902
3992
|
parseBuildConfig,
|
|
3903
3993
|
parseSceneTextFormat,
|
|
3994
|
+
projectRelativeBase,
|
|
3904
3995
|
readGameProjectDependencies,
|
|
3905
3996
|
readGameProjectSetting,
|
|
3906
3997
|
readLibrarySceneDocuments,
|
package/dist/output-paths.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const DEFAULT_INCLUDE: string[];
|
|
|
8
8
|
export declare const SCRIPT_SUFFIX_BY_KIND: Record<ScriptKind, string>;
|
|
9
9
|
export declare function parseBuildConfig(raw: string): BuildConfig;
|
|
10
10
|
export declare function stripIncludeBase(pattern: string): string;
|
|
11
|
+
export declare function projectRelativeBase(pattern: string): string | undefined;
|
|
11
12
|
/**
|
|
12
13
|
* The `require` path Lua reaches a rel by, mirroring the segment math TSTL
|
|
13
14
|
* applies when it emits one: the extension comes off the last segment, then
|
|
@@ -10,5 +10,11 @@ import type { SceneCollectionRoles } from "./scene-collection-roles";
|
|
|
10
10
|
* byte-identical file, and a document map built in a different order emits the
|
|
11
11
|
* same bytes as well; a `Set` iteration order leaking into the output would
|
|
12
12
|
* make every regeneration rewrite the file and re-check the whole program.
|
|
13
|
+
*
|
|
14
|
+
* `scriptModules` maps a script resource a scene names to the import specifier
|
|
15
|
+
* of the program source that compiles to it, relative to the declaration file.
|
|
16
|
+
* A component address whose resource maps is written as that module's default
|
|
17
|
+
* export, so `msg.post` can read the script's messages off it; every other
|
|
18
|
+
* address stays `true`. Omitted, every address is `true`.
|
|
13
19
|
*/
|
|
14
|
-
export declare function buildSceneAddressDeclaration(documents: ReadonlyMap<string, string>, roles: SceneCollectionRoles): string;
|
|
20
|
+
export declare function buildSceneAddressDeclaration(documents: ReadonlyMap<string, string>, roles: SceneCollectionRoles, scriptModules?: ReadonlyMap<string, string>): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defold-typescript/types": "0.
|
|
38
|
+
"@defold-typescript/types": "0.38.0",
|
|
39
39
|
"@typescript-to-lua/language-extensions": "1.19.0",
|
|
40
40
|
"typescript-to-lua": "^1.36.0"
|
|
41
41
|
}
|
package/src/index.ts
CHANGED
|
@@ -47,7 +47,10 @@ type HandlerFunction = (ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowF
|
|
|
47
47
|
body: ts.ConciseBody;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
function handlerName(
|
|
50
|
+
function handlerName(
|
|
51
|
+
property: ts.ObjectLiteralElementLike,
|
|
52
|
+
checker: ts.TypeChecker,
|
|
53
|
+
): string | undefined {
|
|
51
54
|
const name = property.name;
|
|
52
55
|
if (name === undefined) {
|
|
53
56
|
return undefined;
|
|
@@ -55,9 +58,31 @@ function handlerName(property: ts.ObjectLiteralElementLike): string | undefined
|
|
|
55
58
|
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
|
56
59
|
return name.text;
|
|
57
60
|
}
|
|
61
|
+
if (ts.isNumericLiteral(name)) {
|
|
62
|
+
return String(Number(name.text));
|
|
63
|
+
}
|
|
64
|
+
if (ts.isComputedPropertyName(name)) {
|
|
65
|
+
const type = checker.getTypeAtLocation(name.expression);
|
|
66
|
+
if (type.isStringLiteral() || type.isNumberLiteral()) {
|
|
67
|
+
return String(type.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
58
70
|
return undefined;
|
|
59
71
|
}
|
|
60
72
|
|
|
73
|
+
function unlowerableHandler(node: ts.Node, text: string): ts.Diagnostic {
|
|
74
|
+
const file = node.getSourceFile();
|
|
75
|
+
const start = node.getStart(file);
|
|
76
|
+
return {
|
|
77
|
+
file,
|
|
78
|
+
start,
|
|
79
|
+
length: node.getEnd() - start,
|
|
80
|
+
category: ts.DiagnosticCategory.Error,
|
|
81
|
+
code: 0,
|
|
82
|
+
messageText: `${DISPATCH_NAME} cannot lower \`${text}\`: pass an object literal whose keys are an identifier, a quoted or numeric literal, or a computed literal constant, each with its handler written inline.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
function handlerFunction(property: ts.ObjectLiteralElementLike): HandlerFunction | undefined {
|
|
62
87
|
if (ts.isMethodDeclaration(property) && property.body !== undefined) {
|
|
63
88
|
return property as HandlerFunction;
|
|
@@ -108,12 +133,14 @@ function handlerBody(fn: HandlerFunction, context: TransformationContext): State
|
|
|
108
133
|
export const messageDispatchLoweringPlugin: Plugin = {
|
|
109
134
|
visitors: {
|
|
110
135
|
[ts.SyntaxKind.CallExpression]: (node, context): Expression => {
|
|
136
|
+
if (!resolvesToDispatchExport(node.expression, context.checker)) {
|
|
137
|
+
return context.superTransformExpression(node);
|
|
138
|
+
}
|
|
111
139
|
const [handlersArg] = node.arguments;
|
|
112
140
|
if (
|
|
113
141
|
node.arguments.length === 1 &&
|
|
114
142
|
handlersArg !== undefined &&
|
|
115
|
-
ts.isObjectLiteralExpression(handlersArg)
|
|
116
|
-
resolvesToDispatchExport(node.expression, context.checker)
|
|
143
|
+
ts.isObjectLiteralExpression(handlersArg)
|
|
117
144
|
) {
|
|
118
145
|
let chain: IfStatement | undefined;
|
|
119
146
|
const properties = [...handlersArg.properties];
|
|
@@ -122,9 +149,11 @@ export const messageDispatchLoweringPlugin: Plugin = {
|
|
|
122
149
|
if (property === undefined) {
|
|
123
150
|
continue;
|
|
124
151
|
}
|
|
125
|
-
const name = handlerName(property);
|
|
152
|
+
const name = handlerName(property, context.checker);
|
|
126
153
|
const fn = handlerFunction(property);
|
|
127
154
|
if (name === undefined || fn === undefined) {
|
|
155
|
+
const offending = property.name ?? property;
|
|
156
|
+
context.diagnostics.push(unlowerableHandler(property, offending.getText()));
|
|
128
157
|
continue;
|
|
129
158
|
}
|
|
130
159
|
const condition = createBinaryExpression(
|
|
@@ -149,6 +178,8 @@ export const messageDispatchLoweringPlugin: Plugin = {
|
|
|
149
178
|
node,
|
|
150
179
|
);
|
|
151
180
|
}
|
|
181
|
+
const offending = handlersArg ?? node;
|
|
182
|
+
context.diagnostics.push(unlowerableHandler(offending, offending.getText()));
|
|
152
183
|
return context.superTransformExpression(node);
|
|
153
184
|
},
|
|
154
185
|
},
|
package/src/output-paths.ts
CHANGED
|
@@ -46,6 +46,30 @@ export function stripIncludeBase(pattern: string): string {
|
|
|
46
46
|
return lastSlash === -1 ? "" : upToWildcard.slice(0, lastSlash + 1);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// A root is a folder inside the project a tool can safely speak for, so a
|
|
50
|
+
// pattern that resolves to the project root, escapes `cwd`, or is absolute in
|
|
51
|
+
// any spelling yields nothing. Escape is the normalized `..` *segment*, not a
|
|
52
|
+
// two-period prefix: `..local` is an ordinary folder name. Absoluteness is
|
|
53
|
+
// decided by string shape, never by `path.isAbsolute`, which answers for the
|
|
54
|
+
// host OS and would wave a Windows drive or UNC path through on a POSIX runner.
|
|
55
|
+
const ABSOLUTE_SPELLING_RE = /^(\/|\\\\|[A-Za-z]:[\\/])/;
|
|
56
|
+
|
|
57
|
+
// The folder an include pattern speaks for, as a project-relative posix base
|
|
58
|
+
// (`""` for the project root itself), or `undefined` when the pattern reaches
|
|
59
|
+
// outside the project. The scaffold-rule roots, the starter target and the
|
|
60
|
+
// debug launch derivation all answer "which project folder does this pattern
|
|
61
|
+
// speak for" here, so the three cannot drift.
|
|
62
|
+
export function projectRelativeBase(pattern: string): string | undefined {
|
|
63
|
+
if (ABSOLUTE_SPELLING_RE.test(pattern)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const base = path.posix.normalize(stripIncludeBase(pattern.split("\\").join("/")));
|
|
67
|
+
if (base === ".." || base.startsWith("../")) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return base === "." || base === "./" ? "" : base;
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
function relUnderOutDir(rel: string, config: BuildConfig): string {
|
|
50
74
|
const { outDir, include } = config;
|
|
51
75
|
if (outDir === undefined || outDir === "" || outDir === ".") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SceneCollectionRoles } from "./scene-collection-roles";
|
|
2
|
-
import { buildSceneComponentIndex } from "./scene-component-index";
|
|
2
|
+
import { buildSceneComponentIndex, collectComponentDeclarations } from "./scene-component-index";
|
|
3
3
|
import { buildSceneObjectPathIndex, type SceneObjectPathIndex } from "./scene-object-path-index";
|
|
4
|
+
import { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
|
|
4
5
|
|
|
5
6
|
const BANNER = `// Generated from this project's scenes by \`defold-typescript scene-types\`.
|
|
6
7
|
// Do not edit: every run rewrites it from the .go/.collection sources.
|
|
@@ -28,19 +29,88 @@ function componentAddressesOf(
|
|
|
28
29
|
return keys;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// What a component address resolves to: `true` for an address that names no
|
|
33
|
+
// script this program compiles, else the specifier of the script module it hosts.
|
|
34
|
+
type AddressValues = ReadonlyMap<string, string | undefined>;
|
|
35
|
+
|
|
36
|
+
// A bare `#id` is typed only when every declaration of that id anywhere in the
|
|
37
|
+
// project names the same mapped script. The per-object walk covers each object a
|
|
38
|
+
// world reaches; the per-document pass covers what it cannot — a factory
|
|
39
|
+
// prototype with no static path, an embedded component under the same id — so a
|
|
40
|
+
// bare address never borrows one object's script for another's component.
|
|
41
|
+
function bareScriptsOf(
|
|
42
|
+
documents: ReadonlyMap<string, string>,
|
|
43
|
+
paths: SceneObjectPathIndex,
|
|
44
|
+
): Map<string, string | undefined> {
|
|
45
|
+
const agreed = new Map<string, string | undefined>();
|
|
46
|
+
const record = (id: string, resource: string | undefined): void => {
|
|
47
|
+
if (!agreed.has(id)) agreed.set(id, resource);
|
|
48
|
+
else if (agreed.get(id) !== resource) agreed.set(id, undefined);
|
|
49
|
+
};
|
|
50
|
+
for (const [path, ids] of paths.componentsOf) {
|
|
51
|
+
const resources = paths.componentResourcesOf.get(path);
|
|
52
|
+
for (const id of ids) record(id, resources?.get(id));
|
|
53
|
+
}
|
|
54
|
+
for (const [displayPath, text] of documents) {
|
|
55
|
+
let declarations: ReturnType<typeof collectComponentDeclarations>;
|
|
56
|
+
try {
|
|
57
|
+
declarations = collectComponentDeclarations(parseSceneTextFormat(text), displayPath, []);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const id of declarations.ids) record(id, declarations.resources.get(id));
|
|
63
|
+
}
|
|
64
|
+
return agreed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function componentValuesOf(
|
|
68
|
+
documents: ReadonlyMap<string, string>,
|
|
69
|
+
paths: SceneObjectPathIndex,
|
|
70
|
+
scriptModules: ReadonlyMap<string, string>,
|
|
71
|
+
): AddressValues {
|
|
72
|
+
const values = new Map<string, string | undefined>();
|
|
73
|
+
const moduleOf = (resource: string | undefined): string | undefined =>
|
|
74
|
+
resource === undefined ? undefined : scriptModules.get(resource);
|
|
75
|
+
const keys = componentAddressesOf(documents, paths);
|
|
76
|
+
if (scriptModules.size === 0) {
|
|
77
|
+
for (const key of keys) values.set(key, undefined);
|
|
78
|
+
return values;
|
|
79
|
+
}
|
|
80
|
+
const bare = bareScriptsOf(documents, paths);
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const hash = key.indexOf("#");
|
|
83
|
+
const path = key.slice(0, hash);
|
|
84
|
+
const id = key.slice(hash + 1);
|
|
85
|
+
values.set(
|
|
86
|
+
key,
|
|
87
|
+
moduleOf(path === "" ? bare.get(id) : paths.componentResourcesOf.get(path)?.get(id)),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return values;
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
// An interface with no members closes on the same line: an empty body written
|
|
32
94
|
// across two lines is what a formatter would collapse, and the generated file
|
|
33
95
|
// has to survive a consumer's formatter run byte-identical or every regeneration
|
|
34
96
|
// looks like a change.
|
|
35
|
-
function bodyOf(
|
|
36
|
-
if (
|
|
37
|
-
const members =
|
|
38
|
-
.map((key) =>
|
|
97
|
+
function bodyOf(values: AddressValues): string {
|
|
98
|
+
if (values.size === 0) return "{}";
|
|
99
|
+
const members = [...values]
|
|
100
|
+
.map(([key, specifier]) => {
|
|
101
|
+
const value =
|
|
102
|
+
specifier === undefined ? "true" : `typeof import(${JSON.stringify(specifier)}).default`;
|
|
103
|
+
return ` ${JSON.stringify(key)}: ${value};\n`;
|
|
104
|
+
})
|
|
39
105
|
.sort()
|
|
40
106
|
.join("");
|
|
41
107
|
return `{\n${members} }`;
|
|
42
108
|
}
|
|
43
109
|
|
|
110
|
+
function trueValues(keys: Iterable<string>): AddressValues {
|
|
111
|
+
return new Map([...keys].map((key) => [key, undefined]));
|
|
112
|
+
}
|
|
113
|
+
|
|
44
114
|
/**
|
|
45
115
|
* The global augmentation that fills `SceneGameObjectAddresses` and
|
|
46
116
|
* `SceneComponentAddresses` from already-read scene sources — keys are
|
|
@@ -52,19 +122,26 @@ function bodyOf(keys: readonly string[]): string {
|
|
|
52
122
|
* byte-identical file, and a document map built in a different order emits the
|
|
53
123
|
* same bytes as well; a `Set` iteration order leaking into the output would
|
|
54
124
|
* make every regeneration rewrite the file and re-check the whole program.
|
|
125
|
+
*
|
|
126
|
+
* `scriptModules` maps a script resource a scene names to the import specifier
|
|
127
|
+
* of the program source that compiles to it, relative to the declaration file.
|
|
128
|
+
* A component address whose resource maps is written as that module's default
|
|
129
|
+
* export, so `msg.post` can read the script's messages off it; every other
|
|
130
|
+
* address stays `true`. Omitted, every address is `true`.
|
|
55
131
|
*/
|
|
56
132
|
export function buildSceneAddressDeclaration(
|
|
57
133
|
documents: ReadonlyMap<string, string>,
|
|
58
134
|
roles: SceneCollectionRoles,
|
|
135
|
+
scriptModules: ReadonlyMap<string, string> = new Map(),
|
|
59
136
|
): string {
|
|
60
137
|
// One walk for both key sets: the qualified component addresses are the path
|
|
61
138
|
// universe joined to its own prototypes, not a second index over the same files.
|
|
62
139
|
const index = buildSceneObjectPathIndex(documents, roles);
|
|
63
|
-
const components =
|
|
140
|
+
const components = componentValuesOf(documents, index, scriptModules);
|
|
64
141
|
|
|
65
142
|
return `${BANNER}
|
|
66
143
|
declare global {
|
|
67
|
-
interface SceneGameObjectAddresses ${bodyOf(
|
|
144
|
+
interface SceneGameObjectAddresses ${bodyOf(trueValues(index.paths))}
|
|
68
145
|
|
|
69
146
|
interface SceneComponentAddresses ${bodyOf(components)}
|
|
70
147
|
}
|