diff --git a/CHANGES.md b/CHANGES.md index 73b65d407..77bae5b4d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -175,6 +175,10 @@ To be released. ### @fedify/init + - Added a `test` task to projects scaffolded by `fedify init`. It starts + the app, waits for it to become ready, and checks that it resolves a local + actor, giving projects a standard smoke test to run right after scaffolding + and whenever the app changes afterwards. [[#898]] - Fixed `fedify init`'s hydration test validation to run `format` before `format:check`, which previously caused the entire test suite to fail when the package manager is `npm` or `pnpm`: @@ -183,6 +187,7 @@ To be released. - Supported \[SvelteKit\] as a web framework option in `fedify init`. [[#892], [#971] by Jang Hanarae\] +[#898]: https://github.com/fedify-dev/fedify/issues/898 [#950]: https://github.com/fedify-dev/fedify/issues/950 [#952]: https://github.com/fedify-dev/fedify/pull/952 diff --git a/changes.d/init/smoke-test.md b/changes.d/init/smoke-test.md new file mode 100644 index 000000000..372185a6e --- /dev/null +++ b/changes.d/init/smoke-test.md @@ -0,0 +1,4 @@ + - Added a `test` task to projects scaffolded by `fedify init`. It starts + the app, waits for it to become ready, and checks that it resolves a local + actor, giving projects a standard smoke test to run right after scaffolding + and whenever the app changes afterwards. [[#898]] diff --git a/packages/init/src/action/configs.test.ts b/packages/init/src/action/configs.test.ts index 04a77603e..c888ea843 100644 --- a/packages/init/src/action/configs.test.ts +++ b/packages/init/src/action/configs.test.ts @@ -35,6 +35,7 @@ function createInitData(): InitCommandData { initializer: { federationFile: "federation.ts", loggingFile: "logging.ts", + testFile: "scripts/smokeTest.ts", instruction: message`done`, tasks: {}, compilerOptions: {}, diff --git a/packages/init/src/action/patch.test.ts b/packages/init/src/action/patch.test.ts index c97a7644d..9241166f0 100644 --- a/packages/init/src/action/patch.test.ts +++ b/packages/init/src/action/patch.test.ts @@ -1,9 +1,9 @@ +import { message } from "@optique/core"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { message } from "@optique/core"; import type { InitCommandData } from "../types.ts"; import { assertNoGeneratedFileConflicts, @@ -92,6 +92,18 @@ test("patchFiles merges JSONC files containing only comments", async () => { }); }); +test("patchFiles writes the smoke-test script", async () => { + await withTempDir(async (dir) => { + await patchFiles(createInitData(dir, false)); + + const testScript = await readFile( + join(dir, "scripts", "smokeTest.ts"), + "utf8", + ); + assert.match(testScript, /\["npm","run","dev"\]/); + }); +}); + function createInitData( dir: string, allowNonEmpty: boolean, @@ -111,6 +123,7 @@ function createInitData( initializer: { federationFile: "src/federation.ts", loggingFile: "src/logging.ts", + testFile: "scripts/smokeTest.ts", instruction: message`done`, tasks: {}, compilerOptions: {}, diff --git a/packages/init/src/action/patch.ts b/packages/init/src/action/patch.ts index f32b1031d..88d51a80d 100644 --- a/packages/init/src/action/patch.ts +++ b/packages/init/src/action/patch.ts @@ -25,7 +25,12 @@ import { noticeFilesToCreate, noticeFilesToInsert, } from "./notice.ts"; -import { getImports, loadFederation, loadLogging } from "./templates.ts"; +import { + getImports, + loadFederation, + loadLogging, + loadTest, +} from "./templates.ts"; import { joinDir, stringifyEnvs } from "./utils.ts"; const jsonsCache = new Map>(); @@ -133,6 +138,7 @@ const getFiles = async < ...data, }), [data.initializer.loggingFile]: await loadLogging(data), + [data.initializer.testFile]: await loadTest(data), ".env": stringifyEnvs(data.env), ...data.initializer.files, }); @@ -183,6 +189,7 @@ const getJsons = < const getGeneratedFilePaths = (data: InitCommandData): string[] => [ data.initializer.federationFile, data.initializer.loggingFile, + data.initializer.testFile, ".env", ...Object.keys(data.initializer.files ?? {}), ...Object.keys(getJsons(data)), diff --git a/packages/init/src/action/templates.ts b/packages/init/src/action/templates.ts index f8a0ed773..612522dff 100644 --- a/packages/init/src/action/templates.ts +++ b/packages/init/src/action/templates.ts @@ -1,6 +1,6 @@ import { concat, entries, join, map, pipe, when } from "@fxts/core"; import { toMerged } from "es-toolkit"; -import { readTemplate } from "../lib.ts"; +import { getDevCommand, readTemplate } from "../lib.ts"; import type { InitCommandData, PackageManager } from "../types.ts"; import { replace } from "../utils.ts"; import { needsDenoDotenv } from "./utils.ts"; @@ -57,6 +57,28 @@ export const loadLogging = async ( replace(/\/\* project name \*\//, JSON.stringify(projectName)), ); +/** + * Loads the smoke-test script content for the initializer. + * + * Every framework shares the same *defaults/smokeTest.ts* template, so unlike + * {@link loadLogging} there is no per-framework template override. The + * template spawns the project's own dev server, so it needs the dev command + * for the chosen package manager baked in at generation time. + * + * @param param0 - {@link InitCommandData} containing `packageManager` + * @returns The complete smoke-test script content as a string + */ +export const loadTest = async ( + { packageManager }: InitCommandData, +) => + pipe( + await readTemplate("defaults/smokeTest.ts"), + replace( + /\/\* dev command \*\//, + JSON.stringify(getDevCommand(packageManager).split(" ")), + ), + ); + /** * Generates import statements for KV store and message queue dependencies. * Merges imports from both KV and MQ configurations and creates proper diff --git a/packages/init/src/templates/defaults/smokeTest.ts.tpl b/packages/init/src/templates/defaults/smokeTest.ts.tpl new file mode 100644 index 000000000..f744bb0bf --- /dev/null +++ b/packages/init/src/templates/defaults/smokeTest.ts.tpl @@ -0,0 +1,141 @@ +import { getDocumentLoader } from "@fedify/fedify"; +import { type Actor, isActor, lookupObject } from "@fedify/vocab"; +import { spawn } from "node:child_process"; + +const DEV_COMMAND: string[] = /* dev command */; +const HANDLE = "john"; +const STARTUP_TIMEOUT = 15_000; + +async function main(): Promise { + const [command, ...args] = DEV_COMMAND; + const server = spawn(command, args, { + stdio: ["ignore", "pipe", "pipe"], + detached: true, + }); + server.on("error", () => {}); + + const exitOnSignal = () => { + stopServer(server); + process.exit(1); + }; + process.once("SIGINT", exitOnSignal); + process.once("SIGTERM", exitOnSignal); + + let output = ""; + const collectOutput = (chunk: Buffer) => { + output += chunk.toString("utf8"); + }; + server.stdout?.on("data", collectOutput); + server.stderr?.on("data", collectOutput); + + try { + const port = await determinePort(server); + const target = `http://localhost:${port}/users/${HANDLE}`; + await waitForServer(target); + console.log(`Server is up at http://localhost:${port}.`); + const actor = await checkActor(target); + console.log(actor); + console.log(`Smoke test passed: ${target} resolved to an actor.`); + } catch (error) { + console.error("Smoke test failed:", error instanceof Error ? error.message : error); + if (output.trim() !== "") { + console.error(`\nDev server output:\n${output}`); + } + process.exitCode = 1; + } finally { + stopServer(server); + } +} + +function determinePort(server: ReturnType): Promise { + const portPatterns = [ + /listening on.*:(\d+)/i, + /server.*:(\d+)/i, + /port\s*:?\s*(\d+)/i, + /https?:\/\/localhost:(\d+)/i, + /https?:\/\/0\.0\.0\.0:(\d+)/i, + /https?:\/\/127\.0\.0\.1:(\d+)/i, + /https?:\/\/[^:]+:(\d+)/i, + ]; + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject( + new Error( + `Timeout: Could not determine port from server output within ${STARTUP_TIMEOUT}ms.`, + ), + ); + }, STARTUP_TIMEOUT); + + const onData = (chunk: Buffer) => { + const text = chunk.toString("utf8"); + for (const pattern of portPatterns) { + const match = text.match(pattern); + if (match && match[1]) { + const port = Number.parseInt(match[1], 10); + clearTimeout(timeout); + resolve(port); + return; + } + } + }; + + server.stdout?.on("data", onData); + server.stderr?.on("data", onData); + server.once("exit", (code) => { + clearTimeout(timeout); + reject(new Error(`The dev server exited early with code ${String(code)}.`)); + }); + }); +} + +async function waitForServer(url: string): Promise { + const startTime = Date.now(); + let lastStatus: number | undefined; + + while (Date.now() - startTime < STARTUP_TIMEOUT) { + try { + const response = await fetch(url, { + headers: { Accept: "application/activity+json" }, + signal: AbortSignal.timeout(1000), + }); + await response.body?.cancel(); + if (response.ok) return; + lastStatus = response.status; + } catch { + // Server not ready yet, continue waiting + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error( + `The server did not become ready within ${STARTUP_TIMEOUT}ms.` + + (lastStatus == null ? "" : ` Last response status: ${lastStatus}.`), + ); +} + +async function checkActor(url: string): Promise { + const object = await lookupObject(url, { + documentLoader: getDocumentLoader({ allowPrivateAddress: true }), + }); + if (object == null) { + throw new Error(`Could not resolve an actor at ${url}.`); + } + if (!isActor(object)) { + throw new Error(`Expected an actor at ${url}, but got a non-actor object.`); + } + return object; +} + +function stopServer(server: ReturnType): void { + try { + if (server.pid != null) process.kill(-server.pid, "SIGKILL"); + } catch { + // Process group already exited. + } + try { + server.kill("SIGKILL"); + } catch { + // Process already exited. + } +} + +await main(); diff --git a/packages/init/src/types.ts b/packages/init/src/types.ts index b750bd76a..aa77cb40b 100644 --- a/packages/init/src/types.ts +++ b/packages/init/src/types.ts @@ -86,6 +86,8 @@ export interface WebFrameworkInitializer { federationFile: string; /** Relative path where the logging configuration file will be created. */ loggingFile: string; + /** Relative path where the smoke-test script file will be created. */ + testFile: string; /** Optional template path for the logging configuration file. */ loggingTemplate?: string; /** diff --git a/packages/init/src/webframeworks/astro.ts b/packages/init/src/webframeworks/astro.ts index 1d6933a98..af32ef3d4 100644 --- a/packages/init/src/webframeworks/astro.ts +++ b/packages/init/src/webframeworks/astro.ts @@ -3,7 +3,12 @@ import deps from "../json/deps.json" with { type: "json" }; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { PackageManager, WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies } from "./const.ts"; -import { getInstruction, pmToRt } from "./utils.ts"; +import { + getInstruction, + getTestDependencies, + getTestTask, + pmToRt, +} from "./utils.ts"; const astroNodeBunDevDependencies = { "@fedify/lint": PACKAGE_VERSION, @@ -73,9 +78,11 @@ const astroDescription: WebFrameworkDescription = { "@types/node": deps["npm:@types/node@22"], } : {}), + ...getTestDependencies(pm), }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", + testFile: "scripts/smokeTest.ts", format: pm === "deno" ? undefined : { tool: "prettier" }, files: { "astro.config.ts": await readTemplate( @@ -130,17 +137,20 @@ const TASKS = { dev: `${astroDenoCommand} dev`, build: `${astroDenoCommand} build`, preview: `${astroDenoCommand} preview`, + test: getTestTask("deno"), }, "bun": { dev: "bunx --bun astro dev", build: "bunx --bun astro build", preview: "bun ./dist/server/entry.mjs", + test: getTestTask("bun"), ...astroNodeBunDevToolTasks, }, "node": { dev: "dotenvx run -- astro dev", build: "dotenvx run -- astro build", preview: "dotenvx run -- astro preview", + test: getTestTask("npm"), ...astroNodeBunDevToolTasks, }, }; diff --git a/packages/init/src/webframeworks/bare-bones.ts b/packages/init/src/webframeworks/bare-bones.ts index 894440ffb..a1fa7be2c 100644 --- a/packages/init/src/webframeworks/bare-bones.ts +++ b/packages/init/src/webframeworks/bare-bones.ts @@ -3,7 +3,12 @@ import deps from "../json/deps.json" with { type: "json" }; import { readTemplate } from "../lib.ts"; import type { WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, nodeBunDevToolTasks, pmToRt } from "./utils.ts"; +import { + getInstruction, + getTestTask, + nodeBunDevToolTasks, + pmToRt, +} from "./utils.ts"; const bareBonesDescription: WebFrameworkDescription = { label: "Bare-bones", @@ -19,6 +24,7 @@ const bareBonesDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", + testFile: "scripts/smokeTest.ts", files: { "src/main.ts": await readTemplate(`bare-bones/main/${pmToRt(pm)}.ts`), }, @@ -63,15 +69,18 @@ const TASKS = { deno: { dev: "deno run -A --watch ./src/main.ts", prod: "deno run -A ./src/main.ts", + test: getTestTask("deno"), }, bun: { dev: "bun run --hot ./src/main.ts", prod: "bun run ./src/main.ts", + test: getTestTask("bun"), ...nodeBunDevToolTasks, }, node: { dev: "dotenvx run -- tsx watch ./src/main.ts", prod: "dotenvx run -- node --import tsx ./src/main.ts", + test: getTestTask("npm"), ...nodeBunDevToolTasks, }, }; diff --git a/packages/init/src/webframeworks/elysia.ts b/packages/init/src/webframeworks/elysia.ts index 4334995a8..8a41db5f9 100644 --- a/packages/init/src/webframeworks/elysia.ts +++ b/packages/init/src/webframeworks/elysia.ts @@ -3,7 +3,12 @@ import deps from "../json/deps.json" with { type: "json" }; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, nodeBunDevToolTasks, pmToRt } from "./utils.ts"; +import { + getInstruction, + getTestTask, + nodeBunDevToolTasks, + pmToRt, +} from "./utils.ts"; const elysiaDescription: WebFrameworkDescription = { label: "ElysiaJS", @@ -41,6 +46,7 @@ const elysiaDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", + testFile: "scripts/smokeTest.ts", files: { "src/index.ts": (await readTemplate( `elysia/index/${pmToRt(pm)}.ts`, @@ -68,16 +74,19 @@ const TASKS = { dev: "deno serve --allow-read --allow-env --allow-net --watch ./src/index.ts", prod: "deno serve --allow-read --allow-env --allow-net ./src/index.ts", + test: getTestTask("deno"), }, bun: { dev: "bun run --hot ./src/index.ts", prod: "bun run ./src/index.ts", + test: getTestTask("bun"), ...nodeBunDevToolTasks, }, node: { dev: "dotenvx run -- tsx watch src/index.ts", build: "tsc src/index.ts --outDir dist", start: "NODE_ENV=production dotenvx run -- node dist/index.js", + test: getTestTask("npm"), ...nodeBunDevToolTasks, }, }; diff --git a/packages/init/src/webframeworks/express.ts b/packages/init/src/webframeworks/express.ts index 9a09c1a57..d7d9a3543 100644 --- a/packages/init/src/webframeworks/express.ts +++ b/packages/init/src/webframeworks/express.ts @@ -3,7 +3,12 @@ import deps from "../json/deps.json" with { type: "json" }; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, nodeBunDevToolTasks, pmToRt } from "./utils.ts"; +import { + getInstruction, + getTestTask, + nodeBunDevToolTasks, + pmToRt, +} from "./utils.ts"; const expressDescription: WebFrameworkDescription = { label: "Express", @@ -26,6 +31,7 @@ const expressDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", + testFile: "scripts/smokeTest.ts", files: { "src/app.ts": (await readTemplate("express/app.ts")) .replace(/\/\* logger \*\//, projectName), @@ -54,15 +60,18 @@ const TASKS = { "deno run --allow-read --allow-net --allow-env --allow-sys --watch ./src/index.ts", prod: "deno run --allow-read --allow-net --allow-env --allow-sys ./src/index.ts", + test: getTestTask("deno"), }, bun: { dev: "bun run --hot ./src/index.ts", prod: "bun run ./src/index.ts", + test: getTestTask("bun"), ...nodeBunDevToolTasks, }, node: { dev: "dotenvx run -- tsx watch ./src/index.ts", prod: "dotenvx run -- node --import tsx ./src/index.ts", + test: getTestTask("npm"), ...nodeBunDevToolTasks, }, }; diff --git a/packages/init/src/webframeworks/hono.ts b/packages/init/src/webframeworks/hono.ts index f10ffff2c..b600e2e06 100644 --- a/packages/init/src/webframeworks/hono.ts +++ b/packages/init/src/webframeworks/hono.ts @@ -5,7 +5,12 @@ import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { WebFrameworkDescription } from "../types.ts"; import { replace } from "../utils.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, nodeBunDevToolTasks, pmToRt } from "./utils.ts"; +import { + getInstruction, + getTestTask, + nodeBunDevToolTasks, + pmToRt, +} from "./utils.ts"; const honoDescription: WebFrameworkDescription = { label: "Hono", @@ -19,6 +24,7 @@ const honoDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", + testFile: "scripts/smokeTest.ts", files: { "src/app.tsx": pipe( await readTemplate("hono/app.tsx"), @@ -75,15 +81,18 @@ const TASKS = { deno: { dev: "deno run -A --watch ./src/index.ts", prod: "deno run -A ./src/index.ts", + test: getTestTask("deno"), }, bun: { dev: "bun run --hot ./src/index.ts", prod: "bun run ./src/index.ts", + test: getTestTask("bun"), ...nodeBunDevToolTasks, }, node: { dev: "dotenvx run -- tsx watch ./src/index.ts", prod: "dotenvx run -- node --import tsx ./src/index.ts", + test: getTestTask("npm"), ...nodeBunDevToolTasks, }, }; diff --git a/packages/init/src/webframeworks/next.ts b/packages/init/src/webframeworks/next.ts index 5c8e95bcc..6aa49e177 100644 --- a/packages/init/src/webframeworks/next.ts +++ b/packages/init/src/webframeworks/next.ts @@ -3,7 +3,12 @@ import deps from "../json/deps.json" with { type: "json" }; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { PackageManager, WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, getNodeBunDevToolTasks } from "./utils.ts"; +import { + getInstruction, + getNodeBunDevToolTasks, + getTestDependencies, + getTestTask, +} from "./utils.ts"; const nextDescription: WebFrameworkDescription = { label: "Next.js", @@ -23,9 +28,11 @@ const nextDescription: WebFrameworkDescription = { devDependencies: { "@types/node": deps["npm:@types/node@20"], ...defaultDevDependencies, + ...getTestDependencies(pm), }, federationFile: "federation/index.ts", loggingFile: "logging.ts", + testFile: "scripts/smokeTest.ts", format: { ignorePatterns: [".next/**"], }, @@ -33,7 +40,7 @@ const nextDescription: WebFrameworkDescription = { "instrumentation.ts": await readTemplate("next/instrumentation.ts"), "middleware.ts": await readTemplate("next/middleware.ts"), }, - tasks: getNodeBunDevToolTasks(pm), + tasks: { ...getNodeBunDevToolTasks(pm), test: getTestTask(pm) }, instruction: getInstruction(pm, 3000), }), }; diff --git a/packages/init/src/webframeworks/nitro.ts b/packages/init/src/webframeworks/nitro.ts index a7f3682da..3d6864d92 100644 --- a/packages/init/src/webframeworks/nitro.ts +++ b/packages/init/src/webframeworks/nitro.ts @@ -2,7 +2,12 @@ import { PACKAGE_MANAGER } from "../const.ts"; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { PackageManager, WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, getNodeBunDevToolTasks } from "./utils.ts"; +import { + getInstruction, + getNodeBunDevToolTasks, + getTestDependencies, + getTestTask, +} from "./utils.ts"; const nitroDescription: WebFrameworkDescription = { label: "Nitro", @@ -18,9 +23,13 @@ const nitroDescription: WebFrameworkDescription = { "@fedify/h3": PACKAGE_VERSION, ...(pm === "deno" && defaultDenoDependencies), }, - devDependencies: defaultDevDependencies, + devDependencies: { + ...defaultDevDependencies, + ...getTestDependencies(pm), + }, federationFile: "server/federation.ts", loggingFile: "server/logging.ts", + testFile: "scripts/smokeTest.ts", format: { ignorePatterns: [".output/**"], }, @@ -60,7 +69,7 @@ const nitroDescription: WebFrameworkDescription = { lib: ["ESNext", "DOM"], baseUrl: ".", }, - tasks: getNodeBunDevToolTasks(pm), + tasks: { ...getNodeBunDevToolTasks(pm), test: getTestTask(pm) }, instruction: getInstruction(pm, 3000), }), }; diff --git a/packages/init/src/webframeworks/nuxt.ts b/packages/init/src/webframeworks/nuxt.ts index ed66c3568..a7658586e 100644 --- a/packages/init/src/webframeworks/nuxt.ts +++ b/packages/init/src/webframeworks/nuxt.ts @@ -3,7 +3,12 @@ import deps from "../json/deps.json" with { type: "json" }; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { PackageManager, WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, getNodeBunDevToolTasks } from "./utils.ts"; +import { + getInstruction, + getNodeBunDevToolTasks, + getTestDependencies, + getTestTask, +} from "./utils.ts"; const nuxtDescription: WebFrameworkDescription = { label: "Nuxt", @@ -16,10 +21,12 @@ const nuxtDescription: WebFrameworkDescription = { ...defaultDevDependencies, "typescript": deps["npm:typescript"], "@types/node": deps["npm:@types/node@25"], + ...getTestDependencies(pm), }, federationFile: "server/federation.ts", loggingFile: "server/logging.ts", loggingTemplate: "nuxt/server/logging.ts", + testFile: "scripts/smokeTest.ts", format: { ignorePatterns: [".output/**"], }, @@ -30,7 +37,7 @@ const nuxtDescription: WebFrameworkDescription = { "nuxt/server/plugins/logging.ts", ), }, - tasks: getNodeBunDevToolTasks(pm), + tasks: { ...getNodeBunDevToolTasks(pm), test: getTestTask(pm) }, instruction: getInstruction(pm, 3000), }), }; diff --git a/packages/init/src/webframeworks/solidstart.ts b/packages/init/src/webframeworks/solidstart.ts index eae2a8ed9..36527a6f1 100644 --- a/packages/init/src/webframeworks/solidstart.ts +++ b/packages/init/src/webframeworks/solidstart.ts @@ -3,7 +3,13 @@ import deps from "../json/deps.json" with { type: "json" }; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, nodeBunDevToolTasks, pmToRt } from "./utils.ts"; +import { + getInstruction, + getTestDependencies, + getTestTask, + nodeBunDevToolTasks, + pmToRt, +} from "./utils.ts"; const NPM_SOLIDSTART = `npm:@solidjs/start@${deps["npm:@solidjs/start"]}`; const solidstartDescription: WebFrameworkDescription = { @@ -16,9 +22,11 @@ const solidstartDescription: WebFrameworkDescription = { ...defaultDevDependencies, typescript: deps["npm:typescript"], "@types/node": deps["npm:@types/node@22"], + ...getTestDependencies(pm), }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", + testFile: "scripts/smokeTest.ts", format: { ignorePatterns: [".solid/**", ".vinxi/**"], }, @@ -94,17 +102,20 @@ const TASKS = { dev: "deno run -A npm:vinxi dev", build: "deno run -A npm:vinxi build", start: "deno run -A npm:vinxi start", + test: getTestTask("deno"), }, bun: { dev: "bunx vinxi dev", build: "bunx vinxi build", start: "bunx vinxi start", + test: getTestTask("bun"), ...nodeBunDevToolTasks, }, node: { dev: "vinxi dev", build: "vinxi build", start: "dotenvx run -- vinxi start", + test: getTestTask("npm"), ...nodeBunDevToolTasks, }, }; diff --git a/packages/init/src/webframeworks/sveltekit.ts b/packages/init/src/webframeworks/sveltekit.ts index 5ea808cf2..d38fc89b6 100644 --- a/packages/init/src/webframeworks/sveltekit.ts +++ b/packages/init/src/webframeworks/sveltekit.ts @@ -3,7 +3,13 @@ import deps from "../json/deps.json" with { type: "json" }; import { PACKAGE_VERSION, readTemplate } from "../lib.ts"; import type { PackageManager, WebFrameworkDescription } from "../types.ts"; import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; -import { getInstruction, nodeBunDevToolTasks, pmToRt } from "./utils.ts"; +import { + getInstruction, + getTestDependencies, + getTestTask, + nodeBunDevToolTasks, + pmToRt, +} from "./utils.ts"; const sveltekitDescription: WebFrameworkDescription = { label: "SvelteKit", @@ -22,14 +28,18 @@ const sveltekitDescription: WebFrameworkDescription = { ...(pmToRt(pm) === "deno" ? {} : { "@dotenvx/dotenvx": deps["npm:@dotenvx/dotenvx"] }), + ...getTestDependencies(pm), }, federationFile: "src/lib/federation.ts", loggingFile: "src/lib/logging.ts", + testFile: "scripts/smokeTest.ts", env: testMode ? { HOST: "127.0.0.1" } : {} as Record, files: { "src/hooks.server.ts": await readTemplate("sveltekit/hooks.server.ts"), }, - tasks: pmToRt(pm) === "deno" ? {} : { ...TASKS }, + tasks: pmToRt(pm) === "deno" + ? { test: getTestTask("deno") } + : { ...TASKS, test: getTestTask(pm) }, instruction: getInstruction(pm, 5173), }), }; diff --git a/packages/init/src/webframeworks/utils.ts b/packages/init/src/webframeworks/utils.ts index 1448a81e0..cee82f585 100644 --- a/packages/init/src/webframeworks/utils.ts +++ b/packages/init/src/webframeworks/utils.ts @@ -1,6 +1,7 @@ import type { Message } from "@optique/core"; import { commandLine, message } from "@optique/core/message"; import { getDevCommand } from "../lib.ts"; +import deps from "../json/deps.json" with { type: "json" }; import type { PackageManager } from "../types.ts"; export const nodeBunDevToolTasks = { @@ -13,6 +14,30 @@ export const getNodeBunDevToolTasks = ( pm: PackageManager, ): Record => pm === "deno" ? {} : nodeBunDevToolTasks; +const SMOKE_TEST_FILE = "scripts/smokeTest.ts"; + +/** + * Returns the `test` task command that runs the generated smoke-test + * script (`WebFrameworkInitializer.testFile`) with the runtime matching the + * given package manager. + */ +export const getTestTask = (pm: PackageManager): string => + pmToRt(pm) === "deno" + ? `deno run -A ${SMOKE_TEST_FILE}` + : pmToRt(pm) === "bun" + ? `bun run ${SMOKE_TEST_FILE}` + : `tsx ${SMOKE_TEST_FILE}`; + +/** + * Returns the dev dependencies the `test` task needs beyond what the + * framework already declares. Node.js runs the smoke-test script through + * `tsx`; Deno and Bun execute TypeScript natively. + */ +export const getTestDependencies = ( + pm: PackageManager, +): Record => + pmToRt(pm) === "node" ? { tsx: deps["npm:tsx"] } : {}; + /** * Generates the post-initialization instruction message that shows * the user how to start the dev server and look up an actor.