Add techdocs-core plugin to techdocs builder automatically
Currently users need to modify their mkdocs files to container techdocs-core plugin manually when they want to expose techdocs in Backstage. To have a standardized set of extensions without the need to modify existing files (and possibly pollute alternative/additional mkdocs pipelines) this should be added automatically. This PR adds a helper function to modify the mkdocs file to contain this plugin if it doesn't exist in the config file. Co-authored-by: @emmaindal Signed-off-by: Jussi Hallila <jussi@hallila.com>
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
site_name: Test site name
|
||||
site_description: Test site description
|
||||
docs_dir: docs/
|
||||
plugins:
|
||||
- not-techdocs-core
|
||||
- also-not-techdocs-core
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
site_name: Test site name
|
||||
site_description: Test site description
|
||||
# This is a comment that is removed after editing
|
||||
plugins:
|
||||
- techdocs-core
|
||||
@@ -0,0 +1,3 @@
|
||||
site_name: Test site name
|
||||
site_description: Test site description
|
||||
docs_dir: docs/
|
||||
@@ -28,10 +28,14 @@ import {
|
||||
getMkdocsYml,
|
||||
getRepoUrlFromLocationAnnotation,
|
||||
patchIndexPreBuild,
|
||||
patchMkdocsYmlPreBuild,
|
||||
storeEtagMetadata,
|
||||
validateMkdocsYaml,
|
||||
} from './helpers';
|
||||
import {
|
||||
patchMkdocsYmlPreBuild,
|
||||
pathMkdocsYmlWithTechdocsPlugin,
|
||||
} from './mkDocsPatchers';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const mockEntity = {
|
||||
apiVersion: 'version',
|
||||
@@ -65,6 +69,15 @@ const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync(
|
||||
const mkdocsYmlWithComments = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'),
|
||||
);
|
||||
const mkdocsYmlWithTechdocsPlugins = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_with_techdocs_plugin.yml'),
|
||||
);
|
||||
const mkdocsYmlWithoutPlugins = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_without_plugins.yml'),
|
||||
);
|
||||
const mkdocsYmlWithAdditionalPlugins = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_with_additional_plugins.yml'),
|
||||
);
|
||||
const mockLogger = getVoidLogger();
|
||||
const warn = jest.spyOn(mockLogger, 'warn');
|
||||
|
||||
@@ -289,6 +302,60 @@ describe('helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('pathMkdocsYmlWithTechdocsPlugin', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins,
|
||||
'/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins,
|
||||
'/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins,
|
||||
});
|
||||
});
|
||||
it('should not add additional plugins if techdocs exists already in mkdocs file', async () => {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(
|
||||
'/mkdocs_with_techdocs_plugin.yml',
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
'/mkdocs_with_techdocs_plugin.yml',
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
};
|
||||
expect(parsedYml.plugins).toHaveLength(1);
|
||||
expect(parsedYml.plugins).toContain('techdocs-core');
|
||||
});
|
||||
it("should add the needed plugin if it doesn't exist in mkdocs file", async () => {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(
|
||||
'/mkdocs_without_plugins.yml',
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml');
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
};
|
||||
expect(parsedYml.plugins).toHaveLength(1);
|
||||
expect(parsedYml.plugins).toContain('techdocs-core');
|
||||
});
|
||||
it('should not override existing plugins', async () => {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
mockLogger,
|
||||
);
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
};
|
||||
expect(parsedYml.plugins).toHaveLength(3);
|
||||
expect(parsedYml.plugins).toContain('techdocs-core');
|
||||
expect(parsedYml.plugins).toContain('not-techdocs-core');
|
||||
expect(parsedYml.plugins).toContain('also-not-techdocs-core');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchIndexPreBuild', () => {
|
||||
afterEach(() => {
|
||||
warn.mockClear();
|
||||
|
||||
@@ -125,7 +125,7 @@ class UnknownTag {
|
||||
constructor(public readonly data: any, public readonly type?: string) {}
|
||||
}
|
||||
|
||||
const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
export const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
new Type('', {
|
||||
kind: 'scalar',
|
||||
multi: true,
|
||||
@@ -203,101 +203,6 @@ export const validateMkdocsYaml = async (
|
||||
return parsedMkdocsYml.docs_dir;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the mkdocs.yml file before TechDocs generator uses it to generate docs site.
|
||||
*
|
||||
* List of tasks:
|
||||
* - Add repo_url or edit_uri if it does not exists
|
||||
* If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default.
|
||||
* If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get
|
||||
* the repository URL.
|
||||
*
|
||||
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
|
||||
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
|
||||
*
|
||||
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
|
||||
* @param logger - A logger instance
|
||||
* @param parsedLocationAnnotation - Object with location url and type
|
||||
* @param scmIntegrations - the scmIntegration to do url transformations
|
||||
*/
|
||||
export const patchMkdocsYmlPreBuild = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
parsedLocationAnnotation: ParsedLocationAnnotation,
|
||||
scmIntegrations: ScmIntegrationRegistry,
|
||||
) => {
|
||||
// We only want to override the mkdocs.yml if it has actually changed. This is relevant if
|
||||
// used with a 'dir' location on the file system as this would permanently update the file.
|
||||
let didEdit = false;
|
||||
|
||||
let mkdocsYmlFileString;
|
||||
try {
|
||||
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mkdocsYml: any;
|
||||
try {
|
||||
mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA });
|
||||
|
||||
// mkdocsYml should be an object type after successful parsing.
|
||||
// But based on its type definition, it can also be a string or undefined, which we don't want.
|
||||
if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') {
|
||||
throw new Error('Bad YAML format.');
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add edit_uri and/or repo_url to mkdocs.yml if it is missing.
|
||||
// This will enable the Page edit button generated by MkDocs.
|
||||
// If the either has been set, keep the original value
|
||||
if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) {
|
||||
const result = getRepoUrlFromLocationAnnotation(
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
mkdocsYml.docs_dir,
|
||||
);
|
||||
|
||||
if (result.repo_url || result.edit_uri) {
|
||||
mkdocsYml.repo_url = result.repo_url;
|
||||
mkdocsYml.edit_uri = result.edit_uri;
|
||||
didEdit = true;
|
||||
|
||||
logger.info(
|
||||
`Set ${JSON.stringify(
|
||||
result,
|
||||
)}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (didEdit) {
|
||||
await fs.writeFile(
|
||||
mkdocsYmlPath,
|
||||
yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update docs/index.md file before TechDocs generator uses it to generate docs site,
|
||||
* falling back to docs/README.md or README.md in case a default docs/index.md
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Logger } from 'winston';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import { ParsedLocationAnnotation } from '../../helpers';
|
||||
import { getRepoUrlFromLocationAnnotation, MKDOCS_SCHEMA } from './helpers';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
|
||||
type MkDocsObject = {
|
||||
plugins?: string[];
|
||||
docs_dir: string;
|
||||
repo_url?: string;
|
||||
edit_uri?: string;
|
||||
};
|
||||
|
||||
const patchMkdocsFile = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
updateAction: (mkdocsYml: MkDocsObject) => boolean,
|
||||
) => {
|
||||
// We only want to override the mkdocs.yml if it has actually changed. This is relevant if
|
||||
// used with a 'dir' location on the file system as this would permanently update the file.
|
||||
let didEdit = false;
|
||||
|
||||
let mkdocsYmlFileString;
|
||||
try {
|
||||
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mkdocsYml: any;
|
||||
try {
|
||||
mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA });
|
||||
|
||||
// mkdocsYml should be an object type after successful parsing.
|
||||
// But based on its type definition, it can also be a string or undefined, which we don't want.
|
||||
if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') {
|
||||
throw new Error('Bad YAML format.');
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
didEdit = updateAction(mkdocsYml);
|
||||
|
||||
try {
|
||||
if (didEdit) {
|
||||
await fs.writeFile(
|
||||
mkdocsYmlPath,
|
||||
yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the mkdocs.yml file before TechDocs generator uses it to generate docs site.
|
||||
*
|
||||
* List of tasks:
|
||||
* - Add repo_url or edit_uri if it does not exists
|
||||
* If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default.
|
||||
* If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get
|
||||
* the repository URL.
|
||||
*
|
||||
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
|
||||
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
|
||||
*
|
||||
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
|
||||
* @param logger - A logger instance
|
||||
* @param parsedLocationAnnotation - Object with location url and type
|
||||
* @param scmIntegrations - the scmIntegration to do url transformations
|
||||
*/
|
||||
export const patchMkdocsYmlPreBuild = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
parsedLocationAnnotation: ParsedLocationAnnotation,
|
||||
scmIntegrations: ScmIntegrationRegistry,
|
||||
) => {
|
||||
await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => {
|
||||
if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) {
|
||||
// Add edit_uri and/or repo_url to mkdocs.yml if it is missing.
|
||||
// This will enable the Page edit button generated by MkDocs.
|
||||
// If the either has been set, keep the original value
|
||||
const result = getRepoUrlFromLocationAnnotation(
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
mkdocsYml.docs_dir,
|
||||
);
|
||||
|
||||
if (result.repo_url || result.edit_uri) {
|
||||
mkdocsYml.repo_url = result.repo_url;
|
||||
mkdocsYml.edit_uri = result.edit_uri;
|
||||
|
||||
logger.info(
|
||||
`Set ${JSON.stringify(
|
||||
result,
|
||||
)}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the mkdocs.yml file before TechDocs generator uses it to generate docs site.
|
||||
*
|
||||
* List of tasks:
|
||||
* - Add techdocs-core plugin to mkdocs file if it doesn't exist
|
||||
*
|
||||
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
|
||||
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
|
||||
*
|
||||
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
|
||||
* @param logger - A logger instance
|
||||
*/
|
||||
export const pathMkdocsYmlWithTechdocsPlugin = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
) => {
|
||||
await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => {
|
||||
// Modify mkdocs.yaml to contain the needed techdocs-core plugin if it is not there
|
||||
if (!('plugins' in mkdocsYml)) {
|
||||
mkdocsYml.plugins = ['techdocs-core'];
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mkdocsYml.plugins && !mkdocsYml.plugins.includes('techdocs-core')) {
|
||||
mkdocsYml.plugins.push('techdocs-core');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
@@ -26,11 +26,15 @@ import {
|
||||
createOrUpdateMetadata,
|
||||
getMkdocsYml,
|
||||
patchIndexPreBuild,
|
||||
patchMkdocsYmlPreBuild,
|
||||
runCommand,
|
||||
storeEtagMetadata,
|
||||
validateMkdocsYaml,
|
||||
} from './helpers';
|
||||
|
||||
import {
|
||||
patchMkdocsYmlPreBuild,
|
||||
pathMkdocsYmlWithTechdocsPlugin,
|
||||
} from './mkDocsPatchers';
|
||||
import {
|
||||
GeneratorBase,
|
||||
GeneratorConfig,
|
||||
@@ -110,6 +114,10 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir });
|
||||
}
|
||||
|
||||
if (!this.options.omitTechdocsCoreMkdocsPlugin) {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(mkdocsYmlPath, childLogger);
|
||||
}
|
||||
|
||||
// Directories to bind on container
|
||||
const mountDirs = {
|
||||
[inputDir]: '/input',
|
||||
@@ -207,5 +215,8 @@ export function readGeneratorConfig(
|
||||
'docker',
|
||||
dockerImage: config.getOptionalString('techdocs.generator.dockerImage'),
|
||||
pullImage: config.getOptionalBoolean('techdocs.generator.pullImage'),
|
||||
omitTechdocsCoreMkdocsPlugin: config.getOptionalBoolean(
|
||||
'techdocs.generator.mkdocs.omitTechdocsCorePlugin',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export type GeneratorConfig = {
|
||||
runIn: GeneratorRunInType;
|
||||
dockerImage?: string;
|
||||
pullImage?: boolean;
|
||||
omitTechdocsCoreMkdocsPlugin?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user