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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GenerateCommand } from "../command.js";
// The orchestrator (`runLibraryDocsGeneration`) is unit-tested in
// `@fern-api/library-docs-generator`. These tests cover only the v2
// wrapper's responsibilities: workspace/docs validation and delegation
// to the orchestrator with the correct adapter inputs.
// to the (local-only) orchestrator with the correct adapter inputs.
vi.mock("@fern-api/library-docs-generator", () => ({
runLibraryDocsGeneration: vi.fn()
}));
Expand Down Expand Up @@ -57,7 +57,7 @@ describe("GenerateCommand", () => {
await expect(cmd.handle(context, {} as GenerateCommand.Args)).rejects.toThrow(CliError);
});

it("delegates to runLibraryDocsGeneration with adapter inputs", async () => {
it("delegates to runLibraryDocsGeneration with adapter inputs and no authentication", async () => {
const cmd = new GenerateCommand();
const context = makeContext({
docs: {
Expand All @@ -75,10 +75,14 @@ describe("GenerateCommand", () => {
const call = (runLibraryDocsGeneration as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(call).toBeDefined();
expect(call.library).toBe("my-sdk");
expect(call.orgId).toBe("test-org");
expect(call.tokenValue).toBe("tok-123");
// docsDirectoryPath should be the *directory* of docs.yml, not the file itself.
expect(call.docsDirectoryPath).toBe("/tmp/proj/fern");
// Generation is local-only: no token or org is threaded through and
// the command never prompts for authentication.
expect(call.tokenValue).toBeUndefined();
expect(call.orgId).toBeUndefined();
expect(context.getTokenOrPrompt).not.toHaveBeenCalled();
expect(context.verifyOrgAccess).not.toHaveBeenCalled();
});

it("propagates errors thrown by the orchestrator", async () => {
Expand Down
5 changes: 0 additions & 5 deletions packages/cli/cli-v2/src/commands/docs/md/generate/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ export class GenerateCommand {
});
}

const token = await context.getTokenOrPrompt();
await context.verifyOrgAccess({ organization: workspace.org, token });

const docsFilePath = workspace.docs.absoluteFilePath ?? workspace.absoluteFilePath ?? context.cwd;
const docsDirectoryPath = AbsoluteFilePath.of(dirname(docsFilePath));

Expand All @@ -53,8 +50,6 @@ export class GenerateCommand {
libraries,
library: args.library,
docsDirectoryPath,
orgId: workspace.org,
tokenValue: token.value,
context: taskContext,
wrapStep: withSpinner
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- summary: |
`fern docs md generate` now always runs the library parser(s) locally using Docker.
The remote (server-side) generation path has been removed, so the command no longer
requires authentication, and the `--local` flag is deprecated as a no-op.
type: feat
8 changes: 4 additions & 4 deletions packages/cli/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2101,8 +2101,9 @@ function addDocsMdGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliCo
})
.option("local", {
boolean: true,
default: false,
description: "Run the library parser(s) locally using Docker instead of Fern's servers"
default: true,
hidden: true,
deprecated: "Library docs generation always runs locally using Docker; --local is no longer needed."
}),
async (argv) => {
cliContext.instrumentPostHogEvent({
Expand All @@ -2117,8 +2118,7 @@ function addDocsMdGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliCo
await generateLibraryDocs({
project,
cliContext,
library: argv.library,
local: argv.local
library: argv.library
});
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { describe, expect, it, type Mock, vi } from "vitest";

// Per-library orchestration is unit-tested in `@fern-api/library-docs-generator`.
// These tests cover only the v1 wrapper: workspace validation, auth flow,
// and delegation to the orchestrator.
vi.mock("@fern-api/login", () => ({
askToLogin: vi.fn()
}));

// These tests cover only the v1 wrapper: workspace validation and delegation
// to the (local-only) orchestrator.
vi.mock("@fern-api/library-docs-generator", () => ({
runLibraryDocsGeneration: vi.fn()
}));

import { runLibraryDocsGeneration } from "@fern-api/library-docs-generator";
import { askToLogin } from "@fern-api/login";
import { type GenerateLibraryDocsOptions, generateLibraryDocs } from "../generateLibraryDocs.js";

function makeCliContext() {
Expand Down Expand Up @@ -68,8 +63,7 @@ describe("generateLibraryDocs", () => {
generateLibraryDocs({
project: project as unknown as GenerateLibraryDocsOptions["project"],
cliContext: ctx as unknown as GenerateLibraryDocsOptions["cliContext"],
library: undefined,
local: false
library: undefined
})
).rejects.toThrow("No docs workspace found");
});
Expand All @@ -82,73 +76,31 @@ describe("generateLibraryDocs", () => {
generateLibraryDocs({
project: project as unknown as GenerateLibraryDocsOptions["project"],
cliContext: ctx as unknown as GenerateLibraryDocsOptions["cliContext"],
library: undefined,
local: false
library: undefined
})
).rejects.toThrow("No libraries configured");
});

it("fails when authentication returns null", async () => {
const ctx = makeCliContext();
const project = makeProject({
libraries: { "my-sdk": { input: { git: "https://x" }, output: { path: "./d" }, lang: "python" } }
});
(askToLogin as Mock).mockResolvedValue(null);

await expect(
generateLibraryDocs({
project: project as unknown as GenerateLibraryDocsOptions["project"],
cliContext: ctx as unknown as GenerateLibraryDocsOptions["cliContext"],
library: undefined,
local: false
})
).rejects.toThrow("Failed to authenticate");
});

it("delegates to runLibraryDocsGeneration with the workspace's libraries and token", async () => {
it("delegates to runLibraryDocsGeneration without any authentication", async () => {
const ctx = makeCliContext();
const project = makeProject({
libraries: { "my-sdk": { input: { git: "https://x" }, output: { path: "./d" }, lang: "python" } }
});
(askToLogin as Mock).mockResolvedValue({ type: "user", value: "tok-xyz" });
(runLibraryDocsGeneration as Mock).mockResolvedValue({ successful: 1 });

await generateLibraryDocs({
project: project as unknown as GenerateLibraryDocsOptions["project"],
cliContext: ctx as unknown as GenerateLibraryDocsOptions["cliContext"],
library: "my-sdk",
local: false
library: "my-sdk"
});

expect(runLibraryDocsGeneration).toHaveBeenCalledTimes(1);
const call = (runLibraryDocsGeneration as Mock).mock.calls[0]?.[0];
expect(call).toBeDefined();
expect(call.library).toBe("my-sdk");
expect(call.orgId).toBe("test-org");
expect(call.tokenValue).toBe("tok-xyz");
expect(call.docsDirectoryPath).toBe("/home/user/project/fern");
});

it("local mode: skips authentication and delegates with local: true and no token", async () => {
const ctx = makeCliContext();
const project = makeProject({
libraries: { "my-sdk": { input: { path: "./src" }, output: { path: "./d" }, lang: "python" } }
});
(askToLogin as Mock).mockReset();
(runLibraryDocsGeneration as Mock).mockReset();
(runLibraryDocsGeneration as Mock).mockResolvedValue({ successful: 1 });

await generateLibraryDocs({
project: project as unknown as GenerateLibraryDocsOptions["project"],
cliContext: ctx as unknown as GenerateLibraryDocsOptions["cliContext"],
library: undefined,
local: true
});

expect(askToLogin).not.toHaveBeenCalled();
expect(runLibraryDocsGeneration).toHaveBeenCalledTimes(1);
const call = (runLibraryDocsGeneration as Mock).mock.calls[0]?.[0];
expect(call.local).toBe(true);
// Generation is local-only: no token or org is threaded through.
expect(call.tokenValue).toBeUndefined();
expect(call.orgId).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { FernToken } from "@fern-api/auth";
import { runLibraryDocsGeneration } from "@fern-api/library-docs-generator";
import { askToLogin } from "@fern-api/login";
import { Project } from "@fern-api/project-loader";
import { CliError } from "@fern-api/task-context";

Expand All @@ -12,26 +10,16 @@ export interface GenerateLibraryDocsOptions {
cliContext: CliContext;
/** If specified, only generate docs for this library */
library: string | undefined;
/** Run the parser(s) locally in Docker instead of using Fern's servers. */
local: boolean;
}

/**
* Generate library documentation from source code.
*
* Loads the docs workspace and delegates to the shared orchestrator in
* `@fern-api/library-docs-generator`. By default it authenticates with FDR,
* starts server-side parsing, polls for completion, downloads the resulting IR
* from S3, and runs the local MDX generator. With `local`, it skips
* authentication and runs the parser Docker images directly on the user's
* machine.
* `@fern-api/library-docs-generator`, which runs the parser Docker images
* on the user's machine — no authentication or network calls are required.
*/
export async function generateLibraryDocs({
project,
cliContext,
library,
local
}: GenerateLibraryDocsOptions): Promise<void> {
export async function generateLibraryDocs({ project, cliContext, library }: GenerateLibraryDocsOptions): Promise<void> {
const docsWorkspace = project.docsWorkspaces;

if (docsWorkspace == null) {
Expand Down Expand Up @@ -61,30 +49,12 @@ export async function generateLibraryDocs({
return;
}

let tokenValue: string | undefined;
if (!local) {
const token: FernToken | null = await cliContext.runTask(async (context) => {
return askToLogin(context);
});

if (token == null) {
cliContext.failAndThrow("Failed to authenticate. Please run 'fern login' first.", undefined, {
code: CliError.Code.AuthError
});
return;
}
tokenValue = token.value;
}

await cliContext.runTask(async (context) => {
const { successful } = await runLibraryDocsGeneration({
libraries,
library,
docsDirectoryPath: docsWorkspace.absoluteFilePath,
orgId: project.config.organization,
tokenValue,
context,
local
context
});

if (successful > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,8 +560,8 @@ export type ParsedApiReferenceLayoutItem =

/**
* Parsed configuration for the source location of a library documentation source.
* A `git` input can be generated remotely or resolved locally with `--local`;
* a `path` input points at a local checkout and requires `--local`.
* A `git` input is cloned and resolved locally; a `path` input points at a
* local checkout.
*/
export type ParsedLibraryInputConfiguration =
| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { runContainer } from "@fern-api/docker-utils";
import { runLibraryDocsGeneration } from "../orchestrate.js";

/**
* End-to-end coverage of the `fern docs md generate --local` path for a
* End-to-end coverage of the `fern docs md generate` path for a
* local path and git input libraries.
*
* Only the Docker boundary (`runContainer`) is mocked — it emits a realistic
Expand All @@ -29,9 +29,8 @@ import { runLibraryDocsGeneration } from "../orchestrate.js";
* source-path resolution, `LocalParserRunner`, IR validation, and the Python
* MDX generator writing files to `output.path` on disk.
*
* This is the local analogue of the git/remote render coverage in
* fern-platform's `library-docs-generate.spec.ts`, and closes the seam that
* let a `path` input be required by `--local` yet unhandled elsewhere.
* This is the local analogue of the git render coverage in
* fern-platform's `library-docs-generate.spec.ts`.
*/

type LoggerMock = { info: Mock; error: Mock; debug: Mock; warn: Mock; trace: Mock; log: Mock };
Expand Down Expand Up @@ -142,7 +141,7 @@ function pythonIr(): FdrAPI.libraryDocs.PythonLibraryDocsIr {
} as unknown as FdrAPI.libraryDocs.PythonLibraryDocsIr;
}

describe("runLibraryDocsGeneration — --local path input (end-to-end to disk)", () => {
describe("runLibraryDocsGeneration — path input (end-to-end to disk)", () => {
let docsDir: string;

beforeEach(() => {
Expand Down Expand Up @@ -178,9 +177,7 @@ describe("runLibraryDocsGeneration — --local path input (end-to-end to disk)",
const result = await runLibraryDocsGeneration({
libraries,
docsDirectoryPath: AbsoluteFilePath.of(docsDir),
orgId: "smoke-test",
context: makeContext(),
local: true
context: makeContext()
});

expect(result).toEqual({ successful: 1 });
Expand Down Expand Up @@ -245,9 +242,7 @@ describe("runLibraryDocsGeneration — --local path input (end-to-end to disk)",
runLibraryDocsGeneration({
libraries,
docsDirectoryPath: AbsoluteFilePath.of(docsDir),
orgId: "smoke-test",
context: makeContext(),
local: true
context: makeContext()
})
).resolves.toEqual({ successful: 1 });

Expand Down
Loading
Loading