ttsc 0.7.0 → 0.7.2

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 (45) hide show
  1. package/README.md +3 -5
  2. package/cmd/platform/main.go +1 -1
  3. package/cmd/ttsc/api_transform.go +64 -0
  4. package/cmd/ttsc/main.go +4 -18
  5. package/driver/program.go +2 -2
  6. package/lib/TtscCompiler.d.ts +20 -0
  7. package/lib/TtscCompiler.js +49 -0
  8. package/lib/TtscCompiler.js.map +1 -1
  9. package/lib/compiler/internal/runSingleFileEmit.d.ts +3 -0
  10. package/lib/compiler/internal/{runTransform.js → runSingleFileEmit.js} +10 -8
  11. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -0
  12. package/lib/compiler/internal/transformProjectInMemory.d.ts +7 -0
  13. package/lib/compiler/internal/transformProjectInMemory.js +275 -0
  14. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -0
  15. package/lib/launcher/internal/prepareExecution.js +89 -8
  16. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  17. package/lib/launcher/internal/runTtsc.js +2 -76
  18. package/lib/launcher/internal/runTtsc.js.map +1 -1
  19. package/lib/structures/ITtscCompilerTransformation.d.ts +35 -0
  20. package/lib/structures/ITtscCompilerTransformation.js +3 -0
  21. package/lib/structures/ITtscCompilerTransformation.js.map +1 -0
  22. package/lib/structures/TtscPluginStage.d.ts +7 -4
  23. package/lib/structures/index.d.ts +1 -0
  24. package/lib/structures/index.js +1 -0
  25. package/lib/structures/index.js.map +1 -1
  26. package/lib/structures/internal/TtscCommonOptions.d.ts +1 -1
  27. package/lib/structures/internal/{TtscTransformOptions.d.ts → TtscSingleFileEmitOptions.d.ts} +4 -4
  28. package/lib/structures/internal/{TtscTransformOptions.js → TtscSingleFileEmitOptions.js} +1 -1
  29. package/lib/structures/internal/TtscSingleFileEmitOptions.js.map +1 -0
  30. package/package.json +8 -8
  31. package/src/TtscCompiler.ts +64 -0
  32. package/src/compiler/internal/{runTransform.ts → runSingleFileEmit.ts} +11 -7
  33. package/src/compiler/internal/transformProjectInMemory.ts +368 -0
  34. package/src/launcher/internal/prepareExecution.ts +114 -8
  35. package/src/launcher/internal/runTtsc.ts +6 -79
  36. package/src/structures/ITtscCompilerTransformation.ts +46 -0
  37. package/src/structures/TtscPluginStage.ts +7 -4
  38. package/src/structures/index.ts +1 -0
  39. package/src/structures/internal/TtscCommonOptions.ts +1 -1
  40. package/src/structures/internal/{TtscTransformOptions.ts → TtscSingleFileEmitOptions.ts} +4 -4
  41. package/test/compiler.test.ts +196 -8
  42. package/cmd/ttsc/transform.go +0 -160
  43. package/lib/compiler/internal/runTransform.d.ts +0 -3
  44. package/lib/compiler/internal/runTransform.js.map +0 -1
  45. package/lib/structures/internal/TtscTransformOptions.js.map +0 -1
@@ -1,12 +1,14 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
 
4
5
  import { resolveEmittedJavaScript } from "../../compiler/internal/resolveEmittedJavaScript";
5
6
  import { runBuild } from "../../compiler/internal/runBuild";
6
- import { resolveProjectConfig } from "../../compiler/internal/project/resolveProjectConfig";
7
+ import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
7
8
  import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
8
9
 
9
10
  const PROCESS_CACHE_KEY = String(process.pid);
11
+ const MAX_VIRTUAL_PARENT_DEPTH = 3;
10
12
 
11
13
  /** Build the owning project and locate the emitted JavaScript entry for `ttsx`. */
