Skip to content

Commit 10b0b79

Browse files
committed
feat(cli): add BuildCmd and DevCmd for Vite integration commands
1 parent 69a9aa3 commit 10b0b79

File tree

19 files changed

+346
-17
lines changed

19 files changed

+346
-17
lines changed

packages/cli-core/src/packageManagers/PackageManagersModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,4 @@ export class PackageManagersModule {
163163
}
164164
}
165165

166-
injectable(PackageManagersModule).imports([YarnBerryManager, YarnManager, NpmManager, PNpmManager, BunManager]);
166+
injectable(PackageManagersModule).imports([NpmManager, YarnBerryManager, PNpmManager, BunManager, YarnManager]);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// @ts-ignore
2+
import {CliPlatformTest} from "@tsed/cli-testing";
3+
4+
import {CliRunScript} from "../../services/CliRunScript.js";
5+
import {BuildCmd} from "./BuildCmd.js";
6+
7+
describe("BuildCmd", () => {
8+
beforeEach(() => CliPlatformTest.create());
9+
afterEach(() => CliPlatformTest.reset());
10+
11+
it("should run vite build and forward args for vite runtime", async () => {
12+
const runScript = {
13+
run: vi.fn()
14+
};
15+
16+
const command = await CliPlatformTest.invoke<BuildCmd>(BuildCmd, [
17+
{
18+
token: CliRunScript,
19+
use: runScript
20+
}
21+
]);
22+
23+
await command.$exec({
24+
rawArgs: ["--mode", "production"]
25+
});
26+
27+
expect(runScript.run).toHaveBeenCalledTimes(1);
28+
expect(runScript.run).toHaveBeenNthCalledWith(1, "vite build", ["--mode", "production"], {
29+
env: {
30+
...process.env
31+
}
32+
});
33+
});
34+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {command, type CommandProvider, inject} from "@tsed/cli-core";
2+
import {taskLogger} from "@tsed/cli-tasks";
3+
4+
import {CliRunScript} from "../../services/CliRunScript.js";
5+
6+
export interface BuildCmdContext {
7+
rawArgs: string[];
8+
}
9+
10+
export class BuildCmd implements CommandProvider {
11+
protected runScript = inject(CliRunScript);
12+
13+
async $exec(ctx: BuildCmdContext) {
14+
const command = "vite build";
15+
16+
taskLogger().info(`Run ${[command, ...ctx.rawArgs].join(" ")}`);
17+
await this.runScript.run(command, ctx.rawArgs, {
18+
env: process.env
19+
});
20+
}
21+
}
22+
23+
command({
24+
token: BuildCmd,
25+
name: "build",
26+
description: "Build the project",
27+
allowUnknownOption: true
28+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// @ts-ignore
2+
import {CliPlatformTest} from "@tsed/cli-testing";
3+
4+
import {DevCmd} from "./DevCmd.js";
5+
6+
describe("DevCmd", () => {
7+
beforeEach(() => CliPlatformTest.create());
8+
afterEach(() => CliPlatformTest.reset());
9+
10+
it("should delegate to vite runtime runner", async () => {
11+
const command = await CliPlatformTest.invoke<DevCmd>(DevCmd);
12+
13+
const runViteDev = vi.spyOn(command as any, "runViteDev").mockResolvedValue(undefined);
14+
15+
await command.$exec({
16+
rawArgs: ["--watch=false"]
17+
});
18+
19+
expect(runViteDev).toHaveBeenCalledWith(["--watch=false"]);
20+
});
21+
});
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {spawn} from "node:child_process";
2+
import process from "node:process";
3+
4+
import {command, type CommandProvider, normalizePath} from "@tsed/cli-core";
5+
import {taskLogger} from "@tsed/cli-tasks";
6+
7+
export interface DevCmdContext {
8+
rawArgs: string[];
9+
}
10+
11+
export class DevCmd implements CommandProvider {
12+
async $exec(ctx: DevCmdContext) {
13+
await this.runViteDev(ctx.rawArgs);
14+
}
15+
16+
protected parseWatchValue(args: string[]) {
17+
const watchArg = args.find((arg) => arg === "--watch" || arg.startsWith("--watch="));
18+
19+
if (!watchArg) {
20+
return true;
21+
}
22+
23+
if (watchArg === "--watch") {
24+
return true;
25+
}
26+
27+
return watchArg !== "--watch=false";
28+
}
29+
30+
protected async createViteDevServer() {
31+
const {createServer} = await import("vite");
32+
33+
return createServer({
34+
configFile: normalizePath("vite.config.ts"),
35+
server: {
36+
middlewareMode: true,
37+
hmr: false,
38+
ws: false
39+
}
40+
});
41+
}
42+
43+
protected async runViteApp() {
44+
const vite = await this.createViteDevServer();
45+
46+
const shutdown = async () => {
47+
await vite.close();
48+
process.exit(0);
49+
};
50+
51+
process.on("SIGINT", shutdown);
52+
process.on("SIGTERM", shutdown);
53+
54+
await vite.ssrLoadModule(`/src/index.ts?t=${Date.now()}`);
55+
await new Promise(() => {});
56+
}
57+
58+
protected async runViteController(rawArgs: string[]) {
59+
const watch = this.parseWatchValue(rawArgs);
60+
const vite = await this.createViteDevServer();
61+
let childProcess: ReturnType<typeof spawn> | undefined;
62+
let restarting = false;
63+
let queued = false;
64+
65+
const startChild = () => {
66+
const [, scriptPath] = process.argv;
67+
const args = scriptPath ? [scriptPath, "dev", ...rawArgs] : ["dev", ...rawArgs];
68+
69+
childProcess = spawn(process.execPath, args, {
70+
env: {
71+
...process.env,
72+
TSED_VITE_RUN_MODE: "app"
73+
},
74+
stdio: "inherit"
75+
});
76+
};
77+
78+
const stopChild = async () => {
79+
if (!childProcess || childProcess.killed) {
80+
return;
81+
}
82+
83+
await new Promise((resolve) => {
84+
childProcess!.once("exit", resolve);
85+
childProcess!.kill("SIGTERM");
86+
});
87+
};
88+
89+
const restartChild = async (reason: string, file = "") => {
90+
if (restarting) {
91+
queued = true;
92+
return;
93+
}
94+
95+
restarting = true;
96+
const suffix = file ? `: ${file}` : "";
97+
taskLogger().info(`[tsed-dev] restart (${reason})${suffix}`);
98+
await stopChild();
99+
startChild();
100+
restarting = false;
101+
102+
if (queued) {
103+
queued = false;
104+
await restartChild("queued");
105+
}
106+
};
107+
108+
if (watch) {
109+
vite.watcher.on("all", async (event, file) => {
110+
if (!file || file.includes("node_modules") || file.includes(".git") || file.includes("/dist/")) {
111+
return;
112+
}
113+
114+
if (["add", "change", "unlink"].includes(event)) {
115+
await restartChild(event, file);
116+
}
117+
});
118+
}
119+
120+
vite.watcher.once("ready", () => {
121+
startChild();
122+
});
123+
124+
const shutdown = async () => {
125+
await stopChild();
126+
await vite.close();
127+
process.exit(0);
128+
};
129+
130+
process.on("SIGINT", shutdown);
131+
process.on("SIGTERM", shutdown);
132+
133+
await new Promise(() => {});
134+
}
135+
136+
protected async runViteDev(rawArgs: string[]) {
137+
if (process.env.TSED_VITE_RUN_MODE === "app") {
138+
await this.runViteApp();
139+
return;
140+
}
141+
142+
await this.runViteController(rawArgs);
143+
}
144+
}
145+
146+
command({
147+
token: DevCmd,
148+
name: "dev",
149+
description: "Run the project in development mode",
150+
allowUnknownOption: true
151+
});

packages/cli/src/commands/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {AddCmd} from "./add/AddCmd.js";
2+
import {BuildCmd} from "./build/BuildCmd.js";
3+
import {DevCmd} from "./dev/DevCmd.js";
24
import {GenerateCmd} from "./generate/GenerateCmd.js";
35
import {InitCmd} from "./init/InitCmd.js";
46
import {InitOptionsCommand} from "./init/InitOptionsCmd.js";
@@ -7,4 +9,4 @@ import {RunCmd} from "./run/RunCmd.js";
79
import {CreateTemplateCommand} from "./template/CreateTemplateCommand.js";
810
import {UpdateCmd} from "./update/UpdateCmd.js";
911

10-
export default [AddCmd, InitCmd, InitOptionsCommand, GenerateCmd, UpdateCmd, RunCmd, CreateTemplateCommand, McpCommand];
12+
export default [AddCmd, InitCmd, InitOptionsCommand, GenerateCmd, UpdateCmd, RunCmd, DevCmd, BuildCmd, CreateTemplateCommand, McpCommand];

packages/cli/src/commands/init/InitCmd.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {PlatformsModule} from "../../platforms/PlatformsModule.js";
3333
import {RuntimesModule} from "../../runtimes/RuntimesModule.js";
3434
import {BunRuntime} from "../../runtimes/supports/BunRuntime.js";
3535
import {NodeRuntime} from "../../runtimes/supports/NodeRuntime.js";
36+
import {ViteRuntime} from "../../runtimes/supports/ViteRuntime.js";
3637
import {CliProjectService} from "../../services/CliProjectService.js";
3738
import type {TemplateRenderOptions} from "../../services/CliTemplatesService.js";
3839
import {anonymizePaths} from "../../services/mappers/anonymizePaths.js";
@@ -188,7 +189,8 @@ export class InitCmd implements CommandProvider {
188189
...ctx,
189190
node: runtime instanceof NodeRuntime,
190191
bun: runtime instanceof BunRuntime,
191-
compiled: runtime instanceof NodeRuntime && runtime.isCompiled()
192+
vite: runtime instanceof ViteRuntime,
193+
compiled: runtime.isCompiled()
192194
};
193195

194196
return [

packages/cli/src/commands/init/config/FeaturesPrompt.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ export const FeaturesMap: Record<string, Feature> = {
362362
name: "Lint on commit"
363363
},
364364

365+
vite: {
366+
name: "Node.js + Vite",
367+
checked: false
368+
},
365369
node: {
366370
name: "Node.js + SWC",
367371
checked: true
@@ -378,17 +382,17 @@ export const FeaturesMap: Record<string, Feature> = {
378382
name: "Bun.js",
379383
checked: false
380384
},
381-
yarn: {
382-
name: "Yarn",
383-
checked: true
385+
npm: {
386+
name: "NPM",
387+
checked: false
384388
},
385389
yarn_berry: {
386390
name: "Yarn Berry",
387391
checked: false
388392
},
389-
npm: {
390-
name: "NPM",
391-
checked: false
393+
yarn: {
394+
name: "Yarn",
395+
checked: true
392396
},
393397
pnpm: {
394398
name: "PNPM",
@@ -549,7 +553,7 @@ export const FeaturesPrompt = (availableRuntimes: string[], availablePackageMana
549553
message: "Choose the package manager:",
550554
type: "list",
551555
name: "packageManager",
552-
when: hasValue("runtime", ["node", "babel", "swc", "webpack"]),
556+
when: hasValue("runtime", ["vite", "node", "babel", "swc", "webpack"]),
553557
choices: availablePackageManagers
554558
}
555559
] satisfies PromptQuestion[];

packages/cli/src/commands/init/config/InitSchema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ export const InitSchema = () => {
278278
.prompt("Choose the runtime:")
279279
.choices(
280280
[
281+
{
282+
label: "Node.js + Vite",
283+
value: "vite"
284+
},
281285
{
282286
label: "Node.js + SWC",
283287
value: "node"

packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import {getFeaturesPrompt} from "./getFeaturesPrompt.js";
22

33
describe("getFeaturesPrompt", () => {
4+
it("should throw with an explicit message when a choice is unknown", () => {
5+
expect(() => getFeaturesPrompt(["unknown-runtime"], ["yarn"], {})).toThrowError(
6+
'Unknown init prompt choice "unknown-runtime" for prompt "runtime"'
7+
);
8+
});
9+
10+
it("should map vite runtime choice without throwing", () => {
11+
const prompt = getFeaturesPrompt(["node", "vite"], ["yarn", "npm"], {});
12+
const runtimePrompt = prompt.find((item: any) => item.name === "runtime");
13+
14+
expect(runtimePrompt).toBeDefined();
15+
expect((runtimePrompt as any).choices).toEqual(
16+
expect.arrayContaining([
17+
expect.objectContaining({value: "node", name: "Node.js + SWC"}),
18+
expect.objectContaining({value: "vite", name: "Node.js + Vite"})
19+
])
20+
);
21+
});
22+
423
it("should add a provider info", () => {
524
const prompt = getFeaturesPrompt(["node", "bun"], ["yarn", "npm", "pnpm", "bun"], {});
625

0 commit comments

Comments
 (0)