qiskit-documentation/scripts/js/commands/checkPagesRender.ts

239 lines
7.2 KiB
TypeScript

// This code is a Qiskit project.
//
// (C) Copyright IBM 2024.
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
import fs from "fs/promises";
import { globby } from "globby";
import yargs from "yargs/yargs";
import { hideBin } from "yargs/helpers";
import { mean } from "lodash-es";
import { zxMain } from "../lib/zx.js";
// These are expected to 404 in the cloud app due to being legacy only.
const LEGACY_ONLY_PAGES: Set<string> = new Set([
"docs/guides/setup-channel.mdx",
]);
// These are expected to 404 in the legacy app due to being cloud only.
const CLOUD_ONLY_PAGES: Set<string> = new Set([
"docs/guides/calibration-jobs.mdx",
"docs/guides/cloud-setup.mdx",
"docs/guides/upgrade-from-open.mdx",
"docs/guides/view-cost.mdx",
"docs/guides/access-groups.mdx",
"docs/guides/cloud-account-structure.mdx",
"docs/guides/considerations-set-up-runtime.mdx",
"docs/guides/custom-roles.mdx",
"docs/guides/invite-and-manage-users.mdx",
"docs/guides/manage-appid.mdx",
"docs/guides/manage-cloud-users.mdx",
"docs/guides/plans-overview.mdx",
"docs/guides/quickstart-steps-org.mdx",
"docs/guides/allocation-limits.mdx",
]);
const PORT = 3000;
interface Arguments {
[x: string]: unknown;
fromFile?: string;
nonApi?: boolean;
currentApis?: boolean;
devApis?: boolean;
historicalApis?: boolean;
qiskitReleaseNotes?: boolean;
legacy?: boolean;
}
const readArgs = (): Arguments => {
return yargs(hideBin(process.argv))
.version(false)
.option("from-file", {
type: "string",
normalize: true,
conflicts: [
"non-api",
"current-apis",
"dev-apis",
"historical-apis",
"qiskit-release-notes",
],
description:
"Read the file path for file paths and globs to check, like `docs/start/index.md`. " +
"Entries should be separated by a newline and should be valid file types (.md, .mdx, .ipynb). " +
"Mutually exclusive with the other args.",
})
.option("non-api", {
type: "boolean",
description: "Check all the non-API docs, like start/.",
})
.option("current-apis", {
type: "boolean",
description: "Check the pages in the current API docs.",
})
.option("dev-apis", {
type: "boolean",
description: "Check the /dev API docs.",
})
.option("historical-apis", {
type: "boolean",
description:
"Check the pages in the historical API docs, e.g. `api/qiskit/0.44`. " +
"Warning: this is slow.",
})
.option("qiskit-release-notes", {
type: "boolean",
description: "Check the pages in the `api/qiskit/release-notes` folder.",
})
.option("legacy", {
type: "boolean",
description: "Check that the non-API pages render in the legacy app.",
conflicts: [
"current-apis",
"dev-apis",
"historical-apis",
"qiskit-release-notes",
],
})
.parseSync();
};
zxMain(async () => {
const args = readArgs();
await validateDockerRunning();
const files = await determineFilePaths(args);
let failures: string[] = [];
let numFilesChecked = 0;
let renderTimes = [];
for (const fp of files) {
numFilesChecked++;
const response = await canRender(fp);
if (response.ok) {
renderTimes.push(response.timeTaken);
} else {
console.error(`❌ Failed to render: ${fp}`);
failures.push(fp);
}
// This script can be slow, so log progress every 10 files.
if (numFilesChecked % 10 == 0) {
console.log(
`⏳ Checked ${numFilesChecked} / ${files.length} pages ` +
`(~${mean(renderTimes).toFixed(0)}ms per page)`,
);
renderTimes = [];
}
}
if (failures.length === 0) {
console.info(`✅ All ${files.length} pages render without crashing`);
} else {
console.error(
`💔 ${failures.length} pages crash when rendering. This is usually due to invalid syntax, such as forgetting ` +
"the closing component tag, like `</Admonition>`. You can sometimes get a helpful error message " +
"by previewing the docs locally or in CI. See the README for instructions.\n\n" +
failures.join("\n") +
"\n\nIf your files are platform specific, you should add them to the `LEGACY_ONLY_PAGES` or `CLOUD_ONLY_PAGES` list " +
"at the beginning of `scripts/js/commands/checkPagesRender.ts`",
);
process.exit(1);
}
});
type RenderSuccess = { ok: true; timeTaken: number };
type RenderFailure = { ok: false };
async function canRender(fp: string): Promise<RenderSuccess | RenderFailure> {
const url = pathToUrl(fp);
let timeTaken;
try {
const startTime = performance.now();
const response = await fetch(url);
timeTaken = performance.now() - startTime;
if (response.status >= 300) {
return { ok: false };
}
} catch (error) {
return { ok: false };
}
return { ok: true, timeTaken: timeTaken };
}
function pathToUrl(path: string): string {
const strippedPath = path
.replace("docs/", "")
.replace(/\.(?:md|mdx|ipynb)$/g, "");
return `http://localhost:${PORT}/${strippedPath}`;
}
async function validateDockerRunning(): Promise<void> {
try {
const response = await fetch(`http://localhost:${PORT}`);
if (response.status !== 200) {
console.error(
"Failed to access http://localhost:3000. Have you started the Docker server with `./start` in another shell? ",
);
process.exit(1);
}
} catch (error) {
console.error(
"Error when accessing http://localhost:3000. Make sure that you've started the Docker server " +
"with `./start` in another shell.\n\n" +
`${error}`,
);
process.exit(1);
}
}
async function determineFilePaths(args: Arguments): Promise<string[]> {
if (args.fromFile) {
const content = await fs.readFile(args.fromFile, "utf-8");
const result = await globby(content.split("\n").filter((entry) => entry));
return result.filter((fp) => filterPlatformSpecificPage(fp, args.legacy));
}
const globs = [];
if (args.nonApi || args.legacy) {
globs.push("docs/**/*.{ipynb,mdx}");
}
for (const [isIncluded, glob] of [
[args.currentApis, `docs/api/*/*.mdx`],
[args.historicalApis, "docs/api/*/[0-9]*/*.mdx"],
[args.devApis, "docs/api/*/dev/*.mdx"],
[args.qiskitReleaseNotes, "docs/api/qiskit/release-notes/*.mdx"],
]) {
const prefix = isIncluded ? "" : "!";
globs.push(`${prefix}${glob}`);
}
const result = await globby(globs);
return result.filter((fp) => filterPlatformSpecificPage(fp, args.legacy));
}
function filterPlatformSpecificPage(page: string, legacy?: boolean) {
if (legacy) {
// API docs should never be checked with the legacy app because they render
// identically in the cloud app.
return (
!CLOUD_ONLY_PAGES.has(page) &&
!page.startsWith("docs/api") &&
!page.startsWith("docs/tutorials")
);
}
return !LEGACY_ONLY_PAGES.has(page);
}