@fedify/init 2.2.0-pr.715.28 → 2.2.0-pr.731.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,8 @@ Supported options
25
25
 
26
26
  The initializer supports the following project configurations:
27
27
 
28
- - **Web frameworks**: Bare-bones, [Hono], [Nitro], [Next.js], [Elysia], [Express]
28
+ - **Web frameworks**: Bare-bones, [Hono], [Nitro], [Next.js], [Elysia],
29
+ [Express]
29
30
  - **Package managers**: Deno, pnpm, Bun, Yarn, npm
30
31
  - **Key-value stores**: In-Memory, Deno KV, Redis, PostgreSQL
31
32
  - **Message queues**: In-Process, Deno KV, Redis, PostgreSQL, AMQP
@@ -4,7 +4,7 @@ import { makeDirIfHyd } from "./dir.js";
4
4
  import { drawDinosaur, noticeHowToRun, noticeOptions, noticePrecommand } from "./notice.js";
5
5
  import recommendConfigEnv from "./env.js";
6
6
  import { hasCommand, installDependencies, isDry, runPrecommand } from "./utils.js";
7
- import { patchFiles, recommendPatchFiles } from "./patch.js";
7
+ import { assertNoGeneratedFileConflicts, patchFiles, recommendPatchFiles } from "./patch.js";
8
8
  import recommendDependencies from "./recommend.js";
9
9
  import setData from "./set.js";
10
10
  import { pipe, tap, unless, when } from "@fxts/core";
@@ -28,6 +28,6 @@ import process from "node:process";
28
28
  const runInit = (options) => pipe(options, tap(drawDinosaur), setTestMode, askOptions, tap(noticeOptions), setData, when(isDry, handleDryRun), unless(isDry, handleHydRun), tap(recommendConfigEnv), tap(noticeHowToRun));
29
29
  const setTestMode = set("testMode", () => Boolean(process.env["FEDIFY_TEST_MODE"]));
30
30
  const handleDryRun = (data) => pipe(data, tap(when(hasCommand, noticePrecommand)), tap(recommendPatchFiles), tap(recommendDependencies));
31
- const handleHydRun = (data) => pipe(data, tap(makeDirIfHyd), tap(when(hasCommand, runPrecommand)), tap(patchFiles), tap(installDependencies));
31
+ const handleHydRun = (data) => pipe(data, tap(makeDirIfHyd), tap(assertNoGeneratedFileConflicts), tap(when(hasCommand, runPrecommand)), tap(patchFiles), tap(installDependencies));
32
32
  //#endregion
33
33
  export { runInit as default };
@@ -6,8 +6,33 @@ import { devToolConfigs, loadDenoConfig, loadPackageJson, loadTsConfig } from ".
6
6
  import { getImports, loadFederation, loadLogging } from "./templates.js";
7
7
  import { always, apply, entries, map, pipe, pipeLazy, tap } from "@fxts/core";
8
8
  import { toMerged } from "es-toolkit";
9
- import { readFile } from "node:fs/promises";
9
+ import { access, readFile } from "node:fs/promises";
10
+ import { join as join$1 } from "node:path";
10
11
  //#region src/action/patch.ts
