From 81b44d9d533771328663267ba7639c10bf609dfa Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 8 Aug 2026 21:47:04 +0900 Subject: [PATCH 1/8] Generate a smoke-test script in `fedify init` projects Scaffolded projects had no quick way to confirm that their federation setup actually serves an actor. Verifying it meant starting the dev server by hand and looking an actor up separately. Added a smoke-test script that starts the dev server, reads the port, waits for the server to answer, and looks an actor up with `lookupObject()`. https://github.com/fedify-dev/fedify/issues/898 Assisted-by: Claude Code:claude-sonnet-5 --- packages/init/src/action/patch.ts | 9 +- packages/init/src/action/templates.ts | 24 ++- .../src/templates/defaults/smokeTest.ts.tpl | 141 ++++++++++++++++++ packages/init/src/types.ts | 2 + 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 packages/init/src/templates/defaults/smokeTest.ts.tpl 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 883d9f29d..556c6a55e 100644 --- a/packages/init/src/types.ts +++ b/packages/init/src/types.ts @@ -96,6 +96,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; /** From 02f8a796af8b624d906bbd10916af3e908b15f16 Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 8 Aug 2026 21:48:47 +0900 Subject: [PATCH 2/8] Add a `test` task to every `fedify init` framework Every framework now writes the smoke-test script and exposes it as a `test` task, so a scaffolded project can be verified with one command. The task runs the script with the runtime matching the package manager, and Node.js projects gain `tsx` as a dev dependency to execute it. https://github.com/fedify-dev/fedify/issues/898 Assisted-by: Claude Code:claude-sonnet-5 --- packages/init/src/webframeworks/astro.ts | 12 ++++++++- packages/init/src/webframeworks/bare-bones.ts | 11 +++++++- packages/init/src/webframeworks/elysia.ts | 11 +++++++- packages/init/src/webframeworks/express.ts | 11 +++++++- packages/init/src/webframeworks/hono.ts | 11 +++++++- packages/init/src/webframeworks/next.ts | 11 ++++++-- packages/init/src/webframeworks/nitro.ts | 15 ++++++++--- packages/init/src/webframeworks/nuxt.ts | 11 ++++++-- packages/init/src/webframeworks/solidstart.ts | 13 +++++++++- packages/init/src/webframeworks/sveltekit.ts | 14 +++++++++-- packages/init/src/webframeworks/utils.ts | 25 +++++++++++++++++++ 11 files changed, 130 insertions(+), 15 deletions(-) diff --git a/packages/init/src/webframeworks/astro.ts b/packages/init/src/webframeworks/astro.ts index a9d5d20b5..b407f2924 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, @@ -74,9 +79,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( @@ -131,17 +138,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 66f78f5bb..340aa89a2 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: "Elysia", @@ -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. From f36fb54126ef17f032f220d3099d3188997d9a07 Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 8 Aug 2026 21:56:01 +0900 Subject: [PATCH 3/8] Test that `fedify init` writes the smoke-test script Added a test covering that `patchFiles()` writes the script to the initializer's `testFile` path with the dev command baked in, and filled in `testFile` in the existing fixtures now that it is required. https://github.com/fedify-dev/fedify/issues/898 Assisted-by: Claude Code:claude-sonnet-5 --- packages/init/src/action/configs.test.ts | 1 + packages/init/src/action/patch.test.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) 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: {}, From 8bfb90a52e04008d136ab01ce4fc73cea23c699a Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 8 Aug 2026 22:00:57 +0900 Subject: [PATCH 4/8] Add @fedify/init changes (smoke-test task) in CHANGES.md https://github.com/fedify-dev/fedify/issues/898 Assisted-by: Claude Code:claude-sonnet-5 --- changes.d/init/smoke-test.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changes.d/init/smoke-test.md 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]] From b75f6f9735f5b139738796b439920bbfa5ab81dd Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 15 Aug 2026 18:08:19 +0900 Subject: [PATCH 5/8] Change file names to match the naming convention https://github.com/fedify-dev/fedify/issues/898 --- packages/init/src/action/templates.ts | 9 ++++++--- packages/init/src/webframeworks/astro.ts | 2 +- packages/init/src/webframeworks/bare-bones.ts | 2 +- packages/init/src/webframeworks/elysia.ts | 2 +- packages/init/src/webframeworks/express.ts | 2 +- packages/init/src/webframeworks/hono.ts | 2 +- packages/init/src/webframeworks/next.ts | 2 +- packages/init/src/webframeworks/nitro.ts | 2 +- packages/init/src/webframeworks/nuxt.ts | 2 +- packages/init/src/webframeworks/solidstart.ts | 2 +- packages/init/src/webframeworks/sveltekit.ts | 2 +- packages/init/src/webframeworks/utils.ts | 4 ++-- 12 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/init/src/action/templates.ts b/packages/init/src/action/templates.ts index 612522dff..dd12a9433 100644 --- a/packages/init/src/action/templates.ts +++ b/packages/init/src/action/templates.ts @@ -60,7 +60,7 @@ export const loadLogging = async ( /** * Loads the smoke-test script content for the initializer. * - * Every framework shares the same *defaults/smokeTest.ts* template, so unlike + * Every framework shares the same *defaults/smoke.test.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. @@ -72,10 +72,13 @@ export const loadTest = async ( { packageManager }: InitCommandData, ) => pipe( - await readTemplate("defaults/smokeTest.ts"), + await readTemplate("defaults/smoke.test.ts"), replace( /\/\* dev command \*\//, - JSON.stringify(getDevCommand(packageManager).split(" ")), + JSON.stringify(getDevCommand(packageManager).split(" ")).replaceAll( + ",", + ", ", + ), ), ); diff --git a/packages/init/src/webframeworks/astro.ts b/packages/init/src/webframeworks/astro.ts index b407f2924..a2b3386ee 100644 --- a/packages/init/src/webframeworks/astro.ts +++ b/packages/init/src/webframeworks/astro.ts @@ -83,7 +83,7 @@ const astroDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", format: pm === "deno" ? undefined : { tool: "prettier" }, files: { "astro.config.ts": await readTemplate( diff --git a/packages/init/src/webframeworks/bare-bones.ts b/packages/init/src/webframeworks/bare-bones.ts index a1fa7be2c..09719caa4 100644 --- a/packages/init/src/webframeworks/bare-bones.ts +++ b/packages/init/src/webframeworks/bare-bones.ts @@ -24,7 +24,7 @@ const bareBonesDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", files: { "src/main.ts": await readTemplate(`bare-bones/main/${pmToRt(pm)}.ts`), }, diff --git a/packages/init/src/webframeworks/elysia.ts b/packages/init/src/webframeworks/elysia.ts index 340aa89a2..253c84922 100644 --- a/packages/init/src/webframeworks/elysia.ts +++ b/packages/init/src/webframeworks/elysia.ts @@ -46,7 +46,7 @@ const elysiaDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", files: { "src/index.ts": (await readTemplate( `elysia/index/${pmToRt(pm)}.ts`, diff --git a/packages/init/src/webframeworks/express.ts b/packages/init/src/webframeworks/express.ts index d7d9a3543..ca85169bd 100644 --- a/packages/init/src/webframeworks/express.ts +++ b/packages/init/src/webframeworks/express.ts @@ -31,7 +31,7 @@ const expressDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", files: { "src/app.ts": (await readTemplate("express/app.ts")) .replace(/\/\* logger \*\//, projectName), diff --git a/packages/init/src/webframeworks/hono.ts b/packages/init/src/webframeworks/hono.ts index b600e2e06..519e5cf40 100644 --- a/packages/init/src/webframeworks/hono.ts +++ b/packages/init/src/webframeworks/hono.ts @@ -24,7 +24,7 @@ const honoDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", files: { "src/app.tsx": pipe( await readTemplate("hono/app.tsx"), diff --git a/packages/init/src/webframeworks/next.ts b/packages/init/src/webframeworks/next.ts index 6aa49e177..370a01917 100644 --- a/packages/init/src/webframeworks/next.ts +++ b/packages/init/src/webframeworks/next.ts @@ -32,7 +32,7 @@ const nextDescription: WebFrameworkDescription = { }, federationFile: "federation/index.ts", loggingFile: "logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", format: { ignorePatterns: [".next/**"], }, diff --git a/packages/init/src/webframeworks/nitro.ts b/packages/init/src/webframeworks/nitro.ts index 3d6864d92..138c4572b 100644 --- a/packages/init/src/webframeworks/nitro.ts +++ b/packages/init/src/webframeworks/nitro.ts @@ -29,7 +29,7 @@ const nitroDescription: WebFrameworkDescription = { }, federationFile: "server/federation.ts", loggingFile: "server/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", format: { ignorePatterns: [".output/**"], }, diff --git a/packages/init/src/webframeworks/nuxt.ts b/packages/init/src/webframeworks/nuxt.ts index a7658586e..0f40c6b06 100644 --- a/packages/init/src/webframeworks/nuxt.ts +++ b/packages/init/src/webframeworks/nuxt.ts @@ -26,7 +26,7 @@ const nuxtDescription: WebFrameworkDescription = { federationFile: "server/federation.ts", loggingFile: "server/logging.ts", loggingTemplate: "nuxt/server/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", format: { ignorePatterns: [".output/**"], }, diff --git a/packages/init/src/webframeworks/solidstart.ts b/packages/init/src/webframeworks/solidstart.ts index 36527a6f1..23e28d5c4 100644 --- a/packages/init/src/webframeworks/solidstart.ts +++ b/packages/init/src/webframeworks/solidstart.ts @@ -26,7 +26,7 @@ const solidstartDescription: WebFrameworkDescription = { }, federationFile: "src/federation.ts", loggingFile: "src/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", format: { ignorePatterns: [".solid/**", ".vinxi/**"], }, diff --git a/packages/init/src/webframeworks/sveltekit.ts b/packages/init/src/webframeworks/sveltekit.ts index d38fc89b6..d69ae64ee 100644 --- a/packages/init/src/webframeworks/sveltekit.ts +++ b/packages/init/src/webframeworks/sveltekit.ts @@ -32,7 +32,7 @@ const sveltekitDescription: WebFrameworkDescription = { }, federationFile: "src/lib/federation.ts", loggingFile: "src/lib/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", env: testMode ? { HOST: "127.0.0.1" } : {} as Record, files: { "src/hooks.server.ts": await readTemplate("sveltekit/hooks.server.ts"), diff --git a/packages/init/src/webframeworks/utils.ts b/packages/init/src/webframeworks/utils.ts index cee82f585..6f2075da1 100644 --- a/packages/init/src/webframeworks/utils.ts +++ b/packages/init/src/webframeworks/utils.ts @@ -1,7 +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 { getDevCommand } from "../lib.ts"; import type { PackageManager } from "../types.ts"; export const nodeBunDevToolTasks = { @@ -14,7 +14,7 @@ export const getNodeBunDevToolTasks = ( pm: PackageManager, ): Record => pm === "deno" ? {} : nodeBunDevToolTasks; -const SMOKE_TEST_FILE = "scripts/smokeTest.ts"; +const SMOKE_TEST_FILE = "scripts/smoke.test.ts"; /** * Returns the `test` task command that runs the generated smoke-test From c81fb8e704c5d13e38c993b0d3ebae5fc001c85f Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 15 Aug 2026 18:11:18 +0900 Subject: [PATCH 6/8] Update test files to apply changes https://github.com/fedify-dev/fedify/issues/898 --- packages/init/src/action/configs.test.ts | 6 +++--- packages/init/src/action/patch.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/init/src/action/configs.test.ts b/packages/init/src/action/configs.test.ts index c888ea843..e6a11ed0f 100644 --- a/packages/init/src/action/configs.test.ts +++ b/packages/init/src/action/configs.test.ts @@ -1,3 +1,4 @@ +import { message } from "@optique/core"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; @@ -5,11 +6,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { promisify } from "node:util"; -import { message } from "@optique/core"; import { kvStores, messageQueues, PACKAGE_VERSION } from "../lib.ts"; import type { InitCommandData } from "../types.ts"; -import bareBonesDescription from "../webframeworks/bare-bones.ts"; import astroDescription from "../webframeworks/astro.ts"; +import bareBonesDescription from "../webframeworks/bare-bones.ts"; import nextDescription from "../webframeworks/next.ts"; import nitroDescription from "../webframeworks/nitro.ts"; import nuxtDescription from "../webframeworks/nuxt.ts"; @@ -35,7 +35,7 @@ function createInitData(): InitCommandData { initializer: { federationFile: "federation.ts", loggingFile: "logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.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 9241166f0..b922a8169 100644 --- a/packages/init/src/action/patch.test.ts +++ b/packages/init/src/action/patch.test.ts @@ -97,10 +97,10 @@ test("patchFiles writes the smoke-test script", async () => { await patchFiles(createInitData(dir, false)); const testScript = await readFile( - join(dir, "scripts", "smokeTest.ts"), + join(dir, "scripts", "smoke.test.ts"), "utf8", ); - assert.match(testScript, /\["npm","run","dev"\]/); + assert.match(testScript, /\["npm", "run", "dev"\]/); }); }); @@ -123,7 +123,7 @@ function createInitData( initializer: { federationFile: "src/federation.ts", loggingFile: "src/logging.ts", - testFile: "scripts/smokeTest.ts", + testFile: "scripts/smoke.test.ts", instruction: message`done`, tasks: {}, compilerOptions: {}, From 70efe687aea000a81a031942173dd68c979ce544 Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 15 Aug 2026 18:38:48 +0900 Subject: [PATCH 7/8] Fix the generated smoke test to address review comments Running the smoke test on Windows exposed several problems in how it spawns the dev server, tears it down, and reads the port from its log: - Windows resolves package manager commands through `.cmd` shims and has no process groups, so the dev server is spawned through a shell there and `detached` is limited to POSIX. - `process.kill(-pid)` cannot work on Windows, so the whole process tree is terminated with `taskkill /T /F`. - The startup banner can arrive split across chunks, so the port is matched against each stream's accumulated output rather than a single chunk. - Dev servers colorize their startup banner, and Vite in particular emits the port in its own bold sequence, so `http://localhost:5173/` arrives with an escape between the colon and the digits which causes pattern matching failure. Escape sequences are now stripped before matching. - `Port 5173 is in use, trying another one...` matched the generic port pattern, so the script tested whichever server already held that port instead of the one it had just started. The matching pattern was removed. https://github.com/fedify-dev/fedify/issues/898 Assisted-by: Claude Code:Opus 5 --- .../{smokeTest.ts.tpl => smoke.test.ts.tpl} | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) rename packages/init/src/templates/defaults/{smokeTest.ts.tpl => smoke.test.ts.tpl} (72%) diff --git a/packages/init/src/templates/defaults/smokeTest.ts.tpl b/packages/init/src/templates/defaults/smoke.test.ts.tpl similarity index 72% rename from packages/init/src/templates/defaults/smokeTest.ts.tpl rename to packages/init/src/templates/defaults/smoke.test.ts.tpl index f744bb0bf..e98e01c7f 100644 --- a/packages/init/src/templates/defaults/smokeTest.ts.tpl +++ b/packages/init/src/templates/defaults/smoke.test.ts.tpl @@ -1,16 +1,20 @@ import { getDocumentLoader } from "@fedify/fedify"; import { type Actor, isActor, lookupObject } from "@fedify/vocab"; -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; +import type { Readable } from "node:stream"; const DEV_COMMAND: string[] = /* dev command */; const HANDLE = "john"; const STARTUP_TIMEOUT = 15_000; +const IS_WINDOWS = process.platform === "win32"; async function main(): Promise { const [command, ...args] = DEV_COMMAND; const server = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], - detached: true, + shell: IS_WINDOWS, + windowsHide: true, + detached: !IS_WINDOWS, }); server.on("error", () => {}); @@ -22,11 +26,14 @@ async function main(): Promise { process.once("SIGTERM", exitOnSignal); let output = ""; - const collectOutput = (chunk: Buffer) => { - output += chunk.toString("utf8"); + const collectOutput = (stream: Readable | null) => { + const decoder = new TextDecoder(); + stream?.on("data", (chunk: Buffer) => { + output += decoder.decode(chunk, { stream: true }); + }); }; - server.stdout?.on("data", collectOutput); - server.stderr?.on("data", collectOutput); + collectOutput(server.stdout); + collectOutput(server.stderr); try { const port = await determinePort(server); @@ -47,11 +54,14 @@ async function main(): Promise { } } +function stripEscape(text: string): string { + return text.replace(new RegExp("\\u001B\\[[0-9;]*[A-Za-z]", "g"), ""); +} + 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, @@ -66,21 +76,32 @@ function determinePort(server: ReturnType): Promise { ); }, STARTUP_TIMEOUT); - const onData = (chunk: Buffer) => { - const text = chunk.toString("utf8"); + const findPort = (text: string) => { for (const pattern of portPatterns) { const match = text.match(pattern); if (match && match[1]) { const port = Number.parseInt(match[1], 10); + if (port > 0 && port < 65536) return port; + } + } + return null; + }; + + const scan = (stream: Readable | null) => { + const decoder = new TextDecoder(); + let text = ""; + stream?.on("data", (chunk: Buffer) => { + text += decoder.decode(chunk, { stream: true }); + const port = findPort(stripEscape(text)); + if (port != null) { clearTimeout(timeout); resolve(port); - return; } - } + }); }; - server.stdout?.on("data", onData); - server.stderr?.on("data", onData); + scan(server.stdout); + scan(server.stderr); server.once("exit", (code) => { clearTimeout(timeout); reject(new Error(`The dev server exited early with code ${String(code)}.`)); @@ -126,8 +147,16 @@ async function checkActor(url: string): Promise { } function stopServer(server: ReturnType): void { + if (server.pid == null) return; + if (IS_WINDOWS) { + spawnSync("taskkill", ["/pid", String(server.pid), "/T", "/F"], { + stdio: "ignore", + windowsHide: true, + }); + return; + } try { - if (server.pid != null) process.kill(-server.pid, "SIGKILL"); + process.kill(-server.pid, "SIGKILL"); } catch { // Process group already exited. } From 1dbfccdec04e9043a369d4709c93a4f2b8eeae86 Mon Sep 17 00:00:00 2001 From: Palcimer Date: Sat, 15 Aug 2026 18:45:26 +0900 Subject: [PATCH 8/8] Update CHANGES.md https://github.com/fedify-dev/fedify/issues/898 --- CHANGES.md | 6 ++++++ changes.d/init/smoke-test.md | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f4ff61c7a..978ad182c 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], [#990] by Jang Hanarae\] - Added runtime version verification to `fedify init`. It checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a higher version required by a framework (such as Astro's Node.js 22.12), before @@ -189,10 +193,12 @@ 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 [#964]: https://github.com/fedify-dev/fedify/issues/964 [#981]: https://github.com/fedify-dev/fedify/pull/981 +[#990]: https://github.com/fedify-dev/fedify/pull/990 ### @fedify/interaction-controls diff --git a/changes.d/init/smoke-test.md b/changes.d/init/smoke-test.md index 372185a6e..29b6d3db8 100644 --- a/changes.d/init/smoke-test.md +++ b/changes.d/init/smoke-test.md @@ -1,4 +1,8 @@ +--- +links: + '#990': https://github.com/fedify-dev/fedify/pull/990 +--- - 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]] + and whenever the app changes afterwards. [[#898], [#990] by Jang Hanarae]