Unverified Commit bcad8834 authored by Hong Minhee (洪 民憙)'s avatar Hong Minhee (洪 民憙) Committed by GitHub
Browse files

Merge pull request #298 from notJoon/feat/dry-run

parents 654c2dde c89fec12
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -72,6 +72,10 @@ To be released.
     -  The `--allow-private-address` or `-p` option allows looking up
        WebFinger information for private addresses (e.g., `localhost`).

  -  Added `--dry-run` option to `fedify init` command.  This option allows users
    to preview what files and configurations would be created without actually
    creating them.  [[#263], [#298] by Lee ByeongJun]

  -  Fixed a bug where the `fedify node` command had failed to correctly
     render the favicon in terminal emulators that do not support 24-bit
     colors.  [[#168], [#282] by Hyeonseo Kim]
@@ -80,10 +84,12 @@ To be released.
[#248]: https://github.com/fedify-dev/fedify/issues/248
[#260]: https://github.com/fedify-dev/fedify/issues/260
[#262]: https://github.com/fedify-dev/fedify/issues/262
[#263]: https://github.com/fedify-dev/fedify/issues/263
[#278]: https://github.com/fedify-dev/fedify/pull/278
[#281]: https://github.com/fedify-dev/fedify/pull/281
[#282]: https://github.com/fedify-dev/fedify/pull/282
[#285]: https://github.com/fedify-dev/fedify/pull/285
[#298]: https://github.com/fedify-dev/fedify/pull/298
[#300]: https://github.com/fedify-dev/fedify/pull/300


cli/init.test.ts

0 → 100644
+253 −0
Original line number Diff line number Diff line
import { assertEquals, assertStringIncludes } from "@std/assert";
import { join } from "@std/path";
import { exists } from "@std/fs";

const CLI_PATH = join(import.meta.dirname!, "mod.ts");

async function runInit(
  args: string[],
): Promise<{ output: string; success: boolean }> {
  const cmd = new Deno.Command("deno", {
    args: ["run", "-A", CLI_PATH, "init", ...args],
    stdout: "piped",
    stderr: "piped",
    stdin: "null",
  });

  const process = cmd.spawn();
  const output = await process.output();
  const decoder = new TextDecoder();
  const stdout = decoder.decode(output.stdout);
  const stderr = decoder.decode(output.stderr);

  return {
    output: stdout + stderr,
    success: output.success,
  };
}

Deno.test("init --dry-run shows preview without creating files", async () => {
  const testDir = await Deno.makeTempDir();
  const projectDir = join(testDir, "test-project");

  try {
    const result = await runInit([
      projectDir,
      "--dry-run",
      "--runtime",
      "deno",
    ]);

    // Check that dry-run mode is indicated
    assertStringIncludes(result.output, "🔍 DRY RUN MODE");
    assertStringIncludes(result.output, "Would create files:");
    assertStringIncludes(result.output, "Would install dependencies:");

    assertStringIncludes(result.output, "federation.ts");
    assertStringIncludes(result.output, "logging.ts");
    assertStringIncludes(result.output, "main.ts");
    assertStringIncludes(result.output, "deno.json");
    assertStringIncludes(result.output, ".env");

    // Verify no files were actually created
    assertEquals(
      await exists(projectDir),
      false,
      "Project directory should not be created",
    );
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});

Deno.test("init --dry-run with web framework shows correct files", async () => {
  const testDir = await Deno.makeTempDir();
  const projectDir = join(testDir, "test-hono-project");

  try {
    const result = await runInit([
      projectDir,
      "--dry-run",
      "--runtime",
      "deno",
      "--web-framework",
      "hono",
    ]);

    // Check Hono-specific files
    assertStringIncludes(result.output, "src/federation.ts");
    assertStringIncludes(result.output, "src/app.tsx");
    assertStringIncludes(result.output, "src/index.ts");
    assertStringIncludes(result.output, "@hono/hono");

    // Verify no files were created
    assertEquals(await exists(projectDir), false);
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});

Deno.test("init --dry-run with external stores shows dependencies", async () => {
  const testDir = await Deno.makeTempDir();
  const projectDir = join(testDir, "test-redis-project");

  try {
    const result = await runInit([
      projectDir,
      "--dry-run",
      "--runtime",
      "deno",
      "--kv-store",
      "redis",
      "--message-queue",
      "redis",
    ]);

    // Check Redis dependencies
    assertStringIncludes(result.output, "@fedify/redis");
    assertStringIncludes(result.output, "ioredis");
    assertStringIncludes(result.output, "REDIS_URL");

    // Check Redis imports in federation.ts
    assertStringIncludes(result.output, "RedisKvStore");
    assertStringIncludes(result.output, "RedisMessageQueue");

    // Verify no files were created
    assertEquals(await exists(projectDir), false);
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});

Deno.test("init --dry-run shows command for framework initialization", async () => {
  const testDir = await Deno.makeTempDir();
  const projectDir = join(testDir, "test-nitro-project");

  try {
    const result = await runInit([
      projectDir,
      "--dry-run",
      "--runtime",
      "node",
      "--package-manager",
      "npm",
      "--web-framework",
      "nitro",
    ]);

    // Check that initialization command is shown
    assertStringIncludes(result.output, "Would run command:");
    assertStringIncludes(result.output, "giget@latest nitro");

    // Check Node.js specific files
    assertStringIncludes(result.output, "package.json");
    assertStringIncludes(result.output, "biome.json");

    // Verify no files were created
    assertEquals(await exists(projectDir), false);
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});

Deno.test("init without --dry-run creates actual files", async () => {
  const testDir = await Deno.makeTempDir();
  const projectDir = join(testDir, "test-actual-project");

  try {
    // Run without --dry-run
    const result = await runInit([
      projectDir,
      "--runtime",
      "deno",
    ]);

    // Should not show dry-run header
    assertEquals(result.output.includes("DRY RUN MODE"), false);

    // Verify files were actually created
    assertEquals(
      await exists(projectDir),
      true,
      "Project directory should be created",
    );
    assertEquals(await exists(join(projectDir, "federation.ts")), true);
    assertEquals(await exists(join(projectDir, "logging.ts")), true);
    assertEquals(await exists(join(projectDir, "main.ts")), true);
    assertEquals(await exists(join(projectDir, "deno.json")), true);
    assertEquals(await exists(join(projectDir, ".env")), true);
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});

Deno.test("init --dry-run fails on non-empty directory", async () => {
  const testDir = await Deno.makeTempDir();

  try {
    // Create a file in the directory
    await Deno.writeTextFile(join(testDir, "existing.txt"), "content");

    const result = await runInit([
      testDir,
      "--dry-run",
      "--runtime",
      "deno",
    ]);

    assertStringIncludes(result.output, "The directory is not empty");
    assertEquals(result.success, false);
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});

Deno.test("init --dry-run shows prepend files for Fresh", async () => {
  const testDir = await Deno.makeTempDir();
  const projectDir = join(testDir, "test-fresh-project");

  try {
    const result = await runInit([
      projectDir,
      "--dry-run",
      "--runtime",
      "deno",
      "--web-framework",
      "fresh",
    ]);

    // Check that prepend files are shown
    assertStringIncludes(result.output, "Would prepend to files:");
    assertStringIncludes(result.output, "fresh.config.ts");

    // Verify no files were created
    assertEquals(await exists(projectDir), false);
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});

Deno.test("init --dry-run shows dev dependencies for Node.js", async () => {
  const testDir = await Deno.makeTempDir();
  const projectDir = join(testDir, "test-node-project");

  try {
    const result = await runInit([
      projectDir,
      "--dry-run",
      "--runtime",
      "node",
      "--package-manager",
      "npm",
    ]);

    // Check dev dependencies
    assertStringIncludes(result.output, "Would install dev dependencies:");
    assertStringIncludes(result.output, "@biomejs/biome");

    // Verify no files were created
    assertEquals(await exists(projectDir), false);
  } finally {
    await Deno.remove(testDir, { recursive: true });
  }
});
+158 −48
Original line number Diff line number Diff line
@@ -615,7 +615,12 @@ export const command = new Command()
    "-q, --message-queue <message-queue:message-queue>",
    "Choose the message queue to use for background jobs.",
  )
  .option(
    "--dry-run",
    "Show what would be created without actually creating files.",
  )
  .action(async (options, dir: string) => {
    const dryRun = options.dryRun ?? false;
    const projectName = basename(
      await exists(dir) ? await Deno.realPath(dir) : normalize(dir),
    );
@@ -1001,12 +1006,42 @@ await configure({
      ...initializer.files,
    };
    const { prependFiles } = initializer;
    await Deno.mkdir(dir, { recursive: true });
    for await (const _ of Deno.readDir(dir)) {

    if (dryRun) {
      console.log(
        colors.bold.yellow("🔍 DRY RUN MODE - No files will be created\n"),
      );
    }

    // Check if directory is empty
    const checkDirectoryEmpty = async (path: string) => {
      try {
        for await (const _ of Deno.readDir(path)) {
          console.error("The directory is not empty.  Aborting.");
          Deno.exit(1);
        }
      } catch (e) {
        if (!(e instanceof Deno.errors.NotFound)) {
          throw e;
        }
      }
    };

    if (dryRun) {
      await checkDirectoryEmpty(dir);
    } else {
      await Deno.mkdir(dir, { recursive: true });
      await checkDirectoryEmpty(dir);
    }
    if (initializer.command != null) {
      if (dryRun) {
        console.log(colors.bold.cyan("📦 Would run command:"));
        console.log(
          `  ${
            [initializer.command[0], ...initializer.command.slice(1)].join(" ")
          }\n`,
        );
      } else {
        const cmd = new Deno.Command(initializer.command[0], {
          args: initializer.command.slice(1),
          cwd: dir,
@@ -1020,8 +1055,10 @@ await configure({
          Deno.exit(1);
        }
      }
    }
    if (runtime !== "deno") {
      const packageJsonPath = join(dir, "package.json");
      if (!dryRun) {
        try {
          await Deno.stat(packageJsonPath);
        } catch (e) {
@@ -1030,6 +1067,7 @@ await configure({
          } else throw e;
        }
      }
    }
    const dependencies: Record<string, string> = {
      "@fedify/fedify": `^${await getLatestFedifyVersion(metadata.version)}`,
      "@logtape/logtape": "^0.8.2",
@@ -1037,12 +1075,22 @@ await configure({
      ...kvStoreDesc?.dependencies,
      ...mqDesc?.dependencies,
    };
    if (dryRun) {
      const deps = Object.entries(dependencies)
        .map(([name, version]) => `${name}@${version}`)
        .join("\n");
      if (deps) {
        console.log(colors.bold.cyan("📦 Would install dependencies:"));
        console.log(`${deps}\n`);
      }
    } else {
      await addDependencies(
        runtime,
        packageManager,
        dir,
        dependencies,
      );
    }
    if (runtime !== "deno") {
      const devDependencies: Record<string, string> = {
        "@biomejs/biome": "^1.8.3",
@@ -1050,6 +1098,15 @@ await configure({
        ...kvStoreDesc?.devDependencies,
        ...mqDesc?.devDependencies,
      };
      if (dryRun) {
        const devDeps = Object.entries(devDependencies)
          .map(([name, version]) => `${name}@${version}`)
          .join("\n");
        if (devDeps) {
          console.log(colors.bold.cyan("📦 Would install dev dependencies:"));
          console.log(`${devDeps}\n`);
        }
      } else {
        await addDependencies(
          runtime,
          packageManager,
@@ -1058,13 +1115,33 @@ await configure({
          true,
        );
      }
    }
    if (dryRun) {
      console.log(colors.bold.green("📄 Would create files:\n"));
      for (const [filename, content] of Object.entries(files)) {
        const path = join(dir, filename);
        displayFileContent(path, content);
      }
    } else {
      for (const [filename, content] of Object.entries(files)) {
        const path = join(dir, filename);
        const dirName = dirname(path);
        await Deno.mkdir(dirName, { recursive: true });
        await Deno.writeTextFile(path, content);
      }
    }
    if (prependFiles != null) {
      if (dryRun) {
        console.log(colors.bold.blue("Would prepend to files:\n"));
        for (const [filename, prefix] of Object.entries(prependFiles)) {
          const path = join(dir, filename);
          console.log(colors.blue(`${path}`));
          console.log(colors.gray("".repeat(60)));
          console.log(colors.gray("Prepending:"));
          console.log(prefix);
          console.log(colors.gray("".repeat(60)) + "\n");
        }
      } else {
        for (const [filename, prefix] of Object.entries(prependFiles)) {
          const path = join(dir, filename);
          const dirName = dirname(path);
@@ -1075,7 +1152,11 @@ await configure({
          );
        }
      }
    }
    if (runtime === "deno") {
      if (dryRun) {
        console.log(colors.bold.green("Would create/update JSON files:\n"));
      }
      await rewriteJsonFile(
        join(dir, "deno.json"),
        {},
@@ -1094,6 +1175,7 @@ await configure({
          ],
          tasks: { ...cfg.tasks, ...initializer.tasks },
        }),
        dryRun,
      );
      await rewriteJsonFile(
        join(dir, ".vscode", "settings.json"),
@@ -1142,6 +1224,7 @@ await configure({
          },
          ...vsCodeSettings,
        }),
        dryRun,
      );
      await rewriteJsonFile(
        join(dir, ".vscode", "extensions.json"),
@@ -1153,8 +1236,12 @@ await configure({
          ]),
          ...vsCodeExtensions,
        }),
        dryRun,
      );
    } else {
      if (dryRun) {
        console.log(colors.bold.green("Would create/update JSON files:\n"));
      }
      await rewriteJsonFile(
        join(dir, "package.json"),
        {},
@@ -1163,6 +1250,7 @@ await configure({
          ...cfg,
          scripts: { ...cfg.scripts, ...initializer.tasks },
        }),
        dryRun,
      );
      if (initializer.compilerOptions != null) {
        await rewriteJsonFile(
@@ -1175,6 +1263,7 @@ await configure({
              ...initializer.compilerOptions,
            },
          }),
          dryRun,
        );
      }
      await rewriteJsonFile(
@@ -1222,6 +1311,7 @@ await configure({
          },
          ...vsCodeSettings,
        }),
        dryRun,
      );
      await rewriteJsonFile(
        join(dir, ".vscode", "extensions.json"),
@@ -1233,6 +1323,7 @@ await configure({
          ]),
          ...vsCodeExtensions,
        }),
        dryRun,
      );
      await rewriteJsonFile(
        join(dir, "biome.json"),
@@ -1256,6 +1347,7 @@ await configure({
            rules: { recommended: true },
          },
        }),
        dryRun,
      );
    }
    console.error(initializer.instruction);
@@ -1290,6 +1382,18 @@ ${d(" ")} ${f(" |___/")}
`);
}

function displayFileContent(
  path: string,
  content: string,
  emoji: string = "📄",
  pathColor: (text: string) => string = colors.green,
) {
  console.log(pathColor(`${emoji} ${path}`));
  console.error(colors.gray("".repeat(60)));
  console.log(content);
  console.error(colors.gray("".repeat(60)) + "\n");
}

async function isCommandAvailable(
  { checkCommand, outputPattern }: {
    checkCommand: [string, ...string[]];
@@ -1410,6 +1514,7 @@ async function rewriteJsonFile(
  empty: any,
  // deno-lint-ignore no-explicit-any
  rewriter: (json: any) => any,
  dryRun: boolean = false,
): Promise<void> {
  let jsonText: string | null = null;
  try {
@@ -1419,9 +1524,14 @@ async function rewriteJsonFile(
  }
  let json = jsonText == null ? empty : JSON.parse(jsonText);
  json = rewriter(json);

  if (dryRun) {
    displayFileContent(path, JSON.stringify(json, null, 2));
  } else {
    await Deno.mkdir(dirname(path), { recursive: true });
    await Deno.writeTextFile(path, JSON.stringify(json, null, 2) + "\n");
  }
}

function uniqueArray<T extends boolean | number | string>(a: T[]): T[] {
  const result: T[] = [];
+22 −0
Original line number Diff line number Diff line
@@ -196,6 +196,28 @@ option. The available options are:
If it's omitted, the in-process message queue (which is for development purpose)
will be used.

### `--dry-run`: Preview without creating files

*This option is available since Fedify 1.8.0.*

The `--dry-run` option allows you to preview what files and configurations would
be created without actually creating them.  This is useful for reviewing the
project structure before committing to the initialization.

~~~~ sh
fedify init my-fedify-project --dry-run
~~~~

When using `--dry-run`, the command will:

 -  Display all files that would be created with their contents
 -  Show which dependencies would be installed
 -  Preview any commands that would be executed
 -  Not create any directories or files on your filesystem

This option works with all other initialization options, allowing you to preview
different configurations before making a decision.


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