From 59bd37ef77ac8a6d52634637c92cd3995bba1e24 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Mon, 3 Aug 2026 21:07:43 +0900 Subject: [PATCH 1/7] Verify runtime version in `fedify init` Fedify has minimum runtime versions, and some frameworks require even newer ones. `fedify init` did not check this, so it could scaffold a project on a runtime below the minimum, which can then fail at runtime. Verify the selected runtime version before creating any files. The base minimums (Deno 2.0.0, Node.js 22.0.0, Bun 1.1.0) mirror the values declared by `@fedify/fedify` and are defined directly in `rt.json`. Frameworks with stricter requirements, such as Astro's Node.js 22.12, raise the minimum through `minRuntimeVersions`. Assisted-by: Claude Code:claude-opus-4-8 --- packages/init/src/ask/pm.ts | 83 ++++++++++++++--- packages/init/src/const.ts | 3 + packages/init/src/json/rt.json | 27 ++---- packages/init/src/lib.ts | 113 +++++++++++++++++++++++ packages/init/src/types.ts | 16 +++- packages/init/src/webframeworks/astro.ts | 1 + 6 files changed, 209 insertions(+), 34 deletions(-) diff --git a/packages/init/src/ask/pm.ts b/packages/init/src/ask/pm.ts index 342170152..17bd48348 100644 --- a/packages/init/src/ask/pm.ts +++ b/packages/init/src/ask/pm.ts @@ -2,8 +2,11 @@ import { pipe, when } from "@fxts/core"; import { select } from "@inquirer/prompts"; import { message } from "@optique/core/message"; import { print } from "@optique/run"; +import process from "node:process"; import { PACKAGE_MANAGER } from "../const.ts"; import { + checkAllRuntimes, + checkRuntimeRequirement, getInstallUrl, isPackageManagerAvailable, kvStores, @@ -11,8 +14,15 @@ import { packageManagers, runtimes, } from "../lib.ts"; -import type { PackageManager, WebFramework } from "../types.ts"; +import type { + PackageManager, + Runtime, + RuntimeCheck, + WebFramework, +} from "../types.ts"; +import { printErrorMessage } from "../utils.ts"; import webFrameworks from "../webframeworks/mod.ts"; +import { pmToRt } from "../webframeworks/utils.ts"; /** * Fills in the package manager by prompting the user if not provided. @@ -27,7 +37,31 @@ const fillPackageManager: // (options: T) => // Promise & { packageManager: PackageManager }> = // async ({ packageManager, ...options }) => { - const pm = packageManager ?? await askPackageManager(options.webFramework); + if (packageManager != null) { + const pm = packageManager; + if (!await isPackageManagerAvailable(pm)) { + noticeInstallUrl(pm); + process.exit(1); + } + const result = await checkRuntimeRequirement( + pmToRt(pm), + webFrameworks[options.webFramework].minRuntimeVersions, + ); + if (result.status === "ok") { + return ({ ...options, packageManager: pm }); + } + if (result.status === "missing") { + printErrorMessage`The runtime for package manager ${pm} is missing.`; + } + if (result.status === "unsupported") { + printErrorMessage`The runtime for package manager ${pm} is unsupported. Detected: ${result.detected}, Required: ${result.required}.`; + } + if (result.status === "malformed") { + printErrorMessage`The detected runtime version for package manager ${pm} is malformed.`; + } + process.exit(1); + } + const pm = await askPackageManager(options.webFramework); if (await isPackageManagerAvailable(pm)) { return ({ ...options, packageManager: pm }); } @@ -38,19 +72,44 @@ const fillPackageManager: // export default fillPackageManager; -const askPackageManager = (wf: WebFramework) => - select({ +const askPackageManager = async (wf: WebFramework) => { + const runtimeChecks = await checkAllRuntimes( + webFrameworks[wf].minRuntimeVersions, + ); + const choices = PACKAGE_MANAGER.map(choicePackageManager(wf, runtimeChecks)); + if (choices.every((choice) => choice.disabled)) { + printErrorMessage`No package manager with a supported runtime is available for ${ + webFrameworks[wf].label + }.`; + process.exit(1); + } + return select({ message: "Choose the package manager to use", - choices: PACKAGE_MANAGER.map(choicePackageManager(wf)), + choices, }); +}; -const choicePackageManager = (wf: WebFramework) => (value: PackageManager) => ({ - name: isWfSupportsPm(wf, value) - ? value - : `${value} (not supported with ${webFrameworks[wf].label})`, - value, - disabled: !isWfSupportsPm(wf, value), -}); +const choicePackageManager = + (wf: WebFramework, runtimeChecks: Record) => + (value: PackageManager) => { + const check = runtimeChecks[pmToRt(value)]; + const label = runtimes[pmToRt(value)].label; + const reason = !isWfSupportsPm(wf, value) + ? `not supported with ${webFrameworks[wf].label}` + : check.status === "unsupported" + ? `requires ${label} ${check.required} or later` + : check.status === "missing" + ? `requires ${label} which is not installed` + : check.status === "malformed" + ? `could not detect ${label} version` + : ""; + const disabled = !isWfSupportsPm(wf, value) || check.status !== "ok"; + return { + name: disabled ? `${value} (${reason})` : value, + value, + disabled, + }; + }; const isWfSupportsPm = ( wf: WebFramework, diff --git a/packages/init/src/const.ts b/packages/init/src/const.ts index af5cc2e71..a8b889915 100644 --- a/packages/init/src/const.ts +++ b/packages/init/src/const.ts @@ -1,5 +1,6 @@ import kv from "./json/kv.json" with { type: "json" }; import mq from "./json/mq.json" with { type: "json" }; +import rt from "./json/rt.json" with { type: "json" }; /** All supported package manager identifiers, in display order. */ export const PACKAGE_MANAGER = ["deno", "pnpm", "bun", "yarn", "npm"] as const; @@ -23,6 +24,8 @@ export const MESSAGE_QUEUE = Object.keys(mq) as readonly (keyof typeof mq)[]; /** All supported key-value store backend identifiers. */ export const KV_STORE = Object.keys(kv) as readonly (keyof typeof kv)[]; +/** All supported runtime identifiers. */ +export const RUNTIME = Object.keys(rt) as readonly (keyof typeof rt)[]; /** * External database services that need to be running for integration tests. * Used by the test suite to check service availability before running tests. diff --git a/packages/init/src/json/rt.json b/packages/init/src/json/rt.json index 68d546d72..7400a8e5a 100644 --- a/packages/init/src/json/rt.json +++ b/packages/init/src/json/rt.json @@ -5,7 +5,8 @@ "deno", "--version" ], - "outputPattern": "^deno\\s+\\d+\\.\\d+\\.\\d+\\b" + "outputPattern": "^deno\\s+(\\d+\\.\\d+\\.\\d+\\b)", + "minVersion": "2.0.0" }, "bun": { "label": "Bun", @@ -13,30 +14,16 @@ "bun", "--version" ], - "outputPattern": "^\\d+\\.\\d+\\.\\d+$" + "outputPattern": "^(\\d+\\.\\d+\\.\\d+$)", + "minVersion": "1.1.0" }, - "pnpm": { + "node": { "label": "Node.js", "checkCommand": [ "node", "--version" ], - "outputPattern": "^v\\d+\\.\\d+\\.\\d+$" - }, - "yarn": { - "label": "Node.js", - "checkCommand": [ - "node", - "--version" - ], - "outputPattern": "^v\\d+\\.\\d+\\.\\d+$" - }, - "npm": { - "label": "Node.js", - "checkCommand": [ - "node", - "--version" - ], - "outputPattern": "^v\\d+\\.\\d+\\.\\d+$" + "outputPattern": "^v(\\d+\\.\\d+\\.\\d+$)", + "minVersion": "22.0.0" } } diff --git a/packages/init/src/lib.ts b/packages/init/src/lib.ts index ab28a0a76..e848efe1d 100644 --- a/packages/init/src/lib.ts +++ b/packages/init/src/lib.ts @@ -16,6 +16,7 @@ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; import { dirname, join as joinPath } from "node:path"; import process from "node:process"; import metadata from "../deno.json" with { type: "json" }; +import { RUNTIME } from "./const.ts"; import kv from "./json/kv.json" with { type: "json" }; import mq from "./json/mq.json" with { type: "json" }; import pm from "./json/pm.json" with { type: "json" }; @@ -25,6 +26,8 @@ import type { MessageQueues, PackageManager, PackageManagers, + Runtime, + RuntimeCheck, Runtimes, } from "./types.ts"; import { CommandError, isNotFoundError, runSubCommand } from "./utils.ts"; @@ -180,6 +183,116 @@ async function isCommandAvailable( } } +/** + * Compares two dotted version strings segment by segment and returns whether + * `detected` is higher than or equal to `required`. + */ +export function verifyRuntimeVersion(detected: string, required: string) { + const detectedParts = detected.split(".").map(Number); + const requiredParts = required.split(".").map(Number); + + for ( + let i = 0; + i < Math.max(detectedParts.length, requiredParts.length); + i++ + ) { + const detectedPart = detectedParts[i] ?? 0; + const requiredPart = requiredParts[i] ?? 0; + if (detectedPart > requiredPart) { + return true; + } + if (detectedPart < requiredPart) { + return false; + } + } + return true; +} + +/** + * Runs a runtime's version command and classifies the result as `"ok"`, + * `"unsupported"`, `"missing"`, or `"malformed"` against its `minVersion`. + */ +export async function checkRuntimeVersion( + { checkCommand, outputPattern, minVersion }: { + checkCommand: [string, ...string[]]; + outputPattern: RegExp; + minVersion: string; + }, +): Promise< + | { status: "ok" | "unsupported"; detected: string; required: string } + | { status: "missing" | "malformed"; detected: null; required: string } +> { + try { + const { stdout } = await $`${checkCommand}`.stdout("piped").spawn(); + logger.debug( + "The stdout of the command {command} is: {stdout}", + { command: checkCommand, stdout }, + ); + const detected = outputPattern.exec(stdout.trim())?.[1] ?? null; + if (detected == null) { + return { status: "malformed", detected: null, required: minVersion }; + } + if (!verifyRuntimeVersion(detected, minVersion)) { + return { status: "unsupported", detected, required: minVersion }; + } + return { status: "ok", detected, required: minVersion }; + } catch (error) { + if (isNotFoundError(error)) { + return { status: "missing", detected: null, required: minVersion }; + } + logger.debug( + "The command {command} failed with the error: {error}", + { command: checkCommand, error }, + ); + throw error; + } +} + +/** + * Resolves the required version for `runtime` as the higher of its base minimum + * and an optional framework `override`. + */ +export function resolveRequiredVersion( + runtime: Runtime, + override?: string, +): string { + const base = runtimes[runtime].minVersion; + return override != null && verifyRuntimeVersion(override, base) + ? override + : base; +} + +/** + * Checks whether `runtime` meets its required version, applying framework + * `overrides` on top of the base minimum. + */ +export function checkRuntimeRequirement( + runtime: Runtime, + overrides: Partial> = {}, +): Promise { + return checkRuntimeVersion({ + ...runtimes[runtime], + minVersion: resolveRequiredVersion(runtime, overrides[runtime]), + }); +} + +/** + * Checks every supported runtime once and returns a map from each runtime + * identifier to its {@link checkRuntimeRequirement} result. + */ +export async function checkAllRuntimes( + overrides: Partial> = {}, +): Promise< + Record +> { + const checked = await Promise.all( + RUNTIME.map(async (runtime) => + [runtime, await checkRuntimeRequirement(runtime, overrides)] as const + ), + ); + return Object.fromEntries(checked) as Record; +} + /** * Creates a file at the given path with the given content, creating * any necessary parent directories along the way. diff --git a/packages/init/src/types.ts b/packages/init/src/types.ts index b750bd76a..3d0fef00b 100644 --- a/packages/init/src/types.ts +++ b/packages/init/src/types.ts @@ -4,10 +4,14 @@ import type { KV_STORE, MESSAGE_QUEUE, PACKAGE_MANAGER, + RUNTIME, WEB_FRAMEWORK, } from "./const.ts"; import type { RequiredNotNull } from "./utils.ts"; +/** Supported runtime identifiers: `"deno"`, `"bun"`, `"node"`. */ +export type Runtime = typeof RUNTIME[number]; + /** Supported package manager identifiers: `"deno"`, `"pnpm"`, `"bun"`, `"yarn"`, `"npm"`. */ export type PackageManager = typeof PACKAGE_MANAGER[number]; @@ -32,8 +36,13 @@ export type WebFrameworks = Record; /** A mapping from each {@link PackageManager} identifier to its description. */ export type PackageManagers = Record; -/** A mapping from each {@link PackageManager} identifier to its runtime description. */ -export type Runtimes = Record; +/** A mapping from each {@link Runtime} identifier to its description. */ +export type Runtimes = Record; + +/** The result of checking a runtime's installed version against its minimum. */ +export type RuntimeCheck = + | { status: "ok" | "unsupported"; detected: string; required: string } + | { status: "missing" | "malformed"; detected: null; required: string }; /** * Describes a JavaScript runtime (Deno, Node.js, or Bun) and how to check @@ -45,6 +54,7 @@ export interface RuntimeDescription { checkCommand: [string, ...string[]]; /** Regex to match against the command's stdout to confirm the runtime is installed. */ outputPattern: RegExp; + minVersion: string; } /** @@ -122,6 +132,8 @@ export interface WebFrameworkDescription { packageManagers: readonly PackageManager[]; /** Default port for the development server. */ defaultPort: number; + /** Minimum runtime versions this framework requires, if higher than Fedify's baseline. */ + minRuntimeVersions?: Partial>; /** * Factory function that returns the initializer configuration for this * framework, given the user's selected options. diff --git a/packages/init/src/webframeworks/astro.ts b/packages/init/src/webframeworks/astro.ts index 1d6933a98..a9d5d20b5 100644 --- a/packages/init/src/webframeworks/astro.ts +++ b/packages/init/src/webframeworks/astro.ts @@ -32,6 +32,7 @@ const astroDescription: WebFrameworkDescription = { label: "Astro", packageManagers: PACKAGE_MANAGER, defaultPort: 4321, + minRuntimeVersions: { node: "22.12.0" }, init: async ({ packageManager: pm }) => { // Astro loads integrations and middleware through Vite. Vite resolves // bare imports from node_modules rather than Deno's JSR import map, so From d3dedb8d52578d7934d0ecf881f0844500581775 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Mon, 3 Aug 2026 21:08:59 +0900 Subject: [PATCH 2/7] Add tests for runtime version verification Cover `verifyRuntimeVersion` for versions below, equal to, and above the minimum, and `resolveRequiredVersion` for framework overrides that raise the base minimum. Assisted-by: Claude Code:claude-opus-4-8 --- packages/init/src/lib.test.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/init/src/lib.test.ts b/packages/init/src/lib.test.ts index 4c1f40673..b97bd1c6f 100644 --- a/packages/init/src/lib.test.ts +++ b/packages/init/src/lib.test.ts @@ -3,7 +3,11 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { isDirectoryEmpty } from "./lib.ts"; +import { + isDirectoryEmpty, + resolveRequiredVersion, + verifyRuntimeVersion, +} from "./lib.ts"; import { runSubCommand } from "./utils.ts"; test("isDirectoryEmpty allows an unborn Git repository", async () => { @@ -149,6 +153,29 @@ test("isDirectoryEmpty rejects a .git file", async () => { }); }); +test("verifyRuntimeVersion accepts equal versions", () => { + strictEqual(verifyRuntimeVersion("2.0.0", "2.0.0"), true); + strictEqual(verifyRuntimeVersion("2", "2.0.0"), true); +}); + +test("verifyRuntimeVersion accepts higher versions", () => { + strictEqual(verifyRuntimeVersion("2.0.1", "2.0"), true); + strictEqual(verifyRuntimeVersion("2.1.0", "2.0.0"), true); + strictEqual(verifyRuntimeVersion("3.0.0", "2.0.0"), true); +}); + +test("verifyRuntimeVersion rejects lower versions", () => { + strictEqual(verifyRuntimeVersion("1.9.9", "2.0.0"), false); + strictEqual(verifyRuntimeVersion("2.0.1", "2.1.0"), false); + strictEqual(verifyRuntimeVersion("2.0.0", "2.0.1"), false); +}); + +test("resolveRequiredVersion raises the base minimum for stricter frameworks", () => { + strictEqual(resolveRequiredVersion("node", "22.12.0"), "22.12.0"); + strictEqual(resolveRequiredVersion("node", "21.0.0"), "22.0.0"); + strictEqual(resolveRequiredVersion("node", undefined), "22.0.0"); +}); + async function createUnbornGitRepository(dir: string): Promise { await mkdir(join(dir, ".git", "objects"), { recursive: true }); await mkdir(join(dir, ".git", "refs", "heads"), { recursive: true }); From c4cdc1cc329f63c5dfc7a26372dcc422aa4c650f Mon Sep 17 00:00:00 2001 From: userjmmm Date: Mon, 3 Aug 2026 21:19:03 +0900 Subject: [PATCH 3/7] Add changelog entry for runtime version check Assisted-by: Claude Code:claude-opus-4-8 --- CHANGES.md | 7 +++++++ changes.d/init/verify-runtime-version.md | 6 ++++++ 2 files changed, 13 insertions(+) create mode 100644 changes.d/init/verify-runtime-version.md diff --git a/CHANGES.md b/CHANGES.md index 73b65d407..46359f0ea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -175,6 +175,12 @@ To be released. ### @fedify/init + - 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 + generating a project. A missing, malformed, or unsupported runtime now + produces a clear error in non-interactive mode and disables the affected + package managers in interactive mode. [[#964] by Lee Jeongmin\] - 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`: @@ -185,6 +191,7 @@ To be released. [#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 ### @fedify/interaction-controls diff --git a/changes.d/init/verify-runtime-version.md b/changes.d/init/verify-runtime-version.md new file mode 100644 index 000000000..c3a249c70 --- /dev/null +++ b/changes.d/init/verify-runtime-version.md @@ -0,0 +1,6 @@ + - 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 + generating a project. A missing, malformed, or unsupported runtime now + produces a clear error in non-interactive mode and disables the affected + package managers in interactive mode. [[#964] by Lee Jeongmin] From 4e86690b77e6bca1c21264796e896842816246c4 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 5 Aug 2026 16:56:08 +0900 Subject: [PATCH 4/7] Add PR Reference to init changelog --- CHANGES.md | 3 ++- changes.d/init/verify-runtime-version.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 46359f0ea..e64d1f45b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -180,7 +180,7 @@ To be released. version required by a framework (such as Astro's Node.js 22.12), before generating a project. A missing, malformed, or unsupported runtime now produces a clear error in non-interactive mode and disables the affected - package managers in interactive mode. [[#964] by Lee Jeongmin\] + package managers in interactive mode. [[#964], [#981] by Lee Jeongmin\] - 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`: @@ -192,6 +192,7 @@ To be released. [#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/issues/981 ### @fedify/interaction-controls diff --git a/changes.d/init/verify-runtime-version.md b/changes.d/init/verify-runtime-version.md index c3a249c70..9ec20ae6d 100644 --- a/changes.d/init/verify-runtime-version.md +++ b/changes.d/init/verify-runtime-version.md @@ -3,4 +3,4 @@ version required by a framework (such as Astro's Node.js 22.12), before generating a project. A missing, malformed, or unsupported runtime now produces a clear error in non-interactive mode and disables the affected - package managers in interactive mode. [[#964] by Lee Jeongmin] + package managers in interactive mode. [[#964], [#981] by Lee Jeongmin] From dd6dc78df6d16b6746c870a63a1287a7a73067d9 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 5 Aug 2026 17:08:57 +0900 Subject: [PATCH 5/7] Remove unnecessary export and reuse RuntimeCheck type --- packages/init/src/lib.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/init/src/lib.ts b/packages/init/src/lib.ts index e848efe1d..bf4bb5ce4 100644 --- a/packages/init/src/lib.ts +++ b/packages/init/src/lib.ts @@ -212,16 +212,13 @@ export function verifyRuntimeVersion(detected: string, required: string) { * Runs a runtime's version command and classifies the result as `"ok"`, * `"unsupported"`, `"missing"`, or `"malformed"` against its `minVersion`. */ -export async function checkRuntimeVersion( +async function checkRuntimeVersion( { checkCommand, outputPattern, minVersion }: { checkCommand: [string, ...string[]]; outputPattern: RegExp; minVersion: string; }, -): Promise< - | { status: "ok" | "unsupported"; detected: string; required: string } - | { status: "missing" | "malformed"; detected: null; required: string } -> { +): Promise { try { const { stdout } = await $`${checkCommand}`.stdout("piped").spawn(); logger.debug( From af665c12d3525fbe69cb9b1333be90c95379f333 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 5 Aug 2026 18:32:49 +0900 Subject: [PATCH 6/7] Extract and reuse package-manager choices `askPackageManager` recomputed the runtime checks through `checkAllRuntimes` every time it built the prompt. Following the review, that computation was separated into `calculateChoices` to run once. When `fillPackageManager` already has a `packageManager`, it now looks the value up in `choices` instead of calling `checkRuntimeRequirement` again. `checkRuntimeRequirement` is no longer used outside `lib.ts`, so its export was removed. Assisted-by: Claude Code:claude-opus-4-8 --- packages/init/src/ask/pm.ts | 47 ++++++++++++++++--------------------- packages/init/src/lib.ts | 2 +- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/packages/init/src/ask/pm.ts b/packages/init/src/ask/pm.ts index 17bd48348..4fa4de621 100644 --- a/packages/init/src/ask/pm.ts +++ b/packages/init/src/ask/pm.ts @@ -1,12 +1,11 @@ import { pipe, when } from "@fxts/core"; import { select } from "@inquirer/prompts"; -import { message } from "@optique/core/message"; +import { message, optionName, text } from "@optique/core/message"; import { print } from "@optique/run"; import process from "node:process"; import { PACKAGE_MANAGER } from "../const.ts"; import { checkAllRuntimes, - checkRuntimeRequirement, getInstallUrl, isPackageManagerAvailable, kvStores, @@ -37,31 +36,21 @@ const fillPackageManager: // (options: T) => // Promise & { packageManager: PackageManager }> = // async ({ packageManager, ...options }) => { + const choices = await calculateChoices(options.webFramework); if (packageManager != null) { const pm = packageManager; + const choice = choices.find(({ value }) => value === pm)!; + if (choice.disabled != null) { + print(message`${optionName(choice.name)} ${text(choice.disabled)}`); + process.exit(1); + } if (!await isPackageManagerAvailable(pm)) { noticeInstallUrl(pm); process.exit(1); } - const result = await checkRuntimeRequirement( - pmToRt(pm), - webFrameworks[options.webFramework].minRuntimeVersions, - ); - if (result.status === "ok") { - return ({ ...options, packageManager: pm }); - } - if (result.status === "missing") { - printErrorMessage`The runtime for package manager ${pm} is missing.`; - } - if (result.status === "unsupported") { - printErrorMessage`The runtime for package manager ${pm} is unsupported. Detected: ${result.detected}, Required: ${result.required}.`; - } - if (result.status === "malformed") { - printErrorMessage`The detected runtime version for package manager ${pm} is malformed.`; - } - process.exit(1); + return ({ ...options, packageManager: pm }); } - const pm = await askPackageManager(options.webFramework); + const pm = await askPackageManager(choices); if (await isPackageManagerAvailable(pm)) { return ({ ...options, packageManager: pm }); } @@ -72,7 +61,7 @@ const fillPackageManager: // export default fillPackageManager; -const askPackageManager = async (wf: WebFramework) => { +const calculateChoices = async (wf: WebFramework) => { const runtimeChecks = await checkAllRuntimes( webFrameworks[wf].minRuntimeVersions, ); @@ -83,18 +72,23 @@ const askPackageManager = async (wf: WebFramework) => { }.`; process.exit(1); } - return select({ + return choices; +}; + +const askPackageManager = ( + choices: Awaited>, +) => + select({ message: "Choose the package manager to use", choices, }); -}; const choicePackageManager = (wf: WebFramework, runtimeChecks: Record) => (value: PackageManager) => { const check = runtimeChecks[pmToRt(value)]; const label = runtimes[pmToRt(value)].label; - const reason = !isWfSupportsPm(wf, value) + const disabled = !isWfSupportsPm(wf, value) ? `not supported with ${webFrameworks[wf].label}` : check.status === "unsupported" ? `requires ${label} ${check.required} or later` @@ -103,9 +97,8 @@ const choicePackageManager = : check.status === "malformed" ? `could not detect ${label} version` : ""; - const disabled = !isWfSupportsPm(wf, value) || check.status !== "ok"; - return { - name: disabled ? `${value} (${reason})` : value, + return disabled === "" ? { name: value, value } : { + name: value, value, disabled, }; diff --git a/packages/init/src/lib.ts b/packages/init/src/lib.ts index bf4bb5ce4..4de69f8f6 100644 --- a/packages/init/src/lib.ts +++ b/packages/init/src/lib.ts @@ -263,7 +263,7 @@ export function resolveRequiredVersion( * Checks whether `runtime` meets its required version, applying framework * `overrides` on top of the base minimum. */ -export function checkRuntimeRequirement( +function checkRuntimeRequirement( runtime: Runtime, overrides: Partial> = {}, ): Promise { From 1dd33c56de235a05579d5552d712ca27be5aae01 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 5 Aug 2026 18:51:37 +0900 Subject: [PATCH 7/7] Use a loop for the package-manager prompt retry When the chosen package manager was not installed, `fillPackageManager` recursed to prompt again, recomputing `calculateChoices` on every retry, so replace the recursion with a loop that reuses the `choices` computed once at the start. Assisted-by: Claude Code:claude-opus-4-8 --- packages/init/src/ask/pm.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/init/src/ask/pm.ts b/packages/init/src/ask/pm.ts index 4fa4de621..b2448afb4 100644 --- a/packages/init/src/ask/pm.ts +++ b/packages/init/src/ask/pm.ts @@ -50,13 +50,13 @@ const fillPackageManager: // } return ({ ...options, packageManager: pm }); } - const pm = await askPackageManager(choices); - if (await isPackageManagerAvailable(pm)) { - return ({ ...options, packageManager: pm }); + while (true) { + const pm = await askPackageManager(choices); + if (await isPackageManagerAvailable(pm)) { + return ({ ...options, packageManager: pm }); + } + noticeInstallUrl(pm); } - noticeInstallUrl(pm); - return await fillPackageManager(options) as // - typeof options & { packageManager: PackageManager }; }; export default fillPackageManager;