ttsc 0.26.1 → 0.26.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 (38) hide show
  1. package/README.md +1 -1
  2. package/driver/diagnostics_preamble.go +284 -0
  3. package/driver/emit_plugin.go +51 -16
  4. package/driver/program.go +27 -14
  5. package/driver/rewrite.go +2 -2
  6. package/internal/graph/diagnostics.go +4 -3
  7. package/internal/graph/provenance.go +10 -1
  8. package/lib/compiler/internal/compileProjectInMemory.js +7 -0
  9. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  10. package/lib/compiler/internal/runBuild.d.ts +28 -0
  11. package/lib/compiler/internal/runBuild.js +41 -2
  12. package/lib/compiler/internal/runBuild.js.map +1 -1
  13. package/lib/compiler/internal/runSingleFileEmit.js +7 -0
  14. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  15. package/lib/flags/schema.js +21 -0
  16. package/lib/flags/schema.js.map +1 -1
  17. package/lib/launcher/internal/assertNoSolutionBuild.d.ts +31 -0
  18. package/lib/launcher/internal/assertNoSolutionBuild.js +38 -0
  19. package/lib/launcher/internal/assertNoSolutionBuild.js.map +1 -0
  20. package/lib/launcher/internal/prepareExecution.js +31 -12
  21. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  22. package/lib/launcher/internal/runTtsc.js +2 -0
  23. package/lib/launcher/internal/runTtsc.js.map +1 -1
  24. package/lib/launcher/internal/runTtsx.js +2 -0
  25. package/lib/launcher/internal/runTtsx.js.map +1 -1
  26. package/lib/launcher/internal/runtimeHooks.js +7 -0
  27. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  28. package/package.json +8 -8
  29. package/shim/compiler/emit_sourcemap.go +56 -15
  30. package/src/compiler/internal/compileProjectInMemory.ts +7 -0
  31. package/src/compiler/internal/runBuild.ts +77 -4
  32. package/src/compiler/internal/runSingleFileEmit.ts +7 -0
  33. package/src/flags/schema.ts +23 -0
  34. package/src/launcher/internal/assertNoSolutionBuild.ts +40 -0
  35. package/src/launcher/internal/prepareExecution.ts +28 -12
  36. package/src/launcher/internal/runTtsc.ts +2 -0
  37. package/src/launcher/internal/runTtsx.ts +2 -0
  38. package/src/launcher/internal/runtimeHooks.ts +7 -0
