ttsc 0.11.0 → 0.12.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 (79) hide show
  1. package/README.md +25 -6
  2. package/cmd/ttsc/api_compile.go +101 -106
  3. package/cmd/ttsc/api_compile_branches_test.go +81 -0
  4. package/cmd/ttsc/api_transform.go +49 -54
  5. package/cmd/ttsc/api_transform_branches_test.go +41 -0
  6. package/cmd/ttsc/build.go +108 -109
  7. package/cmd/ttsc/build_branches_test.go +130 -0
  8. package/cmd/ttsc/helpers_test.go +47 -0
  9. package/cmd/ttsc/main.go +104 -103
  10. package/cmd/ttsc/main_alias_branches_test.go +29 -0
  11. package/cmd/ttscserver/main.go +104 -96
  12. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +42 -0
  13. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +23 -25
  14. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +44 -0
  15. package/cmd/utility-host/main.go +42 -0
  16. package/driver/lsp.go +60 -0
  17. package/driver/plugins.go +122 -0
  18. package/driver/program.go +346 -356
  19. package/driver/rewrite.go +336 -339
  20. package/{driver → internal/lspserver}/lsp_envelope.go +70 -64
  21. package/{driver → internal/lspserver}/lsp_frame.go +62 -62
  22. package/{driver → internal/lspserver}/lsp_plugin.go +58 -58
  23. package/internal/lspserver/lsp_proxy.go +391 -0
  24. package/internal/lspserver/lsp_server.go +231 -0
  25. package/lib/TtscCompiler.d.ts +3 -3
  26. package/lib/TtscCompiler.js +20 -6
  27. package/lib/TtscCompiler.js.map +1 -1
  28. package/lib/compiler/internal/resolveTsgo.d.ts +1 -0
  29. package/lib/compiler/internal/resolveTsgo.js +15 -4
  30. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  31. package/lib/compiler/internal/runBuild.js +12 -4
  32. package/lib/compiler/internal/runBuild.js.map +1 -1
  33. package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -33
  34. package/lib/compiler/internal/sharedHostHelpers.js +27 -70
  35. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  36. package/lib/compiler/internal/transformProjectInMemory.js +12 -5
  37. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  38. package/lib/index.d.ts +0 -1
  39. package/lib/index.js +0 -1
  40. package/lib/index.js.map +1 -1
  41. package/lib/launcher/internal/runTtsc.js +9 -4
  42. package/lib/launcher/internal/runTtsc.js.map +1 -1
  43. package/lib/launcher/internal/runTtscserver.d.ts +1 -0
  44. package/lib/launcher/internal/runTtscserver.js +38 -1
  45. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  46. package/lib/plugin/internal/buildSourcePlugin.d.ts +3 -0
  47. package/lib/plugin/internal/buildSourcePlugin.js +444 -41
  48. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  49. package/lib/plugin/internal/loadProjectPlugins.js +153 -34
  50. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  51. package/lib/structures/ITtscCompilerContext.d.ts +2 -1
  52. package/lib/structures/ITtscPlugin.d.ts +8 -13
  53. package/lib/structures/ITtscPluginFactoryContext.d.ts +4 -5
  54. package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
  55. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +4 -2
  56. package/package.json +8 -8
  57. package/shim/lsp/shim.go +11 -17
  58. package/src/TtscCompiler.ts +20 -7
  59. package/src/compiler/internal/resolveTsgo.ts +18 -5
  60. package/src/compiler/internal/runBuild.ts +17 -4
  61. package/src/compiler/internal/sharedHostHelpers.ts +34 -67
  62. package/src/compiler/internal/transformProjectInMemory.ts +18 -5
  63. package/src/index.ts +0 -1
  64. package/src/launcher/internal/runTtsc.ts +9 -4
  65. package/src/launcher/internal/runTtscserver.ts +43 -1
  66. package/src/plugin/internal/buildSourcePlugin.ts +475 -41
  67. package/src/plugin/internal/loadProjectPlugins.ts +199 -39
  68. package/src/structures/ITtscCompilerContext.ts +2 -1
  69. package/src/structures/ITtscPlugin.ts +9 -13
  70. package/src/structures/ITtscPluginFactoryContext.ts +4 -5
  71. package/src/structures/ITtscProjectPluginConfig.ts +2 -2
  72. package/src/structures/internal/ITtscLoadedNativePlugin.ts +4 -2
  73. package/utility/host.go +267 -1296
  74. package/driver/lsp_proxy.go +0 -394
  75. package/driver/lsp_server.go +0 -197
  76. package/lib/defineTtscPlugin.d.ts +0 -18
  77. package/lib/defineTtscPlugin.js +0 -7
  78. package/lib/defineTtscPlugin.js.map +0 -1
  79. package/src/defineTtscPlugin.ts +0 -26
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
 
