import {
DefaultExportedRoute,
InvalidHtmlRoute,
KitaConfig,
ParameterParser,
Route,
RouteParser,
kControllerName,
kReplyParam,
kRequestParam
} from '@kitajs/common';
import path from 'path';
import { StringType, ts } from 'ts-json-schema-generator';
import { SuspenseIdParameterParser } from '../parameter-parsers/suspense-id';
import { SchemaBuilder } from '../schema/builder';
import { parseJsDocTags } from '../util/jsdoc';
import { getReturnType, isExportFunction } from '../util/nodes';
import { cwdRelative } from '../util/paths';
import { parseUrl } from '../util/string';
import { traverseParameters } from '../util/traverser';
export class HtmlRouteParser implements RouteParser {
constructor(
private config: KitaConfig,
private paramParser: ParameterParser,
private checker: ts.TypeChecker,
private builder: SchemaBuilder
) {}
supports(node: ts.Node): boolean {
if (!isExportFunction(node)) {
return false;
}
const sourceFile = node.getSourceFile();
if (!node.name || !sourceFile) {
return false;
}
// no .jsx is allowed
if (!sourceFile.fileName.endsWith('.tsx')) {
return false;
}
// all should not be allowed
if (!node.name.text.trim().match(/^get|post|put|delete$/i)) {
return false;
}
const defaultExport = node.modifiers.find((s) => s.kind === ts.SyntaxKind.DefaultKeyword);
if (defaultExport) {
throw new DefaultExportedRoute(defaultExport || node.name || node);
}
return true;
}
async parse(node: ts.FunctionDeclaration): Promise {
const source = node.getSourceFile();
const { url, routeId } = parseUrl(source.fileName, this.config);
const method = node.name!.getText();
const route: Route = {
kind: 'html',
url,
controllerMethod: method,
method: method.toUpperCase() as Uppercase,
relativePath: cwdRelative(path.relative(this.config.cwd, source.fileName)),
parameters: [],
schema: {
operationId: `${method.toLowerCase() + routeId}View`,
hide: true,
response: { [200 as number]: { type: 'string' } }
}
};
// Parses all jsdoc tags
parseJsDocTags(node, route);
const returnType = this.builder.createTypeSchema(getReturnType(node, this.checker));
const primitive = this.builder.toPrimitive(returnType, true);
// Non string return type
if (!primitive || !(primitive instanceof StringType)) {
throw new InvalidHtmlRoute(node.type || node, primitive);
}
// Adds all parameters in their respective position
for await (const { param, index } of traverseParameters(node, this.paramParser, route)) {
route.parameters[index] = param;
}
const routeParameters = route.parameters.map((r) => r.value).join(', ');
if (
// If the SuspenseId parameter was used, we need to render as a stream.
route.parameters.some((p) => p.name === SuspenseIdParameterParser.name)
) {
const boundCall = `${kControllerName}.${route.controllerMethod}.bind(undefined${
routeParameters ? `, ${routeParameters}` : ''
})`;
route.imports ??= [];
route.imports.push({
name: '{ renderToStream }',
path: '@kitajs/html/suspense'
});
route.customReturn = `return ${kReplyParam}.type('text/html; charset=utf-8').send(renderToStream(${boundCall}, ${kRequestParam}.id));`;
} else {
const handlerCall = `${kControllerName}.${route.controllerMethod}.call(undefined${
routeParameters ? `, ${routeParameters}` : ''
})`;
route.customReturn = `${kReplyParam}.type('text/html; charset=utf-8'); return ${handlerCall}`;
}
return route;
}
}