@defold-typescript/transpiler 0.4.3 → 0.5.1

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.js CHANGED
@@ -9,11 +9,20 @@ import * as tstl2 from "typescript-to-lua";
9
9
  import * as ts from "typescript";
10
10
  import {
11
11
  createAssignmentStatement,
12
+ createBinaryExpression,
12
13
  createBlock,
14
+ createCallExpression,
13
15
  createExpressionStatement,
16
+ createForInStatement,
14
17
  createFunctionExpression,
15
18
  createIdentifier,
16
- NodeFlags
19
+ createIfStatement,
20
+ createNilLiteral,
21
+ createReturnStatement,
22
+ createTableIndexExpression,
23
+ createVariableDeclarationStatement,
24
+ NodeFlags,
25
+ SyntaxKind as SyntaxKind2
17
26
  } from "typescript-to-lua";
18
27
  var FACTORY_MODULE = "@defold-typescript/types";
19
28
  var FACTORY_NAMES = new Set(["defineScript", "defineGuiScript", "defineRenderScript"]);
@@ -65,6 +74,43 @@ function transformHookBody(fn, context) {
65
74
  }
66
75
  return [createExpressionStatement(context.transformExpression(fn.body))];
67
76
  }
77
+ function crossesFunctionBoundary(node) {
78
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
79
+ }
80
+ function initReturnsValue(fn) {
81
+ if (!ts.isBlock(fn.body)) {
82
+ return true;
83
+ }
84
+ let found = false;
85
+ const visit = (node) => {
86
+ if (found || crossesFunctionBoundary(node)) {
87
+ return;
88
+ }
89
+ if (ts.isReturnStatement(node) && node.expression !== undefined) {
90
+ found = true;
91
+ return;
92
+ }
93
+ ts.forEachChild(node, visit);
94
+ };
95
+ ts.forEachChild(fn.body, visit);
96
+ return found;
97
+ }
98
+ function initBuilderBody(fn, context) {
99
+ if (ts.isBlock(fn.body)) {
100
+ return context.transformStatements(fn.body.statements);
101
+ }
102
+ return [createReturnStatement([context.transformExpression(fn.body)])];
103
+ }
104
+ function emitInitMerge(fn, context, property) {
105
+ const builder = createFunctionExpression(createBlock(initBuilderBody(fn, context)), [], undefined, NodeFlags.Declaration, fn);
106
+ const builderDecl = createVariableDeclarationStatement(createIdentifier("____init"), builder);
107
+ const stateDecl = createVariableDeclarationStatement(createIdentifier("____s"), createCallExpression(createIdentifier("____init"), []));
108
+ const mergeAssignment = createAssignmentStatement(createTableIndexExpression(createIdentifier("self"), createIdentifier("____k")), createIdentifier("____v"));
109
+ const forIn = createForInStatement(createBlock([mergeAssignment]), [createIdentifier("____k"), createIdentifier("____v")], [createCallExpression(createIdentifier("pairs"), [createIdentifier("____s")])]);
110
+ const guard = createIfStatement(createBinaryExpression(createIdentifier("____s"), createNilLiteral(), SyntaxKind2.InequalityOperator), createBlock([forIn]));
111
+ const initFn = createFunctionExpression(createBlock([stateDecl, guard]), [createIdentifier("self")], undefined, NodeFlags.Declaration, fn);
112
+ return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
113
+ }
68
114
  function eraseFactoryCall(expression, context) {
69
115
  if (!ts.isCallExpression(expression)) {
70
116
  return;
@@ -83,6 +129,10 @@ function eraseFactoryCall(expression, context) {
83
129
  if (name === undefined || fn === undefined) {
84
130
  continue;
85
131
  }
132
+ if (name === "init" && initReturnsValue(fn)) {
133
+ statements.push(...emitInitMerge(fn, context, property));
134
+ continue;
135
+ }
86
136
  const params = fn.parameters.filter((param) => !isThisParameter(param) && ts.isIdentifier(param.name)).map((param) => context.transformExpression(param.name));
87
137
  const fnExpression = createFunctionExpression(createBlock(transformHookBody(fn, context)), params, undefined, NodeFlags.Declaration, fn);
88
138
  statements.push(createAssignmentStatement(createIdentifier(name), fnExpression, property));
@@ -120,7 +170,7 @@ var lifecycleErasurePlugin = {
120
170
  };
121
171
 
122
172
  // src/transpile.ts
123
- import { readFileSync } from "node:fs";
173
+ import { readdirSync, readFileSync } from "node:fs";
124
174
  import { createRequire } from "node:module";
125
175
  import * as path from "node:path";
126
176
  import * as tstl from "typescript-to-lua";
@@ -136,18 +186,34 @@ var TSTL_LANG_EXT_ROOT = path.dirname(requireFromHere.resolve("@typescript-to-lu
136
186
  function readAmbient(rel) {
137
187
  return readFileSync(path.join(TYPES_PKG_ROOT, rel), "utf8");
138
188
  }
139
- var AMBIENT_FILES = {
140
- "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(path.join(TSTL_LANG_EXT_ROOT, "index.d.ts"), "utf8"),
141
- "node_modules/@defold-typescript/types/generated/vmath.d.ts": readAmbient("generated/vmath.d.ts"),
142
- "node_modules/@defold-typescript/types/generated/msg.d.ts": readAmbient("generated/msg.d.ts"),
143
- "node_modules/@defold-typescript/types/generated/go.d.ts": readAmbient("generated/go.d.ts"),
144
- "node_modules/@defold-typescript/types/generated/builtin-messages.d.ts": readAmbient("generated/builtin-messages.d.ts"),
145
- "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
146
- "node_modules/@defold-typescript/types/src/msg-overloads.d.ts": readAmbient("src/msg-overloads.d.ts"),
147
- "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
148
- "node_modules/@defold-typescript/types/index.ts": `export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";
189
+ function buildAmbientFiles() {
190
+ const files = {
191
+ "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(path.join(TSTL_LANG_EXT_ROOT, "index.d.ts"), "utf8"),
192
+ "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
193
+ "node_modules/@defold-typescript/types/src/msg-overloads.d.ts": readAmbient("src/msg-overloads.d.ts"),
194
+ "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
195
+ "node_modules/@defold-typescript/types/index.ts": [
196
+ 'export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";',
197
+ 'export type { GuiScriptHooks, InputAction, InputTouch, RenderScriptHooks, ScriptHooks } from "./src/lifecycle";',
198
+ 'export type { Hash, Matrix4, Quaternion, Url, Vector, Vector3, Vector4 } from "./src/core-types";',
199
+ ""
200
+ ].join(`
201
+ `),
202
+ "node_modules/@defold-typescript/types/script.d.ts": `export {};
203
+ `,
204
+ "node_modules/@defold-typescript/types/gui-script.d.ts": `export {};
205
+ `,
206
+ "node_modules/@defold-typescript/types/render-script.d.ts": `export {};
149
207
  `
150
- };
208
+ };
209
+ for (const entry of readdirSync(path.join(TYPES_PKG_ROOT, "generated"))) {
210
+ if (entry.endsWith(".d.ts")) {
211
+ files[`node_modules/@defold-typescript/types/generated/${entry}`] = readAmbient(`generated/${entry}`);
212
+ }
213
+ }
214
+ return files;
215
+ }
216
+ var AMBIENT_FILES = buildAmbientFiles();
151
217
  function collectOutputs(transpiledFiles, diagnostics, userKeys) {
152
218
  const lua = {};
153
219
  const sourceMaps = {};
@@ -174,6 +240,7 @@ function transpileProject(input) {
174
240
  const result = tstl.transpileVirtualProject(merged, {
175
241
  luaTarget: tstl.LuaTarget.Lua54,
176
242
  sourceMap: true,
243
+ skipLibCheck: true,
177
244
  luaPlugins: [{ plugin: lifecycleErasurePlugin }]
178
245
  });
179
246
  return collectOutputs(result.transpiledFiles, result.diagnostics, userKeys);
@@ -192,6 +259,7 @@ var requireFromHere2 = createRequire2(import.meta.url);
192
259
  var COMPILER_OPTIONS = {
193
260
  luaTarget: tstl2.LuaTarget.Lua54,
194
261
  sourceMap: true,
262
+ skipLibCheck: true,
195
263
  luaPlugins: [{ plugin: lifecycleErasurePlugin }]
196
264
  };
197
265
  function normalizeSlashes(p) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "test": "bun test"
29
29
  },
30
30
  "dependencies": {
31
- "@defold-typescript/types": "0.4.3",
31
+ "@defold-typescript/types": "0.5.1",
32
32
  "@typescript-to-lua/language-extensions": "1.19.0",
33
33
  "typescript-to-lua": "^1.36.0"
34
34
  }
@@ -1,14 +1,23 @@
1
1
  import * as ts from "typescript";
2
2
  import {
3
3
  createAssignmentStatement,
4
+ createBinaryExpression,
4
5
  createBlock,
6
+ createCallExpression,
5
7
  createExpressionStatement,
8
+ createForInStatement,
6
9
  createFunctionExpression,
7
10
  createIdentifier,
11
+ createIfStatement,
12
+ createNilLiteral,
13
+ createReturnStatement,
14
+ createTableIndexExpression,
15
+ createVariableDeclarationStatement,
8
16
  type Identifier,
9
17
  NodeFlags,
10
18
  type Plugin,
11
19
  type Statement,
20
+ SyntaxKind,
12
21
  type TransformationContext,
13
22
  } from "typescript-to-lua";
14
23
 
@@ -75,6 +84,92 @@ function transformHookBody(fn: HookFunction, context: TransformationContext): St
75
84
  return [createExpressionStatement(context.transformExpression(fn.body))];
76
85
  }
77
86
 
87
+ function crossesFunctionBoundary(node: ts.Node): boolean {
88
+ return (
89
+ ts.isFunctionDeclaration(node) ||
90
+ ts.isFunctionExpression(node) ||
91
+ ts.isArrowFunction(node) ||
92
+ ts.isMethodDeclaration(node) ||
93
+ ts.isGetAccessorDeclaration(node) ||
94
+ ts.isSetAccessorDeclaration(node)
95
+ );
96
+ }
97
+
98
+ function initReturnsValue(fn: HookFunction): boolean {
99
+ if (!ts.isBlock(fn.body)) {
100
+ return true;
101
+ }
102
+ let found = false;
103
+ const visit = (node: ts.Node): void => {
104
+ if (found || crossesFunctionBoundary(node)) {
105
+ return;
106
+ }
107
+ if (ts.isReturnStatement(node) && node.expression !== undefined) {
108
+ found = true;
109
+ return;
110
+ }
111
+ ts.forEachChild(node, visit);
112
+ };
113
+ ts.forEachChild(fn.body, visit);
114
+ return found;
115
+ }
116
+
117
+ function initBuilderBody(fn: HookFunction, context: TransformationContext): Statement[] {
118
+ if (ts.isBlock(fn.body)) {
119
+ return context.transformStatements(fn.body.statements);
120
+ }
121
+ return [createReturnStatement([context.transformExpression(fn.body)])];
122
+ }
123
+
124
+ // init-merge: Defold owns `self` (a userdata-backed table) and a script can
125
+ // populate but not replace it, so a returning `init(): TSelf` can't be emitted
126
+ // verbatim. Wrap the body in a `____init` builder and merge its result onto the
127
+ // engine `self`; a `nil` return (stateless script) merges nothing.
128
+ function emitInitMerge(
129
+ fn: HookFunction,
130
+ context: TransformationContext,
131
+ property: ts.ObjectLiteralElementLike,
132
+ ): Statement[] {
133
+ const builder = createFunctionExpression(
134
+ createBlock(initBuilderBody(fn, context)),
135
+ [],
136
+ undefined,
137
+ NodeFlags.Declaration,
138
+ fn,
139
+ );
140
+ const builderDecl = createVariableDeclarationStatement(createIdentifier("____init"), builder);
141
+
142
+ const stateDecl = createVariableDeclarationStatement(
143
+ createIdentifier("____s"),
144
+ createCallExpression(createIdentifier("____init"), []),
145
+ );
146
+ const mergeAssignment = createAssignmentStatement(
147
+ createTableIndexExpression(createIdentifier("self"), createIdentifier("____k")),
148
+ createIdentifier("____v"),
149
+ );
150
+ const forIn = createForInStatement(
151
+ createBlock([mergeAssignment]),
152
+ [createIdentifier("____k"), createIdentifier("____v")],
153
+ [createCallExpression(createIdentifier("pairs"), [createIdentifier("____s")])],
154
+ );
155
+ const guard = createIfStatement(
156
+ createBinaryExpression(
157
+ createIdentifier("____s"),
158
+ createNilLiteral(),
159
+ SyntaxKind.InequalityOperator,
160
+ ),
161
+ createBlock([forIn]),
162
+ );
163
+ const initFn = createFunctionExpression(
164
+ createBlock([stateDecl, guard]),
165
+ [createIdentifier("self")],
166
+ undefined,
167
+ NodeFlags.Declaration,
168
+ fn,
169
+ );
170
+ return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
171
+ }
172
+
78
173
  function eraseFactoryCall(
79
174
  expression: ts.Expression,
80
175
  context: TransformationContext,
@@ -96,6 +191,10 @@ function eraseFactoryCall(
96
191
  if (name === undefined || fn === undefined) {
97
192
  continue;
98
193
  }
194
+ if (name === "init" && initReturnsValue(fn)) {
195
+ statements.push(...emitInitMerge(fn, context, property));
196
+ continue;
197
+ }
99
198
  const params: Identifier[] = fn.parameters
100
199
  .filter((param) => !isThisParameter(param) && ts.isIdentifier(param.name))
101
200
  .map((param) => context.transformExpression(param.name as ts.Identifier) as Identifier);
package/src/session.ts CHANGED
@@ -16,6 +16,9 @@ const requireFromHere = createRequire(import.meta.url);
16
16
  const COMPILER_OPTIONS: tstl.CompilerOptions = {
17
17
  luaTarget: tstl.LuaTarget.Lua54,
18
18
  sourceMap: true,
19
+ // Don't cross-check the seeded ambient .d.ts surface against itself; only user
20
+ // files matter (mirrors transpileProject and the editor's skipLibCheck).
21
+ skipLibCheck: true,
19
22
  luaPlugins: [{ plugin: lifecycleErasurePlugin }],
20
23
  };
21
24
 
package/src/transpile.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readdirSync, readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import type * as ts from "typescript";
@@ -47,24 +47,44 @@ function readAmbient(rel: string): string {
47
47
  return readFileSync(path.join(TYPES_PKG_ROOT, rel), "utf8");
48
48
  }
49
49
 
50
- export const AMBIENT_FILES: Readonly<Record<string, string>> = {
51
- "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(
52
- path.join(TSTL_LANG_EXT_ROOT, "index.d.ts"),
53
- "utf8",
54
- ),
55
- "node_modules/@defold-typescript/types/generated/vmath.d.ts": readAmbient("generated/vmath.d.ts"),
56
- "node_modules/@defold-typescript/types/generated/msg.d.ts": readAmbient("generated/msg.d.ts"),
57
- "node_modules/@defold-typescript/types/generated/go.d.ts": readAmbient("generated/go.d.ts"),
58
- "node_modules/@defold-typescript/types/generated/builtin-messages.d.ts": readAmbient(
59
- "generated/builtin-messages.d.ts",
60
- ),
61
- "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
62
- "node_modules/@defold-typescript/types/src/msg-overloads.d.ts":
63
- readAmbient("src/msg-overloads.d.ts"),
64
- "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
65
- "node_modules/@defold-typescript/types/index.ts":
66
- 'export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";\n',
67
- };
50
+ function buildAmbientFiles(): Record<string, string> {
51
+ const files: Record<string, string> = {
52
+ "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(
53
+ path.join(TSTL_LANG_EXT_ROOT, "index.d.ts"),
54
+ "utf8",
55
+ ),
56
+ "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
57
+ "node_modules/@defold-typescript/types/src/msg-overloads.d.ts":
58
+ readAmbient("src/msg-overloads.d.ts"),
59
+ "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
60
+ // Mirror the consumer-facing re-exports of the real package index so a user
61
+ // file can `import { defineScript }` and `import type { Hash, Vector3 }`.
62
+ "node_modules/@defold-typescript/types/index.ts": [
63
+ 'export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";',
64
+ 'export type { GuiScriptHooks, InputAction, InputTouch, RenderScriptHooks, ScriptHooks } from "./src/lifecycle";',
65
+ 'export type { Hash, Matrix4, Quaternion, Url, Vector, Vector3, Vector4 } from "./src/core-types";',
66
+ "",
67
+ ].join("\n"),
68
+ // Per-kind subpath entrypoints exist as package exports for the editor.
69
+ // Their namespaces are already seeded ambiently below, so the transpiler
70
+ // only needs the specifiers to resolve — an empty module is enough.
71
+ "node_modules/@defold-typescript/types/script.d.ts": "export {};\n",
72
+ "node_modules/@defold-typescript/types/gui-script.d.ts": "export {};\n",
73
+ "node_modules/@defold-typescript/types/render-script.d.ts": "export {};\n",
74
+ };
75
+ // Seed every generated namespace so real multi-namespace user code (sprite,
76
+ // physics, label, ...) resolves — not just the historical vmath/msg/go subset.
77
+ for (const entry of readdirSync(path.join(TYPES_PKG_ROOT, "generated"))) {
78
+ if (entry.endsWith(".d.ts")) {
79
+ files[`node_modules/@defold-typescript/types/generated/${entry}`] = readAmbient(
80
+ `generated/${entry}`,
81
+ );
82
+ }
83
+ }
84
+ return files;
85
+ }
86
+
87
+ export const AMBIENT_FILES: Readonly<Record<string, string>> = buildAmbientFiles();
68
88
 
69
89
  interface CollectableFile {
70
90
  readonly sourceFiles: readonly ts.SourceFile[];
@@ -108,6 +128,9 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
108
128
  const result = tstl.transpileVirtualProject(merged, {
109
129
  luaTarget: tstl.LuaTarget.Lua54,
110
130
  sourceMap: true,
131
+ // Don't cross-check the seeded ambient .d.ts surface against itself; we only
132
+ // care about diagnostics on user files (mirrors the editor's skipLibCheck).
133
+ skipLibCheck: true,
111
134
  luaPlugins: [{ plugin: lifecycleErasurePlugin }],
112
135
  });
113
136