Skip to content

Commit a5454eb

Browse files
Scaffold IPC-based API (#711)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2736e15 commit a5454eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+9625
-401
lines changed

Diff for: .github/workflows/ci.yml

+18-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
steps:
2424
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
2525
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
26+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
2627
- uses: ./.github/actions/setup-go
2728
with:
2829
cache-name: build
@@ -36,6 +37,7 @@ jobs:
3637
steps:
3738
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
3839
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
40+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
3941

4042
- run: npm ci
4143

@@ -90,6 +92,7 @@ jobs:
9092
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
9193
with:
9294
node-version: 'lts/*'
95+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
9396
- uses: ./.github/actions/setup-go
9497
with:
9598
cache-name: test
@@ -103,7 +106,16 @@ jobs:
103106

104107
- name: Tests
105108
id: test
106-
run: npx hereby test:all
109+
run: npx hereby test
110+
111+
- name: Benchmarks
112+
run: npx hereby test:benchmarks
113+
114+
- name: Tools Tests
115+
run: npx hereby test:tools
116+
117+
- name: API Tests
118+
run: npx hereby test:api
107119

108120
- run: git add .
109121

@@ -150,6 +162,7 @@ jobs:
150162
steps:
151163
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
152164
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
165+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
153166
- uses: ./.github/actions/setup-go
154167
with:
155168
cache-name: lint${{ (matrix.noembed && '-noembed') || ''}}
@@ -163,6 +176,7 @@ jobs:
163176
steps:
164177
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
165178
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
179+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
166180
- uses: ./.github/actions/setup-go
167181
with:
168182
cache-name: format
@@ -179,6 +193,7 @@ jobs:
179193
with:
180194
submodules: true
181195
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
196+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
182197
- uses: ./.github/actions/setup-go
183198
with:
184199
cache-name: generate
@@ -211,6 +226,7 @@ jobs:
211226
with:
212227
submodules: true
213228
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
229+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
214230
- uses: ./.github/actions/setup-go
215231
with:
216232
cache-name: smoke
@@ -242,6 +258,7 @@ jobs:
242258
with:
243259
submodules: true
244260
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
261+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
245262
- uses: ./.github/actions/setup-go
246263
with:
247264
cache-name: baselines

Diff for: .github/workflows/merge-queue.yml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
2323
with:
2424
node-version: 'lts/*'
25+
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
2526
- uses: ./.github/actions/setup-go
2627
with:
2728
cache-name: merge-queue-test

Diff for: Herebyfile.mjs

+19
Original file line numberDiff line numberDiff line change
@@ -314,18 +314,37 @@ async function runTestTools() {
314314
await $test({ cwd: path.join(__dirname, "_tools") })`${gotestsum("tools")} ./...`;
315315
}
316316

317+
async function runTestAPI() {
318+
await $`npm run -w @typescript/api test`;
319+
}
320+
317321
export const testTools = task({
318322
name: "test:tools",
319323
run: runTestTools,
320324
});
321325

326+
export const buildAPITests = task({
327+
name: "build:api:test",
328+
run: async () => {
329+
await $`npm run -w @typescript/api build:test`;
330+
},
331+
});
332+
333+
export const testAPI = task({
334+
name: "test:api",
335+
dependencies: [tsgo, buildAPITests],
336+
run: runTestAPI,
337+
});
338+
322339
export const testAll = task({
323340
name: "test:all",
341+
dependencies: [tsgo, buildAPITests],
324342
run: async () => {
325343
// Prevent interleaving by running these directly instead of in parallel.
326344
await runTests();
327345
await runTestBenchmarks();
328346
await runTestTools();
347+
await runTestAPI();
329348
},
330349
});
331350

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Interested developers can clone and run locally to try out things as they become
77

88
## How to Build and Run
99

10-
This repo uses [Go 1.24 or higher](https://go.dev/dl/), [Node.js with npm](https://nodejs.org/), and [`hereby`](https://www.npmjs.com/package/hereby).
10+
This repo uses [Go 1.24 or higher](https://go.dev/dl/), [Rust 1.85 or higher](https://www.rust-lang.org/tools/install), [Node.js with npm](https://nodejs.org/), and [`hereby`](https://www.npmjs.com/package/hereby).
1111

1212
For tests and code generation, this repo contains a git submodule to the main TypeScript repo pointing to the commit being ported.
1313
When cloning, you'll want to clone with submodules:

Diff for: _packages/api/package.json

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"private": true,
3+
"name": "@typescript/api",
4+
"version": "1.0.0",
5+
"type": "module",
6+
"imports": {
7+
"#symbolFlags": {
8+
"@typescript/source": {
9+
"types": "./src/symbolFlags.enum.ts",
10+
"default": "./src/symbolFlags.ts"
11+
},
12+
"types": "./dist/symbolFlags.enum.d.ts",
13+
"default": "./dist/symbolFlags.js"
14+
},
15+
"#typeFlags": {
16+
"@typescript/source": {
17+
"types": "./src/typeFlags.enum.ts",
18+
"default": "./src/typeFlags.ts"
19+
},
20+
"types": "./dist/typeFlags.enum.d.ts",
21+
"default": "./dist/typeFlags.js"
22+
}
23+
},
24+
"exports": {
25+
".": {
26+
"@typescript/source": "./src/api.ts",
27+
"default": "./dist/api.js"
28+
},
29+
"./fs": {
30+
"@typescript/source": "./src/fs.ts",
31+
"default": "./dist/fs.js"
32+
},
33+
"./proto": {
34+
"@typescript/source": "./src/proto.ts",
35+
"default": "./dist/proto.js"
36+
}
37+
},
38+
"scripts": {
39+
"build": "tsc -b",
40+
"build:test": "tsc -b test",
41+
"bench": "node --experimental-strip-types --no-warnings --conditions @typescript/source test/api.bench.ts",
42+
"test": "node --test --experimental-strip-types --no-warnings --conditions @typescript/source ./test/**/*.test.ts"
43+
},
44+
"devDependencies": {
45+
"tinybench": "^3.1.1"
46+
},
47+
"dependencies": {
48+
"@typescript/ast": "1.0.0",
49+
"libsyncrpc": "github:microsoft/libsyncrpc#bb02d84"
50+
}
51+
}

Diff for: _packages/api/src/api.ts

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/// <reference path="./node.ts" preserve="true" />
2+
import { SymbolFlags } from "#symbolFlags";
3+
import { TypeFlags } from "#typeFlags";
4+
import type {
5+
Node,
6+
SourceFile,
7+
} from "@typescript/ast";
8+
import { Client } from "./client.ts";
9+
import type { FileSystem } from "./fs.ts";
10+
import { RemoteSourceFile } from "./node.ts";
11+
import { ObjectRegistry } from "./objectRegistry.ts";
12+
import type {
13+
ConfigResponse,
14+
ProjectResponse,
15+
SymbolResponse,
16+
TypeResponse,
17+
} from "./proto.ts";
18+
19+
export { SymbolFlags, TypeFlags };
20+
21+
export interface APIOptions {
22+
tsserverPath: string;
23+
cwd?: string;
24+
logFile?: string;
25+
fs?: FileSystem;
26+
}
27+
28+
export class API {
29+
private client: Client;
30+
private objectRegistry: ObjectRegistry;
31+
constructor(options: APIOptions) {
32+
this.client = new Client(options);
33+
this.objectRegistry = new ObjectRegistry(this.client);
34+
}
35+
36+
parseConfigFile(fileName: string): ConfigResponse {
37+
return this.client.request("parseConfigFile", { fileName });
38+
}
39+
40+
loadProject(configFileName: string): Project {
41+
const data = this.client.request("loadProject", { configFileName });
42+
return this.objectRegistry.getProject(data);
43+
}
44+
45+
echo(message: string): string {
46+
return this.client.echo(message);
47+
}
48+
49+
echoBinary(message: Uint8Array): Uint8Array {
50+
return this.client.echoBinary(message);
51+
}
52+
53+
close(): void {
54+
this.client.close();
55+
}
56+
}
57+
58+
export class DisposableObject {
59+
private disposed: boolean = false;
60+
protected objectRegistry: ObjectRegistry;
61+
constructor(objectRegistry: ObjectRegistry) {
62+
this.objectRegistry = objectRegistry;
63+
}
64+
[globalThis.Symbol.dispose](): void {
65+
this.objectRegistry.release(this);
66+
this.disposed = true;
67+
}
68+
dispose(): void {
69+
this[globalThis.Symbol.dispose]();
70+
}
71+
isDisposed(): boolean {
72+
return this.disposed;
73+
}
74+
ensureNotDisposed(): this {
75+
if (this.disposed) {
76+
throw new Error(`${this.constructor.name} is disposed`);
77+
}
78+
return this;
79+
}
80+
}
81+
82+
export class Project extends DisposableObject {
83+
private decoder = new TextDecoder();
84+
private client: Client;
85+
86+
id: string;
87+
configFileName!: string;
88+
compilerOptions!: Record<string, unknown>;
89+
rootFiles!: readonly string[];
90+
91+
constructor(client: Client, objectRegistry: ObjectRegistry, data: ProjectResponse) {
92+
super(objectRegistry);
93+
this.id = data.id;
94+
this.client = client;
95+
this.loadData(data);
96+
}
97+
98+
loadData(data: ProjectResponse): void {
99+
this.configFileName = data.configFileName;
100+
this.compilerOptions = data.compilerOptions;
101+
this.rootFiles = data.rootFiles;
102+
}
103+
104+
reload(): void {
105+
this.ensureNotDisposed();
106+
this.loadData(this.client.request("loadProject", { configFileName: this.configFileName }));
107+
}
108+
109+
getSourceFile(fileName: string): SourceFile | undefined {
110+
this.ensureNotDisposed();
111+
const data = this.client.requestBinary("getSourceFile", { project: this.id, fileName });
112+
return data ? new RemoteSourceFile(data, this.decoder) as unknown as SourceFile : undefined;
113+
}
114+
115+
getSymbolAtLocation(node: Node): Symbol | undefined;
116+
getSymbolAtLocation(nodes: readonly Node[]): (Symbol | undefined)[];
117+
getSymbolAtLocation(nodeOrNodes: Node | readonly Node[]): Symbol | (Symbol | undefined)[] | undefined {
118+
this.ensureNotDisposed();
119+
if (Array.isArray(nodeOrNodes)) {
120+
const data = this.client.request("getSymbolsAtLocations", { project: this.id, locations: nodeOrNodes.map(node => node.id) });
121+
return data.map((d: SymbolResponse | null) => d ? this.objectRegistry.getSymbol(d) : undefined);
122+
}
123+
const data = this.client.request("getSymbolAtLocation", { project: this.id, location: (nodeOrNodes as Node).id });
124+
return data ? this.objectRegistry.getSymbol(data) : undefined;
125+
}
126+
127+
getSymbolAtPosition(fileName: string, position: number): Symbol | undefined;
128+
getSymbolAtPosition(fileName: string, positions: readonly number[]): (Symbol | undefined)[];
129+
getSymbolAtPosition(fileName: string, positionOrPositions: number | readonly number[]): Symbol | (Symbol | undefined)[] | undefined {
130+
this.ensureNotDisposed();
131+
if (typeof positionOrPositions === "number") {
132+
const data = this.client.request("getSymbolAtPosition", { project: this.id, fileName, position: positionOrPositions });
133+
return data ? this.objectRegistry.getSymbol(data) : undefined;
134+
}
135+
const data = this.client.request("getSymbolsAtPositions", { project: this.id, fileName, positions: positionOrPositions });
136+
return data.map((d: SymbolResponse | null) => d ? this.objectRegistry.getSymbol(d) : undefined);
137+
}
138+
139+
getTypeOfSymbol(symbol: Symbol): Type | undefined;
140+
getTypeOfSymbol(symbols: readonly Symbol[]): (Type | undefined)[];
141+
getTypeOfSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Type | (Type | undefined)[] | undefined {
142+
this.ensureNotDisposed();
143+
if (Array.isArray(symbolOrSymbols)) {
144+
const data = this.client.request("getTypesOfSymbols", { project: this.id, symbols: symbolOrSymbols.map(symbol => symbol.ensureNotDisposed().id) });
145+
return data.map((d: TypeResponse | null) => d ? this.objectRegistry.getType(d) : undefined);
146+
}
147+
const data = this.client.request("getTypeOfSymbol", { project: this.id, symbol: (symbolOrSymbols as Symbol).ensureNotDisposed().id });
148+
return data ? this.objectRegistry.getType(data) : undefined;
149+
}
150+
}
151+
152+
export class Symbol extends DisposableObject {
153+
private client: Client;
154+
id: string;
155+
name: string;
156+
flags: SymbolFlags;
157+
checkFlags: number;
158+
159+
constructor(client: Client, objectRegistry: ObjectRegistry, data: SymbolResponse) {
160+
super(objectRegistry);
161+
this.client = client;
162+
this.id = data.id;
163+
this.name = data.name;
164+
this.flags = data.flags;
165+
this.checkFlags = data.checkFlags;
166+
}
167+
}
168+
169+
export class Type extends DisposableObject {
170+
private client: Client;
171+
id: string;
172+
flags: TypeFlags;
173+
constructor(client: Client, objectRegistry: ObjectRegistry, data: TypeResponse) {
174+
super(objectRegistry);
175+
this.client = client;
176+
this.id = data.id;
177+
this.flags = data.flags;
178+
}
179+
}

0 commit comments

Comments
 (0)