12
+ const jsonsCache = /* @__PURE__ */ new Map();
13
+ const getJsonsCacheKey = (data) => JSON.stringify({
14
+ dir: data.dir,
15
+ packageManager: data.packageManager,
16
+ dryRun: data.dryRun,
17
+ testMode: data.testMode,
18
+ env: data.env,
19
+ initializer: {
20
+ compilerOptions: data.initializer.compilerOptions ?? {},
21
+ dependencies: data.initializer.dependencies ?? {},
22
+ devDependencies: data.initializer.devDependencies ?? {},
23
+ tasks: data.initializer.tasks ?? {}
24
+ },
25
+ kv: {
26
+ dependencies: data.kv.dependencies ?? {},
27
+ denoUnstable: data.kv.denoUnstable ?? [],
28
+ devDependencies: data.kv.devDependencies ?? {}
29
+ },
30
+ mq: {
31
+ dependencies: data.mq.dependencies ?? {},
32
+ denoUnstable: data.mq.denoUnstable ?? [],
33
+ devDependencies: data.mq.devDependencies ?? {}
34
+ }
35
+ });
11
36
  /**
12
37
  * Main function that initializes the project by creating necessary files and configurations.
13
38
  * Handles both dry-run mode (recommending files) and actual file creation.
@@ -20,6 +45,24 @@ import { readFile } from "node:fs/promises";
20
45
  const patchFiles = (data) => pipe(data, set("files", getFiles), set("jsons", getJsons), createFiles);
21
46
  const recommendPatchFiles = (data) => pipe(data, set("files", getFiles), set("jsons", getJsons), recommendFiles);
22
47
  /**
48
+ * Verifies that `--allow-non-empty` will not modify files that already
49
+ * existed before any framework scaffolding command runs. This only covers
50
+ * files that Fedify writes itself; framework scaffolders may still reject
51
+ * unrelated pre-existing files independently.
52
+ */
53
+ async function assertNoGeneratedFileConflicts(data) {
54
+ if (!data.allowNonEmpty) return;
55
+ const conflicts = await getExistingGeneratedFiles(data);
56
+ if (conflicts.length > 0) throw new GeneratedFileConflictError(conflicts);
57
+ }
58
+ var GeneratedFileConflictError = class extends Error {
59
+ constructor(conflicts) {
60
+ super(formatConflictMessage(conflicts));
61
+ this.conflicts = conflicts;
62
+ this.name = "GeneratedFileConflictError";
63
+ }
64
+ };
65
+ /**
23
66
  * Generates text-based files (TypeScript, environment files) for the project.
24
67
  * Creates federation configuration, logging setup, environment variables, and
25
68
  * framework-specific files by processing templates and combining them with
@@ -46,17 +89,57 @@ const getFiles = async (data) => ({
46
89
  * @param data - The initialization command data
47
90
  * @returns A record of file paths to their JSON object content
48
91
  */
