ttsc 0.8.1 → 0.9.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.
Files changed (84) hide show
  1. package/README.md +39 -68
  2. package/cmd/ttsc/api_compile.go +5 -1
  3. package/driver/program.go +23 -1
  4. package/driver/rewrite.go +126 -9
  5. package/driver/rewrite_test.go +52 -0
  6. package/lib/TtscCompiler.js +28 -46
  7. package/lib/TtscCompiler.js.map +1 -1
  8. package/lib/compiler/internal/buildNativeCompiler.js +8 -38
  9. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  10. package/lib/compiler/internal/compileProjectInMemory.js +36 -63
  11. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  12. package/lib/compiler/internal/project/readProjectConfig.js +108 -60
  13. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  14. package/lib/compiler/internal/project/resolveProjectConfig.js +15 -45
  15. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  16. package/lib/compiler/internal/resolveBinary.js +10 -40
  17. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  18. package/lib/compiler/internal/resolveEmittedJavaScript.js +21 -48
  19. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  20. package/lib/compiler/internal/resolveTsgo.js +14 -44
  21. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  22. package/lib/compiler/internal/runBuild.d.ts +7 -0
  23. package/lib/compiler/internal/runBuild.js +47 -66
  24. package/lib/compiler/internal/runBuild.js.map +1 -1
  25. package/lib/compiler/internal/runSingleFileEmit.js +18 -48
  26. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  27. package/lib/compiler/internal/transformProjectInMemory.js +41 -75
  28. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  29. package/lib/launcher/internal/getCompilerVersionText.js +7 -37
  30. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  31. package/lib/launcher/internal/prepareExecution.js +48 -69
  32. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  33. package/lib/launcher/internal/runTtsc.js +53 -64
  34. package/lib/launcher/internal/runTtsc.js.map +1 -1
  35. package/lib/launcher/internal/runTtsx.js +328 -57
  36. package/lib/launcher/internal/runTtsx.js.map +1 -1
  37. package/lib/plugin/internal/buildSourcePlugin.d.ts +8 -0
  38. package/lib/plugin/internal/buildSourcePlugin.js +179 -125
  39. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  40. package/lib/plugin/internal/loadProjectPlugins.d.ts +2 -0
  41. package/lib/plugin/internal/loadProjectPlugins.js +197 -9
  42. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  43. package/lib/structures/ITtscCompilerContext.d.ts +16 -3
  44. package/lib/structures/ITtscCompilerResult.d.ts +4 -0
  45. package/lib/structures/ITtscCompilerTransformation.d.ts +2 -0
  46. package/lib/structures/ITtscPluginFactoryContext.d.ts +6 -3
  47. package/lib/structures/ITtscProjectPluginConfig.d.ts +8 -7
  48. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  49. package/lib/structures/internal/ITtscProjectLocatorOptions.d.ts +2 -0
  50. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -0
  51. package/package.json +9 -14
  52. package/shim/diagnosticwriter/lint.go +8 -0
  53. package/src/TtscCompiler.ts +20 -5
  54. package/src/compiler/internal/buildNativeCompiler.ts +2 -2
  55. package/src/compiler/internal/compileProjectInMemory.ts +21 -16
  56. package/src/compiler/internal/project/readProjectConfig.ts +134 -35
  57. package/src/compiler/internal/project/resolveProjectConfig.ts +2 -2
  58. package/src/compiler/internal/resolveBinary.ts +2 -2
  59. package/src/compiler/internal/resolveEmittedJavaScript.ts +11 -7
  60. package/src/compiler/internal/resolveTsgo.ts +2 -2
  61. package/src/compiler/internal/runBuild.ts +34 -21
  62. package/src/compiler/internal/runSingleFileEmit.ts +3 -3
  63. package/src/compiler/internal/transformProjectInMemory.ts +37 -37
  64. package/src/launcher/internal/getCompilerVersionText.ts +2 -2
  65. package/src/launcher/internal/prepareExecution.ts +14 -4
  66. package/src/launcher/internal/runTtsc.ts +30 -4
  67. package/src/launcher/internal/runTtsx.ts +377 -26
  68. package/src/plugin/internal/buildSourcePlugin.ts +127 -32
  69. package/src/plugin/internal/loadProjectPlugins.ts +266 -10
  70. package/src/structures/ITtscCompilerContext.ts +17 -3
  71. package/src/structures/ITtscCompilerResult.ts +5 -0
  72. package/src/structures/ITtscCompilerTransformation.ts +3 -0
  73. package/src/structures/ITtscPluginFactoryContext.ts +6 -3
  74. package/src/structures/ITtscProjectPluginConfig.ts +8 -7
  75. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  76. package/src/structures/internal/ITtscProjectLocatorOptions.ts +2 -0
  77. package/src/structures/internal/TtscCommonOptions.ts +2 -0
  78. package/utility/host.go +610 -18
  79. package/utility/host_test.go +162 -0
  80. package/test/compiler.test.ts +0 -422
  81. package/test/platform.test.ts +0 -12
  82. package/test/project.test.ts +0 -197
  83. package/test/source-build.test.ts +0 -45
  84. package/test/tsgo.test.ts +0 -69
