@typespec/http-server-js
Version:
TypeSpec HTTP server code generator for JavaScript
234 lines • 8.56 kB
JavaScript
// 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