ttsc 0.20.1 → 0.22.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.
- package/cmd/ttscserver/main.go +79 -3
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
- package/driver/lsp.go +4 -0
- package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
- package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
- package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
- package/internal/lspserver/lsp_native_plugin_source.go +171 -55
- package/internal/lspserver/lsp_native_resident.go +110 -9
- package/internal/lspserver/lsp_plugin.go +6 -0
- package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
- package/internal/lspserver/lsp_project_diagnostics.go +235 -0
- package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
- package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
- package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
- package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
- package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
- package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_registration.go +504 -0
- package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
- package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
- package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
- package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
- package/internal/lspserver/lsp_project_inputs.go +944 -0
- package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
- package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
- package/internal/lspserver/lsp_proxy.go +591 -28
- package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
- package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
- package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
- package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
- package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
- package/lib/compiler/internal/project/readProjectConfig.js +46 -14
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
- package/lib/compiler/internal/residentCheckProcess.js +227 -0
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +64 -1
- package/lib/compiler/internal/runBuild.js +484 -7
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +1 -0
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/flags/parser.js +13 -17
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +10 -3
- package/lib/flags/schema.js +99 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +23 -0
- package/lib/internal/projectInputPathIdentity.js +222 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +121 -88
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +13 -1
- package/lib/launcher/internal/runTtscserver.js +415 -45
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
- package/lib/launcher/internal/singleFileOutput.js +103 -0
- package/lib/launcher/internal/singleFileOutput.js.map +1 -0
- package/lib/launcher/internal/watchTopology.d.ts +214 -2
- package/lib/launcher/internal/watchTopology.js +1560 -41
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +5 -0
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/project/readProjectConfig.ts +62 -8
- package/src/compiler/internal/residentCheckProcess.ts +279 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
- package/src/compiler/internal/runBuild.ts +728 -12
- package/src/compiler/internal/runSingleFileEmit.ts +1 -0
- package/src/flags/parser.ts +17 -17
- package/src/flags/schema.ts +111 -3
- package/src/internal/projectInputPathIdentity.ts +273 -0
- package/src/launcher/internal/runTtsc.ts +141 -117
- package/src/launcher/internal/runTtscserver.ts +561 -54
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2008 -59
- package/src/plugin/internal/loadProjectPlugins.ts +8 -0
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
package/src/flags/parser.ts
CHANGED
|
@@ -156,12 +156,9 @@ export function parseFlags(opts: ParseOptions): ParseResult {
|
|
|
156
156
|
// `--target all` masquerade as `--all` now that the lookup is dash- and
|
|
157
157
|
// case-insensitive.
|
|
158
158
|
if (current.startsWith("-")) {
|
|
159
|
-
// `--foo=value` / `-p=value`
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// would fall through as an unknown token and be forwarded to tsgo,
|
|
163
|
-
// bypassing the launcher's own consumer (e.g. plugin discovery against
|
|
164
|
-
// the wrong project root).
|
|
159
|
+
// Split `--foo=value` / `-p=value` before resolving launcher-owned
|
|
160
|
+
// options. A tsgo-only option is still forwarded byte-for-byte; pinned
|
|
161
|
+
// tsgo does not split `=`, so it will reject that spelling itself.
|
|
165
162
|
const equalsIndex = current.indexOf("=");
|
|
166
163
|
const token =
|
|
167
164
|
equalsIndex === -1 ? current : current.slice(0, equalsIndex);
|
|
@@ -369,13 +366,10 @@ function validatePositiveInt(
|
|
|
369
366
|
* `--tsgo-args`); without this branch the parser would lose the value token of
|
|
370
367
|
* a `--flag value` pair.
|
|
371
368
|
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* behalf of a flag that does not own it, so `ttsc --pretty a.ts` lost `a.ts`
|
|
377
|
-
* out of `positional` and silently switched from single-file mode to project
|
|
378
|
-
* mode (issue #663's failure shape, reached through the sibling branch).
|
|
369
|
+
* A value option owned by tsgo always consumes its next bare token, even when
|
|
370
|
+
* it ends in `.ts`; `--rootDir src.ts main.ts` has one option value and one
|
|
371
|
+
* source. Schema rows belonging only to another ttsc layer still consult
|
|
372
|
+
* `isPositional`, because tsgo does not own their arity.
|
|
379
373
|
*/
|
|
380
374
|
function forwardKnownButUnaccepted(
|
|
381
375
|
passthrough: string[],
|
|
@@ -386,15 +380,21 @@ function forwardKnownButUnaccepted(
|
|
|
386
380
|
isPositional: ((token: string) => boolean) | undefined,
|
|
387
381
|
): void {
|
|
388
382
|
passthrough.push(original);
|
|
389
|
-
// Boolean flags carry no value. `--foo=value` already
|
|
390
|
-
// inline. Otherwise the next argv token is the flag's value: keep it
|
|
391
|
-
// adjacent so tsgo receives the pair intact.
|
|
383
|
+
// Boolean flags carry no required value. `--foo=value` is already one token.
|
|
392
384
|
if (flag.kind === "boolean" || inlineValue !== undefined) {
|
|
393
385
|
return;
|
|
394
386
|
}
|
|
395
387
|
if (rest.length === 0) return;
|
|
396
388
|
if (rest[0]!.startsWith("-")) return;
|
|
397
|
-
|
|
389
|
+
const tsgoOwnsArity =
|
|
390
|
+
flag.consumedBy.includes("tsgo") || flag.forwardTo === "tsgo";
|
|
391
|
+
if (
|
|
392
|
+
tsgoOwnsArity === false &&
|
|
393
|
+
isPositional !== undefined &&
|
|
394
|
+
isPositional(rest[0]!)
|
|
395
|
+
) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
398
|
passthrough.push(rest.shift()!);
|
|
399
399
|
}
|
|
400
400
|
|
package/src/flags/schema.ts
CHANGED
|
@@ -61,9 +61,10 @@ export type FlagLayer =
|
|
|
61
61
|
/**
|
|
62
62
|
* Argument shape of a flag.
|
|
63
63
|
*
|
|
64
|
-
* - `boolean` — `--flag`
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* - `boolean` — `--flag` with an optional spaced `true` / `false`. Launcher flags
|
|
65
|
+
* also accept `--flag=true` / `--flag=false`; tsgo-forwarded flags stay
|
|
66
|
+
* verbatim because pinned tsgo does not split `=`.
|
|
67
|
+
* - `value` — `--flag value`. Launcher flags also accept `--flag=value`.
|
|
67
68
|
* - `valueOptional` — `--flag` standalone is allowed; if followed by a non-flag
|
|
68
69
|
* token that token is consumed as the value. (Currently unused — declared for
|
|
69
70
|
* future flags like `--watch [path]`.)
|
|
@@ -161,6 +162,13 @@ export interface FlagSpec {
|
|
|
161
162
|
*/
|
|
162
163
|
readonly internalShadow?: boolean;
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* `true` when pinned tsgo only permits the option in tsconfig, except for
|
|
167
|
+
* command-line `false` or `null`. The launcher forwards the original argv so
|
|
168
|
+
* tsgo remains the diagnostic authority.
|
|
169
|
+
*/
|
|
170
|
+
readonly tsconfigOnly?: boolean;
|
|
171
|
+
|
|
164
172
|
/**
|
|
165
173
|
* Native sidecar capability that must be declared before ttsc sends this flag
|
|
166
174
|
* as a bare CLI argument. Everything else routes through `--tsgo-args` or
|
|
@@ -275,6 +283,106 @@ export const FLAG_SCHEMA: readonly FlagSpec[] = [
|
|
|
275
283
|
consumedBy: ["launcher", "host", "lint"],
|
|
276
284
|
description: "Override compilerOptions.outDir for this invocation.",
|
|
277
285
|
},
|
|
286
|
+
{
|
|
287
|
+
name: "--composite",
|
|
288
|
+
kind: "boolean",
|
|
289
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
290
|
+
consumedBy: ["tsgo"],
|
|
291
|
+
forwardTo: "tsgo",
|
|
292
|
+
tsconfigOnly: true,
|
|
293
|
+
description:
|
|
294
|
+
"Configure project-reference constraints in tsconfig; CLI accepts only false or null.",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "--incremental",
|
|
298
|
+
aliases: ["-i"],
|
|
299
|
+
kind: "boolean",
|
|
300
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
301
|
+
consumedBy: ["tsgo"],
|
|
302
|
+
forwardTo: "tsgo",
|
|
303
|
+
description: "Write build information for incremental compilation.",
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "--tsBuildInfoFile",
|
|
307
|
+
kind: "value",
|
|
308
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
309
|
+
consumedBy: ["tsgo"],
|
|
310
|
+
forwardTo: "tsgo",
|
|
311
|
+
description: "Choose the incremental build-information file.",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "--declaration",
|
|
315
|
+
aliases: ["-d"],
|
|
316
|
+
kind: "boolean",
|
|
317
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
318
|
+
consumedBy: ["tsgo"],
|
|
319
|
+
forwardTo: "tsgo",
|
|
320
|
+
description: "Emit declaration files.",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "--declarationDir",
|
|
324
|
+
kind: "value",
|
|
325
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
326
|
+
consumedBy: ["tsgo"],
|
|
327
|
+
forwardTo: "tsgo",
|
|
328
|
+
description: "Choose the declaration output directory.",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "--declarationMap",
|
|
332
|
+
kind: "boolean",
|
|
333
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
334
|
+
consumedBy: ["tsgo"],
|
|
335
|
+
forwardTo: "tsgo",
|
|
336
|
+
description: "Emit source maps for declaration files.",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "--emitDeclarationOnly",
|
|
340
|
+
kind: "boolean",
|
|
341
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
342
|
+
consumedBy: ["tsgo"],
|
|
343
|
+
forwardTo: "tsgo",
|
|
344
|
+
description: "Emit declarations without JavaScript.",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "--inlineSourceMap",
|
|
348
|
+
kind: "boolean",
|
|
349
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
350
|
+
consumedBy: ["tsgo"],
|
|
351
|
+
forwardTo: "tsgo",
|
|
352
|
+
description: "Inline source maps into emitted JavaScript.",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "--sourceMap",
|
|
356
|
+
kind: "boolean",
|
|
357
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
358
|
+
consumedBy: ["tsgo"],
|
|
359
|
+
forwardTo: "tsgo",
|
|
360
|
+
description: "Emit external JavaScript source maps.",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "--outFile",
|
|
364
|
+
kind: "value",
|
|
365
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
366
|
+
consumedBy: ["tsgo"],
|
|
367
|
+
forwardTo: "tsgo",
|
|
368
|
+
description: "Bundle emitted JavaScript and declarations into one file.",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "--rootDir",
|
|
372
|
+
kind: "value",
|
|
373
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
374
|
+
consumedBy: ["tsgo"],
|
|
375
|
+
forwardTo: "tsgo",
|
|
376
|
+
description: "Choose the compiler input root.",
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "--jsx",
|
|
380
|
+
kind: "value",
|
|
381
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
382
|
+
consumedBy: ["tsgo"],
|
|
383
|
+
forwardTo: "tsgo",
|
|
384
|
+
description: "Choose the JSX emit transform.",
|
|
385
|
+
},
|
|
278
386
|
|
|
279
387
|
// -------------------------------------------------------------------------
|
|
280
388
|
// Watch mode (launcher only).
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type ProjectInputPathIdentity = {
|
|
6
|
+
key: string;
|
|
7
|
+
path: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ProjectInputPathIdentityOperations = {
|
|
11
|
+
caseSensitive(directory: string): boolean;
|
|
12
|
+
realpath(location: string): string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ProjectInputPathIdentityContext = {
|
|
16
|
+
caseSensitive(directory: string): boolean;
|
|
17
|
+
isWithin(root: string, candidate: string): boolean;
|
|
18
|
+
resolve(location: string): ProjectInputPathIdentity;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CachedIdentity = {
|
|
22
|
+
ancestor: string;
|
|
23
|
+
identity: ProjectInputPathIdentity;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CachedRealpath =
|
|
27
|
+
| {
|
|
28
|
+
found: false;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
found: true;
|
|
32
|
+
path: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create one filesystem-identity resolver for a project-input transaction.
|
|
37
|
+
*
|
|
38
|
+
* Existing segments use their physical spelling. A missing suffix keeps exact
|
|
39
|
+
* spelling only under a case-sensitive directory; otherwise its canonical
|
|
40
|
+
* spelling is folded so aliases remain one declaration before they exist.
|
|
41
|
+
*/
|
|
42
|
+
export function createProjectInputPathIdentityContext(
|
|
43
|
+
operations: Partial<ProjectInputPathIdentityOperations> = {},
|
|
44
|
+
): ProjectInputPathIdentityContext {
|
|
45
|
+
const identities = new Map<string, CachedIdentity>();
|
|
46
|
+
const realpaths = new Map<string, CachedRealpath>();
|
|
47
|
+
const sensitivities = new Map<string, boolean>();
|
|
48
|
+
const realpath = operations.realpath ?? physicalRealpath;
|
|
49
|
+
const caseSensitive =
|
|
50
|
+
operations.caseSensitive ?? filesystemDirectoryIsCaseSensitive;
|
|
51
|
+
|
|
52
|
+
const resolve = (location: string): ProjectInputPathIdentity => {
|
|
53
|
+
const normalized = resolveProjectInputPath(location);
|
|
54
|
+
const cached = identities.get(normalized);
|
|
55
|
+
if (cached !== undefined) return cached.identity;
|
|
56
|
+
let existing = normalized;
|
|
57
|
+
const missing: string[] = [];
|
|
58
|
+
while (true) {
|
|
59
|
+
const physical = cachedRealpath(realpaths, realpath, existing);
|
|
60
|
+
if (physical !== undefined) {
|
|
61
|
+
const sensitive =
|
|
62
|
+
missing.length === 0
|
|
63
|
+
? true
|
|
64
|
+
: cachedCaseSensitivity(sensitivities, caseSensitive, physical);
|
|
65
|
+
const suffix = sensitive
|
|
66
|
+
? missing
|
|
67
|
+
: missing.map((segment) => segment.toLowerCase());
|
|
68
|
+
const canonical = path.resolve(physical, ...suffix);
|
|
69
|
+
const identity = {
|
|
70
|
+
key: canonical,
|
|
71
|
+
path: canonical,
|
|
72
|
+
};
|
|
73
|
+
identities.set(normalized, {
|
|
74
|
+
ancestor: physical,
|
|
75
|
+
identity,
|
|
76
|
+
});
|
|
77
|
+
return identity;
|
|
78
|
+
}
|
|
79
|
+
const parent = path.dirname(existing);
|
|
80
|
+
if (parent === existing) {
|
|
81
|
+
const identity = {
|
|
82
|
+
key: normalized,
|
|
83
|
+
path: normalized,
|
|
84
|
+
};
|
|
85
|
+
identities.set(normalized, {
|
|
86
|
+
ancestor: normalized,
|
|
87
|
+
identity,
|
|
88
|
+
});
|
|
89
|
+
return identity;
|
|
90
|
+
}
|
|
91
|
+
missing.unshift(path.basename(existing));
|
|
92
|
+
existing = parent;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
caseSensitive: (directory) => {
|
|
98
|
+
const normalized = resolveProjectInputPath(directory);
|
|
99
|
+
resolve(normalized);
|
|
100
|
+
return cachedCaseSensitivity(
|
|
101
|
+
sensitivities,
|
|
102
|
+
caseSensitive,
|
|
103
|
+
identities.get(normalized)!.ancestor,
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
isWithin: (root, candidate) =>
|
|
107
|
+
isProjectInputPathIdentityWithin(
|
|
108
|
+
resolve(root).key,
|
|
109
|
+
resolve(candidate).key,
|
|
110
|
+
),
|
|
111
|
+
resolve,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isProjectInputPathIdentityWithin(
|
|
116
|
+
root: string,
|
|
117
|
+
candidate: string,
|
|
118
|
+
): boolean {
|
|
119
|
+
if (candidate === root) return true;
|
|
120
|
+
return candidate.startsWith(
|
|
121
|
+
root.endsWith(path.sep) ? root : `${root}${path.sep}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveProjectInputPath(location: string): string {
|
|
126
|
+
if (process.platform !== "win32") {
|
|
127
|
+
return path.resolve(location);
|
|
128
|
+
}
|
|
129
|
+
const normalized = location.replaceAll("/", "\\");
|
|
130
|
+
if (normalized.toLowerCase().startsWith("\\\\?\\unc\\")) {
|
|
131
|
+
return path.resolve(`\\\\${normalized.slice(8)}`);
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
normalized.startsWith("\\\\?\\") &&
|
|
135
|
+
/^[A-Za-z]:\\/.test(normalized.slice(4))
|
|
136
|
+
) {
|
|
137
|
+
return path.resolve(normalized.slice(4));
|
|
138
|
+
}
|
|
139
|
+
return path.resolve(normalized);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cachedRealpath(
|
|
143
|
+
cache: Map<string, CachedRealpath>,
|
|
144
|
+
realpath: (location: string) => string,
|
|
145
|
+
location: string,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
const cached = cache.get(location);
|
|
148
|
+
if (cached !== undefined) return cached.found ? cached.path : undefined;
|
|
149
|
+
try {
|
|
150
|
+
const physical = resolveProjectInputPath(realpath(location));
|
|
151
|
+
cache.set(location, { found: true, path: physical });
|
|
152
|
+
return physical;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (isMissingFilesystemEntry(error) === false) throw error;
|
|
155
|
+
cache.set(location, { found: false });
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function cachedCaseSensitivity(
|
|
161
|
+
cache: Map<string, boolean>,
|
|
162
|
+
caseSensitive: (directory: string) => boolean,
|
|
163
|
+
directory: string,
|
|
164
|
+
): boolean {
|
|
165
|
+
const cached = cache.get(directory);
|
|
166
|
+
if (cached !== undefined) return cached;
|
|
167
|
+
const sensitive = caseSensitive(directory);
|
|
168
|
+
cache.set(directory, sensitive);
|
|
169
|
+
return sensitive;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function physicalRealpath(location: string): string {
|
|
173
|
+
return fs.realpathSync.native?.(location) ?? fs.realpathSync(location);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function filesystemDirectoryIsCaseSensitive(directory: string): boolean {
|
|
177
|
+
let entries: string[];
|
|
178
|
+
try {
|
|
179
|
+
entries = fs.readdirSync(directory);
|
|
180
|
+
} catch {
|
|
181
|
+
return unprovenCaseSensitivity();
|
|
182
|
+
}
|
|
183
|
+
const foldedNames = new Map<string, string>();
|
|
184
|
+
for (const name of entries) {
|
|
185
|
+
const folded = name.toLowerCase();
|
|
186
|
+
const previous = foldedNames.get(folded);
|
|
187
|
+
if (previous !== undefined && previous !== name) return true;
|
|
188
|
+
foldedNames.set(folded, name);
|
|
189
|
+
}
|
|
190
|
+
for (const name of entries) {
|
|
191
|
+
const alternate = alternateCase(name);
|
|
192
|
+
if (alternate === name) continue;
|
|
193
|
+
try {
|
|
194
|
+
physicalRealpath(path.join(directory, alternate));
|
|
195
|
+
return false;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (isMissingFilesystemEntry(error)) continue;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (process.platform === "darwin") {
|
|
202
|
+
// APFS/HFS case semantics are volume-wide. An empty directory has no child
|
|
203
|
+
// name to probe, so ask the same read-only question of its existing name in
|
|
204
|
+
// the parent and walk upward until a name with ASCII case is available.
|
|
205
|
+
// This avoids a write probe while still distinguishing default APFS from a
|
|
206
|
+
// case-sensitive volume.
|
|
207
|
+
let current = resolveProjectInputPath(directory);
|
|
208
|
+
while (true) {
|
|
209
|
+
const parent = path.dirname(current);
|
|
210
|
+
if (parent === current) break;
|
|
211
|
+
const name = path.basename(current);
|
|
212
|
+
const alternate = alternateCase(name);
|
|
213
|
+
if (alternate !== name) {
|
|
214
|
+
try {
|
|
215
|
+
physicalRealpath(path.join(parent, alternate));
|
|
216
|
+
return false;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (isMissingFilesystemEntry(error) === false) throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
current = parent;
|
|
222
|
+
}
|
|
223
|
+
return unprovenCaseSensitivity();
|
|
224
|
+
}
|
|
225
|
+
if (process.platform !== "win32") return true;
|
|
226
|
+
// Node does not expose the Windows per-directory flag. Prefer fsutil's
|
|
227
|
+
// read-only answer, then conservatively preserve distinct declarations.
|
|
228
|
+
const result = childProcess.spawnSync(
|
|
229
|
+
"fsutil.exe",
|
|
230
|
+
["file", "queryCaseSensitiveInfo", directory],
|
|
231
|
+
{ encoding: "utf8", windowsHide: true },
|
|
232
|
+
);
|
|
233
|
+
if (result.error === undefined && result.status === 0) {
|
|
234
|
+
if (/\bdisabled\b/iu.test(result.stdout)) return false;
|
|
235
|
+
if (/\benabled\b/iu.test(result.stdout)) return true;
|
|
236
|
+
}
|
|
237
|
+
return unprovenCaseSensitivity();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* What to assume when the volume refused to answer.
|
|
242
|
+
*
|
|
243
|
+
* Every probe above returns a proof: two names that fold together prove the
|
|
244
|
+
* volume keeps them apart, and a name that opens under the other case proves it
|
|
245
|
+
* does not. This is the remaining case — an empty or unreadable directory, a
|
|
246
|
+
* name with no letter to alter, a Windows build whose `fsutil` lacks the
|
|
247
|
+
* subcommand — and the only honest answer is the platform's own default.
|
|
248
|
+
*
|
|
249
|
+
* NTFS and APFS are case-insensitive unless a volume or a directory was opted
|
|
250
|
+
* out, so guessing sensitive there splits one file into two identities under
|
|
251
|
+
* two spellings, which is the failure this module exists to remove. The rarer
|
|
252
|
+
* mistake, on a volume that really was opted out, merges two files differing
|
|
253
|
+
* only in case into one watched identity.
|
|
254
|
+
*/
|
|
255
|
+
function unprovenCaseSensitivity(): boolean {
|
|
256
|
+
return process.platform !== "win32" && process.platform !== "darwin";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function alternateCase(value: string): string {
|
|
260
|
+
return value.replace(/[A-Za-z]/g, (character) =>
|
|
261
|
+
character === character.toLowerCase()
|
|
262
|
+
? character.toUpperCase()
|
|
263
|
+
: character.toLowerCase(),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isMissingFilesystemEntry(error: unknown): boolean {
|
|
268
|
+
return (
|
|
269
|
+
error instanceof Error &&
|
|
270
|
+
"code" in error &&
|
|
271
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
272
|
+
);
|
|
273
|
+
}
|