Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
75 changes: 69 additions & 6 deletions packages/typescript/scripts/generateSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* // @sync-skip-block-start/end — omit all lines between (inclusive)
* // @sync-only-start/end — uncomment lines between (strip "// " prefix)
* // @sync: <code> — replace this line with <code> (preserving indent)
* // @generators: <code> - replace this line only in generator output
*
* AST-based transforms (applied after directives using the TypeScript compiler API):
* - Generate synchronous implementations from async methods
Expand Down Expand Up @@ -60,6 +61,7 @@ function generateSyncFile(
transform: SourceTransform,
force: boolean,
variant: SyncVariant = "sync",
preserveGeneratorOverrides = false,
): GeneratedFile | undefined {
const generated = new GeneratedFile(destPath, [import.meta.filename, srcPath]);
if (generated.isCurrent(force)) return;
Expand All @@ -69,7 +71,7 @@ function generateSyncFile(
const normalized = source.replace(/\r/g, "");

// Phase 1: Process sync directives (text-based, operates on comments/lines)
const afterDirectives = processDirectives(normalized.split("\n"), variant).join("\n");
const afterDirectives = processDirectives(normalized.split("\n"), variant, preserveGeneratorOverrides).join("\n");

// Phase 2: AST-based async→sync transforms
const fileName = destPath.split("/").pop()!;
Expand All @@ -88,7 +90,7 @@ function generateSyncFile(

// ── Directive processing ─────────────────────────────────────────

function processDirectives(lines: string[], variant: SyncVariant): string[] {
function processDirectives(lines: string[], variant: SyncVariant, preserveGeneratorOverrides = false): string[] {
const output: string[] = [];
let skipBlock = false;
let syncOnlyBlock = false;
Expand Down Expand Up @@ -163,7 +165,7 @@ function processDirectives(lines: string[], variant: SyncVariant): string[] {
continue;
}

const lineWithoutGeneratorsDirective = generatorsReplaceMatch
const lineWithoutGeneratorsDirective = generatorsReplaceMatch && !preserveGeneratorOverrides
? line.slice(0, generatorsReplaceMatch.index).trimEnd()
: line;
const syncReplaceMatch = lineWithoutGeneratorsDirective.match(/\/\/ @sync: (.+)$/);
Expand Down Expand Up @@ -211,14 +213,35 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators
const edits: Edit[] = [];
const asyncMethodNames = new Set<string>();
const synchronousMethodNames = new Set<string>();
const batchedMethodNames = new Set<string>();
const asyncMethods: ts.MethodDeclaration[] = [];

for (const classDeclaration of getNamedClasses(sourceFile).values()) {
for (const group of getMethodGroups(classDeclaration).values()) {
const method = group.implementation;
if (isGeneratorMethod(method)) asyncMethodNames.add((method.name as ts.Identifier).text);
if (isGeneratorMethod(method)) {
asyncMethodNames.add((method.name as ts.Identifier).text);
asyncMethods.push(method);
}
else synchronousMethodNames.add((method.name as ts.Identifier).text);
}
}
let changed = true;
while (changed) {
changed = false;
for (const method of asyncMethods) {
const name = (method.name as ts.Identifier).text;
if (!batchedMethodNames.has(name) && containsBatchedCall(method.body!)) {
batchedMethodNames.add(name);
changed = true;
}
}
}

function containsBatchedCall(node: ts.Node): boolean {
if (isPromiseAllCall(node) || ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && batchedMethodNames.has(node.expression.name.text)) return true;
return ts.forEachChild(node, containsBatchedCall) ?? false;
}

visit(sourceFile);
return applyEdits(source, edits);
Expand Down Expand Up @@ -321,6 +344,9 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators
if (!node.type) return "void";
if (mode === "generator" && isPromiseType(node.type)) {
const returnType = getUnwrappedPromiseText(node.type, source, sourceFile);
if (node.name && ts.isIdentifier(node.name) && batchedMethodNames.has(node.name.text)) {
return `APIRequestGenerator<${returnType}>`;
}
return `Generator<ProtocolRequest, ${returnType}, ProtocolResponse["result"]>`;
}
return transformNodeText(node.type, mode, false);
Expand All @@ -347,7 +373,10 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators
}

visitNode(node);
return applyEdits(source.slice(start, node.end), localEdits);
const result = applyEdits(source.slice(start, node.end), localEdits);
return ts.isBlock(node)
? processDirectives(result.split("\n"), mode === "generator" ? "generators" : "sync").join("\n")
: result;
}

function addGeneratorEdit(node: ts.Node, offset: number, targetEdits: Edit[]): boolean {
Expand Down Expand Up @@ -387,6 +416,24 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators
}

function getGeneratorAwaitText(node: ts.AwaitExpression): string {
if (isPromiseAllCall(node.expression)) {
const argument = node.expression.arguments[0];
if (
!ts.isCallExpression(argument) ||
!ts.isPropertyAccessExpression(argument.expression) ||
argument.expression.name.text !== "map" ||
argument.arguments.length !== 1 ||
!ts.isArrowFunction(argument.arguments[0]) ||
!ts.isCallExpression(argument.arguments[0].body) ||
!isGeneratorCall(argument.arguments[0].body)
) {
throw new Error("Promise.all requires a map callback returning an API method call");
}
const callback = argument.arguments[0];
const receiver = getTextWithOwner(argument.expression.expression);
const parameters = callback.parameters.map(parameter => getTextWithOwner(parameter)).join(", ");
return `(yield* all(...${receiver}.map((${parameters}) => ${getGeneratorCallText(argument.arguments[0].body)})))`;
}
if (isAPIRequestCall(node.expression)) {
const receiver = node.expression.expression.expression;
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "client") return getRequestYieldText(node.expression);
Expand Down Expand Up @@ -487,6 +534,11 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators
}

function addSyncEdit(node: ts.Node, source: string, sourceFile: ts.SourceFile, edits: Edit[], offset: number = 0): boolean {
if (isPromiseAllCall(node)) {
const argument = node.arguments[0];
edits.push({ start: node.getStart(sourceFile) - offset, end: argument.getStart(sourceFile) - offset, newText: "" });
edits.push({ start: argument.end - offset, end: node.end - offset, newText: "" });
}
if (
(ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
node.modifiers
Expand Down Expand Up @@ -540,6 +592,15 @@ function addSyncEdit(node: ts.Node, source: string, sourceFile: ts.SourceFile, e
return false;
}

function isPromiseAllCall(node: ts.Node): node is ts.CallExpression {
return ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.expression) &&
node.expression.expression.text === "Promise" &&
node.expression.name.text === "all" &&
node.arguments.length === 1;
}

function isPromiseTypeReference(node: ts.Node | undefined): node is ts.TypeReferenceNode {
return !!node &&
ts.isTypeReferenceNode(node) &&
Expand Down Expand Up @@ -641,7 +702,7 @@ export function generateSync(force = false): void {
? (source, fileName) =>
[
'import type { APIRequest as ProtocolRequest, APIResponse as ProtocolResponse } from "../proto.ts";',
'import { apiRequest, cacheGeneratorMethod } from "./generatorSupport.ts";',
'import { all, apiRequest, cacheGeneratorMethod, type APIRequestGenerator } from "./generatorSupport.ts";',
"",
transformAsyncSource(source, fileName, true),
].join("\n")
Expand All @@ -652,6 +713,8 @@ export function generateSync(force = false): void {
transformAsyncSource(source, fileName, true),
].join("\n"),
force,
"sync",
relPath === "api.ts",
));
}

Expand Down
21 changes: 20 additions & 1 deletion packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { assertNever } from "../../internal/utils.ts";
import {
encodeNode,
sourceFileResponseToUint8Array,
uint8ArrayToBase64,
} from "../node/encoder.ts";
import {
Expand Down Expand Up @@ -95,13 +96,15 @@ import type {
ModuleResolutionSpec,
PackageId,
ParsedCommandLine,
ProgramStatisticsResponse,
ProjectId,
ProjectReference,
ProjectResponse,
ProtocolSymbolResponse,
RawCompilerOptions,
ReadConfigFileResponse,
ReconfigureSnapshotProgramParams as ProtocolReconfigureSnapshotProgramParams,
RelationCacheSizes,
ResolutionMode,
ResolvedModule,
ResolvedTypeReferenceDirective,
Expand Down Expand Up @@ -238,10 +241,12 @@ export type {
ObjectType,
PackageId,
ParsedCommandLine,
ProgramStatisticsResponse,
ProjectId,
ProjectReference,
RawCompilerOptions,
ReadConfigFileResponse,
RelationCacheSizes,
RequestTiming,
ResolutionMode,
ResolvedModule,
Expand Down Expand Up @@ -344,7 +349,6 @@ export interface TranspileOutput {
// @sync-only-start
// export { all, defer, type APIRequestGenerator, type AnyAPIRequestGenerator, type AllAPIRequestGenerator, type DeferredAPIRequestGenerator, type ExecutedGeneratorsResults } from "./generatorSupport.ts";
// import {executeRequestGenerators, type ExecutedGeneratorsResults, type AnyAPIRequestGenerator} from "./generatorSupport.ts";
// import { sourceFileResponseToUint8Array } from "../node/encoder.ts";
// @sync-only-end

export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHost {
Expand Down Expand Up @@ -1796,6 +1800,21 @@ export class Program<Id extends ProjectId = ProjectId> implements FormatDiagnost
return this.project.parsedCommandLine.options;
}

getRootFileNames(): readonly RootedFilePath[] {
return this.project.parsedCommandLine.fileNames;
}

getProjectReferences(): readonly ProjectReference[] | undefined {
return this.project.parsedCommandLine.projectReferences;
}

getStatistics(): Promise<ProgramStatisticsResponse> {
return this.client.apiRequest("getProgramStatistics", {
snapshot: this.snapshotId,
project: this.project.handle,
});
}

async getSourceFile(file: DocumentIdentifier): Promise<SourceFile | undefined> {
const fileName = resolveFileName(file);
const path = this.pathKeyForFileName(fileName);
Expand Down
31 changes: 25 additions & 6 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export interface APIMethodInfo {
getNonMissingTypeOfSymbol: APIMethod<GetTypeOfSymbolParams, TypeResponse>;
getSourceFile: APIMethod<GetSourceFileParams, SourceFileResponse | null>;
getSourceFileNames: APIMethod<GetSourceFileNamesParams, RootedFilePath[]>;
getProgramStatistics: APIMethod<GetProjectDiagnosticsParams, ProgramStatisticsResponse>;
getSourceFileMetadata: APIMethod<GetSourceFileParams, SourceFileMetadata | null>;
getModeForUsageLocation: APIMethod<GetModeForUsageLocationParams, ModuleKind>;
getModeForResolutionAtIndex: APIMethod<GetModeForResolutionAtIndexParams, ModuleKind>;
Expand Down Expand Up @@ -602,6 +603,21 @@ export interface GetSourceFileNamesParams {
project: number;
}

/** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */
export interface GetProjectDiagnosticsParams {
snapshot: number;
project: number;
}

export interface ProgramStatisticsResponse {
nodeCount: number;
identifierCount: number;
symbolCount: number;
typeCount: number;
instantiationCount: number;
relationCacheSizes: RelationCacheSizes;
}

/** SourceFileMetadata carries program-stored metadata about a single source file. */
export interface SourceFileMetadata {
isDefaultLibrary: boolean;
Expand Down Expand Up @@ -675,12 +691,6 @@ export interface GetResolvedTypeReferenceDirectiveFromReferenceParams {
resolutionMode: ModuleKind;
}

/** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */
export interface GetProjectDiagnosticsParams {
snapshot: number;
project: number;
}

export interface ResolveNameParams {
snapshot: number;
project: number;
Expand Down Expand Up @@ -1290,6 +1300,7 @@ export interface BatchRequest {
| "getParentOfSymbolForChecker"
| "getPrivateIdentifierPropertyOfType"
| "getProgramDiagnostics"
| "getProgramStatistics"
| "getPropertiesOfType"
| "getPropertyOfType"
| "getPropertySymbolOfDestructuringAssignment"
Expand Down Expand Up @@ -1483,6 +1494,7 @@ export interface BatchResponse {
| "getParentOfSymbolForChecker"
| "getPrivateIdentifierPropertyOfType"
| "getProgramDiagnostics"
| "getProgramStatistics"
| "getPropertiesOfType"
| "getPropertyOfType"
| "getPropertySymbolOfDestructuringAssignment"
Expand Down Expand Up @@ -1737,6 +1749,13 @@ export interface TranspileOptions {
reportDiagnostics?: boolean | undefined;
}

export interface RelationCacheSizes {
assignable: number;
identity: number;
subtype: number;
strictSubtype: number;
}

export interface PackageId {
name: string;
subModuleName: string;
Expand Down
Loading
Loading