ttsc 0.7.3 → 0.8.0-dev.20260506

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/cmd/ttsc/main.go +1 -1
  3. package/driver/program.go +56 -3
  4. package/driver/rewrite.go +22 -2
  5. package/lib/compiler/internal/runBuild.js +46 -49
  6. package/lib/compiler/internal/runBuild.js.map +1 -1
  7. package/lib/compiler/internal/transformProjectInMemory.js +43 -4
  8. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  9. package/lib/launcher/internal/runTtsc.js +20 -2
  10. package/lib/launcher/internal/runTtsc.js.map +1 -1
  11. package/lib/plugin/internal/buildSourcePlugin.js +6 -0
  12. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  13. package/lib/plugin/internal/loadProjectPlugins.js +12 -6
  14. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  15. package/lib/structures/ITtscPlugin.d.ts +4 -3
  16. package/lib/structures/ITtscPlugin.js +4 -0
  17. package/lib/structures/ITtscPlugin.js.map +1 -1
  18. package/lib/structures/TtscPluginStage.d.ts +3 -11
  19. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +2 -0
  20. package/package.json +11 -10
  21. package/shim/ast/lint.go +1 -0
  22. package/shim/ast/shim.go +5 -4
  23. package/shim/core/shim.go +3 -0
  24. package/src/compiler/internal/runBuild.ts +53 -85
  25. package/src/compiler/internal/transformProjectInMemory.ts +50 -6
  26. package/src/launcher/internal/runTtsc.ts +18 -2
  27. package/src/plugin/internal/buildSourcePlugin.ts +23 -0
  28. package/src/plugin/internal/loadProjectPlugins.ts +17 -6
  29. package/src/structures/ITtscPlugin.ts +12 -3
  30. package/src/structures/TtscPluginStage.ts +3 -11
  31. package/src/structures/internal/ITtscLoadedNativePlugin.ts +2 -0
  32. package/test/tsgo.test.ts +9 -6
  33. package/utility/host.go +735 -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(
@@ -224,15 +224,59 @@ function serializeNativePlugins(
224
224
  );
225
225
  }
226
226
 
227
- function assertSingleTransformHost(
227
+ function assertTransformHostCompatibility(
228
228
  plugins: readonly ITtscLoadedNativePlugin[],
229
229
  ): void {
230
230
  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
- );
231
+ if (binaries.length <= 1) {
232
+ return;
233
+ }
234
+ if (plugins.every(isFirstPartyUtilityTransformPlugin)) {
235
+ return;
236
+ }
237
+ throw new Error(
238
+ "ttsc: multiple transform native backends cannot share one source-to-source pass; " +
239
+ "compose transform libraries through one aggregate native host",
240
+ );
241
+ }
242
+
243
+ function isFirstPartyUtilityTransformPlugin(
244
+ plugin: ITtscLoadedNativePlugin,
245
+ ): boolean {
246
+ if (plugin.stage !== "transform") return false;
247
+ if (!firstPartyUtilityPluginNames.has(plugin.name)) return false;
248
+ const manifest = readNearestPackageManifest(plugin.source);
249
+ return manifest?.name === plugin.name;
250
+ }
251
+
252
+ const firstPartyUtilityPluginNames = new Set([
253
+ "@ttsc/banner",
254
+ "@ttsc/paths",
255
+ "@ttsc/strip",
256
+ ]);
257
+
258
+ function readNearestPackageManifest(
259
+ source: string,
260
+ ): { name?: unknown } | undefined {
261
+ try {
262
+ let current = fs.statSync(source).isDirectory()
263
+ ? source
264
+ : path.dirname(source);
265
+ for (let i = 0; i < 4; i += 1) {
266
+ const manifest = path.join(current, "package.json");
267
+ if (fs.existsSync(manifest)) {
268
+ return JSON.parse(fs.readFileSync(manifest, "utf8")) as {
269
+ name?: unknown;
270
+ };
271
+ }
272
+ const parent = path.dirname(current);
273
+ if (parent === current) break;
274
+ current = parent;
275
+ }
276
+ } catch {
277
+ return undefined;
235
278
  }
279
+ return undefined;
236
280
  }
237
281
 
238
282
  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,11 +364,12 @@ 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.",
355
371
  " Plugin modules are descriptors for ordered native transformer backends.",
356
- " JS transformOutput/transformSource hooks are not part of the public contract.",
372
+ " JS transformOutput/transformSource functions are not part of the public contract.",
357
373
  "",
358
374
  "Compatibility aliases:",
359
375
  " ttsc build [options] Same project build lane as `ttsc [options]`.",
