@tsed/cli
Version:
CLI to bootstrap your Ts.ED project
401 lines (400 loc) • 16.8 kB
JavaScript
import { __decorate } from "tslib";
import { basename, join } from "node:path";
import { CliExeca, CliFs, CliLoadFile, cliPackageJson, CliPlugins, CliService, Command, Configuration, createSubTasks, createTasksRunner, inject, PackageManager, PackageManagersModule, ProjectPackageJson } from "@tsed/cli-core";
import { kebabCase, pascalCase } from "change-case";
import { DEFAULT_TSED_TAGS } from "../../constants/index.js";
import { ArchitectureConvention } from "../../interfaces/ArchitectureConvention.js";
import { PlatformType } from "../../interfaces/index.js";
import { ProjectConvention } from "../../interfaces/ProjectConvention.js";
import { OutputFilePathPipe } from "../../pipes/OutputFilePathPipe.js";
import { InitPlatformsModule } from "../../platforms/InitPlatformsModule.js";
import { RuntimesModule } from "../../runtimes/RuntimesModule.js";
import { BunRuntime } from "../../runtimes/supports/BunRuntime.js";
import { NodeRuntime } from "../../runtimes/supports/NodeRuntime.js";
import { RootRendererService } from "../../services/Renderer.js";
import { fillImports } from "../../utils/fillImports.js";
import { FeaturesMap, FeatureType } from "./config/FeaturesPrompt.js";
import { InitFileSchema } from "./config/InitFileSchema.js";
import { mapToContext } from "./mappers/mapToContext.js";
import { getFeaturesPrompt } from "./prompts/getFeaturesPrompt.js";
let InitCmd = class InitCmd {
constructor() {
this.configuration = inject(Configuration);
this.cliPlugins = inject(CliPlugins);
this.packageJson = inject(ProjectPackageJson);
this.packageManagers = inject(PackageManagersModule);
this.runtimes = inject(RuntimesModule);
this.platforms = inject(InitPlatformsModule);
this.cliPackageJson = cliPackageJson();
this.cliService = inject(CliService);
this.cliLoadFile = inject(CliLoadFile);
this.rootRenderer = inject(RootRendererService);
this.outputFilePathPipe = inject(OutputFilePathPipe);
this.execa = inject(CliExeca);
this.fs = inject(CliFs);
}
checkPrecondition(ctx) {
const isValid = (types, value) => (value ? Object.values(types).includes(value) : true);
if (!isValid(PlatformType, ctx.platform)) {
throw new Error(`Invalid selected platform: ${ctx.platform}. Possible values: ${Object.values(PlatformType).join(", ")}.`);
}
if (!isValid(ArchitectureConvention, ctx.architecture)) {
throw new Error(`Invalid selected architecture: ${ctx.architecture}. Possible values: ${Object.values(ArchitectureConvention).join(", ")}.`);
}
if (!isValid(ProjectConvention, ctx.convention)) {
throw new Error(`Invalid selected convention: ${ctx.convention}. Possible values: ${Object.values(ProjectConvention).join(", ")}.`);
}
const runtimes = this.runtimes.list();
if (!runtimes.includes(ctx.runtime)) {
throw new Error(`Invalid selected runtime: ${ctx.runtime}. Possible values: ${runtimes.join(", ")}.`);
}
const managers = this.packageManagers.list();
if (!managers.includes(ctx.packageManager)) {
throw new Error(`Invalid selected package manager: ${ctx.packageManager}. Possible values: ${managers.join(", ")}.`);
}
if (ctx.features) {
ctx.features.forEach((value) => {
const feature = FeaturesMap[value.toLowerCase()];
if (!feature) {
throw new Error(`Invalid selected feature: ${value}. Possible values: ${Object.values(FeatureType).join(", ")}.`);
}
});
}
}
async $beforePrompt(initialOptions) {
if (initialOptions.file) {
const file = join(this.packageJson.dir, initialOptions.file);
return {
...initialOptions,
...(await this.cliLoadFile.loadFile(file, InitFileSchema))
};
}
return initialOptions;
}
$prompt(initialOptions) {
if (initialOptions.skipPrompt) {
return [];
}
const packageManagers = this.packageManagers.list();
const runtimes = this.runtimes.list();
return [
{
type: "input",
name: "projectName",
message: "What is your project name",
default: kebabCase(initialOptions.root),
when: initialOptions.root !== ".",
transformer(input) {
return kebabCase(input);
}
},
...getFeaturesPrompt(runtimes, packageManagers.filter((o) => o !== "bun"), initialOptions)
];
}
$mapContext(ctx) {
this.resolveRootDir(ctx);
ctx = mapToContext(ctx);
this.runtimes.init(ctx);
this.runtimes.list().forEach((key) => {
ctx[key] = ctx.runtime === key;
});
this.packageManagers.list().forEach((key) => {
ctx[key] = ctx.packageManager === key;
});
return fillImports({
...ctx,
entryServer: ctx.convention !== ProjectConvention.ANGULAR ? "Server" : "server",
cliVersion: ctx.cliVersion || this.cliPackageJson.version,
srcDir: this.configuration.project?.srcDir,
platformSymbol: ctx.platform && pascalCase(`Platform ${ctx.platform}`)
});
}
async $beforeExec(ctx) {
this.fs.ensureDirSync(this.packageJson.dir);
this.packageJson.name = ctx.projectName;
ctx.packageManager && this.packageJson.setPreference("packageManager", ctx.packageManager);
ctx.runtime && this.packageJson.setPreference("runtime", ctx.runtime);
ctx.architecture && this.packageJson.setPreference("architecture", ctx.architecture);
ctx.convention && this.packageJson.setPreference("convention", ctx.convention);
ctx.GH_TOKEN && this.packageJson.setGhToken(ctx.GH_TOKEN);
await createTasksRunner([
{
title: "Write RC files",
skip: () => !ctx.GH_TOKEN,
task: () => this.rootRenderer.renderAll(["/init/.npmrc.hbs", "/init/.yarnrc.hbs"], ctx, {
baseDir: "/init"
})
},
{
title: "Initialize package.json",
task: async () => {
await this.packageManagers.init(ctx);
this.addScripts(ctx);
this.addDependencies(ctx);
this.addDevDependencies(ctx);
this.addFeatures(ctx);
}
},
{
title: "Install plugins",
task: createSubTasks(() => this.packageManagers.install(ctx), { ...ctx, concurrent: false })
},
{
title: "Load plugins",
task: () => this.cliPlugins.loadPlugins()
},
{
title: "Install plugins dependencies",
task: createSubTasks(() => this.cliPlugins.addPluginsDependencies(ctx), { ...ctx, concurrent: false })
}
], ctx);
}
async $exec(ctx) {
this.checkPrecondition(ctx);
const subTasks = [
...(await this.cliService.getTasks("generate", {
...ctx,
type: "server",
name: "Server",
route: "/rest"
})),
...(await this.cliService.getTasks("generate", {
type: "controller",
route: "hello-world",
name: "HelloWorld",
directory: "rest"
})),
...(ctx.commands
? await this.cliService.getTasks("generate", {
type: "command",
route: "hello",
name: "hello"
})
: [])
];
return [
{
title: "Generate project files",
task: createSubTasks([
{
title: "Root files",
task: () => {
return this.generateFiles(ctx);
}
},
...subTasks
], { ...ctx, concurrent: false })
}
];
}
$afterPostInstall() {
return [
{
title: "Generate barrels files",
task: () => {
return this.packageManagers.runScript("barrels", {
ignoreError: true
});
}
}
];
}
resolveRootDir(ctx) {
const rootDirName = kebabCase(ctx.projectName || basename(this.packageJson.dir));
if (this.packageJson.dir.endsWith(rootDirName)) {
ctx.projectName = ctx.projectName || rootDirName;
ctx.root = ".";
return;
}
ctx.projectName = ctx.projectName || rootDirName;
if (ctx.root && ctx.root !== ".") {
this.packageJson.dir = join(this.packageJson.dir, rootDirName);
ctx.root = ".";
}
}
addScripts(ctx) {
this.packageJson.addScripts(this.runtimes.scripts(ctx));
if (ctx.eslint || ctx.testing) {
const runtime = this.runtimes.get();
const scripts = {
test: [ctx.eslint && runtime.run("test:lint"), ctx.testing && runtime.run("test:coverage")].filter(Boolean).join("&&")
};
this.packageJson.addScripts(scripts);
}
}
addDependencies(ctx) {
this.packageJson.addDependencies({
"@tsed/core": ctx.tsedVersion,
"@tsed/di": ctx.tsedVersion,
"@tsed/ajv": ctx.tsedVersion,
"@tsed/exceptions": ctx.tsedVersion,
"@tsed/schema": ctx.tsedVersion,
"@tsed/json-mapper": ctx.tsedVersion,
"@tsed/openspec": ctx.tsedVersion,
"@tsed/platform-http": ctx.tsedVersion,
"@tsed/platform-cache": ctx.tsedVersion,
"@tsed/platform-exceptions": ctx.tsedVersion,
"@tsed/platform-log-request": ctx.tsedVersion,
"@tsed/platform-middlewares": ctx.tsedVersion,
"@tsed/platform-params": ctx.tsedVersion,
"@tsed/platform-response-filter": ctx.tsedVersion,
"@tsed/platform-views": ctx.tsedVersion,
"@tsed/platform-multer": ctx.tsedVersion,
"@tsed/logger": "latest",
"@tsed/engines": "latest",
"@tsed/barrels": "latest",
ajv: "latest",
"cross-env": "latest",
dotenv: "latest",
"dotenv-expand": "latest",
"dotenv-flow": "latest",
...this.runtimes.get().dependencies(),
...this.platforms.get(ctx.platform).dependencies(ctx)
});
}
addDevDependencies(ctx) {
this.packageJson.addDevDependencies({
"@types/node": "latest",
"@types/multer": "latest",
tslib: "latest",
...this.runtimes.get().devDependencies(),
...this.platforms.get(ctx.platform).devDependencies(ctx)
}, ctx);
}
addFeatures(ctx) {
ctx.features.forEach((value) => {
const feature = FeaturesMap[value.toLowerCase()];
if (feature) {
if (feature.dependencies) {
this.packageJson.addDependencies(feature.dependencies, ctx);
}
if (feature.devDependencies) {
this.packageJson.addDevDependencies(feature.devDependencies, ctx);
}
}
});
if (ctx.features.find((value) => value === FeatureType.GRAPHQL)) {
this.packageJson.addDependencies({
[`apollo-server-${ctx.platform}`]: "2.25.2"
}, ctx);
}
}
generateFiles(ctx) {
const indexCtrlBaseName = basename(`${this.outputFilePathPipe.transform({
name: "Index",
type: "controller",
format: ctx.convention
})}.ts`);
const runtime = this.runtimes.get();
const packageManager = this.packageManagers.get();
ctx = {
...ctx,
node: runtime instanceof NodeRuntime,
bun: runtime instanceof BunRuntime,
compiled: runtime instanceof NodeRuntime && runtime.isCompiled()
};
const pm2 = ctx.bun ? "bun" : ctx.compiled ? "node-compiled" : "node-loader";
return this.rootRenderer.renderAll([
...runtime.files(),
"/init/.dockerignore.hbs",
"/init/.gitignore.hbs",
"/init/.barrels.json.hbs",
{
path: `/init/pm2/${pm2}/processes.config.cjs.hbs`,
output: `processes.config.cjs`,
replaces: [`pm2/${pm2}`]
},
"/init/docker-compose.yml.hbs",
{
path: `/init/docker/${packageManager.name}/Dockerfile.hbs`,
output: `Dockerfile`,
replaces: [`docker/${packageManager.name}`]
},
"/init/README.md.hbs",
"/init/tsconfig.json.hbs",
"/init/tsconfig.base.json.hbs",
"/init/tsconfig.node.json.hbs",
ctx.testing && "/init/tsconfig.spec.json.hbs",
"/init/src/index.ts.hbs",
"/init/src/config/envs/index.ts.hbs",
"/init/src/config/logger/index.ts.hbs",
"/init/src/config/index.ts.hbs",
ctx.commands && "/init/src/bin/index.ts.hbs",
ctx.swagger && "/init/views/swagger.ejs.hbs",
ctx.swagger && {
path: "/init/src/controllers/pages/IndexController.ts.hbs",
basename: indexCtrlBaseName,
replaces: [ctx.architecture === ArchitectureConvention.FEATURE ? "controllers" : null]
}
].filter(Boolean), ctx, {
baseDir: "/init"
});
}
};
InitCmd = __decorate([
Command({
name: "init",
description: "Init a new Ts.ED project",
args: {
root: {
type: String,
defaultValue: ".",
description: "Root directory to initialize the Ts.ED project"
}
},
options: {
"-n, --project-name <projectName>": {
type: String,
defaultValue: "",
description: "Set the project name. By default, the project is the same as the name directory."
},
"-a, --arch <architecture>": {
type: String,
defaultValue: ArchitectureConvention.DEFAULT,
description: `Set the default architecture convention (${ArchitectureConvention.DEFAULT} or ${ArchitectureConvention.FEATURE})`
},
"-c, --convention <convention>": {
type: String,
defaultValue: ProjectConvention.DEFAULT,
description: `Set the default project convention (${ArchitectureConvention.DEFAULT} or ${ArchitectureConvention.FEATURE})`
},
"-p, --platform <platform>": {
type: String,
defaultValue: PlatformType.EXPRESS,
description: "Set the default platform for Ts.ED (express, koa or fastify)"
},
"--features <features...>": {
type: Array,
itemType: String,
defaultValue: [],
description: "List of the Ts.ED features."
},
"--runtime <runtime>": {
itemType: String,
defaultValue: "node",
description: "The default runtime used to run the project"
},
"-m, --package-manager <packageManager>": {
itemType: String,
defaultValue: PackageManager.YARN,
description: "The default package manager to install the project"
},
"-t, --tsed-version <version>": {
type: String,
defaultValue: DEFAULT_TSED_TAGS,
description: "Use a specific version of Ts.ED (format: 5.x.x)."
},
"-f, --file <path>": {
type: String,
description: "Location of a file in which the features are defined."
},
"-s, --skip-prompt": {
type: Boolean,
defaultValue: false,
description: "Skip the prompt."
}
},
disableReadUpPkg: true
})
], InitCmd);
export { InitCmd };