Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scaffold IPC-based API #711

Merged
merged 71 commits into from
Apr 10, 2025
Merged
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
af278cc
Example working
andrewbranch Mar 3, 2025
2ffb88e
Refactor API into own package
andrewbranch Mar 3, 2025
c563027
WIP
andrewbranch Mar 5, 2025
8867ecb
WIP
andrewbranch Mar 8, 2025
b6c10fe
WIP
andrewbranch Mar 10, 2025
8ccc215
WIP
andrewbranch Mar 11, 2025
75191c1
Switch to new libsyncrpc protocol
andrewbranch Mar 12, 2025
04b0181
Switch to integer project ids
andrewbranch Mar 12, 2025
386fff2
Custom binary encoding for symbols and types
andrewbranch Mar 12, 2025
29b04b6
Revert "Custom binary encoding for symbols and types"
andrewbranch Mar 12, 2025
057c6cb
Use pnpm
andrewbranch Mar 14, 2025
f932949
Split AST and API packages
andrewbranch Mar 14, 2025
e65d5d5
Clean up
andrewbranch Mar 14, 2025
5819b1a
Add client benchmarks
andrewbranch Mar 17, 2025
8257e8e
Batch symbol requests more efficiently
andrewbranch Mar 17, 2025
db7ce0f
Switch to byte indexes in encoder
andrewbranch Mar 18, 2025
8131c9b
Transfer source file text
andrewbranch Mar 19, 2025
db1d249
Merge branch 'main' into api
andrewbranch Mar 19, 2025
518a5b0
Delete async API for now
andrewbranch Mar 19, 2025
56d7e67
More pnpm things after merge
andrewbranch Mar 19, 2025
bd92afa
Benchmark client-hosted file system
andrewbranch Mar 19, 2025
7a07c5c
Implement extended data table
andrewbranch Mar 20, 2025
e454aeb
Document binary encoding
andrewbranch Mar 20, 2025
9602b07
Include Strada for comparison in benchmarks
andrewbranch Mar 21, 2025
9c9c255
Support client-side object equality
andrewbranch Mar 24, 2025
e60c60f
Add tests to hereby/CI
andrewbranch Mar 24, 2025
4ceae39
Merge branch 'main' into api
andrewbranch Mar 24, 2025
7a8005c
Delete unused TextLength tracking
andrewbranch Mar 24, 2025
15776e4
Delete DS_Store
andrewbranch Mar 24, 2025
df0968d
Fix parseConfigFile and update libsyncrpc
andrewbranch Mar 25, 2025
c880885
Add tests for getting symbols and types
andrewbranch Mar 26, 2025
de52e45
Wrap protocol in MessagePack
andrewbranch Mar 26, 2025
8130058
Switch back to npm
andrewbranch Mar 26, 2025
44328e7
Use extended node data for SourceFile, include fileName property
andrewbranch Mar 26, 2025
5ce3cb7
Switch to explicit-only resource management
andrewbranch Mar 27, 2025
f01b3e1
Change batching, add node id handling
andrewbranch Mar 27, 2025
39121ea
Fix getSymbolAtLocation, materializing children, etc
andrewbranch Mar 31, 2025
2eefe62
Merge branch 'main' into api
andrewbranch Mar 31, 2025
17c625b
Format
andrewbranch Apr 1, 2025
66fe030
Add vfs.FS.Remove to api.Server
andrewbranch Apr 1, 2025
8c785bd
Add libsyncrpc as a git dependency
andrewbranch Apr 1, 2025
f9655ab
Add encoder baseline
andrewbranch Apr 1, 2025
d985ac2
Type check JS before testing, use --experimental-strip-types
andrewbranch Apr 1, 2025
d76c2f6
Kind is 16-bit
andrewbranch Apr 1, 2025
ef3fe0f
lint
andrewbranch Apr 1, 2025
3630c83
Add dependencies to test:all
andrewbranch Apr 1, 2025
866518c
Fix @typescript/ast exports
andrewbranch Apr 1, 2025
7fd8f5f
Explicitly install rust toolchain on runners
andrewbranch Apr 1, 2025
b4a48fe
Add .exe to tsserverPath on Windows
andrewbranch Apr 1, 2025
e9099b5
Fix windows paths
andrewbranch Apr 2, 2025
5bcf558
Update benchmarks
andrewbranch Apr 2, 2025
4f87998
Split hereby test:all
andrewbranch Apr 2, 2025
8f36dc0
Add Rust toolchain to README
andrewbranch Apr 2, 2025
8c31046
Delete DS_Store
andrewbranch Apr 2, 2025
5580a3a
Move env to job level
andrewbranch Apr 2, 2025
81d1574
Preserve same CI testing order
andrewbranch Apr 2, 2025
4fbc66a
Update _packages/api/src/api.ts
andrewbranch Apr 2, 2025
9c25263
Fix overload implementation signature
andrewbranch Apr 2, 2025
df6851f
Merge branch 'main' into api
andrewbranch Apr 2, 2025
e1d90df
Merge branch 'main' into api
andrewbranch Apr 3, 2025
efc981f
Use atomic id getter functions
andrewbranch Apr 3, 2025
9f8dffc
tsconfig hierarchy
andrewbranch Apr 3, 2025
f5d829f
Bring back hereby test:all
andrewbranch Apr 3, 2025
072d696
Use constants for handle prefixes
andrewbranch Apr 3, 2025
15600c8
Move benchmarks into tests, run single iteration as part of tests
andrewbranch Apr 3, 2025
5efde45
Skip benchmarks when submodule isn’t cloned
andrewbranch Apr 3, 2025
9274c93
Merge branch 'main' into api
andrewbranch Apr 8, 2025
a7d15d0
Merge branch 'main' into api
andrewbranch Apr 8, 2025
a212d0e
Merge branch 'main' into api
andrewbranch Apr 10, 2025
c93f782
Install rust on merge-queue workflow
andrewbranch Apr 10, 2025
ae749fb
Fix test:all dependencies
andrewbranch Apr 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
cache-name: build
@@ -36,6 +37,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable

