ttsc 0.22.0 → 0.23.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 (46) hide show
  1. package/go.mod +1 -1
  2. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  3. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  4. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  5. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  6. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  7. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  8. package/internal/lspserver/lsp_project_inputs.go +117 -49
  9. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  10. package/lib/flags/schema.js +1 -1
  11. package/lib/flags/schema.js.map +1 -1
  12. package/lib/internal/projectInputPathIdentity.d.ts +13 -5
  13. package/lib/internal/projectInputPathIdentity.js +95 -41
  14. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  15. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  16. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  17. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  18. package/lib/launcher/internal/runTtsc.js +5 -0
  19. package/lib/launcher/internal/runTtsc.js.map +1 -1
  20. package/lib/launcher/internal/runTtscserver.js +59 -8
  21. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  22. package/lib/launcher/internal/runTtsx.js +61 -7
  23. package/lib/launcher/internal/runTtsx.js.map +1 -1
  24. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  25. package/lib/launcher/internal/runtimeHooks.js +7 -15
  26. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  27. package/lib/launcher/internal/watchTopology.d.ts +82 -8
  28. package/lib/launcher/internal/watchTopology.js +535 -187
  29. package/lib/launcher/internal/watchTopology.js.map +1 -1
  30. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  31. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  32. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  33. package/lib/plugin/internal/loadProjectPlugins.js +29 -8
  34. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  35. package/package.json +12 -8
  36. package/src/flags/schema.ts +2 -1
  37. package/src/internal/projectInputPathIdentity.ts +150 -46
  38. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  39. package/src/launcher/internal/runTtsc.ts +7 -0
  40. package/src/launcher/internal/runTtscserver.ts +81 -11
  41. package/src/launcher/internal/runTtsx.ts +87 -10
  42. package/src/launcher/internal/runtimeHooks.ts +16 -16
  43. package/src/launcher/internal/watchTopology.ts +679 -208
  44. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  45. package/src/plugin/internal/loadProjectPlugins.ts +41 -8
  46. package/tools/shim_audit/baseline.json +0 -9
@@ -8,7 +8,11 @@ import { createNativeProjectContextArgs } from "../../compiler/internal/project/
8
8
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
9
9
  import { resolveBinary } from "../../compiler/internal/resolveBinary";
10
10
  import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
11
- import { resolveProjectInputPath } from "../../internal/projectInputPathIdentity";
11
+ import {
12
+ type ProjectInputPathIdentityContext,
13
+ createProjectInputPathIdentityContext,
14
+ resolveProjectInputPath,
15
+ } from "../../internal/projectInputPathIdentity";
12
16
  import {
13
17
  hasProjectPluginEntries,
14
18
  loadProjectPlugins,
@@ -313,8 +317,7 @@ function loadLSPProjectPlugins(
313
317
  binary: resolveBinary() ?? "",
314
318
  cwd,
315
319
  pluginConfigDir: pluginConfigOrigin,
316
- projectRoot: project.root,
317
- tsconfig: project.path,
320
+ tsconfig: project.identity.logicalConfigPath,
318
321
  })
319
322
  : { nativePlugins: [], project };
320
323
  }
