ttsc 0.7.3 → 0.8.0-dev.20260505

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +3 -4
  2. package/driver/rewrite.go +41 -0
  3. package/lib/compiler/internal/runBuild.js +47 -49
  4. package/lib/compiler/internal/runBuild.js.map +1 -1
  5. package/lib/compiler/internal/transformProjectInMemory.js +44 -4
  6. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  7. package/lib/launcher/internal/runTtsc.js +19 -1
  8. package/lib/launcher/internal/runTtsc.js.map +1 -1
  9. package/lib/plugin/internal/buildSourcePlugin.js +6 -0
  10. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  11. package/lib/plugin/internal/loadProjectPlugins.js +55 -2
  12. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  13. package/lib/structures/ITtscPlugin.d.ts +19 -3
  14. package/lib/structures/ITtscPlugin.js +4 -0
  15. package/lib/structures/ITtscPlugin.js.map +1 -1
  16. package/lib/structures/TtscPluginStage.d.ts +4 -10
  17. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  18. package/package.json +11 -10
  19. package/shim/ast/lint.go +1 -0
  20. package/shim/ast/shim.go +1 -0
  21. package/shim/compiler/emit_hooks.go +494 -0
  22. package/shim/core/shim.go +3 -0
  23. package/src/compiler/internal/runBuild.ts +54 -85
  24. package/src/compiler/internal/transformProjectInMemory.ts +51 -6
  25. package/src/launcher/internal/runTtsc.ts +17 -1
  26. package/src/plugin/internal/buildSourcePlugin.ts +23 -0
  27. package/src/plugin/internal/loadProjectPlugins.ts +81 -2
  28. package/src/structures/ITtscPlugin.ts +30 -3
  29. package/src/structures/TtscPluginStage.ts +4 -10
  30. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  31. package/test/compiler.test.ts +3 -3
  32. package/test/tsgo.test.ts +9 -6
  33. package/utility/host.go +707 -0