- run: npm ci

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

- name: Tests
id: test
run: npx hereby test:all
run: npx hereby test

- name: Benchmarks
run: npx hereby test:benchmarks

- name: Tools Tests
run: npx hereby test:tools

- name: API Tests
run: npx hereby test:api

- run: git add .

@@ -150,6 +162,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
cache-name: lint${{ (matrix.noembed && '-noembed') || ''}}
@@ -163,6 +176,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
cache-name: format
@@ -179,6 +193,7 @@ jobs:
with:
submodules: true
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
cache-name: generate
@@ -211,6 +226,7 @@ jobs:
with:
submodules: true
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
cache-name: smoke
@@ -242,6 +258,7 @@ jobs:
with:
submodules: true
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
cache-name: baselines
1 change: 1 addition & 0 deletions .github/workflows/merge-queue.yml
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ jobs:
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 'lts/*'
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
cache-name: merge-queue-test
19 changes: 19 additions & 0 deletions Herebyfile.mjs
Original file line number Diff line number Diff line change
@@ -314,18 +314,37 @@ async function runTestTools() {
await $test({ cwd: path.join(__dirname, "_tools") })`${gotestsum("tools")} ./...`;
}

async function runTestAPI() {
await $`npm run -w @typescript/api test`;
}

export const testTools = task({
name: "test:tools",
run: runTestTools,
});

export const buildAPITests = task({
name: "build:api:test",
run: async () => {
await $`npm run -w @typescript/api build:test`;
},
});

export const testAPI = task({
name: "test:api",
dependencies: [tsgo, buildAPITests],
run: runTestAPI,
});

export const testAll = task({
name: "test:all",
dependencies: [tsgo, buildAPITests],
run: async () => {
// Prevent interleaving by running these directly instead of in parallel.
await runTests();
await runTestBenchmarks();
await runTestTools();
await runTestAPI();
},
});

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ Interested developers can clone and run locally to try out things as they become

