Skip to content

Commit 35e41ca

Browse files
authored
Merge pull request #717 from dahlia/bugfix/nuxt
Fix `@fedify/nuxt` runtime packaging and `fedify init` scaffolding edge cases
2 parents 44ea8f2 + 79dfe11 commit 35e41ca

25 files changed

Lines changed: 1019 additions & 67 deletions

CHANGES.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,25 @@ To be released.
148148

149149
### @fedify/init
150150

151+
- Added a `--allow-non-empty` option to `fedify init` for automated
152+
scaffolding in directories that already contain unrelated files. The
153+
command still fails before making changes if any file that Fedify would
154+
generate already exists, avoiding accidental merges or appends.
155+
[[#716], [#717]]
156+
157+
- Fixed `fedify init` so that a directory containing only a freshly
158+
initialized Git repository is treated as empty. Directories whose Git
159+
`HEAD` already resolves to a commit, whose Git metadata contains loose or
160+
packed refs, stored objects, an index, or reflogs, or that contain any
161+
files besides *.git*, still require the existing non-empty-directory
162+
confirmation. [[#716], [#717]]
163+
164+
- Fixed generated *biome.json* files to use Biome 2 configuration syntax,
165+
matching the `@biomejs/biome` version that `fedify init` installs.
166+
Generated projects now enable import organization through Biome's
167+
`assist.actions.source.organizeImports` setting instead of the removed
168+
top-level `organizeImports` option. [[#716], [#717]]
169+
151170
- Fixed errors when using `fedify init` with certain web framework
152171
integration packages (Astro, ElysiaJS, Nitro) alongside `@fedify/mysql`.
153172
Environment variables are now properly loaded at runtime, resolving the
@@ -161,6 +180,8 @@ To be released.
161180
[#649]: https://github.com/fedify-dev/fedify/issues/649
162181
[#656]: https://github.com/fedify-dev/fedify/pull/656
163182
[#675]: https://github.com/fedify-dev/fedify/pull/675
183+
[#716]: https://github.com/fedify-dev/fedify/issues/716
184+
[#717]: https://github.com/fedify-dev/fedify/pull/717
164185

165186
### Docs
166187

deno.lock

Lines changed: 1 addition & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,28 @@ When using `--dry-run`, the command will:
339339
This option works with all other initialization options, allowing you to preview
340340
different configurations before making a decision.
341341

342+
### `--allow-non-empty`: Initialize in a non-empty directory
343+
344+
*This option is available since Fedify 2.2.0.*
345+
346+
By default, `fedify init` asks for confirmation before using a directory that
347+
already contains files. This prompt protects you from accidentally
348+
initializing a project in the wrong directory. In non-interactive scripts or
349+
CI jobs, use the `--allow-non-empty` option to allow a non-empty target
350+
directory:
351+
352+
~~~~ sh
353+
fedify init . --allow-non-empty
354+
~~~~
355+
356+
This option does not overwrite existing project files. Before making changes,
357+
`fedify init` checks the files it would generate and fails if any of them
358+
already exist. Unrelated files can remain in the target directory only when
359+
the selected framework scaffolder accepts them. Some scaffolders, such as
360+
*create-next-app*, still reject unrelated files even if `fedify init` skips its
361+
own confirmation prompt, while a freshly initialized *.git* directory remains
362+
acceptable.
363+
342364

343365
`fedify lookup`: Looking up an ActivityPub object
344366
-------------------------------------------------

packages/fedify/src/federation/mq.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import {
1313
ParallelMessageQueue,
1414
} from "./mq.ts";
1515

16+
async function disposeMessageQueue(mq: object): Promise<void> {
17+
if (Symbol.asyncDispose in mq) {
18+
const dispose = mq[Symbol.asyncDispose];
19+
if (typeof dispose === "function") {
20+
await dispose.call(mq);
21+
return;
22+
}
23+
}
24+
if (Symbol.dispose in mq) {
25+
const dispose = mq[Symbol.dispose];
26+
if (typeof dispose === "function") dispose.call(mq);
27+
}
28+
}
29+
1630
test("InProcessMessageQueue", async (t) => {
1731
const mq = new InProcessMessageQueue();
1832

@@ -189,10 +203,7 @@ test("MessageQueue.nativeRetrial", async (t) => {
189203
await globalThis.Deno.openKv(":memory:"),
190204
);
191205
assert(mq.nativeRetrial);
192-
if (Symbol.dispose in mq) {
193-
const dispose = mq[Symbol.dispose];
194-
if (typeof dispose === "function") dispose.call(mq);
195-
}
206+
await disposeMessageQueue(mq);
196207
});
197208
}
198209

@@ -321,10 +332,7 @@ for (const mqName in queues) {
321332
controller.abort();
322333
await listening;
323334

324-
if (Symbol.dispose in mq) {
325-
const dispose = mq[Symbol.dispose];
326-
if (typeof dispose === "function") dispose.call(mq);
327-
}
335+
await disposeMessageQueue(mq);
328336
},
329337
});
330338
}

packages/init/src/action/configs.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import assert from "node:assert/strict";
2+
import { mkdtemp, readFile, rm } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
25
import test from "node:test";
36
import { message } from "@optique/core";
7+
import { kvStores, messageQueues } from "../lib.ts";
48
import type { InitCommandData } from "../types.ts";
9+
import bareBonesDescription from "../webframeworks/bare-bones.ts";
510
import { loadDenoConfig } from "./configs.ts";
11+
import { patchFiles } from "./patch.ts";
612

713
function createInitData(): InitCommandData {
814
const data = {
@@ -13,6 +19,7 @@ function createInitData(): InitCommandData {
1319
kvStore: "denokv",
1420
messageQueue: "denokv",
1521
dryRun: false,
22+
allowNonEmpty: false,
1623
testMode: false,
1724
dir: "/tmp/example",
1825
initializer: {
@@ -94,3 +101,93 @@ test("loadDenoConfig keeps unstable.temporal before Deno 2.7.0", () => {
94101
restoreDeno(originalDeno);
95102
}
96103
});
104+
105+
test("patchFiles creates a Biome config matching the npm package version", async () => {
106+
const dir = await mkdtemp(join(tmpdir(), "fedify-init-biome-"));
107+
108+
try {
109+
const data = await createNpmInitData(dir);
110+
await patchFiles(data);
111+
112+
const packageJson = JSON.parse(
113+
await readFile(join(dir, "package.json"), "utf8"),
114+
) as {
115+
devDependencies?: Record<string, string>;
116+
};
117+
const biomeConfig = JSON.parse(
118+
await readFile(join(dir, "biome.json"), "utf8"),
119+
) as Record<string, unknown>;
120+
121+
const biomeVersion = packageJson.devDependencies?.["@biomejs/biome"];
122+
const schema = biomeConfig.$schema;
123+
assert.ok(typeof biomeVersion === "string");
124+
assert.ok(typeof schema === "string");
125+
assert.equal(getSchemaVersion(schema), getPackageVersion(biomeVersion));
126+
assert.equal(getOrganizeImportsSetting(biomeConfig), "on");
127+
assert.equal(
128+
"organizeImports" in biomeConfig,
129+
false,
130+
);
131+
} finally {
132+
await rm(dir, { recursive: true, force: true });
133+
}
134+
});
135+
136+
async function createNpmInitData(dir: string): Promise<InitCommandData> {
137+
const initializer = await bareBonesDescription.init({
138+
command: "init",
139+
projectName: "example",
140+
packageManager: "npm",
141+
webFramework: "bare-bones",
142+
kvStore: "in-memory",
143+
messageQueue: "in-process",
144+
dryRun: false,
145+
allowNonEmpty: false,
146+
testMode: false,
147+
dir,
148+
});
149+
150+
const data = {
151+
command: "init",
152+
projectName: "example",
153+
packageManager: "npm",
154+
webFramework: "bare-bones",
155+
kvStore: "in-memory",
156+
messageQueue: "in-process",
157+
dryRun: false,
158+
allowNonEmpty: false,
159+
testMode: false,
160+
dir,
161+
initializer,
162+
kv: kvStores["in-memory"],
163+
mq: messageQueues["in-process"],
164+
env: {},
165+
} satisfies InitCommandData;
166+
return data;
167+
}
168+
169+
function getSchemaVersion(schema: string): string {
170+
const match = schema.match(/\/schemas\/(\d+\.\d+\.\d+)\//);
171+
assert.ok(match, `Unexpected Biome schema URL: ${schema}`);
172+
return match[1];
173+
}
174+
175+
function getPackageVersion(version: string): string {
176+
const match = version.match(/\d+\.\d+\.\d+/);
177+
assert.ok(match, `Unexpected Biome package version: ${version}`);
178+
return match[0];
179+
}
180+
181+
function getOrganizeImportsSetting(config: Record<string, unknown>): unknown {
182+
const assist = config.assist;
183+
assert.ok(isRecord(assist));
184+
const actions = assist.actions;
185+
assert.ok(isRecord(actions));
186+
const source = actions.source;
187+
assert.ok(isRecord(source));
188+
return source.organizeImports;
189+
}
190+
191+
function isRecord(value: unknown): value is Record<string, unknown> {
192+
return typeof value === "object" && value != null;
193+
}

packages/init/src/action/mod.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
noticeOptions,
1313
noticePrecommand,
1414
} from "./notice.ts";
15-
import { patchFiles, recommendPatchFiles } from "./patch.ts";
15+
import {
16+
assertNoGeneratedFileConflicts,
17+
patchFiles,
18+
recommendPatchFiles,
19+
} from "./patch.ts";
1620
import recommendDependencies from "./recommend.ts";
1721
import setData from "./set.ts";
1822
import {
@@ -69,6 +73,7 @@ const handleHydRun = (data: InitCommandData) =>
6973
pipe(
7074
data,
7175
tap(makeDirIfHyd),
76+
tap(assertNoGeneratedFileConflicts),
7277
tap(when(hasCommand, runPrecommand)),
7378
tap(patchFiles),
7479
tap(installDependencies),
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import assert from "node:assert/strict";
2+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import test from "node:test";
6+
import { message } from "@optique/core";
7+
import type { InitCommandData } from "../types.ts";
8+
import {
9+
assertNoGeneratedFileConflicts,
10+
GeneratedFileConflictError,
11+
getJsonsCacheKey,
12+
} from "./patch.ts";
13+
14+
test("assertNoGeneratedFileConflicts allows unrelated files", async () => {
15+
await withTempDir(async (dir) => {
16+
await writeFile(join(dir, "README.md"), "# Example\n");
17+
18+
await assert.doesNotReject(() =>
19+
assertNoGeneratedFileConflicts(createInitData(dir, true))
20+
);
21+
});
22+
});
23+
24+
test("assertNoGeneratedFileConflicts rejects existing generated files", async () => {
25+
await withTempDir(async (dir) => {
26+
await mkdir(join(dir, "src"), { recursive: true });
27+
await writeFile(join(dir, "package.json"), "{}\n");
28+
await writeFile(join(dir, "src", "main.ts"), "");
29+
30+
await assert.rejects(
31+
() => assertNoGeneratedFileConflicts(createInitData(dir, true)),
32+
(error) => {
33+
assert.ok(error instanceof GeneratedFileConflictError);
34+
assert.deepEqual(error.conflicts, ["src/main.ts", "package.json"]);
35+
assert.match(error.message, /src\/main\.ts/);
36+
assert.match(error.message, /package\.json/);
37+
return true;
38+
},
39+
);
40+
});
41+
});
42+
43+
test("assertNoGeneratedFileConflicts skips checks without allowNonEmpty", async () => {
44+
await withTempDir(async (dir) => {
45+
await writeFile(join(dir, "package.json"), "{}\n");
46+
47+
await assert.doesNotReject(() =>
48+
assertNoGeneratedFileConflicts(createInitData(dir, false))
49+
);
50+
});
51+
});
52+
53+
test("getJsonsCacheKey stays stable across pipeline clones", () => {
54+
const data = createInitData("/tmp/example", true);
55+
const cloned = {
56+
...data,
57+
files: { "src/main.ts": "" },
58+
jsons: { "package.json": {} },
59+
};
60+
61+
assert.equal(getJsonsCacheKey(cloned), getJsonsCacheKey(data));
62+
});
63+
64+
function createInitData(
65+
dir: string,
66+
allowNonEmpty: boolean,
67+
): InitCommandData {
68+
const data = {
69+
command: "init",
70+
projectName: "example",
71+
packageManager: "npm",
72+
webFramework: "bare-bones",
73+
kvStore: "in-memory",
74+
messageQueue: "in-process",
75+
dryRun: false,
76+
allowNonEmpty,
77+
testMode: false,
78+
dir,
79+
initializer: {
80+
federationFile: "src/federation.ts",
81+
loggingFile: "src/logging.ts",
82+
instruction: message`done`,
83+
tasks: {},
84+
compilerOptions: {},
85+
files: {
86+
"src/main.ts": "",
87+
},
88+
},
89+
kv: {
90+
label: "In-Memory",
91+
packageManagers: ["npm"],
92+
imports: {},
93+
object: "new MemoryKvStore()",
94+
},
95+
mq: {
96+
label: "In-Process",
97+
packageManagers: ["npm"],
98+
imports: {},
99+
object: "new InProcessMessageQueue()",
100+
},
101+
env: {},
102+
} satisfies InitCommandData;
103+
return data;
104+
}
105+
106+
async function withTempDir(
107+
fn: (dir: string) => Promise<void>,
108+
): Promise<void> {
109+
const dir = await mkdtemp(join(tmpdir(), "fedify-init-patch-"));
110+
try {
111+
await fn(dir);
112+
} finally {
113+
await rm(dir, { recursive: true, force: true });
114+
}
115+
}

0 commit comments

Comments
 (0)