5
5
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
6
- import { FIRST_PARTY_UTILITY_PLUGIN_NAMES } from "../../compiler/internal/sharedHostHelpers";
6
+ import type { ITtscPluginContributor } from "../../structures/ITtscPluginContributor";
7
7
  import type { ITtscPlugin } from "../../structures/ITtscPlugin";
8
8
  import type { ITtscPluginFactoryContext } from "../../structures/ITtscPluginFactoryContext";
9
9
  import type { ITtscProjectPluginConfig } from "../../structures/ITtscProjectPluginConfig";
@@ -12,6 +12,8 @@ import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoa
12
12
  import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
13
13
  import { buildSourcePlugin } from "./buildSourcePlugin";
14
14
 
15
+ const GO_MOD_SEARCH_MAX_DEPTH = 3;
16
+
15
17
  type TtscPluginFactory<T = ITtscProjectPluginConfig> = (
16
18
  context: ITtscPluginFactoryContext<T>,
17
19
  ) => ITtscPlugin;
@@ -76,30 +78,105 @@ export function loadProjectPlugins(options: {
76
78
  ),
77
79
  );
78
80
 
79
- const nativePlugins: ITtscLoadedNativePlugin[] = [];
80
81
  const ttscVersion = readTtscVersion();
81
82
  const tsgoVersion = readTsgoVersion(context.projectRoot);