## How to Build and Run

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).
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).

For tests and code generation, this repo contains a git submodule to the main TypeScript repo pointing to the commit being ported.
When cloning, you'll want to clone with submodules:
51 changes: 51 additions & 0 deletions _packages/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"private": true,
"name": "@typescript/api",
"version": "1.0.0",
"type": "module",
"imports": {
"#symbolFlags": {
"@typescript/source": {
"types": "./src/symbolFlags.enum.ts",
"default": "./src/symbolFlags.ts"
},
"types": "./dist/symbolFlags.enum.d.ts",
"default": "./dist/symbolFlags.js"
},
"#typeFlags": {
"@typescript/source": {
"types": "./src/typeFlags.enum.ts",
"default": "./src/typeFlags.ts"
},
"types": "./dist/typeFlags.enum.d.ts",
"default": "./dist/typeFlags.js"
}
},
"exports": {
".": {
"@typescript/source": "./src/api.ts",
"default": "./dist/api.js"
},
"./fs": {
"@typescript/source": "./src/fs.ts",
"default": "./dist/fs.js"
},
"./proto": {
"@typescript/source": "./src/proto.ts",
"default": "./dist/proto.js"
}
},
"scripts": {
"build": "tsc -b",
"build:test": "tsc -b test",
"bench": "node --experimental-strip-types --no-warnings --conditions @typescript/source test/api.bench.ts",
"test": "node --test --experimental-strip-types --no-warnings --conditions @typescript/source ./test/**/*.test.ts"
},
"devDependencies": {
"tinybench": "^3.1.1"
},
"dependencies": {
"@typescript/ast": "1.0.0",
"libsyncrpc": "github:microsoft/libsyncrpc#bb02d84"
}
}
179 changes: 179 additions & 0 deletions _packages/api/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/// <reference path="./node.ts" preserve="true" />
import { SymbolFlags } from "#symbolFlags";
import { TypeFlags } from "#typeFlags";
import type {
Node,
SourceFile,
} from "@typescript/ast";
import { Client } from "./client.ts";
import type { FileSystem } from "./fs.ts";
import { RemoteSourceFile } from "./node.ts";
import { ObjectRegistry } from "./objectRegistry.ts";
import type {
ConfigResponse,
ProjectResponse,
SymbolResponse,
TypeResponse,
} from "./proto.ts";

export { SymbolFlags, TypeFlags };

export interface APIOptions {
tsserverPath: string;
cwd?: string;
logFile?: string;
fs?: FileSystem;
}

export class API {
private client: Client;
private objectRegistry: ObjectRegistry;
constructor(options: APIOptions) {
this.client = new Client(options);
this.objectRegistry = new ObjectRegistry(this.client);
}

parseConfigFile(fileName: string): ConfigResponse {
return this.client.request("parseConfigFile", { fileName });
}

loadProject(configFileName: string): Project {
const data = this.client.request("loadProject", { configFileName });
return this.objectRegistry.getProject(data);
}

echo(message: string): string {
return this.client.echo(message);
}

echoBinary(message: Uint8Array): Uint8Array {
return this.client.echoBinary(message);
}

close(): void {
this.client.close();
}
}

export class DisposableObject {
private disposed: boolean = false;
protected objectRegistry: ObjectRegistry;
constructor(objectRegistry: ObjectRegistry) {
this.objectRegistry = objectRegistry;
}
[globalThis.Symbol.dispose](): void {
this.objectRegistry.release(this);
this.disposed = true;
}
dispose(): void {
this[globalThis.Symbol.dispose]();
}
isDisposed(): boolean {
return this.disposed;
}
ensureNotDisposed(): this {
if (this.disposed) {
throw new Error(`${this.constructor.name} is disposed`);
}
return this;
}
}

