Skip to content

Commit ed9b879

Browse files
committed
Merge branch 'main' into make-codegen-incremental
2 parents 4603f1b + 9a75076 commit ed9b879

68 files changed

Lines changed: 4400 additions & 866 deletions

Some content is hidden

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

.github/skills/api-client/SKILL.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
name: api-client
3+
description: Instructions for working on the API client in ./packages/typescript
4+
---
5+
6+
## Code comments
7+
8+
The user may tell you to reference Strada (the old TypeScript codebase) for implementation, but your code comments should not acknowledge that this codebase is a port of anything else. Comments should stand on their own and describe the code in the current codebase. Never say "Mirrors TypeScript's 'foo' function" or "...like the classic API's 'Bar' type."
9+
10+
## Unnecessary spread and `exactOptionalPropertyTypes`
11+
12+
Never write code like `...(options ? { snapshot: options.snapshot } : {})`. All optional properties should tolerate `undefined`. If `--exactOptionalPropertyTypes` is forcing you to write code this way, fix the offending type. Rewriting as `if (options) foo.snapshot = options.snapshot` is not an acceptable workaround; always fix the type. As a rule, object types used in input positions should always define optional properties with `prop?: T | undefined` so this isn't a problem.

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ jobs:
9393
os: windows
9494
coverage: true
9595
skip: ${{ github.event_name == 'merge_group' }}
96+
- runner: ubuntu-26.04-arm
97+
name: 'linux arm64'
98+
os: linux
99+
coverage: true
100+
skip: ${{ github.event_name == 'merge_group' }}
101+
- runner: windows-11-arm
102+
name: 'windows arm64'
103+
os: windows
104+
coverage: true
105+
skip: ${{ github.event_name == 'merge_group' }}
96106
- runner: macos-latest
97107
os: macos
98108
coverage: true
@@ -214,6 +224,7 @@ jobs:
214224
- run: npx hereby test:extension
215225
- run: npx hereby test:tools
216226
- run: npx hereby test:api
227+
- run: npx hereby test:benchmarks:api
217228
- run: git add .
218229
- uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0
219230
if: ${{ always() && matrix.config.coverage && github.event_name != 'merge_group' }}