@@ -1,19 +1,14 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import * as crypto from "node:crypto";
3
- import * as fs from "node:fs";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
4
  import { createRequire } from "node:module";
5
- import * as path from "node:path";
5
+ import os from "node:os";
6
+ import path from "node:path";
6
7
 
7
8
  const GO_MOD_SEARCH_MAX_DEPTH = 3;
8
9
 
9
- const SKIP_DIRS = new Set([
10
- "node_modules",
11
- ".git",
12
- "dist",
13
- "build",
14
- "vendor",
15
- "lib",
16
- ]);
10
+ const PRUNE_DIRS = new Set(["node_modules", ".git", ".ttsc"]);
11
+ const GENERATED_WORKSPACE_FILES = new Set(["go.work", "go.work.sum"]);
17
12
 
18
13
  /** Build one Go source plugin into a cached executable. */
19
14
  export function buildSourcePlugin(opts: {
@@ -28,9 +23,13 @@ export function buildSourcePlugin(opts: {
28
23
  tsgoVersion: string;
29
24
  }): string {
30
25
  const { dir, entry, source } = resolveSourceBuildTarget(opts);
26
+ const overlayDirs = opts.overlayDirs ?? findTtscOverlayDirs();
27
+ const goBinary = resolveGoCompiler();
31
28
  const key = computeCacheKey({
32
29
  dir,
33
30
  entry,
31
+ goBinary,
32
+ overlayDirs,
34
33
  ttscVersion: opts.ttscVersion,
35
34
  tsgoVersion: opts.tsgoVersion,
36
35
  });
@@ -49,27 +48,46 @@ export function buildSourcePlugin(opts: {
49
48
  );
50
49
  }
51
50
 
52
- const scratchDir = path.join(
53
- root,
54
- `scratch-${key}-${process.pid}-${Date.now()}`,
51
+ const scratchDir = fs.mkdtempSync(
52
+ path.join(os.tmpdir(), `ttsc-plugin-${key}-`),
55
53
  );
56
54
  try {
57
55
  materializeScratchDir(dir, scratchDir);
58
- writeGoWork(scratchDir, opts.overlayDirs ?? findTtscOverlayDirs());
56
+ writeGoWork(scratchDir, overlayDirs);
59
57
  const scratchBinaryName =
60
58
  process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
61
- runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName);
59
+ runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName, goBinary);
62
60
  const builtBinary = path.join(scratchDir, scratchBinaryName);
63
- fs.renameSync(builtBinary, binaryPath);
64
- if (process.platform !== "win32") {
65
- fs.chmodSync(binaryPath, 0o755);
66
- }
61
+ publishBuiltBinary(builtBinary, binaryPath);
67
62
  return binaryPath;
68
63
  } finally {
69
64
  fs.rmSync(scratchDir, { recursive: true, force: true });
70
65
  }
71
66
  }
72
67
 
68
+ function publishBuiltBinary(builtBinary: string, binaryPath: string): void {
69
+ const pending = `${binaryPath}.${process.pid}.${Date.now()}-${Math.random()
70
+ .toString(16)
71
+ .slice(2)}.tmp`;
72
+ fs.copyFileSync(builtBinary, pending);
73
+ if (process.platform !== "win32") {
74
+ fs.chmodSync(pending, 0o755);
75
+ }
76
+ try {
77
+ fs.renameSync(pending, binaryPath);
78
+ } catch (error) {
79
+ fs.rmSync(pending, { force: true });
80
+ const code = (error as NodeJS.ErrnoException).code;
81
+ if (
82
+ (code === "EEXIST" || code === "EPERM" || code === "EACCES") &&
83
+ fs.existsSync(binaryPath)
84
+ ) {
85
+ return;
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+
73
91
  function resolveSourceBuildTarget(opts: {
74
92
  source: string;
75
93
  pluginName: string;
@@ -136,8 +154,8 @@ function materializeScratchDir(source: string, scratch: string): void {
136
154
  recursive: true,
137
155
  filter: (src) => {
138
156
  const base = path.basename(src);
139
- if (SKIP_DIRS.has(base)) return false;
140
- if (base === "go.work" || base === "go.work.sum") return false;
157
+ if (shouldPruneDirectory(base)) return false;
158
+ if (shouldOmitSourceFile(base)) return false;
141
159
  return true;
142
160
  },
143
161
  });
@@ -189,8 +207,8 @@ function runGoBuild(
189
207
  entry: string,
190
208
  binaryName: string,
191
209
  pluginName: string,
210
+ goBinary: string,
192
211
  ): void {
193
- const goBinary = resolveGoCompiler();
194
212
  ensureExecutableGoToolchain(goBinary);
195
213
  const result = spawnSync(goBinary, ["build", "-o", binaryName, entry], {
196
214
  cwd,
@@ -305,7 +323,7 @@ function walkForGoMod(dir: string, out: string[]): void {
305
323
  }
306
324
  for (const entry of entries) {
307
325
  if (!entry.isDirectory()) continue;
308
- if (SKIP_DIRS.has(entry.name)) continue;
326
+ if (shouldPruneDirectory(entry.name)) continue;
309
327
  walkForGoMod(path.join(dir, entry.name), out);
310
328
  }
311
329
  }
@@ -315,7 +333,7 @@ function resolvePluginCacheRoot(
315
333
  cacheDir?: string,
316
334
  ): string {
317
335
  if (cacheDir) {
318
- return path.resolve(cacheDir, "plugins");
336
+ return path.resolve(projectRoot, cacheDir, "plugins");
319
337
  }
320
338
  if (process.env.TTSC_CACHE_DIR) {
321
339
  return path.resolve(process.env.TTSC_CACHE_DIR, "plugins");
@@ -387,9 +405,11 @@ function resolveGoCompiler(): string {
387
405
  return "go";
388
406
  }
389
407
 
390
- function computeCacheKey(inputs: {
408
+ export function computeCacheKey(inputs: {
391
409
  dir: string;
392
410
  entry: string;
411
+ goBinary?: string;
412
+ overlayDirs?: readonly string[];
393
413
  ttscVersion: string;
394
414
  tsgoVersion: string;
395
415
  }): string {
@@ -398,12 +418,28 @@ function computeCacheKey(inputs: {
398
418
  hash.update(`tsgo=${inputs.tsgoVersion}\n`);
399
419
  hash.update(`platform=${process.platform}/${process.arch}\n`);
400
420
  hash.update(`entry=${inputs.entry}\n`);
401
- for (const file of collectSourceFiles(inputs.dir)) {
402
- const rel = path.relative(inputs.dir, file).replace(/\\/g, "/");
421
+ if (inputs.goBinary !== undefined) {
422
+ hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary)}\n`);
423
+ }
424
+ hashSourceDirectory(hash, "plugin", inputs.dir);
425
+ for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
426
+ hashSourceDirectory(hash, `overlay:${index}`, dir);
427
+ }
428
+ return hash.digest("hex").slice(0, 32);
429
+ }
430
+
431
+ function hashSourceDirectory(
432
+ hash: crypto.Hash,
433
+ label: string,
434
+ root: string,
435
+ ): void {
436
+ hash.update(`dir=${label}\n`);
437
+ for (const file of collectSourceFiles(root)) {
438
+ const rel = path.relative(root, file).replace(/\\/g, "/");
403
439
  hash.update(`f=${rel}\n`);
404
440
  hash.update(fs.readFileSync(file));
441
+ hash.update("\n");
405
442
  }
406
- return hash.digest("hex").slice(0, 32);
407
443
  }
408
444
 
409
445
  function collectSourceFiles(root: string): string[] {
@@ -421,19 +457,78 @@ function walk(dir: string, out: string[]): void {
421
457
  return;
422
458
  }
423
459
  for (const entry of entries) {
424
- if (SKIP_DIRS.has(entry.name)) continue;
425
460
  const full = path.join(dir, entry.name);
426
461
  if (entry.isDirectory()) {
462
+ if (shouldPruneDirectory(entry.name)) continue;
427
463
  walk(full, out);
428
464
  continue;
429
465
  }
430
466
  if (!entry.isFile()) continue;
467
+ if (shouldOmitSourceFile(entry.name)) continue;
431
468
  if (!isHashableFile(entry.name)) continue;
432
469
  out.push(full);
433
470
  }
434
471
  }
435
472
 
473
+ function shouldPruneDirectory(name: string): boolean {
474
+ return PRUNE_DIRS.has(name);
475
+ }
476
+
477
+ function shouldOmitSourceFile(name: string): boolean {
478
+ return GENERATED_WORKSPACE_FILES.has(name);
479
+ }
480
+
436
481
  function isHashableFile(name: string): boolean {
437
- if (name === "go.mod" || name === "go.sum" || name === "go.work") return true;
438
- return /\.(?:go|s|c|h|cpp|hpp)$/i.test(name);
482
+ return !name.endsWith("~");
483
+ }
484
+
485
+ function resolveGoCompilerIdentity(goBinary: string): string {
486
+ const resolved = resolveExecutableIdentityPath(goBinary);
487
+ const version = spawnSync(goBinary, ["version"], {
488
+ encoding: "utf8",
489
+ windowsHide: true,
490
+ });
491
+ const versionText =
492
+ version.error !== undefined
493
+ ? version.error.message
494
+ : `${version.status ?? 0}:${version.stdout}${version.stderr}`;
495
+ let statText = "";
496
+ try {
497
+ const stat = fs.statSync(resolved);
498
+ statText = `${stat.size}:${stat.mtimeMs}`;
499
+ } catch {
500
+ statText = "missing";
501
+ }
502
+ return `${goBinary}:${resolved}:${statText}:${versionText}`;
503
+ }
504
+
505
+ function resolveExecutableIdentityPath(binary: string): string {
506
+ if (path.isAbsolute(binary)) {
507
+ return resolveRealPath(binary);
508
+ }
509
+ if (binary.includes(path.sep)) {
510
+ return resolveRealPath(path.resolve(binary));
511
+ }
512
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
513
+ if (dir.length === 0) continue;
514
+ const candidate = path.join(dir, binary);
515
+ if (fs.existsSync(candidate)) {
516
+ return resolveRealPath(candidate);
517
+ }
518
+ if (process.platform === "win32") {
519
+ const executable = `${candidate}.exe`;
520
+ if (fs.existsSync(executable)) {
521
+ return resolveRealPath(executable);
522
+ }
523
+ }
524
+ }
525
+ return binary;
526
+ }
527
+
528
+ function resolveRealPath(location: string): string {
529
+ try {
530
+ return fs.realpathSync(location);
531
+ } catch {
532
+ return location;
533
+ }
439
534
  }
@@ -15,12 +15,25 @@ type TtscPluginFactory<T = ITtscProjectPluginConfig> = (
15
15
  context: ITtscPluginFactoryContext<T>,
16
16
  ) => ITtscPlugin;
17
17
 
18
+ type ProjectPluginEntry = {
19
+ baseDir: string;
20
+ config: ITtscProjectPluginConfig;
21
+ };
22
+
23
+ type PackageManifest = {
24
+ dependencies?: Record<string, unknown>;
25
+ devDependencies?: Record<string, unknown>;
26
+ name?: unknown;
27
+ ttsc?: unknown;
28
+ };
29
+
18
30
  export function loadProjectPlugins(options: {
19
31
  binary: string;
20
32
  cacheDir?: string;
21
33
  cwd?: string;
22
34
  entries?: readonly ITtscProjectPluginConfig[] | false;
23
35
  file?: string;
36
+ projectRoot?: string;
24
37
  tsconfig?: string;
25
38
  }): {
26
39
  nativePlugins: ITtscLoadedNativePlugin[];
@@ -29,13 +42,14 @@ export function loadProjectPlugins(options: {
29
42
  const project = readProjectConfig({
30
43
  cwd: options.cwd,
31
44
  file: options.file,
45
+ projectRoot: options.projectRoot,
32
46
  tsconfig: options.tsconfig,
33
47
  });
34
- const entries =
48
+ const entries: ProjectPluginEntry[] =
35
49
  options.entries === false
36
50
  ? []
37
- : [...(options.entries ?? project.compilerOptions.plugins)].filter(
38
- (entry) => entry.enabled !== false,
51
+ : resolvePluginEntries(project, options.entries).filter(
52
+ (entry) => entry.config.enabled !== false,
39
53
  );
40
54
  if (entries.length === 0) {
41
55
  return {
@@ -51,7 +65,11 @@ export function loadProjectPlugins(options: {
51
65
  tsconfig: project.path,
52
66
  };
53
67
  const plugins = entries.map((entry) =>
54
- loadPluginEntry(entry, { ...context, plugin: entry }),
68
+ loadPluginEntry(
69
+ entry.config,
70
+ { ...context, plugin: entry.config },
71
+ entry.baseDir,
72
+ ),
55
73
  );
56
74
 
57
75
  const nativePlugins: ITtscLoadedNativePlugin[] = [];
@@ -70,28 +88,258 @@ export function loadProjectPlugins(options: {
70
88
  });
71
89
  nativePlugins.push({
72
90
  binary,
73
- config: entries[index]!,
91
+ config: entries[index]!.config,
74
92
  name: plugin.name,
75
93
  source: plugin.source,
76
94
  stage,
77
95
  });
78
96
  });
79
97
  return {
80
- nativePlugins,
98
+ nativePlugins: orderNativePlugins(nativePlugins),
81
99
  project,
82
100
  };
83
101
  }
84
102
 
103
+ export function hasProjectPluginEntries(
104
+ project: ITtscParsedProjectConfig,
105
+ entries?: readonly ITtscProjectPluginConfig[] | false,
106
+ ): boolean {
107
+ if (entries === false) {
108
+ return false;
109
+ }
110
+ return resolvePluginEntries(project, entries).some(
111
+ (entry) => entry.config.enabled !== false,
112
+ );
113
+ }
114
+
115
+ function resolvePluginEntries(
116
+ project: ITtscParsedProjectConfig,
117
+ entries?: readonly ITtscProjectPluginConfig[],
118
+ ): ProjectPluginEntry[] {
119
+ if (entries !== undefined) {
120
+ return entries.map((config) => ({
121
+ baseDir: project.root,
122
+ config,
123
+ }));
124
+ }
125
+ const configured = project.compilerOptions.plugins.map((config, index) => ({
126
+ baseDir: project.pluginBaseDirs[index] ?? project.root,
127
+ config,
128
+ }));
129
+ return [...configured, ...discoverPackagePluginEntries(project, configured)];
130
+ }
131
+
132
+ function discoverPackagePluginEntries(
133
+ project: ITtscParsedProjectConfig,
134
+ configured: readonly ProjectPluginEntry[],
135
+ ): ProjectPluginEntry[] {
136
+ const projectPackageJson = findNearestPackageJson(project.root);
137
+ if (projectPackageJson === undefined) {
138
+ return [];
139
+ }
140
+ const projectPackageRoot = path.dirname(projectPackageJson);
141
+ const projectManifest = readPackageManifest(projectPackageJson);
142
+ if (projectManifest === undefined) {
143
+ return [];
144
+ }
145
+
146
+ const configuredTransforms = createConfiguredTransformSet(configured);
147
+ const out: ProjectPluginEntry[] = [];
148
+ for (const name of directDependencyNames(projectManifest)) {
149
+ const packageJson = resolveDependencyPackageJson(name, projectPackageRoot);
150
+ if (packageJson === undefined) {
151
+ continue;
152
+ }
153
+ const manifest = readPackageManifest(packageJson);
154
+ const config = readPackagePluginConfig(name, manifest);
155
+ if (config === undefined || config.enabled === false) {
156
+ continue;
157
+ }
158
+ const packageRoot = path.dirname(packageJson);
159
+ const transform = config.transform;
160
+ if (typeof transform !== "string") {
161
+ continue;
162
+ }
163
+ const baseDir = isRelativePluginSpecifier(transform)
164
+ ? packageRoot
165
+ : projectPackageRoot;
166
+ const resolved = resolvePluginRequest(transform, baseDir);
167
+ if (hasConfiguredTransform(configuredTransforms, transform, resolved)) {
168
+ continue;
169
+ }
170
+ out.push({
171
+ baseDir,
172
+ config,
173
+ });
174
+ addConfiguredTransform(configuredTransforms, transform, resolved);
175
+ }
176
+ return out;
177
+ }
178
+
179
+ type ConfiguredTransformSet = {
180
+ raw: Set<string>;
181
+ resolved: Set<string>;
182
+ };
183
+
184
+ function createConfiguredTransformSet(
185
+ entries: readonly ProjectPluginEntry[],
186
+ ): ConfiguredTransformSet {
187
+ const raw = new Set<string>();
188
+ const resolved = new Set<string>();
189
+ for (const entry of entries) {
190
+ const transform = entry.config.transform;
191
+ if (typeof transform !== "string" || transform.length === 0) {
192
+ continue;
193
+ }
194
+ if (!isRelativePluginSpecifier(transform)) {
195
+ raw.add(transform);
196
+ }
197
+ try {
198
+ resolved.add(resolvePluginRequest(transform, entry.baseDir));
199
+ } catch {
200
+ // Keep the normal plugin loading error path for invalid explicit entries.
201
+ }
202
+ }
203
+ return { raw, resolved };
204
+ }
205
+
206
+ function hasConfiguredTransform(
207
+ configuredTransforms: ConfiguredTransformSet,
208
+ transform: string,
209
+ resolved: string,
210
+ ): boolean {
211
+ return (
212
+ configuredTransforms.resolved.has(resolved) ||
213
+ (!isRelativePluginSpecifier(transform) &&
214
+ configuredTransforms.raw.has(transform))
215
+ );
216
+ }
217
+
218
+ function addConfiguredTransform(
219
+ configuredTransforms: ConfiguredTransformSet,
220
+ transform: string,
221
+ resolved: string,
222
+ ): void {
223
+ if (!isRelativePluginSpecifier(transform)) {
224
+ configuredTransforms.raw.add(transform);
225
+ }
226
+ configuredTransforms.resolved.add(resolved);
227
+ }
228
+
229
+ function directDependencyNames(manifest: PackageManifest): string[] {
230
+ const seen = new Set<string>();
231
+ const out: string[] = [];
232
+ for (const dependencies of [
233
+ manifest.dependencies,
234
+ manifest.devDependencies,
235
+ ]) {
236
+ if (!isRecord(dependencies)) {
237
+ continue;
238
+ }
239
+ for (const name of Object.keys(dependencies)) {
240
+ if (seen.has(name)) {
241
+ continue;
242
+ }
243
+ seen.add(name);
244
+ out.push(name);
245
+ }
246
+ }
247
+ return out;
248
+ }
249
+
250
+ function resolveDependencyPackageJson(
251
+ name: string,
252
+ projectRoot: string,
253
+ ): string | undefined {
254
+ const direct = path.join(projectRoot, "node_modules", ...name.split("/"));
255
+ const directManifest = path.join(direct, "package.json");
256
+ if (fs.existsSync(directManifest)) {
257
+ return resolveRealPath(directManifest);
258
+ }
259
+ const projectPackage = path.join(projectRoot, "package.json");
260
+ const projectRequire = createRequire(projectPackage);
261
+ try {
262
+ return resolveRealPath(projectRequire.resolve(`${name}/package.json`));
263
+ } catch {
264
+ try {
265
+ return findNearestPackageJson(projectRequire.resolve(name));
266
+ } catch {
267
+ return undefined;
268
+ }
269
+ }
270
+ }
271
+
272
+ function findNearestPackageJson(location: string): string | undefined {
273
+ let current = fs.statSync(location).isDirectory()
274
+ ? location
275
+ : path.dirname(location);
276
+ while (true) {
277
+ const manifest = path.join(current, "package.json");
278
+ if (fs.existsSync(manifest)) {
279
+ return resolveRealPath(manifest);
280
+ }
281
+ const parent = path.dirname(current);
282
+ if (parent === current) {
283
+ return undefined;
284
+ }
285
+ current = parent;
286
+ }
287
+ }
288
+
289
+ function readPackageManifest(file: string): PackageManifest | undefined {
290
+ if (!fs.existsSync(file)) {
291
+ return undefined;
292
+ }
293
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
294
+ return isRecord(parsed) ? (parsed as PackageManifest) : undefined;
295
+ }
296
+
297
+ function readPackagePluginConfig(
298
+ packageName: string,
299
+ manifest: PackageManifest | undefined,
300
+ ): ITtscProjectPluginConfig | undefined {
301
+ const ttsc = manifest?.ttsc;
302
+ if (!isRecord(ttsc) || !("plugin" in ttsc)) {
303
+ return undefined;
304
+ }
305
+ const plugin = ttsc.plugin;
306
+ if (!isRecord(plugin) || Array.isArray(plugin)) {
307
+ throw new Error(
308
+ `ttsc: package ${JSON.stringify(packageName)} declares invalid "ttsc.plugin"; expected an object`,
309
+ );
310
+ }
311
+ if (typeof plugin.transform !== "string" || plugin.transform.length === 0) {
312
+ throw new Error(
313
+ `ttsc: package ${JSON.stringify(packageName)} declares invalid "ttsc.plugin.transform"; expected a non-empty string`,
314
+ );
315
+ }
316
+ return { ...plugin } as ITtscProjectPluginConfig;
317
+ }
318
+
319
+ function isRecord(value: unknown): value is Record<string, unknown> {
320
+ return typeof value === "object" && value !== null;
321
+ }
322
+
323
+ function orderNativePlugins(
324
+ plugins: readonly ITtscLoadedNativePlugin[],
325
+ ): ITtscLoadedNativePlugin[] {
326
+ return [
327
+ ...plugins.filter((plugin) => plugin.stage === "check"),
328
+ ...plugins.filter((plugin) => plugin.stage === "transform"),
329
+ ];
330
+ }
331
+
85
332
  function loadPluginEntry(
86
333
  entry: ITtscProjectPluginConfig,
87
334
  context: ITtscPluginFactoryContext,
335
+ baseDir: string,
88
336
  ): ITtscPlugin {
89
337
  const specifier = entry.transform;
90
338
  if (typeof specifier !== "string" || specifier.length === 0) {
91
339
  throw new Error(`ttsc: plugin entry is missing a string "transform" field`);
92
340
  }
93
341
 
94
- const request = resolvePluginRequest(specifier, context.projectRoot);
342
+ const request = resolvePluginRequest(specifier, baseDir);
95
343
  const mod = require(request) as {
96
344
  createTtscPlugin?: TtscPluginFactory;
97
345
  default?: ITtscPlugin | TtscPluginFactory;
@@ -170,12 +418,20 @@ function isPluginStage(value: string): value is TtscPluginStage {
170
418
 
171
419
  function resolvePluginRequest(specifier: string, projectRoot: string): string {
172
420
  if (path.isAbsolute(specifier)) {
173
- return specifier;
421
+ return resolveRealPath(specifier);
174
422
  }
175
423
  if (isRelativePluginSpecifier(specifier)) {
176
- return path.resolve(projectRoot, specifier);
424
+ return resolveRealPath(path.resolve(projectRoot, specifier));
425
+ }
426
+ return resolveRealPath(require.resolve(specifier, { paths: [projectRoot] }));
427
+ }
428
+
429
+ function resolveRealPath(location: string): string {
430
+ try {
431
+ return fs.realpathSync(location);
432
+ } catch {
433
+ return location;
177
434
  }
178
- return require.resolve(specifier, { paths: [projectRoot] });
179
435
  }
180
436
 
181
437
  function isRelativePluginSpecifier(specifier: string): boolean {
@@ -35,6 +35,17 @@ export interface ITtscCompilerContext {
35
35
  */
36
36
  tsconfig?: string;
37
37
 
38
+ /**
39
+ * Project root override for generated config wrappers.
40
+ *
41
+ * Most callers should leave this unset so the project root is the directory
42
+ * containing {@link ITtscCompilerContext.tsconfig}. Embedders that synthesize
43
+ * a temporary config extending a real project config can set this to keep
44
+ * plugin resolution, cache paths, and native sidecar `--cwd` anchored to the
45
+ * real project.
46
+ */
47
+ projectRoot?: string;
48
+
38
49
  /**
39
50
  * Explicit TypeScript-Go executable for controlled embedding.
40
51
  *
@@ -73,9 +84,12 @@ export interface ITtscCompilerContext {
73
84
  /**
74
85
  * Plugin entries for this compiler instance.
75
86
  *
76
- * - `undefined`: read `compilerOptions.plugins` from the project config.
77
- * - `false`: ignore project plugins for this compiler instance.
78
- * - array: use these plugin entries instead of the project config entries.
87
+ * - `undefined`: read project plugins from `compilerOptions.plugins` and
88
+ * directly installed package markers.
89
+ * - `false`: ignore project config plugins and package markers for this
90
+ * compiler instance.
91
+ * - array: use these plugin entries instead of project config entries and
92
+ * package markers.
79
93
  *
80
94
  * Plugin entries are resolved once per operation from this instance context.
81
95
  * Per-call plugin overrides are intentionally not part of the public API.
@@ -34,6 +34,11 @@ export namespace ITtscCompilerResult {
34
34
  */
35
35
  type: "success";
36
36
 
37
+ /**
38
+ * Non-fatal diagnostics reported during compilation.
39
+ */
40
+ diagnostics?: ITtscCompilerDiagnostic[];
41
+
37
42
  /**
38
43
  * The generated compiler output.
39
44
  *
@@ -19,6 +19,9 @@ export namespace ITtscCompilerTransformation {
19
19
  /** Indicates that transformation completed without diagnostics. */
20
20
  type: "success";
21
21
 
22
+ /** Non-fatal diagnostics reported during transformation. */
23
+ diagnostics?: ITtscCompilerDiagnostic[];
24
+
22
25
  /** Transformed TypeScript source text keyed by project file path. */
23
26
  typescript: Record<string, string>;
24
27
  }
@@ -43,10 +43,13 @@ export interface ITtscPluginFactoryContext<T = ITtscProjectPluginConfig> {
43
43
  plugin: T;
44
44
 
45
45
  /**
46
- * Directory containing the resolved tsconfig/jsconfig.
46
+ * Project root used as the native plugin working directory and package
47
+ * discovery base.
47
48
  *
48
- * Relative plugin module specifiers and plugin source paths are resolved from
49
- * this directory, matching TypeScript plugin config behavior.
49
+ * By default this is the directory containing the resolved tsconfig/jsconfig.
50
+ * Callers may override it when a wrapper config lives outside the project.
51
+ * Relative `source` paths returned by a plugin descriptor resolve from this
52
+ * directory.
50
53
  */
51
54
  projectRoot: string;
52
55