12
14
  export function prepareExecution(
@@ -48,22 +50,42 @@ function createProjectContext(
48
50
  filename: string,
49
51
  options: NonNullable<Parameters<typeof prepareExecution>[1]>,
50
52
  ) {
51
- const tsconfig = options.project
52
- ? resolveProjectConfig({ cwd, tsconfig: path.resolve(cwd, options.project) })
53
- : resolveProjectConfig({ cwd, file: filename });
54
- const root = path.dirname(tsconfig);
53
+ const project = readProjectConfig(
54
+ options.project
55
+ ? { cwd, tsconfig: path.resolve(cwd, options.project) }
56
+ : { cwd, file: filename },
57
+ );
58
+ const tsconfig = project.path;
59
+ const root = project.root;
55
60
  const cacheDir =
56
61
  options.cacheDir ?? path.join(root, "node_modules", ".cache", "ttsc", "ttsx");
62
+ const processDir = path.join(cacheDir, "project", PROCESS_CACHE_KEY);
63
+ const virtualRoot = path.join(processDir, "fs");
57
64
  return {
58
65
  tsconfig,
59
66
  root,
60
67
  cacheDir,
61
- emitDir: path.join(cacheDir, "project", PROCESS_CACHE_KEY),
68
+ processDir,
69
+ virtualRoot,
70
+ emitDir: project.compilerOptions.outDir
71
+ ? virtualPath(virtualRoot, project.compilerOptions.outDir)
72
+ : virtualPath(virtualRoot, resolveRuntimeSourceRoot(project, filename)),
62
73
  built: false,
63
74
  emittedFiles: undefined as string[] | undefined,
64
75
  };
65
76
  }
66
77
 
78
+ function resolveRuntimeSourceRoot(
79
+ project: ReturnType<typeof readProjectConfig>,
80
+ filename: string,
81
+ ): string {
82
+ const rootDir = project.compilerOptions.rootDir;
83
+ if (typeof rootDir === "string") {
84
+ return path.isAbsolute(rootDir) ? rootDir : path.resolve(project.root, rootDir);
85
+ }
86
+ return path.dirname(filename);
87
+ }
88
+
67
89
  function buildProject(
68
90
  context: ReturnType<typeof createProjectContext>,
69
91
  options: NonNullable<Parameters<typeof prepareExecution>[1]>,
@@ -71,7 +93,8 @@ function buildProject(
71
93
  if (context.built) return;
72
94
 
73
95
  fs.mkdirSync(context.cacheDir, { recursive: true });
74
- fs.rmSync(context.emitDir, { recursive: true, force: true });
96
+ fs.rmSync(context.processDir, { recursive: true, force: true });
97
+ fs.mkdirSync(path.dirname(context.emitDir), { recursive: true });
75
98
  const result = runBuild({
76
99
  binary: options.binary,
77
100
  cwd: context.root,
@@ -84,6 +107,7 @@ function buildProject(
84
107
  tsconfig: context.tsconfig,
85
108
  });
86
109
  if (result.status === 0) {
110
+ linkVirtualProjectLayout(context);
87
111
  context.built = true;
88
112
  context.emittedFiles =
89
113
  result.emittedFiles && result.emittedFiles.length !== 0
@@ -92,7 +116,7 @@ function buildProject(
92
116
  return;
93
117
  }
94
118
 
95
- fs.rmSync(context.emitDir, { recursive: true, force: true });
119
+ fs.rmSync(context.processDir, { recursive: true, force: true });
96
120
  const detail = [
97
121
  `ttsx: project check failed for ${context.tsconfig}`,
98
122
  result.stderr || result.stdout,
@@ -102,6 +126,88 @@ function buildProject(
102
126
  throw new Error(detail);
103
127
  }
104
128
 
129
+ function linkVirtualProjectLayout(
130
+ context: ReturnType<typeof createProjectContext>,
131
+ ): void {
132
+ for (const directory of collectLinkDirectories(context.root)) {
133
+ const virtualDirectory = virtualPath(context.virtualRoot, directory);
134
+ fs.mkdirSync(virtualDirectory, { recursive: true });
135
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
136
+ const realEntry = path.join(directory, entry.name);
137
+ const virtualEntry = path.join(virtualDirectory, entry.name);
138
+ if (fs.existsSync(virtualEntry)) {
139
+ continue;
140
+ }
141
+ linkVirtualEntry(realEntry, virtualEntry, entry);
142
+ }
143
+ }
144
+ }
145
+
146
+ function linkVirtualEntry(
147
+ realEntry: string,
148
+ virtualEntry: string,
149
+ entry: fs.Dirent,
150
+ ): void {
151
+ if (entry.isDirectory()) {
152
+ fs.symlinkSync(
153
+ realEntry,
154
+ virtualEntry,
155
+ process.platform === "win32" ? "junction" : undefined,
156
+ );
157
+ return;
158
+ }
159
+ if (entry.isFile()) {
160
+ try {
161
+ fs.linkSync(realEntry, virtualEntry);
162
+ } catch {
163
+ fs.copyFileSync(realEntry, virtualEntry);
164
+ }
165
+ return;
166
+ }
167
+ fs.symlinkSync(realEntry, virtualEntry);
168
+ }
169
+
170
+ function collectLinkDirectories(projectRoot: string): string[] {
171
+ const out: string[] = [];
172
+ let current = projectRoot;
173
+ for (let depth = 0; depth <= MAX_VIRTUAL_PARENT_DEPTH; depth += 1) {
174
+ out.push(current);
175
+ if (
176
+ depth > 0 &&
177
+ (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) ||
178
+ fs.existsSync(path.join(current, ".git")))
179
+ ) {
180
+ break;
181
+ }
182
+ const parent = path.dirname(current);
183
+ if (parent === current || isUnsafeVirtualParent(parent)) {
184
+ break;
185
+ }
186
+ current = parent;
187
+ }
188
+ return out.reverse();
189
+ }
190
+
191
+ function isUnsafeVirtualParent(directory: string): boolean {
192
+ const resolved = path.resolve(directory);
193
+ const root = path.parse(resolved).root;
194
+ return (
195
+ resolved === root ||
196
+ resolved === path.resolve(os.tmpdir())
197
+ );
198
+ }
199
+
200
+ function virtualPath(root: string, absolute: string): string {
201
+ const parsed = path.parse(path.resolve(absolute));
202
+ const label =
203
+ parsed.root === path.sep
204
+ ? "posix"
205
+ : parsed.root.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") ||
206
+ "root";
207
+ const relative = path.relative(parsed.root, path.resolve(absolute));
208
+ return path.join(root, label, relative);
209
+ }
210
+
105
211
  function looksLikeESM(output: string): boolean {
106
212
  if (
107
213
  /\bObject\.defineProperty\(exports\b/.test(output) ||
@@ -4,13 +4,14 @@ import * as path from "node:path";
4
4
 
5
5
  import { resolveBinary } from "../../compiler/internal/resolveBinary";
6
6
  import { runBuild } from "../../compiler/internal/runBuild";
7
- import { runTransform } from "../../compiler/internal/runTransform";
7
+ import { runSingleFileEmit } from "../../compiler/internal/runSingleFileEmit";
8
8
  import { resolveProjectConfig } from "../../compiler/internal/project/resolveProjectConfig";
9
9
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
10
- import type { TtscTransformOptions } from "../../structures/internal/TtscTransformOptions";
11
10
  import { getCompilerVersionText } from "./getCompilerVersionText";
12
11
 
13
- export function runTtsc(argv: readonly string[] = process.argv.slice(2)): number {
12
+ export function runTtsc(
13
+ argv: readonly string[] = process.argv.slice(2),
14
+ ): number {
14
15
  try {
15
16
  if (argv.length === 0) {
16
17
  return runCompatibleBuild([], false);
@@ -34,8 +35,6 @@ export function runTtsc(argv: readonly string[] = process.argv.slice(2)): number
34
35
  return runCompatibleBuild(rest, true);
35
36
  case "clean":
36
37
  return runClean(rest);
37
- case "transform":
38
- return runTransformCommand(rest);
39
38
  case "demo":
40
39
  return delegateToNative(argv);
41
40
  case "-p":
@@ -83,15 +82,6 @@ function runCompatibleBuild(
83
82
  return result.status;
84
83
  }
85
84
 
86
- function runTransformCommand(argv: readonly string[]): number {
87
- const options = parseTransformArgs(argv);
88
- const text = runTransform(options);
89
- if (!options.out) {
90
- process.stdout.write(text);
91
- }
92
- return 0;
93
- }
94
-
95
85
  function runClean(argv: readonly string[]): number {
96
86
  const options = parseCleanArgs(argv);
97
87
  const cwd = path.resolve(options.cwd ?? process.cwd());
@@ -216,10 +206,7 @@ function parseCleanArgs(argv: readonly string[]) {
216
206
  return { cwd, tsconfig };
217
207
  }
218
208
 
219
- function parseBuildArgs(
220
- argv: readonly string[],
221
- checkOnly: boolean,
222
- ) {
209
+ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
223
210
  let binary: string | undefined;
224
211
  let cwd: string | undefined;
225
212
  let emit: boolean | undefined = checkOnly ? false : undefined;
@@ -306,64 +293,6 @@ function parseBuildArgs(
306
293
  };
307
294
  }
308
295
 
309
- function parseTransformArgs(argv: readonly string[]): TtscTransformOptions {
310
- let binary: string | undefined;
311
- let cwd: string | undefined;
312
- let file: string | undefined;
313
- let out: string | undefined;
314
- let tsconfig: string | undefined;
315
-
316
- const rest = [...argv];
317
- while (rest.length !== 0) {
318
- const current = rest.shift()!;
319
- switch (current) {
320
- case "--binary":
321
- binary = takeValue(current, rest);
322
- break;
323
- case "--cwd":
324
- cwd = takeValue(current, rest);
325
- break;
326
- case "--file":
327
- file = takeValue(current, rest);
328
- break;
329
- case "--out":
330
- out = takeValue(current, rest);
331
- break;
332
- case "--tsconfig":
333
- case "--project":
334
- tsconfig = takeValue(current, rest);
335
- break;
336
- default:
337
- if (current.startsWith("--binary=")) {
338
- binary = current.slice("--binary=".length);
339
- } else if (current.startsWith("--cwd=")) {
340
- cwd = current.slice("--cwd=".length);
341
- } else if (current.startsWith("--file=")) {
342
- file = current.slice("--file=".length);
343
- } else if (current.startsWith("--out=")) {
344
- out = current.slice("--out=".length);
345
- } else if (current.startsWith("--tsconfig=")) {
346
- tsconfig = current.slice("--tsconfig=".length);
347
- } else if (current.startsWith("--project=")) {
348
- tsconfig = current.slice("--project=".length);
349
- } else {
350
- throw new Error(`ttsc: unknown option ${current}`);
351
- }
352
- break;
353
- }
354
- }
355
- if (!file) {
356
- throw new Error("ttsc: transform requires --file");
357
- }
358
- return {
359
- binary,
360
- cwd,
361
- file,
362
- out,
363
- tsconfig,
364
- };
365
- }
366
-
367
296
  function printHelp(): void {
368
297
  process.stdout.write(
369
298
  [
@@ -375,7 +304,6 @@ function printHelp(): void {
375
304
  " ttsc --watch",
376
305
  " ttsc --noEmit",
377
306
  " ttsc clean [options]",
378
- " ttsc transform --file <path> [options]",
379
307
  " ttsc version",
380
308
  " ttsc --help",
381
309
  "",
@@ -390,7 +318,6 @@ function printHelp(): void {
390
318
  " --outDir <dir> Override compilerOptions.outDir for this invocation",
391
319
  " --quiet Keep build output quiet (default)",
392
320
  " --verbose Print the build summary and emitted files",
393
- " --out <path> Write transform output to a file instead of stdout",
394
321
  " --binary <path> Use an explicit tsgo binary",
395
322
  "",
396
323
  "Plugin contract:",
@@ -416,7 +343,7 @@ function runSingleFile(options: ReturnType<typeof parseBuildArgs>): number {
416
343
  const cwd = path.resolve(options.cwd ?? process.cwd());
417
344
  const file = path.resolve(cwd, options.files[0]!);
418
345
  const out = resolveSingleFileOut(file, cwd, options.outDir);
419
- const text = runTransform({
346
+ const text = runSingleFileEmit({
420
347
  binary: options.binary,
421
348
  cwd,
422
349
  file,
@@ -0,0 +1,46 @@
1
+ import type { ITtscCompilerDiagnostic } from "./ITtscCompilerDiagnostic";
2
+
3
+ /**
4
+ * Result of a TypeScript source-to-source transformation operation.
5
+ *
6
+ * This mirrors `embed-typescript`'s `IEmbedTypeScriptTransformation` model.
7
+ * Unlike {@link ITtscCompilerResult}, this contract is not an emit contract:
8
+ * the `typescript` map must contain TypeScript source text, not generated
9
+ * JavaScript, declaration files, or source maps.
10
+ */
11
+ export type ITtscCompilerTransformation =
12
+ | ITtscCompilerTransformation.ISuccess
13
+ | ITtscCompilerTransformation.IFailure
14
+ | ITtscCompilerTransformation.IException;
15
+
16
+ export namespace ITtscCompilerTransformation {
17
+ /** Successful source-to-source transformation result. */
18
+ export interface ISuccess {
19
+ /** Indicates that transformation completed without diagnostics. */
20
+ type: "success";
21
+
22
+ /** Transformed TypeScript source text keyed by project file path. */
23
+ typescript: Record<string, string>;
24
+ }
25
+
26
+ /** Source-to-source transformation result that completed with diagnostics. */
27
+ export interface IFailure {
28
+ /** Indicates that transformation completed with diagnostics. */
29
+ type: "failure";
30
+
31
+ /** Transformed or partially transformed TypeScript source text. */
32
+ typescript: Record<string, string>;
33
+
34
+ /** Diagnostics reported during transformation. */
35
+ diagnostics: ITtscCompilerDiagnostic[];
36
+ }
37
+
38
+ /** Unexpected host-level error during transformation. */
39
+ export interface IException {
40
+ /** Indicates that transformation could not complete normally. */
41
+ type: "exception";
42
+
43
+ /** Raw error thrown by the ttsc host. */
44
+ error: unknown;
45
+ }
46
+ }
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Pipeline stage where a plugin's lazily built Go sidecar participates.
3
3
  *
4
- * - `"transform"`: owns the TypeScript-Go emit pass. This is the default and
5
- * is the right stage for compiler-transform plugins such as typia-style
6
- * source rewrites. A transform sidecar receives the ordered plugin manifest
7
- * and is responsible for producing the project's emitted output.
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.
8
11
  * - `"check"`: runs before emit and reports diagnostics only. Use this for
9
12
  * lint or validation plugins that should fail the compile before JavaScript
10
13
  * or declaration output is generated.
@@ -1,6 +1,7 @@
1
1
  export * from "./ITtscCompilerContext";
2
2
  export * from "./ITtscCompilerDiagnostic";
3
3
  export * from "./ITtscCompilerResult";
4
+ export * from "./ITtscCompilerTransformation";
4
5
  export * from "./ITtscPlugin";
5
6
  export * from "./ITtscPluginFactoryContext";
6
7
  export * from "./ITtscProjectPluginConfig";
@@ -1,6 +1,6 @@
1
1
  import type { ITtscProjectPluginConfig } from "../ITtscProjectPluginConfig";
2
2
 
3
- /** Internal options shared by the CLI build, transform, and runtime paths. */
3
+ /** Internal options shared by the CLI build, single-file emit, and runtime paths. */
4
4
  export interface TtscCommonOptions {
5
5
  /**
6
6
  * Explicit TypeScript-Go executable.
@@ -1,12 +1,12 @@
1
1
  import type { TtscCommonOptions } from "./TtscCommonOptions";
2
2
 
3
- /** Internal options for transforming one source file through the CLI path. */
4
- export interface TtscTransformOptions extends TtscCommonOptions {
5
- /** Source file to transform. Absolute paths and `cwd`-relative paths work. */
3
+ /** Internal options for emitting one source file through the file-argument path. */
4
+ export interface TtscSingleFileEmitOptions extends TtscCommonOptions {
5
+ /** Source file to emit. Absolute paths and `cwd`-relative paths work. */
6
6
  file: string;
7
7
  /** Project config owning `file`; discovered from `file` when omitted. */
8
8
  tsconfig?: string;
9
- /** Optional output file path. Omit to return emitted text from stdout. */
9
+ /** Optional output file path. */
10
10
  out?: string;
11
11
  /** Suppress summary banners from ttsc/native sidecars. Defaults to `true`. */
12
12
  quiet?: boolean;