diff --git a/packages/cli/project-loader/src/index.ts b/packages/cli/project-loader/src/index.ts index ba732c17f7d..15bd6a44545 100644 --- a/packages/cli/project-loader/src/index.ts +++ b/packages/cli/project-loader/src/index.ts @@ -1,2 +1,2 @@ -export { loadApis, loadProject } from "./loadProject"; +export { loadApis, loadProject, loadProjectFromDirectory } from "./loadProject"; export { type Project } from "./Project"; diff --git a/packages/cli/project-loader/src/loadProject.ts b/packages/cli/project-loader/src/loadProject.ts index 5ae870b5ebf..2dfba11f1b8 100644 --- a/packages/cli/project-loader/src/loadProject.ts +++ b/packages/cli/project-loader/src/loadProject.ts @@ -2,10 +2,10 @@ import { APIS_DIRECTORY, ASYNCAPI_DIRECTORY, DEFINITION_DIRECTORY, - fernConfigJson, FERN_DIRECTORY, - generatorsYml, + fernConfigJson, GENERATORS_CONFIGURATION_FILENAME, + generatorsYml, getFernDirectory, OPENAPI_DIRECTORY } from "@fern-api/configuration"; @@ -35,33 +35,48 @@ export declare namespace loadProject { nameOverride?: string; sdkLanguage?: generatorsYml.GenerationLanguage; } + + export interface LoadProjectFromDirectoryArgs extends Args { + absolutePathToFernDirectory: AbsoluteFilePath; + } } -export async function loadProject({ - cliName, - cliVersion, - commandLineApiWorkspace, - defaultToAllApiWorkspaces, - context, - nameOverride -}: loadProject.Args): Promise { +export async function loadProject({ context, nameOverride, ...args }: loadProject.Args): Promise { const fernDirectory = await getFernDirectory(nameOverride); if (fernDirectory == null) { return context.failAndThrow(`Directory "${nameOverride ?? FERN_DIRECTORY}" not found.`); } + return await loadProjectFromDirectory({ + absolutePathToFernDirectory: fernDirectory, + context, + nameOverride, + ...args + }); +} + +export async function loadProjectFromDirectory({ + absolutePathToFernDirectory, + cliName, + cliVersion, + commandLineApiWorkspace, + defaultToAllApiWorkspaces, + context +}: loadProject.LoadProjectFromDirectoryArgs): Promise { let apiWorkspaces: APIWorkspace[] = []; if ( - (await doesPathExist(join(fernDirectory, RelativeFilePath.of(APIS_DIRECTORY)))) || - doesPathExist(join(fernDirectory, RelativeFilePath.of(DEFINITION_DIRECTORY))) || - doesPathExist(join(fernDirectory, RelativeFilePath.of(GENERATORS_CONFIGURATION_FILENAME))) || - doesPathExist(join(fernDirectory, RelativeFilePath.of(OPENAPI_DIRECTORY))) || - doesPathExist(join(fernDirectory, RelativeFilePath.of(ASYNCAPI_DIRECTORY))) + (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(APIS_DIRECTORY)))) || + (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(DEFINITION_DIRECTORY)))) || + (await doesPathExist( + join(absolutePathToFernDirectory, RelativeFilePath.of(GENERATORS_CONFIGURATION_FILENAME)) + )) || + (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(OPENAPI_DIRECTORY)))) || + (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(ASYNCAPI_DIRECTORY)))) ) { apiWorkspaces = await loadApis({ cliName, - fernDirectory, + fernDirectory: absolutePathToFernDirectory, cliVersion, context, commandLineApiWorkspace, @@ -70,9 +85,9 @@ export async function loadProject({ } return { - config: await fernConfigJson.loadProjectConfig({ directory: fernDirectory, context }), + config: await fernConfigJson.loadProjectConfig({ directory: absolutePathToFernDirectory, context }), apiWorkspaces, - docsWorkspaces: await loadDocsWorkspace({ fernDirectory, context }), + docsWorkspaces: await loadDocsWorkspace({ fernDirectory: absolutePathToFernDirectory, context }), loadAPIWorkspace: (name: string | undefined): APIWorkspace | undefined => { if (name == null) { return apiWorkspaces[0]; diff --git a/packages/cli/yaml/docs-validator/package.json b/packages/cli/yaml/docs-validator/package.json index 73018a6bfca..35adc75a8ce 100644 --- a/packages/cli/yaml/docs-validator/package.json +++ b/packages/cli/yaml/docs-validator/package.json @@ -32,6 +32,7 @@ "@fern-api/docs-markdown-utils": "workspace:*", "@fern-api/fs-utils": "workspace:*", "@fern-api/logger": "workspace:*", + "@fern-api/project-loader": "workspace:*", "@fern-api/task-context": "workspace:*", "@fern-api/workspace-loader": "workspace:*", "@fern-api/yaml-schema": "workspace:*", @@ -43,6 +44,7 @@ "rehype-katex": "^7.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", + "strip-ansi": "^7.1.0", "tinycolor2": "^1.6.0", "zod": "^3.22.3" }, diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/__snapshots__/playground-environments-exist.test.ts.snap b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/__snapshots__/playground-environments-exist.test.ts.snap new file mode 100644 index 00000000000..fad5e845378 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/__snapshots__/playground-environments-exist.test.ts.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`playground-environments-exist > no environments in api definition 1`] = ` +[ + { + "message": "The API does not contain the Staging environment. ", + "nodePath": [ + "navigation", + "0", + "api", + ], + "relativeFilepath": "docs.yml", + "severity": "error", + }, +] +`; + +exports[`playground-environments-exist > non existent environment specified 1`] = ` +[ + { + "message": "The API does not contain the Staging environment. Existing enviroments include Production, Dev.", + "nodePath": [ + "navigation", + "0", + "api", + ], + "relativeFilepath": "docs.yml", + "severity": "error", + }, +] +`; diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/definition/api.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/definition/api.yml new file mode 100644 index 00000000000..79c79c049a4 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/definition/api.yml @@ -0,0 +1,3 @@ +name: api +error-discrimination: + strategy: status-code diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/definition/imdb.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/definition/imdb.yml new file mode 100644 index 00000000000..e101041c5d8 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/definition/imdb.yml @@ -0,0 +1,60 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +service: + auth: false + base-path: /movies + endpoints: + createMovie: + docs: Add a movie to the database + method: POST + path: /create-movie + request: CreateMovieRequest + response: MovieId + + getMovie: + docs: Retrieve a movie from the database based on the ID + method: GET + path: /{id} + path-parameters: + id: MovieId + response: Movie + errors: + - MovieDoesNotExistError + examples: + # Success response + - path-parameters: + id: tt0111161 + response: + body: + id: tt0111161 + title: The Shawshank Redemption + rating: 9.3 + # Error response + - path-parameters: + id: tt1234 + response: + error: MovieDoesNotExistError + body: tt1234 + +types: + MovieId: + type: string + docs: The unique identifier for a Movie in the database + + Movie: + properties: + id: MovieId + title: string + rating: + type: double + docs: The rating scale out of ten stars + + CreateMovieRequest: + properties: + title: string + rating: double + +errors: + MovieDoesNotExistError: + status-code: 404 + type: MovieId diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/docs.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/docs.yml new file mode 100644 index 00000000000..286638d4735 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/docs.yml @@ -0,0 +1,11 @@ +instances: + - url: https://fern.docs.buildwithfern.com +title: Fern | Documentation +navigation: + - api: API Reference + playground: + environments: + - Staging +colors: + accentPrimary: '#ffffff' + background: '#000000' diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/fern.config.json b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/fern.config.json new file mode 100644 index 00000000000..38233178d2f --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "0.37.16" +} \ No newline at end of file diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/generators.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/generators.yml new file mode 100644 index 00000000000..c8ae78dfffa --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/no-environments-in-api/fern/generators.yml @@ -0,0 +1,9 @@ +default-group: local +groups: + local: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.9.5 + output: + location: local-file-system + path: ../sdks/typescript diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/definition/api.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/definition/api.yml new file mode 100644 index 00000000000..575b72decd6 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/definition/api.yml @@ -0,0 +1,6 @@ +name: api +environments: + Production: prod.com + Dev: dev.com +error-discrimination: + strategy: status-code diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/definition/imdb.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/definition/imdb.yml new file mode 100644 index 00000000000..e101041c5d8 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/definition/imdb.yml @@ -0,0 +1,60 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +service: + auth: false + base-path: /movies + endpoints: + createMovie: + docs: Add a movie to the database + method: POST + path: /create-movie + request: CreateMovieRequest + response: MovieId + + getMovie: + docs: Retrieve a movie from the database based on the ID + method: GET + path: /{id} + path-parameters: + id: MovieId + response: Movie + errors: + - MovieDoesNotExistError + examples: + # Success response + - path-parameters: + id: tt0111161 + response: + body: + id: tt0111161 + title: The Shawshank Redemption + rating: 9.3 + # Error response + - path-parameters: + id: tt1234 + response: + error: MovieDoesNotExistError + body: tt1234 + +types: + MovieId: + type: string + docs: The unique identifier for a Movie in the database + + Movie: + properties: + id: MovieId + title: string + rating: + type: double + docs: The rating scale out of ten stars + + CreateMovieRequest: + properties: + title: string + rating: double + +errors: + MovieDoesNotExistError: + status-code: 404 + type: MovieId diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/docs.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/docs.yml new file mode 100644 index 00000000000..286638d4735 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/docs.yml @@ -0,0 +1,11 @@ +instances: + - url: https://fern.docs.buildwithfern.com +title: Fern | Documentation +navigation: + - api: API Reference + playground: + environments: + - Staging +colors: + accentPrimary: '#ffffff' + background: '#000000' diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/fern.config.json b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/fern.config.json new file mode 100644 index 00000000000..38233178d2f --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "0.37.16" +} \ No newline at end of file diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/generators.yml b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/generators.yml new file mode 100644 index 00000000000..c8ae78dfffa --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/fixtures/wrong-environments-in-docs/fern/generators.yml @@ -0,0 +1,9 @@ +default-group: local +groups: + local: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.9.5 + output: + location: local-file-system + path: ../sdks/typescript diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/playground-environments-exist.test.ts b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/playground-environments-exist.test.ts new file mode 100644 index 00000000000..9cc0efe4dac --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/__test__/playground-environments-exist.test.ts @@ -0,0 +1,33 @@ +import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils"; +import { getViolationsForRule } from "../../../testing-utils/getViolationsForRule"; +import { PlaygroundEnvironmentsExistRule } from "../playground-environments-exist"; + +describe("playground-environments-exist", () => { + it("no environments in api definition", async () => { + const violations = await getViolationsForRule({ + rule: PlaygroundEnvironmentsExistRule, + absolutePathToFernDirectory: join( + AbsoluteFilePath.of(__dirname), + RelativeFilePath.of("fixtures"), + RelativeFilePath.of("no-environments-in-api"), + RelativeFilePath.of("fern") + ) + }); + + expect(violations).toMatchSnapshot(); + }); + + it("non existent environment specified", async () => { + const violations = await getViolationsForRule({ + rule: PlaygroundEnvironmentsExistRule, + absolutePathToFernDirectory: join( + AbsoluteFilePath.of(__dirname), + RelativeFilePath.of("fixtures"), + RelativeFilePath.of("wrong-environments-in-docs"), + RelativeFilePath.of("fern") + ) + }); + + expect(violations).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/playground-environments-exist.ts b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/playground-environments-exist.ts index 33eba2e3759..a78ad906b1e 100644 --- a/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/playground-environments-exist.ts +++ b/packages/cli/yaml/docs-validator/src/rules/playground-environments-exist/playground-environments-exist.ts @@ -1,4 +1,4 @@ -import { Rule, RuleViolation } from "../../Rule"; +import { Rule } from "../../Rule"; export const PlaygroundEnvironmentsExistRule: Rule = { name: "playground-environments-exist", @@ -6,32 +6,49 @@ export const PlaygroundEnvironmentsExistRule: Rule = { apiSection: async ({ workspace, context, config }) => { const apiSpecificationEnvironments = (await workspace.getDefinition({ context })).rootApiFile.contents .environments; - const playgroundEnvironmentIds = config.playground?.environments || []; - - if (!apiSpecificationEnvironments) { - if (playgroundEnvironmentIds.length > 0) { - return [ - { - severity: "error", - message: `${playgroundEnvironmentIds.join(", ")} are not valid environments` - } - ]; - } + + const availableEnvironmentIds = new Set(Object.keys(apiSpecificationEnvironments ?? {})); + const playgroundEnvironmentIds = config.playground?.environments; + + if (playgroundEnvironmentIds == null || playgroundEnvironmentIds.length == null) { return []; } - const availableEnvironmentIds = new Set(Object.keys(apiSpecificationEnvironments)); - const violatingIds = playgroundEnvironmentIds.filter((id) => !availableEnvironmentIds.has(id)); - return violatingIds.length > 0 - ? [ - { - severity: "error", - message: `${violatingIds.join(", ")} are not valid environments. Choose from ${Array.from( - availableEnvironmentIds - ).join(", ")}` - } - ] - : []; + const nonExistentEnviromentIds = playgroundEnvironmentIds.filter((id) => !availableEnvironmentIds.has(id)); + + if (nonExistentEnviromentIds.length == 0) { + return []; + } + + if (nonExistentEnviromentIds.length === 1) { + return [ + { + severity: "error", + message: `The API does not contain the ${nonExistentEnviromentIds[0]} environment. ${ + getExistingEnviromentIds(Array.from(availableEnvironmentIds)) ?? "" + }` + } + ]; + } + + return [ + { + severity: "error", + message: `The API does not contain the following enviroments: ${nonExistentEnviromentIds.join( + ", " + )}. ${getExistingEnviromentIds(Array.from(availableEnvironmentIds)) ?? ""}` + } + ]; } }) }; + +function getExistingEnviromentIds(availableEnvironmentIds: string[]): string | undefined { + if (availableEnvironmentIds.length === 0) { + return undefined; + } + if (availableEnvironmentIds.length === 1 && availableEnvironmentIds[0] != null) { + return `The only configured environment is ${availableEnvironmentIds[0]}`; + } + return `Existing enviroments include ${availableEnvironmentIds.join(", ")}.`; +} diff --git a/packages/cli/yaml/docs-validator/src/testing-utils/getViolationsForRule.ts b/packages/cli/yaml/docs-validator/src/testing-utils/getViolationsForRule.ts new file mode 100644 index 00000000000..7522c3e0eb8 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/testing-utils/getViolationsForRule.ts @@ -0,0 +1,45 @@ +import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { loadProjectFromDirectory } from "@fern-api/project-loader"; +import { createMockTaskContext } from "@fern-api/task-context"; +import stripAnsi from "strip-ansi"; +import { Rule } from "../Rule"; +import { runRulesOnDocsWorkspace } from "../validateDocsWorkspace"; +import { ValidationViolation } from "../ValidationViolation"; + +export declare namespace getViolationsForRule { + export interface Args { + rule: Rule; + absolutePathToFernDirectory: AbsoluteFilePath; + } +} + +export async function getViolationsForRule({ + rule, + absolutePathToFernDirectory +}: getViolationsForRule.Args): Promise { + const context = createMockTaskContext(); + const project = await loadProjectFromDirectory({ + absolutePathToFernDirectory, + context, + cliVersion: "0.0.0", + defaultToAllApiWorkspaces: true, + commandLineApiWorkspace: undefined, + cliName: "fern" + }); + + if (project.docsWorkspaces == null) { + throw new Error("Expected docs workspace to be present, but found none"); + } + + const violations = await runRulesOnDocsWorkspace({ + workspace: project.docsWorkspaces, + context, + rules: [rule], + loadApiWorkspace: project.loadAPIWorkspace + }); + + return violations.map((violation) => ({ + ...violation, + message: stripAnsi(violation.message) + })); +} diff --git a/packages/cli/yaml/docs-validator/tsconfig.json b/packages/cli/yaml/docs-validator/tsconfig.json index 1a26f58599b..c9c7bb4d42e 100644 --- a/packages/cli/yaml/docs-validator/tsconfig.json +++ b/packages/cli/yaml/docs-validator/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../../logger" }, { "path": "../../task-context" }, { "path": "../../workspace-loader" }, + { "path": "../../project-loader" }, { "path": "../yaml-schema" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb2c6427e98..87862ad2822 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3177,6 +3177,19 @@ importers: specifier: 4.6.4 version: 4.6.4 + packages/cli/cli/dist/dev: {} + + packages/cli/cli/dist/local: + devDependencies: + globals: + specifier: link:@types/vitest/globals + version: link:@types/vitest/globals + vitest: + specifier: ^2.0.5 + version: 2.0.5(@types/node@18.7.18)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5) + + packages/cli/cli/dist/prod: {} + packages/cli/configuration: dependencies: '@fern-api/core-utils': @@ -4866,6 +4879,9 @@ importers: '@fern-api/logger': specifier: workspace:* version: link:../../logger + '@fern-api/project-loader': + specifier: workspace:* + version: link:../../project-loader '@fern-api/task-context': specifier: workspace:* version: link:../../task-context @@ -4899,6 +4915,9 @@ importers: remark-math: specifier: ^6.0.0 version: 6.0.0 + strip-ansi: + specifier: ^7.1.0 + version: 7.1.0 tinycolor2: specifier: ^1.6.0 version: 1.6.0