Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5be1066
Reject empty createSourceFile names (PORT TO MAIN, TYPED PATHS, STRAD…
jakebailey Sep 17, 2026
5397153
Fix inherited path presentation loss (PORT TO MAIN, TYPED PATHS, STRA…
jakebailey Sep 4, 2026
c3a52e5
Test auto-import package directory realpaths (PORT TO MAIN, TYPED PAT…
jakebailey Sep 2, 2026
bb62a8a
Fix auto-import package directory realpaths (PORT TO MAIN, TYPED PATH…
jakebailey Sep 2, 2026
ca1d8e8
Test node_modules package path kinds (PORT TO MAIN, TYPED PATHS, STRA…
jakebailey Sep 2, 2026
05904cf
Type node_modules package path kinds (PORT TO MAIN, TYPED PATHS, STRA…
jakebailey Sep 2, 2026
26779d2
Test nested package.json specifiers (PORT TO MAIN, TYPED PATHS, STRAD…
jakebailey Sep 2, 2026
a48d71f
Fix nested package.json specifiers (PORT TO MAIN, TYPED PATHS, STRADA…
jakebailey Sep 2, 2026
a0ad4b3
Test nested triple-slash redirects (PORT TO MAIN, TYPED PATHS, STRADA…
jakebailey Sep 2, 2026
a880450
Fix nested triple-slash redirects (PORT TO MAIN, TYPED PATHS, STRADA …
jakebailey Sep 2, 2026
79d8760
Fix project-relative API import edits (PORT TO MAIN, TYPED PATHS, STR…
jakebailey Sep 2, 2026
4403df6
Fix project-relative API source cache (PORT TO MAIN, TYPED PATHS, NAT…
jakebailey Sep 2, 2026
07a2ed1
Recognize local file URL roots case-insensitively (PORT TO MAIN, REVI…
jakebailey Sep 4, 2026
4947689
Decode imported source maps safely (PORT TO MAIN, REVIEW, STRADA BUG)
jakebailey Sep 4, 2026
1cb23ea
Preserve structured non-file URI identity (PORT TO MAIN, REVIEW, STRA…
jakebailey Sep 8, 2026
4a0b83a
Validate completion item file names (PORT TO MAIN, REVIEW, NATIVE ONLY)
jakebailey Sep 8, 2026
f345740
Keep failed API updates transactional (PORT TO MAIN, REVIEW, NATIVE O…
jakebailey Sep 4, 2026
3adde4f
Release the final session snapshot (PORT TO MAIN, REVIEW, NATIVE ONLY)
jakebailey Sep 4, 2026
a76ca6f
Match content mapper manifests canonically (PORT TO MAIN, REVIEW, NAT…
jakebailey Sep 4, 2026
190a3a5
Harden virtual filesystem lookups (PORT TO MAIN, REVIEW, NATIVE API)
jakebailey Sep 16, 2026
9540bc3
Introduce typed path invariants throughout tsc (TYPED PATHS)
jakebailey Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions packages/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
"@typescript/source": "./src/api/fs.ts",
"default": "./dist/api/fs.js"
},
"./unstable/path": {
"@typescript/source": "./src/api/typedPaths.ts",
"default": "./dist/api/typedPaths.js"
},
"./unstable/proto": {
"@typescript/source": "./src/api/proto.ts",
"default": "./dist/api/proto.js"
Expand Down
131 changes: 85 additions & 46 deletions packages/typescript/src/api/async/api.ts

Large diffs are not rendered by default.

87 changes: 70 additions & 17 deletions packages/typescript/src/api/async/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {
} from "#vscode-jsonrpc/node";
import type { ChildProcess } from "node:child_process";
import type { Socket } from "node:net";
import type {
RootedDirectoryPath,
RootedFilePath,
RootedPath,
} from "../../ast/index.ts";
import type { FileSystemCallbacks } from "../fs.ts";
import {
configureFileSystemCallbacks,
Expand Down Expand Up @@ -142,24 +147,72 @@ export class Client {
): void {
if (!fs) return;
for (const name of configuration.callbackNames) {
if (name === "writeFile") {
const callback = fs.writeFile;
if (typeof callback !== "function") throw new Error("Invalid writeFile callback configuration");

const requestType = new RequestType<{ path: string; data: string; }, unknown, void>(name);
connection.onRequest(requestType, (arg: { path: string; data: string; }) => {
return encodeFileSystemCallbackResult(name, callback(arg.path, arg.data));
});

continue;
switch (name) {
case "readFile": {
const callback = fs.readFile;
if (typeof callback !== "function") throw new Error("Invalid readFile callback configuration");
connection.onRequest(new RequestType<RootedFilePath, unknown, void>(name), fileName => {
return encodeFileSystemCallbackResult(name, callback(fileName));
});
break;
}
case "fileExists": {
const callback = fs.fileExists;
if (typeof callback !== "function") throw new Error("Invalid fileExists callback configuration");
connection.onRequest(new RequestType<RootedFilePath, unknown, void>(name), fileName => {
return encodeFileSystemCallbackResult(name, callback(fileName));
});
break;
}
case "directoryExists": {
const callback = fs.directoryExists;
if (typeof callback !== "function") throw new Error("Invalid directoryExists callback configuration");
connection.onRequest(new RequestType<RootedDirectoryPath, unknown, void>(name), directoryName => {
return encodeFileSystemCallbackResult(name, callback(directoryName));
});
break;
}
case "getAccessibleEntries": {
const callback = fs.getAccessibleEntries;
if (typeof callback !== "function") throw new Error("Invalid getAccessibleEntries callback configuration");
connection.onRequest(new RequestType<RootedDirectoryPath, unknown, void>(name), directoryName => {
return encodeFileSystemCallbackResult(name, callback(directoryName));
});
break;
}
case "realpath": {
const callback = fs.realpath;
if (typeof callback !== "function") throw new Error("Invalid realpath callback configuration");
connection.onRequest(new RequestType<RootedPath, unknown, void>(name), path => {
return encodeFileSystemCallbackResult(name, callback(path));
});
break;
}
case "stat": {
const callback = fs.stat;
if (typeof callback !== "function") throw new Error("Invalid stat callback configuration");
connection.onRequest(new RequestType<RootedPath, unknown, void>(name), path => {
return encodeFileSystemCallbackResult(name, callback(path));
});
break;
}
case "writeFile": {
const callback = fs.writeFile;
if (typeof callback !== "function") throw new Error("Invalid writeFile callback configuration");
connection.onRequest(new RequestType<{ path: RootedFilePath; data: string; }, unknown, void>(name), arg => {
return encodeFileSystemCallbackResult(name, callback(arg.path, arg.data));
});
break;
}
case "removeFile": {
const callback = fs.removeFile;
if (typeof callback !== "function") throw new Error("Invalid removeFile callback configuration");
connection.onRequest(new RequestType<RootedPath, unknown, void>(name), path => {
return encodeFileSystemCallbackResult(name, callback(path));
});
break;
}
}

const callback = fs[name];
if (typeof callback !== "function") throw new Error(`Invalid ${name} callback configuration`);
const requestType = new RequestType<unknown, unknown, void>(name);
connection.onRequest(requestType, (arg: unknown) => {
return encodeFileSystemCallbackResult(name, callback(arg as string));
});
}
}

Expand Down
12 changes: 8 additions & 4 deletions packages/typescript/src/api/async/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import type {
NamedTupleMember,
ParameterDeclaration,
} from "../../ast/ast.ts";
import type {
RootedDirectoryPath,
RootedFilePath,
} from "../../ast/index.ts";
import type {
Diagnostic,
RequestFileSystem,
Expand Down Expand Up @@ -410,28 +414,28 @@ export interface CompletionInfo {
}

export interface FormatDiagnosticsHost {
getCurrentDirectory(): string;
getCurrentDirectory(): RootedDirectoryPath;
getCanonicalFileName(fileName: string): string;
getNewLine(): string;
}

export interface EmitOutputFile {
readonly text: string;
readonly sourceFileName?: string | undefined;
readonly sourceFileName?: RootedFilePath | undefined;
}

export interface EmitResult {
readonly emitSkipped: boolean;
readonly diagnostics: readonly Diagnostic[];
readonly emittedFiles: readonly string[];
readonly emittedFiles: readonly RootedFilePath[];
/** Emitted files captured as a filesystem layer suitable for {@link Snapshot.update}. */
readonly fileSystem?: RequestFileSystem | undefined;
}

export interface EmitOutput {
readonly emitSkipped: boolean;
readonly diagnostics: readonly Diagnostic[];
readonly outputFiles: ReadonlyMap<string, EmitOutputFile>;
readonly outputFiles: ReadonlyMap<RootedFilePath, EmitOutputFile>;
}

export interface ImportSymbolAction {
Expand Down
8 changes: 6 additions & 2 deletions packages/typescript/src/api/diagnosticFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type {
RootedDirectoryPath,
RootedFilePath,
} from "../ast/index.ts";
import { convertToRelativePath } from "./path.ts";
import type { DiagnosticResponse as Diagnostic } from "./proto.generated.ts";

export interface FormatDiagnosticsHost {
getCurrentDirectory(): string;
getCurrentDirectory(): RootedDirectoryPath;
getCanonicalFileName(fileName: string): string;
getNewLine(): string;
}
Expand Down Expand Up @@ -70,7 +74,7 @@ function flattenDiagnosticMessage(diagnostic: Diagnostic, newLine: string, inden
return result;
}

function relativeFileName(fileName: string, host: FormatDiagnosticsHost): string {
function relativeFileName(fileName: RootedFilePath, host: FormatDiagnosticsHost): string {
return convertToRelativePath(
fileName,
host.getCurrentDirectory(),
Expand Down
21 changes: 13 additions & 8 deletions packages/typescript/src/api/fs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import getExePath from "#getExePath";
import { dirname } from "node:path";
import type {
RootedDirectoryPath,
RootedFilePath,
RootedPath,
} from "../ast/index.ts";
import { normalizePath } from "./path.ts";
import type {
RequestDirectoryEntries,
Expand Down Expand Up @@ -54,15 +59,15 @@ export const serverFS: {

export interface FileSystemCallbacks {
directoryExists:
| ((directoryName: string) => boolean | typeof serverFS.useOS | typeof serverFS.error)
| ((directoryName: RootedDirectoryPath) => boolean | typeof serverFS.useOS | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.error;
fileExists:
| ((fileName: string) => boolean | typeof serverFS.useOS | typeof serverFS.error)
| ((fileName: RootedFilePath) => boolean | typeof serverFS.useOS | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.error;
getAccessibleEntries:
| ((directoryName: string) => FileSystemEntries | typeof serverFS.useOS | typeof serverFS.error)
| ((directoryName: RootedDirectoryPath) => FileSystemEntries | typeof serverFS.useOS | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.error;
/**
Expand All @@ -72,26 +77,26 @@ export interface FileSystemCallbacks {
* - Return {@link serverFS.useOS} to fall back to the server's operating-system filesystem.
*/
readFile:
| ((fileName: string) => string | undefined | typeof serverFS.useOS | typeof serverFS.error)
| ((fileName: RootedFilePath) => string | undefined | typeof serverFS.useOS | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.error;
realpath:
| ((path: string) => string | typeof serverFS.useOS | typeof serverFS.identity | typeof serverFS.error)
| ((path: RootedPath) => RootedPath | typeof serverFS.useOS | typeof serverFS.identity | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.identity
| typeof serverFS.error;
stat:
| ((path: string) => FileSystemStat | undefined | typeof serverFS.useOS | typeof serverFS.fakeStat | typeof serverFS.error)
| ((path: RootedPath) => FileSystemStat | undefined | typeof serverFS.useOS | typeof serverFS.fakeStat | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.fakeStat
| typeof serverFS.error;
writeFile:
| ((path: string, content: string) => void | typeof serverFS.useOS | typeof serverFS.noop | typeof serverFS.error)
| ((path: RootedFilePath, content: string) => void | typeof serverFS.useOS | typeof serverFS.noop | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.noop
| typeof serverFS.error;
removeFile:
| ((path: string) => void | typeof serverFS.useOS | typeof serverFS.noop | typeof serverFS.error)
| ((path: RootedPath) => void | typeof serverFS.useOS | typeof serverFS.noop | typeof serverFS.error)
| typeof serverFS.useOS
| typeof serverFS.noop
| typeof serverFS.error;
Expand Down
1 change: 1 addition & 0 deletions packages/typescript/src/api/node/node.infrastructure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type FileReference,
type Node,
type PathKey,
SyntaxKind,
} from "../../ast/index.ts";
export { modifierToFlag } from "../../ast/modifiers.ts";
Expand Down
67 changes: 49 additions & 18 deletions packages/typescript/src/api/node/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import {
type MappedDiagnosticDirective,
type Node,
NodeFlags,
type Path,
type PathKey,
type RootedFilePath,
SpanMap,
SpanMapFeature,
SpanMapKind,
SyntaxKind,
TokenFlags,
} from "../../ast/index.ts";
import { tryPathKeyFromCanonical } from "../path.ts";
import type { TimingCollector } from "../timing.ts";
import { MsgpackReader } from "./msgpack.ts";
import {
Expand Down Expand Up @@ -96,7 +98,7 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
private _cachedAmbientModuleNames: readonly string[] | undefined;
private _cachedSpanMap: SpanMap | undefined;
private _spanMapRead = false;
private _cachedSupplementalSourceFileNames: readonly string[] | undefined;
private _cachedSupplementalSourceFileNames: readonly RootedFilePath[] | undefined;
private _cachedDiagnosticDirectives: readonly MappedDiagnosticDirective[] | undefined;
private _diagnosticDirectivesRead = false;

Expand Down Expand Up @@ -214,14 +216,26 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
return this._offsetExtendedData + (this.data & NODE_EXTENDED_DATA_MASK);
}

get fileName(): string {
private getFileName(stringIndex: number): RootedFilePath {
return this.getString(stringIndex) as RootedFilePath;
}

private getPathKey(stringIndex: number): PathKey {
return this.getString(stringIndex) as PathKey;
}

private readFileNameArray(structuredDataOffset: number): readonly RootedFilePath[] {
return this.readStringArray(structuredDataOffset) as readonly RootedFilePath[];
}

get fileName(): RootedFilePath {
const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.FileName, true);
return this.getString(stringIndex);
return this.getFileName(stringIndex);
}

get path(): Path {
get path(): PathKey {
const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.Path, true);
return this.getString(stringIndex) as Path;
return this.getPathKey(stringIndex);
}

get languageVariant(): number {
Expand Down Expand Up @@ -323,26 +337,26 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
return this._cachedSpanMap = new SpanMap(segments);
}

get supplementalSourceFileNames(): readonly string[] | undefined {
get supplementalSourceFileNames(): readonly RootedFilePath[] | undefined {
if (this._cachedSupplementalSourceFileNames !== undefined) return this._cachedSupplementalSourceFileNames;
const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.SupplementalSourceFileNames, true);
if (offset === NO_STRUCTURED_DATA) return undefined;
return this._cachedSupplementalSourceFileNames = this.readStringArray(offset);
return this._cachedSupplementalSourceFileNames = this.readFileNameArray(offset);
}

get canonicalSourceFileName(): string | undefined {
get canonicalSourceFileName(): RootedFilePath | undefined {
const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.CanonicalSourceFileName, true);
return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getString(stringIndex);
return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getFileName(stringIndex);
}

get contentMapper(): string | undefined {
const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.ContentMapper, true);
return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getString(stringIndex);
}

get virtualFileName(): string | undefined {
get virtualFileName(): RootedFilePath | undefined {
const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.VirtualFileName, true);
return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getString(stringIndex);
return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getFileName(stringIndex);
}

get diagnosticDirectives(): readonly MappedDiagnosticDirective[] | undefined {
Expand Down Expand Up @@ -453,14 +467,14 @@ export function findDescendant(root: Node, pos: number, end: number, kind: Synta
export interface ParsedNodeHandle {
index: number;
kind: SyntaxKind;
path: Path;
path: PathKey;
}

/**
* Parse a node handle string into its components.
* Parse a compiler-produced node handle into its components.
* Handle format: "index.kind.path" where path may contain dots.
*/
export function parseNodeHandle(handle: string): ParsedNodeHandle {
export function parseNodeHandleFromCompiler(handle: string): ParsedNodeHandle {
Comment thread
jakebailey marked this conversation as resolved.
const firstDot = handle.indexOf(".");
if (firstDot === -1) {
throw new Error(`Invalid node handle: ${handle}`);
Expand All @@ -470,10 +484,27 @@ export function parseNodeHandle(handle: string): ParsedNodeHandle {
throw new Error(`Invalid node handle: ${handle}`);
}

const indexText = handle.slice(0, firstDot);
const kindText = handle.slice(firstDot + 1, secondDot);
const path = handle.slice(secondDot + 1);
const index = Number(indexText);
const kind = Number(kindText);
const key = tryPathKeyFromCanonical(path);
if (
!Number.isSafeInteger(index) ||
index < 0 ||
String(index) !== indexText ||
!Number.isSafeInteger(kind) ||
kind < 0 ||
String(kind) !== kindText ||
key === undefined
) {
throw new Error(`Invalid node handle: ${handle}`);
}
return {
index: parseInt(handle.slice(0, firstDot), 10),
kind: parseInt(handle.slice(firstDot + 1, secondDot), 10) as SyntaxKind,
path: handle.slice(secondDot + 1) as Path,
index,
kind: kind as SyntaxKind,
path: key,
};
}

Expand Down
Loading
Loading