Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ To be released.
- Added `SqliteMessageQueue.getDepth()` for reporting queued, ready, and
delayed message counts. [[#735], [#748]]

### @fedify/init

- Added a `--skip-install` option to `fedify init` that skips automatic
dependency installation after scaffolding. This is useful for CI
environments, monorepo workspaces that install dependencies from the
root, or when you want to inspect the generated files before
installing. [[#720], [#776] by fru1tworld]

[#720]: https://github.com/fedify-dev/fedify/issues/720
[#776]: https://github.com/fedify-dev/fedify/pull/776

### Claude Code plugin

- Added a Claude Code plugin at *claude-plugin/*, installable with:
Expand Down
22 changes: 22 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,28 @@ the selected framework scaffolder accepts them. Some scaffolders, such as
own confirmation prompt, while a freshly initialized *.git* directory remains
acceptable.

### `--skip-install`: Skip installing dependencies

*This option is available since Fedify 2.3.0.*

By default, `fedify init` runs the selected package manager's install command
right after scaffolding the project. The `--skip-install` option scaffolds the
files without running install, which is useful when:

- installation is handled separately in a CI pipeline;
- the new project lives inside a monorepo whose dependencies are installed
from the workspace root; or
- you want to inspect the generated files before installing.

~~~~ sh
fedify init my-fedify-project --skip-install
~~~~

After scaffolding, `fedify init` prints the command to run to install
dependencies later. Other steps such as creating files, applying patches, and
running the framework-specific scaffolder (e.g., *create-next-app*) still
happen as usual; only the final install step is skipped.


`fedify lookup`: Looking up an ActivityPub object
-------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions packages/init/src/action/configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function createInitData(): InitCommandData {
messageQueue: "denokv",
dryRun: false,
allowNonEmpty: false,
skipInstall: false,
testMode: false,
dir: "/tmp/example",
initializer: {
Expand Down Expand Up @@ -166,6 +167,7 @@ async function createNpmInitData(dir: string): Promise<InitCommandData> {
messageQueue: "in-process",
dryRun: false,
allowNonEmpty: false,
skipInstall: false,
testMode: false,
dir,
});
Expand All @@ -179,6 +181,7 @@ async function createNpmInitData(dir: string): Promise<InitCommandData> {
messageQueue: "in-process",
dryRun: false,
allowNonEmpty: false,
skipInstall: false,
testMode: false,
dir,
initializer,
Expand All @@ -199,6 +202,7 @@ async function createNuxtNpmInitData(dir: string): Promise<InitCommandData> {
messageQueue: "in-process",
dryRun: false,
allowNonEmpty: false,
skipInstall: false,
testMode: false,
dir,
});
Expand All @@ -212,6 +216,7 @@ async function createNuxtNpmInitData(dir: string): Promise<InitCommandData> {
messageQueue: "in-process",
dryRun: false,
allowNonEmpty: false,
skipInstall: false,
testMode: false,
dir,
initializer,
Expand Down
9 changes: 7 additions & 2 deletions packages/init/src/action/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
noticeHowToRun,
noticeOptions,
noticePrecommand,
noticeSkippedInstall,
} from "./notice.ts";
import {
assertNoGeneratedFileConflicts,
Expand All @@ -23,6 +24,7 @@ import {
hasCommand,
installDependencies,
isDry,
isSkipInstall,
runPrecommand,
} from "./utils.ts";

Expand All @@ -39,7 +41,9 @@ import {
* - If dry run, executes `handleDryRun`.
* - Otherwise, executes `handleHydRun`.
* 7. Recommends configuration environment via `recommendConfigEnv`.
* 8. Shows how to run the project via `noticeHowToRun`.
* 8. If installation was skipped and not a dry run, prints how to install
* dependencies manually via `noticeSkippedInstall`.
* 9. Shows how to run the project via `noticeHowToRun`.
*/
const runInit = (options: InitCommand) =>
pipe(
Expand All @@ -52,6 +56,7 @@ const runInit = (options: InitCommand) =>
when(isDry, handleDryRun),
unless(isDry, handleHydRun),
tap(recommendConfigEnv),
tap(unless(isDry, when(isSkipInstall, noticeSkippedInstall))),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems more sense to place when(isSkipInstall, noticeSkippedInstall) inside handleHydRun. Or is there a specific reason you intended for it to be here?

tap(noticeHowToRun),
);

Expand All @@ -76,5 +81,5 @@ const handleHydRun = (data: InitCommandData) =>
tap(assertNoGeneratedFileConflicts),
tap(when(hasCommand, runPrecommand)),
tap(patchFiles),
tap(installDependencies),
tap(unless(isSkipInstall, installDependencies)),
);
10 changes: 10 additions & 0 deletions packages/init/src/action/notice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ ${instruction}
Start by editing the ${text(federationFile)} file to define your federation!
`;

/** Prints a notice that dependency installation was skipped and how to install them manually. */
export const noticeSkippedInstall = (
{ packageManager }: InitCommandData,
) =>
printMessage`
Dependencies were not installed. Run ${
text(`${packageManager} install`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text function outputs its arguments so that they appear as plain text. I think commandLine is better for this.

} in the project directory to install them.
`;

/**
* Returns an error handler that prints a formatted error message when
* a dependency installation command fails, then throws.
Expand Down
1 change: 1 addition & 0 deletions packages/init/src/action/patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function createInitData(
messageQueue: "in-process",
dryRun: false,
allowNonEmpty,
skipInstall: false,
testMode: false,
dir,
initializer: {
Expand Down
5 changes: 5 additions & 0 deletions packages/init/src/action/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import type { InitCommandData } from "../types.ts";
/** Returns `true` if the current run is in dry-run mode. */
export const isDry = ({ dryRun }: InitCommandData) => dryRun;

/** Returns `true` if the current run skips dependency installation. */
export const isSkipInstall = (
{ skipInstall }: Pick<InitCommandData, "skipInstall">,
) => skipInstall;

/** Returns `true` if the framework initializer has a precommand to execute. */
export const hasCommand = (data: InitCommandData) => !!data.initializer.command;

Expand Down
4 changes: 4 additions & 0 deletions packages/init/src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export const initOptions = object("Initialization options", {
description:
message`Allow initializing in a non-empty directory when the selected framework scaffolder supports it, failing if any generated file already exists.`,
}),
skipInstall: option("--skip-install", {
description:
message`Skip installing dependencies after scaffolding the project.`,
}),
});

/**
Expand Down
1 change: 1 addition & 0 deletions packages/init/src/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ test(
dir: packageDir,
dryRun: true,
allowNonEmpty: false,
skipInstall: false,
kvStore: "in-memory",
messageQueue: "in-process",
packageManager: "bun",
Expand Down
26 changes: 26 additions & 0 deletions packages/init/src/skip-install.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { parse } from "@optique/core/parser";
import { ok, strictEqual } from "node:assert/strict";
import test from "node:test";
import { isSkipInstall } from "./action/utils.ts";
import { initOptions } from "./command.ts";

test("initOptions parses --skip-install as true", () => {
const result = parse(initOptions, ["--skip-install"]);
ok(result.success);
if (result.success) {
strictEqual(result.value.skipInstall, true);
}
});

test("initOptions defaults skipInstall to false when the flag is absent", () => {
const result = parse(initOptions, []);
ok(result.success);
if (result.success) {
strictEqual(result.value.skipInstall, false);
}
});

test("isSkipInstall mirrors the skipInstall field", () => {
strictEqual(isSkipInstall({ skipInstall: false }), false);
strictEqual(isSkipInstall({ skipInstall: true }), true);
});
4 changes: 4 additions & 0 deletions packages/init/src/webframeworks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ test("Nitro template loads LogTape during server startup", async () => {
testMode: false,
dryRun: true,
allowNonEmpty: false,
skipInstall: false,
});

ok(files);
Expand All @@ -38,6 +39,7 @@ test("Next.js template loads LogTape through instrumentation", async () => {
testMode: false,
dryRun: true,
allowNonEmpty: false,
skipInstall: false,
});

ok(files);
Expand All @@ -61,6 +63,7 @@ test("Astro template loads LogTape through middleware", async () => {
testMode: false,
dryRun: true,
allowNonEmpty: false,
skipInstall: false,
});

ok(files);
Expand All @@ -81,6 +84,7 @@ test("SolidStart template loads LogTape through middleware", async () => {
testMode: false,
dryRun: true,
allowNonEmpty: false,
skipInstall: false,
});

ok(files);
Expand Down
12 changes: 9 additions & 3 deletions packages/init/src/webframeworks/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const nextDescription: WebFrameworkDescription = {
label: "Next.js",
packageManagers: PACKAGE_MANAGER,
defaultPort: 3000,
init: async ({ packageManager: pm }) => ({
command: getNextInitCommand(pm),
init: async ({ packageManager: pm, skipInstall }) => ({
command: getNextInitCommand(pm, skipInstall),
dependencies: {
"@fedify/next": PACKAGE_VERSION,
...(pm === "deno" ? defaultDenoDependencies : {}),
Expand Down Expand Up @@ -43,7 +43,13 @@ export default nextDescription;
*/
const getNextInitCommand = (
pm: PackageManager,
): string[] => [...createNextAppCommand(pm), ".", "--yes"];
skipInstall: boolean,
): string[] => [
...createNextAppCommand(pm),
".",
"--yes",
...(skipInstall ? ["--skip-install"] : []),
];

const createNextAppCommand = (pm: PackageManager): string[] =>
pm === "deno"
Expand Down