@@ -38,6 +38,34 @@ export type RunBuildOptions = TtscBuildOptions & {
38
38
  forceListEmittedFiles?: boolean;
39
39
  /** Keep every compiler-owned side product inside this private directory. */
40
40
  isolateOutputsTo?: string;
41
+ /**
42
+ * Hand tsgo the `rootDir` it would otherwise infer, for a build whose
43
+ * `outDir` this process injected rather than the project declaring it.
44
+ *
45
+ * Tsgo answers an inferred common source directory with TS5011 the moment an
46
+ * `outDir` is in play, so injecting one turns a project that declares no
47
+ * output at all — the `noEmit` check-only shape `tsgo`, `ttsc`, and `ttsc
48
+ * --emit` all accept — into one that must configure the layout of output the
49
+ * user never asked for and never sees (issue #1172).
50
+ *
51
+ * The pinned value is the one tsgo itself infers: with a config file in play
52
+ * its common source directory is that file's directory, never the computed
53
+ * common directory of the input files (`outputpaths.GetCommonSourceDirectory`
54
+ * consults the file list only for a config-less program). Pinning it
55
+ * therefore silences the demand without moving a single output, and it is the
56
+ * same source root `prepareExecution.ts::resolveRuntimeSourceRoot`,
57
+ * `runtimeHooks.ts::resolveDependencySourceRoot`, and
58
+ * `watchTopology.ts::inferPerSourceCompilerOutputs` already model on the
59
+ * JavaScript side.
60
+ *
61
+ * Ignored when the project declares its own `rootDir`: that project already
62
+ * satisfies tsgo, and overriding it would relocate the emit out from under
63
+ * every consumer that mirrors the declared root.
64
+ *
65
+ * Never set for a user-supplied `--outDir`. That outDir is the user's own
66
+ * request, and TS5011 is then tsgo's genuine answer to it.
67
+ */
68
+ pinInferredRootDir?: boolean;
41
69
  /**
42
70
  * Receives selected native-plugin source roots after the project resolves.
43
71
  * The watch launcher uses these roots to invalidate a sidecar when its Go
@@ -1084,7 +1112,7 @@ function forwardsInternalShadowFlag(
1084
1112
  * inside it via the `--plugins-json` flag.
1085
1113
  */
1086
1114
  function buildWithNativeCompilerPlugins(
1087
- options: TtscBuildOptions,
1115
+ options: RunBuildOptions,
1088
1116
  execution: ReturnType<typeof resolveExecutionContext>,
1089
1117
  plugins: readonly ITtscLoadedNativePlugin[],
1090
1118
  timing: BuildTiming,
@@ -1245,6 +1273,9 @@ function createTsgoBuildArgs(
1245
1273
  if (options.forceRuntimeSourceMap === true) {
1246
1274
  args.push("--sourceMap", "true");
1247
1275
  }
1276
+ // Pushed before passthrough for the same reason: a user `--rootDir` still
1277
+ // wins over the value ttsc pins for its own injected `outDir`.
1278
+ args.push(...pinnedRootDirArgs(execution, options));
1248
1279
  } else if (options.emit === false) {
1249
1280
  args.push("--noEmit");
1250
1281
  }
@@ -1264,6 +1295,37 @@ function createTsgoBuildArgs(
1264
1295
  return args;
1265
1296
  }
1266
1297
 
1298
+ /**
1299
+ * The explicit `--rootDir` a caller-injected `outDir` needs, or nothing.
1300
+ *
1301
+ * See {@link RunBuildOptions.pinInferredRootDir} for why an injected `outDir`
1302
+ * needs one at all. Three conditions gate it, and each one is load-bearing:
1303
+ *
1304
+ * - The caller injected the `outDir` itself, so a user `--outDir` keeps tsgo's
1305
+ * own TS5011 answer;
1306
+ * - This pass emits, because a no-emit pass has no layout to pin (tsgo skips the
1307
+ * check for `noEmit` too);
1308
+ * - The project declares no `rootDir` of its own, so a declared layout is never
1309
+ * overridden.
1310
+ *
1311
+ * `execution.projectRoot` is the directory of the tsconfig ttsc resolved and
1312
+ * hands tsgo as `-p`, which is exactly the directory tsgo infers, and it is
1313
+ * spelled the way tsgo will spell the input file names it compares against it —
1314
+ * both come from the same `fs.realpathSync` pass. Resolving it any further (a
1315
+ * Windows 8.3 expansion, say) would leave the comparison lexically mixed, and
1316
+ * `ContainsPath` is lexical: every input would count as outside `rootDir` and
1317
+ * tsgo would emit it beside the user's source instead of under `outDir`.
1318
+ */
1319
+ function pinnedRootDirArgs(
1320
+ execution: ReturnType<typeof resolveExecutionContext>,
1321
+ options: RunBuildOptions,
1322
+ ): string[] {
1323
+ if (options.pinInferredRootDir !== true) return [];
1324
+ if (options.emit !== true) return [];
1325
+ if (typeof execution.project.compilerOptions.rootDir === "string") return [];
1326
+ return ["--rootDir", execution.projectRoot];
1327
+ }
1328
+
1267
1329
  /**
1268
1330
  * Return `["--pretty", "false"]` when structured diagnostics are requested so
1269
1331
  * that the output can be parsed line-by-line, or an empty array otherwise.
@@ -1304,7 +1366,7 @@ function createTsgoThreadingArgs(options: TtscCommonOptions): string[] {
1304
1366
  /** Build the argument list for a native plugin `build`/`check` invocation. */
1305
1367
  function createNativeBuildArgs(
1306
1368
  execution: ReturnType<typeof resolveExecutionContext>,
1307
- options: TtscBuildOptions,
1369
+ options: RunBuildOptions,
1308
1370
  plugins: readonly ITtscLoadedNativePlugin[],
1309
1371
  ): string[] {
1310
1372
  const args = [
@@ -1339,7 +1401,12 @@ function createNativeBuildArgs(
1339
1401
  args.push("--quiet");
1340
1402
  }
1341
1403
  }
1342
- args.push(...createNativeTsgoArgs(options));
1404
+ // The sidecar builds its Program in-process, so the pinned `rootDir` has to
1405
+ // travel the same channel every other tsgo option takes to it — the host flag
1406
+ // set does not declare `--rootDir`, and `filterHostArgs` would strip it.
1407
+ args.push(
1408
+ ...createNativeTsgoArgs(options, pinnedRootDirArgs(execution, options)),
1409
+ );
1343
1410
  return args;
1344
1411
  }
1345
1412
 
@@ -1488,8 +1555,14 @@ function transformHostTimingLabel(
1488
1555
  * a plugin build the same way it reaches the plain tsgo lane. Encoded as a
1489
1556
  * single token so the sidecars' unknown-flag filters keep it intact.
1490
1557
  */
1491
- function createNativeTsgoArgs(options: TtscCommonOptions): string[] {
1558
+ function createNativeTsgoArgs(
1559
+ options: TtscCommonOptions,
1560
+ leading: readonly string[] = [],
1561
+ ): string[] {
1492
1562
  const passthrough = [
1563
+ // Ahead of the user's own flags, so the same precedence holds here as on
1564
+ // the direct tsgo lane: whatever the user forwarded wins.
1565
+ ...leading,
1493
1566
  ...(nativeTsgoPassthroughArgs(options) ?? []),
1494
1567
  ...isolatedTsgoOutputArgs(options),
1495
1568
  ];
@@ -41,6 +41,13 @@ export function runSingleFileEmit(options: TtscSingleFileEmitOptions): string {
41
41
  forceListEmittedFiles: true,
42
42
  isolateOutputsTo: outDir,
43
43
  outDir,
44
+ // The private temp directory above is an `outDir` this lane injected, not
45
+ // one the project declared, and tsgo answers an inferred common source
46
+ // directory with TS5011 as soon as any `outDir` is in play. Pinning the
47
+ // root tsgo would infer keeps `ttsc <file.ts>` working on a project that
48
+ // declares no output at all, and it is the same root
49
+ // `resolveEmittedJavaScript` mirrors below (issue #1172).
50
+ pinInferredRootDir: true,
44
51
  resolvedProject: project,
45
52
  tsconfig,
46
53
  });
@@ -219,6 +219,29 @@ export const FLAG_SCHEMA: readonly FlagSpec[] = [
219
219
  description: "Print the launcher version and exit.",
220
220
  },
221
221
 
222
+ // -------------------------------------------------------------------------
223
+ // Solution build mode — declared so the launcher can refuse it in its own
224
+ // voice instead of forwarding it.
225
+ //
226
+ // `createTsgoBuildArgs` opens the forwarded argv with `-p <tsconfig>` because
227
+ // ttsc resolves the project itself (extends chains, plugin config discovery,
228
+ // cache keys, resident session identity) and pins the result. A forwarded
229
+ // `--build` therefore always lands after `-p`, and tsgo answers with TS6369
230
+ // "Option '--build' must be the first command line argument" — a diagnostic
231
+ // that contradicts the command line the user actually typed. ttsc's plugin,
232
+ // cache, and emit architecture is built around one resolved project, so
233
+ // solution mode is unsupported rather than merely misordered.
234
+ // -------------------------------------------------------------------------
235
+ {
236
+ name: "--build",
237
+ aliases: ["-b"],
238
+ kind: "boolean",
239
+ subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
240
+ consumedBy: ["launcher"],
241
+ description:
242
+ "Refused by the launcher: ttsc pins one resolved project, so tsgo's solution-build mode is unsupported.",
243
+ },
244
+
222
245
  // -------------------------------------------------------------------------
223
246
  // Project location: shared by every subcommand.
224
247
  // -------------------------------------------------------------------------
@@ -0,0 +1,40 @@
1
+ import { type ParseResult } from "../../flags/parser";
2
+
3
+ /**
4
+ * Refuse tsgo's solution-build mode (`--build` / `-b`) in the launcher's own
5
+ * voice, for whichever launcher parsed the arguments.
6
+ *
7
+ * Every ttsc build lane hands tsgo an argument list that opens with the project
8
+ * ttsc resolved (`-p <tsconfig>` in `createTsgoBuildArgs`), because ttsc — not
9
+ * tsgo — owns the `extends` chain, plugin config discovery, cache keys, and the
10
+ * resident session's identity, and pins that one resolved project instead of
11
+ * forwarding raw argv. A forwarded `--build` therefore always arrives after
12
+ * `-p`, and tsgo replies "Option '--build' must be the first command line
13
+ * argument" even though the user wrote it first. No spelling of the command
14
+ * line can satisfy that diagnostic, so the flag is rejected here rather than
15
+ * forwarded into an error that blames the user's argument order.
16
+ *
17
+ * Both launchers refuse it, because both reach tsgo through the same pinned
18
+ * project. `ttsx` additionally builds one entry inside that project, so a mode
19
+ * whose whole purpose is compiling many projects has nothing to mean there
20
+ * either.
21
+ *
22
+ * Presence — not the parsed boolean — is what is refused: `--build false` still
23
+ * asks for a mode ttsc does not implement, and silently consuming it would drop
24
+ * the flag with no diagnostic at all.
25
+ *
26
+ * What this deliberately does not reach is a `--build` that belongs to the
27
+ * program `ttsx` is running. `runTtsx` parses with
28
+ * `forwardAfterFirstPositional` and `honorDoubleDashSeparator`, so a token
29
+ * after the entry lands in `result.tail` and never in `result.values`; reading
30
+ * `values` is what keeps `ttsx script.ts --build` the user program's own flag.
31
+ */
32
+ export function assertNoSolutionBuild(
33
+ result: ParseResult,
34
+ errorPrefix: string,
35
+ ): void {
36
+ if (!result.values.has("--build")) return;
37
+ throw new Error(
38
+ `${errorPrefix} --build (solution mode) is not supported; ttsc resolves and pins one project per run, so compile each referenced project with its own ttsc -p <tsconfig> run`,
39
+ );
40
+ }
@@ -57,7 +57,6 @@ export function prepareExecution(
57
57
  const context = createProjectContext(
58
58
  cwd,
59
59
  path.resolve(cwd, entryFile),
60
- entry,
61
60
  options,
62
61
  );
63
62
  try {
@@ -151,13 +150,10 @@ function resolveEntrySpelling(cwd: string, entryFile: string): string {
151
150
  /**
152
151
  * @param discoveryFile - The entry as the user named it. Project discovery
153
152
  * walks up from here, so it must not be retargeted through a symlink.
154
- * @param entry - The entry in its physical spelling, which is what the emit
155
- * layout and the runtime hooks both speak.
156
153
  */
157
154
  function createProjectContext(
158
155
  cwd: string,
159
156
  discoveryFile: string,
160
- entry: string,
161
157
  options: NonNullable<Parameters<typeof prepareExecution>[1]>,
162
158
  ) {
163
159
  const project = readProjectConfig(
@@ -179,7 +175,7 @@ function createProjectContext(
179
175
  const virtualRoot = path.join(processDir, "fs");
180
176
  // Resolved once: it now costs a realpath (and, for a missing directory on
181
177
  // Windows, a case-sensitivity probe) rather than a string join.
182
- const runtimeRootDir = resolveRuntimeSourceRoot(project, entry);
178
+ const runtimeRootDir = resolveRuntimeSourceRoot(project);
183
179
  return {
184
180
  project,
185
181
  tsconfig,
@@ -217,6 +213,16 @@ function createProjectContext(
217
213
  * The source-tree root the emit mirrors, in the same physical spelling as the
218
214
  * entry it will be compared against.
219
215
  *
216
+ * Undeclared, the root is the project's own directory, because that is the one
217
+ * tsgo uses: with a config file in play `GetCommonSourceDirectory` answers that
218
+ * file's directory and never computes a common directory of the input files.
219
+ * The entry's directory is not that root — it is only the same directory when
220
+ * the entry happens to sit beside the tsconfig, which is precisely why a
221
+ * `src/`-shaped project mislaid its emit here (issue #1172) while a flat one
222
+ * worked. `runtimeHooks.ts::resolveDependencySourceRoot` and
223
+ * `watchTopology.ts::inferPerSourceCompilerOutputs` already model the same
224
+ * rule, and `runBuild.ts::pinnedRootDirArgs` pins it for tsgo itself.
225
+ *
220
226
  * Resolving it is the other half of `resolveEntrySpelling`, and skipping it
221
227
  * leaves the comparison mixed rather than merely imprecise. `project.root`
222
228
  * arrives through plain `fs.realpathSync`, which resolves reparse points but
@@ -227,22 +233,25 @@ function createProjectContext(
227
233
  * way the gate reads an in-project entry as outside its own root, pays a second
228
234
  * whole build for it, and publishes a wider root than the project has.
229
235
  *
230
- * `path.dirname(entry)` is already physical, so only the declared branch needs
231
- * the pass.
236
+ * The pass costs nothing in agreement with the root tsgo was pinned to, which
237
+ * stays unresolved on purpose so it matches the spelling tsgo gives the input
238
+ * file names it compares against it. Only the prefix differs between the two;
239
+ * each side strips its own, so the relative path below the root — the part that
240
+ * decides where the emit lands and where the lookup reads — is identical.
232
241
  */
233
242
  function resolveRuntimeSourceRoot(
234
243
  project: ReturnType<typeof readProjectConfig>,
235
- entry: string,
236
244
  ): string {
237
245
  const rootDir = project.compilerOptions.rootDir;
238
- if (typeof rootDir !== "string") {
239
- return path.dirname(entry);
240
- }
241
246
  const identities = createFilesystemPathIdentityContext({
242
247
  throwOnRealpathError: false,
243
248
  });
244
249
  return identities.resolve(
245
- path.isAbsolute(rootDir) ? rootDir : path.resolve(project.root, rootDir),
250
+ typeof rootDir !== "string"
251
+ ? project.root
252
+ : path.isAbsolute(rootDir)
253
+ ? rootDir
254
+ : path.resolve(project.root, rootDir),
246
255
  ).path;
247
256
  }
248
257
 
@@ -265,6 +274,13 @@ function buildProject(
265
274
  cacheDir: context.pluginCacheDir,
266
275
  outDir: context.emitDir,
267
276
  passthrough: options.passthrough,
277
+ // `context.emitDir` is ttsx's own temp directory, not an output the project
278
+ // asked for, and tsgo demands an explicit `rootDir` (TS5011) as soon as any
279
+ // `outDir` is in play. Pinning the root tsgo would infer keeps a check-only
280
+ // project runnable without moving its emit (issue #1172); a project that
281
+ // declares `rootDir` is left exactly as it is, which is also the root
282
+ // `resolveRuntimeSourceRoot` published above.
283
+ pinInferredRootDir: true,
268
284
  // Emit a source map on the transient entry emit (a PID-isolated temp dir,
269
285
  // never the consumer's `outDir`) so the serve path can inline it under the
270
286
  // source URL. Routed as a dedicated build option, not a forwarded tsgo
@@ -27,6 +27,7 @@ import {
27
27
  import type { ITtscProjectInputSnapshot } from "../../structures/internal/ITtscProjectInputSnapshot";
28
28
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
29
29
  import type { TtscSingleFileEmitOptions } from "../../structures/internal/TtscSingleFileEmitOptions";
30
+ import { assertNoSolutionBuild } from "./assertNoSolutionBuild";
30
31
  import { getCompilerVersionText } from "./getCompilerVersionText";
31
32
  import { resolveCacheDir } from "./resolveCacheDir";
32
33
  import { resolveSingleFileOutput } from "./singleFileOutput";
@@ -441,6 +442,7 @@ function parseBuildArgs(argv: readonly string[]) {
441
442
  isPositional: looksLikeInputFile,
442
443
  subcommand: "build",
443
444
  });
445
+ assertNoSolutionBuild(result, "ttsc:");
444
446
  // Defaults: pinned by the previous hand-parser. `quiet` defaults true,
445
447
  // `--verbose` flips it to false; `emit` defaults `undefined` so the resolved
446
448
  // project controls ordinary build mode. `runCompatibleBuild` applies the
@@ -11,6 +11,7 @@ import {
11
11
  parseFlags,
12
12
  } from "../../flags/parser";
13
13
  import { resolveFlagSpec } from "../../flags/schema";
14
+ import { assertNoSolutionBuild } from "./assertNoSolutionBuild";
14
15
  import { getCompilerVersionText } from "./getCompilerVersionText";
15
16
  import { prepareExecution } from "./prepareExecution";
16
17
  import { resolveCacheDir } from "./resolveCacheDir";
@@ -130,6 +131,7 @@ function parseCLI(argv: readonly string[]) {
130
131
  isPositional: looksLikeEntryFile,
131
132
  subcommand: "ttsx",
132
133
  });
134
+ assertNoSolutionBuild(result, "ttsx:");
133
135
 
134
136
  const entry = result.positional.find(looksLikeEntryFile);
135
137
  if (entry === undefined) {
@@ -1527,6 +1527,13 @@ function buildDependency(
1527
1527
  emit: true,
1528
1528
  forceListEmittedFiles: true,
1529
1529
  outDir: emitDir,
1530
+ // The generation directory is an `outDir` this lane injected, not one the
1531
+ // dependency declared, and tsgo demands an explicit `rootDir` (TS5011) as
1532
+ // soon as any `outDir` is in play. Pinning the root tsgo would infer keeps
1533
+ // a source-shipping dependency that declares no output buildable, and it is
1534
+ // the same root `resolveDependencySourceRoot` publishes for it below —
1535
+ // without it that dependency falls back to type-stripping (issue #1172).
1536
+ pinInferredRootDir: true,
1530
1537
  // Emit a source map on the transient dependency emit (it never reaches the
1531
1538
  // dependency's published `lib/`) so the serve path can inline it under the
1532
1539
  // source URL, but only when the dependency configures none itself. Routed