Skip to content

Commit 12093e7

Browse files
dahliaclaude
andcommitted
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 #624 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 50ac1c1 commit 12093e7

8 files changed

Lines changed: 77 additions & 47 deletions

File tree

CHANGES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Version 2.0.6
88

99
To be released.
1010

11+
### @fedify/init
12+
13+
- Fixed `fedify init` crashing when `@fedify/cli` or `@fedify/init` is
14+
executed through the JSR/Deno distribution. `import.meta.dirname` is
15+
`undefined` for remote JSR modules, so the template loading and
16+
repository-relative path logic has been made safe for published JSR
17+
execution. [[#624], [#633]]
18+
19+
[#624]: https://github.com/fedify-dev/fedify/issues/624
20+
[#633]: https://github.com/fedify-dev/fedify/pull/633
21+
1122
### @fedify/vocab-runtime
1223

1324
- Added `http://joinmastodon.org/ns` to preloaded JSON-LD contexts.

packages/init/src/action/configs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import vscodeSettings from "../json/vscode-settings.json" with {
1919
};
2020
import type { InitCommandData } from "../types.ts";
2121
import { merge } from "../utils.ts";
22-
import { PACKAGES_PATH } from "./const.ts";
22+
import { getPackagesPath } from "./const.ts";
2323
import { getDependencies, getDevDependencies, joinDepsReg } from "./deps.ts";
2424

2525
/**
@@ -66,7 +66,7 @@ const getLinks = <
6666
keys as (obj: object) => Iterable<string>,
6767
filter((dep) => dep.includes("@fedify/")),
6868
map((dep) => dep.replace("@fedify/", "")),
69-
map((dep) => joinPath(PACKAGES_PATH, dep)),
69+
map((dep) => joinPath(getPackagesPath(), dep)),
7070
map((absolutePath) => realpathSync(absolutePath)),
7171
map((realAbsolutePath) => relative(realpathSync(dir), realAbsolutePath)),
7272
toArray,

packages/init/src/action/const.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { join as joinPath } from "node:path";
22

3-
export const PACKAGES_PATH = joinPath(
4-
import.meta.dirname!, // action
5-
"..", // src
6-
"..", // init
7-
"..", // packages
8-
);
3+
export const getPackagesPath = (): string =>
4+
joinPath(
5+
import.meta.dirname!, // action
6+
"..", // src
7+
"..", // init
8+
"..", // packages
9+
);

packages/init/src/action/deps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { join as joinPath } from "node:path";
1111
import { merge, replace } from "../utils.ts";
1212
import { PACKAGE_VERSION } from "../lib.ts";
1313
import type { InitCommandData, PackageManager } from "../types.ts";
14-
import { PACKAGES_PATH } from "./const.ts";
14+
import { getPackagesPath } from "./const.ts";
1515
import { isDeno } from "./utils.ts";
1616

1717
type Deps = Record<string, string>;
@@ -71,7 +71,7 @@ const convertFedifyToLocal = (name: string): string =>
7171
pipe(
7272
name,
7373
replace("@fedify/", ""),
74-
(pkg) => joinPath(PACKAGES_PATH, pkg),
74+
(pkg) => joinPath(getPackagesPath(), pkg),
7575
);
7676

7777
/** Gathers all devDependencies required for the project based on the

packages/init/src/action/patch.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ export const recommendPatchFiles = (data: InitCommandData) =>
5252
* @param data - The initialization command data
5353
* @returns A record of file paths to their string content
5454
*/
55-
const getFiles = <
55+
const getFiles = async <
5656
T extends InitCommandData,
5757
>(data: T) => ({
58-
[data.initializer.federationFile]: loadFederation({
58+
[data.initializer.federationFile]: await loadFederation({
5959
imports: getImports(data),
6060
...data,
6161
}),
62-
[data.initializer.loggingFile]: loadLogging(data),
62+
[data.initializer.loggingFile]: await loadLogging(data),
6363
".env": stringifyEnvs(data.env),
6464
...data.initializer.files,
6565
});

packages/init/src/lib.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,26 @@ export async function isPackageManagerAvailable(
8282
return false;
8383
}
8484

85-
export const readTemplate: (templatePath: string) => string = (
86-
templatePath,
87-
) =>
88-
readFileSync(
89-
joinPath(
90-
import.meta.dirname!,
91-
"templates",
92-
...(templatePath + ".tpl").split("/"),
93-
),
94-
"utf8",
85+
export const readTemplate = async (
86+
templatePath: string,
87+
): Promise<string> => {
88+
const segments = (templatePath + ".tpl").split("/");
89+
if (import.meta.dirname) {
90+
return readFileSync(
91+
joinPath(import.meta.dirname, "templates", ...segments),
92+
"utf8",
93+
);
94+
}
95+
const url = new URL(
96+
["templates", ...segments].join("/"),
97+
import.meta.url,
9598
);
99+
const resp = await fetch(url);
100+
if (!resp.ok) {
101+
throw new Error(`Failed to fetch template: ${url}`);
102+
}
103+
return resp.text();
104+
};
96105

97106
export const getInstruction: (
98107
packageManager: PackageManager,

packages/init/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export interface WebFrameworkDescription {
5050
defaultPort: number;
5151
init(
5252
data: InitCommandOptions & { projectName: string; testMode: boolean },
53-
): WebFrameworkInitializer;
53+
): WebFrameworkInitializer | Promise<WebFrameworkInitializer>;
5454
}
5555

5656
export interface MessageQueueDescription {

packages/init/src/webframeworks.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const webFrameworks: WebFrameworks = {
1515
hono: {
1616
label: "Hono",
1717
packageManagers: PACKAGE_MANAGER,
18-
init: ({ projectName, packageManager: pm }) => ({
18+
init: async ({ projectName, packageManager: pm }) => ({
1919
dependencies: pm === "deno"
2020
? {
2121
...defaultDenoDependencies,
@@ -46,16 +46,17 @@ const webFrameworks: WebFrameworks = {
4646
loggingFile: "src/logging.ts",
4747
files: {
4848
"src/app.tsx": pipe(
49-
"hono/app.tsx",
50-
readTemplate,
49+
await readTemplate("hono/app.tsx"),
5150
replace(/\/\* hono \*\//, pm === "deno" ? "@hono/hono" : "hono"),
5251
replace(/\/\* logger \*\//, projectName),
5352
),
54-
"src/index.ts": readTemplate(
53+
"src/index.ts": await readTemplate(
5554
`hono/index/${packageManagerToRuntime(pm)}.ts`,
5655
),
5756
...(pm !== "deno"
58-
? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") }
57+
? {
58+
"eslint.config.ts": await readTemplate("defaults/eslint.config.ts"),
59+
}
5960
: {}),
6061
},
6162
compilerOptions: pm === "deno" ? undefined : {
@@ -90,7 +91,7 @@ const webFrameworks: WebFrameworks = {
9091
elysia: {
9192
label: "ElysiaJS",
9293
packageManagers: PACKAGE_MANAGER,
93-
init: ({ projectName, packageManager: pm }) => ({
94+
init: async ({ projectName, packageManager: pm }) => ({
9495
dependencies: pm === "deno"
9596
? {
9697
...defaultDenoDependencies,
@@ -124,11 +125,13 @@ const webFrameworks: WebFrameworks = {
124125
federationFile: "src/federation.ts",
125126
loggingFile: "src/logging.ts",
126127
files: {
127-
"src/index.ts": readTemplate(
128+
"src/index.ts": (await readTemplate(
128129
`elysia/index/${packageManagerToRuntime(pm)}.ts`,
129-
).replace(/\/\* logger \*\//, projectName),
130+
)).replace(/\/\* logger \*\//, projectName),
130131
...(pm !== "deno"
131-
? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") }
132+
? {
133+
"eslint.config.ts": await readTemplate("defaults/eslint.config.ts"),
134+
}
132135
: {}),
133136
},
134137
compilerOptions: pm === "deno" || pm === "bun" ? undefined : {
@@ -164,7 +167,7 @@ const webFrameworks: WebFrameworks = {
164167
express: {
165168
label: "Express",
166169
packageManagers: PACKAGE_MANAGER,
167-
init: ({ projectName, packageManager: pm }) => ({
170+
init: async ({ projectName, packageManager: pm }) => ({
168171
dependencies: {
169172
"npm:express": "^4.19.2",
170173
"@fedify/express": PACKAGE_VERSION,
@@ -181,11 +184,13 @@ const webFrameworks: WebFrameworks = {
181184
federationFile: "src/federation.ts",
182185
loggingFile: "src/logging.ts",
183186
files: {
184-
"src/app.ts": readTemplate("express/app.ts")
187+
"src/app.ts": (await readTemplate("express/app.ts"))
185188
.replace(/\/\* logger \*\//, projectName),
186-
"src/index.ts": readTemplate("express/index.ts"),
189+
"src/index.ts": await readTemplate("express/index.ts"),
187190
...(pm !== "deno"
188-
? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") }
191+
? {
192+
"eslint.config.ts": await readTemplate("defaults/eslint.config.ts"),
193+
}
189194
: {}),
190195
},
191196
compilerOptions: pm === "deno" ? undefined : {
@@ -218,7 +223,7 @@ const webFrameworks: WebFrameworks = {
218223
nitro: {
219224
label: "Nitro",
220225
packageManagers: PACKAGE_MANAGER,
221-
init: ({ packageManager: pm, testMode }) => ({
226+
init: async ({ packageManager: pm, testMode }) => ({
222227
command: getNitroInitCommand(pm),
223228
dependencies: {
224229
"@fedify/h3": PACKAGE_VERSION,
@@ -228,16 +233,18 @@ const webFrameworks: WebFrameworks = {
228233
federationFile: "server/federation.ts",
229234
loggingFile: "server/logging.ts",
230235
files: {
231-
"server/middleware/federation.ts": readTemplate(
236+
"server/middleware/federation.ts": await readTemplate(
232237
"nitro/server/middleware/federation.ts",
233238
),
234-
"server/error.ts": readTemplate("nitro/server/error.ts"),
235-
"nitro.config.ts": readTemplate("nitro/nitro.config.ts"),
239+
"server/error.ts": await readTemplate("nitro/server/error.ts"),
240+
"nitro.config.ts": await readTemplate("nitro/nitro.config.ts"),
236241
...(
237-
testMode ? { ".env": readTemplate("nitro/.env.test") } : {}
242+
testMode ? { ".env": await readTemplate("nitro/.env.test") } : {}
238243
),
239244
...(pm !== "deno"
240-
? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") }
245+
? {
246+
"eslint.config.ts": await readTemplate("defaults/eslint.config.ts"),
247+
}
241248
: {}),
242249
},
243250
tasks: {
@@ -250,7 +257,7 @@ const webFrameworks: WebFrameworks = {
250257
next: {
251258
label: "Next.js",
252259
packageManagers: PACKAGE_MANAGER,
253-
init: ({ packageManager: pm }) => ({
260+
init: async ({ packageManager: pm }) => ({
254261
label: "Next.js",
255262
command: getNextInitCommand(pm),
256263
dependencies: {
@@ -264,9 +271,11 @@ const webFrameworks: WebFrameworks = {
264271
federationFile: "federation/index.ts",
265272
loggingFile: "logging.ts",
266273
files: {
267-
"middleware.ts": readTemplate("next/middleware.ts"),
274+
"middleware.ts": await readTemplate("next/middleware.ts"),
268275
...(pm !== "deno"
269-
? { "eslint.config.ts": readTemplate("defaults/eslint.config.ts") }
276+
? {
277+
"eslint.config.ts": await readTemplate("defaults/eslint.config.ts"),
278+
}
270279
: {}),
271280
},
272281
tasks: {
@@ -276,7 +285,7 @@ const webFrameworks: WebFrameworks = {
276285
}),
277286
defaultPort: 3000,
278287
},
279-
} as const;
288+
};
280289
export default webFrameworks;
281290

282291
const defaultDevDependencies = {

0 commit comments

Comments
 (0)