From 29b7af0d6b9208fcb20b3d3f1cfa7719d021979b Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Fri, 11 Nov 2022 19:21:25 +0000 Subject: [PATCH 1/2] Generate an exportable and ambient version of the TypeScript types --- types/BUILD.bazel | 2 +- types/src/index.ts | 136 +++++++++++++++----- types/src/program.ts | 55 ++++---- types/src/standards.ts | 68 ++++++++++ types/src/transforms/ambient.ts | 18 +++ types/src/transforms/comments.ts | 123 ++++++++++++++++++ types/src/transforms/helpers.ts | 164 ++++++++++++++++++++++++ types/src/transforms/importable.ts | 18 +++ types/src/transforms/index.ts | 1 + types/src/transforms/overrides/index.ts | 130 +------------------ 10 files changed, 536 insertions(+), 179 deletions(-) create mode 100644 types/src/standards.ts create mode 100644 types/src/transforms/ambient.ts create mode 100644 types/src/transforms/comments.ts create mode 100644 types/src/transforms/helpers.ts create mode 100644 types/src/transforms/importable.ts diff --git a/types/BUILD.bazel b/types/BUILD.bazel index e933bc5575f..9d67f86d69e 100644 --- a/types/BUILD.bazel +++ b/types/BUILD.bazel @@ -28,7 +28,7 @@ js_run_binary( srcs = [ "//src/workerd/tools:api_encoder", ], - outs = ["api.d.ts"], # TODO(soon) switch to out_dirs when generating multiple files + outs = ["api.d.ts", "api.ts"], # TODO(soon) switch to out_dirs when generating multiple files args = [ "src/workerd/tools/api.capnp.bin", "--output", diff --git a/types/src/index.ts b/types/src/index.ts index 6633394e9b3..e39a0677b1a 100644 --- a/types/src/index.ts +++ b/types/src/index.ts @@ -11,18 +11,75 @@ import ts from "typescript"; import { generateDefinitions } from "./generator"; import { printNodeList, printer } from "./print"; import { createMemoryProgram } from "./program"; +import { ParsedTypeDefinition, collateStandards } from "./standards"; import { compileOverridesDefines, + createCommentsTransformer, createGlobalScopeTransformer, createIteratorTransformer, createOverrideDefineTransformer, } from "./transforms"; - +import { createAmbientTransformer } from "./transforms/ambient"; +import { createImportableTransformer } from "./transforms/importable"; const definitionsHeader = `/* eslint-disable */ // noinspection JSUnusedGlobalSymbols `; -function printDefinitions(root: StructureGroups): string { +function checkDiagnostics(sources: Map) { + const host = ts.createCompilerHost( + { noEmit: true }, + /* setParentNodes */ true + ); + + host.getDefaultLibLocation = () => + path.dirname(require.resolve("typescript")); + const program = createMemoryProgram(sources, host, { + lib: ["lib.esnext.d.ts"], + }); + const emitResult = program.emit(); + + const allDiagnostics = ts + .getPreEmitDiagnostics(program) + .concat(emitResult.diagnostics); + + allDiagnostics.forEach((diagnostic) => { + if (diagnostic.file) { + const { line, character } = ts.getLineAndCharacterOfPosition( + diagnostic.file, + diagnostic.start! + ); + const message = ts.flattenDiagnosticMessageText( + diagnostic.messageText, + "\n" + ); + console.log(`(${line + 1},${character + 1}): ${message}`); + } else { + console.log( + ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n") + ); + } + }); +} +function transform( + sources: Map, + sourcePath: string, + transforms: ( + program: ts.Program, + checker: ts.TypeChecker + ) => ts.TransformerFactory[] +) { + const program = createMemoryProgram(sources); + const checker = program.getTypeChecker(); + const sourceFile = program.getSourceFile(sourcePath); + assert(sourceFile !== undefined); + const result = ts.transform(sourceFile, transforms(program, checker)); + assert.strictEqual(result.transformed.length, 1); + return printer.printFile(result.transformed[0]); +} +function printDefinitions( + root: StructureGroups, + standards: ParsedTypeDefinition +): { ambient: string; importable: string } { // Generate TypeScript nodes from capnp request const nodes = generateDefinitions(root); @@ -33,45 +90,46 @@ function printDefinitions(root: StructureGroups): string { let source = printNodeList(nodes); sources.set(sourcePath, source); - // Build TypeScript program from source files and overrides. Importantly, - // these are in the same program, so we can use nodes from one in the other. - let program = createMemoryProgram(sources); - let checker = program.getTypeChecker(); - let sourceFile = program.getSourceFile(sourcePath); - assert(sourceFile !== undefined); - // Run post-processing transforms on program - let result = ts.transform(sourceFile, [ + source = transform(sources, sourcePath, (program, checker) => [ // Run iterator transformer before overrides so iterator-like interfaces are // still removed if they're replaced in overrides createIteratorTransformer(checker), createOverrideDefineTransformer(program, replacements), ]); - assert.strictEqual(result.transformed.length, 1); // We need the type checker to respect our updated definitions after applying // overrides (e.g. to find the correct nodes when traversing heritage), so // rebuild the program to re-run type checking. // TODO: is there a way to re-run the type checker on an existing program? - source = printer.printFile(result.transformed[0]); - program = createMemoryProgram(new Map([[sourcePath, source]])); - checker = program.getTypeChecker(); - sourceFile = program.getSourceFile(sourcePath); - assert(sourceFile !== undefined); + source = transform( + new Map([[sourcePath, source]]), + sourcePath, + (program, checker) => [ + // Run global scope transformer after overrides so members added in + // overrides are extracted + createGlobalScopeTransformer(checker), + createCommentsTransformer(standards), + createAmbientTransformer(), + // TODO(polish): maybe flatten union types? + ] + ); - result = ts.transform(sourceFile, [ - // Run global scope transformer after overrides so members added in - // overrides are extracted - createGlobalScopeTransformer(checker), - // TODO(polish): maybe flatten union types? - ]); - assert.strictEqual(result.transformed.length, 1); + checkDiagnostics(new Map([[sourcePath, source]])); + + const importable = transform( + new Map([[sourcePath, source]]), + sourcePath, + () => [createImportableTransformer()] + ); - // TODO(polish): maybe log diagnostics with `ts.getPreEmitDiagnostics(program, sourceFile)`? - // (see https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#a-minimal-compiler) + checkDiagnostics(new Map([[sourcePath, importable]])); // Print program to string - return definitionsHeader + printer.printFile(result.transformed[0]); + return { + ambient: definitionsHeader + source, + importable: definitionsHeader + importable, + }; } // Generates TypeScript types from a binary Cap’n Proto file containing encoded @@ -109,17 +167,31 @@ export async function main(args?: string[]) { const message = new Message(buffer, /* packed */ false); const root = message.getRoot(StructureGroups); - let definitions = printDefinitions(root); + const standards = await collateStandards( + path.join( + path.dirname(require.resolve("typescript")), + "lib.webworker.d.ts" + ), + path.join( + path.dirname(require.resolve("typescript")), + "lib.webworker.iterable.d.ts" + ) + ); + + let { ambient, importable } = printDefinitions(root, standards); + if (options.format) { - definitions = prettier.format(definitions, { parser: "typescript" }); + ambient = prettier.format(ambient, { parser: "typescript" }); + importable = prettier.format(importable, { parser: "typescript" }); } if (options.output !== undefined) { + console.log(options.output); const output = path.resolve(options.output); await mkdir(path.dirname(output), { recursive: true }); - await writeFile(output, definitions); - } else { - // Write to stdout without extra newline - process.stdout.write(definitions); + await writeFile(output, ambient); + + const importableFile = path.join(path.dirname(output), "api.ts"); + await writeFile(importableFile, importable); } } diff --git a/types/src/program.ts b/types/src/program.ts index 71d79306e34..226aac92ff4 100644 --- a/types/src/program.ts +++ b/types/src/program.ts @@ -1,3 +1,4 @@ +import { assert } from "console"; import path from "path"; import ts from "typescript"; @@ -6,11 +7,32 @@ interface MemorySourceFile { sourceFile: ts.SourceFile; } +// Update compiler host to return in-memory source file +function patchHostMethod( + host: ts.CompilerHost, + sourceFiles: Map, + key: K, + placeholderResult: (f: MemorySourceFile) => ReturnType +) { + const originalMethod: (...args: any[]) => any = host[key]; + host[key] = (fileName: string, ...args: any[]) => { + const sourceFile = sourceFiles.get(path.resolve(fileName)); + if (sourceFile !== undefined) { + return placeholderResult(sourceFile); + } + return originalMethod.call(host, fileName, ...args); + }; +} + // Creates a TypeScript program from in-memory source files. Accepts a Map of // fully-resolved "virtual" paths to source code. -export function createMemoryProgram(sources: Map): ts.Program { - const options = ts.getDefaultCompilerOptions(); - const host = ts.createCompilerHost(options, true); +export function createMemoryProgram( + sources: Map, + host?: ts.CompilerHost, + options?: ts.CompilerOptions +): ts.Program { + options ??= ts.getDefaultCompilerOptions(); + host ??= ts.createCompilerHost(options, true); const sourceFiles = new Map(); for (const [sourcePath, source] of sources) { @@ -24,25 +46,14 @@ export function createMemoryProgram(sources: Map): ts.Program { sourceFiles.set(sourcePath, { source, sourceFile }); } - // Update compiler host to return in-memory source file - function patchHostMethod< - K extends "fileExists" | "readFile" | "getSourceFile" - >( - key: K, - placeholderResult: (f: MemorySourceFile) => ReturnType - ) { - const originalMethod: (...args: any[]) => any = host[key]; - host[key] = (fileName: string, ...args: any[]) => { - const sourceFile = sourceFiles.get(path.resolve(fileName)); - if (sourceFile !== undefined) { - return placeholderResult(sourceFile); - } - return originalMethod.call(host, fileName, ...args); - }; - } - patchHostMethod("fileExists", () => true); - patchHostMethod("readFile", ({ source }) => source); - patchHostMethod("getSourceFile", ({ sourceFile }) => sourceFile); + patchHostMethod(host, sourceFiles, "fileExists", () => true); + patchHostMethod(host, sourceFiles, "readFile", ({ source }) => source); + patchHostMethod( + host, + sourceFiles, + "getSourceFile", + ({ sourceFile }) => sourceFile + ); const rootNames = [...sourceFiles.keys()]; return ts.createProgram(rootNames, options, host); diff --git a/types/src/standards.ts b/types/src/standards.ts new file mode 100644 index 00000000000..b74022decdb --- /dev/null +++ b/types/src/standards.ts @@ -0,0 +1,68 @@ +import { assert } from "console"; +import { readFile } from "fs/promises"; +import * as ts from "typescript"; +import { createMemoryProgram } from "./program"; +export interface ParsedTypeDefinition { + program: ts.Program; + source: ts.SourceFile; + checker: ts.TypeChecker; + parsed: { + functions: Map; + interfaces: Map; + vars: Map; + types: Map; + classes: Map; + }; +} + +// Collate standards (to support lib.(dom|webworker).iterable.d.ts being defined separately) +export async function collateStandards( + ...standardTypes: string[] +): Promise { + const STANDARDS_PATH = "/source.ts"; + const text: string = ( + await Promise.all( + standardTypes.map( + async (s) => + // Remove the Microsoft copyright notices from the file, to prevent them being copied in as TS comments + (await readFile(s, "utf-8")).split(`/////////////////////////////`)[2] + ) + ) + ).join("\n"); + const program = createMemoryProgram(new Map([[STANDARDS_PATH, text]])); + const source = program.getSourceFile(STANDARDS_PATH)!; + const checker = program.getTypeChecker(); + const parsed = { + functions: new Map(), + interfaces: new Map(), + vars: new Map(), + types: new Map(), + classes: new Map(), + }; + ts.forEachChild(source, (node) => { + let name = ""; + if (node && ts.isFunctionDeclaration(node)) { + name = node.name?.text ?? ""; + parsed.functions.set(name, node); + } else if (ts.isVariableStatement(node)) { + assert(node.declarationList.declarations.length === 1); + name = node.declarationList.declarations[0].name.getText(source); + parsed.vars.set(name, node.declarationList.declarations[0]); + } else if (ts.isInterfaceDeclaration(node)) { + name = node.name.text; + parsed.interfaces.set(name, node); + } else if (ts.isTypeAliasDeclaration(node)) { + name = node.name.text; + parsed.types.set(name, node); + } else if (ts.isClassDeclaration(node)) { + name = node.name?.text ?? ""; + parsed.classes.set(name, node); + } + }); + return { + program, + source, + checker, + parsed, + }; +} diff --git a/types/src/transforms/ambient.ts b/types/src/transforms/ambient.ts new file mode 100644 index 00000000000..efcae64b26f --- /dev/null +++ b/types/src/transforms/ambient.ts @@ -0,0 +1,18 @@ +import ts from "typescript"; +import { ensureStatementModifiers } from "./helpers"; + +// This ensures that all nodes have the `declare` keyword +export function createAmbientTransformer(): ts.TransformerFactory { + return (ctx) => { + return (node) => { + const visitor = createVisitor(ctx); + return ts.visitEachChild(node, visitor, ctx); + }; + }; +} + +function createVisitor(ctx: ts.TransformationContext) { + return (node: ts.Node) => { + return ensureStatementModifiers(ctx, node, false); + }; +} diff --git a/types/src/transforms/comments.ts b/types/src/transforms/comments.ts new file mode 100644 index 00000000000..a05270fd648 --- /dev/null +++ b/types/src/transforms/comments.ts @@ -0,0 +1,123 @@ +import assert from "assert"; +import ts from "typescript"; +import { ParsedTypeDefinition } from "../standards"; +import { hasModifier } from "./helpers"; + +function attachComments( + from: ts.Node, + to: ts.Node, + sourceFile: ts.SourceFile +): void { + const text = sourceFile.getFullText(); + const extractCommentText = (comment: ts.CommentRange) => + comment.kind === ts.SyntaxKind.MultiLineCommentTrivia + ? text.slice(comment.pos + 2, comment.end - 2) + : text.slice(comment.pos + 2, comment.end); + const leadingComments: string[] = + ts + .getLeadingCommentRanges(text, from.getFullStart()) + ?.map(extractCommentText) ?? []; + + leadingComments.map((c) => + ts.addSyntheticLeadingComment( + to, + ts.SyntaxKind.MultiLineCommentTrivia, + c, + true + ) + ); +} + +// Copy comments from a parsed standards TS program. It relies on the shape of lib.dom or lib.webworker +// Specifically, classes must be defined as a global var with the static properties, and an interface with the instance properties +export function createCommentsTransformer( + standards: ParsedTypeDefinition +): ts.TransformerFactory { + return (ctx) => { + return (node) => { + const visitor = createVisitor(standards); + return ts.visitEachChild(node, visitor, ctx); + }; + }; +} + +function createVisitor(standards: ParsedTypeDefinition) { + return (node: ts.Node) => { + if (ts.isClassDeclaration(node)) { + const name = node.name?.getText(); + assert(name !== undefined); + const standardsVersion = standards.parsed.vars.get(name); + if (standardsVersion) { + const type = standardsVersion.type; + assert( + type !== undefined && ts.isTypeLiteralNode(type), + `Non type literal found for "${name}"` + ); + attachComments(standardsVersion, node, standards.source); + const standardsInterface = standards.parsed.interfaces.get(name); + assert(standardsInterface !== undefined); + node.members.forEach((member) => { + const n = member.name?.getText(); + if (!n && ts.isConstructorDeclaration(member)) return; + + assert(n !== undefined, `No name for child of ${name}`); + if (member.modifiers === undefined) return; + + const isStatic = hasModifier( + member.modifiers, + ts.SyntaxKind.StaticKeyword + ); + const target = isStatic ? type : standardsInterface; + const standardsEquivalent = target.members.find( + (member) => member.name?.getText() === n + ); + + if (standardsEquivalent) + attachComments(standardsEquivalent, member, standards.source); + }); + } + } else if (ts.isFunctionDeclaration(node)) { + assert(node.name !== undefined); + const name = node.name.text; + const standardsVersion = standards.parsed.functions.get(name); + if (standardsVersion) { + attachComments(standardsVersion, node, standards.source); + } + } else if (ts.isInterfaceDeclaration(node)) { + assert(node.name !== undefined); + const name = node.name.text; + const standardsVersion = standards.parsed.interfaces.get(name); + if (standardsVersion) { + attachComments(standardsVersion, node, standards.source); + node.members.forEach((member) => { + const n = member.name?.getText(); + assert(n !== undefined); + const standardsEquivalent = standardsVersion.members.find( + (m) => m.name?.getText() === n + ); + if (standardsEquivalent) + attachComments(standardsEquivalent, member, standards.source); + }); + } + } else if (ts.isTypeAliasDeclaration(node)) { + assert(node.name !== undefined); + const name = node.name.text; + const standardsVersion = standards.parsed.types.get(name); + if (standardsVersion) { + attachComments(standardsVersion, node, standards.source); + } + } else if (ts.isVariableStatement(node)) { + // These contain comments that are misleading in the context of a Worker + const ignoreForComments = new Set(["self", "navigator", "caches"]); + assert(node.declarationList.declarations.length === 1); + assert(node.declarationList.declarations[0].name !== undefined); + const name = node.declarationList.declarations[0].name.getText(); + const standardsVersion = standards.parsed.vars.get(name); + if (standardsVersion && !ignoreForComments.has(name)) { + attachComments(standardsVersion, node, standards.source); + } + } + + return node; + }; +} diff --git a/types/src/transforms/helpers.ts b/types/src/transforms/helpers.ts new file mode 100644 index 00000000000..9136edb8145 --- /dev/null +++ b/types/src/transforms/helpers.ts @@ -0,0 +1,164 @@ +import assert from "assert"; +import * as ts from "typescript"; +import { printNode } from "../print"; +// Checks whether the modifiers array contains a modifier of the specified kind +export function hasModifier( + modifiers: ts.ModifiersArray, + kind: ts.Modifier["kind"] +) { + let hasModifier = false; + modifiers?.forEach((modifier) => { + hasModifier ||= modifier.kind === kind; + }); + return hasModifier; +} + +// Ensure a modifiers array has the specified modifier, inserting it at the +// start if it doesn't. +export function ensureModifier( + ctx: ts.TransformationContext, + modifiers: ts.ModifiersArray | undefined, + ensure: ts.SyntaxKind.ExportKeyword | ts.SyntaxKind.DeclareKeyword +): ts.ModifiersArray { + // If modifiers already contains the required modifier, return it as is... + if (modifiers !== undefined && hasModifier(modifiers, ensure)) { + return modifiers; + } + // ...otherwise, add the modifier to the start of the array + return ctx.factory.createNodeArray( + [ctx.factory.createToken(ensure), ...(modifiers ?? [])], + modifiers?.hasTrailingComma + ); +} + +// Ensure a modifiers array doesn't have the specified modifier +export function ensureNoModifier( + ctx: ts.TransformationContext, + modifiers: ts.ModifiersArray | undefined, + ensure: ts.SyntaxKind.ExportKeyword | ts.SyntaxKind.DeclareKeyword +): ts.ModifiersArray { + // If modifiers already doesn't contain the required modifier, return it as is... + if (modifiers !== undefined && !hasModifier(modifiers, ensure)) { + return modifiers; + } + // ...otherwise, remove the modifier + return ctx.factory.createNodeArray( + modifiers?.filter((m) => m.kind !== ensure), + modifiers?.hasTrailingComma + ); +} + +// Ensure a modifiers array includes the `export` modifier +export function ensureExportModifier( + ctx: ts.TransformationContext, + modifiers: ts.ModifiersArray | undefined, + exported = true +): ts.ModifiersArray { + return exported + ? ensureModifier(ctx, modifiers, ts.SyntaxKind.ExportKeyword) + : ensureNoModifier(ctx, modifiers, ts.SyntaxKind.ExportKeyword); +} + +// Ensures a modifiers array includes the `export declare` modifiers +export function ensureExportDeclareModifiers( + ctx: ts.TransformationContext, + modifiers: ts.ModifiersArray | undefined, + exported = true +): ts.ModifiersArray { + // Call in reverse, so we end up with `export declare` not `declare export` + modifiers = ensureModifier(ctx, modifiers, ts.SyntaxKind.DeclareKeyword); + return ensureExportModifier(ctx, modifiers, exported); +} + +// Make sure replacement node is `export`ed, with the `declare` modifier if it's +// a class, variable or function declaration. +// If the `noExport` option is set, only ensure `declare` modifiers +export function ensureStatementModifiers( + ctx: ts.TransformationContext, + node: ts.Node, + exported = true +): ts.Node { + if (ts.isClassDeclaration(node)) { + return ctx.factory.updateClassDeclaration( + node, + node.decorators, + ensureExportDeclareModifiers(ctx, node.modifiers, exported), + node.name, + node.typeParameters, + node.heritageClauses, + node.members + ); + } + if (ts.isInterfaceDeclaration(node)) { + return ctx.factory.updateInterfaceDeclaration( + node, + node.decorators, + ensureExportModifier( + ctx, + exported + ? ensureNoModifier(ctx, node.modifiers, ts.SyntaxKind.DeclareKeyword) + : ensureModifier(ctx, node.modifiers, ts.SyntaxKind.DeclareKeyword), + exported + ), + node.name, + node.typeParameters, + node.heritageClauses, + node.members + ); + } + if (ts.isEnumDeclaration(node)) { + return ctx.factory.updateEnumDeclaration( + node, + node.decorators, + ensureExportDeclareModifiers(ctx, node.modifiers, exported), + node.name, + node.members + ); + } + if (ts.isTypeAliasDeclaration(node)) { + return ctx.factory.updateTypeAliasDeclaration( + node, + node.decorators, + ensureExportModifier( + ctx, + exported + ? ensureNoModifier(ctx, node.modifiers, ts.SyntaxKind.DeclareKeyword) + : ensureModifier(ctx, node.modifiers, ts.SyntaxKind.DeclareKeyword), + exported + ), + node.name, + node.typeParameters, + node.type + ); + } + if (ts.isVariableStatement(node)) { + return ctx.factory.updateVariableStatement( + node, + ensureExportDeclareModifiers(ctx, node.modifiers, exported), + node.declarationList + ); + } + if (ts.isFunctionDeclaration(node)) { + return ctx.factory.updateFunctionDeclaration( + node, + node.decorators, + ensureExportDeclareModifiers(ctx, node.modifiers, exported), + node.asteriskToken, + node.name, + node.typeParameters, + node.parameters, + node.type, + node.body + ); + } + if (ts.isModuleDeclaration(node)) { + return ctx.factory.updateModuleDeclaration( + node, + node.decorators, + ensureExportDeclareModifiers(ctx, node.modifiers, exported), + node.name, + node.body + ); + } + assert.fail(`Expected statement, got "${printNode(node)}"`); +} diff --git a/types/src/transforms/importable.ts b/types/src/transforms/importable.ts new file mode 100644 index 00000000000..3e1768ffd3b --- /dev/null +++ b/types/src/transforms/importable.ts @@ -0,0 +1,18 @@ +import ts from "typescript"; +import { ensureStatementModifiers } from "./helpers"; + +// This ensures that all nodes have the `export` keyword (and, where relevant, `export declare`) +export function createImportableTransformer(): ts.TransformerFactory { + return (ctx) => { + return (node) => { + const visitor = createVisitor(ctx); + return ts.visitEachChild(node, visitor, ctx); + }; + }; +} + +function createVisitor(ctx: ts.TransformationContext) { + return (node: ts.Node) => { + return ensureStatementModifiers(ctx, node); + }; +} diff --git a/types/src/transforms/index.ts b/types/src/transforms/index.ts index a0be2533944..aab9691736a 100644 --- a/types/src/transforms/index.ts +++ b/types/src/transforms/index.ts @@ -1,3 +1,4 @@ export * from "./globals"; export * from "./iterators"; export * from "./overrides"; +export * from "./comments"; diff --git a/types/src/transforms/overrides/index.ts b/types/src/transforms/overrides/index.ts index 6c2ec629033..f5c81593aca 100644 --- a/types/src/transforms/overrides/index.ts +++ b/types/src/transforms/overrides/index.ts @@ -2,6 +2,12 @@ import assert from "assert"; import ts from "typescript"; import { isUnsatisfiable } from "../../generator/type"; import { printNode } from "../../print"; +import { + ensureExportDeclareModifiers, + ensureExportModifier, + ensureStatementModifiers, + hasModifier, +} from "../helpers"; import { maybeGetDefines, maybeGetOverride } from "./compiler"; export { compileOverridesDefines } from "./compiler"; @@ -427,127 +433,3 @@ function maybeGetStatementName(node: ts.Statement): ts.Identifier | undefined { return node.name; } } - -// Checks whether the modifiers array contains a modifier of the specified kind -function hasModifier(modifiers: ts.ModifiersArray, kind: ts.Modifier["kind"]) { - let hasModifier = false; - modifiers?.forEach((modifier) => { - hasModifier ||= modifier.kind === kind; - }); - return hasModifier; -} - -// Ensure a modifiers array has the specified modifier, inserting it at the -// start if it doesn't. -function ensureModifier( - ctx: ts.TransformationContext, - modifiers: ts.ModifiersArray | undefined, - ensure: ts.SyntaxKind.ExportKeyword | ts.SyntaxKind.DeclareKeyword -): ts.ModifiersArray { - // If modifiers already contains the required modifier, return it as is... - if (modifiers !== undefined && hasModifier(modifiers, ensure)) { - return modifiers; - } - // ...otherwise, add the modifier to the start of the array - return ctx.factory.createNodeArray( - [ctx.factory.createToken(ensure), ...(modifiers ?? [])], - modifiers?.hasTrailingComma - ); -} - -// Ensure a modifiers array includes the `export` modifier -function ensureExportModifier( - ctx: ts.TransformationContext, - modifiers: ts.ModifiersArray | undefined -): ts.ModifiersArray { - return ensureModifier(ctx, modifiers, ts.SyntaxKind.ExportKeyword); -} - -// Ensures a modifiers array includes the `export declare` modifiers -function ensureExportDeclareModifiers( - ctx: ts.TransformationContext, - modifiers: ts.ModifiersArray | undefined -): ts.ModifiersArray { - // Call in reverse, so we end up with `export declare` not `declare export` - modifiers = ensureModifier(ctx, modifiers, ts.SyntaxKind.DeclareKeyword); - return ensureModifier(ctx, modifiers, ts.SyntaxKind.ExportKeyword); -} - -// Make sure replacement node is `export`ed, with the `declare` modifier if it's -// a class, variable or function declaration. -function ensureStatementModifiers( - ctx: ts.TransformationContext, - node: ts.Node -): ts.Node { - if (ts.isClassDeclaration(node)) { - return ctx.factory.updateClassDeclaration( - node, - node.decorators, - ensureExportDeclareModifiers(ctx, node.modifiers), - node.name, - node.typeParameters, - node.heritageClauses, - node.members - ); - } - if (ts.isInterfaceDeclaration(node)) { - return ctx.factory.updateInterfaceDeclaration( - node, - node.decorators, - ensureExportModifier(ctx, node.modifiers), - node.name, - node.typeParameters, - node.heritageClauses, - node.members - ); - } - if (ts.isEnumDeclaration(node)) { - return ctx.factory.updateEnumDeclaration( - node, - node.decorators, - ensureExportDeclareModifiers(ctx, node.modifiers), - node.name, - node.members - ); - } - if (ts.isTypeAliasDeclaration(node)) { - return ctx.factory.updateTypeAliasDeclaration( - node, - node.decorators, - ensureExportModifier(ctx, node.modifiers), - node.name, - node.typeParameters, - node.type - ); - } - if (ts.isVariableStatement(node)) { - return ctx.factory.updateVariableStatement( - node, - ensureExportDeclareModifiers(ctx, node.modifiers), - node.declarationList - ); - } - if (ts.isFunctionDeclaration(node)) { - return ctx.factory.updateFunctionDeclaration( - node, - node.decorators, - ensureExportDeclareModifiers(ctx, node.modifiers), - node.asteriskToken, - node.name, - node.typeParameters, - node.parameters, - node.type, - node.body - ); - } - if (ts.isModuleDeclaration(node)) { - return ctx.factory.updateModuleDeclaration( - node, - node.decorators, - ensureExportDeclareModifiers(ctx, node.modifiers), - node.name, - node.body - ); - } - assert.fail(`Expected statement, got "${printNode(node)}"`); -} From 3d5045a179b5a3b5fc3e28e4bc4314ed924b88bb Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Fri, 11 Nov 2022 19:51:05 +0000 Subject: [PATCH 2/2] Fix tests --- types/test/index.spec.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/types/test/index.spec.ts b/types/test/index.spec.ts index 087df93bf3d..94279539fdf 100644 --- a/types/test/index.spec.ts +++ b/types/test/index.spec.ts @@ -119,23 +119,24 @@ test("main: generates types", async () => { output, `/* eslint-disable */ // noinspection JSUnusedGlobalSymbols -export declare class EventTarget = Record> { +declare class EventTarget = Record> { constructor(); addEventListener(type: Type, handler: (event: EventMap[Type]) => void): void; } -export type WorkerGlobalScopeEventMap = { +declare type WorkerGlobalScopeEventMap = { fetch: Event; scheduled: Event; }; -export declare abstract class WorkerGlobalScope extends EventTarget { +declare abstract class WorkerGlobalScope extends EventTarget { } -export interface ServiceWorkerGlobalScope extends WorkerGlobalScope { +/** This ServiceWorker API interface represents the global execution context of a service worker. */ +declare interface ServiceWorkerGlobalScope extends WorkerGlobalScope { things(param0: boolean): IterableIterator; get prop(): Promise; } -export declare function addEventListener(type: Type, handler: (event: WorkerGlobalScopeEventMap[Type]) => void): void; -export declare function things(param0: boolean): IterableIterator; -export declare const prop: Promise; +declare function addEventListener(type: Type, handler: (event: WorkerGlobalScopeEventMap[Type]) => void): void; +declare function things(param0: boolean): IterableIterator; +declare const prop: Promise; ` ); @@ -146,7 +147,7 @@ export declare const prop: Promise; output, `/* eslint-disable */ // noinspection JSUnusedGlobalSymbols -export declare class EventTarget< +declare class EventTarget< EventMap extends Record = Record > { constructor(); @@ -155,20 +156,22 @@ export declare class EventTarget< handler: (event: EventMap[Type]) => void ): void; } -export type WorkerGlobalScopeEventMap = { +declare type WorkerGlobalScopeEventMap = { fetch: Event; scheduled: Event; }; -export declare abstract class WorkerGlobalScope extends EventTarget {} -export interface ServiceWorkerGlobalScope extends WorkerGlobalScope { +declare abstract class WorkerGlobalScope extends EventTarget {} +/** This ServiceWorker API interface represents the global execution context of a service worker. */ +declare interface ServiceWorkerGlobalScope extends WorkerGlobalScope { things(param0: boolean): IterableIterator; get prop(): Promise; } -export declare function addEventListener< - Type extends keyof WorkerGlobalScopeEventMap ->(type: Type, handler: (event: WorkerGlobalScopeEventMap[Type]) => void): void; -export declare function things(param0: boolean): IterableIterator; -export declare const prop: Promise; +declare function addEventListener( + type: Type, + handler: (event: WorkerGlobalScopeEventMap[Type]) => void +): void; +declare function things(param0: boolean): IterableIterator; +declare const prop: Promise; ` ); });