82
- plugins.forEach((plugin, index) => {
83
+ const records = plugins.map((plugin, index) => {
83
84
  const stage = resolvePluginStage(plugin);
84
85
  validatePluginSource(plugin);
85
86
  const contributors = validatePluginContributors(plugin);
86
- const binary = buildSourcePlugin({
87
- baseDir: context.projectRoot,
88
- cacheDir: options.cacheDir,
87
+ const source = resolvePluginSource(plugin.source, context.projectRoot);
88
+ const kind = resolveNativeSourceKind(source, plugin, entries[index]!.config, index);
89
+ if (kind === "linked" && stage !== "transform") {
90
+ throw new Error(
91
+ `ttsc: plugin "${pluginLabel(plugin, entries[index]!.config, index)}" source is a linked Go package, but only transform-stage plugins can be linked into a compiler host`,
92
+ );
93
+ }
94
+ const linkedContributorName =
95
+ kind === "linked" ? `linked_${String(index).padStart(6, "0")}` : undefined;
96
+ return {
89
97
  contributors,
90
- pluginName: plugin.name,
91
- source: plugin.source,
92
- ttscVersion,
93
- tsgoVersion,
94
- });
95
- nativePlugins.push({
96
- binary,
97
98
  config: entries[index]!.config,
98
- contributors,
99
+ kind,
100
+ label: pluginLabel(plugin, entries[index]!.config, index),
101
+ linkedContributorName,
99
102
  name: plugin.name,
100
- source: plugin.source,
103
+ source,
101
104
  stage,
102
- });
105
+ };
106
+ });
107
+ const linkedContributors = records
108
+ .filter((record) => record.stage === "transform")
109
+ .flatMap((record) =>
110
+ record.kind === "linked"
111
+ ? [{ name: record.linkedContributorName!, source: record.source }]
112
+ : [],
113
+ );
114
+ const transformHosts = records.filter(
115
+ (record) => record.stage === "transform" && record.kind === "executable",
116
+ );
117
+ const hostContributors =
118
+ linkedContributors.length === 0 ? undefined : linkedContributors;
119
+ const builtTransformHosts = new Map<object, string>();
120
+ for (const record of transformHosts) {
121
+ builtTransformHosts.set(
122
+ record,
123
+ buildSourcePlugin({
124
+ baseDir: context.projectRoot,
125
+ cacheDir: options.cacheDir,
126
+ contributors: mergeContributors(record.contributors, hostContributors),
127
+ pluginName: record.label,
128
+ source: record.source,
129
+ ttscVersion,
130
+ tsgoVersion,
131
+ }),
132
+ );
133
+ }
134
+ const fallbackDriverHost =
135
+ transformHosts.length === 0 && linkedContributors.length !== 0
136
+ ? buildSourcePlugin({
137
+ baseDir: context.projectRoot,
138
+ cacheDir: options.cacheDir,
139
+ contributors: linkedContributors,
140
+ label: "linked plugin host",
141
+ pluginName: "linked-plugin-host",
142
+ source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
143
+ ttscVersion,
144
+ tsgoVersion,
145
+ })
146
+ : undefined;
147
+ const selectedTransformHost =
148
+ transformHosts.length === 0
149
+ ? fallbackDriverHost
150
+ : builtTransformHosts.get(transformHosts[0]!);
151
+ const nativePlugins: ITtscLoadedNativePlugin[] = records.map((record) => {
152
+ const binary =
153
+ record.stage === "transform" && record.kind === "linked"
154
+ ? selectedTransformHost
155
+ : record.stage === "transform"
156
+ ? builtTransformHosts.get(record)
157
+ : buildSourcePlugin({
158
+ baseDir: context.projectRoot,
159
+ cacheDir: options.cacheDir,
160
+ contributors: record.contributors,
161
+ pluginName: record.label,
162
+ source: record.source,
163
+ ttscVersion,
164
+ tsgoVersion,
165
+ });
166
+ if (binary === undefined) {
167
+ throw new Error(
168
+ `ttsc: plugin "${record.label}" is a linked Go package, but no compiler host is available`,
169
+ );
170
+ }
171
+ return {
172
+ binary,
173
+ config: record.config,
174
+ contributors: record.contributors,
175
+ kind: record.kind,
176
+ name: record.name,
177
+ source: record.source,
178
+ stage: record.stage,
179
+ };
103
180
  });
104
181
  return {
105
182
  nativePlugins: orderNativePlugins(nativePlugins),
@@ -127,22 +204,6 @@ function composePluginSources(
127
204
  }
128
205
  }
129
206
  }
130
- // First-party utility plugin names are never legal composes targets for
131
- // user / third-party plugins. The first-party shared compiler host has its
132
- // own opt-in path through the manifest-pinned whitelist in
133
- // `sharedHostHelpers.ts::isFirstPartyUtilityTransformPlugin`. Letting any
134
- // descriptor borrow the binary of `@ttsc/banner` etc. would bypass that
135
- // pin and turn `composes` into a supply-chain redirect vector.
136
- for (const { plugin } of aggregates) {
137
- for (const target of plugin.composes!) {
138
- if (FIRST_PARTY_UTILITY_PLUGIN_NAMES.has(target)) {
139
- throw new Error(
140
- `ttsc: plugin "${plugin.name}" cannot compose first-party utility "${target}"; ` +
141
- `first-party utility plugins are composed automatically through their shared compiler host`,
142
- );
143
- }
144
- }
145
- }
146
207
  // Composition is intentionally one hop only: A.composes=[B] sends B to A's
147
208
  // binary, but if B.composes=[C] then C uses B's original source and does NOT
148
209
  // cascade to A. Detect cycles (A.composes=[B] && B.composes=[A]) and throw,
@@ -484,11 +545,7 @@ function loadPluginEntry(
484
545
  }
485
546
 
486
547
  function isTtscPlugin(value: unknown): value is ITtscPlugin {
487
- return (
488
- typeof value === "object" &&
489
- value !== null &&
490
- typeof (value as { name?: unknown }).name === "string"
491
- );
548
+ return typeof value === "object" && value !== null && !Array.isArray(value);
492
549
  }
493
550
 
494
551
  function rejectJsTransformFunctions(
@@ -523,10 +580,101 @@ function resolvePluginStage(plugin: ITtscPlugin): TtscPluginStage {
523
580
 
524
581
  function validatePluginSource(plugin: ITtscPlugin): void {
525
582
  if (typeof plugin.source !== "string" || plugin.source.length === 0) {
526
- throw new Error(`ttsc: plugin "${plugin.name}" must declare source`);
583
+ throw new Error(`ttsc: plugin must declare source`);
527
584
  }
528
585
  }
529
586
 
587
+ function pluginLabel(
588
+ plugin: ITtscPlugin,
589
+ config: ITtscProjectPluginConfig,
590
+ index: number,
591
+ ): string {
592
+ if (typeof plugin.name === "string" && plugin.name.length !== 0) {
593
+ return plugin.name;
594
+ }
595
+ if (typeof config.transform === "string" && config.transform.length !== 0) {
596
+ return config.transform;
597
+ }
598
+ return `#${index}`;
599
+ }
600
+
601
+ function resolvePluginSource(source: string, projectRoot: string): string {
602
+ return resolveRealPath(
603
+ path.isAbsolute(source) ? source : path.resolve(projectRoot, source),
604
+ );
605
+ }
606
+
607
+ function resolveNativeSourceKind(
608
+ source: string,
609
+ plugin: ITtscPlugin,
610
+ config: ITtscProjectPluginConfig,
611
+ index: number,
612
+ ): "executable" | "linked" {
613
+ const packageDir = resolveGoPackageDir(source, pluginLabel(plugin, config, index));
614
+ if (findNearestGoMod(packageDir, GO_MOD_SEARCH_MAX_DEPTH) === null) {
615
+ throw new Error(
616
+ `ttsc: plugin "${pluginLabel(plugin, config, index)}" source must be inside a Go module with go.mod within ${GO_MOD_SEARCH_MAX_DEPTH} parent directories: ${source}`,
617
+ );
618
+ }
619
+ const packageName = readGoPackageName(packageDir);
620
+ if (packageName === null) {
621
+ throw new Error(
622
+ `ttsc: plugin "${pluginLabel(plugin, config, index)}" source must contain at least one non-test ".go" file with a package declaration: ${packageDir}`,
623
+ );
624
+ }
625
+ return packageName === "main" ? "executable" : "linked";
626
+ }
627
+
628
+ function findNearestGoMod(from: string, maxDepth: number): string | null {
629
+ let current = path.resolve(from);
630
+ let depth = 0;
631
+ while (true) {
632
+ const candidate = path.join(current, "go.mod");
633
+ if (fs.existsSync(candidate)) return candidate;
634
+ if (depth >= maxDepth) return null;
635
+ const parent = path.dirname(current);
636
+ if (parent === current) return null;
637
+ current = parent;
638
+ depth += 1;
639
+ }
640
+ }
641
+
642
+ function resolveGoPackageDir(source: string, label: string): string {
643
+ if (!fs.existsSync(source)) {
644
+ throw new Error(`ttsc: plugin "${label}" source does not exist: ${source}`);
645
+ }
646
+ const stat = fs.statSync(source);
647
+ if (stat.isFile() && path.basename(source) === "go.mod") {
648
+ return path.dirname(source);
649
+ }
650
+ if (stat.isDirectory()) {
651
+ return source;
652
+ }
653
+ throw new Error(
654
+ `ttsc: plugin "${label}" source must be a Go package directory or go.mod file: ${source}`,
655
+ );
656
+ }
657
+
658
+ function readGoPackageName(dir: string): string | null {
659
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
660
+ if (
661
+ !entry.isFile() ||
662
+ !entry.name.endsWith(".go") ||
663
+ entry.name.endsWith("_test.go")
664
+ ) {
665
+ continue;
666
+ }
667
+ const file = path.join(dir, entry.name);
668
+ for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
669
+ const match = /^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)\b/.exec(line);
670
+ if (match) {
671
+ return match[1]!;
672
+ }
673
+ }
674
+ }
675
+ return null;
676
+ }
677
+
530
678
  const CONTRIBUTOR_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
