diff --git a/fern/pages/changelogs/cli/2025-01-05.mdx b/fern/pages/changelogs/cli/2025-01-05.mdx new file mode 100644 index 00000000000..a69a59145f8 --- /dev/null +++ b/fern/pages/changelogs/cli/2025-01-05.mdx @@ -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. + + diff --git a/packages/cli/cli/src/__test__/checkOutputDirectory.test.ts b/packages/cli/cli/src/__test__/checkOutputDirectory.test.ts index a6251693c87..a71ab2a30d6 100644 --- a/packages/cli/cli/src/__test__/checkOutputDirectory.test.ts +++ b/packages/cli/cli/src/__test__/checkOutputDirectory.test.ts @@ -1,4 +1,4 @@ -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"; @@ -6,7 +6,6 @@ import { getOutputDirectories } from "../persistence/output-directories/getOutpu 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) diff --git a/packages/cli/cli/src/__test__/checkOutputDirectoryCI.test.ts b/packages/cli/cli/src/__test__/checkOutputDirectoryCI.test.ts index 430641ad008..3d4855d05a3 100644 --- a/packages/cli/cli/src/__test__/checkOutputDirectoryCI.test.ts +++ b/packages/cli/cli/src/__test__/checkOutputDirectoryCI.test.ts @@ -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) diff --git a/packages/cli/cli/src/cli.ts b/packages/cli/cli/src/cli.ts index a015859b267..3c29bcaa5e4 100644 --- a/packages/cli/cli/src/cli.ts +++ b/packages/cli/cli/src/cli.ts @@ -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"; @@ -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"; @@ -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(); @@ -228,6 +226,10 @@ function addInitCommand(cli: Argv, 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) { @@ -240,6 +242,14 @@ function addInitCommand(cli: Argv, 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) { diff --git a/packages/cli/cli/src/commands/generate/checkOutputDirectory.ts b/packages/cli/cli/src/commands/generate/checkOutputDirectory.ts index 7c2f2918047..53009c36e60 100644 --- a/packages/cli/cli/src/commands/generate/checkOutputDirectory.ts +++ b/packages/cli/cli/src/commands/generate/checkOutputDirectory.ts @@ -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; diff --git a/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts b/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts index 3ec5ad37a0f..b25b1ae1fd2 100644 --- a/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts +++ b/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts @@ -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" diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index cda57f60323..73d2b8a3f03 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -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 diff --git a/packages/cli/docs-importers/mintlify/src/__test__/migrateFromMintlify.test.ts b/packages/cli/docs-importers/mintlify/src/__test__/migrateFromMintlify.test.ts index 17f1dd19e27..9146288cae1 100644 --- a/packages/cli/docs-importers/mintlify/src/__test__/migrateFromMintlify.test.ts +++ b/packages/cli/docs-importers/mintlify/src/__test__/migrateFromMintlify.test.ts @@ -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")); @@ -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 - ) - ); }); } }); diff --git a/packages/cli/docs-importers/mintlify/src/convertNavigationItem.ts b/packages/cli/docs-importers/mintlify/src/convertNavigationItem.ts index a850f6bcfbb..c99ac97c0be 100644 --- a/packages/cli/docs-importers/mintlify/src/convertNavigationItem.ts +++ b/packages/cli/docs-importers/mintlify/src/convertNavigationItem.ts @@ -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"; @@ -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; } diff --git a/packages/cli/docs-importers/mintlify/src/index.ts b/packages/cli/docs-importers/mintlify/src/index.ts index 9480b4fce6e..bb8f5b89743 100644 --- a/packages/cli/docs-importers/mintlify/src/index.ts +++ b/packages/cli/docs-importers/mintlify/src/index.ts @@ -1 +1,2 @@ export { MintlifyImporter } from "./MintlifyImporter"; +export { runMintlifyMigration } from "./runMintlifyMigration"; diff --git a/packages/cli/docs-importers/mintlify/src/runMintlifyMigration.ts b/packages/cli/docs-importers/mintlify/src/runMintlifyMigration.ts new file mode 100644 index 00000000000..f220328cf0c --- /dev/null +++ b/packages/cli/docs-importers/mintlify/src/runMintlifyMigration.ts @@ -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 { + 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 + ) + ); +} diff --git a/packages/cli/ete-tests/src/tests/init/__snapshots__/init.test.ts.snap b/packages/cli/ete-tests/src/tests/init/__snapshots__/init.test.ts.snap index e29afb58707..5c9247ba7dd 100644 --- a/packages/cli/ete-tests/src/tests/init/__snapshots__/init.test.ts.snap +++ b/packages/cli/ete-tests/src/tests/init/__snapshots__/init.test.ts.snap @@ -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": "", + "name": "favicon.png", + "type": "file", + }, + { + "contents": "{ + "version": "0.0.0", + "organization": "fern" +}", + "name": "fern.config.json", + "type": "file", + }, + { + "contents": "", + "name": "logo.png", + "type": "file", + }, + ], + "name": "fern", + "type": "directory", + }, +] +`; + exports[`fern init > init openapi 1`] = ` [ { diff --git a/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/favicon.png b/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/favicon.png new file mode 100644 index 00000000000..85f2da51747 Binary files /dev/null and b/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/favicon.png differ diff --git a/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/logo.png b/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/logo.png new file mode 100644 index 00000000000..85f2da51747 Binary files /dev/null and b/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/logo.png differ diff --git a/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/mint.json b/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/mint.json new file mode 100644 index 00000000000..bcf7f20bafc --- /dev/null +++ b/packages/cli/ete-tests/src/tests/init/fixtures/mintlify/mint.json @@ -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": [] +} \ No newline at end of file diff --git a/packages/cli/ete-tests/src/tests/init/init.test.ts b/packages/cli/ete-tests/src/tests/init/init.test.ts index 9e960221fca..48e65a2d66f 100644 --- a/packages/cli/ete-tests/src/tests/init/init.test.ts +++ b/packages/cli/ete-tests/src/tests/init/init.test.ts @@ -49,7 +49,12 @@ describe("fern init", () => { RelativeFilePath.of("openapi"), RelativeFilePath.of("petstore-openapi.yml") ); - const pathOfDirectory = await init({ openApiArg: openApiPath }); + const pathOfDirectory = await init({ + additionalArgs: [ + { name: "--openapi", value: openApiPath }, + { name: "--log-level", value: "debug" } + ] + }); expect(await getDirectoryContentsForSnapshot(pathOfDirectory)).toMatchSnapshot(); }, 60_000); @@ -62,4 +67,14 @@ describe("fern init", () => { expect(await getDirectoryContentsForSnapshot(pathOfDirectory)).toMatchSnapshot(); }, 60_000); + + it("init mintlify", async () => { + const mintJsonPath = join(FIXTURES_DIR, RelativeFilePath.of("mintlify"), RelativeFilePath.of("mint.json")); + + const pathOfDirectory = await init({ + additionalArgs: [{ name: "--mintlify", value: mintJsonPath }] + }); + + expect(await getDirectoryContentsForSnapshot(pathOfDirectory, { skipBinaryContents: true })).toMatchSnapshot(); + }, 60_000); }); diff --git a/packages/cli/ete-tests/src/tests/init/init.ts b/packages/cli/ete-tests/src/tests/init/init.ts index 028cc8d95a5..261c46e69fd 100644 --- a/packages/cli/ete-tests/src/tests/init/init.ts +++ b/packages/cli/ete-tests/src/tests/init/init.ts @@ -2,19 +2,25 @@ import { AbsoluteFilePath } from "@fern-api/fs-utils"; import tmp from "tmp-promise"; import { runFernCli } from "../../utils/runFernCli"; -export async function init({ - directory, - openApiArg -}: { directory?: AbsoluteFilePath; openApiArg?: string } = {}): Promise { +interface InitOptions { + directory?: AbsoluteFilePath; + additionalArgs?: { + name: "--openapi" | "--mintlify" | "--log-level"; + value: string; + }[]; +} + +export async function init(options: InitOptions = {}): Promise { + let directory = options.directory; if (directory == null) { const tmpDir = await tmp.dir(); directory = AbsoluteFilePath.of(tmpDir.path); } const cliArgs = ["init", "--organization", "fern"]; - if (openApiArg != null) { - cliArgs.push("--openapi", openApiArg); - cliArgs.push("--log-level", "debug"); + + for (const additionalArg of options.additionalArgs ?? []) { + cliArgs.push(additionalArg.name, additionalArg.value); } await runFernCli(cliArgs, { diff --git a/packages/cli/init/package.json b/packages/cli/init/package.json index 92b21f0db50..b0e06df448b 100644 --- a/packages/cli/init/package.json +++ b/packages/cli/init/package.json @@ -36,6 +36,7 @@ "@fern-api/task-context": "workspace:*", "@fern-api/fern-definition-formatter": "workspace:*", "@fern-api/fern-definition-schema": "workspace:*", + "@fern-api/mintlify-importer": "workspace:*", "axios": "^1.7.7", "chalk": "^5.3.0", "fs-extra": "^11.1.1", @@ -45,7 +46,6 @@ }, "devDependencies": { "@types/fs-extra": "^11.0.1", - "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.8", "@types/lodash-es": "^4.17.12", "@types/node": "18.15.3", diff --git a/packages/cli/init/src/__test__/fixtures/mintlify/favicon.png b/packages/cli/init/src/__test__/fixtures/mintlify/favicon.png new file mode 100644 index 00000000000..85f2da51747 Binary files /dev/null and b/packages/cli/init/src/__test__/fixtures/mintlify/favicon.png differ diff --git a/packages/cli/init/src/__test__/fixtures/mintlify/logo.png b/packages/cli/init/src/__test__/fixtures/mintlify/logo.png new file mode 100644 index 00000000000..85f2da51747 Binary files /dev/null and b/packages/cli/init/src/__test__/fixtures/mintlify/logo.png differ diff --git a/packages/cli/init/src/__test__/fixtures/mintlify/mint.json b/packages/cli/init/src/__test__/fixtures/mintlify/mint.json new file mode 100644 index 00000000000..bcf7f20bafc --- /dev/null +++ b/packages/cli/init/src/__test__/fixtures/mintlify/mint.json @@ -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": [] +} \ No newline at end of file diff --git a/packages/cli/init/src/__test__/initializeWithMintlify.test.ts b/packages/cli/init/src/__test__/initializeWithMintlify.test.ts new file mode 100644 index 00000000000..0181b5dad31 --- /dev/null +++ b/packages/cli/init/src/__test__/initializeWithMintlify.test.ts @@ -0,0 +1,100 @@ +import { vi } from "vitest"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AbsoluteFilePath, cwd, resolve } from "@fern-api/fs-utils"; +import { runMintlifyMigration } from "@fern-api/mintlify-importer"; +import { initializeWithMintlify } from "../initializeWithMintlify"; + +// We'll mock calling runMintlifyMigration instead of actually calling it +vi.mock("@fern-api/mintlify-importer", () => ({ + runMintlifyMigration: vi.fn() +})); + +describe("initializeWithMintlify", () => { + it("Throws an error if the user provides a URL", async () => { + // We don't need to test the task context in this test, so we can type cast as any + const taskContext = { + failAndThrow: vi.fn((errorMessage: string) => { + throw new Error(errorMessage); + }) + } as any; + + await expect( + initializeWithMintlify({ + pathToMintJson: "https://example.com/mint.json", + taskContext, + versionOfCli: "0.0.0" + }) + ).rejects.toThrow(); + + expect(taskContext.failAndThrow).toHaveBeenCalledWith( + "Clone the repo locally and run this command again by referencing the path to the local mint.json file" + ); + }); + + it("Throws an error if the user does not provide a path to a mint.json file", async () => { + // We don't need to test the task context in this test, so we can type cast as any + const taskContext = { + failAndThrow: vi.fn((errorMessage: string) => { + throw new Error(errorMessage); + }) + } as any; + + await expect( + initializeWithMintlify({ + pathToMintJson: "./mint.yml", + taskContext, + versionOfCli: "0.0.0" + }) + ).rejects.toThrow(); + + expect(taskContext.failAndThrow).toHaveBeenCalledWith("Provide a path to a mint.json file"); + }); + + it("Throws an error if the mint.json file does not exist", async () => { + // We don't need to test the task context in this test, so we can type cast as any + const taskContext = { + failAndThrow: vi.fn((errorMessage: string) => { + throw new Error(errorMessage); + }) + } as any; + + await expect( + initializeWithMintlify({ + pathToMintJson: "./mint.json", + taskContext, + versionOfCli: "0.0.0" + }) + ).rejects.toThrow(); + + const absolutePathToMintJson = resolve(cwd(), "./mint.json"); + + expect(taskContext.failAndThrow).toHaveBeenCalledWith(`${absolutePathToMintJson} does not exist`); + }); + + it("Successfully runs the mintlify migration if a proper mint.json file is provided", async () => { + const taskContext = { + failAndThrow: vi.fn((errorMessage: string) => { + throw new Error(errorMessage); + }) + } as any; + + const absolutePathToMintJson = resolve(cwd(), "./src/__test__/fixtures/mintlify/mint.json"); + const outputPath = AbsoluteFilePath.of(cwd()); + + await initializeWithMintlify({ + pathToMintJson: absolutePathToMintJson, + taskContext, + versionOfCli: "0.0.0" + }); + + expect(taskContext.failAndThrow).not.toHaveBeenCalled(); + + expect(runMintlifyMigration).toHaveBeenCalledWith({ + absolutePathToMintJson, + outputPath, + taskContext, + versionOfCli: "0.0.0" + }); + }); +}); diff --git a/packages/cli/init/src/index.ts b/packages/cli/init/src/index.ts index d954fe01bad..61990c6940e 100644 --- a/packages/cli/init/src/index.ts +++ b/packages/cli/init/src/index.ts @@ -1,2 +1,3 @@ export { initializeAPI } from "./initializeAPI"; export { initializeDocs } from "./initializeDocs"; +export { initializeWithMintlify } from "./initializeWithMintlify"; diff --git a/packages/cli/init/src/initializeWithMintlify.ts b/packages/cli/init/src/initializeWithMintlify.ts new file mode 100644 index 00000000000..9bb7a244541 --- /dev/null +++ b/packages/cli/init/src/initializeWithMintlify.ts @@ -0,0 +1,47 @@ +import { AbsoluteFilePath, cwd, doesPathExist, isURL, resolve } from "@fern-api/fs-utils"; +import { runMintlifyMigration } from "@fern-api/mintlify-importer"; +import { TaskContext } from "@fern-api/task-context"; + +export const initializeWithMintlify = async ({ + pathToMintJson, + taskContext, + versionOfCli +}: { + pathToMintJson?: string; + taskContext: TaskContext; + versionOfCli: string; +}): Promise => { + // The file path should include `mint.json` in it + if (!pathToMintJson?.includes("mint.json")) { + taskContext.failAndThrow("Provide a path to a mint.json file"); + return; + } + + let absolutePathToMintJson: AbsoluteFilePath | undefined = undefined; + + // @todo get urls to work - for now, throw an error if the user provides a URL + if (isURL(pathToMintJson)) { + taskContext.failAndThrow( + "Clone the repo locally and run this command again by referencing the path to the local mint.json file" + ); + return; + } else { + absolutePathToMintJson = AbsoluteFilePath.of(resolve(cwd(), pathToMintJson)); + } + + const pathExists = await doesPathExist(absolutePathToMintJson); + + if (!pathExists || !absolutePathToMintJson) { + taskContext.failAndThrow(`${absolutePathToMintJson} does not exist`); + return; + } + + const outputPath = AbsoluteFilePath.of(cwd()); + + await runMintlifyMigration({ + absolutePathToMintJson, + outputPath, + taskContext, + versionOfCli + }); +}; diff --git a/packages/cli/init/tsconfig.json b/packages/cli/init/tsconfig.json index db84a28ad3d..cb9e85efbfc 100644 --- a/packages/cli/init/tsconfig.json +++ b/packages/cli/init/tsconfig.json @@ -32,6 +32,9 @@ }, { "path": "../fern-definition/schema" + }, + { + "path": "../docs-importers/mintlify" } ] } \ No newline at end of file diff --git a/packages/commons/fs-utils/src/getDirectoryContents.ts b/packages/commons/fs-utils/src/getDirectoryContents.ts index 0bdc79ffb37..7c04f01c6bd 100644 --- a/packages/commons/fs-utils/src/getDirectoryContents.ts +++ b/packages/commons/fs-utils/src/getDirectoryContents.ts @@ -23,6 +23,7 @@ export interface Directory { export declare namespace getDirectoryContents { export interface Options { fileExtensions?: string[]; + skipBinaryContents?: boolean; } } @@ -80,13 +81,18 @@ export interface SnapshotDirectory { contents: SnapshotFileOrDirectory[]; } +const BINARY_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".ico", ".bin"]; + export async function getDirectoryContentsForSnapshot( absolutePath: AbsoluteFilePath, - options: getDirectoryContents.Options = {} + options: getDirectoryContents.Options = { skipBinaryContents: false } ): Promise { const contents = await getDirectoryContents(absolutePath, options); const removeAbsolutePath = (fileOrDir: FileOrDirectory): SnapshotFileOrDirectory => { if (fileOrDir.type === "file") { + if (options.skipBinaryContents && BINARY_EXTENSIONS.includes(path.extname(fileOrDir.name))) { + return { type: "file", name: fileOrDir.name, contents: "" }; + } return { type: "file", name: fileOrDir.name, contents: fileOrDir.contents }; } else { return { diff --git a/packages/commons/fs-utils/src/index.ts b/packages/commons/fs-utils/src/index.ts index bc4558a4219..0ee368e2fea 100644 --- a/packages/commons/fs-utils/src/index.ts +++ b/packages/commons/fs-utils/src/index.ts @@ -28,3 +28,5 @@ export { } from "./osPathConverter"; export { getAllFilesInDirectory } from "./getAllFilesInDirectory"; export { getFilename } from "./getFilename"; +export { isURL } from "./isUrl"; +export { isCI } from "./isCI"; diff --git a/packages/cli/cli/src/utils/isCI.ts b/packages/commons/fs-utils/src/isCI.ts similarity index 100% rename from packages/cli/cli/src/utils/isCI.ts rename to packages/commons/fs-utils/src/isCI.ts diff --git a/packages/cli/cli/src/utils/isUrl.ts b/packages/commons/fs-utils/src/isUrl.ts similarity index 100% rename from packages/cli/cli/src/utils/isUrl.ts rename to packages/commons/fs-utils/src/isUrl.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ec24321e82..bb6480ce976 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5632,6 +5632,9 @@ importers: '@fern-api/login': specifier: workspace:* version: link:../login + '@fern-api/mintlify-importer': + specifier: workspace:* + version: link:../docs-importers/mintlify '@fern-api/task-context': specifier: workspace:* version: link:../task-context @@ -5657,9 +5660,6 @@ importers: '@types/fs-extra': specifier: ^11.0.1 version: 11.0.1 - '@types/jest': - specifier: ^29.5.14 - version: 29.5.14 '@types/js-yaml': specifier: ^4.0.8 version: 4.0.8