From 08d53bfbe17b8b055c274b194887e1df12a74fbf Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 8 Apr 2026 14:38:36 +0200 Subject: [PATCH 1/2] fix(cli-plugin-vitest): migrate configuration to vitest v8 --- .../src/CliPluginVitestModule.ts | 4 +--- .../src/templates/vitest.config.template.ts | 19 +++++-------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/packages/cli-plugin-vitest/src/CliPluginVitestModule.ts b/packages/cli-plugin-vitest/src/CliPluginVitestModule.ts index 3b5618da5..faf034926 100644 --- a/packages/cli-plugin-vitest/src/CliPluginVitestModule.ts +++ b/packages/cli-plugin-vitest/src/CliPluginVitestModule.ts @@ -23,9 +23,7 @@ export class CliPluginVitestModule implements AlterInitSubTasks, AlterPackageJso }); packageJson.addDevDependencies({ vitest: "latest", - "unplugin-swc": "latest", - "@vitest/coverage-v8": "latest", - "@swc/core": "latest" + "@vitest/coverage-v8": "latest" }); } diff --git a/packages/cli-plugin-vitest/src/templates/vitest.config.template.ts b/packages/cli-plugin-vitest/src/templates/vitest.config.template.ts index acaf1f18d..800171137 100644 --- a/packages/cli-plugin-vitest/src/templates/vitest.config.template.ts +++ b/packages/cli-plugin-vitest/src/templates/vitest.config.template.ts @@ -9,26 +9,17 @@ export default defineTemplate({ hidden: true, render() { - return `import swc from "unplugin-swc"; -import {defineConfig} from "vitest/config"; + return `import {defineConfig} from "vitest/config"; export default defineConfig({ + resolve: { + tsconfigPaths: true + }, test: { globals: true, root: "./" }, - plugins: [ - // This is required to build the test files with SWC - swc.vite({ - // Explicitly set the module type to avoid inheriting this value from a \`.swcrc\` config file - module: {type: "es6"}, - jsc: { - transform: { - useDefineForClassFields: false - } - } - }) - ] + plugins: [] });`; } }); From e33c03d986c01300e1e783803692f5eae033a126 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 8 Apr 2026 14:47:07 +0200 Subject: [PATCH 2/2] feat(cli): add BuildCmd and DevCmd for Vite integration commands --- .../packageManagers/PackageManagersModule.ts | 2 +- .../cli/src/commands/build/BuildCmd.spec.ts | 34 ++++ packages/cli/src/commands/build/BuildCmd.ts | 28 ++++ packages/cli/src/commands/dev/DevCmd.spec.ts | 21 +++ packages/cli/src/commands/dev/DevCmd.ts | 151 ++++++++++++++++++ packages/cli/src/commands/index.ts | 4 +- packages/cli/src/commands/init/InitCmd.ts | 4 +- .../commands/init/config/FeaturesPrompt.ts | 18 ++- .../src/commands/init/config/InitSchema.ts | 4 + .../init/prompts/getFeaturesPrompt.spec.ts | 19 +++ .../init/prompts/getFeaturesPrompt.ts | 10 +- .../mcp/schema/ProjectPreferencesSchema.ts | 4 +- packages/cli/src/commands/run/RunCmd.ts | 1 - .../cli/src/interfaces/RenderDataContext.ts | 1 + packages/cli/src/interfaces/RuntimeTypes.ts | 2 +- packages/cli/src/runtimes/RuntimesModule.ts | 3 +- packages/cli/src/runtimes/index.ts | 1 + .../cli/src/runtimes/supports/ViteRuntime.ts | 35 ++++ packages/cli/templates/vite.config.ts | 21 +++ 19 files changed, 346 insertions(+), 17 deletions(-) create mode 100644 packages/cli/src/commands/build/BuildCmd.spec.ts create mode 100644 packages/cli/src/commands/build/BuildCmd.ts create mode 100644 packages/cli/src/commands/dev/DevCmd.spec.ts create mode 100644 packages/cli/src/commands/dev/DevCmd.ts create mode 100644 packages/cli/src/runtimes/supports/ViteRuntime.ts create mode 100644 packages/cli/templates/vite.config.ts diff --git a/packages/cli-core/src/packageManagers/PackageManagersModule.ts b/packages/cli-core/src/packageManagers/PackageManagersModule.ts index de0211d8f..b6d3c0d23 100644 --- a/packages/cli-core/src/packageManagers/PackageManagersModule.ts +++ b/packages/cli-core/src/packageManagers/PackageManagersModule.ts @@ -163,4 +163,4 @@ export class PackageManagersModule { } } -injectable(PackageManagersModule).imports([YarnBerryManager, YarnManager, NpmManager, PNpmManager, BunManager]); +injectable(PackageManagersModule).imports([NpmManager, YarnBerryManager, PNpmManager, BunManager, YarnManager]); diff --git a/packages/cli/src/commands/build/BuildCmd.spec.ts b/packages/cli/src/commands/build/BuildCmd.spec.ts new file mode 100644 index 000000000..c61ab4ea3 --- /dev/null +++ b/packages/cli/src/commands/build/BuildCmd.spec.ts @@ -0,0 +1,34 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; + +import {CliRunScript} from "../../services/CliRunScript.js"; +import {BuildCmd} from "./BuildCmd.js"; + +describe("BuildCmd", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + it("should run vite build and forward args for vite runtime", async () => { + const runScript = { + run: vi.fn() + }; + + const command = await CliPlatformTest.invoke(BuildCmd, [ + { + token: CliRunScript, + use: runScript + } + ]); + + await command.$exec({ + rawArgs: ["--mode", "production"] + }); + + expect(runScript.run).toHaveBeenCalledTimes(1); + expect(runScript.run).toHaveBeenNthCalledWith(1, "vite build", ["--mode", "production"], { + env: { + ...process.env + } + }); + }); +}); diff --git a/packages/cli/src/commands/build/BuildCmd.ts b/packages/cli/src/commands/build/BuildCmd.ts new file mode 100644 index 000000000..e63fe7a7a --- /dev/null +++ b/packages/cli/src/commands/build/BuildCmd.ts @@ -0,0 +1,28 @@ +import {command, type CommandProvider, inject} from "@tsed/cli-core"; +import {taskLogger} from "@tsed/cli-tasks"; + +import {CliRunScript} from "../../services/CliRunScript.js"; + +export interface BuildCmdContext { + rawArgs: string[]; +} + +export class BuildCmd implements CommandProvider { + protected runScript = inject(CliRunScript); + + async $exec(ctx: BuildCmdContext) { + const command = "vite build"; + + taskLogger().info(`Run ${[command, ...ctx.rawArgs].join(" ")}`); + await this.runScript.run(command, ctx.rawArgs, { + env: process.env + }); + } +} + +command({ + token: BuildCmd, + name: "build", + description: "Build the project", + allowUnknownOption: true +}); diff --git a/packages/cli/src/commands/dev/DevCmd.spec.ts b/packages/cli/src/commands/dev/DevCmd.spec.ts new file mode 100644 index 000000000..75e6c1b4b --- /dev/null +++ b/packages/cli/src/commands/dev/DevCmd.spec.ts @@ -0,0 +1,21 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; + +import {DevCmd} from "./DevCmd.js"; + +describe("DevCmd", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + it("should delegate to vite runtime runner", async () => { + const command = await CliPlatformTest.invoke(DevCmd); + + const runViteDev = vi.spyOn(command as any, "runViteDev").mockResolvedValue(undefined); + + await command.$exec({ + rawArgs: ["--watch=false"] + }); + + expect(runViteDev).toHaveBeenCalledWith(["--watch=false"]); + }); +}); diff --git a/packages/cli/src/commands/dev/DevCmd.ts b/packages/cli/src/commands/dev/DevCmd.ts new file mode 100644 index 000000000..d282cb45c --- /dev/null +++ b/packages/cli/src/commands/dev/DevCmd.ts @@ -0,0 +1,151 @@ +import {spawn} from "node:child_process"; +import process from "node:process"; + +import {command, type CommandProvider, normalizePath} from "@tsed/cli-core"; +import {taskLogger} from "@tsed/cli-tasks"; + +export interface DevCmdContext { + rawArgs: string[]; +} + +export class DevCmd implements CommandProvider { + async $exec(ctx: DevCmdContext) { + await this.runViteDev(ctx.rawArgs); + } + + protected parseWatchValue(args: string[]) { + const watchArg = args.find((arg) => arg === "--watch" || arg.startsWith("--watch=")); + + if (!watchArg) { + return true; + } + + if (watchArg === "--watch") { + return true; + } + + return watchArg !== "--watch=false"; + } + + protected async createViteDevServer() { + const {createServer} = await import("vite"); + + return createServer({ + configFile: normalizePath("vite.config.ts"), + server: { + middlewareMode: true, + hmr: false, + ws: false + } + }); + } + + protected async runViteApp() { + const vite = await this.createViteDevServer(); + + const shutdown = async () => { + await vite.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + await vite.ssrLoadModule(`/src/index.ts?t=${Date.now()}`); + await new Promise(() => {}); + } + + protected async runViteController(rawArgs: string[]) { + const watch = this.parseWatchValue(rawArgs); + const vite = await this.createViteDevServer(); + let childProcess: ReturnType | undefined; + let restarting = false; + let queued = false; + + const startChild = () => { + const [, scriptPath] = process.argv; + const args = scriptPath ? [scriptPath, "dev", ...rawArgs] : ["dev", ...rawArgs]; + + childProcess = spawn(process.execPath, args, { + env: { + ...process.env, + TSED_VITE_RUN_MODE: "app" + }, + stdio: "inherit" + }); + }; + + const stopChild = async () => { + if (!childProcess || childProcess.killed) { + return; + } + + await new Promise((resolve) => { + childProcess!.once("exit", resolve); + childProcess!.kill("SIGTERM"); + }); + }; + + const restartChild = async (reason: string, file = "") => { + if (restarting) { + queued = true; + return; + } + + restarting = true; + const suffix = file ? `: ${file}` : ""; + taskLogger().info(`[tsed-dev] restart (${reason})${suffix}`); + await stopChild(); + startChild(); + restarting = false; + + if (queued) { + queued = false; + await restartChild("queued"); + } + }; + + if (watch) { + vite.watcher.on("all", async (event, file) => { + if (!file || file.includes("node_modules") || file.includes(".git") || file.includes("/dist/")) { + return; + } + + if (["add", "change", "unlink"].includes(event)) { + await restartChild(event, file); + } + }); + } + + vite.watcher.once("ready", () => { + startChild(); + }); + + const shutdown = async () => { + await stopChild(); + await vite.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + await new Promise(() => {}); + } + + protected async runViteDev(rawArgs: string[]) { + if (process.env.TSED_VITE_RUN_MODE === "app") { + await this.runViteApp(); + return; + } + + await this.runViteController(rawArgs); + } +} + +command({ + token: DevCmd, + name: "dev", + description: "Run the project in development mode", + allowUnknownOption: true +}); diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 0fb792c78..00bf1d664 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,4 +1,6 @@ import {AddCmd} from "./add/AddCmd.js"; +import {BuildCmd} from "./build/BuildCmd.js"; +import {DevCmd} from "./dev/DevCmd.js"; import {GenerateCmd} from "./generate/GenerateCmd.js"; import {InitCmd} from "./init/InitCmd.js"; import {InitOptionsCommand} from "./init/InitOptionsCmd.js"; @@ -7,4 +9,4 @@ import {RunCmd} from "./run/RunCmd.js"; import {CreateTemplateCommand} from "./template/CreateTemplateCommand.js"; import {UpdateCmd} from "./update/UpdateCmd.js"; -export default [AddCmd, InitCmd, InitOptionsCommand, GenerateCmd, UpdateCmd, RunCmd, CreateTemplateCommand, McpCommand]; +export default [AddCmd, InitCmd, InitOptionsCommand, GenerateCmd, UpdateCmd, RunCmd, DevCmd, BuildCmd, CreateTemplateCommand, McpCommand]; diff --git a/packages/cli/src/commands/init/InitCmd.ts b/packages/cli/src/commands/init/InitCmd.ts index 437e84b3d..293d328f9 100644 --- a/packages/cli/src/commands/init/InitCmd.ts +++ b/packages/cli/src/commands/init/InitCmd.ts @@ -33,6 +33,7 @@ import {PlatformsModule} from "../../platforms/PlatformsModule.js"; import {RuntimesModule} from "../../runtimes/RuntimesModule.js"; import {BunRuntime} from "../../runtimes/supports/BunRuntime.js"; import {NodeRuntime} from "../../runtimes/supports/NodeRuntime.js"; +import {ViteRuntime} from "../../runtimes/supports/ViteRuntime.js"; import {CliProjectService} from "../../services/CliProjectService.js"; import type {TemplateRenderOptions} from "../../services/CliTemplatesService.js"; import {anonymizePaths} from "../../services/mappers/anonymizePaths.js"; @@ -188,7 +189,8 @@ export class InitCmd implements CommandProvider { ...ctx, node: runtime instanceof NodeRuntime, bun: runtime instanceof BunRuntime, - compiled: runtime instanceof NodeRuntime && runtime.isCompiled() + vite: runtime instanceof ViteRuntime, + compiled: runtime.isCompiled() }; return [ diff --git a/packages/cli/src/commands/init/config/FeaturesPrompt.ts b/packages/cli/src/commands/init/config/FeaturesPrompt.ts index d3faf9d22..a98236a33 100644 --- a/packages/cli/src/commands/init/config/FeaturesPrompt.ts +++ b/packages/cli/src/commands/init/config/FeaturesPrompt.ts @@ -362,6 +362,10 @@ export const FeaturesMap: Record = { name: "Lint on commit" }, + vite: { + name: "Node.js + Vite", + checked: false + }, node: { name: "Node.js + SWC", checked: true @@ -378,17 +382,17 @@ export const FeaturesMap: Record = { name: "Bun.js", checked: false }, - yarn: { - name: "Yarn", - checked: true + npm: { + name: "NPM", + checked: false }, yarn_berry: { name: "Yarn Berry", checked: false }, - npm: { - name: "NPM", - checked: false + yarn: { + name: "Yarn", + checked: true }, pnpm: { name: "PNPM", @@ -549,7 +553,7 @@ export const FeaturesPrompt = (availableRuntimes: string[], availablePackageMana message: "Choose the package manager:", type: "list", name: "packageManager", - when: hasValue("runtime", ["node", "babel", "swc", "webpack"]), + when: hasValue("runtime", ["vite", "node", "babel", "swc", "webpack"]), choices: availablePackageManagers } ] satisfies PromptQuestion[]; diff --git a/packages/cli/src/commands/init/config/InitSchema.ts b/packages/cli/src/commands/init/config/InitSchema.ts index 5a57fb5f2..ba997119f 100644 --- a/packages/cli/src/commands/init/config/InitSchema.ts +++ b/packages/cli/src/commands/init/config/InitSchema.ts @@ -278,6 +278,10 @@ export const InitSchema = () => { .prompt("Choose the runtime:") .choices( [ + { + label: "Node.js + Vite", + value: "vite" + }, { label: "Node.js + SWC", value: "node" diff --git a/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts b/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts index f2e4f9833..b2c6b778f 100644 --- a/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts +++ b/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts @@ -1,6 +1,25 @@ import {getFeaturesPrompt} from "./getFeaturesPrompt.js"; describe("getFeaturesPrompt", () => { + it("should throw with an explicit message when a choice is unknown", () => { + expect(() => getFeaturesPrompt(["unknown-runtime"], ["yarn"], {})).toThrowError( + 'Unknown init prompt choice "unknown-runtime" for prompt "runtime"' + ); + }); + + it("should map vite runtime choice without throwing", () => { + const prompt = getFeaturesPrompt(["node", "vite"], ["yarn", "npm"], {}); + const runtimePrompt = prompt.find((item: any) => item.name === "runtime"); + + expect(runtimePrompt).toBeDefined(); + expect((runtimePrompt as any).choices).toEqual( + expect.arrayContaining([ + expect.objectContaining({value: "node", name: "Node.js + SWC"}), + expect.objectContaining({value: "vite", name: "Node.js + Vite"}) + ]) + ); + }); + it("should add a provider info", () => { const prompt = getFeaturesPrompt(["node", "bun"], ["yarn", "npm", "pnpm", "bun"], {}); diff --git a/packages/cli/src/commands/init/prompts/getFeaturesPrompt.ts b/packages/cli/src/commands/init/prompts/getFeaturesPrompt.ts index 4c7ce959b..38f126254 100644 --- a/packages/cli/src/commands/init/prompts/getFeaturesPrompt.ts +++ b/packages/cli/src/commands/init/prompts/getFeaturesPrompt.ts @@ -5,10 +5,16 @@ import {FeaturesMap, FeaturesPrompt} from "../config/FeaturesPrompt.js"; function mapChoices(item: any, options: Partial) { return item.choices.map((choice: string) => { - const {checked} = FeaturesMap[choice]; + const config = FeaturesMap[choice]; + + if (!config) { + throw new Error(`Unknown init prompt choice "${choice}" for prompt "${item.name}"`); + } + + const {checked} = config; return cleanObject({ - ...FeaturesMap[choice], + ...config, value: choice, checked: isFunction(checked) ? checked(options) : checked }); diff --git a/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts b/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts index 9559af0d0..5c0368eab 100644 --- a/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts +++ b/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts @@ -10,8 +10,8 @@ export const ProjectPreferenceSchema = s packageManager: s.string().enum(PackageManager).description("Used project manager to install dependencies"), runtime: s .string() - .enum("node", "babel", "swc", "webpack", "bun") - .description("The javascript runtime used to start application (node, node + webpack, node + swc, node + babel, bun)"), + .enum("vite", "node", "babel", "swc", "webpack", "bun") + .description("The javascript runtime used to start application (node, node + webpack, node + swc, node + babel, bun, vite)"), platform: s.string().enum(PlatformType).description("Node.js framework used to run server (Express, Koa, Fastify)") }) .optional() diff --git a/packages/cli/src/commands/run/RunCmd.ts b/packages/cli/src/commands/run/RunCmd.ts index eb59b18cc..fa2a41217 100644 --- a/packages/cli/src/commands/run/RunCmd.ts +++ b/packages/cli/src/commands/run/RunCmd.ts @@ -1,6 +1,5 @@ import {CliFs, command, type CommandProvider, inject, normalizePath, ProjectPackageJson} from "@tsed/cli-core"; import {taskLogger} from "@tsed/cli-tasks"; -import {logger} from "@tsed/di"; import {CliRunScript} from "../../services/CliRunScript.js"; diff --git a/packages/cli/src/interfaces/RenderDataContext.ts b/packages/cli/src/interfaces/RenderDataContext.ts index 3e712b181..61ad0840f 100644 --- a/packages/cli/src/interfaces/RenderDataContext.ts +++ b/packages/cli/src/interfaces/RenderDataContext.ts @@ -50,6 +50,7 @@ export interface RenderDataContext extends CommandData, TsED.RenderDataContext { configPostgres?: boolean; barrels?: string; bun?: boolean; + vite?: boolean; node?: boolean; compiled?: boolean; testing?: boolean; diff --git a/packages/cli/src/interfaces/RuntimeTypes.ts b/packages/cli/src/interfaces/RuntimeTypes.ts index 72acfeb0c..92f32765f 100644 --- a/packages/cli/src/interfaces/RuntimeTypes.ts +++ b/packages/cli/src/interfaces/RuntimeTypes.ts @@ -1,4 +1,4 @@ -export type RuntimeTypes = "node" | "babel" | "swc" | "webpack" | "bun"; +export type RuntimeTypes = "node" | "babel" | "swc" | "webpack" | "bun" | "vite"; declare global { namespace TsED { diff --git a/packages/cli/src/runtimes/RuntimesModule.ts b/packages/cli/src/runtimes/RuntimesModule.ts index 62e381f64..791f8bc40 100644 --- a/packages/cli/src/runtimes/RuntimesModule.ts +++ b/packages/cli/src/runtimes/RuntimesModule.ts @@ -5,6 +5,7 @@ import {BabelRuntime} from "./supports/BabelRuntime.js"; import {BaseRuntime} from "./supports/BaseRuntime.js"; import {BunRuntime} from "./supports/BunRuntime.js"; import {NodeRuntime} from "./supports/NodeRuntime.js"; +import {ViteRuntime} from "./supports/ViteRuntime.js"; import {WebpackRuntime} from "./supports/WebpackRuntime.js"; export interface RuntimeInitOptions { @@ -62,4 +63,4 @@ export class RuntimesModule { } } -injectable(RuntimesModule).imports([NodeRuntime, BabelRuntime, WebpackRuntime, BunRuntime]); +injectable(RuntimesModule).imports([ViteRuntime, NodeRuntime, BabelRuntime, WebpackRuntime, , BunRuntime]); diff --git a/packages/cli/src/runtimes/index.ts b/packages/cli/src/runtimes/index.ts index 175fae2a5..1378c58a0 100644 --- a/packages/cli/src/runtimes/index.ts +++ b/packages/cli/src/runtimes/index.ts @@ -3,4 +3,5 @@ export * from "./supports/BabelRuntime.js"; export * from "./supports/BaseRuntime.js"; export * from "./supports/BunRuntime.js"; export * from "./supports/NodeRuntime.js"; +export * from "./supports/ViteRuntime.js"; export * from "./supports/WebpackRuntime.js"; diff --git a/packages/cli/src/runtimes/supports/ViteRuntime.ts b/packages/cli/src/runtimes/supports/ViteRuntime.ts new file mode 100644 index 000000000..8e6713605 --- /dev/null +++ b/packages/cli/src/runtimes/supports/ViteRuntime.ts @@ -0,0 +1,35 @@ +import {injectable} from "@tsed/di"; + +import {BaseRuntime} from "./BaseRuntime.js"; + +export class ViteRuntime extends BaseRuntime { + readonly name = "vite"; + readonly cmd = "node"; + readonly order: number = -1; + + files() { + return ["vite.config.ts"]; + } + + compile(): string { + return "tsed build"; + } + + startDev(): string { + return "tsed dev"; + } + + startProd(args: string): string { + return `node ${args}`; + } + + devDependencies(): Record { + return { + "@tsed/cli": "{{cliVersion}}", + typescript: "latest", + vite: "latest" + }; + } +} + +injectable(ViteRuntime).type("runtime"); diff --git a/packages/cli/templates/vite.config.ts b/packages/cli/templates/vite.config.ts new file mode 100644 index 000000000..13996661f --- /dev/null +++ b/packages/cli/templates/vite.config.ts @@ -0,0 +1,21 @@ +import {builtinModules} from "node:module"; + +import {defineConfig} from "vite"; + +export default defineConfig({ + appType: "custom", + build: { + outDir: "dist", + target: "node24", + ssr: "src/index.ts", + sourcemap: true, + minify: false, + rollupOptions: { + external: [...builtinModules, ...builtinModules.map((module) => `node:${module}`)], + output: { + entryFileNames: "index.js", + format: "es" + } + } + } +});