Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,4 @@ export class PackageManagersModule {
}
}

injectable(PackageManagersModule).imports([YarnBerryManager, YarnManager, NpmManager, PNpmManager, BunManager]);
injectable(PackageManagersModule).imports([NpmManager, YarnBerryManager, PNpmManager, BunManager, YarnManager]);
4 changes: 1 addition & 3 deletions packages/cli-plugin-vitest/src/CliPluginVitestModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
});
}

Expand Down
19 changes: 5 additions & 14 deletions packages/cli-plugin-vitest/src/templates/vitest.config.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
});`;
}
});
34 changes: 34 additions & 0 deletions packages/cli/src/commands/build/BuildCmd.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(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
}
});
});
});
28 changes: 28 additions & 0 deletions packages/cli/src/commands/build/BuildCmd.ts
Original file line number Diff line number Diff line change
@@ -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
});
21 changes: 21 additions & 0 deletions packages/cli/src/commands/dev/DevCmd.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(DevCmd);

const runViteDev = vi.spyOn(command as any, "runViteDev").mockResolvedValue(undefined);

await command.$exec({
rawArgs: ["--watch=false"]
});

expect(runViteDev).toHaveBeenCalledWith(["--watch=false"]);
});
});
151 changes: 151 additions & 0 deletions packages/cli/src/commands/dev/DevCmd.ts
Original file line number Diff line number Diff line change
@@ -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<typeof spawn> | 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
});
4 changes: 3 additions & 1 deletion packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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];
4 changes: 3 additions & 1 deletion packages/cli/src/commands/init/InitCmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 [
Expand Down
18 changes: 11 additions & 7 deletions packages/cli/src/commands/init/config/FeaturesPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ export const FeaturesMap: Record<string, Feature> = {
name: "Lint on commit"
},

vite: {
name: "Node.js + Vite",
checked: false
},
node: {
name: "Node.js + SWC",
checked: true
Expand All @@ -378,17 +382,17 @@ export const FeaturesMap: Record<string, Feature> = {
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",
Expand Down Expand Up @@ -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[];
4 changes: 4 additions & 0 deletions packages/cli/src/commands/init/config/InitSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ export const InitSchema = () => {
.prompt("Choose the runtime:")
.choices(
[
{
label: "Node.js + Vite",
value: "vite"
},
{
label: "Node.js + SWC",
value: "node"
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts
Original file line number Diff line number Diff line change
@@ -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"], {});

Expand Down
Loading
Loading