49
- const getJsons = (data) => data.packageManager === "deno" ? {
50
- "deno.json": loadDenoConfig(data).data,
51
- [devToolConfigs["vscSetDeno"].path]: devToolConfigs["vscSetDeno"].data,
52
- [devToolConfigs["vscExtDeno"].path]: devToolConfigs["vscExtDeno"].data
53
- } : {
54
- ...data.initializer.compilerOptions ? { "tsconfig.json": loadTsConfig(data).data } : {},
55
- "package.json": loadPackageJson(data).data,
56
- [devToolConfigs["biome"].path]: devToolConfigs["biome"].data,
57
- [devToolConfigs["vscSet"].path]: devToolConfigs["vscSet"].data,
58
- [devToolConfigs["vscExt"].path]: devToolConfigs["vscExt"].data
92
+ const getJsons = (data) => {
93
+ const cacheKey = getJsonsCacheKey(data);
94
+ const cached = jsonsCache.get(cacheKey);
95
+ if (cached != null) return cached;
96
+ const jsons = data.packageManager === "deno" ? {
97
+ "deno.json": loadDenoConfig(data).data,
98
+ [devToolConfigs["vscSetDeno"].path]: devToolConfigs["vscSetDeno"].data,
99
+ [devToolConfigs["vscExtDeno"].path]: devToolConfigs["vscExtDeno"].data
100
+ } : {
101
+ ...data.initializer.compilerOptions ? { "tsconfig.json": loadTsConfig(data).data } : {},
102
+ "package.json": loadPackageJson(data).data,
103
+ [devToolConfigs["biome"].path]: devToolConfigs["biome"].data,
104
+ [devToolConfigs["vscSet"].path]: devToolConfigs["vscSet"].data,
105
+ [devToolConfigs["vscExt"].path]: devToolConfigs["vscExt"].data
106
+ };
107
+ jsonsCache.set(cacheKey, jsons);
108
+ return jsons;
109
+ };
110
+ /**
111
+ * Returns only the file paths written directly by Fedify after any framework
112
+ * scaffolding command finishes. Files created by
113
+ * `WebFrameworkInitializer.command` are intentionally excluded.
114
+ */
115
+ const getGeneratedFilePaths = (data) => [
116
+ data.initializer.federationFile,
117
+ data.initializer.loggingFile,
118
+ ".env",
119
+ ...Object.keys(data.initializer.files ?? {}),
120
+ ...Object.keys(getJsons(data))
121
+ ];
122
+ const getExistingGeneratedFiles = async (data) => {
123
+ const paths = [...new Set(getGeneratedFilePaths(data))];
124
+ return (await Promise.all(paths.map(async (path) => {
125
+ return await pathExists(join$1(data.dir, path)) ? path : null;
126
+ }))).filter((path) => path != null);
127
+ };
128
+ const pathExists = async (path) => {
129
+ try {
130
+ await access(path);
131
+ return true;
132
+ } catch (e) {
133
+ throwUnlessNotExists(e);
134
+ return false;
135
+ }
59
136
  };
137
+ const formatConflictMessage = (conflicts) => [
138
+ "Cannot initialize in a non-empty directory because these generated files",
139
+ "already exist:",
140
+ ...conflicts.map((path) => ` - ${path}`),
141
+ "Remove the conflicting files or choose another directory."
142
+ ].join("\n");
60
143
  /**
61
144
  * Handles dry-run mode by recommending files to be created without actually
62
145
  * creating them.
@@ -138,4 +221,4 @@ const appendText = (prev, data) => prev ? `${prev}\n${data}` : data;
138
221
  */
139
222
  const readFileIfExists = (path) => readFile(path, "utf8").catch(pipeLazy(tap(throwUnlessNotExists), always("")));
140
223
  //#endregion
141
- export { patchFiles, recommendPatchFiles };
224
+ export { assertNoGeneratedFileConflicts, patchFiles, recommendPatchFiles };
@@ -15,13 +15,19 @@ import { toMerged } from "es-toolkit";
15
15
  */
16
16
  const loadFederation = async ({ imports, projectName, kv, mq, packageManager }) => pipe(await readTemplate("defaults/federation.ts"), replace(/\/\* imports \*\//, imports), replace(/\/\* logger \*\//, JSON.stringify(projectName)), replace(/\/\* kv \*\//, convertEnv(kv.object, packageManager)), replace(/\/\* queue \*\//, convertEnv(mq.object, packageManager)));
17
17
  /**
18
- * Loads the logging configuration file content from template.
19
- * Reads the default logging template and replaces the project name placeholder.
18
+ * Loads logging configuration file content for the initializer.
20
19
  *
21
- * @param param0 - Destructured object containing the project name
20
+ * `loadLogging` accepts the full {@link InitCommandData} so it can read the
21
+ * project name and the framework initializer. It uses {@link readTemplate} to
22
+ * read `initializer.loggingTemplate` when provided, or falls back to
23
+ * *defaults/logging.ts*, then replaces the project name placeholder.
24
+ *
25
+ * @param param0 - {@link InitCommandData} containing `projectName` and
26
+ * `initializer`; `initializer.loggingTemplate` selects a framework-specific
27
+ * logging template when present.
22
28
  * @returns The complete logging configuration file content as a string
23
29
  */
24
- const loadLogging = async ({ projectName }) => pipe(await readTemplate("defaults/logging.ts"), replace(/\/\* project name \*\//, JSON.stringify(projectName)));
30
+ const loadLogging = async ({ projectName, initializer }) => pipe(await readTemplate(initializer.loggingTemplate ?? "defaults/logging.ts"), replace(/\/\* project name \*\//, JSON.stringify(projectName)));
25
31
  /**
26
32
  * Generates import statements for KV store and message queue dependencies.
27
33
  * Merges imports from both KV and MQ configurations and creates proper
package/dist/ask/dir.js CHANGED
@@ -18,6 +18,10 @@ import toggle from "inquirer-toggle";
18
18
  */
19
19
  const fillDir = async (options) => {
20
20
  const dir = options.dir ?? await askDir(getCwd());
21
+ if (options.allowNonEmpty) return {
22
+ ...options,
23
+ dir
24
+ };
21
25
  return await askIfNonEmpty(dir) ? {
22
26
  ...options,
23
27
  dir
package/dist/command.d.ts CHANGED
@@ -5,7 +5,7 @@ import { InferValue } from "@optique/core";
5
5
  /**
6
6
  * The `@optique/core` option schema for the `fedify init` command.
7
7
  * Defines `dir`, `webFramework`, `packageManager`, `kvStore`, `messageQueue`,
8
- * and `dryRun` options that the CLI parser will accept.
8
+ * `dryRun`, and `allowNonEmpty` options that the CLI parser will accept.
9
9
  */
10
10
  declare const initOptions: _$_optique_core0.Parser<"sync", {
11
11
  readonly dir: string;
@@ -14,6 +14,7 @@ declare const initOptions: _$_optique_core0.Parser<"sync", {
14
14
  readonly kvStore: "postgres" | "mysql" | "in-memory" | "redis" | "denokv";
15
15
  readonly messageQueue: "postgres" | "mysql" | "redis" | "denokv" | "in-process" | "amqp";
16
16
  readonly dryRun: boolean;
17
+ readonly allowNonEmpty: boolean;
17
18
  }, {
18
19
  readonly dir: [_$_optique_core0.ValueParserResult<string>];
19
20
  readonly webFramework: [_$_optique_core0.ValueParserResult<"bare-bones" | "hono" | "nitro" | "next" | "elysia" | "astro" | "express" | "nuxt" | "solidstart">];
@@ -21,6 +22,7 @@ declare const initOptions: _$_optique_core0.Parser<"sync", {
21
22
  readonly kvStore: [_$_optique_core0.ValueParserResult<"postgres" | "mysql" | "in-memory" | "redis" | "denokv">];
22
23
  readonly messageQueue: [_$_optique_core0.ValueParserResult<"postgres" | "mysql" | "redis" | "denokv" | "in-process" | "amqp">];
23
24
  readonly dryRun: _$_optique_core0.ValueParserResult<boolean>;
25
+ readonly allowNonEmpty: _$_optique_core0.ValueParserResult<boolean>;
24
26
  }>;
25
27
  /**
26
28
  * The `fedify init` CLI command parser.
@@ -32,6 +34,7 @@ declare const initCommand: _$_optique_core0.Parser<"sync", {
32
34
  readonly kvStore: "postgres" | "mysql" | "in-memory" | "redis" | "denokv";
33
35
  readonly messageQueue: "postgres" | "mysql" | "redis" | "denokv" | "in-process" | "amqp";
34
36
  readonly dryRun: boolean;
37
+ readonly allowNonEmpty: boolean;
35
38
  } & {
36
39
  readonly command: "init";
37
40
  }, ["matched", string] | ["parsing", Record<string | symbol, unknown>]>;
package/dist/command.js CHANGED
@@ -9,7 +9,7 @@ const messageQueue = optional(option("-m", "--message-queue", choice(MESSAGE_QUE
9
9
  /**
10
10
  * The `@optique/core` option schema for the `fedify init` command.
11
11
  * Defines `dir`, `webFramework`, `packageManager`, `kvStore`, `messageQueue`,
12
- * and `dryRun` options that the CLI parser will accept.
12
+ * `dryRun`, and `allowNonEmpty` options that the CLI parser will accept.
13
13
  */
14
14
  const initOptions = object("Initialization options", {
15
15
  dir: optional(argument(path({ metavar: "DIR" }), { description: message`The project directory to initialize. If a specified directory does not exist, it will be created.` })),
@@ -17,7 +17,8 @@ const initOptions = object("Initialization options", {
17
17
  packageManager,
18
18
  kvStore,
19
19
  messageQueue,
20
- dryRun: option("--dry-run", { description: message`Perform a trial run with no changes made.` })
20
+ dryRun: option("--dry-run", { description: message`Perform a trial run with no changes made.` }),
21
+ allowNonEmpty: option("--allow-non-empty", { description: message`Allow initializing in a non-empty directory when the selected framework scaffolder supports it, failing if any generated file already exists.` })
21
22
  });
22
23
  /**
23
24
  * The `fedify init` CLI command parser.
package/dist/deno.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region deno.json
2
- var version = "2.2.0-pr.715.28+06979f5f";
2
+ var version = "2.2.0-pr.731.34+1e1bb860";
3
3
  //#endregion
4
4
  export { version };
@@ -1,7 +1,10 @@
1
1
  //#region src/json/biome.json
2
2
  var biome_default = {
3
- $schema: "https://biomejs.dev/schemas/1.8.3/schema.json",
4
- organizeImports: { "enabled": true },
3
+ $schema: "https://biomejs.dev/schemas/2.4.9/schema.json",
4
+ assist: {
5
+ "enabled": true,
6
+ "actions": { "source": { "organizeImports": "on" } }
7
+ },
5
8
  formatter: {
6
9
  "enabled": true,
7
10
  "indentStyle": "space",
@@ -1,7 +1,12 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
3
- "organizeImports": {
4
- "enabled": true
2
+ "$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
3
+ "assist": {
4
+ "enabled": true,
5
+ "actions": {
6
+ "source": {
7
+ "organizeImports": "on"
8
+ }
9
+ }
5
10
  },
6
11
  "formatter": {
7
12
  "enabled": true,
package/dist/lib.js CHANGED
@@ -3,14 +3,14 @@ import kv_default from "./json/kv.js";
3
3
  import mq_default from "./json/mq.js";
4
4
  import pm_default from "./json/pm.js";
5
5
  import rt_default from "./json/rt.js";
6
- import { isNotFoundError } from "./utils.js";
6
+ import { CommandError, isNotFoundError, runSubCommand } from "./utils.js";
7
7
  import { entries, evolve, fromEntries, isObject, map, negate, pipe, throwIf } from "@fxts/core";
8
8
  import process from "node:process";
9
9
  import $ from "@david/dax";
10
10
  import { getLogger } from "@logtape/logtape";
11
11
  import { toMerged } from "es-toolkit";
12
12
  import { readFileSync } from "node:fs";
13
- import { mkdir, readdir, writeFile } from "node:fs/promises";
13
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
14
14
  import { dirname, join as join$1 } from "node:path";
15
15
  //#region src/lib.ts
16
16
  /** The current `@fedify/init` package version, read from *deno.json*. */
@@ -119,17 +119,161 @@ const isNotExistsError = (e) => isObject(e) && "code" in e && e.code === "ENOENT
119
119
  */
120
120
  const throwUnlessNotExists = throwIf(negate(isNotExistsError));
121
121
  /**
122
- * Checks whether a directory is empty or does not exist.
123
- * Returns `true` if the directory has no entries or does not exist yet.
122
+ * Checks whether a directory is safe to initialize as an empty project.
123
+ * Returns `true` if the directory does not exist, has no entries, or only
124
+ * contains an unborn Git repository created by `git init`.
124
125
  */
125
126
  const isDirectoryEmpty = async (path) => {
126
127
  try {
127
- return (await readdir(path)).length === 0;
128
+ const files = await readdir(path);
129
+ if (files.length === 0) return true;
130
+ if (files.length === 1 && files[0] === ".git") return await isUnbornGitRepository(path);
131
+ return false;
128
132
  } catch (e) {
129
133
  throwUnlessNotExists(e);
130
134
  return true;
131
135
  }
132
136
  };
137
+ const isUnbornGitRepository = async (path) => {
138
+ if (await hasGitHeadCommit(path)) return false;
139
+ return await looksLikeUnbornGitRepository(path);
140
+ };
141
+ const hasGitHeadCommit = async (path) => {
142
+ try {
143
+ await runSubCommand([
144
+ "git",
145
+ "-C",
146
+ path,
147
+ "rev-parse",
148
+ "--verify",
149
+ "HEAD^{commit}"
150
+ ], {});
151
+ return true;
152
+ } catch (e) {
153
+ if (isNotFoundError(e) || e instanceof CommandError) return false;
154
+ logger.debug("Failed to resolve Git HEAD in {path}: {error}", {
155
+ path,
156
+ error: e
157
+ });
158
+ return false;
159
+ }
160
+ };
161
+ const looksLikeUnbornGitRepository = async (path) => {
162
+ const gitDir = join$1(path, ".git");
163
+ if (!await isDirectory(gitDir)) return false;
164
+ if (!await isDirectory(join$1(gitDir, "objects"))) return false;
165
+ if (!await isDirectory(join$1(gitDir, "refs"))) return false;
166
+ const head = await readGitFile(join$1(gitDir, "HEAD"));
167
+ if (head == null) return false;
168
+ if (!isValidHeadRef(head)) return false;
169
+ if (await hasAnyLooseRef(gitDir)) return false;
170
+ if (await hasAnyPackedRef(gitDir)) return false;
171
+ if (await hasAnyObjectFile(gitDir)) return false;
172
+ if (await hasAnyGitStatePath(gitDir)) return false;
173
+ return true;
174
+ };
175
+ const isValidHeadRef = (head) => {
176
+ const match = head.trim().match(/^ref: (refs\/heads\/\S+)$/);
177
+ if (match == null) return false;
178
+ return !match[1].includes("..");
179
+ };
180
+ const hasAnyLooseRef = async (gitDir) => await hasAnyFile(join$1(gitDir, "refs"), "Git refs");
181
+ const hasAnyObjectFile = async (gitDir) => await hasAnyFile(join$1(gitDir, "objects"), "Git objects");
182
+ const hasAnyFile = async (dir, description) => {
183
+ let entries;
184
+ try {
185
+ entries = await readdir(dir, { withFileTypes: true });
186
+ } catch (e) {
187
+ if (isNotFoundError(e)) return false;
188
+ logger.debug("Failed to read {description} in {path}: {error}", {
189
+ description,
190
+ path: dir,
191
+ error: e
192
+ });
193
+ return true;
194
+ }
195
+ for (const entry of entries) {
196
+ const path = join$1(dir, entry.name);
197
+ if (entry.isDirectory()) {
198
+ if (await hasAnyFile(path, description)) return true;
199
+ } else return true;
200
+ }
201
+ return false;
202
+ };
203
+ const GIT_STATE_PATHS = [
204
+ "AUTO_MERGE",
205
+ "BISECT_LOG",
206
+ "CHERRY_PICK_HEAD",
207
+ "FETCH_HEAD",
208
+ "MERGE_HEAD",
209
+ "MERGE_MODE",
210
+ "MERGE_MSG",
211
+ "ORIG_HEAD",
212
+ "REBASE_HEAD",
213
+ "REVERT_HEAD",
214
+ "SQUASH_MSG",
215
+ "index",
216
+ "logs",
217
+ "modules",
218
+ "rebase-apply",
219
+ "rebase-merge",
220
+ "sequencer",
221
+ "shallow",
222
+ "worktrees"
223
+ ];
224
+ const hasAnyGitStatePath = async (gitDir) => {
225
+ for (const path of GIT_STATE_PATHS) if (await pathExists(join$1(gitDir, path))) return true;
226
+ return false;
227
+ };
228
+ const pathExists = async (path) => {
229
+ try {
230
+ await stat(path);
231
+ return true;
232
+ } catch (e) {
233
+ if (isNotFoundError(e)) return false;
234
+ logger.debug("Failed to stat Git state path {path}: {error}", {
235
+ path,
236
+ error: e
237
+ });
238
+ return true;
239
+ }
240
+ };
241
+ const hasAnyPackedRef = async (gitDir) => {
242
+ let packedRefs;
243
+ try {
244
+ packedRefs = await readFile(join$1(gitDir, "packed-refs"), "utf8");
245
+ } catch (e) {
246
+ if (isNotFoundError(e)) return false;
247
+ logger.debug("Failed to read Git packed refs in {path}: {error}", {
248
+ path: gitDir,
249
+ error: e
250
+ });
251
+ return true;
252
+ }
253
+ return packedRefs.split(/\r?\n/).some((line) => {
254
+ const trimmed = line.trim();
255
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("^")) return false;
256
+ return true;
257
+ });
258
+ };
259
+ const readGitFile = async (path) => {
260
+ try {
261
+ return await readFile(path, "utf8");
262
+ } catch (e) {
263
+ if (!isNotFoundError(e)) logger.debug("Failed to read Git file {path}: {error}", {
264
+ path,
265
+ error: e
266
+ });
267
+ return null;
268
+ }
269
+ };
270
+ const isDirectory = async (path) => {
271
+ try {
272
+ return (await stat(path)).isDirectory();
273
+ } catch {
274
+ return false;
275
+ }
276
+ };
133
277
  /** Returns `true` if the current run is in test mode. */
134
278
  const isTest = ({ testMode }) => testMode;
135
279
  //#endregion
@@ -1,3 +1,4 @@
1
+ import "./logging.ts";
1
2
  import { fedifyMiddleware } from "@fedify/astro";
2
3
  import federation from "./federation.ts";
3
4
 
@@ -0,0 +1,5 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME === "nodejs") {
3
+ await import("./logging");
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ import "../logging";
2
+
3
+ export default function setupLogging() {}
@@ -0,0 +1,23 @@
1
+ import { configure, getConsoleSink } from "@logtape/logtape";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+
4
+ export default configure({
5
+ contextLocalStorage: new AsyncLocalStorage(),
6
+ sinks: {
7
+ console: getConsoleSink(),
8
+ },
9
+ filters: {},
10
+ loggers: [
11
+ {
12
+ category: /* project name */,
13
+ lowestLevel: "debug",
14
+ sinks: ["console"],
15
+ },
16
+ { category: "fedify", lowestLevel: "info", sinks: ["console"] },
17
+ {
18
+ category: ["logtape", "meta"],
19
+ lowestLevel: "warning",
20
+ sinks: ["console"],
21
+ },
22
+ ],
23
+ });
@@ -0,0 +1,5 @@
1
+ import loggingConfigured from "../logging";
2
+
3
+ export default defineNitroPlugin(async () => {
4
+ await loggingConfigured;
5
+ });
package/dist/types.d.ts CHANGED
@@ -22,6 +22,8 @@ interface WebFrameworkInitializer {
22
22
  federationFile: string;
23
23
  /** Relative path where the logging configuration file will be created. */
24
24
  loggingFile: string;
25
+ /** Optional template path for the logging configuration file. */
26
+ loggingTemplate?: string;
25
27
  /**
26
28
  * Additional files to create, keyed by relative path to file content.
27
29
  * Do not use `".env"` as a key — use the {@link env} property instead so
package/dist/utils.js CHANGED
@@ -4,7 +4,7 @@ import { print, printError } from "@optique/run";
4
4
  import { flow, toMerged } from "es-toolkit";
5
5
  import { message } from "@optique/core";
6
6
  import { Chalk } from "chalk";
7
- import "node:child_process";
7
+ import { spawn } from "node:child_process";
8
8
  /** Chalk instance configured based on {@link colorEnabled}. */
9
9
  const colors = new Chalk(process.stdout.isTTY && !("NO_COLOR" in process.env && process.env.NO_COLOR !== "") ? {} : { level: 0 });
10
10
  /** Type guard that checks whether a value is a `Promise`. */
@@ -53,6 +53,79 @@ const formatJson = (obj) => JSON.stringify(obj, null, 2) + "\n";
53
53
  const notEmpty = (s) => s.length > 0;
54
54
  /** Type guard that checks whether an error is a "file not found" (`ENOENT`) error. */
55
55
  const isNotFoundError = (e) => isObject(e) && "code" in e && e.code === "ENOENT";
56
+ /**
57
+ * Error thrown when a spawned shell command exits with a non-zero code.
58
+ * Captures stdout, stderr, exit code, and the original command array.
59
+ */
60
+ var CommandError = class extends Error {
61
+ commandLine;
62
+ constructor(message, stdout, stderr, code, command) {
63
+ super(message);
64
+ this.stdout = stdout;
65
+ this.stderr = stderr;
66
+ this.code = code;
67
+ this.command = command;
68
+ this.name = "CommandError";
69
+ this.commandLine = command.join(" ");
70
+ }
71
+ };
72
+ /**
73
+ * Executes a shell command (or a chain of commands joined by `"&&"`) as child
74
+ * processes and returns the combined stdout/stderr output.
75
+ * Throws a {@link CommandError} if any command in the chain exits with a
76
+ * non-zero code.
77
+ *
78
+ * @param command - The command as an array of strings; use `"&&"` to chain
79
+ * @param options - Options forwarded to `node:child_process.spawn`
80
+ * @returns A promise resolving to `{ stdout, stderr }`
81
+ */
82
+ const runSubCommand = async (command, options) => {
83
+ const commands = command.reduce((acc, cur) => {
84
+ if (cur === "&&") acc.push([]);
85
+ else {
86
+ if (acc.length === 0) acc.push([]);
87
+ acc[acc.length - 1].push(cur);
88
+ }
89
+ return acc;
90
+ }, []);
91
+ const results = {
92
+ stdout: "",
93
+ stderr: ""
94
+ };
95
+ for (const cmd of commands) try {
96
+ const result = await runSingularCommand(cmd, options);
97
+ results.stdout += (results.stdout ? "\n" : "") + result.stdout;
98
+ results.stderr += (results.stderr ? "\n" : "") + result.stderr;
99
+ } catch (error) {
100
+ if (error instanceof CommandError) {
101
+ results.stdout += (results.stdout ? "\n" : "") + error.stdout;
102
+ results.stderr += (results.stderr ? "\n" : "") + error.stderr;
103
+ }
104
+ throw error;
105
+ }
106
+ return results;
107
+ };
108
+ const runSingularCommand = (command, options) => new Promise((resolve, reject) => {
109
+ let stdout = "";
110
+ let stderr = "";
111
+ const child = spawn(command[0], command.slice(1), options);
112
+ child.stdout?.on("data", (data) => {
113
+ stdout += data.toString();
114
+ });
115
+ child.stderr?.on("data", (data) => {
116
+ stderr += data.toString();
117
+ });
118
+ child.on("close", (code) => {
119
+ if (code === 0) resolve({
120
+ stdout: stdout.trim(),
121
+ stderr: stderr.trim()
122
+ });
123
+ else reject(new CommandError(`Command exited with code ${code ?? "unknown"}`, stdout.trim(), stderr.trim(), code ?? -1, command));
124
+ });
125
+ child.on("error", (error) => {
126
+ reject(error);
127
+ });
128
+ });
56
129
  /** Returns the current working directory. */
57
130
  const getCwd = () => process.cwd();
58
131
  /** Returns the current OS platform (e.g., `"darwin"`, `"win32"`, `"linux"`). */
@@ -76,4 +149,4 @@ const printMessage = flow(message, print);
76
149
  /** Prints a formatted error message to stderr using `@optique/run`'s `printError`. */
77
150
  const printErrorMessage = flow(message, printError);
78
151
  //#endregion
79
- export { colors, formatJson, getCwd, getOsType, isNotFoundError, merge$1 as merge, notEmpty, printErrorMessage, printMessage, product, replace, replaceAll, set };
152
+ export { CommandError, colors, formatJson, getCwd, getOsType, isNotFoundError, merge$1 as merge, notEmpty, printErrorMessage, printMessage, product, replace, replaceAll, runSubCommand, set };
@@ -21,6 +21,7 @@ const nextDescription = {
21
21
  federationFile: "federation/index.ts",
22
22
  loggingFile: "logging.ts",
23
23
  files: {
24
+ "instrumentation.ts": await readTemplate("next/instrumentation.ts"),
24
25
  "middleware.ts": await readTemplate("next/middleware.ts"),
25
26
  ...pm !== "deno" && { "eslint.config.ts": await readTemplate("defaults/eslint.config.ts") }
26
27
  },
@@ -18,6 +18,7 @@ const nitroDescription = {
18
18
  loggingFile: "server/logging.ts",
19
19
  env: testMode ? { HOST: "127.0.0.1" } : {},
20
20
  files: {
21
+ "server/plugins/logging.ts": await readTemplate("nitro/server/plugins/logging.ts"),
21
22
  "server/middleware/federation.ts": await readTemplate("nitro/server/middleware/federation.ts"),
22
23
  "server/error.ts": await readTemplate("nitro/server/error.ts"),
23
24
  "nitro.config.ts": await readTemplate("nitro/nitro.config.ts"),
@@ -18,9 +18,11 @@ const nuxtDescription = {
18
18
  },
19
19
  federationFile: "server/federation.ts",
20
20
  loggingFile: "server/logging.ts",
21
+ loggingTemplate: "nuxt/server/logging.ts",
21
22
  env: testMode ? { HOST: "127.0.0.1" } : {},
22
23
  files: {
23
24
  "nuxt.config.ts": await readTemplate("nuxt/nuxt.config.ts"),
25
+ "server/plugins/logging.ts": await readTemplate("nuxt/server/plugins/logging.ts"),
24
26
  ...pm !== "deno" && { "eslint.config.ts": await readTemplate("defaults/eslint.config.ts") }
25
27
  },
26
28
  tasks: pm !== "deno" ? { "lint": "eslint ." } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/init",
3
- "version": "2.2.0-pr.715.28+06979f5f",
3
+ "version": "2.2.0-pr.731.34+1e1bb860",
4
4
  "description": "Project initializer for Fedify",
5
5
  "keywords": [
6
6
  "fedify",
@@ -52,8 +52,8 @@
52
52
  "@fxts/core": "^1.20.0",
53
53
  "@inquirer/prompts": "^7.8.4",
54
54
  "@logtape/logtape": "^2.0.5",
55
- "@optique/core": "^1.0.0",
56
- "@optique/run": "^1.0.0",
55
+ "@optique/core": "^1.0.2",
56
+ "@optique/run": "^1.0.2",
57
57
  "chalk": "^5.6.2",
58
58
  "es-toolkit": "1.43.0",
59
59
  "inquirer-toggle": "^1.0.1"