@defold-typescript/transpiler 0.23.0 → 0.25.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/config-key-index.d.ts +1 -0
- package/dist/editor-script-erasure.d.ts +2 -1
- package/dist/gui-node-index.d.ts +5 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +923 -28
- package/dist/input-action-index.d.ts +1 -0
- package/dist/output-paths.d.ts +11 -0
- package/dist/project-resources.d.ts +2 -0
- package/dist/scene-component-index.d.ts +5 -0
- package/dist/scene-object-path-index.d.ts +6 -0
- package/dist/scene-text-format.d.ts +9 -0
- package/dist/sprite-animation-index.d.ts +10 -0
- package/dist/url-address-slots.d.ts +19 -0
- package/dist/url-fragment-reachability.d.ts +23 -0
- package/package.json +2 -2
- package/src/config-key-index.ts +34 -0
- package/src/editor-script-erasure.ts +44 -13
- package/src/gui-node-index.ts +71 -0
- package/src/index.ts +33 -0
- package/src/input-action-index.ts +41 -0
- package/src/lifecycle-erasure.ts +12 -3
- package/src/output-paths.ts +69 -0
- package/src/project-resources.ts +20 -0
- package/src/scene-component-index.ts +80 -0
- package/src/scene-object-path-index.ts +148 -0
- package/src/scene-text-format.ts +171 -0
- package/src/sprite-animation-index.ts +241 -0
- package/src/transpile.ts +5 -0
- package/src/url-address-slots.ts +290 -0
- package/src/url-fragment-reachability.ts +175 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildInputActionIndex(documents: ReadonlyMap<string, string>): ReadonlySet<string>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ScriptKind = "script" | "gui-script" | "render-script" | "editor-script";
|
|
2
|
+
export type SourceOutputKind = ScriptKind | "module";
|
|
3
|
+
export interface BuildConfig {
|
|
4
|
+
readonly outDir: string | undefined;
|
|
5
|
+
readonly include: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_INCLUDE: string[];
|
|
8
|
+
export declare const SCRIPT_SUFFIX_BY_KIND: Record<ScriptKind, string>;
|
|
9
|
+
export declare function parseBuildConfig(raw: string): BuildConfig;
|
|
10
|
+
export declare function stripIncludeBase(pattern: string): string;
|
|
11
|
+
export declare function computeOutputRel(rel: string, config: BuildConfig, kind: SourceOutputKind): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface SceneObjectPathIndex {
|
|
2
|
+
readonly paths: ReadonlySet<string>;
|
|
3
|
+
readonly declaredIn: ReadonlyMap<string, readonly string[]>;
|
|
4
|
+
readonly incomplete: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function buildSceneObjectPathIndex(documents: ReadonlyMap<string, string>): SceneObjectPathIndex;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SceneMessage {
|
|
2
|
+
readonly fields: ReadonlyMap<string, readonly string[]>;
|
|
3
|
+
readonly messages: ReadonlyMap<string, readonly SceneMessage[]>;
|
|
4
|
+
}
|
|
5
|
+
export declare class SceneTextFormatError extends Error {
|
|
6
|
+
readonly line: number;
|
|
7
|
+
constructor(message: string, line: number);
|
|
8
|
+
}
|
|
9
|
+
export declare function parseSceneTextFormat(text: string): SceneMessage;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SpriteAnimationIndex {
|
|
2
|
+
readonly byScriptResource: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>;
|
|
3
|
+
readonly tileSetByScriptResource: ReadonlyMap<string, ReadonlyMap<string, string>>;
|
|
4
|
+
readonly unresolved: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function buildSpriteAnimationIndex(input: {
|
|
7
|
+
scenes: ReadonlyMap<string, string>;
|
|
8
|
+
assets: ReadonlyMap<string, string>;
|
|
9
|
+
}): SpriteAnimationIndex;
|
|
10
|
+
export declare function componentIdOfSameObjectAddress(address: string): string | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { UrlParameterClass, UrlParameterTable } from "@defold-typescript/types";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
export interface ClassifiedSlot {
|
|
4
|
+
readonly class: UrlParameterClass;
|
|
5
|
+
readonly text: string;
|
|
6
|
+
readonly textStart: number;
|
|
7
|
+
readonly fragmentStart: number;
|
|
8
|
+
readonly addressText?: string;
|
|
9
|
+
readonly resourceExtensions?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function isAddressClass(parameterClass: UrlParameterClass): boolean;
|
|
12
|
+
export declare function isFragmentCaret(slot: ClassifiedSlot, position: number): boolean;
|
|
13
|
+
export declare function addressClassOfArgument(checker: ts.TypeChecker, table: UrlParameterTable, argument: ts.Expression): UrlParameterClass;
|
|
14
|
+
export declare function resolveClassifiedSlotAtPosition(input: {
|
|
15
|
+
program: ts.Program;
|
|
16
|
+
table: UrlParameterTable;
|
|
17
|
+
fileName: string;
|
|
18
|
+
position: number;
|
|
19
|
+
}): ClassifiedSlot | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { UrlParameterTable } from "@defold-typescript/types";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import type { SceneComponentIndex } from "./scene-component-index";
|
|
4
|
+
export interface UrlFragmentFinding {
|
|
5
|
+
readonly fileName: string;
|
|
6
|
+
readonly start: number;
|
|
7
|
+
readonly length: number;
|
|
8
|
+
readonly fragment: string;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}
|
|
11
|
+
export type UrlFragmentReport = {
|
|
12
|
+
readonly kind: "suppressed";
|
|
13
|
+
readonly reasons: readonly string[];
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "checked";
|
|
16
|
+
readonly findings: readonly UrlFragmentFinding[];
|
|
17
|
+
};
|
|
18
|
+
export declare function checkUrlFragmentReachability(input: {
|
|
19
|
+
program: ts.Program;
|
|
20
|
+
table: UrlParameterTable;
|
|
21
|
+
index: SceneComponentIndex;
|
|
22
|
+
sourceFiles?: readonly ts.SourceFile[];
|
|
23
|
+
}): UrlFragmentReport;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.25.0",
|
|
39
39
|
"@typescript-to-lua/language-extensions": "1.19.0",
|
|
40
40
|
"typescript-to-lua": "^1.36.0"
|
|
41
41
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The `<section>.<key>` ids the project's `game.project` declares — the whole
|
|
2
|
+
// universe `sys.get_config_string` can resolve, since a key the file never
|
|
3
|
+
// writes answers the reader's default instead.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately a line reader rather than a parse of the scene text format:
|
|
6
|
+
// `game.project` is a flat INI the editor writes, the same shape
|
|
7
|
+
// `readExtensionDependencies` already walks. A key may carry a `#`
|
|
8
|
+
// (`dependencies#0`), which is part of its name here and never a fragment.
|
|
9
|
+
|
|
10
|
+
function sectionNameOf(line: string): string | undefined {
|
|
11
|
+
const match = line.trim().match(/^\[(.+)\]$/);
|
|
12
|
+
return match?.[1];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildConfigKeyIndex(gameProjectText: string): ReadonlySet<string> {
|
|
16
|
+
const keys = new Set<string>();
|
|
17
|
+
let section: string | undefined;
|
|
18
|
+
for (const line of gameProjectText.split("\n")) {
|
|
19
|
+
const header = sectionNameOf(line);
|
|
20
|
+
if (header !== undefined) {
|
|
21
|
+
section = header;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// A line before the first header belongs to no section, so it names nothing
|
|
25
|
+
// a reader could ask for.
|
|
26
|
+
if (section === undefined) continue;
|
|
27
|
+
const separator = line.indexOf("=");
|
|
28
|
+
if (separator === -1) continue;
|
|
29
|
+
const key = line.slice(0, separator).trim();
|
|
30
|
+
if (key === "") continue;
|
|
31
|
+
keys.add(`${section}.${key}`);
|
|
32
|
+
}
|
|
33
|
+
return keys;
|
|
34
|
+
}
|
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import {
|
|
3
3
|
createReturnStatement,
|
|
4
|
+
type Expression,
|
|
4
5
|
type Plugin,
|
|
5
6
|
type Statement,
|
|
6
7
|
type TransformationContext,
|
|
7
8
|
} from "typescript-to-lua";
|
|
8
9
|
|
|
9
10
|
export const EDITOR_FACTORY_MODULE = "@defold-typescript/types";
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export const EDITOR_SCRIPT_FACTORY_NAME = "defineEditorScript";
|
|
12
|
+
export const EDITOR_COMMAND_FACTORY_NAME = "defineEditorCommand";
|
|
13
|
+
const EDITOR_FACTORY_NAMES: ReadonlySet<string> = new Set([
|
|
14
|
+
EDITOR_SCRIPT_FACTORY_NAME,
|
|
15
|
+
EDITOR_COMMAND_FACTORY_NAME,
|
|
16
|
+
]);
|
|
17
|
+
// The bare main entry and the walled per-kind subpath the guide teaches; the
|
|
18
|
+
// other kinds' subpaths export their own factories, never these.
|
|
19
|
+
const EDITOR_FACTORY_SPECIFIERS = new Set([
|
|
20
|
+
EDITOR_FACTORY_MODULE,
|
|
21
|
+
`${EDITOR_FACTORY_MODULE}/editor-script`,
|
|
22
|
+
]);
|
|
14
23
|
|
|
15
|
-
function
|
|
24
|
+
function resolveEditorFactoryExport(
|
|
25
|
+
callee: ts.Expression,
|
|
26
|
+
checker: ts.TypeChecker,
|
|
27
|
+
): string | undefined {
|
|
16
28
|
let symbol = checker.getSymbolAtLocation(callee);
|
|
17
29
|
if (symbol === undefined) {
|
|
18
|
-
return
|
|
30
|
+
return undefined;
|
|
19
31
|
}
|
|
20
32
|
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
21
33
|
symbol = checker.getAliasedSymbol(symbol);
|
|
22
34
|
}
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
const name = symbol.getName();
|
|
36
|
+
if (!EDITOR_FACTORY_NAMES.has(name)) {
|
|
37
|
+
return undefined;
|
|
25
38
|
}
|
|
26
39
|
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
27
40
|
if (declaration === undefined) {
|
|
28
|
-
return
|
|
41
|
+
return undefined;
|
|
29
42
|
}
|
|
30
|
-
return declaration.getSourceFile().fileName.includes(EDITOR_FACTORY_MODULE);
|
|
43
|
+
return declaration.getSourceFile().fileName.includes(EDITOR_FACTORY_MODULE) ? name : undefined;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
// Unlike the runtime kinds (lowered to flat top-level lifecycle globals), the
|
|
@@ -41,7 +54,12 @@ function lowerEditorFactoryCall(
|
|
|
41
54
|
if (!ts.isCallExpression(expression)) {
|
|
42
55
|
return undefined;
|
|
43
56
|
}
|
|
44
|
-
|
|
57
|
+
// Only the module factory owns the chunk-level return; a command factory call
|
|
58
|
+
// is unwrapped in place by the `CallExpression` visitor below.
|
|
59
|
+
if (
|
|
60
|
+
resolveEditorFactoryExport(expression.expression, context.checker) !==
|
|
61
|
+
EDITOR_SCRIPT_FACTORY_NAME
|
|
62
|
+
) {
|
|
45
63
|
return undefined;
|
|
46
64
|
}
|
|
47
65
|
const module = expression.arguments[0];
|
|
@@ -77,8 +95,8 @@ export function isEditorFactoryOnlyImport(node: ts.ImportDeclaration): boolean {
|
|
|
77
95
|
if (runtime.length === 0) {
|
|
78
96
|
return false;
|
|
79
97
|
}
|
|
80
|
-
return runtime.every(
|
|
81
|
-
(
|
|
98
|
+
return runtime.every((element) =>
|
|
99
|
+
EDITOR_FACTORY_NAMES.has((element.propertyName ?? element.name).text),
|
|
82
100
|
);
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -96,6 +114,19 @@ export const editorScriptErasurePlugin: Plugin = {
|
|
|
96
114
|
}
|
|
97
115
|
return context.superTransformStatements(node);
|
|
98
116
|
},
|
|
117
|
+
// `defineEditorCommand` is a compile-time identity with no runtime
|
|
118
|
+
// counterpart once its import is erased, so the call must collapse to the
|
|
119
|
+
// command table it wraps rather than survive as a nil-global call.
|
|
120
|
+
[ts.SyntaxKind.CallExpression]: (node, context): Expression => {
|
|
121
|
+
const command = node.arguments[0];
|
|
122
|
+
if (
|
|
123
|
+
command !== undefined &&
|
|
124
|
+
resolveEditorFactoryExport(node.expression, context.checker) === EDITOR_COMMAND_FACTORY_NAME
|
|
125
|
+
) {
|
|
126
|
+
return context.transformExpression(command);
|
|
127
|
+
}
|
|
128
|
+
return context.superTransformExpression(node);
|
|
129
|
+
},
|
|
99
130
|
[ts.SyntaxKind.ImportDeclaration]: (node, context) =>
|
|
100
131
|
isEditorFactoryOnlyImport(node) ? [] : context.superTransformStatements(node),
|
|
101
132
|
},
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
|
|
2
|
+
|
|
3
|
+
// The node ids each script may address, keyed by the project-relative resource
|
|
4
|
+
// path of the `.gui_script` that owns them, plus an honest record of every scene
|
|
5
|
+
// whose ownership could not be settled. A key's absence is not an empty scene:
|
|
6
|
+
// it means no single `.gui` owns that resource, so nothing may be offered for
|
|
7
|
+
// it.
|
|
8
|
+
export interface GuiNodeIndex {
|
|
9
|
+
readonly byScriptResource: ReadonlyMap<string, ReadonlySet<string>>;
|
|
10
|
+
readonly unresolved: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// A `.gui` names its script as a project-absolute resource path, and the key is
|
|
14
|
+
// that path with the leading `/` removed — never a guess at the source it came
|
|
15
|
+
// from. An output path cannot say which include base was stripped to produce it,
|
|
16
|
+
// so the only well-defined direction is forward: a caller maps the file it holds
|
|
17
|
+
// through the build's own output-path math and looks the result up here. A
|
|
18
|
+
// hand-written Lua `.gui_script` keys itself, which no computed name (always
|
|
19
|
+
// `.ts.gui_script`) can equal, so it needs no separate check.
|
|
20
|
+
const SCRIPT_SUFFIX = ".gui_script";
|
|
21
|
+
|
|
22
|
+
function scriptKeyOf(script: string): string | undefined {
|
|
23
|
+
if (!script.endsWith(SCRIPT_SUFFIX)) return undefined;
|
|
24
|
+
const key = script.startsWith("/") ? script.slice(1) : script;
|
|
25
|
+
return key === SCRIPT_SUFFIX ? undefined : key;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildGuiNodeIndex(documents: ReadonlyMap<string, string>): GuiNodeIndex {
|
|
29
|
+
const byScriptResource = new Map<string, ReadonlySet<string>>();
|
|
30
|
+
const claimedBy = new Map<string, string>();
|
|
31
|
+
const unresolved: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (const [displayPath, text] of documents) {
|
|
34
|
+
let document: ReturnType<typeof parseSceneTextFormat>;
|
|
35
|
+
try {
|
|
36
|
+
document = parseSceneTextFormat(text);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
39
|
+
unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [script] = document.fields.get("script") ?? [];
|
|
44
|
+
if (script === undefined) continue;
|
|
45
|
+
const key = scriptKeyOf(script);
|
|
46
|
+
if (key === undefined) continue;
|
|
47
|
+
|
|
48
|
+
const owner = claimedBy.get(key);
|
|
49
|
+
if (owner !== undefined) {
|
|
50
|
+
byScriptResource.delete(key);
|
|
51
|
+
unresolved.push(
|
|
52
|
+
`${key}: claimed by both ${owner} and ${displayPath}, so its node ids are ambiguous`,
|
|
53
|
+
);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
claimedBy.set(key, displayPath);
|
|
57
|
+
|
|
58
|
+
// Top-level `nodes` only. A `layouts` or `templates` block repeats a node
|
|
59
|
+
// under an override, and walking into them would offer the same id twice
|
|
60
|
+
// while inventing ids from scenes this script does not drive.
|
|
61
|
+
const ids = new Set<string>();
|
|
62
|
+
for (const message of document.messages.get("nodes") ?? []) {
|
|
63
|
+
for (const id of message.fields.get("id") ?? []) {
|
|
64
|
+
if (id !== "") ids.add(id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
byScriptResource.set(key, ids);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { byScriptResource, unresolved };
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
export { buildConfigKeyIndex } from "./config-key-index";
|
|
2
|
+
export type { GuiNodeIndex } from "./gui-node-index";
|
|
3
|
+
export { buildGuiNodeIndex } from "./gui-node-index";
|
|
4
|
+
export { buildInputActionIndex } from "./input-action-index";
|
|
1
5
|
export { findMainEntryFactoryImports } from "./main-entry-factory-imports";
|
|
6
|
+
export type { BuildConfig, ScriptKind, SourceOutputKind } from "./output-paths";
|
|
7
|
+
export {
|
|
8
|
+
computeOutputRel,
|
|
9
|
+
DEFAULT_INCLUDE,
|
|
10
|
+
parseBuildConfig,
|
|
11
|
+
SCRIPT_SUFFIX_BY_KIND,
|
|
12
|
+
stripIncludeBase,
|
|
13
|
+
} from "./output-paths";
|
|
2
14
|
export { getProgramDiagnostics } from "./program-diagnostics";
|
|
15
|
+
export { isDefignoredPath, SCAFFOLDED_DEFIGNORE_LINES } from "./project-resources";
|
|
16
|
+
export type { SceneComponentIndex } from "./scene-component-index";
|
|
17
|
+
export { buildSceneComponentIndex } from "./scene-component-index";
|
|
18
|
+
export type { SceneObjectPathIndex } from "./scene-object-path-index";
|
|
19
|
+
export { buildSceneObjectPathIndex } from "./scene-object-path-index";
|
|
20
|
+
export type { SceneMessage } from "./scene-text-format";
|
|
21
|
+
export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
|
|
3
22
|
export type { TranspileSession } from "./session";
|
|
4
23
|
export { createTranspileSession } from "./session";
|
|
24
|
+
export type { SpriteAnimationIndex } from "./sprite-animation-index";
|
|
25
|
+
export {
|
|
26
|
+
buildSpriteAnimationIndex,
|
|
27
|
+
componentIdOfSameObjectAddress,
|
|
28
|
+
} from "./sprite-animation-index";
|
|
5
29
|
export type {
|
|
6
30
|
TranspileDiagnostic,
|
|
7
31
|
TranspileProjectInput,
|
|
@@ -9,3 +33,12 @@ export type {
|
|
|
9
33
|
TranspileResult,
|
|
10
34
|
} from "./transpile";
|
|
11
35
|
export { transpile, transpileProject } from "./transpile";
|
|
36
|
+
export type { ClassifiedSlot } from "./url-address-slots";
|
|
37
|
+
export {
|
|
38
|
+
addressClassOfArgument,
|
|
39
|
+
isAddressClass,
|
|
40
|
+
isFragmentCaret,
|
|
41
|
+
resolveClassifiedSlotAtPosition,
|
|
42
|
+
} from "./url-address-slots";
|
|
43
|
+
export type { UrlFragmentFinding, UrlFragmentReport } from "./url-fragment-reachability";
|
|
44
|
+
export { checkUrlFragmentReachability } from "./url-fragment-reachability";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
|
|
2
|
+
|
|
3
|
+
// The action names the project's `.input_binding` files declare — the whole
|
|
4
|
+
// universe an `on_input` handler can compare against, since an action no binding
|
|
5
|
+
// produces never fires. A project-wide union, deliberately: which binding is
|
|
6
|
+
// live is a `game.project` `[input] game_binding` setting, and a suggestion
|
|
7
|
+
// claims nothing about what is absent.
|
|
8
|
+
//
|
|
9
|
+
// Every top-level sub-message contributes, not a list of the trigger kinds
|
|
10
|
+
// Defold ships today: `key_trigger`, `mouse_trigger`, `gamepad_trigger` and
|
|
11
|
+
// `text_trigger` all carry the same `action` field, and a kind added upstream
|
|
12
|
+
// would otherwise drop out of the universe silently. The `input` half names an
|
|
13
|
+
// engine constant rather than anything the project declares, so it is never
|
|
14
|
+
// offered.
|
|
15
|
+
//
|
|
16
|
+
// A document that will not parse is skipped and its siblings still contribute —
|
|
17
|
+
// the same honest-hole convention `buildGuiNodeIndex` uses, with no `unresolved`
|
|
18
|
+
// channel because nothing here reports.
|
|
19
|
+
export function buildInputActionIndex(documents: ReadonlyMap<string, string>): ReadonlySet<string> {
|
|
20
|
+
const actions = new Set<string>();
|
|
21
|
+
|
|
22
|
+
for (const text of documents.values()) {
|
|
23
|
+
let document: ReturnType<typeof parseSceneTextFormat>;
|
|
24
|
+
try {
|
|
25
|
+
document = parseSceneTextFormat(text);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const triggers of document.messages.values()) {
|
|
32
|
+
for (const trigger of triggers) {
|
|
33
|
+
for (const action of trigger.fields.get("action") ?? []) {
|
|
34
|
+
if (action !== "") actions.add(action);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return actions;
|
|
41
|
+
}
|
package/src/lifecycle-erasure.ts
CHANGED
|
@@ -312,13 +312,22 @@ export function isFactoryOnlyImport(node: ts.ImportDeclaration): boolean {
|
|
|
312
312
|
if (clause === undefined || clause.name !== undefined) {
|
|
313
313
|
return false;
|
|
314
314
|
}
|
|
315
|
+
// A whole-clause `import type { ... }` binds nothing at runtime; leave it for
|
|
316
|
+
// the normal transform, which elides it.
|
|
317
|
+
if (clause.isTypeOnly) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
315
320
|
const bindings = clause.namedBindings;
|
|
316
321
|
if (bindings === undefined || !ts.isNamedImports(bindings)) {
|
|
317
322
|
return false;
|
|
318
323
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
);
|
|
324
|
+
// Type-only specifiers ride along and drop with the erased statement; erase
|
|
325
|
+
// only when every runtime specifier is a lifecycle factory.
|
|
326
|
+
const runtime = bindings.elements.filter((element) => !element.isTypeOnly);
|
|
327
|
+
if (runtime.length === 0) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return runtime.every((element) => FACTORY_NAMES.has((element.propertyName ?? element.name).text));
|
|
322
331
|
}
|
|
323
332
|
|
|
324
333
|
export const lifecycleErasurePlugin: Plugin = {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
export type ScriptKind = "script" | "gui-script" | "render-script" | "editor-script";
|
|
4
|
+
|
|
5
|
+
export type SourceOutputKind = ScriptKind | "module";
|
|
6
|
+
|
|
7
|
+
export interface BuildConfig {
|
|
8
|
+
readonly outDir: string | undefined;
|
|
9
|
+
readonly include: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TsConfig {
|
|
13
|
+
compilerOptions?: {
|
|
14
|
+
outDir?: string;
|
|
15
|
+
};
|
|
16
|
+
include?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_INCLUDE = ["src/**/*.ts"];
|
|
20
|
+
|
|
21
|
+
export const SCRIPT_SUFFIX_BY_KIND: Record<ScriptKind, string> = {
|
|
22
|
+
script: ".ts.script",
|
|
23
|
+
"gui-script": ".ts.gui_script",
|
|
24
|
+
"render-script": ".ts.render_script",
|
|
25
|
+
"editor-script": ".ts.editor_script",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// The `tsconfig.json` reading the output-path math depends on, separated from
|
|
29
|
+
// how the text was obtained: the CLI reads it off disk, the editor plugin gets
|
|
30
|
+
// it from the language-service host, and both must derive the same names from
|
|
31
|
+
// the same text or the plugin predicts a resource the build never writes.
|
|
32
|
+
export function parseBuildConfig(raw: string): BuildConfig {
|
|
33
|
+
const tsconfig = JSON.parse(raw) as TsConfig;
|
|
34
|
+
const outDir = tsconfig.compilerOptions?.outDir;
|
|
35
|
+
const include = tsconfig.include?.length ? tsconfig.include : DEFAULT_INCLUDE;
|
|
36
|
+
return { outDir, include };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stripIncludeBase(pattern: string): string {
|
|
40
|
+
const firstWildcard = pattern.search(/[*?[]/);
|
|
41
|
+
if (firstWildcard === -1) {
|
|
42
|
+
return pattern.endsWith("/") ? pattern : `${path.posix.dirname(pattern)}/`;
|
|
43
|
+
}
|
|
44
|
+
const upToWildcard = pattern.slice(0, firstWildcard);
|
|
45
|
+
const lastSlash = upToWildcard.lastIndexOf("/");
|
|
46
|
+
return lastSlash === -1 ? "" : upToWildcard.slice(0, lastSlash + 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function relUnderOutDir(rel: string, config: BuildConfig): string {
|
|
50
|
+
const { outDir, include } = config;
|
|
51
|
+
if (outDir === undefined || outDir === "" || outDir === ".") {
|
|
52
|
+
return rel;
|
|
53
|
+
}
|
|
54
|
+
const includeBase =
|
|
55
|
+
include
|
|
56
|
+
.map(stripIncludeBase)
|
|
57
|
+
.filter((base) => rel.startsWith(base))
|
|
58
|
+
.sort((a, b) => b.length - a.length)[0] ?? "";
|
|
59
|
+
const relUnderBase = rel.slice(includeBase.length);
|
|
60
|
+
return path.posix.join(outDir, relUnderBase);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function computeOutputRel(rel: string, config: BuildConfig, kind: SourceOutputKind): string {
|
|
64
|
+
const baseRel = relUnderOutDir(rel, config);
|
|
65
|
+
if (kind === "module") {
|
|
66
|
+
return baseRel.replace(/\.ts$/, ".lua");
|
|
67
|
+
}
|
|
68
|
+
return baseRel.replace(/\.ts$/, SCRIPT_SUFFIX_BY_KIND[kind]);
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The Defold editor and bob read neither `.gitignore` nor `.vscode`; they scan
|
|
2
|
+
// the whole project tree for resources. `.defignore` (one root-relative path per
|
|
3
|
+
// line) is how a project tells them to skip a folder. These three carry files
|
|
4
|
+
// the editor would otherwise misread as project resources: `node_modules`
|
|
5
|
+
// dependency trees, the resolver-generated `.defold-types` LuaLS surface, and
|
|
6
|
+
// `.vscode` config plus the multi-MB debug engine binary. `src/` is never
|
|
7
|
+
// listed — its emitted `.ts.script`/`.gui_script`/`.lua` components are exactly
|
|
8
|
+
// what Defold must load. Committed config (not in `GITIGNORE_LINES`, like
|
|
9
|
+
// `.vscode`).
|
|
10
|
+
export const SCAFFOLDED_DEFIGNORE_LINES = ["/node_modules", "/.defold-types", "/.vscode"];
|
|
11
|
+
|
|
12
|
+
// Whether a project-relative display path lies under one of the lines `init`
|
|
13
|
+
// scaffolds. Root-anchored, because a `.defignore` line is a root-relative
|
|
14
|
+
// path: `assets/node_modules/tiles.atlas` is a resource Defold loads.
|
|
15
|
+
export function isDefignoredPath(displayPath: string): boolean {
|
|
16
|
+
const rooted = `/${displayPath.replace(/^\/+/, "")}`;
|
|
17
|
+
return SCAFFOLDED_DEFIGNORE_LINES.some(
|
|
18
|
+
(line) => rooted === line || rooted.startsWith(`${line}/`),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
2
|
+
|
|
3
|
+
// Every component id the project declares, plus an honest record of everything
|
|
4
|
+
// the walk could not read. A non-empty `incomplete` means the universe is not
|
|
5
|
+
// provably whole, so no consumer may conclude that a missing id is absent.
|
|
6
|
+
export interface SceneComponentIndex {
|
|
7
|
+
readonly ids: ReadonlySet<string>;
|
|
8
|
+
readonly incomplete: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// `id` under these names is a component id. `instances`,
|
|
12
|
+
// `collection_instances` and `embedded_instances` carry game-object ids, which
|
|
13
|
+
// name a *path* — and a path can never be proven wrong, because `factory.create`
|
|
14
|
+
// invents them at runtime.
|
|
15
|
+
const COMPONENT_BLOCKS = new Set(["components", "embedded_components"]);
|
|
16
|
+
|
|
17
|
+
// The only two places Defold embeds a whole escaped document in `data:`. Every
|
|
18
|
+
// other `data:` is a plain scalar — `embedded_collision_shape` stores sphere
|
|
19
|
+
// radii there — so re-parsing all of them would report the repo's own example
|
|
20
|
+
// projects as unreadable and suppress the check for good.
|
|
21
|
+
const EMBEDDING_BLOCKS = new Set(["embedded_instances", "embedded_components"]);
|
|
22
|
+
|
|
23
|
+
function collect(
|
|
24
|
+
message: SceneMessage,
|
|
25
|
+
blockName: string,
|
|
26
|
+
depth: number,
|
|
27
|
+
displayPath: string,
|
|
28
|
+
ids: Set<string>,
|
|
29
|
+
incomplete: string[],
|
|
30
|
+
): void {
|
|
31
|
+
if (COMPONENT_BLOCKS.has(blockName)) {
|
|
32
|
+
for (const id of message.fields.get("id") ?? []) {
|
|
33
|
+
ids.add(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (EMBEDDING_BLOCKS.has(blockName)) {
|
|
37
|
+
for (const payload of message.fields.get("data") ?? []) {
|
|
38
|
+
try {
|
|
39
|
+
collect(parseSceneTextFormat(payload), "", depth + 1, displayPath, ids, incomplete);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
42
|
+
incomplete.push(
|
|
43
|
+
`${displayPath}: embedded payload at depth ${depth + 1} could not be parsed (${error.message})`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const [name, nested] of message.messages) {
|
|
49
|
+
for (const child of nested) {
|
|
50
|
+
collect(child, name, depth, displayPath, ids, incomplete);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build the component-id universe from already-read scene sources: keys are
|
|
56
|
+
// display paths, values are file text. Pure — the filesystem walk belongs to the
|
|
57
|
+
// caller, so a test can drive this from inline strings and a build can drive it
|
|
58
|
+
// from a real scan.
|
|
59
|
+
export function buildSceneComponentIndex(
|
|
60
|
+
documents: ReadonlyMap<string, string>,
|
|
61
|
+
): SceneComponentIndex {
|
|
62
|
+
const ids = new Set<string>();
|
|
63
|
+
const incomplete: string[] = [];
|
|
64
|
+
|
|
65
|
+
if (documents.size === 0) {
|
|
66
|
+
incomplete.push("no scene sources were read, so no component id can be proven absent");
|
|
67
|
+
return { ids, incomplete };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const [displayPath, text] of documents) {
|
|
71
|
+
try {
|
|
72
|
+
collect(parseSceneTextFormat(text), "", 0, displayPath, ids, incomplete);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
75
|
+
incomplete.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { ids, incomplete };
|
|
80
|
+
}
|