@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 +2 -1
- package/dist/action/mod.js +2 -2
- package/dist/action/patch.js +95 -12
- package/dist/action/templates.js +10 -4
- package/dist/ask/dir.js +4 -0
- package/dist/command.d.ts +4 -1
- package/dist/command.js +3 -2
- package/dist/deno.js +1 -1
- package/dist/json/biome.js +5 -2
- package/dist/json/biome.json +8 -3
- package/dist/lib.js +149 -5
- package/dist/templates/astro/src/middleware.ts.tpl +1 -0
- package/dist/templates/next/instrumentation.ts.tpl +5 -0
- package/dist/templates/nitro/server/plugins/logging.ts.tpl +3 -0
- package/dist/templates/nuxt/server/logging.ts.tpl +23 -0
- package/dist/templates/nuxt/server/plugins/logging.ts.tpl +5 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils.js +75 -2
- package/dist/webframeworks/next.js +1 -0
- package/dist/webframeworks/nitro.js +1 -0
- package/dist/webframeworks/nuxt.js +2 -0
- package/package.json +3 -3
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],
|
|
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
|
package/dist/action/mod.js
CHANGED
|
@@ -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 };
|
package/dist/action/patch.js
CHANGED
|
@@ -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) =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 };
|
package/dist/action/templates.js
CHANGED
|
@@ -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
|
|
19
|
-
* Reads the default logging template and replaces the project name placeholder.
|
|
18
|
+
* Loads logging configuration file content for the initializer.
|
|
20
19
|
*
|
|
21
|
-
* @
|
|
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 `
|
|
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 `
|
|
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
package/dist/json/biome.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
//#region src/json/biome.json
|
|
2
2
|
var biome_default = {
|
|
3
|
-
$schema: "https://biomejs.dev/schemas/
|
|
4
|
-
|
|
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",
|
package/dist/json/biome.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/
|
|
3
|
-
"
|
|
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
|
|
123
|
-
* Returns `true` if the directory has no entries or
|
|
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
|
-
|
|
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
|
|
@@ -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
|
+
});
|
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.
|
|
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.
|
|
56
|
-
"@optique/run": "^1.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"
|