@@ -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,6 +58,7 @@ 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);
62
63
  const binary = buildSourcePlugin({
63
64
  baseDir: context.projectRoot,
@@ -71,7 +72,8 @@ export function loadProjectPlugins(options: {
71
72
  binary,
72
73
  config: entries[index]!,
73
74
  name: plugin.name,
74
- stage: resolvePluginStage(plugin),
75
+ source: plugin.source,
76
+ stage,
75
77
  });
76
78
  });
77
79
  return {
@@ -106,11 +108,11 @@ function loadPluginEntry(
106
108
  `ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
107
109
  );
108
110
  }
109
- rejectJsTransformHooks(specifier, plugin);
111
+ rejectJsTransformFunctions(specifier, plugin);
110
112
  return plugin;
111
113
  }
112
114
  if (isTtscPlugin(candidate)) {
113
- rejectJsTransformHooks(specifier, candidate);
115
+ rejectJsTransformFunctions(specifier, candidate);
114
116
  return candidate;
115
117
  }
116
118
  throw new Error(
@@ -126,10 +128,13 @@ function isTtscPlugin(value: unknown): value is ITtscPlugin {
126
128
  );
127
129
  }
128
130
 
129
- function rejectJsTransformHooks(specifier: string, candidate: object): void {
131
+ function rejectJsTransformFunctions(
132
+ specifier: string,
133
+ candidate: object,
134
+ ): void {
130
135
  if ("transformSource" in candidate || "transformOutput" in candidate) {
131
136
  throw new Error(
132
- `ttsc: plugin "${specifier}" declares unsupported JS transform hooks; ` +
137
+ `ttsc: plugin "${specifier}" declares unsupported JS transform functions; ` +
133
138
  "declare a native backend instead",
134
139
  );
135
140
  }
@@ -140,6 +145,12 @@ function resolvePluginStage(plugin: ITtscPlugin): TtscPluginStage {
140
145
  return "transform";
141
146
  }
142
147
  if (!isPluginStage(plugin.stage)) {
148
+ if (plugin.stage === "output") {
149
+ throw new Error(
150
+ `ttsc: plugin "${plugin.name}" requested removed stage "output"; ` +
151
+ "upgrade the plugin to a transform-stage descriptor compatible with this ttsc version",
152
+ );
153
+ }
143
154
  throw new Error(
144
155
  `ttsc: plugin "${plugin.name}" requested unsupported stage ${JSON.stringify(plugin.stage)}`,
145
156
  );
@@ -154,7 +165,7 @@ function validatePluginSource(plugin: ITtscPlugin): void {
154
165
  }
155
166
 
156
167
  function isPluginStage(value: string): value is TtscPluginStage {
157
- return value === "transform" || value === "check" || value === "output";
168
+ return value === "transform" || value === "check";
158
169
  }
159
170
 
160
171
  function resolvePluginRequest(specifier: string, projectRoot: string): string {
@@ -48,11 +48,20 @@ export interface ITtscPlugin {
48
48
  /**
49
49
  * Pipeline stage implemented by the sidecar.
50
50
  *
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"`).
51
+ * Omit this field for normal compiler-transform plugins. The only explicit
52
+ * non-transform stage is `"check"`.
54
53
  *
55
54
  * @default "transform"
56
55
  */
57
56
  stage?: TtscPluginStage;
58
57
  }
58
+
59
+ export function defineTtscPlugin<T extends ITtscPlugin>(plugin: T): T;
60
+ export function defineTtscPlugin<TContext, T extends ITtscPlugin>(
61
+ factory: (context: TContext) => T,
62
+ ): (context: TContext) => T;
63
+ export function defineTtscPlugin(
64
+ value: ITtscPlugin | ((context: unknown) => ITtscPlugin),
65
+ ): ITtscPlugin | ((context: unknown) => ITtscPlugin) {
66
+ return value;
67
+ }
@@ -1,18 +1,10 @@
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
10
- * text.
4
+ * - `"transform"`: participates in the TypeScript-Go transform path. Transform
5
+ * plugins do not receive emitted JavaScript or emitted file text.
11
6
  * - `"check"`: runs before emit and reports diagnostics only. Use this for
12
7
  * lint or validation plugins that should fail the compile before JavaScript
13
8
  * 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
9
  */
18
- export type TtscPluginStage = "transform" | "check" | "output";
10
+ export type TtscPluginStage = "transform" | "check";
@@ -9,6 +9,8 @@ export interface ITtscLoadedNativePlugin {
9
9
  config: ITtscProjectPluginConfig;
10
10
  /** Stable plugin name used in diagnostics and native manifests. */
11
11
  name: string;
12
+ /** Go source directory selected by the plugin descriptor. */
13
+ source: string;
12
14
  /** Pipeline stage where the sidecar participates. */
13
15
  stage: TtscPluginStage;
14
16
  }
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
  });