UNPKG

@typespec/http-server-js

Version:

TypeSpec HTTP server code generator for JavaScript

530 lines (445 loc) 14.4 kB
// Copyright (c) Microsoft Corporation // Licensed under the MIT license. import { compilerAssert, Enum, Interface, isArrayModelType, isRecordModelType, listServices, Model, Namespace, NoTarget, Program, Scalar, Service, Type, Union, UnionVariant, } from "@typespec/compiler"; import { emitDeclaration } from "./common/declaration.js"; import { createOrGetModuleForNamespace } from "./common/namespace.js"; import { SerializableType } from "./common/serialization/index.js"; import { emitUnion } from "./common/union.js"; import { JsEmitterOptions, reportDiagnostic } from "./lib.js"; import { parseCase } from "./util/case.js"; import { UnimplementedError } from "./util/error.js"; import { createOnceQueue, OnceQueue } from "./util/once-queue.js"; import { unsafe_Mutator } from "@typespec/compiler/experimental"; import { MetadataInfo } from "@typespec/http"; import { createModule as initializeHelperModule } from "../generated-defs/helpers/index.js"; export type DeclarationType = Model | Enum | Union | Interface | Scalar; /** * Determines whether or not a type is importable into a JavaScript module. * * i.e. whether or not it is declared as a named symbol within the module. * * In TypeScript, unions are rendered inline, so they are not ordinarily * considered importable. * * @param ctx - The JS emitter context. * @param t - the type to test * @returns `true` if the type is an importable declaration, `false` otherwise. */ export function isImportableType(ctx: JsContext, t: Type): t is DeclarationType { return ( (t.kind === "Model" && !isArrayModelType(ctx.program, t) && !isRecordModelType(ctx.program, t)) || t.kind === "Enum" || t.kind === "Interface" ); } /** * Stores stateful information consumed and modified by the JavaScript server * emitter. */ export interface JsContext { /** * The TypeSpec Program that this emitter instance operates over. */ program: Program; /** * The emitter options. */ options: JsEmitterOptions; /** * The global (root) namespace of the program. */ globalNamespace: Namespace; /** * The service definition to use for emit. */ service: Service; /** * A queue of all types to be included in the emit tree. This queue * automatically deduplicates types, so if a type is added multiple times it * will only be visited once. */ typeQueue: OnceQueue<DeclarationType>; /** * A list of synthetic types (anonymous types that are given names) that are * included in the emit tree. */ synthetics: Synthetic[]; /** * A cache of names given to synthetic types. These names may be used to avoid * emitting the same synthetic type multiple times. */ syntheticNames: Map<DeclarationType, string>; /** * The root module for the emit tree. */ rootModule: Module; /** * The parent of the generated module. */ srcModule: Module; /** * The module that contains all generated code. */ generatedModule: Module; /** * A map relating each namespace to the module that contains its declarations. * * @see createOrGetModuleForNamespace */ namespaceModules: Map<Namespace, Module>; /** * The module that contains all synthetic types. */ syntheticModule: Module; /** * The root module for all named declarations of types referenced by the program. */ modelsModule: Module; /** * The module within `models` that maps to the global namespace. */ globalNamespaceModule: Module; /** * A map of all types that require serialization code to the formats they require. */ serializations: OnceQueue<SerializableType>; gensym: (name: string) => string; metadataInfo?: MetadataInfo; canonicalizationCache: { [vfKey: string]: unsafe_Mutator | undefined }; } export async function createInitialContext( program: Program, options: JsEmitterOptions, ): Promise<JsContext | undefined> { const services = listServices(program); if (services.length === 0) { reportDiagnostic(program, { code: "no-services-in-program", target: NoTarget, messageId: "default", }); return undefined; } else if (services.length > 1) { throw new UnimplementedError("multiple service definitions per program."); } const [service] = services; const serviceModuleName = parseCase(service.type.name).snakeCase; const rootCursor = createPathCursor(); const globalNamespace = program.getGlobalNamespaceType(); // Root module for emit. const rootModule: Module = { name: serviceModuleName, cursor: rootCursor, imports: [], declarations: [], }; const srcModule = createModule("src", rootModule); const generatedModule = createModule("generated", srcModule); // This dummy module hosts the root of the helper tree. It's not a "real" module // because we want the modules to be emitted lazily, but we need some module to // pass in to the root helper index. const dummyModule: Module = { name: "generated", cursor: generatedModule.cursor, imports: [], declarations: [], }; // This has the side effect of setting the `module` property of all helpers. // Don't do anything with the emitter code before this is called. await initializeHelperModule(dummyModule); // Module for all models, including synthetic and all. const modelsModule: Module = createModule("models", generatedModule); // Module for all types in all namespaces. const allModule: Module = createModule("all", modelsModule, globalNamespace); // Module for all synthetic (named ad-hoc) types. const syntheticModule: Module = createModule("synthetic", modelsModule); const jsCtx: JsContext = { program, options, globalNamespace, service, typeQueue: createOnceQueue(), synthetics: [], syntheticNames: new Map(), rootModule, srcModule, generatedModule, namespaceModules: new Map([[globalNamespace, allModule]]), syntheticModule, modelsModule, globalNamespaceModule: allModule, serializations: createOnceQueue(), gensym: (name) => { return gensym(jsCtx, name); }, canonicalizationCache: {}, }; return jsCtx; } /** * A synthetic type that is not directly represented with a name in the TypeSpec program. */ export type Synthetic = AnonymousSynthetic | PartialUnionSynthetic; /** * An ordinary, anonymous type that is given a name. */ export interface AnonymousSynthetic { kind: "anonymous"; name: string; underlying: DeclarationType; } /** * A partial union with a name for the given variants. */ export interface PartialUnionSynthetic { kind: "partialUnion"; name: string; variants: UnionVariant[]; } /** * Adds all pending declarations from the type queue to the module tree. * * The JavaScript emitter is lazy, and sometimes emitter components may visit * types that are not yet declared. This function ensures that all types * reachable from existing declarations are complete. * * @param ctx - The JavaScript emitter context. */ export function completePendingDeclarations(ctx: JsContext): void { // Add all pending declarations to the module tree. while (!ctx.typeQueue.isEmpty() || ctx.synthetics.length > 0) { while (!ctx.typeQueue.isEmpty()) { const type = ctx.typeQueue.take()!; compilerAssert(type.namespace !== undefined, "no namespace for declaration type", type); const module = createOrGetModuleForNamespace(ctx, type.namespace); module.declarations.push([...emitDeclaration(ctx, type, module)]); } while (ctx.synthetics.length > 0) { const synthetic = ctx.synthetics.shift()!; switch (synthetic.kind) { case "anonymous": { ctx.syntheticModule.declarations.push([ ...emitDeclaration(ctx, synthetic.underlying, ctx.syntheticModule, synthetic.name), ]); break; } case "partialUnion": { ctx.syntheticModule.declarations.push([ ...emitUnion(ctx, synthetic, ctx.syntheticModule, synthetic.name), ]); break; } } } } } // #region Module /** * A declaration within a module. This may be a string (i.e. a line), an array of * strings (emitted as multiple lines), or another module (emitted as a nested module). */ export type ModuleBodyDeclaration = string[] | string | Module; /** * A type-guard that checks whether or not a given value is a module. * @returns `true` if the value is a module, `false` otherwise. */ export function isModule(value: unknown): value is Module { return ( typeof value === "object" && value !== null && "declarations" in value && Array.isArray(value.declarations) ); } /** * Creates a new module with the given name and attaches it to the parent module. * * Optionally, a namespace may be associated with the module. This namespace is * _NOT_ stored in the context (this function does not use the JsContext), and * is only stored as metadata within the module. To associate a module with a * namespace inside the context, use `createOrGetModuleForNamespace`. * * The module is automatically declared as a declaration within its parent * module. * * @param name - The name of the module. * @param parent - The parent module to attach the new module to. * @param namespace - an optional TypeSpec Namespace to associate with the module * @returns the newly created module */ export function createModule(name: string, parent: Module, namespace?: Namespace): Module { const self = { name, cursor: parent.cursor.enter(name), namespace, imports: [], declarations: [], }; parent.declarations.push(self); return self; } /** * The type of a binding for an import statement. Either: * * - A string beginning with `* as` followed by the name of the binding, which * imports all exports from the module as a single object. * - A binding name, which imports the default export of the module. * - An array of strings, each of which is a named import from the module. */ export type ImportBinder = string | string[]; /** * An object representing a ECMAScript module import declaration. */ export interface Import { /** * The binder to define the import as. */ binder: ImportBinder; /** * Where to import from. This is either a literal string (which will be used verbatim), or Module object, which will * be resolved to a relative file path. */ from: Module | string; } /** * A module that does not exist and is not emitted. Use this for functions that require a module but you only * want to analyze the type and not emit any relative paths. * * For example, this is used internally to canonicalize operation types, because it calls some functions that * require a module, but canonicalizing the operation does not itself emit any code. */ export const NoModule: Module = { name: "", cursor: createPathCursor(), imports: [], declarations: [], }; /** * An output module within the module tree. */ export interface Module { /** * The name of the module, which should be suitable for use as the basename of * a file and as an identifier. */ name: string; /** * The cursor for the module, which assists navigation and relative path * computation between modules. */ readonly cursor: PathCursor; /** * An optional namespace for the module. This is not used by the code writer, * but is used to track dependencies between TypeSpec namespaces and create * imports between them. */ namespace?: Namespace; /** * A list of imports that the module requires. */ imports: Import[]; /** * A list of declarations within the module. */ declarations: ModuleBodyDeclaration[]; } // #endregion /** * A cursor that assists in navigating the module tree and computing relative * paths between modules. */ export interface PathCursor { /** * The path to this cursor. This is an array of strings that represents the * path from the root module to another module. */ readonly path: string[]; /** * The parent cursor of this cursor (equivalent to moving up one level in the * module tree). If this cursor is the root cursor, this property is `undefined`. */ readonly parent: PathCursor | undefined; /** * Returns a new cursor that includes the given path components appended to * this cursor's path. * * @param path - the path to append to this cursor */ enter(...path: string[]): PathCursor; /** * Computes a relative path from this cursor to another cursor, using the string `up` * to navigate upwards one level in the path. This is similar to `path.relative` when * working with file paths, but operates over PathCursor objects. * * @param to - the cursor to compute the path to * @param up - the string to use to move up a level in the path (defaults to "..") */ relativePath(to: PathCursor, up?: string): string[]; } /** * Create a new cursor with the given path. * * @param base - the base path of this cursor * @returns */ export function createPathCursor(...base: string[]): PathCursor { const self: PathCursor = { path: base, get parent() { return self.path.length === 0 ? undefined : createPathCursor(...self.path.slice(0, -1)); }, enter(...path: string[]) { return createPathCursor(...self.path, ...path); }, relativePath(to: PathCursor, up: string = ".."): string[] { const commonPrefix = getCommonPrefix(self.path, to.path); const outputPath = []; for (let i = 0; i < self.path.length - commonPrefix.length; i++) { outputPath.push(up); } outputPath.push(...to.path.slice(commonPrefix.length)); return outputPath; }, }; return self; } /** * Compute the common prefix of two paths. */ function getCommonPrefix(a: string[], b: string[]): string[] { const prefix = []; for (let i = 0; i < Math.min(a.length, b.length); i++) { if (a[i] !== b[i]) { break; } prefix.push(a[i]); } return prefix; } const SYM_TAB = new WeakMap<Program, { idx: number }>(); export function gensym(ctx: JsContext, name: string): string { let symTab = SYM_TAB.get(ctx.program); if (symTab === undefined) { symTab = { idx: 0 }; SYM_TAB.set(ctx.program, symTab); } return `__${name}_${symTab.idx++}`; }