Herebyfile.mjs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,7 +1512,7 @@ async function runTestBenchmarks() {
15121512

15131513
export const testBenchmarks = task({
15141514
name: "test:benchmarks",
1515-
description: "Runs all benchmarks.",
1515+
description: "Runs Go benchmarks once; excluded from validate.",
15161516
run: runTestBenchmarks,
15171517
});
15181518

@@ -1525,6 +1525,12 @@ async function runTestAPI() {
15251525
await run("node", ["--conditions", "@typescript/source", "--test", "./test/**/*.test.ts"], { cwd: "./packages/typescript" });
15261526
}
15271527

1528+
async function runTestAPIBenchmarks() {
1529+
for (const variant of ["async", "sync", "generators"]) {
1530+
await run("node", ["--conditions", "@typescript/source", `./test/${variant}/api.bench.ts`, "--singleIteration"], { cwd: "./packages/typescript" });
1531+
}
1532+
}
1533+
15281534
export const testTools = task({
15291535
name: "test:tools",
15301536
description: "Runs all tests in the tools module.",
@@ -1564,6 +1570,13 @@ export const testAPI = task({
15641570
run: runTestAPI,
15651571
});
15661572

1573+
export const testAPIBenchmarks = task({
1574+
name: "test:benchmarks:api",
1575+
description: "Runs async, sync, and generator API benchmarks once; excluded from validate.",
1576+
dependencies: [tsgo, buildAPITests],
1577+
run: runTestAPIBenchmarks,
1578+
});
1579+
15671580
export const testAll = task({
15681581
name: "test:all",
15691582
description: "Runs compiler, extension, benchmark, tools, and API tests. Codegen tests are opt-in via test:codegen.",
@@ -1575,6 +1588,7 @@ export const testAll = task({
15751588
await runTestBenchmarks();
15761589
await runTestTools();
15771590
await runTestAPI();
1591+
await runTestAPIBenchmarks();
15781592
},
15791593
});
15801594

@@ -1703,7 +1717,7 @@ async function runFormat() {
17031717

17041718
export const validate = task({
17051719
name: "validate",
1706-
description: "Generates, builds, tests, lints, and formats the repo. Pass --api to include API tests, or --all to include all code generation and ancillary repository tests.",
1720+
description: "Generates, builds, tests, lints, and formats the repo. Pass --api to include API tests, or --all to include all code generation and ancillary repository tests Benchmarks are separate: test:benchmarks and test:benchmarks:api.",
17071721
dependencies: [options.all ? generateAll : generate],
17081722
run: async () => {
17091723
await generateLibs(builtLocal);
@@ -1730,7 +1744,12 @@ export const validate = task({
17301744
await runValidation("test:api", runTestAPI);
17311745
}
17321746
if (options.all) {
1747+
<<<<<<< HEAD
17331748
await runValidation("test:benchmarks", runTestBenchmarks);
1749+
=======
1750+
await runGenerateExtension();
1751+
await runGenerateVendor();
1752+
>>>>>>> main
17341753
await runValidation("test:tools", runTestTools);
17351754
await runValidation("test:smoke", runSmokeTest); // in CI this is run with `--race`
17361755
}

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"@jakebailey/dprint-plugin-gofumpt": "^0.0.18",
3030
"@types/node": "^22.20.1",
3131
"@unicode/unicode-15.1.0": "^1.6.17",
32-
"adm-zip": "^0.6.0",
32+
"adm-zip": "^0.6.1",
3333
"chokidar": "^5.0.0",
3434
"dprint": "^0.57.4",
3535
"dprint-plugin-yaml": "^0.6.0",

packages/typescript/src/api/async/api.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,9 @@ export type {
233233
};
234234

235235
export interface TranspileOptions {
236-
compilerOptions?: CompilerOptions;
237-
fileName?: string;
238-
reportDiagnostics?: boolean;
236+
compilerOptions?: CompilerOptions | undefined;
237+
fileName?: string | undefined;
238+
reportDiagnostics?: boolean | undefined;
239239
}
240240

241241
export interface TranspileOutput {
@@ -432,6 +432,10 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
432432
snapshot: baseSnapshot.id,
433433
changes: toCreateSnapshotRequest(params),
434434
});
435+
if (data.snapshot === baseSnapshot.id) {
436+
await this.client.apiRequest("release", { snapshot: data.snapshot });
437+
return baseSnapshot;
438+
}
435439
this.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes);
436440
const snapshot = new Snapshot(
437441
data,
@@ -484,8 +488,8 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
484488
throw new Error("Cannot use an inactive snapshot as a response base");
485489
}
486490
const data = await this.client.apiRequest("getCurrentLanguageServerSnapshot", {
487-
...(baseSnapshot ? { baseSnapshot: baseSnapshot.id } : {}),
488-
...(changes ? { changes } : {}),
491+
baseSnapshot: baseSnapshot?.id,
492+
changes,
489493
});
490494
if (baseSnapshot) {
491495
this.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes);
@@ -574,12 +578,13 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
574578
/** Creates a program from current filesystem state. */
575579
async createProgram(
576580
rootFiles: readonly DocumentIdentifier[],
577-
createProgramOptions: CreateProgramOptions,
581+
compilerOptions: CompilerOptions,
582+
createProgramOptions?: CreateProgramOptions,
578583
): Promise<Program> {
579584
await this.ensureInitialized();
580585

581586
const snapshot = await this.createSnapshot({
582-
createPrograms: [{ rootFiles, options: createProgramOptions }],
587+
createPrograms: [{ rootFiles, compilerOptions, options: createProgramOptions }],
583588
});
584589
const program = snapshot.operation.createdPrograms[0];
585590
if (!program) {
@@ -624,27 +629,47 @@ export class InternalAPI {
624629
type SnapshotUpdater = (params: CreateSnapshotParams) => Promise<Snapshot>; // @sync: type SnapshotUpdater = ((params: CreateSnapshotParams) => Snapshot) & { gen(params: CreateSnapshotParams): Generator<ProtocolRequest, Snapshot, ProtocolResponse["result"]>; };
625630

626631
export interface SnapshotOperation {
627-
readonly createdPrograms?: readonly Program<SyntheticProjectId>[];
628-
readonly openedFiles?: readonly SnapshotOpenedFileOperation[];
632+
readonly createdPrograms?: readonly Program<SyntheticProjectId>[] | undefined;
633+
readonly openedFiles?: readonly SnapshotOpenedFileOperation[] | undefined;
629634
}
630635

631636
export interface SnapshotOpenedFileOperation {
632637
readonly project: Project;
633638
}
634639

640+
/** Replaces every element of a tuple while preserving its length and index structure. */
635641
type MapTupleTo<Tuple extends readonly unknown[], Result> = {
636642
readonly [Index in keyof Tuple]: Result;
637643
};
638644

645+
/**
646+
* Keeps `Tuple` as an inference target while contextually typing each element from
647+
* `Elements`. The mapped intersection supplies nested completions and excess-property
648+
* checks without widening an inferred tuple to an array.
649+
*/
650+
type ContextualizeTuple<
651+
Tuple extends readonly unknown[] | undefined,
652+
Elements extends readonly unknown[] | undefined,
653+
> =
654+
& Tuple
655+
& {
656+
readonly [Index in keyof Tuple]: NonNullable<Elements>[number];
657+
};
658+
659+
/** Substitutes the operation arrays with contextually typed, tuple-preserving versions. */
639660
type SnapshotOperationParams<
640661
Params extends CreateSnapshotParams,
641662
CreatePrograms extends Params["createPrograms"],
642663
OpenFiles extends Params["openFiles"],
643664
> = Omit<Params, "createPrograms" | "openFiles"> & {
644-
createPrograms?: CreatePrograms;
645-
openFiles?: OpenFiles;
665+
createPrograms?: ContextualizeTuple<CreatePrograms, Params["createPrograms"]> | undefined;
666+
openFiles?: ContextualizeTuple<OpenFiles, Params["openFiles"]> | undefined;
646667
};
647668

669+
/**
670+
* Refines a snapshot's operation results to required tuples when the corresponding
671+
* operation arrays were supplied, preserving their lengths for indexed access.
672+
*/
648673
type SnapshotForOperationResults<
649674
CreatePrograms extends CreateSnapshotParams["createPrograms"],
650675
OpenFiles extends CreateSnapshotParams["openFiles"],
@@ -655,6 +680,7 @@ type SnapshotForOperationResults<
655680
& (OpenFiles extends readonly unknown[] ? { readonly openedFiles: MapTupleTo<OpenFiles, SnapshotOpenedFileOperation>; } : unknown);
656681
};
657682

683+
/** Derives the refined snapshot result type from a complete operation parameter type. */
658684
export type SnapshotForOperation<Params extends CreateSnapshotParams> = SnapshotForOperationResults<
659685
Params extends { createPrograms: infer CreatePrograms extends readonly unknown[]; } ? CreatePrograms : undefined,
660686
Params extends { openFiles: infer OpenFiles extends readonly unknown[]; } ? OpenFiles : undefined
@@ -706,8 +732,8 @@ export class Snapshot {
706732
}
707733

708734
this.operation = {
709-
...(data.operation.createdPrograms ? { createdPrograms: data.operation.createdPrograms.map(projectId => this.requireProject(projectId).program) } : {}),
710-
...(data.operation.openedFiles ? { openedFiles: data.operation.openedFiles.map(result => ({ project: this.requireProject(result.project) })) } : {}),
735+
createdPrograms: data.operation.createdPrograms?.map(projectId => this.requireProject(projectId).program),
736+
openedFiles: data.operation.openedFiles?.map(result => ({ project: this.requireProject(result.project) })),
711737
};
712738

713739
this.internal = new SnapshotInternalAPI(this.id, client);
@@ -1652,7 +1678,7 @@ export class Program<Id extends ProjectId = ProjectId> implements FormatDiagnost
16521678
emitSkipped: response.emitSkipped,
16531679
diagnostics: response.diagnostics,
16541680
emittedFiles: response.emittedFiles,
1655-
...(fileSystem ? { fileSystem } : {}),
1681+
fileSystem,
16561682
};
16571683
}
16581684

packages/typescript/src/api/async/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,11 @@ export interface EmitOutput {
423423
export interface ImportSymbolAction {
424424
readonly kind: "importSymbol";
425425
readonly symbol: Symbol;
426-
readonly isValidTypeOnlyUseSite?: boolean;
426+
readonly isValidTypeOnlyUseSite?: boolean | undefined;
427427
}
428428

429429
export type ImportAdderAction = ImportSymbolAction;
430430

431431
export interface GetImportEditsForSymbolsOptions {
432-
readonly isValidTypeOnlyUseSite?: boolean;
432+
readonly isValidTypeOnlyUseSite?: boolean | undefined;
433433
}

packages/typescript/src/api/fs.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,35 +20,35 @@ export interface FileSystemEntries {
2020
}
2121

2222
export interface FileSystem {
23-
directoryExists?: (directoryName: string) => boolean | undefined;
24-
fileExists?: (fileName: string) => boolean | undefined;
25-
getAccessibleEntries?: (directoryName: string) => FileSystemEntries | undefined;
23+
directoryExists?: ((directoryName: string) => boolean | undefined) | undefined;
24+
fileExists?: ((fileName: string) => boolean | undefined) | undefined;
25+
getAccessibleEntries?: ((directoryName: string) => FileSystemEntries | undefined) | undefined;
2626
/**
2727
* Read a file's content.
2828
* - Return the file content as a `string` (including `""` for empty files).
2929
* - Return `null` to indicate the file does not exist (without falling back to the real FS).
3030
* - Return `undefined` to fall back to the real filesystem.
3131
*/
32-
readFile?: (fileName: string) => string | null | undefined;
33-
realpath?: (path: string) => string | undefined;
34-
writeFile?: (path: string, content: string) => void;
35-
removeFile?: (path: string) => void;
32+
readFile?: ((fileName: string) => string | null | undefined) | undefined;
33+
realpath?: ((path: string) => string | undefined) | undefined;
34+
writeFile?: ((path: string, content: string) => void) | undefined;
35+
removeFile?: ((path: string) => void) | undefined;
3636
}
3737

3838
/** The callback names supported by the Go server for virtual FS delegation. */
3939
export const fsCallbackNames = ["readFile", "fileExists", "directoryExists", "getAccessibleEntries", "realpath", "writeFile"] as const;
4040

4141
export interface CreateFileSystemOptions {
4242
/** Complete directory listings. Full filesystems derive these from `files` when omitted. */
43-
directories?: Record<string, RequestDirectoryEntries>;
44-
symlinks?: Record<string, RequestSymlink>;
43+
directories?: Record<string, RequestDirectoryEntries> | undefined;
44+
symlinks?: Record<string, RequestSymlink> | undefined;
4545
/** Files or directory trees hidden from an underlying snapshot or host filesystem. */
46-
removedPaths?: readonly string[];
46+
removedPaths?: readonly string[] | undefined;
4747
}
4848

4949
export interface CreateFileSystemWithLibOptions extends CreateFileSystemOptions {
5050
/** Default library directory used by a custom or non-embedded compiler executable. */
51-
defaultLibraryPath?: string;
51+
defaultLibraryPath?: string | undefined;
5252
}
5353

5454
/**
@@ -91,8 +91,8 @@ export function createFileSystemWithLib(
9191
}
9292
return createRequestFileSystem("full", files, {
9393
symlinks,
94-
...(options.directories ? { directories: options.directories } : {}),
95-
...(options.removedPaths?.length ? { removedPaths: options.removedPaths } : {}),
94+
directories: options.directories,
95+
removedPaths: options.removedPaths?.length ? options.removedPaths : undefined,
9696
});
9797
}
9898

@@ -122,9 +122,9 @@ function createRequestFileSystem(
122122
return {
123123
kind,
124124
files: fileRecord,
125-
...(directories ? { directories } : {}),
126-
...(options.symlinks ? { symlinks: options.symlinks } : {}),
127-
...(options.removedPaths?.length ? { removedPaths: [...options.removedPaths] } : {}),
125+
directories,
126+
symlinks: options.symlinks,
127+
removedPaths: options.removedPaths?.length ? [...options.removedPaths] : undefined,
128128
};
129129
}
130130

0 commit comments

Comments
 (0)