diff --git a/.changeset/twelve-elephants-pay.md b/.changeset/twelve-elephants-pay.md new file mode 100644 index 0000000000..f82b0464b0 --- /dev/null +++ b/.changeset/twelve-elephants-pay.md @@ -0,0 +1,18 @@ +--- +'@backstage/plugin-techdocs-backend': minor +'@backstage/plugin-techdocs-node': minor +--- + +Add support for default mkdocs plugins. + +So far only `techdocs-core` was added as default to the list of mkdocs plugins. In case you use a +custom image for techdocs, you also might want to add custom default plugins for mkdocs. +With this change one can do that - example: + +```yaml +techdocs: + generator: + mkdocs: + defaultPlugins: + - section-index +``` diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 49ee9d72c2..f567bfbac9 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -49,6 +49,15 @@ techdocs: # will be broken in these scenarios. legacyCopyReadmeMdToIndexMd: false + # (Optional) Configures the default plugins which should be added + # automatically to every mkdocs.yaml file. This simplifies the usage as + # e.g. styling plugins can be added once for all. + # Make sure that the defined plugins are installed locally / in the Docker + # image. + # By default, only the techdocs-core plugin will be added (except if + # omitTechdocsCorePlugin: true). + defaultPlugins: ['techdocs-core'] + # techdocs.builder can be either 'local' or 'external'. # Using the default build strategy, if builder is set to 'local' and you open a TechDocs page, # techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwards. diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 3a05beabdc..9ad2594ffd 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -57,6 +57,11 @@ export interface Config { * will be broken in these scenarios. */ legacyCopyReadmeMdToIndexMd?: boolean; + + /** + * List of mkdocs plugins which should be added as default to all mkdocs.yml files. + */ + defaultPlugins?: string[]; }; }; diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index 6bcdf0d984..e20e20ea5b 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -33,7 +33,7 @@ import { } from './helpers'; import { patchMkdocsYmlPreBuild, - pathMkdocsYmlWithTechdocsPlugin, + patchMkdocsYmlWithPlugins, } from './mkdocsPatchers'; import yaml from 'js-yaml'; @@ -310,7 +310,7 @@ describe('helpers', () => { }); }); - describe('pathMkdocsYmlWithTechdocsPlugin', () => { + describe('patchMkdocsYmlWithPlugins', () => { beforeEach(() => { mockFs({ '/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins, @@ -319,7 +319,7 @@ describe('helpers', () => { }); }); it('should not add additional plugins if techdocs exists already in mkdocs file', async () => { - await pathMkdocsYmlWithTechdocsPlugin( + await patchMkdocsYmlWithPlugins( '/mkdocs_with_techdocs_plugin.yml', mockLogger, ); @@ -334,7 +334,7 @@ describe('helpers', () => { expect(parsedYml.plugins).toContain('techdocs-core'); }); it("should add the needed plugin if it doesn't exist in mkdocs file", async () => { - await pathMkdocsYmlWithTechdocsPlugin( + await patchMkdocsYmlWithPlugins( '/mkdocs_without_plugins.yml', mockLogger, ); @@ -347,7 +347,7 @@ describe('helpers', () => { expect(parsedYml.plugins).toContain('techdocs-core'); }); it('should not override existing plugins', async () => { - await pathMkdocsYmlWithTechdocsPlugin( + await patchMkdocsYmlWithPlugins( '/mkdocs_with_additional_plugins.yml', mockLogger, ); @@ -362,6 +362,23 @@ describe('helpers', () => { expect(parsedYml.plugins).toContain('not-techdocs-core'); expect(parsedYml.plugins).toContain('also-not-techdocs-core'); }); + it('should add all provided default plugins', async () => { + await patchMkdocsYmlWithPlugins( + '/mkdocs_with_additional_plugins.yml', + mockLogger, + ['techdocs-core', 'custom-plugin'], + ); + + const updatedMkdocsYml = await fs.readFile( + '/mkdocs_with_additional_plugins.yml', + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(4); + expect(parsedYml.plugins).toContain('techdocs-core'); + expect(parsedYml.plugins).toContain('custom-plugin'); + }); }); describe('patchIndexPreBuild', () => { diff --git a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts index d03b5d83c2..945aab3884 100644 --- a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts +++ b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts @@ -138,27 +138,34 @@ export const patchMkdocsYmlPreBuild = async ( * 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 + * - Add all provided default plugins * * 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 defaultPlugins - List of default mkdocs plugins */ -export const pathMkdocsYmlWithTechdocsPlugin = async ( +export const patchMkdocsYmlWithPlugins = async ( mkdocsYmlPath: string, logger: Logger, + defaultPlugins: string[] = ['techdocs-core'], ) => { await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { - // Modify mkdocs.yaml to contain the needed techdocs-core plugin if it is not there + // Modify mkdocs.yaml to contain the required default plugins if (!('plugins' in mkdocsYml)) { - mkdocsYml.plugins = ['techdocs-core']; + mkdocsYml.plugins = defaultPlugins; return true; } - if (mkdocsYml.plugins && !mkdocsYml.plugins.includes('techdocs-core')) { - mkdocsYml.plugins.push('techdocs-core'); + if ( + mkdocsYml.plugins && + !defaultPlugins.every(plugin => mkdocsYml.plugins!.includes(plugin)) + ) { + mkdocsYml.plugins = [ + ...new Set([...mkdocsYml.plugins, ...defaultPlugins]), + ]; return true; } return false; diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.test.ts b/plugins/techdocs-node/src/stages/generate/techdocs.test.ts index 11c9ee0135..82bfcfff55 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.test.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.test.ts @@ -157,4 +157,24 @@ describe('readGeneratorConfig', () => { legacyCopyReadmeMdToIndexMd: true, }); }); + + it('should read the default plugins config', () => { + const config = new ConfigReader({ + techdocs: { + generator: { + runIn: 'docker', + dockerImage: 'my-org/techdocs', + pullImage: false, + mkdocs: { defaultPlugins: ['mkdocs-custom-plugin'] }, + }, + }, + }); + + expect(readGeneratorConfig(config, logger)).toEqual({ + runIn: 'docker', + dockerImage: 'my-org/techdocs', + pullImage: false, + defaultPlugins: ['mkdocs-custom-plugin'], + }); + }); }); diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts index 06160b85c9..b1baf214b3 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -33,7 +33,7 @@ import { import { patchMkdocsYmlPreBuild, - pathMkdocsYmlWithTechdocsPlugin, + patchMkdocsYmlWithPlugins, } from './mkdocsPatchers'; import { GeneratorBase, @@ -121,10 +121,18 @@ export class TechdocsGenerator implements GeneratorBase { await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); } - if (!this.options.omitTechdocsCoreMkdocsPlugin) { - await pathMkdocsYmlWithTechdocsPlugin(mkdocsYmlPath, childLogger); + // patch the list of mkdocs plugins + const defaultPlugins = this.options.defaultPlugins ?? []; + + if ( + !this.options.omitTechdocsCoreMkdocsPlugin && + !defaultPlugins.includes('techdocs-core') + ) { + defaultPlugins.push('techdocs-core'); } + await patchMkdocsYmlWithPlugins(mkdocsYmlPath, childLogger, defaultPlugins); + // Directories to bind on container const mountDirs = { [inputDir]: '/input', @@ -233,5 +241,8 @@ export function readGeneratorConfig( legacyCopyReadmeMdToIndexMd: config.getOptionalBoolean( 'techdocs.generator.mkdocs.legacyCopyReadmeMdToIndexMd', ), + defaultPlugins: config.getOptionalStringArray( + 'techdocs.generator.mkdocs.defaultPlugins', + ), }; } diff --git a/plugins/techdocs-node/src/stages/generate/types.ts b/plugins/techdocs-node/src/stages/generate/types.ts index 70c332344a..eca1057914 100644 --- a/plugins/techdocs-node/src/stages/generate/types.ts +++ b/plugins/techdocs-node/src/stages/generate/types.ts @@ -41,6 +41,7 @@ export type GeneratorConfig = { pullImage?: boolean; omitTechdocsCoreMkdocsPlugin?: boolean; legacyCopyReadmeMdToIndexMd?: boolean; + defaultPlugins?: string[]; }; /**