Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
925440b
Fix inherited path presentation loss (PORT TO MAIN, TYPED PATHS, STRA…
jakebailey Sep 4, 2026
780c086
Test auto-import package directory realpaths (PORT TO MAIN, TYPED PAT…
jakebailey Sep 2, 2026
de601eb
Fix auto-import package directory realpaths (PORT TO MAIN, TYPED PATH…
jakebailey Sep 2, 2026
3a9b545
Test node_modules package path kinds (PORT TO MAIN, TYPED PATHS, STRA…
jakebailey Sep 2, 2026
b57a0ec
Type node_modules package path kinds (PORT TO MAIN, TYPED PATHS, STRA…
jakebailey Sep 2, 2026
5516a57
Test nested package.json specifiers (PORT TO MAIN, TYPED PATHS, STRAD…
jakebailey Sep 2, 2026
4e9c8e7
Fix nested package.json specifiers (PORT TO MAIN, TYPED PATHS, STRADA…
jakebailey Sep 2, 2026
31ae00d
Test nested triple-slash redirects (PORT TO MAIN, TYPED PATHS, STRADA…
jakebailey Sep 2, 2026
e0fdec3
Fix nested triple-slash redirects (PORT TO MAIN, TYPED PATHS, STRADA …
jakebailey Sep 2, 2026
d9da53f
Fix project-relative API import edits (PORT TO MAIN, TYPED PATHS, STR…
jakebailey Sep 2, 2026
8758cf8
Fix project-relative API source cache (PORT TO MAIN, TYPED PATHS, NAT…
jakebailey Sep 2, 2026
8695802
Recognize local file URL roots case-insensitively (PORT TO MAIN, REVI…
jakebailey Sep 4, 2026
1c3bc0b
Decode imported source maps safely (PORT TO MAIN, REVIEW, STRADA BUG)
jakebailey Sep 4, 2026
ef58d70
Preserve structured non-file URI identity (PORT TO MAIN, REVIEW, STRA…
jakebailey Sep 4, 2026
72303d6
Keep failed API updates transactional (PORT TO MAIN, REVIEW, NATIVE O…
jakebailey Sep 4, 2026
000e14a
Deduplicate canonical API opens (PORT TO MAIN, REVIEW, NATIVE ONLY)
jakebailey Sep 4, 2026
17becdd
Release the final session snapshot (PORT TO MAIN, REVIEW, NATIVE ONLY)
jakebailey Sep 4, 2026
fd167d6
Match content mapper manifests canonically (PORT TO MAIN, REVIEW, NAT…
jakebailey Sep 4, 2026
6f16b34
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
  •  
  •  
  •  
1 change: 1 addition & 0 deletions Herebyfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ const enumDefs = [
{ name: "NewLineKind", goPrefix: "NewLineKind", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
{ name: "JsxEmit", goPrefix: "JsxEmit", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
{ name: "ScriptKind", goPrefix: "ScriptKind", goFile: "tsc/internal/core/scriptkind.go", outDir: "packages/typescript/src/enums" },
{ name: "CaseSensitivity", goPrefix: "Case", goFile: "tsc/internal/tspath/path.go", outDir: "packages/typescript/src/enums" },
{ name: "TokenFlags", goPrefix: "TokenFlags", goFile: "tsc/internal/ast/tokenflags.go", outDir: "packages/typescript/src/enums" },
{ name: "DiagnosticDirectivePolicy", goPrefix: "MappedDiagnosticDirectivePolicy", goFile: "tsc/internal/ast/ast.go", outDir: "packages/typescript/src/enums" },
{ name: "SpanMapKind", goPrefix: "Kind", goFile: "tsc/internal/spanmap/spanmap.go", outDir: "packages/typescript/src/enums" },
Expand Down
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
157 changes: 98 additions & 59 deletions packages/typescript/src/api/async/api.ts

Large diffs are not rendered by default.

68 changes: 43 additions & 25 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 FileSystem,
fsCallbackNames,
Expand Down Expand Up @@ -141,32 +146,45 @@ export class Client {
private registerFSCallbacks(connection: MessageConnection, fs: FileSystem | undefined): void {
if (!fs) return;
for (const name of fsCallbackNames) {
if (name === "writeFile") {
if (!fs.writeFile) continue;
const callback = fs.writeFile;

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

continue;
}

const callback = fs[name];
if (callback) {
const requestType = new RequestType<unknown, unknown, void>(name);
connection.onRequest(requestType, (arg: unknown) => {
const result = callback(arg as any);
if (name === "readFile") {
// readFile has 3 returns: string (content), null (not found), undefined (fall back).
// JSON-RPC can't distinguish null from undefined, so wrap in object.
if (result === undefined) return null;
return { content: result };
switch (name) {
case "readFile":
if (fs.readFile) {
connection.onRequest(new RequestType<RootedFilePath, unknown, void>(name), fileName => {
const result = fs.readFile!(fileName);
// readFile has 3 returns: string (content), null (not found), undefined (fall back).
// JSON-RPC can't distinguish null from undefined, so wrap in object.
return result === undefined ? null : { content: result };
});
}
break;
case "fileExists":
if (fs.fileExists) {
connection.onRequest(new RequestType<RootedFilePath, unknown, void>(name), fileName => fs.fileExists!(fileName) ?? null);
}
break;
case "directoryExists":
if (fs.directoryExists) {
connection.onRequest(new RequestType<RootedDirectoryPath, unknown, void>(name), directoryName => fs.directoryExists!(directoryName) ?? null);
}
break;
case "getAccessibleEntries":
if (fs.getAccessibleEntries) {
connection.onRequest(new RequestType<RootedDirectoryPath, unknown, void>(name), directoryName => fs.getAccessibleEntries!(directoryName) ?? null);
}
break;
case "realpath":
if (fs.realpath) {
connection.onRequest(new RequestType<RootedPath, unknown, void>(name), path => fs.realpath!(path) ?? null);
}
break;
case "writeFile":
if (fs.writeFile) {
connection.onRequest(new RequestType<{ path: RootedFilePath; data: string; }, unknown, void>(name), arg => {
fs.writeFile!(arg.path, arg.data);
return null;
});
}
return result ?? null;
});
break;
}
}
}
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 } from "../proto.ts";
import type {
NodeHandle,
Expand Down Expand Up @@ -387,26 +391,26 @@ 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[];
}

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
77 changes: 48 additions & 29 deletions packages/typescript/src/api/fs.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
import { getPathComponents } from "./path.ts";
import type {
RootedDirectoryPath,
RootedFilePath,
RootedPath,
} from "../ast/index.ts";
import {
getPathComponents,
toRootedFilePath,
} from "./path.ts";

export interface FileSystemEntries {
files: string[];
directories: string[];
}

export interface FileSystem {
directoryExists?: (directoryName: string) => boolean | undefined;
fileExists?: (fileName: string) => boolean | undefined;
getAccessibleEntries?: (directoryName: string) => FileSystemEntries | undefined;
directoryExists?: (directoryName: RootedDirectoryPath) => boolean | undefined;
fileExists?: (fileName: RootedFilePath) => boolean | undefined;
getAccessibleEntries?: (directoryName: RootedDirectoryPath) => FileSystemEntries | undefined;
/**
* Read a file's content.
* - Return the file content as a `string` (including `""` for empty files).
* - Return `null` to indicate the file does not exist (without falling back to the real FS).
* - Return `undefined` to fall back to the real filesystem.
*/
readFile?: (fileName: string) => string | null | undefined;
realpath?: (path: string) => string | undefined;
writeFile?: (path: string, content: string) => void;
removeFile?: (path: string) => void;
readFile?: (fileName: RootedFilePath) => string | null | undefined;
realpath?: (path: RootedPath) => RootedPath | undefined;
writeFile?: (path: RootedFilePath, content: string) => void;
removeFile?: (path: RootedFilePath) => void;
}

export interface VirtualFileSystem extends FileSystem {
directoryExists(directoryName: RootedDirectoryPath): boolean;
fileExists(fileName: RootedFilePath): boolean;
getAccessibleEntries(directoryName: RootedDirectoryPath): FileSystemEntries | undefined;
readFile(fileName: RootedFilePath): string | undefined;
realpath(path: RootedPath): RootedPath;
writeFile(path: RootedFilePath, content: string): void;
removeFile(path: RootedFilePath): void;
}

/** The callback names supported by the Go server for virtual FS delegation. */
Expand All @@ -35,15 +53,16 @@ interface VFile {

type VNode = VDirectory | VFile;

export function createVirtualFileSystem(files: Record<string, string>): FileSystem {
export function createVirtualFileSystem(files: Record<string, string>): VirtualFileSystem {
const root: VDirectory = {
type: "directory",
children: {},
};
const content: Record<string, string> = {};
const content = new Map<RootedFilePath, string>();

for (const filePath of Object.keys(files)) {
content[filePath] = files[filePath];
for (const [rawFilePath, data] of Object.entries(files)) {
const filePath = toRootedFilePath(rawFilePath, undefined);
content.set(filePath, data);
addToTree(filePath);
}

Expand All @@ -57,11 +76,14 @@ export function createVirtualFileSystem(files: Record<string, string>): FileSyst
removeFile,
};

function getNodeFromPath(path: string): VNode | undefined {
function getNodeFromPath(path: RootedPath): VNode | undefined {
if (!path || path === "/") {
return root;
}
const segments = getPathComponents(path).slice(1);
return getNodeFromSegments(getPathComponents(path).slice(1));
}

function getNodeFromSegments(segments: readonly string[]): VNode | undefined {
let current: VNode = root;
for (const segment of segments) {
if (current.type !== "directory") {
Expand Down Expand Up @@ -90,7 +112,7 @@ export function createVirtualFileSystem(files: Record<string, string>): FileSyst
return current;
}

function addToTree(path: string): void {
function addToTree(path: RootedFilePath): void {
const segments = getPathComponents(path).slice(1);
if (segments.length === 0) {
throw new Error(`Invalid file path: "${path}"`);
Expand All @@ -100,32 +122,32 @@ export function createVirtualFileSystem(files: Record<string, string>): FileSyst
dirNode.children[filename] = { type: "file" };
}

function writeFile(path: string, data: string): void {
content[path] = data;
function writeFile(path: RootedFilePath, data: string): void {
content.set(path, data);
addToTree(path);
}

function removeFile(path: string): void {
delete content[path];
function removeFile(path: RootedFilePath): void {
content.delete(path);
const segments = getPathComponents(path).slice(1);
if (segments.length === 0) return;
const filename = segments.pop()!;
const dirNode = getNodeFromPath("/" + segments.join("/"));
const dirNode = getNodeFromSegments(segments);
if (dirNode && dirNode.type === "directory") {
delete dirNode.children[filename];
}
}

function directoryExists(directoryName: string): boolean {
function directoryExists(directoryName: RootedDirectoryPath): boolean {
const node = getNodeFromPath(directoryName);
return !!node && node.type === "directory";
}

function fileExists(fileName: string): boolean {
return fileName in content;
function fileExists(fileName: RootedFilePath): boolean {
return content.has(fileName);
}

function getAccessibleEntries(directoryName: string): FileSystemEntries | undefined {
function getAccessibleEntries(directoryName: RootedDirectoryPath): FileSystemEntries | undefined {
const node = getNodeFromPath(directoryName);
if (!node || node.type !== "directory") {
return undefined;
Expand All @@ -143,10 +165,7 @@ export function createVirtualFileSystem(files: Record<string, string>): FileSyst
return { files: fileEntries, directories };
}

function readFile(fileName: string): string | undefined {
if (fileName in content) {
return content[fileName];
}
return undefined;
function readFile(fileName: RootedFilePath): string | undefined {
return content.get(fileName);
}
}
3 changes: 2 additions & 1 deletion packages/typescript/src/api/node/node.infrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type FileReference,
ModifierFlags,
type Node,
type PathKey,
SyntaxKind,
} from "../../ast/index.ts";
import type { TimingCollector } from "../timing.ts";
Expand Down Expand Up @@ -52,7 +53,7 @@ export interface SourceFileInfo {
readonly _offsetStructuredData: number;
readonly _decoder: TextDecoder;
nodes: any[];
readonly path?: string;
readonly path?: PathKey;
/**
* The timing collector that per-node materialization is reported into, and
* that this source file registered itself with when fetched. Present only
Expand Down
Loading
Loading