@typespec/http-server-js
Version:
TypeSpec HTTP server code generator for JavaScript
264 lines (223 loc) • 7.69 kB
text/typescript
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
import { Namespace, getNamespaceFullName } from "@typespec/compiler";
import {
DeclarationType,
JsContext,
Module,
ModuleBodyDeclaration,
createModule,
isModule,
} from "../ctx.js";
import { parseCase } from "../util/case.js";
import { UnimplementedError } from "../util/error.js";
import { cat, indent, isIterable } from "../util/iter.js";
import { OnceQueue } from "../util/once-queue.js";
import { emitOperationGroup } from "./interface.js";
/**
* Enqueue all declarations in the namespace to be included in the emit, recursively.
*
* @param ctx - The emitter context.
* @param namespace - The root namespace to begin traversing.
*/
export function visitAllTypes(ctx: JsContext, namespace: Namespace) {
const {
enums,
interfaces,
models,
unions,
namespaces,
scalars,
operations: _operations,
} = namespace;
for (const type of cat<DeclarationType>(
enums.values(),
interfaces.values(),
models.values(),
unions.values(),
scalars.values(),
)) {
if (!type.isFinished) continue;
ctx.typeQueue.add(type);
}
for (const ns of namespaces.values()) {
visitAllTypes(ctx, ns);
}
const operations = [..._operations.values()].filter((op) => op.isFinished);
if (operations.length > 0) {
// If the namespace has any floating operations in it, we will synthesize an interface for them in the parent module.
// This requires some special handling by other parts of the emitter to ensure that the interface for a namespace's
// own operations is properly imported.
if (!namespace.namespace) {
throw new UnimplementedError("no parent namespace in visitAllTypes");
}
const parentModule = createOrGetModuleForNamespace(ctx, namespace.namespace);
parentModule.declarations.push([
// prettier-ignore
`/** An interface representing the operations defined in the '${getNamespaceFullName(namespace)}' namespace. */`,
`export interface ${parseCase(namespace.name).pascalCase}<Context = unknown> {`,
...indent(emitOperationGroup(ctx, operations.values(), parentModule)),
"}",
]);
}
}
/**
* Create a module for a namespace, or get an existing module if one has already been created.
*
* @param ctx - The emitter context.
* @param namespace - The namespace to create a module for.
* @returns the module for the namespace.
*/
export function createOrGetModuleForNamespace(
ctx: JsContext,
namespace: Namespace,
root: Module = ctx.globalNamespaceModule,
): Module {
if (ctx.namespaceModules.has(namespace)) {
return ctx.namespaceModules.get(namespace)!;
}
if (!namespace.namespace) {
throw new Error("UNREACHABLE: no parent namespace in createOrGetModuleForNamespace");
}
const parent =
namespace.namespace === ctx.globalNamespace
? root
: createOrGetModuleForNamespace(ctx, namespace.namespace);
const name = namespace.name === "TypeSpec" ? "typespec" : parseCase(namespace.name).kebabCase;
const module: Module = createModule(name, parent, namespace);
ctx.namespaceModules.set(namespace, module);
return module;
}
/**
* Get a reference to the interface representing the namespace's floating operations.
*
* This does not check that such an interface actually exists, so it should only be called in situations where it is
* known to exist (for example, if an operation comes from the namespace).
*
* @param ctx - The emitter context.
* @param namespace - The namespace to get the interface reference for.
* @param module - The module the the reference will be written to.
*/
export function emitNamespaceInterfaceReference(
ctx: JsContext,
namespace: Namespace,
module: Module,
): string {
if (!namespace.namespace) {
throw new Error("UNREACHABLE: no parent namespace in emitNamespaceInterfaceReference");
}
const namespaceName = parseCase(namespace.name).pascalCase;
module.imports.push({
binder: [namespaceName],
from: createOrGetModuleForNamespace(ctx, namespace.namespace),
});
return namespaceName;
}
/**
* Emits a single declaration within a module. If the declaration is a module, it is enqueued for later processing.
*
* @param ctx - The emitter context.
* @param decl - The declaration to emit.
* @param queue - The queue to add the declaration to if it is a module.
*/
function* emitModuleBodyDeclaration(
ctx: JsContext,
decl: ModuleBodyDeclaration,
queue: OnceQueue<Module>,
): Iterable<string> {
if (isIterable(decl)) {
yield* decl;
} else if (typeof decl === "string") {
yield* decl.split(/\r?\n/);
} else {
if (decl.declarations.length > 0) {
queue.add(decl);
}
}
}
/**
* Gets a file path from a given module to another module.
*/
function computeRelativeFilePath(from: Module, to: Module): string {
const fromIsIndex = from.declarations.some((d) => isModule(d));
const toIsIndex = to.declarations.some((d) => isModule(d));
const relativePath = (fromIsIndex ? from.cursor : from.cursor.parent!).relativePath(to.cursor);
if (relativePath.length === 0 && !toIsIndex)
throw new Error("UNREACHABLE: relativePath returned no fragments");
if (relativePath.length === 0) return "./index.js";
const prefix = relativePath[0] === ".." ? "" : "./";
const suffix = toIsIndex ? "/index.js" : ".js";
return prefix + relativePath.join("/") + suffix;
}
/**
* Deduplicates, consolidates, and writes the import statements for a module.
*/
function* writeImportsNormalized(
ctx: JsContext,
module: Module,
queue: OnceQueue<Module>,
): Iterable<string> {
const allTargets = new Set<string>();
const importMap = new Map<string, Set<string>>();
const starAsMap = new Map<string, string>();
const extraStarAs: [string, string][] = [];
for (const _import of module.imports) {
// check for same module and continue
if (_import.from === module) continue;
let target: string;
if (typeof _import.from === "string") {
target = _import.from;
} else {
target = computeRelativeFilePath(module, _import.from);
queue.add(_import.from);
}
allTargets.add(target);
if (typeof _import.binder === "string") {
if (starAsMap.has(target)) {
extraStarAs.push([_import.binder, target]);
} else {
starAsMap.set(target, _import.binder);
}
} else {
const binders = importMap.get(target) ?? new Set<string>();
for (const binder of _import.binder) {
binders.add(binder);
}
importMap.set(target, binders);
}
}
for (const target of allTargets) {
const binders = importMap.get(target);
const starAs = starAsMap.get(target);
if (binders && starAs) {
yield `import ${starAs}, { ${[...binders].join(", ")} } from "${target}";`;
} else if (binders) {
yield `import { ${[...binders].join(", ")} } from "${target}";`;
} else if (starAs) {
yield `import ${starAs} from "${target}";`;
}
yield "";
}
for (const [binder, target] of extraStarAs) {
yield `import ${binder} from "${target}";`;
}
}
/**
* Emits the body of a module file.
*
* @param ctx - The emitter context.
* @param module - The module to emit.
* @param queue - The queue to add any submodules to for later processing.
*/
export function* emitModuleBody(
ctx: JsContext,
module: Module,
queue: OnceQueue<Module>,
): Iterable<string> {
yield* writeImportsNormalized(ctx, module, queue);
if (module.imports.length > 0) yield "";
for (const decl of module.declarations) {
yield* emitModuleBodyDeclaration(ctx, decl, queue);
yield "";
}
}