Merge pull request #7080 from backstage/vinzscam/techdocs-use-readme-as-fallback

techdocs: use README.md as index fallback
This commit is contained in:
Camila Belo
2021-10-20 22:04:46 +02:00
committed by GitHub
4 changed files with 141 additions and 6 deletions
@@ -27,6 +27,7 @@ import {
getGeneratorKey,
getMkdocsYml,
getRepoUrlFromLocationAnnotation,
patchIndexPreBuild,
patchMkdocsYmlPreBuild,
storeEtagMetadata,
validateMkdocsYaml,
@@ -65,6 +66,8 @@ const mkdocsYmlWithComments = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'),
);
const mockLogger = getVoidLogger();
const warn = jest.spyOn(mockLogger, 'warn');
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({}));
@@ -286,6 +289,78 @@ describe('helpers', () => {
});
});
describe('patchIndexPreBuild', () => {
afterEach(() => {
warn.mockClear();
});
it('should have no effect if docs/index.md exists', async () => {
mockFs({
'/docs/index.md': 'index.md content',
'/docs/README.md': 'docs/README.md content',
});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
expect(fs.readFileSync('/docs/index.md', 'utf-8')).toEqual(
'index.md content',
);
expect(warn).not.toHaveBeenCalledWith();
mockFs.restore();
});
it("should use docs/README.md if docs/index.md doesn't exists", async () => {
mockFs({
'/docs/README.md': 'docs/README.md content',
'/README.md': 'main README.md content',
});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
expect(fs.readFileSync('/docs/index.md', 'utf-8')).toEqual(
'docs/README.md content',
);
expect(warn.mock.calls).toEqual([['docs/index.md not found.']]);
mockFs.restore();
});
it('should use README.md if neither docs/index.md or docs/README.md exist', async () => {
mockFs({
'/README.md': 'main README.md content',
});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
expect(fs.readFileSync('/docs/index.md', 'utf-8')).toEqual(
'main README.md content',
);
expect(warn.mock.calls).toEqual([
['docs/index.md not found.'],
['docs/README.md not found.'],
['docs/readme.md not found.'],
]);
mockFs.restore();
});
it('should not use any file as index.md if no one matches the requirements', async () => {
mockFs({});
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
expect(() => fs.readFileSync('/docs/index.md', 'utf-8')).toThrow();
expect(warn.mock.calls).toEqual([
['docs/index.md not found.'],
['docs/README.md not found.'],
['docs/readme.md not found.'],
['README.md not found.'],
['readme.md not found.'],
[
"Could not find any techdocs' index file. Please make sure at least one of /docs/index.md /docs/README.md /docs/readme.md /README.md /readme.md exists.",
],
]);
mockFs.restore();
});
});
describe('addBuildTimestampMetadata', () => {
beforeEach(() => {
mockFs.restore();
@@ -406,7 +481,7 @@ describe('helpers', () => {
it('should return true on when a valid docs_dir is present', async () => {
await expect(
validateMkdocsYaml(inputDir, mkdocsYmlWithValidDocDir.toString()),
).resolves.toBeUndefined();
).resolves.toBe('docs/');
});
it('should return false on absolute doc_dir path', async () => {
@@ -176,24 +176,31 @@ export const getMkdocsYml = async (
* @param {string} inputDir base dir to be used as a docs_dir path validity check
* @param {string} mkdocsYmlFileString The string contents of the loaded
* mkdocs.yml or equivalent of a docs site
* @returns the parsed docs_dir or undefined
*/
export const validateMkdocsYaml = async (
inputDir: string,
mkdocsYmlFileString: string,
) => {
const mkdocsYml: any = yaml.load(mkdocsYmlFileString, {
): Promise<string | undefined> => {
const mkdocsYml = yaml.load(mkdocsYmlFileString, {
schema: MKDOCS_SCHEMA,
});
if (mkdocsYml === null || typeof mkdocsYml !== 'object') {
return undefined;
}
const parsedMkdocsYml: Record<string, any> = mkdocsYml;
if (
mkdocsYml.docs_dir &&
!isChildPath(inputDir, resolvePath(inputDir, mkdocsYml.docs_dir))
parsedMkdocsYml.docs_dir &&
!isChildPath(inputDir, resolvePath(inputDir, parsedMkdocsYml.docs_dir))
) {
throw new Error(
`docs_dir configuration value in mkdocs can't be an absolute directory or start with ../ for security reasons.
Use relative paths instead which are resolved relative to your mkdocs.yml file location.`,
);
}
return parsedMkdocsYml.docs_dir;
};
/**
@@ -291,6 +298,52 @@ export const patchMkdocsYmlPreBuild = async (
}
};
/**
* 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
* is not provided.
*/
export const patchIndexPreBuild = async ({
inputDir,
logger,
docsDir = 'docs',
}: {
inputDir: string;
logger: Logger;
docsDir?: string;
}) => {
const docsPath = path.join(inputDir, docsDir);
const indexMdPath = path.join(docsPath, 'index.md');
if (await fs.pathExists(indexMdPath)) {
return;
}
logger.warn(`${path.join(docsDir, 'index.md')} not found.`);
const fallbacks = [
path.join(docsPath, 'README.md'),
path.join(docsPath, 'readme.md'),
path.join(inputDir, 'README.md'),
path.join(inputDir, 'readme.md'),
];
await fs.ensureDir(docsPath);
for (const filePath of fallbacks) {
try {
await fs.copyFile(filePath, indexMdPath);
return;
} catch (error) {
logger.warn(`${path.relative(inputDir, filePath)} not found.`);
}
}
logger.warn(
`Could not find any techdocs' index file. Please make sure at least one of ${[
indexMdPath,
...fallbacks,
].join(' ')} exists.`,
);
};
/**
* Update the techdocs_metadata.json to add a new build timestamp metadata. Create the .json file if it doesn't exist.
*
@@ -25,6 +25,7 @@ import {
import {
addBuildTimestampMetadata,
getMkdocsYml,
patchIndexPreBuild,
patchMkdocsYmlPreBuild,
runCommand,
storeEtagMetadata,
@@ -94,7 +95,7 @@ export class TechdocsGenerator implements GeneratorBase {
const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir);
// validate the docs_dir first
await validateMkdocsYaml(inputDir, content);
const docsDir = await validateMkdocsYaml(inputDir, content);
if (parsedLocationAnnotation) {
await patchMkdocsYmlPreBuild(
@@ -103,6 +104,7 @@ export class TechdocsGenerator implements GeneratorBase {
parsedLocationAnnotation,
this.scmIntegrations,
);
await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir });
}
// Directories to bind on container