diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index a5a6d99c29..a267736bd2 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -23,6 +23,7 @@ import { RemoteProtocol } from '../prepare/types'; import { addBuildTimestampMetadata, getGeneratorKey, + getMkdocsYml, getRepoUrlFromLocationAnnotation, isValidRepoUrlForMkdocs, patchMkdocsYmlPreBuild, @@ -53,6 +54,9 @@ const mkdocsYmlWithValidDocDir = fs.readFileSync( const mkdocsYmlWithInvalidDocDir = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir.yml'), ); +const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir2.yml'), +); const mockLogger = getVoidLogger(); const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; @@ -334,48 +338,68 @@ describe('helpers', () => { }); }); - describe('validateMkdocsYaml', () => { - beforeEach(() => { - mockFs({ - '/mkdocs.yml': mkdocsYml, - '/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions, - '/mkdocs_valid_doc_dir.yml': mkdocsYmlWithValidDocDir, - '/mkdocs_invalid_doc_dir.yml': mkdocsYmlWithInvalidDocDir, - }); - }); - + describe('getMkdocsYml', () => { afterEach(() => { mockFs.restore(); }); const inputDir = resolvePath(__filename, '../__fixtures__/'); + + it('returns expected contents when .yml file is present', async () => { + const key = path.join(inputDir, 'mkdocs.yml'); + mockFs({ [key]: mkdocsYml }); + const { path: mkdocsPath, content } = await getMkdocsYml(inputDir); + + expect(mkdocsPath).toBe(key); + expect(content).toBe(mkdocsYml.toString()); + }); + + it('returns expected contents when .yaml file is present', async () => { + const key = path.join(inputDir, 'mkdocs.yaml'); + mockFs({ [key]: mkdocsYml }); + const { path: mkdocsPath, content } = await getMkdocsYml(inputDir); + expect(mkdocsPath).toBe(key); + expect(content).toBe(mkdocsYml.toString()); + }); + + it('throws when neither .yml nor .yaml file is present', async () => { + const invalidInputDir = resolvePath(__filename); + await expect(getMkdocsYml(invalidInputDir)).rejects.toThrowError( + /Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation/, + ); + }); + }); + + describe('validateMkdocsYaml', () => { + const inputDir = resolvePath(__filename, '../__fixtures__/'); + it('should return true on when no docs_dir present', async () => { await expect( - validateMkdocsYaml(inputDir, '/mkdocs.yml'), + validateMkdocsYaml(inputDir, mkdocsYml.toString()), ).resolves.toBeUndefined(); }); it('should return true on when a valid docs_dir is present', async () => { await expect( - validateMkdocsYaml(inputDir, '/mkdocs_valid_doc_dir.yml'), + validateMkdocsYaml(inputDir, mkdocsYmlWithValidDocDir.toString()), ).resolves.toBeUndefined(); }); it('should return false on absolute doc_dir path', async () => { await expect( - validateMkdocsYaml(inputDir, '/mkdocs_invalid_doc_dir.yml'), + validateMkdocsYaml(inputDir, mkdocsYmlWithInvalidDocDir.toString()), ).rejects.toThrow(); }); it('should return false on doc_dir path that traverses directory structure backwards', async () => { await expect( - validateMkdocsYaml(inputDir, '/mkdocs_invalid_doc_dir2.yml'), + validateMkdocsYaml(inputDir, mkdocsYmlWithInvalidDocDir2.toString()), ).rejects.toThrow(); }); it('should validate files with custom yaml tags', async () => { await expect( - validateMkdocsYaml(inputDir, '/mkdocs_with_extensions.yml'), + validateMkdocsYaml(inputDir, mkdocsYmlWithExtensions.toString()), ).resolves.toBeUndefined(); }); }); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 590e75e7b2..c10303f29c 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -19,7 +19,7 @@ import { isChildPath } from '@backstage/backend-common'; import { spawn } from 'child_process'; import fs from 'fs-extra'; import yaml, { DEFAULT_SCHEMA, Type } from 'js-yaml'; -import { resolve as resolvePath } from 'path'; +import path, { resolve as resolvePath } from 'path'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { ParsedLocationAnnotation } from '../../helpers'; @@ -155,26 +155,50 @@ const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ }), ]); +/** + * Finds and loads the contents of either an mkdocs.yml or mkdocs.yaml file, + * depending on which is present (MkDocs supports both as of v1.2.2). + * + * @param {string} inputDir base dir to be searched for either an mkdocs.yml or + * mkdocs.yaml file. + */ +export const getMkdocsYml = async ( + inputDir: string, +): Promise<{ path: string; content: string }> => { + let mkdocsYmlPath: string; + let mkdocsYmlFileString: string; + try { + mkdocsYmlPath = path.join(inputDir, 'mkdocs.yaml'); + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch { + try { + mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch (error) { + throw new Error( + `Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation: ${error.message}`, + ); + } + } + + return { + path: mkdocsYmlPath, + content: mkdocsYmlFileString, + }; +}; + /** * Validating mkdocs config file for incorrect/insecure values * Throws on invalid configs * * @param {string} inputDir base dir to be used as a docs_dir path validity check - * @param {string} mkdocsYmlPath Absolute path to mkdocs.yml or equivalent of a docs site + * @param {string} mkdocsYmlFileString The string contents of the loaded + * mkdocs.yml or equivalent of a docs site */ export const validateMkdocsYaml = async ( inputDir: string, - mkdocsYmlPath: string, + mkdocsYmlFileString: string, ) => { - let mkdocsYmlFileString; - try { - mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch (error) { - throw new Error( - `Could not read MkDocs YAML config file ${mkdocsYmlPath} before for validation: ${error.message}`, - ); - } - const mkdocsYml: any = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA, }); diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 9d6bf38c54..13bb63ca44 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -16,10 +16,10 @@ import { ContainerRunner } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import path from 'path'; import { Logger } from 'winston'; import { addBuildTimestampMetadata, + getMkdocsYml, patchMkdocsYmlPreBuild, runCommand, storeEtagMetadata, @@ -71,19 +71,13 @@ export class TechdocsGenerator implements GeneratorBase { logger: childLogger, logStream, }: GeneratorRunOptions): Promise { - // TODO: In future mkdocs.yml can be mkdocs.yaml. So, use a config variable here to find out - // the correct file name. // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url - const mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + const { path, content } = await getMkdocsYml(inputDir); if (parsedLocationAnnotation) { - await patchMkdocsYmlPreBuild( - mkdocsYmlPath, - childLogger, - parsedLocationAnnotation, - ); + await patchMkdocsYmlPreBuild(path, childLogger, parsedLocationAnnotation); } - await validateMkdocsYaml(inputDir, mkdocsYmlPath); + await validateMkdocsYaml(inputDir, content); // Directories to bind on container const mountDirs = {