@@ -472,9 +475,8 @@ export function fingerprintInitialLSPProjectInputSnapshot(
472
475
  const reloadDirectoryDigests: Record<string, string> = {};
473
476
  const reloadFileDigests: Record<string, string> = {};
474
477
  for (const directory of snapshot.reloadDirectories ?? []) {
475
- reloadDirectoryDigests[directory] = lspProjectInputDirectoryDigest(
476
- realLSPProjectInputPath(directory),
477
- );
478
+ reloadDirectoryDigests[directory] =
479
+ lspProjectInputReloadDirectoryDigest(directory);
478
480
  }
479
481
  for (const file of snapshot.reloadFiles ?? []) {
480
482
  reloadFileDigests[file] = lspProjectInputFileDigest(
@@ -495,7 +497,7 @@ export function initialLSPProjectInputSnapshotIsCurrent(
495
497
  (snapshot.reloadDirectories ?? []).every(
496
498
  (directory) =>
497
499
  snapshot.reloadDirectoryDigests[directory] ===
498
- lspProjectInputDirectoryDigest(realLSPProjectInputPath(directory)),
500
+ lspProjectInputReloadDirectoryDigest(directory),
499
501
  ) &&
500
502
  (snapshot.reloadFiles ?? []).every(
501
503
  (file) =>
@@ -524,16 +526,84 @@ function lspSelectionSignature(
524
526
  });
525
527
  }
526
528
 
527
- function lspProjectInputDirectoryDigest(location: string): string {
529
+ function lspProjectInputReloadDirectoryDigest(location: string): string {
530
+ const identities = createProjectInputPathIdentityContext();
531
+ const identity = lspProjectInputPhysicalPathIdentity(location, identities);
532
+ return createHash("sha256")
533
+ .update(
534
+ Buffer.concat([
535
+ Buffer.from("directory\0"),
536
+ identity,
537
+ Buffer.from([0]),
538
+ Buffer.from(
539
+ lspProjectInputDirectoryTopologyDigest(
540
+ process.platform === "win32"
541
+ ? identities.resolve(location).path
542
+ : identity,
543
+ ),
544
+ ),
545
+ ]),
546
+ )
547
+ .digest("hex");
548
+ }
549
+
550
+ function lspProjectInputPhysicalPathIdentity(
551
+ location: string,
552
+ identities: ProjectInputPathIdentityContext,
553
+ ): Buffer {
554
+ if (process.platform === "win32") {
555
+ return Buffer.from(
556
+ identities.resolve(location).path.replaceAll("\\", "/"),
557
+ "utf8",
558
+ );
559
+ }
560
+ let existing = path.resolve(location);
561
+ const missing: Buffer[] = [];
562
+ while (true) {
563
+ try {
564
+ const realpath = fs.realpathSync.native ?? fs.realpathSync;
565
+ let physical = realpath(Buffer.from(existing), {
566
+ encoding: "buffer",
567
+ });
568
+ for (const segment of missing) {
569
+ physical = Buffer.concat([
570
+ physical,
571
+ physical.at(-1) === 0x2f ? Buffer.alloc(0) : Buffer.from("/"),
572
+ segment,
573
+ ]);
574
+ }
575
+ return physical;
576
+ } catch (error) {
577
+ if (
578
+ error instanceof Error &&
579
+ "code" in error &&
580
+ error.code !== "ENOENT" &&
581
+ error.code !== "ENOTDIR"
582
+ ) {
583
+ throw error;
584
+ }
585
+ const parent = path.dirname(existing);
586
+ if (parent === existing) return Buffer.from(existing);
587
+ missing.unshift(Buffer.from(path.basename(existing)));
588
+ existing = parent;
589
+ }
590
+ }
591
+ }
592
+
593
+ function lspProjectInputDirectoryTopologyDigest(
594
+ location: string | Buffer,
595
+ ): string {
528
596
  const entries: Buffer[] = [];
529
597
  try {
530
598
  if (process.platform === "win32") {
531
- for (const entry of fs.readdirSync(location, { withFileTypes: true })) {
599
+ const directory =
600
+ typeof location === "string" ? location : location.toString("utf8");
601
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
532
602
  let target = Buffer.alloc(0);
533
603
  if (entry.isSymbolicLink()) {
534
604
  try {
535
605
  target = Buffer.from(
536
- fs.readlinkSync(path.join(location, entry.name)),
606
+ fs.readlinkSync(path.join(directory, entry.name)),
537
607
  "utf8",
538
608
  );
539
609
  } catch {
@@ -558,7 +628,7 @@ function lspProjectInputDirectoryDigest(location: string): string {
558
628
  try {
559
629
  target = fs.readlinkSync(
560
630
  Buffer.concat([
561
- Buffer.from(location),
631
+ Buffer.isBuffer(location) ? location : Buffer.from(location),
562
632
  Buffer.from(path.sep),
563
633
  entry.name,
564
634
  ]),
@@ -299,20 +299,52 @@ function runPreparedEntry(
299
299
  sourceEntry,
300
300
  ...parsed.passthrough,
301
301
  ];
302
+ const evaluatorBoundary = readEvaluatorBoundary(process.env);
303
+ const runtimeEnv: NodeJS.ProcessEnv = {
304
+ ...process.env,
305
+ NODE_OPTIONS: appendNodeOption(
306
+ process.env.NODE_OPTIONS,
307
+ `--require ${JSON.stringify(path.join(__dirname, "runtimeHookPreload.js"))}`,
308
+ ),
309
+ TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? tsgo,
310
+ TTSX_RUNTIME_MANIFEST: manifestPath,
311
+ };
312
+ delete runtimeEnv[TTSC_TTSX_EVALUATOR_DEADLINE_ENV];
313
+ delete runtimeEnv[TTSC_TTSX_EVALUATOR_MAX_BUFFER_ENV];
314
+ delete runtimeEnv[TTSC_TTSX_EVALUATOR_STATUS_FD_ENV];
315
+
302
316
  const result = spawnSync(process.execPath, args, {
303
317
  cwd,
304
- env: {
305
- ...process.env,
306
- NODE_OPTIONS: appendNodeOption(
307
- process.env.NODE_OPTIONS,
308
- `--require ${JSON.stringify(path.join(__dirname, "runtimeHookPreload.js"))}`,
309
- ),
310
- TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? tsgo,
311
- TTSX_RUNTIME_MANIFEST: manifestPath,
312
- },
313
- stdio: "inherit",
318
+ env: runtimeEnv,
319
+ ...(evaluatorBoundary
320
+ ? {
321
+ killSignal: "SIGKILL" as const,
322
+ timeout: Math.max(
323
+ 1,
324
+ Math.min(
325
+ MAX_CHILD_PROCESS_TIMEOUT_MS,
326
+ evaluatorBoundary.deadlineMs - Date.now(),
327
+ ),
328
+ ),
329
+ maxBuffer: evaluatorBoundary.maxBuffer,
330
+ stdio: ["inherit", "pipe", "pipe"] as const,
331
+ }
332
+ : { stdio: "inherit" as const }),
314
333
  windowsHide: true,
315
334
  });
335
+ const nestedCode = (result.error as NodeJS.ErrnoException | undefined)
336
+ ?.code;
337
+ if (
338
+ evaluatorBoundary &&
339
+ (nestedCode === "ETIMEDOUT" || nestedCode === "ENOBUFS")
340
+ ) {
341
+ reportEvaluatorFailure(evaluatorBoundary.statusFd, nestedCode);
342
+ return 1;
343
+ }
344
+ if (evaluatorBoundary) {
345
+ if (result.stdout) process.stdout.write(result.stdout);
346
+ if (result.stderr) process.stderr.write(result.stderr);
347
+ }
316
348
  if (result.error) {
317
349
  process.stderr.write(`${result.error.message}\n`);
318
350
  return 1;
@@ -323,6 +355,51 @@ function runPreparedEntry(
323
355
  }
324
356
  }
325
357
 
358
+ interface TtsxEvaluatorBoundary {
359
+ deadlineMs: number;
360
+ maxBuffer: number;
361
+ statusFd: number;
362
+ }
363
+
364
+ const TTSC_TTSX_EVALUATOR_DEADLINE_ENV = "TTSC_TTSX_EVALUATOR_DEADLINE_MS";
365
+ const TTSC_TTSX_EVALUATOR_MAX_BUFFER_ENV =
366
+ "TTSC_TTSX_EVALUATOR_MAX_BUFFER_BYTES";
367
+ const TTSC_TTSX_EVALUATOR_STATUS_FD_ENV = "TTSC_TTSX_EVALUATOR_STATUS_FD";
368
+ const TTSC_TTSX_EVALUATOR_STATUS_FD = 3;
369
+ const MAX_CHILD_PROCESS_TIMEOUT_MS = 2_147_483_647;
370
+
371
+ function readEvaluatorBoundary(
372
+ env: NodeJS.ProcessEnv,
373
+ ): TtsxEvaluatorBoundary | undefined {
374
+ const deadlineMs = Number(env[TTSC_TTSX_EVALUATOR_DEADLINE_ENV]);
375
+ const maxBuffer = Number(env[TTSC_TTSX_EVALUATOR_MAX_BUFFER_ENV]);
376
+ const statusFd = Number(env[TTSC_TTSX_EVALUATOR_STATUS_FD_ENV]);
377
+ if (
378
+ !Number.isSafeInteger(deadlineMs) ||
379
+ deadlineMs <= 0 ||
380
+ !Number.isSafeInteger(maxBuffer) ||
381
+ maxBuffer <= 0 ||
382
+ maxBuffer > MAX_CHILD_PROCESS_TIMEOUT_MS ||
383
+ statusFd !== TTSC_TTSX_EVALUATOR_STATUS_FD
384
+ ) {
385
+ return undefined;
386
+ }
387
+ return { deadlineMs, maxBuffer, statusFd };
388
+ }
389
+
390
+ function reportEvaluatorFailure(
391
+ statusFd: number,
392
+ code: "ENOBUFS" | "ETIMEDOUT",
393
+ ): void {
394
+ try {
395
+ fs.writeSync(statusFd, code);
396
+ } catch {
397
+ // The boundary is an internal parent/child protocol. If a standalone ttsx
398
+ // caller supplied only the environment variables, preserve the ordinary
399
+ // non-zero bounded-process result instead of replacing it with an fd error.
400
+ }
401
+ }
402
+
326
403
  function removeRuntimeOutput(directory: string): void {
327
404
  try {
328
405
  fs.rmSync(directory, { force: true, recursive: true });
@@ -15,6 +15,10 @@ import { resolveEmittedJavaScript } from "../../compiler/internal/resolveEmitted
15
15
  import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
16
16
  import { runBuild } from "../../compiler/internal/runBuild";
17
17
  import { outputText, spawnNative } from "../../compiler/internal/spawnNative";
18
+ import {
19
+ type FilesystemPathIdentityContext,
20
+ createFilesystemPathIdentityContext,
21
+ } from "../../internal/projectInputPathIdentity";
18
22
  import { inlineServedSourceMap } from "./servedSourceMap";
19
23
 
20
24
  /**
@@ -1143,23 +1147,19 @@ function serveEntryEmit(real: string): ServedSource | null {
1143
1147
  * Both sides are normalized to native separators first: a manifest `rootDir`
1144
1148
  * arrives slash-normalized from the synthesized tsconfig (`C:/` on Windows)
1145
1149
  * while `real` paths are native, and a raw prefix comparison across the two
1146
- * forms silently never matches. Windows paths also compare case-insensitively
1147
- * (a lowercase `TEMP` yields a `c:`-rooted `rootDir` for the same volume the
1148
- * real paths spell `C:`). Exported for direct exercise by the ttsx e2e suite —
1149
- * spawned runs cannot pin the Windows normalization branches on CI.
1150
+ * forms silently never matches. Filesystem identity preserves ordinary Windows
1151
+ * aliases while keeping case-distinct paths under an opted-in directory
1152
+ * separate. Exported for direct exercise by the ttsx e2e suite — spawned runs
1153
+ * cannot pin both Windows case-semantics branches on CI.
1150
1154
  */
1151
- export function isWithin(real: string, directory: string): boolean {
1152
- const fold = (value: string): string =>
1153
- process.platform === "win32"
1154
- ? path.normalize(value).toLowerCase()
1155
- : path.normalize(value);
1156
- const target = fold(real);
1157
- const dir = fold(directory);
1158
- if (target === dir) {
1159
- return true;
1160
- }
1161
- const prefix = dir.endsWith(path.sep) ? dir : dir + path.sep;
1162
- return target.startsWith(prefix);
1155
+ export function isWithin(
1156
+ real: string,
1157
+ directory: string,
1158
+ identities: FilesystemPathIdentityContext = createFilesystemPathIdentityContext(
1159
+ { throwOnRealpathError: false },
1160
+ ),
1161
+ ): boolean {
1162
+ return identities.isWithin(directory, real);
1163
1163
  }
1164
1164
 
1165
1165
  /**