531
679
 
532
680
  function validatePluginContributors(
@@ -594,6 +742,14 @@ function validatePluginContributors(
594
742
  return out;
595
743
  }
596
744
 
745
+ function mergeContributors(
746
+ first: readonly ITtscPluginContributor[] | undefined,
747
+ second: readonly ITtscPluginContributor[] | undefined,
748
+ ): readonly ITtscPluginContributor[] | undefined {
749
+ const out = [...(first ?? []), ...(second ?? [])];
750
+ return out.length === 0 ? undefined : out;
751
+ }
752
+
597
753
  function isPluginStage(value: string): value is TtscPluginStage {
598
754
  return value === "transform" || value === "check";
599
755
  }
@@ -651,7 +807,7 @@ function readTtscVersion(): string {
651
807
  return cachedTtscVersion;
652
808
  }
653
809
  try {
654
- const file = path.resolve(__dirname, "..", "..", "..", "package.json");
810
+ const file = path.join(ttscPackageRoot(), "package.json");
655
811
  const pkg = JSON.parse(fs.readFileSync(file, "utf8")) as {
656
812
  version?: string;
657
813
  };
@@ -662,6 +818,10 @@ function readTtscVersion(): string {
662
818
  return cachedTtscVersion;
663
819
  }
664
820
 
821
+ function ttscPackageRoot(): string {
822
+ return path.resolve(__dirname, "..", "..", "..");
823
+ }
824
+
665
825
  function readTsgoVersion(projectRoot: string): string {
666
826
  try {
667
827
  const projectRequire = createRequire(
@@ -74,7 +74,8 @@ export interface ITtscCompilerContext {
74
74
  *
75
75
  * Relative paths are resolved from {@link ITtscCompilerContext.cwd}. When
76
76
  * omitted, ttsc uses its normal cache location: `TTSC_CACHE_DIR` when
77
- * present, otherwise `node_modules/.ttsc` or `.ttsc` under the project root.
77
+ * present, otherwise the user-level ttsc cache shared by projects on the same
78
+ * machine.
78
79
  *
79
80
  * The same cache stores source-plugin binaries and the lazily built native
80
81
  * compiler host used for in-memory API compilation.
@@ -7,24 +7,19 @@ import type { TtscPluginStage } from "./TtscPluginStage";
7
7
  * A JavaScript plugin entry in `compilerOptions.plugins[]` is only the loading
8
8
  * point. After ttsc resolves that JavaScript module, the module must return an
9
9
  * `ITtscPlugin` descriptor either directly, as `default`, as `plugin`, or from
10
- * `createTtscPlugin(context)`.
10
+ * a descriptor factory.
11
11
  *
12
- * The descriptor tells ttsc which Go command package implements the native
13
- * sidecar and where that sidecar participates in the TypeScript-Go pipeline.
14
- * ttsc then builds the Go source lazily with the bundled Go toolchain and
15
- * passes the original project plugin config to the sidecar through
16
- * `--plugins-json`.
12
+ * The descriptor tells ttsc which Go source package implements the native
13
+ * behavior and where it participates in the TypeScript-Go pipeline. ttsc then
14
+ * builds the Go source lazily with the bundled Go toolchain and passes the
15
+ * original project plugin config through `--plugins-json`.
17
16
  */
18
17
  export interface ITtscPlugin {
19
18
  /**
20
- * Stable plugin name used in diagnostics, build messages, and native plugin
21
- * manifests.
22
- *
23
- * Keep this stable across releases. Native sidecars and downstream tooling
24
- * can use the name to select their own config from the ordered
25
- * `--plugins-json` payload.
19
+ * Optional human-readable label used in diagnostics and build messages.
20
+ * Routing is never based on this value.
26
21
  */
27
- name: string;
22
+ name?: string;
28
23
 
29
24
  /**
30
25
  * Go command package directory, or a `go.mod` file, that ttsc lazily builds.
@@ -101,4 +96,5 @@ export interface ITtscPlugin {
101
96
  * unique within a single plugin build.
102
97
  */
103
98
  contributors?: ITtscPluginContributor[];
99
+
104
100
  }
@@ -1,13 +1,12 @@
1
1
  import type { ITtscProjectPluginConfig } from "./ITtscProjectPluginConfig";
2
2
 
3
3
  /**
4
- * Project context passed to `createTtscPlugin()` factories.
4
+ * Project context passed to plugin descriptor factories.
5
5
  *
6
6
  * Plugin packages may export a static {@link ITtscPlugin} descriptor when the
7
- * descriptor never depends on the consuming project. Export a
8
- * `createTtscPlugin(context)` factory instead when the descriptor needs to
9
- * inspect the original plugin config, the resolved tsconfig path, or the
10
- * project root.
7
+ * descriptor never depends on the consuming project. Export a factory when the
8
+ * descriptor needs to inspect the original plugin config, the resolved tsconfig
9
+ * path, or the project root.
11
10
  *
12
11
  * The factory runs in Node.js while ttsc is loading `compilerOptions.plugins`.
13
12
  * It should only create the descriptor. Heavy validation and TypeScript-Go work
@@ -34,8 +34,8 @@ export interface ITtscProjectPluginConfig {
34
34
  * resolution from that same directory.
35
35
  *
36
36
  * The loaded JavaScript module must export an {@link ITtscPlugin} descriptor
37
- * or a `createTtscPlugin(context)` factory. The Go implementation itself is
38
- * declared by the descriptor's {@link ITtscPlugin.source} field.
37
+ * or descriptor factory. The Go implementation itself is declared by the
38
+ * descriptor's {@link ITtscPlugin.source} field.
39
39
  */
40
40
  transform?: string;
41
41
 
@@ -10,8 +10,10 @@ export interface ITtscLoadedNativePlugin {
10
10
  config: ITtscProjectPluginConfig;
11
11
  /** Contributor Go sources statically linked into the binary, if any. */
12
12
  contributors?: readonly ITtscPluginContributor[];
13
- /** Stable plugin name used in diagnostics and native manifests. */
14
- name: string;
13
+ /** Whether this source owns a process or is linked into another host. */
14
+ kind: "executable" | "linked";
15
+ /** Optional human label used in diagnostics and native manifests. */
16
+ name?: string;
15
17
  /** Go source directory selected by the plugin descriptor. */
16
18
  source: string;
17
19
  /** Pipeline stage where the sidecar participates. */