@tsed/cli
Version:
CLI to bootstrap your Ts.ED project
177 lines (176 loc) • 6.45 kB
JavaScript
import { dirname, join } from "node:path";
import { CliDockerComposeYaml, CliFs, ProjectPackageJson } from "@tsed/cli-core";
import { isString } from "@tsed/core";
import { constant, inject } from "@tsed/di";
import { ObjectLiteralExpression, Project, SyntaxKind } from "ts-morph";
import { OutputFilePathPipe } from "../pipes/OutputFilePathPipe.js";
export class ProjectClient extends Project {
constructor({ rootDir, ...options }) {
super({
fileSystem: inject(CliFs),
...options
});
this.pkg = inject(ProjectPackageJson);
this.fs = inject(CliFs);
this.dockerCompose = inject(CliDockerComposeYaml);
this.rootDir = rootDir;
}
get srcDir() {
return constant("project.srcDir", "/src");
}
get serverSourceFile() {
return this.getSource(join(this.srcDir, this.serverName));
}
get configSourceFile() {
return this.getSource(join(this.srcDir, `config/config.ts`));
}
get indexSourceFile() {
return this.getSource(join(this.srcDir, "index.ts"));
}
get binSourceFile() {
return this.getSource(join(this.srcDir, "bin/index.ts"));
}
get serverName() {
return inject(OutputFilePathPipe).getServerName();
}
getSource(path) {
const resolved = join(this.rootDir, path.replace("{{srcDir}}", this.srcDir));
return this.getSourceFile(resolved);
}
async createSource(path, sourceFileText, options) {
path = join(this.rootDir, path);
if (path.endsWith(".ts") || path.endsWith(".mts")) {
await this.fs.mkdir(dirname(path));
if (typeof sourceFileText === "string") {
await this.fs.writeFile(path, sourceFileText, { encoding: "utf-8" });
return this.getSourceFile(path) || this.addSourceFileAtPath(path);
}
const source = this.createSourceFile(path, sourceFileText, options);
await this.fs.writeFile(path, source.getFullText(), { encoding: "utf-8" });
return source;
}
await this.fs.ensureDir(dirname(path));
await this.fs.writeFile(path, sourceFileText, { encoding: "utf-8" });
}
findClassDecorator(sourceFile, name) {
return sourceFile
.getClasses()
.flatMap((cls) => cls.getDecorators())
.find((decorator) => decorator.getName() === name);
}
addMountPath(path, specifier) {
if (!this.serverSourceFile) {
return;
}
const options = this.findConfigurationDecorationOptions();
if (!options) {
return;
}
let mount = this.getPropertyAssignment(options, {
name: "mount",
kind: SyntaxKind.ObjectLiteralExpression,
initializer: "{}"
});
const escape = '"' + ((path || "").startsWith("/") ? path : "/" + path) + '"';
const property = this.getPropertyAssignment(mount, {
name: escape,
kind: SyntaxKind.ArrayLiteralExpression,
initializer: "[]"
});
if (isString(specifier)) {
property.addElement(specifier);
}
options.formatText({
indentSize: 2
});
}
addNamespaceImport(sourceFile, moduleSpecifier, name) {
return sourceFile
.addImportDeclaration({
moduleSpecifier,
namespaceImport: name
})
.getNamespaceImport();
}
findConfiguration(kind) {
switch (kind) {
case "server":
return this.findConfigurationDecorationOptions();
case "config":
return this.findConfigConfiguration();
case "bin":
return this.findBinConfiguration();
default:
throw new Error(`Unknown configuration kind: ${kind}`);
}
}
getPropertyAssignment(input, { name, initializer, kind }) {
const assigment = input.getProperty(name)?.getChildAtIndex(2).asKind(kind);
if (!initializer) {
return assigment;
}
return (assigment ||
input
.addPropertyAssignment({
name,
initializer
})
.getInitializerIfKindOrThrow(kind));
}
addConfigSource(name, { content = name, moduleSpecifier }) {
const sourceFile = this.configSourceFile;
const options = this.findConfiguration("config");
if (!options) {
return;
}
const extendsConfig = this.getPropertyAssignment(options, {
name: "extends",
kind: SyntaxKind.ArrayLiteralExpression,
initializer: "[]"
});
const has = extendsConfig.getElements().some((expression) => {
return expression.getText().includes(name);
});
if (!has) {
sourceFile.addImportDeclaration({
moduleSpecifier,
namedImports: [{ name: name }]
});
extendsConfig.addElement("\n" + content);
}
if (content.match("withOptions")) {
if (!sourceFile.getImportDeclarations().some((imp) => {
return imp.getText().match("withOptions");
})) {
sourceFile.addImportDeclaration({
moduleSpecifier: "@tsed/config",
namedImports: [{ name: "withOptions" }]
});
}
}
}
findConfigurationDecorationOptions() {
if (!this.serverSourceFile) {
return;
}
return this.findClassDecorator(this.serverSourceFile, "Configuration")?.getArguments().at(0);
}
findConfigConfiguration() {
const sourceFile = this.configSourceFile;
if (!sourceFile) {
return;
}
return sourceFile.getVariableDeclaration("config")?.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
}
findBinConfiguration() {
if (!this.binSourceFile) {
return;
}
for (const child of this.binSourceFile.getStatements()) {
if (child.getKind() === SyntaxKind.ExpressionStatement && child.getText().includes("CliCore.bootstrap")) {
return child.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
}
}
return undefined;
}
}