UNPKG

@typespec/http-server-js

Version:

TypeSpec HTTP server code generator for JavaScript

234 lines 8.56 kB
// Copyright (c) Microsoft Corporation // Licensed under the MIT license. import { compilerAssert, isArrayModelType, isRecordModelType, listServices, NoTarget, } from "@typespec/compiler"; import { emitDeclaration } from "./common/declaration.js"; import { createOrGetModuleForNamespace } from "./common/namespace.js"; import { emitUnion } from "./common/union.js"; import { reportDiagnostic } from "./lib.js"; import { parseCase } from "./util/case.js"; import { UnimplementedError } from "./util/error.js"; import { createOnceQueue } from "./util/once-queue.js"; import { createModule as initializeHelperModule } from "../generated-defs/helpers/index.js"; /** * 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, t) { return ((t.kind === "Model" && !isArrayModelType(ctx.program, t) && !isRecordModelType(ctx.program, t)) || t.kind === "Enum" || t.kind === "Interface"); } export async function createInitialContext(program, options) { 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 = { 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 = { 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 = createModule("models", generatedModule); // Module for all types in all namespaces. const allModule = createModule("all", modelsModule, globalNamespace); // Module for all synthetic (named ad-hoc) types. const syntheticModule = createModule("synthetic", modelsModule); const jsCtx = { 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; } /** * 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) { // 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; } } } } } /** * 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) { 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, parent, namespace) { const self = { name, cursor: parent.cursor.enter(name), namespace, imports: [], declarations: [], }; parent.declarations.push(self); return self; } /** * 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 = { name: "", cursor: createPathCursor(), imports: [], declarations: [], }; /** * Create a new cursor with the given path. * * @param base - the base path of this cursor * @returns */ export function createPathCursor(...base) { const self = { path: base, get parent() { return self.path.length === 0 ? undefined : createPathCursor(...self.path.slice(0, -1)); }, enter(...path) { return createPathCursor(...self.path, ...path); }, relativePath(to, up = "..") { 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, b) { 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(); export function gensym(ctx, name) { let symTab = SYM_TAB.get(ctx.program); if (symTab === undefined) { symTab = { idx: 0 }; SYM_TAB.set(ctx.program, symTab); } return `__${name}_${symTab.idx++}`; } //# sourceMappingURL=ctx.js.map