Skip to content

Commit

Permalink
feat(cli): Turn Mintlify migrator into a CLI command (#5106)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattblank11 authored Jan 5, 2025
1 parent 6e2c245 commit 11f3aa1
Show file tree
Hide file tree
Showing 30 changed files with 407 additions and 69 deletions.
6 changes: 6 additions & 0 deletions fern/pages/changelogs/cli/2025-01-05.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## 0.46.20
**`(feat):`** The `fern init` command now supports a `--mintlify` option. You can pass in
the path to your `mint.json` and the Fern CLI will generate a fern documentation
website.


3 changes: 1 addition & 2 deletions packages/cli/cli/src/__test__/checkOutputDirectory.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { AbsoluteFilePath, join, RelativeFilePath, isCI } from "@fern-api/fs-utils";
import { mkdir, writeFile } from "fs/promises";
import tmp from "tmp-promise";
import { checkOutputDirectory } from "../commands/generate/checkOutputDirectory";
import { getOutputDirectories } from "../persistence/output-directories/getOutputDirectories";
import { storeOutputDirectories } from "../persistence/output-directories/storeOutputDirectories";
import { describe, it, expect, beforeEach, vi, Mock, afterEach } from "vitest";
import { CliContext } from "../cli-context/CliContext";
import { isCI } from "../utils/isCI";

vi.mock("../utils/isCI", () => ({
isCI: vi.fn().mockReturnValue(false)
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/cli/src/__test__/checkOutputDirectoryCI.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { mkdir, writeFile } from "fs/promises";
import tmp from "tmp-promise";
import { checkOutputDirectory } from "../commands/generate/checkOutputDirectory";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CliContext } from "../cli-context/CliContext";
import { isCI } from "../utils/isCI";
import { checkOutputDirectory } from "../commands/generate/checkOutputDirectory";

vi.mock("../utils/isCI", () => ({
isCI: vi.fn().mockReturnValue(true)
Expand Down
32 changes: 21 additions & 11 deletions packages/cli/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
#!/usr/bin/env node

import {
fernConfigJson,
generatorsYml,
GENERATORS_CONFIGURATION_FILENAME,
generatorsYml,
getFernDirectory,
loadProjectConfig,
PROJECT_CONFIG_FILENAME
} from "@fern-api/configuration-loader";
import { AbsoluteFilePath, cwd, doesPathExist, resolve } from "@fern-api/fs-utils";
import { initializeAPI, initializeDocs } from "@fern-api/init";
import { LogLevel, LOG_LEVELS } from "@fern-api/logger";
import { AbsoluteFilePath, cwd, doesPathExist, isURL, resolve } from "@fern-api/fs-utils";
import { initializeAPI, initializeDocs, initializeWithMintlify } from "@fern-api/init";
import { LOG_LEVELS, LogLevel } from "@fern-api/logger";
import { askToLogin, login } from "@fern-api/login";
import { FernCliError, LoggableFernCliError } from "@fern-api/task-context";
import { RUNTIME } from "@fern-typescript/fetcher";
import getPort from "get-port";
import { Argv } from "yargs";
import { hideBin } from "yargs/helpers";
Expand All @@ -25,12 +25,15 @@ import { addGeneratorCommands, addGetOrganizationCommand } from "./cliV2";
import { addGeneratorToWorkspaces } from "./commands/add-generator/addGeneratorToWorkspaces";
import { previewDocsWorkspace } from "./commands/docs-dev/devDocsWorkspace";
import { formatWorkspaces } from "./commands/format/formatWorkspaces";
import { generateDynamicIrForWorkspaces } from "./commands/generate-dynamic-ir/generateDynamicIrForWorkspaces";
import { generateFdrApiDefinitionForWorkspaces } from "./commands/generate-fdr/generateFdrApiDefinitionForWorkspaces";
import { generateIrForWorkspaces } from "./commands/generate-ir/generateIrForWorkspaces";
import { generateOpenApiToFdrApiDefinitionForWorkspaces } from "./commands/generate-openapi-fdr/generateOpenApiToFdrApiDefinitionForWorkspaces";
import { generateOpenAPIIrForWorkspaces } from "./commands/generate-openapi-ir/generateOpenAPIIrForWorkspaces";
import { writeOverridesForWorkspaces } from "./commands/generate-overrides/writeOverridesForWorkspaces";
import { generateAPIWorkspaces, GenerationMode } from "./commands/generate/generateAPIWorkspaces";
import { generateDocsWorkspace } from "./commands/generate/generateDocsWorkspace";
import { generateJsonschemaForWorkspaces } from "./commands/jsonschema/generateJsonschemaForWorkspace";
import { mockServer } from "./commands/mock/mockServer";
import { registerWorkspacesV1 } from "./commands/register/registerWorkspacesV1";
import { registerWorkspacesV2 } from "./commands/register/registerWorkspacesV2";
Expand All @@ -40,14 +43,9 @@ import { updateApiSpec } from "./commands/upgrade/updateApiSpec";
import { upgrade } from "./commands/upgrade/upgrade";
import { validateWorkspaces } from "./commands/validate/validateWorkspaces";
import { writeDefinitionForWorkspaces } from "./commands/write-definition/writeDefinitionForWorkspaces";
import { writeDocsDefinitionForProject } from "./commands/write-docs-definition/writeDocsDefinitionForProject";
import { FERN_CWD_ENV_VAR } from "./cwd";
import { rerunFernCliAtVersion } from "./rerunFernCliAtVersion";
import { isURL } from "./utils/isUrl";
import { generateJsonschemaForWorkspaces } from "./commands/jsonschema/generateJsonschemaForWorkspace";
import { generateDynamicIrForWorkspaces } from "./commands/generate-dynamic-ir/generateDynamicIrForWorkspaces";
import { writeDocsDefinitionForProject } from "./commands/write-docs-definition/writeDocsDefinitionForProject";
import { RUNTIME } from "@fern-typescript/fetcher";
import { generateOpenApiToFdrApiDefinitionForWorkspaces } from "./commands/generate-openapi-fdr/generateOpenApiToFdrApiDefinitionForWorkspaces";

void runCli();

Expand Down Expand Up @@ -228,6 +226,10 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
.option("openapi", {
type: "string",
description: "Filepath or url to an existing OpenAPI spec"
})
.option("mintlify", {
type: "string",
description: "Migrate docs from Mintlify"
}),
async (argv) => {
if (argv.api != null && argv.docs != null) {
Expand All @@ -240,6 +242,14 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
taskContext: context
});
});
} else if (argv.mintlify != null) {
await cliContext.runTask(async (taskContext) => {
await initializeWithMintlify({
pathToMintJson: argv.mintlify,
taskContext,
versionOfCli: await getLatestVersionOfCli({ cliEnvironment: cliContext.environment })
});
});
} else {
let absoluteOpenApiPath: AbsoluteFilePath | undefined = undefined;
if (argv.openapi != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { AbsoluteFilePath, doesPathExist } from "@fern-api/fs-utils";
import { AbsoluteFilePath, doesPathExist, isCI } from "@fern-api/fs-utils";
import { readdir } from "fs/promises";
import { CliContext } from "../../cli-context/CliContext";
import { getOutputDirectories } from "../../persistence/output-directories/getOutputDirectories";
import { storeOutputDirectories } from "../../persistence/output-directories/storeOutputDirectories";
import { isCI } from "../../utils/isCI";

export interface CheckOutputDirectoryResult {
shouldProceed: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { createOrganizationIfDoesNotExist, FernToken, FernUserToken } from "@fern-api/auth";
import { createOrganizationIfDoesNotExist, FernToken } from "@fern-api/auth";
import { Values } from "@fern-api/core-utils";
import { join, RelativeFilePath } from "@fern-api/fs-utils";
import { askToLogin } from "@fern-api/login";
import { Project } from "@fern-api/project-loader";
import { CliContext } from "../../cli-context/CliContext";
import { PREVIEW_DIRECTORY } from "../../constants";
import { generateWorkspace } from "./generateAPIWorkspace";
import { checkOutputDirectory } from "./checkOutputDirectory";
import { isCI } from "../../utils/isCI";
import { generateWorkspace } from "./generateAPIWorkspace";

export const GenerationMode = {
PullRequest: "pull-request"
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/cli/versions.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@

- changelogEntry:
- summary: |
The `fern init` command now supports a `--mintlify` option. You can pass in
the path to your `mint.json` and the Fern CLI will generate a fern documentation
website.
type: feat
irVersion: 53
version: 0.46.20

- changelogEntry:
- summary: |
If a schema in OpenAPI or AsyncAPI has `additionalProperties: true` then the Fern CLI will now respect bringing in
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { AbsoluteFilePath, doesPathExist, join, RelativeFilePath } from "@fern-api/fs-utils";
import path from "path";
import { MintlifyImporter } from "..";
import { mkdir, rmdir } from "fs/promises";
import { createMockTaskContext } from "@fern-api/task-context";
import { CONSOLE_LOGGER } from "@fern-api/logger";
import { FernDocsBuilderImpl } from "@fern-api/docs-importer-commons";
import { writeFile } from "fs/promises";
import { FERN_DIRECTORY, PROJECT_CONFIG_FILENAME } from "@fern-api/configuration";
import { createMockTaskContext } from "@fern-api/task-context";
import { mkdir, rmdir } from "fs/promises";
import path from "path";
import { runMintlifyMigration } from "../runMintlifyMigration";

const FIXTURES_PATH = AbsoluteFilePath.of(path.join(__dirname, "fixtures"));
const OUTPUTS_PATH = AbsoluteFilePath.of(path.join(__dirname, "outputs"));
Expand All @@ -17,40 +14,24 @@ describe("add-generator-groups", () => {
for (const fixture of fixtures) {
it(`${fixture}`, async () => {
const fixturePath = join(FIXTURES_PATH, RelativeFilePath.of(fixture));
const absolutePathToMintJson = join(fixturePath, RelativeFilePath.of("mint.json"));

const outputPath = join(OUTPUTS_PATH, RelativeFilePath.of(fixture));

if (await doesPathExist(outputPath)) {
await rmdir(outputPath, { recursive: true });
}

await mkdir(outputPath, { recursive: true });

const context = createMockTaskContext({ logger: CONSOLE_LOGGER });
const mintlifyImporter = new MintlifyImporter({
context
});
const builder = new FernDocsBuilderImpl();
const taskContext = createMockTaskContext({ logger: CONSOLE_LOGGER });

await mintlifyImporter.import({
args: { absolutePathToMintJson: join(fixturePath, RelativeFilePath.of("mint.json")) },
builder
await runMintlifyMigration({
absolutePathToMintJson,
outputPath,
taskContext,
versionOfCli: "0.0.0"
});

await builder.build({ outputDirectory: outputPath });

await writeFile(
join(
AbsoluteFilePath.of(outputPath),
RelativeFilePath.of(FERN_DIRECTORY),
RelativeFilePath.of(PROJECT_CONFIG_FILENAME)
),
JSON.stringify(
{
version: "*",
organization: "fern"
},
undefined,
4
)
);
});
}
});
19 changes: 17 additions & 2 deletions packages/cli/docs-importers/mintlify/src/convertNavigationItem.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { docsYml } from "@fern-api/configuration";
import { isNonNullish } from "@fern-api/core-utils";
import { FernDocsBuilder } from "@fern-api/docs-importer-commons";
import { AbsoluteFilePath, dirname, join, RelativeFilePath } from "@fern-api/fs-utils";
import { AbsoluteFilePath, dirname, doesPathExist, join, RelativeFilePath } from "@fern-api/fs-utils";
import { TaskContext } from "@fern-api/task-context";
import { convertMarkdown } from "./convertMarkdown";
import { MintNavigationItem } from "./mintlify";
Expand Down Expand Up @@ -30,12 +30,27 @@ export async function convertNavigationItem({
const relativeFilepathFromRoot = RelativeFilePath.of(
item.endsWith("mdx") ? item : `${item}.mdx`
);

const absoluteFilepathToMarkdown = join(
dirname(absolutePathToMintJson),
relativeFilepathFromRoot
);

// Ensure the file exists before we convert it
const fileExists = await doesPathExist(absoluteFilepathToMarkdown);

// If we return undefined then we will filter out the page from the section below
if (!fileExists) {
return undefined;
}

const convertedMarkdown = await convertMarkdown({
absolutePathToMintJson,
relativeFilepathFromRoot,
absoluteFilepathToMarkdown: join(dirname(absolutePathToMintJson), relativeFilepathFromRoot),
absoluteFilepathToMarkdown,
builder
});

if (convertedMarkdown.mintlifyFrontmatter.openapi != null) {
return undefined;
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/docs-importers/mintlify/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { MintlifyImporter } from "./MintlifyImporter";
export { runMintlifyMigration } from "./runMintlifyMigration";
49 changes: 49 additions & 0 deletions packages/cli/docs-importers/mintlify/src/runMintlifyMigration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FERN_DIRECTORY, PROJECT_CONFIG_FILENAME } from "@fern-api/configuration";
import { FernDocsBuilderImpl } from "@fern-api/docs-importer-commons";
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { TaskContext } from "@fern-api/task-context";
import { writeFile } from "fs/promises";
import { MintlifyImporter } from "./MintlifyImporter";

interface RunMintlifyMigrationParams {
absolutePathToMintJson: AbsoluteFilePath;
outputPath: AbsoluteFilePath;
taskContext: TaskContext;
versionOfCli: string;
}

export async function runMintlifyMigration({
absolutePathToMintJson,
outputPath,
taskContext,
versionOfCli
}: RunMintlifyMigrationParams): Promise<void> {
const mintlifyImporter = new MintlifyImporter({
context: taskContext
});

const builder = new FernDocsBuilderImpl();

await mintlifyImporter.import({
args: { absolutePathToMintJson },
builder
});

await builder.build({ outputDirectory: outputPath });

await writeFile(
join(
AbsoluteFilePath.of(outputPath),
RelativeFilePath.of(FERN_DIRECTORY),
RelativeFilePath.of(PROJECT_CONFIG_FILENAME)
),
JSON.stringify(
{
version: versionOfCli,
organization: "fern"
},
undefined,
4
)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,54 @@ groups:
]
`;

exports[`fern init > init mintlify 1`] = `
[
{
"contents": [
{
"contents": "instances:
- url: https://test-api.docs.buildwithfern.com
title: Test API
favicon: favicon.png
logo:
light: logo.png
dark: logo.png
height: 28
colors:
accentPrimary:
dark: '#0050B4'
light: '#4D9CFF'
background: {}
navigation: []
",
"name": "docs.yml",
"type": "file",
},
{
"contents": "<binary>",
"name": "favicon.png",
"type": "file",
},
{
"contents": "{
"version": "0.0.0",
"organization": "fern"
}",
"name": "fern.config.json",
"type": "file",
},
{
"contents": "<binary>",
"name": "logo.png",
"type": "file",
},
],
"name": "fern",
"type": "directory",
},
]
`;

exports[`fern init > init openapi 1`] = `
[
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions packages/cli/ete-tests/src/tests/init/fixtures/mintlify/mint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "Test API",
"logo": "/logo.png",
"favicon": "/favicon.png",
"colors": {
"primary": "#0069ED",
"light": "#4D9CFF",
"dark": "#0050B4"
},
"topbarLinks": [
{
"name": "Login",
"url": "https://example.com/login"
}
],
"topbarCtaButton": {
"name": "Get Started",
"url": "https://example.com/register"
},
"navigation": []
}
Loading

0 comments on commit 11f3aa1

Please sign in to comment.