@defold-typescript/transpiler 0.5.4 → 0.6.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.
@@ -0,0 +1,3 @@
1
+ import * as ts from "typescript";
2
+ export declare const GO_PROPERTY_DIRECT_CALL_MESSAGE = "`go.property` called directly is deprecated; declare the property in `defineScript({ properties })` so it types onto `self` (the transpiler emits the registration for you).";
3
+ export declare function findDirectGoPropertyCalls(sourceFile: ts.SourceFile): ts.CallExpression[];
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync as readFileSync2 } from "node:fs";
3
3
  import { createRequire as createRequire2 } from "node:module";
4
4
  import * as path2 from "node:path";
5
- import * as ts4 from "typescript";
5
+ import * as ts5 from "typescript";
6
6
  import * as tstl2 from "typescript-to-lua";
7
7
 
8
8
  // src/lifecycle-erasure.ts
@@ -19,6 +19,7 @@ import {
19
19
  createIfStatement as createIfStatement2,
20
20
  createNilLiteral,
21
21
  createReturnStatement,
22
+ createStringLiteral as createStringLiteral2,
22
23
  createTableIndexExpression,
23
24
  createVariableDeclarationStatement as createVariableDeclarationStatement2,
24
25
  NodeFlags as NodeFlags2,
@@ -141,6 +142,12 @@ var messageDispatchLoweringPlugin = {
141
142
  // src/lifecycle-erasure.ts
142
143
  var FACTORY_MODULE = "@defold-typescript/types";
143
144
  var FACTORY_NAMES = new Set(["defineScript", "defineGuiScript", "defineRenderScript"]);
145
+ var FACTORY_SPECIFIERS = new Set([
146
+ FACTORY_MODULE,
147
+ `${FACTORY_MODULE}/script`,
148
+ `${FACTORY_MODULE}/gui-script`,
149
+ `${FACTORY_MODULE}/render-script`
150
+ ]);
144
151
  function resolvesToFactoryExport(callee, checker) {
145
152
  let symbol = checker.getSymbolAtLocation(callee);
146
153
  if (symbol === undefined) {
@@ -236,6 +243,23 @@ function emitInitMerge(fn, context, property) {
236
243
  const initFn = createFunctionExpression2(createBlock2([stateDecl, guard]), [createIdentifier2("self")], undefined, NodeFlags2.Declaration, fn);
237
244
  return [builderDecl, createAssignmentStatement(createIdentifier2("init"), initFn, property)];
238
245
  }
246
+ function emitPropertyRegistrations(property, context) {
247
+ if (!ts2.isPropertyAssignment(property) || !ts2.isObjectLiteralExpression(property.initializer)) {
248
+ return [];
249
+ }
250
+ const registrations = [];
251
+ for (const member of property.initializer.properties) {
252
+ if (!ts2.isPropertyAssignment(member)) {
253
+ continue;
254
+ }
255
+ const key = member.name;
256
+ if (!ts2.isIdentifier(key) && !ts2.isStringLiteral(key)) {
257
+ continue;
258
+ }
259
+ registrations.push(createExpressionStatement(createCallExpression2(createTableIndexExpression(createIdentifier2("go"), createStringLiteral2("property")), [createStringLiteral2(key.text), context.transformExpression(member.initializer)])));
260
+ }
261
+ return registrations;
262
+ }
239
263
  function eraseFactoryCall(expression, context) {
240
264
  if (!ts2.isCallExpression(expression)) {
241
265
  return;
@@ -247,12 +271,17 @@ function eraseFactoryCall(expression, context) {
247
271
  if (hooks === undefined || !ts2.isObjectLiteralExpression(hooks)) {
248
272
  return;
249
273
  }
274
+ const registrations = [];
250
275
  const statements = [];
251
276
  for (const property of hooks.properties) {
252
277
  const name = hookName(property);
253
278
  if (name === undefined) {
254
279
  continue;
255
280
  }
281
+ if (name === "properties") {
282
+ registrations.push(...emitPropertyRegistrations(property, context));
283
+ continue;
284
+ }
256
285
  const fn = hookFunction(property);
257
286
  if (fn === undefined) {
258
287
  const dispatch = dispatcherInitializer(property, context.checker);
@@ -269,10 +298,10 @@ function eraseFactoryCall(expression, context) {
269
298
  const fnExpression = createFunctionExpression2(createBlock2(transformHookBody(fn, context)), params, undefined, NodeFlags2.Declaration, fn);
270
299
  statements.push(createAssignmentStatement(createIdentifier2(name), fnExpression, property));
271
300
  }
272
- return statements;
301
+ return [...registrations, ...statements];
273
302
  }
274
303
  function isFactoryOnlyImport(node) {
275
- if (!ts2.isStringLiteral(node.moduleSpecifier) || node.moduleSpecifier.text !== FACTORY_MODULE) {
304
+ if (!ts2.isStringLiteral(node.moduleSpecifier) || !FACTORY_SPECIFIERS.has(node.moduleSpecifier.text)) {
276
305
  return false;
277
306
  }
278
307
  const clause = node.importClause;
@@ -347,6 +376,23 @@ import { readdirSync, readFileSync } from "node:fs";
347
376
  import { createRequire } from "node:module";
348
377
  import * as path from "node:path";
349
378
  import * as tstl from "typescript-to-lua";
379
+
380
+ // src/go-property-direct-call.ts
381
+ import * as ts4 from "typescript";
382
+ var GO_PROPERTY_DIRECT_CALL_MESSAGE = "`go.property` called directly is deprecated; declare the property in `defineScript({ properties })` so it types onto `self` (the transpiler emits the registration for you).";
383
+ function findDirectGoPropertyCalls(sourceFile) {
384
+ const calls = [];
385
+ const visit = (node) => {
386
+ if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts4.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
387
+ calls.push(node);
388
+ }
389
+ ts4.forEachChild(node, visit);
390
+ };
391
+ ts4.forEachChild(sourceFile, visit);
392
+ return calls;
393
+ }
394
+
395
+ // src/transpile.ts
350
396
  function flattenDiagnosticMessage(text) {
351
397
  if (typeof text === "string") {
352
398
  return text;
@@ -363,22 +409,27 @@ function buildAmbientFiles() {
363
409
  const files = {
364
410
  "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(path.join(TSTL_LANG_EXT_ROOT, "index.d.ts"), "utf8"),
365
411
  "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
412
+ "node_modules/@defold-typescript/types/src/engine-globals.d.ts": readAmbient("src/engine-globals.d.ts"),
413
+ "node_modules/@defold-typescript/types/src/go-overloads.d.ts": readAmbient("src/go-overloads.d.ts"),
366
414
  "node_modules/@defold-typescript/types/src/msg-overloads.d.ts": readAmbient("src/msg-overloads.d.ts"),
367
415
  "node_modules/@defold-typescript/types/src/message-guard.d.ts": readAmbient("src/message-guard.d.ts"),
368
416
  "node_modules/@defold-typescript/types/src/message-dispatch.d.ts": readAmbient("src/message-dispatch.d.ts"),
369
417
  "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
370
418
  "node_modules/@defold-typescript/types/index.ts": [
371
419
  'export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";',
372
- 'export type { GuiScriptHooks, InputAction, InputTouch, RenderScriptHooks, ScriptHooks } from "./src/lifecycle";',
420
+ 'export type { GuiScriptHooks, InputAction, InputTouch, RenderScriptHooks, ScriptHooks, ScriptProperties, ScriptProperty } from "./src/lifecycle";',
373
421
  'export type { Hash, Matrix4, Quaternion, Url, Vector, Vector3, Vector4 } from "./src/core-types";',
374
422
  ""
375
423
  ].join(`
376
424
  `),
377
- "node_modules/@defold-typescript/types/script.d.ts": `export {};
425
+ "node_modules/@defold-typescript/types/script.d.ts": `export { defineScript } from "./src/lifecycle.js";
426
+ export type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";
378
427
  `,
379
- "node_modules/@defold-typescript/types/gui-script.d.ts": `export {};
428
+ "node_modules/@defold-typescript/types/gui-script.d.ts": `export { defineGuiScript } from "./src/lifecycle.js";
429
+ export type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";
380
430
  `,
381
- "node_modules/@defold-typescript/types/render-script.d.ts": `export {};
431
+ "node_modules/@defold-typescript/types/render-script.d.ts": `export { defineRenderScript } from "./src/lifecycle.js";
432
+ export type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";
382
433
  `
383
434
  };
384
435
  for (const entry of readdirSync(path.join(TYPES_PKG_ROOT, "generated"))) {
@@ -407,6 +458,22 @@ function collectOutputs(transpiledFiles, diagnostics, userKeys) {
407
458
  const message = flattenDiagnosticMessage(d.messageText);
408
459
  return fileName !== undefined && userKeys.has(fileName) ? { file: fileName, message } : { message };
409
460
  });
461
+ const scanned = new Set;
462
+ for (const file of transpiledFiles) {
463
+ for (const sourceFile of file.sourceFiles) {
464
+ if (!userKeys.has(sourceFile.fileName) || scanned.has(sourceFile.fileName)) {
465
+ continue;
466
+ }
467
+ scanned.add(sourceFile.fileName);
468
+ if (findDirectGoPropertyCalls(sourceFile).length > 0) {
469
+ collectedDiagnostics.push({
470
+ file: sourceFile.fileName,
471
+ message: GO_PROPERTY_DIRECT_CALL_MESSAGE,
472
+ category: "warning"
473
+ });
474
+ }
475
+ }
476
+ }
410
477
  return { lua, sourceMaps, diagnostics: collectedDiagnostics };
411
478
  }
412
479
  function transpileProject(input) {
@@ -485,7 +552,7 @@ function createTranspileSession() {
485
552
  if (cached) {
486
553
  return cached;
487
554
  }
488
- const created = ts4.createSourceFile(fileName, content, ts4.ScriptTarget.Latest, false);
555
+ const created = ts5.createSourceFile(fileName, content, ts5.ScriptTarget.Latest, false);
489
556
  sourceFileCache.set(normalized, created);
490
557
  return created;
491
558
  }
@@ -504,7 +571,7 @@ function createTranspileSession() {
504
571
  return cached;
505
572
  }
506
573
  const fileContent = readFileSync2(filePath, "utf8");
507
- const created = ts4.createSourceFile(filePath, fileContent, ts4.ScriptTarget.Latest, false);
574
+ const created = ts5.createSourceFile(filePath, fileContent, ts5.ScriptTarget.Latest, false);
508
575
  libCache.set(fileName, created);
509
576
  return created;
510
577
  }
@@ -517,10 +584,10 @@ function createTranspileSession() {
517
584
  return AMBIENT_FILES[normalized];
518
585
  }
519
586
  const host = {
520
- fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts4.sys.fileExists(fileName),
587
+ fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts5.sys.fileExists(fileName),
521
588
  getCanonicalFileName: (fileName) => fileName,
522
589
  getCurrentDirectory: () => "",
523
- getDefaultLibFileName: ts4.getDefaultLibFileName,
590
+ getDefaultLibFileName: ts5.getDefaultLibFileName,
524
591
  readFile: () => "",
525
592
  getNewLine: () => `
526
593
  `,
@@ -541,15 +608,15 @@ function createTranspileSession() {
541
608
  }
542
609
  const userKeys = new Set(userFiles.keys());
543
610
  const rootNames = [...Object.keys(AMBIENT_FILES).map(normalizeSlashes), ...userKeys];
544
- program = ts4.createProgram(rootNames, COMPILER_OPTIONS, host, program);
545
- const preEmitDiagnostics = ts4.getPreEmitDiagnostics(program);
611
+ program = ts5.createProgram(rootNames, COMPILER_OPTIONS, host, program);
612
+ const preEmitDiagnostics = ts5.getPreEmitDiagnostics(program);
546
613
  const collector = createOutputCollector();
547
614
  const { diagnostics: transpileDiagnostics } = new tstl2.Transpiler().emit({
548
615
  program,
549
616
  writeFile: collector.writeFile
550
617
  });
551
618
  const diagnostics = [
552
- ...ts4.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
619
+ ...ts5.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
553
620
  ];
554
621
  return collectOutputs(collector.files, diagnostics, userKeys);
555
622
  }
@@ -1,2 +1,4 @@
1
+ import * as ts from "typescript";
1
2
  import { type Plugin } from "typescript-to-lua";
3
+ export declare function isFactoryOnlyImport(node: ts.ImportDeclaration): boolean;
2
4
  export declare const lifecycleErasurePlugin: Plugin;
@@ -7,6 +7,7 @@ export interface TranspileResult {
7
7
  export interface TranspileDiagnostic {
8
8
  readonly file?: string;
9
9
  readonly message: string;
10
+ readonly category?: "warning";
10
11
  }
11
12
  export interface TranspileProjectInput {
12
13
  readonly files: Readonly<Record<string, string>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
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.5.4",
31
+ "@defold-typescript/types": "0.6.0",
32
32
  "@typescript-to-lua/language-extensions": "1.19.0",
33
33
  "typescript-to-lua": "^1.36.0"
34
34
  }
@@ -0,0 +1,26 @@
1
+ import * as ts from "typescript";
2
+
3
+ export const GO_PROPERTY_DIRECT_CALL_MESSAGE =
4
+ "`go.property` called directly is deprecated; declare the property in `defineScript({ properties })` so it types onto `self` (the transpiler emits the registration for you).";
5
+
6
+ // Match a user-source `go.property(...)` call — a `property` access on the `go`
7
+ // identifier. The blessed `properties`-field form never produces such a call;
8
+ // the synthesized registrations live only in emitted Lua, which this scan never
9
+ // sees (it walks the user TypeScript AST).
10
+ export function findDirectGoPropertyCalls(sourceFile: ts.SourceFile): ts.CallExpression[] {
11
+ const calls: ts.CallExpression[] = [];
12
+ const visit = (node: ts.Node): void => {
13
+ if (
14
+ ts.isCallExpression(node) &&
15
+ ts.isPropertyAccessExpression(node.expression) &&
16
+ node.expression.name.text === "property" &&
17
+ ts.isIdentifier(node.expression.expression) &&
18
+ node.expression.expression.text === "go"
19
+ ) {
20
+ calls.push(node);
21
+ }
22
+ ts.forEachChild(node, visit);
23
+ };
24
+ ts.forEachChild(sourceFile, visit);
25
+ return calls;
26
+ }
@@ -11,6 +11,7 @@ import {
11
11
  createIfStatement,
12
12
  createNilLiteral,
13
13
  createReturnStatement,
14
+ createStringLiteral,
14
15
  createTableIndexExpression,
15
16
  createVariableDeclarationStatement,
16
17
  type Identifier,
@@ -24,6 +25,15 @@ import { resolvesToDispatchExport } from "./message-dispatch-lowering";
24
25
 
25
26
  const FACTORY_MODULE = "@defold-typescript/types";
26
27
  const FACTORY_NAMES = new Set(["defineScript", "defineGuiScript", "defineRenderScript"]);
28
+ // A walled source imports its factory from the matching kind subpath
29
+ // (`@defold-typescript/types/gui-script`) to avoid pulling the cross-kind
30
+ // `declare global` namespaces; those imports erase exactly like the bare one.
31
+ const FACTORY_SPECIFIERS = new Set([
32
+ FACTORY_MODULE,
33
+ `${FACTORY_MODULE}/script`,
34
+ `${FACTORY_MODULE}/gui-script`,
35
+ `${FACTORY_MODULE}/render-script`,
36
+ ]);
27
37
 
28
38
  function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
29
39
  let symbol = checker.getSymbolAtLocation(callee);
@@ -185,6 +195,38 @@ function emitInitMerge(
185
195
  return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
186
196
  }
187
197
 
198
+ // The value-keyed `properties` field maps each `key: default` to a chunk-scope
199
+ // `go.property("key", default)` the editor reads. Emitted before the hook
200
+ // assignments and after the source's `local` consts, so a default referencing a
201
+ // module const resolves.
202
+ function emitPropertyRegistrations(
203
+ property: ts.ObjectLiteralElementLike,
204
+ context: TransformationContext,
205
+ ): Statement[] {
206
+ if (!ts.isPropertyAssignment(property) || !ts.isObjectLiteralExpression(property.initializer)) {
207
+ return [];
208
+ }
209
+ const registrations: Statement[] = [];
210
+ for (const member of property.initializer.properties) {
211
+ if (!ts.isPropertyAssignment(member)) {
212
+ continue;
213
+ }
214
+ const key = member.name;
215
+ if (!ts.isIdentifier(key) && !ts.isStringLiteral(key)) {
216
+ continue;
217
+ }
218
+ registrations.push(
219
+ createExpressionStatement(
220
+ createCallExpression(
221
+ createTableIndexExpression(createIdentifier("go"), createStringLiteral("property")),
222
+ [createStringLiteral(key.text), context.transformExpression(member.initializer)],
223
+ ),
224
+ ),
225
+ );
226
+ }
227
+ return registrations;
228
+ }
229
+
188
230
  function eraseFactoryCall(
189
231
  expression: ts.Expression,
190
232
  context: TransformationContext,
@@ -199,12 +241,17 @@ function eraseFactoryCall(
199
241
  if (hooks === undefined || !ts.isObjectLiteralExpression(hooks)) {
200
242
  return undefined;
201
243
  }
244
+ const registrations: Statement[] = [];
202
245
  const statements: Statement[] = [];
203
246
  for (const property of hooks.properties) {
204
247
  const name = hookName(property);
205
248
  if (name === undefined) {
206
249
  continue;
207
250
  }
251
+ if (name === "properties") {
252
+ registrations.push(...emitPropertyRegistrations(property, context));
253
+ continue;
254
+ }
208
255
  const fn = hookFunction(property);
209
256
  if (fn === undefined) {
210
257
  // A hook whose initializer is a recognized `onMessage({...})` call: let it
@@ -238,11 +285,14 @@ function eraseFactoryCall(
238
285
  );
239
286
  statements.push(createAssignmentStatement(createIdentifier(name), fnExpression, property));
240
287
  }
241
- return statements;
288
+ return [...registrations, ...statements];
242
289
  }
243
290
 
244
- function isFactoryOnlyImport(node: ts.ImportDeclaration): boolean {
245
- if (!ts.isStringLiteral(node.moduleSpecifier) || node.moduleSpecifier.text !== FACTORY_MODULE) {
291
+ export function isFactoryOnlyImport(node: ts.ImportDeclaration): boolean {
292
+ if (
293
+ !ts.isStringLiteral(node.moduleSpecifier) ||
294
+ !FACTORY_SPECIFIERS.has(node.moduleSpecifier.text)
295
+ ) {
246
296
  return false;
247
297
  }
248
298
  const clause = node.importClause;
package/src/transpile.ts CHANGED
@@ -3,6 +3,10 @@ import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import type * as ts from "typescript";
5
5
  import * as tstl from "typescript-to-lua";
6
+ import {
7
+ findDirectGoPropertyCalls,
8
+ GO_PROPERTY_DIRECT_CALL_MESSAGE,
9
+ } from "./go-property-direct-call";
6
10
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
7
11
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
8
12
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
@@ -16,6 +20,10 @@ export interface TranspileResult {
16
20
  export interface TranspileDiagnostic {
17
21
  readonly file?: string;
18
22
  readonly message: string;
23
+ // Present only on advisory diagnostics (e.g. the deprecated direct
24
+ // `go.property` call). Absent means a hard failure, so `collectFailures`
25
+ // keeps treating uncategorized diagnostics as fatal.
26
+ readonly category?: "warning";
19
27
  }
20
28
 
21
29
  export interface TranspileProjectInput {
@@ -56,6 +64,10 @@ function buildAmbientFiles(): Record<string, string> {
56
64
  "utf8",
57
65
  ),
58
66
  "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
67
+ "node_modules/@defold-typescript/types/src/engine-globals.d.ts":
68
+ readAmbient("src/engine-globals.d.ts"),
69
+ "node_modules/@defold-typescript/types/src/go-overloads.d.ts":
70
+ readAmbient("src/go-overloads.d.ts"),
59
71
  "node_modules/@defold-typescript/types/src/msg-overloads.d.ts":
60
72
  readAmbient("src/msg-overloads.d.ts"),
61
73
  "node_modules/@defold-typescript/types/src/message-guard.d.ts":
@@ -68,16 +80,22 @@ function buildAmbientFiles(): Record<string, string> {
68
80
  // file can `import { defineScript }` and `import type { Hash, Vector3 }`.
69
81
  "node_modules/@defold-typescript/types/index.ts": [
70
82
  'export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";',
71
- 'export type { GuiScriptHooks, InputAction, InputTouch, RenderScriptHooks, ScriptHooks } from "./src/lifecycle";',
83
+ 'export type { GuiScriptHooks, InputAction, InputTouch, RenderScriptHooks, ScriptHooks, ScriptProperties, ScriptProperty } from "./src/lifecycle";',
72
84
  'export type { Hash, Matrix4, Quaternion, Url, Vector, Vector3, Vector4 } from "./src/core-types";',
73
85
  "",
74
86
  ].join("\n"),
75
87
  // Per-kind subpath entrypoints exist as package exports for the editor.
76
- // Their namespaces are already seeded ambiently below, so the transpiler
77
- // only needs the specifiers to resolve an empty module is enough.
78
- "node_modules/@defold-typescript/types/script.d.ts": "export {};\n",
79
- "node_modules/@defold-typescript/types/gui-script.d.ts": "export {};\n",
80
- "node_modules/@defold-typescript/types/render-script.d.ts": "export {};\n",
88
+ // Their namespaces are already seeded ambiently below; mirror slice A's
89
+ // generated `generateKindIndex` output by re-exporting only the matching
90
+ // factory, so a walled source's subpath import resolves to it and the
91
+ // call-site erasure fires (otherwise the import lowers to a broken
92
+ // `require("@defold-typescript/types/gui-script")`).
93
+ "node_modules/@defold-typescript/types/script.d.ts":
94
+ 'export { defineScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
95
+ "node_modules/@defold-typescript/types/gui-script.d.ts":
96
+ 'export { defineGuiScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
97
+ "node_modules/@defold-typescript/types/render-script.d.ts":
98
+ 'export { defineRenderScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
81
99
  };
82
100
  // Seed every generated namespace so real multi-namespace user code (sprite,
83
101
  // physics, label, ...) resolves — not just the historical vmath/msg/go subset.
@@ -125,6 +143,26 @@ export function collectOutputs(
125
143
  : { message };
126
144
  });
127
145
 
146
+ // Advisory scan of the user TypeScript AST for the deprecated direct
147
+ // `go.property` call. Run here (shared by transpileProject and the watch
148
+ // session) so both paths report it identically.
149
+ const scanned = new Set<string>();
150
+ for (const file of transpiledFiles) {
151
+ for (const sourceFile of file.sourceFiles) {
152
+ if (!userKeys.has(sourceFile.fileName) || scanned.has(sourceFile.fileName)) {
153
+ continue;
154
+ }
155
+ scanned.add(sourceFile.fileName);
156
+ if (findDirectGoPropertyCalls(sourceFile).length > 0) {
157
+ collectedDiagnostics.push({
158
+ file: sourceFile.fileName,
159
+ message: GO_PROPERTY_DIRECT_CALL_MESSAGE,
160
+ category: "warning",
161
+ });
162
+ }
163
+ }
164
+ }
165
+
128
166
  return { lua, sourceMaps, diagnostics: collectedDiagnostics };
129
167
  }
130
168