@@ -118,7 +118,7 @@ function transformProjectWithPlugins(
118
118
  if (transformers.length === 0) {
119
119
  return transformProjectWithNativeHost(options, project);
120
120
  }
121
- assertSingleTransformHost(transformers);
121
+ assertTransformHostCompatibility(transformers);
122
122
 
123
123
  const plugin = transformers[0]!;
124
124
  const res = spawnNative(
@@ -218,21 +218,66 @@ function serializeNativePlugins(
218
218
  return JSON.stringify(
219
219
  plugins.map((plugin) => ({
220
220
  config: plugin.config,
221
+ hooks: plugin.hooks,
221
222
  name: plugin.name,
222
223
  stage: plugin.stage,
223
224
  })),
224
225
  );
225
226
  }
226
227
 
227
- function assertSingleTransformHost(
228
+ function assertTransformHostCompatibility(
228
229
  plugins: readonly ITtscLoadedNativePlugin[],
229
230
  ): void {
230
231
  const binaries = [...new Set(plugins.map((plugin) => plugin.binary))];
231
- if (binaries.length > 1) {
232
- throw new Error(
233
- "ttsc: multiple transform native backends cannot share one source-to-source pass",
234
- );
232
+ if (binaries.length <= 1) {
233
+ return;
234
+ }
235
+ if (plugins.every(isFirstPartyUtilityTransformPlugin)) {
236
+ return;
237
+ }
238
+ throw new Error(
239
+ "ttsc: multiple transform native backends cannot share one source-to-source pass; " +
240
+ "compose transform hook libraries through one aggregate host",
241
+ );
242
+ }
243
+
244
+ function isFirstPartyUtilityTransformPlugin(
245
+ plugin: ITtscLoadedNativePlugin,
246
+ ): boolean {
247
+ if (plugin.stage !== "transform") return false;
248
+ if (!firstPartyUtilityPluginNames.has(plugin.name)) return false;
249
+ const manifest = readNearestPackageManifest(plugin.source);
250
+ return manifest?.name === plugin.name;
251
+ }
252
+
253
+ const firstPartyUtilityPluginNames = new Set([
254
+ "@ttsc/banner",
255
+ "@ttsc/paths",
256
+ "@ttsc/strip",
257
+ ]);
258
+
259
+ function readNearestPackageManifest(
260
+ source: string,
261
+ ): { name?: unknown } | undefined {
262
+ try {
263
+ let current = fs.statSync(source).isDirectory()
264
+ ? source
265
+ : path.dirname(source);
266
+ for (let i = 0; i < 4; i += 1) {
267
+ const manifest = path.join(current, "package.json");
268
+ if (fs.existsSync(manifest)) {
269
+ return JSON.parse(fs.readFileSync(manifest, "utf8")) as {
270
+ name?: unknown;
271
+ };
272
+ }
273
+ const parent = path.dirname(current);
274
+ if (parent === current) break;
275
+ current = parent;
276
+ }
277
+ } catch {
278
+ return undefined;
235
279
  }
280
+ return undefined;
236
281
  }
237
282
 
238
283
  function appendBuildResult(
@@ -89,6 +89,7 @@ function runPrepare(argv: readonly string[]): number {
89
89
  const options = parseProjectArgs(argv);
90
90
  const cwd = path.resolve(options.cwd ?? process.cwd());
91
91
  const compiler = new TtscCompiler({
92
+ cacheDir: options.cacheDir,
92
93
  cwd,
93
94
  tsconfig: options.tsconfig,
94
95
  });
@@ -118,6 +119,7 @@ function runClean(argv: readonly string[]): number {
118
119
  const targets = [
119
120
  path.join(projectRoot, "node_modules", ".ttsc"),
120
121
  path.join(projectRoot, ".ttsc"),
122
+ ...(options.cacheDir ? [path.resolve(cwd, options.cacheDir)] : []),
121
123
  ...(process.env.TTSC_CACHE_DIR
122
124
  ? [path.resolve(process.env.TTSC_CACHE_DIR, "plugins")]
123
125
  : []),
@@ -204,6 +206,7 @@ function ensureExecutable(binary: string): void {
204
206
  }
205
207
 
206
208
  function parseProjectArgs(argv: readonly string[]) {
209
+ let cacheDir: string | undefined;
207
210
  let cwd: string | undefined;
208
211
  let tsconfig: string | undefined;
209
212
 
@@ -214,6 +217,9 @@ function parseProjectArgs(argv: readonly string[]) {
214
217
  case "--cwd":
215
218
  cwd = takeValue(current, rest);
216
219
  break;
220
+ case "--cache-dir":
221
+ cacheDir = takeValue(current, rest);
222
+ break;
217
223
  case "-p":
218
224
  case "--tsconfig":
219
225
  case "--project":
@@ -222,6 +228,8 @@ function parseProjectArgs(argv: readonly string[]) {
222
228
  default:
223
229
  if (current.startsWith("--cwd=")) {
224
230
  cwd = current.slice("--cwd=".length);
231
+ } else if (current.startsWith("--cache-dir=")) {
232
+ cacheDir = current.slice("--cache-dir=".length);
225
233
  } else if (current.startsWith("--tsconfig=")) {
226
234
  tsconfig = current.slice("--tsconfig=".length);
227
235
  } else if (current.startsWith("--project=")) {
@@ -232,11 +240,12 @@ function parseProjectArgs(argv: readonly string[]) {
232
240
  break;
233
241
  }
234
242
  }
235
- return { cwd, tsconfig };
243
+ return { cacheDir, cwd, tsconfig };
236
244
  }
237
245
 
238
246
  function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
239
247
  let binary: string | undefined;
248
+ let cacheDir: string | undefined;
240
249
  let cwd: string | undefined;
241
250
  let emit: boolean | undefined = checkOnly ? false : undefined;
242
251
  const files: string[] = [];
@@ -283,6 +292,9 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
283
292
  case "--binary":
284
293
  binary = takeValue(current, rest);
285
294
  break;
295
+ case "--cache-dir":
296
+ cacheDir = takeValue(current, rest);
297
+ break;
286
298
  default:
287
299
  if (current.startsWith("--cwd=")) {
288
300
  cwd = current.slice("--cwd=".length);
@@ -299,6 +311,8 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
299
311
  current.slice("--preserveWatchOutput=".length) !== "false";
300
312
  } else if (current.startsWith("--binary=")) {
301
313
  binary = current.slice("--binary=".length);
314
+ } else if (current.startsWith("--cache-dir=")) {
315
+ cacheDir = current.slice("--cache-dir=".length);
302
316
  } else if (current === "--verbose") {
303
317
  quiet = false;
304
318
  } else if (current.startsWith("-")) {
@@ -311,6 +325,7 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
311
325
  }
312
326
  return {
313
327
  binary,
328
+ cacheDir,
314
329
  cwd,
315
330
  emit,
316
331
  files,
@@ -349,6 +364,7 @@ function printHelp(): void {
349
364
  " --quiet Keep build output quiet (default)",
350
365
  " --verbose Print the build summary and emitted files",
351
366
  " --binary <path> Use an explicit tsgo binary",
367
+ " --cache-dir <dir> Use this cache root for source-plugin builds",
352
368
  "",
353
369
  "Plugin contract:",
354
370
  " ttsc reads compilerOptions.plugins from tsconfig.json.",
@@ -348,6 +348,20 @@ function resolveGoCompiler(): string {
348
348
  /* fall through */
349
349
  }
350
350
 
351
+ const platformPackage = path.resolve(
352
+ __dirname,
353
+ "..",
354
+ "..",
355
+ "..",
356
+ "..",
357
+ `ttsc-${process.platform}-${process.arch}`,
358
+ "bin",
359
+ "go",
360
+ "bin",
361
+ process.platform === "win32" ? "go.exe" : "go",
362
+ );
363
+ if (fs.existsSync(platformPackage)) return platformPackage;
364
+
351
365
  const local = path.resolve(
352
366
  __dirname,
353
367
  "..",
@@ -361,6 +375,15 @@ function resolveGoCompiler(): string {
361
375
  );
362
376
  if (fs.existsSync(local)) return local;
363
377
 
378
+ const homeSdk = path.join(
379
+ process.env.HOME ?? "",
380
+ "go-sdk",
381
+ "go",
382
+ "bin",
383
+ process.platform === "win32" ? "go.exe" : "go",
384
+ );
385
+ if (fs.existsSync(homeSdk)) return homeSdk;
386
+
364
387
  return "go";
365
388
  }
366
389
 
@@ -58,7 +58,9 @@ export function loadProjectPlugins(options: {
58
58
  const ttscVersion = readTtscVersion();
59
59
  const tsgoVersion = readTsgoVersion(context.projectRoot);
60
60
  plugins.forEach((plugin, index) => {
61
+ const stage = resolvePluginStage(plugin);
61
62
  validatePluginSource(plugin);
63
+ validatePluginHooks(plugin, stage);
62
64
  const binary = buildSourcePlugin({
63
65
  baseDir: context.projectRoot,
64
66
  cacheDir: options.cacheDir,
@@ -70,8 +72,10 @@ export function loadProjectPlugins(options: {
70
72
  nativePlugins.push({
71
73
  binary,
72
74
  config: entries[index]!,
75
+ hooks: plugin.hooks,
73
76
  name: plugin.name,
74
- stage: resolvePluginStage(plugin),
77
+ source: plugin.source,
78
+ stage,
75
79
  });
76
80
  });
77
81
  return {
@@ -88,6 +92,7 @@ function loadPluginEntry(
88
92
  if (typeof specifier !== "string" || specifier.length === 0) {
89
93
  throw new Error(`ttsc: plugin entry is missing a string "transform" field`);
90
94
  }
95
+ rejectUserPhaseOptions(specifier, entry);
91
96
 
92
97
  const request = resolvePluginRequest(specifier, context.projectRoot);
93
98
  const mod = require(request) as {
@@ -135,11 +140,37 @@ function rejectJsTransformHooks(specifier: string, candidate: object): void {
135
140
  }
136
141
  }
137
142
 
143
+ function rejectUserPhaseOptions(
144
+ specifier: string,
145
+ entry: ITtscProjectPluginConfig,
146
+ ): void {
147
+ for (const key of [
148
+ "after",
149
+ "afterDeclarations",
150
+ "before",
151
+ "phase",
152
+ "source:after",
153
+ ]) {
154
+ if (key in entry) {
155
+ throw new Error(
156
+ `ttsc: plugin entry for "${specifier}" uses unsupported ts-patch option ${JSON.stringify(key)}; ` +
157
+ "ttsc plugins declare source/declaration hooks in the package descriptor, not in user tsconfig",
158
+ );
159
+ }
160
+ }
161
+ }
162
+
138
163
  function resolvePluginStage(plugin: ITtscPlugin): TtscPluginStage {
139
164
  if (plugin.stage === undefined) {
140
165
  return "transform";
141
166
  }
142
167
  if (!isPluginStage(plugin.stage)) {
168
+ if (plugin.stage === "output") {
169
+ throw new Error(
170
+ `ttsc: plugin "${plugin.name}" requested removed stage "output"; ` +
171
+ "upgrade the plugin to a transform hook descriptor compatible with this ttsc version",
172
+ );
173
+ }
143
174
  throw new Error(
144
175
  `ttsc: plugin "${plugin.name}" requested unsupported stage ${JSON.stringify(plugin.stage)}`,
145
176
  );
@@ -153,8 +184,56 @@ function validatePluginSource(plugin: ITtscPlugin): void {
153
184
  }
154
185
  }
155
186
 
187
+ function validatePluginHooks(
188
+ plugin: ITtscPlugin,
189
+ stage: TtscPluginStage,
190
+ ): void {
191
+ const hooks = plugin.hooks;
192
+ if (hooks !== undefined && typeof hooks !== "object") {
193
+ throw new Error(`ttsc: plugin "${plugin.name}" hooks must be an object`);
194
+ }
195
+ if (stage === "check") {
196
+ if (hooks !== undefined && Object.keys(hooks).length !== 0) {
197
+ throw new Error(
198
+ `ttsc: check plugin "${plugin.name}" must not declare transform hooks`,
199
+ );
200
+ }
201
+ return;
202
+ }
203
+ if (hooks === undefined) {
204
+ throw new Error(
205
+ `ttsc: transform plugin "${plugin.name}" must declare source/declaration hooks in its package descriptor`,
206
+ );
207
+ }
208
+ for (const key of Object.keys(hooks)) {
209
+ if (key !== "source" && key !== "declaration") {
210
+ throw new Error(
211
+ `ttsc: plugin "${plugin.name}" requested unsupported hook ${JSON.stringify(key)}`,
212
+ );
213
+ }
214
+ }
215
+ if (hooks.source !== undefined && typeof hooks.source !== "boolean") {
216
+ throw new Error(
217
+ `ttsc: plugin "${plugin.name}" hooks.source must be boolean`,
218
+ );
219
+ }
220
+ if (
221
+ hooks.declaration !== undefined &&
222
+ typeof hooks.declaration !== "boolean"
223
+ ) {
224
+ throw new Error(
225
+ `ttsc: plugin "${plugin.name}" hooks.declaration must be boolean`,
226
+ );
227
+ }
228
+ if (hooks.source !== true && hooks.declaration !== true) {
229
+ throw new Error(
230
+ `ttsc: transform plugin "${plugin.name}" must enable at least one source/declaration hook`,
231
+ );
232
+ }
233
+ }
234
+
156
235
  function isPluginStage(value: string): value is TtscPluginStage {
157
- return value === "transform" || value === "check" || value === "output";
236
+ return value === "transform" || value === "check";
158
237
  }
159
238
 
160
239
  function resolvePluginRequest(specifier: string, projectRoot: string): string {
@@ -1,5 +1,20 @@
1
1
  import type { TtscPluginStage } from "./TtscPluginStage";
2
2
 
3
+ export interface ITtscPluginHooks {
4
+ /**
5
+ * Runs against TypeScript SourceFile AST before TypeScript-Go's built-in emit
6
+ * transforms. The Program/checker are created from the original source before
7
+ * this hook participates in emit.
8
+ */
9
+ source?: boolean;
10
+
11
+ /**
12
+ * Runs against declaration AST before declaration printing. This is a
13
+ * plugin-package capability, never a user-facing tsconfig phase option.
14
+ */
15
+ declaration?: boolean;
16
+ }
17
+
3
18
  /**
4
19
  * Runtime descriptor returned by a ttsc plugin module.
5
20
  *
@@ -48,11 +63,23 @@ export interface ITtscPlugin {
48
63
  /**
49
64
  * Pipeline stage implemented by the sidecar.
50
65
  *
51
- * Omit this field for normal compiler-transform plugins. Explicit stages are
52
- * intended for tools that only validate (`"check"`) or rewrite already
53
- * emitted files (`"output"`).
66
+ * Omit this field for normal compiler-transform plugins. The only explicit
67
+ * non-transform stage is `"check"`.
54
68
  *
55
69
  * @default "transform"
56
70
  */
57
71
  stage?: TtscPluginStage;
72
+
73
+ /** Native transform hook capabilities implemented by the plugin package. */
74
+ hooks?: ITtscPluginHooks;
75
+ }
76
+
77
+ export function defineTtscPlugin<T extends ITtscPlugin>(plugin: T): T;
78
+ export function defineTtscPlugin<TContext, T extends ITtscPlugin>(
79
+ factory: (context: TContext) => T,
80
+ ): (context: TContext) => T;
81
+ export function defineTtscPlugin(
82
+ value: ITtscPlugin | ((context: unknown) => ITtscPlugin),
83
+ ): ITtscPlugin | ((context: unknown) => ITtscPlugin) {
84
+ return value;
58
85
  }
@@ -1,18 +1,12 @@
1
1
  /**
2
2
  * Pipeline stage where a plugin's lazily built Go sidecar participates.
3
3
  *
4
- * - `"transform"`: owns the TypeScript transform/emit pass. This is the
5
- * default and is the right stage for compiler-transform plugins such as
6
- * typia-style source rewrites. During compilation, a transform sidecar
7
- * produces the project's emitted output. During
8
- * {@link TtscCompiler.transform}, the same sidecar may expose a
9
- * source-to-source `transform` command that returns transformed TypeScript
4
+ * - `"transform"`: participates in the TypeScript-Go transform path. Transform
5
+ * plugins declare their source/declaration hook capabilities in the plugin
6
+ * package descriptor and do not receive emitted JavaScript or emitted file
10
7
  * text.
11
8
  * - `"check"`: runs before emit and reports diagnostics only. Use this for
12
9
  * lint or validation plugins that should fail the compile before JavaScript
13
10
  * or declaration output is generated.
14
- * - `"output"`: runs after emit for each emitted output file. Use this for
15
- * post-processing plugins such as banner insertion, import-path rewriting,
16
- * or output stripping that operate on `.js`, `.d.ts`, or map files.
17
11
  */
18
- export type TtscPluginStage = "transform" | "check" | "output";
12
+ export type TtscPluginStage = "transform" | "check";
@@ -1,4 +1,5 @@
1
1
  import type { ITtscProjectPluginConfig } from "../ITtscProjectPluginConfig";
2
+ import type { ITtscPluginHooks } from "../ITtscPlugin";
2
3
  import type { TtscPluginStage } from "../TtscPluginStage";
3
4
 
4
5
  /** Native sidecar selected and built from one plugin descriptor. */
@@ -9,6 +10,10 @@ export interface ITtscLoadedNativePlugin {
9
10
  config: ITtscProjectPluginConfig;
10
11
  /** Stable plugin name used in diagnostics and native manifests. */
11
12
  name: string;
13
+ /** Go source directory selected by the plugin descriptor. */
14
+ source: string;
12
15
  /** Pipeline stage where the sidecar participates. */
13
16
  stage: TtscPluginStage;
17
+ /** Native transform hook capabilities declared by the plugin package. */
18
+ hooks?: ITtscPluginHooks;
14
19
  }
@@ -274,7 +274,7 @@ function createProject(options = {}) {
274
274
  function writeSourcePlugin(root) {
275
275
  fs.writeFileSync(
276
276
  path.join(root, "plugin.cjs"),
277
- 'module.exports = { name: "prepare-fixture", source: "./plugin-go" };\n',
277
+ 'module.exports = { name: "prepare-fixture", source: "./plugin-go", hooks: { source: true } };\n',
278
278
  "utf8",
279
279
  );
280
280
  fs.mkdirSync(path.join(root, "plugin-go"), { recursive: true });
@@ -293,7 +293,7 @@ function writeSourcePlugin(root) {
293
293
  function writeBrokenTransformPlugin(root) {
294
294
  fs.writeFileSync(
295
295
  path.join(root, "plugin.cjs"),
296
- 'module.exports = { name: "broken-transform-fixture", source: "./plugin-go" };\n',
296
+ 'module.exports = { name: "broken-transform-fixture", source: "./plugin-go", hooks: { source: true } };\n',
297
297
  "utf8",
298
298
  );
299
299
  fs.mkdirSync(path.join(root, "plugin-go"), { recursive: true });
@@ -327,7 +327,7 @@ function writeBrokenTransformPlugin(root) {
327
327
  function writeCompilerPlugin(root) {
328
328
  fs.writeFileSync(
329
329
  path.join(root, "plugin.cjs"),
330
- 'module.exports = { name: "compile-fixture", source: "./plugin-go" };\n',
330
+ 'module.exports = { name: "compile-fixture", source: "./plugin-go", hooks: { source: true } };\n',
331
331
  "utf8",
332
332
  );
333
333
  fs.mkdirSync(path.join(root, "plugin-go"), { recursive: true });
package/test/tsgo.test.ts CHANGED
@@ -30,7 +30,7 @@ test("resolveTsgo resolves the consumer native-preview platform package", () =>
30
30
  root,
31
31
  "node_modules",
32
32
  "@typescript",
33
- "native-preview-linux-x64",
33
+ `native-preview-${process.platform}-${process.arch}`,
34
34
  );
35
35
  fs.mkdirSync(nativeRoot, { recursive: true });
36
36
  fs.mkdirSync(path.join(platformRoot, "lib"), { recursive: true });
@@ -46,21 +46,24 @@ test("resolveTsgo resolves the consumer native-preview platform package", () =>
46
46
  fs.writeFileSync(
47
47
  path.join(platformRoot, "package.json"),
48
48
  JSON.stringify({
49
- name: "@typescript/native-preview-linux-x64",
49
+ name: `@typescript/native-preview-${process.platform}-${process.arch}`,
50
50
  version: "7.0.0-dev.consumer",
51
51
  }),
52
52
  "utf8",
53
53
  );
54
- fs.writeFileSync(path.join(platformRoot, "lib", "tsgo"), "", "utf8");
54
+ const binary = path.join(
55
+ platformRoot,
56
+ "lib",
57
+ process.platform === "win32" ? "tsgo.exe" : "tsgo",
58
+ );
59
+ fs.writeFileSync(binary, "", "utf8");
55
60
 
56
61
  const resolved = resolveTsgo({
57
- arch: "x64",
58
62
  cwd: root,
59
63
  env: {},
60
- platform: "linux",
61
64
  });
62
65
 
63
66
  assert.equal(resolved.version, "7.0.0-dev.consumer");
64
67
  assert.equal(resolved.gitHead, "abc123");
65
- assert.equal(resolved.binary, path.join(platformRoot, "lib", "tsgo"));
68
+ assert.equal(resolved.binary, binary);
66
69
  });