From 12093e7246119bfbdfa6b3dfcbdbfd526bde00d0 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Wed, 18 Mar 2026 20:14:39 +0900 Subject: [PATCH] Fix `fedify init` crash on JSR/Deno distribution `@fedify/init` crashed when executed through the JSR/Deno distribution (e.g., `deno run -A jsr:@fedify/cli init`) because `import.meta.dirname` is `undefined` for remote JSR modules. Two fixes applied: - Made `PACKAGES_PATH` lazy by converting it to a `getPackagesPath()` function. This constant was eagerly computed at module top-level using `import.meta.dirname!`, causing an immediate crash on import. Since it is only used in test mode (which always runs locally), deferring the computation is safe. - Made `readTemplate()` async with a URL-based fallback. When `import.meta.dirname` is available (Node.js, local Deno, deno compile), the existing `readFileSync` path is used. When it is not (JSR remote execution), `import.meta.url` + `fetch()` is used to load template files from the JSR CDN. Fixes https://github.com/fedify-dev/fedify/issues/624 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGES.md | 11 ++++++ packages/init/src/action/configs.ts | 4 +-- packages/init/src/action/const.ts | 13 +++---- packages/init/src/action/deps.ts | 4 +-- packages/init/src/action/patch.ts | 6 ++-- packages/init/src/lib.ts | 29 +++++++++------ packages/init/src/types.ts | 2 +- packages/init/src/webframeworks.ts | 55 +++++++++++++++++------------ 8 files changed, 77 insertions(+), 47 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e6f222896..a29f9d47a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,17 @@ Version 2.0.6 To be released. +### @fedify/init + + - Fixed `fedify init` crashing when `@fedify/cli` or `@fedify/init` is + executed through the JSR/Deno distribution. `import.meta.dirname` is + `undefined` for remote JSR modules, so the template loading and + repository-relative path logic has been made safe for published JSR + execution. [[#624], [#633]] + +[#624]: https://github.com/fedify-dev/fedify/issues/624 +[#633]: https://github.com/fedify-dev/fedify/pull/633 + ### @fedify/vocab-runtime - Added `http://joinmastodon.org/ns` to preloaded JSON-LD contexts. diff --git a/packages/init/src/action/configs.ts b/packages/init/src/action/configs.ts index 2d58b87bd..f64aeda55 100644 --- a/packages/init/src/action/configs.ts +++ b/packages/init/src/action/configs.ts @@ -19,7 +19,7 @@ import vscodeSettings from "../json/vscode-settings.json" with { }; import type { InitCommandData } from "../types.ts"; import { merge } from "../utils.ts"; -import { PACKAGES_PATH } from "./const.ts"; +import { getPackagesPath } from "./const.ts"; import { getDependencies, getDevDependencies, joinDepsReg } from "./deps.ts"; /** @@ -66,7 +66,7 @@ const getLinks = < keys as (obj: object) => Iterable, filter((dep) => dep.includes("@fedify/")), map((dep) => dep.replace("@fedify/", "")), - map((dep) => joinPath(PACKAGES_PATH, dep)), + map((dep) => joinPath(getPackagesPath(), dep)), map((absolutePath) => realpathSync(absolutePath)), map((realAbsolutePath) => relative(realpathSync(dir), realAbsolutePath)), toArray, diff --git a/packages/init/src/action/const.ts b/packages/init/src/action/const.ts index 03885ef54..b5733d191 100644 --- a/packages/init/src/action/const.ts +++ b/packages/init/src/action/const.ts @@ -1,8 +1,9 @@ import { join as joinPath } from "node:path"; -export const PACKAGES_PATH = joinPath( - import.meta.dirname!, // action - "..", // src - "..", // init - "..", // packages -); +export const getPackagesPath = (): string => + joinPath( + import.meta.dirname!, // action + "..", // src + "..", // init + "..", // packages + ); diff --git a/packages/init/src/action/deps.ts b/packages/init/src/action/deps.ts index bbcd1cd9f..28dc21e80 100644 --- a/packages/init/src/action/deps.ts +++ b/packages/init/src/action/deps.ts @@ -11,7 +11,7 @@ import { join as joinPath } from "node:path"; import { merge, replace } from "../utils.ts"; import { PACKAGE_VERSION } from "../lib.ts"; import type { InitCommandData, PackageManager } from "../types.ts"; -import { PACKAGES_PATH } from "./const.ts"; +import { getPackagesPath } from "./const.ts"; import { isDeno } from "./utils.ts"; type Deps = Record; @@ -71,7 +71,7 @@ const convertFedifyToLocal = (name: string): string => pipe( name, replace("@fedify/", ""), - (pkg) => joinPath(PACKAGES_PATH, pkg), + (pkg) => joinPath(getPackagesPath(), pkg), ); /** Gathers all devDependencies required for the project based on the diff --git a/packages/init/src/action/patch.ts b/packages/init/src/action/patch.ts index 868fc3f7a..553cfca05 100644 --- a/packages/init/src/action/patch.ts +++ b/packages/init/src/action/patch.ts @@ -52,14 +52,14 @@ export const recommendPatchFiles = (data: InitCommandData) => * @param data - The initialization command data * @returns A record of file paths to their string content */ -const getFiles = < +const getFiles = async < T extends InitCommandData, >(data: T) => ({ - [data.initializer.federationFile]: loadFederation({ + [data.initializer.federationFile]: await loadFederation({ imports: getImports(data), ...data, }), - [data.initializer.loggingFile]: loadLogging(data), + [data.initializer.loggingFile]: await loadLogging(data), ".env": stringifyEnvs(data.env), ...data.initializer.files, }); diff --git a/packages/init/src/lib.ts b/packages/init/src/lib.ts index e8d43a806..f3b05f49f 100644 --- a/packages/init/src/lib.ts +++ b/packages/init/src/lib.ts @@ -82,17 +82,26 @@ export async function isPackageManagerAvailable( return false; } -export const readTemplate: (templatePath: string) => string = ( - templatePath, -) => - readFileSync( - joinPath( - import.meta.dirname!, - "templates", - ...(templatePath + ".tpl").split("/"), - ), - "utf8", +export const readTemplate = async ( + templatePath: string, +): Promise => { + const segments = (templatePath + ".tpl").split("/"); + if (import.meta.dirname) { + return readFileSync( + joinPath(import.meta.dirname, "templates", ...segments), + "utf8", + ); + } + const url = new URL( + ["templates", ...segments].join("/"), + import.meta.url, ); + const resp = await fetch(url); + if (!resp.ok) { + throw new Error(`Failed to fetch template: ${url}`); + } + return resp.text(); +}; export const getInstruction: ( packageManager: PackageManager, diff --git a/packages/init/src/types.ts b/packages/init/src/types.ts index 689c19128..42b284171 100644 --- a/packages/init/src/types.ts +++ b/packages/init/src/types.ts @@ -50,7 +50,7 @@ export interface WebFrameworkDescription { defaultPort: number; init( data: InitCommandOptions & { projectName: string; testMode: boolean }, - ): WebFrameworkInitializer; + ): WebFrameworkInitializer | Promise; } export interface MessageQueueDescription { diff --git a/packages/init/src/webframeworks.ts b/packages/init/src/webframeworks.ts index 612a232de..60b949cda 100644 --- a/packages/init/src/webframeworks.ts +++ b/packages/init/src/webframeworks.ts @@ -15,7 +15,7 @@ const webFrameworks: WebFrameworks = { hono: { label: "Hono", packageManagers: PACKAGE_MANAGER, - init: ({ projectName, packageManager: pm }) => ({ + init: async ({ projectName, packageManager: pm }) => ({ dependencies: pm === "deno" ? { ...defaultDenoDependencies, @@ -46,16 +46,17 @@ const webFrameworks: WebFrameworks = { loggingFile: "src/logging.ts", files: { "src/app.tsx": pipe( - "hono/app.tsx", - readTemplate, + await readTemplate("hono/app.tsx"), replace(/\/\* hono \*\//, pm === "deno" ? "@hono/hono" : "hono"), replace(/\/\* logger \*\//, projectName), ), - "src/index.ts": readTemplate( + "src/index.ts": await readTemplate( `hono/index/${packageManagerToRuntime(pm)}.ts`, ), ...(pm !== "deno" - ? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") } + ? { + "eslint.config.ts": await readTemplate("defaults/eslint.config.ts"), + } : {}), }, compilerOptions: pm === "deno" ? undefined : { @@ -90,7 +91,7 @@ const webFrameworks: WebFrameworks = { elysia: { label: "ElysiaJS", packageManagers: PACKAGE_MANAGER, - init: ({ projectName, packageManager: pm }) => ({ + init: async ({ projectName, packageManager: pm }) => ({ dependencies: pm === "deno" ? { ...defaultDenoDependencies, @@ -124,11 +125,13 @@ const webFrameworks: WebFrameworks = { federationFile: "src/federation.ts", loggingFile: "src/logging.ts", files: { - "src/index.ts": readTemplate( + "src/index.ts": (await readTemplate( `elysia/index/${packageManagerToRuntime(pm)}.ts`, - ).replace(/\/\* logger \*\//, projectName), + )).replace(/\/\* logger \*\//, projectName), ...(pm !== "deno" - ? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") } + ? { + "eslint.config.ts": await readTemplate("defaults/eslint.config.ts"), + } : {}), }, compilerOptions: pm === "deno" || pm === "bun" ? undefined : { @@ -164,7 +167,7 @@ const webFrameworks: WebFrameworks = { express: { label: "Express", packageManagers: PACKAGE_MANAGER, - init: ({ projectName, packageManager: pm }) => ({ + init: async ({ projectName, packageManager: pm }) => ({ dependencies: { "npm:express": "^4.19.2", "@fedify/express": PACKAGE_VERSION, @@ -181,11 +184,13 @@ const webFrameworks: WebFrameworks = { federationFile: "src/federation.ts", loggingFile: "src/logging.ts", files: { - "src/app.ts": readTemplate("express/app.ts") + "src/app.ts": (await readTemplate("express/app.ts")) .replace(/\/\* logger \*\//, projectName), - "src/index.ts": readTemplate("express/index.ts"), + "src/index.ts": await readTemplate("express/index.ts"), ...(pm !== "deno" - ? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") } + ? { + "eslint.config.ts": await readTemplate("defaults/eslint.config.ts"), + } : {}), }, compilerOptions: pm === "deno" ? undefined : { @@ -218,7 +223,7 @@ const webFrameworks: WebFrameworks = { nitro: { label: "Nitro", packageManagers: PACKAGE_MANAGER, - init: ({ packageManager: pm, testMode }) => ({ + init: async ({ packageManager: pm, testMode }) => ({ command: getNitroInitCommand(pm), dependencies: { "@fedify/h3": PACKAGE_VERSION, @@ -228,16 +233,18 @@ const webFrameworks: WebFrameworks = { federationFile: "server/federation.ts", loggingFile: "server/logging.ts", files: { - "server/middleware/federation.ts": readTemplate( + "server/middleware/federation.ts": await readTemplate( "nitro/server/middleware/federation.ts", ), - "server/error.ts": readTemplate("nitro/server/error.ts"), - "nitro.config.ts": readTemplate("nitro/nitro.config.ts"), + "server/error.ts": await readTemplate("nitro/server/error.ts"), + "nitro.config.ts": await readTemplate("nitro/nitro.config.ts"), ...( - testMode ? { ".env": readTemplate("nitro/.env.test") } : {} + testMode ? { ".env": await readTemplate("nitro/.env.test") } : {} ), ...(pm !== "deno" - ? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") } + ? { + "eslint.config.ts": await readTemplate("defaults/eslint.config.ts"), + } : {}), }, tasks: { @@ -250,7 +257,7 @@ const webFrameworks: WebFrameworks = { next: { label: "Next.js", packageManagers: PACKAGE_MANAGER, - init: ({ packageManager: pm }) => ({ + init: async ({ packageManager: pm }) => ({ label: "Next.js", command: getNextInitCommand(pm), dependencies: { @@ -264,9 +271,11 @@ const webFrameworks: WebFrameworks = { federationFile: "federation/index.ts", loggingFile: "logging.ts", files: { - "middleware.ts": readTemplate("next/middleware.ts"), + "middleware.ts": await readTemplate("next/middleware.ts"), ...(pm !== "deno" - ? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") } + ? { + "eslint.config.ts": await readTemplate("defaults/eslint.config.ts"), + } : {}), }, tasks: { @@ -276,7 +285,7 @@ const webFrameworks: WebFrameworks = { }), defaultPort: 3000, }, -} as const; +}; export default webFrameworks; const defaultDevDependencies = {