export class Project extends DisposableObject {
private decoder = new TextDecoder();
private client: Client;

id: string;
configFileName!: string;
compilerOptions!: Record<string, unknown>;
rootFiles!: readonly string[];

constructor(client: Client, objectRegistry: ObjectRegistry, data: ProjectResponse) {
super(objectRegistry);
this.id = data.id;
this.client = client;
this.loadData(data);
}

loadData(data: ProjectResponse): void {
this.configFileName = data.configFileName;
this.compilerOptions = data.compilerOptions;
this.rootFiles = data.rootFiles;
}

reload(): void {
this.ensureNotDisposed();
this.loadData(this.client.request("loadProject", { configFileName: this.configFileName }));
}

getSourceFile(fileName: string): SourceFile | undefined {
this.ensureNotDisposed();
const data = this.client.requestBinary("getSourceFile", { project: this.id, fileName });
return data ? new RemoteSourceFile(data, this.decoder) as unknown as SourceFile : undefined;
}

getSymbolAtLocation(node: Node): Symbol | undefined;
getSymbolAtLocation(nodes: readonly Node[]): (Symbol | undefined)[];
getSymbolAtLocation(nodeOrNodes: Node | readonly Node[]): Symbol | (Symbol | undefined)[] | undefined {
this.ensureNotDisposed();
if (Array.isArray(nodeOrNodes)) {
const data = this.client.request("getSymbolsAtLocations", { project: this.id, locations: nodeOrNodes.map(node => node.id) });
return data.map((d: SymbolResponse | null) => d ? this.objectRegistry.getSymbol(d) : undefined);
}
const data = this.client.request("getSymbolAtLocation", { project: this.id, location: (nodeOrNodes as Node).id });
return data ? this.objectRegistry.getSymbol(data) : undefined;
}

getSymbolAtPosition(fileName: string, position: number): Symbol | undefined;
getSymbolAtPosition(fileName: string, positions: readonly number[]): (Symbol | undefined)[];
getSymbolAtPosition(fileName: string, positionOrPositions: number | readonly number[]): Symbol | (Symbol | undefined)[] | undefined {
this.ensureNotDisposed();
if (typeof positionOrPositions === "number") {
const data = this.client.request("getSymbolAtPosition", { project: this.id, fileName, position: positionOrPositions });
return data ? this.objectRegistry.getSymbol(data) : undefined;
}
const data = this.client.request("getSymbolsAtPositions", { project: this.id, fileName, positions: positionOrPositions });
return data.map((d: SymbolResponse | null) => d ? this.objectRegistry.getSymbol(d) : undefined);
}

getTypeOfSymbol(symbol: Symbol): Type | undefined;
getTypeOfSymbol(symbols: readonly Symbol[]): (Type | undefined)[];
getTypeOfSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Type | (Type | undefined)[] | undefined {
this.ensureNotDisposed();
if (Array.isArray(symbolOrSymbols)) {
const data = this.client.request("getTypesOfSymbols", { project: this.id, symbols: symbolOrSymbols.map(symbol => symbol.ensureNotDisposed().id) });
return data.map((d: TypeResponse | null) => d ? this.objectRegistry.getType(d) : undefined);
}
const data = this.client.request("getTypeOfSymbol", { project: this.id, symbol: (symbolOrSymbols as Symbol).ensureNotDisposed().id });
return data ? this.objectRegistry.getType(data) : undefined;
}
}

export class Symbol extends DisposableObject {
private client: Client;
id: string;
name: string;
flags: SymbolFlags;
checkFlags: number;

constructor(client: Client, objectRegistry: ObjectRegistry, data: SymbolResponse) {
super(objectRegistry);
this.client = client;
this.id = data.id;
this.name = data.name;
this.flags = data.flags;
this.checkFlags = data.checkFlags;
}
}

export class Type extends DisposableObject {
private client: Client;
id: string;
flags: TypeFlags;
constructor(client: Client, objectRegistry: ObjectRegistry, data: TypeResponse) {
super(objectRegistry);
this.client = client;
this.id = data.id;
this.flags = data.flags;
}
}
Loading