Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- summary: |
Rename the `--pack` and `--pack-mode` flags on `fern generate` to `--package` and
`--package-mode`, and add `--package-only`, which builds the distributable artifact into
`fern-dist/` and removes the generated SDK source, leaving only the package in the output
directory.
type: feat

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

Removing --pack/--pack-mode with no alias is a breaking CLI change; type: feat will bury it in a minor release note. Use the breaking type (break) so users who scripted against --pack actually see it — or keep hidden aliases for one release.

41 changes: 26 additions & 15 deletions packages/cli/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -875,17 +875,23 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
description:
"Generate test files even when generating to a local file system (tests are normally only generated for GitHub output modes)"
})
.option("pack", {
.option("package", {
boolean: true,
default: false,
description:
"After generating to the local file system, build distributable package artifacts (npm tarball, wheel, JAR, NuGet package, gem, etc.) into a fern-dist/ folder inside the output directory."
})
.option("pack-mode", {
.option("package-mode", {
choices: ["host", "docker"] as const,
default: "host" as const,
description:
"Where --pack runs the packaging toolchain: 'host' uses toolchains installed on this machine; 'docker' runs each toolchain inside an official Docker image (node, python, gradle, dotnet/sdk, ruby, composer, rust) with the output directory mounted, so no local toolchains are needed."
"Where --package runs the packaging toolchain: 'host' uses toolchains installed on this machine; 'docker' runs each toolchain inside an official Docker image (node, python, gradle, dotnet/sdk, ruby, composer, rust) with the output directory mounted, so no local toolchains are needed."
})
.option("package-only", {
boolean: true,
default: false,
description:
"Like --package, but only the fern-dist/ artifact is kept in the output directory — the generated SDK source is removed after the package is built."
}),
async (argv) => {
if (argv.api != null && argv.api.length > 0 && argv.docs != null) {
Expand Down Expand Up @@ -961,22 +967,25 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
{ code: CliError.Code.ConfigError }
);
}
if (argv.pack && argv.preview) {
return cliContext.failWithoutThrowing("The --pack flag cannot be used with --preview.", undefined, {
const shouldPackage = argv.package || argv.packageOnly;
if (shouldPackage && argv.preview) {
return cliContext.failWithoutThrowing("The --package flag cannot be used with --preview.", undefined, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

When the user passed --package-only, these messages say --package, which they never typed. Consider deriving the flag name from which option was set, e.g. const packageFlag = argv.packageOnly ? "--package-only" : "--package"; and interpolating it into the three validation messages.

code: CliError.Code.ConfigError
});
}
if (argv.pack && argv.docs != null) {
if (shouldPackage && argv.docs != null) {
return cliContext.failWithoutThrowing(
"The --pack flag can only be used for API generation, not docs generation.",
"The --package flag can only be used for API generation, not docs generation.",
undefined,
{ code: CliError.Code.ConfigError }
);
}
if (argv.packMode !== "host" && !argv.pack) {
return cliContext.failWithoutThrowing("The --pack-mode flag can only be used with --pack.", undefined, {
code: CliError.Code.ConfigError
});
if (argv.packageMode !== "host" && !shouldPackage) {
return cliContext.failWithoutThrowing(
"The --package-mode flag can only be used with --package.",
undefined,
{ code: CliError.Code.ConfigError }
);
}
if (argv.output != null && argv.docs != null) {
return cliContext.failWithoutThrowing(
Expand Down Expand Up @@ -1018,8 +1027,9 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
requireEnvVars: argv["require-env-vars"],
skipIfNoDiff: argv["skip-if-no-diff"],
generateTests: argv["generate-tests"],
pack: argv.pack,
packMode: argv.packMode
pack: shouldPackage,
packMode: argv.packageMode,
packOnly: argv.packageOnly
});
}
if (argv.docs != null) {
Expand Down Expand Up @@ -1083,8 +1093,9 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
requireEnvVars: argv["require-env-vars"],
skipIfNoDiff: argv["skip-if-no-diff"],
generateTests: argv["generate-tests"],
pack: argv.pack,
packMode: argv.packMode
pack: shouldPackage,
packMode: argv.packageMode,
packOnly: argv.packageOnly
});
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,67 @@ describe("packLocalOutputForGroup", () => {
expect(command).toBe("podman");
});

it("removes everything except fern-dist when packOnly is set", async () => {
await writeFile(path.join(outputDir, "go.mod"), "module example.com/test\n");
await mkdir(path.join(outputDir, "client"), { recursive: true });
await writeFile(path.join(outputDir, "client", "client.go"), "package client\n");
const group = {
groupName: "test",
audiences: { type: "all" },
generators: [createGenerator({ name: "fernapi/fern-go-sdk", language: "go", outputPath: outputDir })]
} as unknown as generatorsYml.GeneratorGroup;

await packLocalOutputForGroup({ group, context: createMockTaskContext(), packOnly: true });

expect(await readdir(outputDir)).toEqual(["fern-dist"]);
const distFiles = await readdir(path.join(outputDir, "fern-dist"));
expect(distFiles).toEqual([`${path.basename(outputDir)}-source.zip`]);
});

it("preserves .git, .fernignore, and fernignore-listed paths when packOnly is set", async () => {
await writeFile(path.join(outputDir, "go.mod"), "module example.com/test\n");
await mkdir(path.join(outputDir, ".git"), { recursive: true });
await writeFile(path.join(outputDir, ".git", "HEAD"), "ref: refs/heads/main\n");
await mkdir(path.join(outputDir, "custom"), { recursive: true });
await writeFile(path.join(outputDir, "custom", "handwritten.go"), "package custom\n");
await writeFile(path.join(outputDir, ".fernignore"), "custom/**\n");
const group = {
groupName: "test",
audiences: { type: "all" },
generators: [createGenerator({ name: "fernapi/fern-go-sdk", language: "go", outputPath: outputDir })]
} as unknown as generatorsYml.GeneratorGroup;

await packLocalOutputForGroup({ group, context: createMockTaskContext(), packOnly: true });

expect((await readdir(outputDir)).sort()).toEqual([".fernignore", ".git", "custom", "fern-dist"]);
});

it("does not wipe the output directory when no artifact is produced (swift) and packOnly is set", async () => {
await writeFile(path.join(outputDir, "Package.swift"), "// swift-tools-version:5.9\n");
const group = {
groupName: "test",
audiences: { type: "all" },
generators: [createGenerator({ name: "fernapi/fern-swift-sdk", language: "swift", outputPath: outputDir })]
} as unknown as generatorsYml.GeneratorGroup;

await packLocalOutputForGroup({ group, context: createMockTaskContext(), packOnly: true });

expect(await readdir(outputDir)).toEqual(["Package.swift"]);
});

it("keeps generated source alongside fern-dist when packOnly is not set", async () => {
await writeFile(path.join(outputDir, "go.mod"), "module example.com/test\n");
const group = {
groupName: "test",
audiences: { type: "all" },
generators: [createGenerator({ name: "fernapi/fern-go-sdk", language: "go", outputPath: outputDir })]
} as unknown as generatorsYml.GeneratorGroup;

await packLocalOutputForGroup({ group, context: createMockTaskContext() });

expect((await readdir(outputDir)).sort()).toEqual(["fern-dist", "go.mod"]);
});

it("fails when packaging a generator errors", async () => {
loggingExecaMock.mockRejectedValueOnce(new Error("python3 not found"));
const group = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export async function generateWorkspace({
generateTests,
automation,
pack,
packMode
packMode,
packOnly
}: {
organization: string;
workspace: AbstractAPIWorkspace<unknown>;
Expand Down Expand Up @@ -99,8 +100,10 @@ export async function generateWorkspace({
automation?: AutomationRunOptions;
/** Build distributable package artifacts for local-file-system outputs after generation. */
pack?: boolean;
/** Where --pack runs the packaging toolchain: on the host or inside Docker toolchain images. */
/** Where packaging runs the toolchain: on the host or inside Docker toolchain images. */
packMode?: PackMode;
/** Keep only the fern-dist/ artifact in the output directory, removing the generated SDK source. */
packOnly?: boolean;
}): Promise<void> {
if (workspace.generatorsConfiguration == null) {
context.logger.warn("This workspaces has no generators.yml");
Expand Down Expand Up @@ -239,7 +242,7 @@ export async function generateWorkspace({
});
}
if (pack) {
await packLocalOutputForGroup({ group, context: groupContext, mode: packMode, runner });
await packLocalOutputForGroup({ group, context: groupContext, mode: packMode, runner, packOnly });
}
})
)
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export async function generateAPIWorkspaces({
generateTests,
automation,
pack,
packMode
packMode,
packOnly
}: {
project: Project;
cliContext: CliContext;
Expand Down Expand Up @@ -90,8 +91,10 @@ export async function generateAPIWorkspaces({
automation?: AutomationRunOptions;
/** Build distributable package artifacts for local-file-system outputs after generation. */
pack?: boolean;
/** Where --pack runs the packaging toolchain: on the host or inside Docker toolchain images. */
/** Where packaging runs the toolchain: on the host or inside Docker toolchain images. */
packMode?: PackMode;
/** Keep only the fern-dist/ artifact in the output directory, removing the generated SDK source. */
packOnly?: boolean;
}): Promise<void> {
let token: FernToken | undefined = undefined;

Expand Down Expand Up @@ -195,7 +198,8 @@ export async function generateAPIWorkspaces({
generateTests,
automation,
pack,
packMode
packMode,
packOnly
});
});
})
Expand Down
Loading