From 9c3618d66550b7a72cade402b035ecdbcb485625 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Wed, 1 Feb 2023 22:33:15 +0100 Subject: [PATCH] infra(docs): add docs diff script (#1755) --- .gitignore | 1 + package.json | 1 + scripts/apidoc/apiDocsWriter.ts | 8 ++- scripts/apidoc/diff.ts | 98 +++++++++++++++++++++++++++++++++ scripts/apidoc/generate.ts | 11 +++- scripts/apidoc/moduleMethods.ts | 33 +++++++---- scripts/apidoc/utils.ts | 39 +++++++++++++ scripts/diff.ts | 30 ++++++++++ 8 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 scripts/apidoc/diff.ts create mode 100644 scripts/diff.ts diff --git a/.gitignore b/.gitignore index 312f10264ab..ab84a0a2ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,7 @@ versions.json /docs/.vitepress/cache /docs/.vitepress/dist /docs/api/typedoc.json +/docs/public/api-diff-index.json /lib /locale diff --git a/package.json b/package.json index b5517ee7f3a..9961d050e1d 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "docs:dev": "run-s docs:prepare docs:dev:run", "docs:dev:run": "vitepress dev docs", "docs:serve": "vitepress serve docs --port 5173", + "docs:diff": "tsx ./scripts/diff.ts", "format": "prettier --write .", "lint": "eslint --cache --cache-strategy content .", "ts-check:scripts": "tsc --project tsconfig.check-scripts.json", diff --git a/scripts/apidoc/apiDocsWriter.ts b/scripts/apidoc/apiDocsWriter.ts index f18b7cd97e1..d40dec34359 100644 --- a/scripts/apidoc/apiDocsWriter.ts +++ b/scripts/apidoc/apiDocsWriter.ts @@ -9,8 +9,8 @@ import { selectApiMethods, selectApiModules, } from './typedoc'; -import type { PageIndex } from './utils'; -import { pathDocsDir, pathOutputDir } from './utils'; +import type { DocsApiDiffIndex, PageIndex } from './utils'; +import { pathDocsDiffIndexFile, pathDocsDir, pathOutputDir } from './utils'; const pathDocsApiPages = resolve(pathDocsDir, '.vitepress', 'api-pages.ts'); const pathDocsApiSearchIndex = resolve( @@ -120,6 +120,10 @@ export function writeApiPagesIndex(pages: PageIndex): void { writeFileSync(pathDocsApiPages, apiPagesContent); } +export function writeApiDiffIndex(diffIndex: DocsApiDiffIndex): void { + writeFileSync(pathDocsDiffIndexFile, JSON.stringify(diffIndex)); +} + export function writeApiSearchIndex(project: ProjectReflection): void { const apiIndex: APIGroup[] = []; diff --git a/scripts/apidoc/diff.ts b/scripts/apidoc/diff.ts new file mode 100644 index 00000000000..913062c82d7 --- /dev/null +++ b/scripts/apidoc/diff.ts @@ -0,0 +1,98 @@ +import type { DocsApiDiffIndex } from './utils'; +import { nameDocsDiffIndexFile, pathDocsDiffIndexFile } from './utils'; + +/** + * Loads the diff index from the given source url. + * + * @param url The url to load the diff index from. + */ +async function loadRemote(url: string): Promise { + return fetch(url).then((res) => { + if (!res.ok) { + throw new Error( + `Failed to load remote diff index from ${url}: ${res.statusText}` + ); + } else { + return res.json() as Promise; + } + }); +} + +/** + * Loads the diff index from the given local path. + * + * @param path The path to load the diff index from. Should start with `file://` for cross platform compatibility. + */ +async function loadLocal(path: string): Promise { + return import(path).then((imp) => imp.default as DocsApiDiffIndex); +} + +/** + * Loads the diff index from the given source. + * If the source starts with `https://` it will be loaded from the remote url. + * Otherwise it will be loaded from the local path. + * + * @param source The source to load the diff index from. + */ +async function load(source: string): Promise { + if (source.startsWith('https://')) { + return loadRemote(source); + } else { + return loadLocal(source); + } +} + +/** + * Returns a set of all keys from the given entries. + * + * @param entries The entries to get the keys from. + */ +function allKeys( + ...entries: ReadonlyArray> +): Set { + return new Set(entries.map(Object.keys).flat()); +} + +/** + * Compares the target (reference) and source (changed) diff index and returns the differences. + * The returned object contains the module names as keys and the method names as values. + * If the module name is `ADDED` or `REMOVED` it means that the module was added or removed in the local diff index. + * + * @param targetDiffIndex The url to the target (reference) diff index. Defaults to the next.fakerjs.dev diff index. + * @param sourceDiffIndex The path to the source (changed) index. Defaults to the local diff index. + */ +export async function diff( + targetDiffIndex = `https://next.fakerjs.dev/${nameDocsDiffIndexFile}`, + sourceDiffIndex = `file://${pathDocsDiffIndexFile}` +): Promise> { + const target = await load(targetDiffIndex); + const source = await load(sourceDiffIndex); + + const diff: Record = {}; + + for (const moduleName of allKeys(target, source)) { + const remoteModule = target[moduleName]; + const localModule = source[moduleName]; + + if (!remoteModule) { + diff[moduleName] = ['ADDED']; + continue; + } + + if (!localModule) { + diff[moduleName] = ['REMOVED']; + continue; + } + + for (const methodName of allKeys(remoteModule, localModule)) { + const remoteMethod = remoteModule[methodName]; + const localMethod = localModule[methodName]; + + if (remoteMethod !== localMethod) { + (diff[moduleName] ??= []).push(methodName); + } + } + } + + return diff; +} diff --git a/scripts/apidoc/generate.ts b/scripts/apidoc/generate.ts index 1f7c001644b..6aa3ae0c7eb 100644 --- a/scripts/apidoc/generate.ts +++ b/scripts/apidoc/generate.ts @@ -1,5 +1,9 @@ import { resolve } from 'path'; -import { writeApiPagesIndex, writeApiSearchIndex } from './apiDocsWriter'; +import { + writeApiDiffIndex, + writeApiPagesIndex, + writeApiSearchIndex, +} from './apiDocsWriter'; import { processModuleMethods } from './moduleMethods'; import { loadProject } from './typedoc'; import { pathOutputDir } from './utils'; @@ -16,7 +20,10 @@ export async function generate(): Promise { await app.generateJson(project, pathOutputJson); const modules = processModuleMethods(project); - writeApiPagesIndex(modules); + writeApiPagesIndex(modules.map(({ text, link }) => ({ text, link }))); + writeApiDiffIndex( + modules.reduce((data, { text, diff }) => ({ ...data, [text]: diff }), {}) + ); writeApiSearchIndex(project); } diff --git a/scripts/apidoc/moduleMethods.ts b/scripts/apidoc/moduleMethods.ts index b52d47806ec..bf7b93f91f7 100644 --- a/scripts/apidoc/moduleMethods.ts +++ b/scripts/apidoc/moduleMethods.ts @@ -8,7 +8,8 @@ import { selectApiMethodSignatures, selectApiModules, } from './typedoc'; -import type { PageIndex } from './utils'; +import type { PageAndDiffIndex } from './utils'; +import { diffHash } from './utils'; /** * Analyzes and writes the documentation for modules and their methods such as `faker.animal.cat()`. @@ -16,8 +17,10 @@ import type { PageIndex } from './utils'; * @param project The project used to extract the modules. * @returns The generated pages. */ -export function processModuleMethods(project: ProjectReflection): PageIndex { - const pages: PageIndex = []; +export function processModuleMethods( + project: ProjectReflection +): PageAndDiffIndex { + const pages: PageAndDiffIndex = []; // Generate module files for (const module of selectApiModules(project)) { @@ -33,10 +36,11 @@ export function processModuleMethods(project: ProjectReflection): PageIndex { * @param module The module to process. * @returns The generated pages. */ -function processModuleMethod(module: DeclarationReflection): PageIndex { +function processModuleMethod(module: DeclarationReflection): PageAndDiffIndex { const moduleName = extractModuleName(module); const moduleFieldName = extractModuleFieldName(module); console.log(`Processing Module ${moduleName}`); + const comment = toBlock(module.comment); const methods: Method[] = []; @@ -45,22 +49,29 @@ function processModuleMethod(module: DeclarationReflection): PageIndex { selectApiMethodSignatures(module) )) { console.debug(`- ${methodName}`); - methods.push(analyzeSignature(signature, moduleFieldName, methodName)); } - writeApiDocsModulePage( - moduleName, - moduleFieldName, - toBlock(module.comment), - methods - ); + writeApiDocsModulePage(moduleName, moduleFieldName, comment, methods); writeApiDocsData(moduleFieldName, methods); return [ { text: moduleName, link: `/api/${moduleFieldName}.html`, + diff: methods.reduce( + (data, method) => ({ + ...data, + [method.name]: diffHash(method), + }), + { + moduleHash: diffHash({ + name: moduleName, + field: moduleFieldName, + comment, + }), + } + ), }, ]; } diff --git a/scripts/apidoc/utils.ts b/scripts/apidoc/utils.ts index f480c0b10cc..5de80cc3c5b 100644 --- a/scripts/apidoc/utils.ts +++ b/scripts/apidoc/utils.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { resolve } from 'node:path'; // Types @@ -5,10 +6,39 @@ import { resolve } from 'node:path'; export type Page = { text: string; link: string }; export type PageIndex = Page[]; +export type PageAndDiff = Page & { + diff: DocsApiDiff; +}; +export type PageAndDiffIndex = PageAndDiff[]; + +export interface DocsApiDiffIndex { + /** + * The methods in the module by name. + */ + [module: string]: DocsApiDiff; +} + +export interface DocsApiDiff { + /** + * The checksum of the entire module. + */ + moduleHash: string; + /** + * The checksum of the method by name. + */ + [method: string]: string; +} + // Paths const pathRoot = resolve(__dirname, '..', '..'); export const pathDocsDir = resolve(pathRoot, 'docs'); +const pathPublicDir = resolve(pathDocsDir, 'public'); +export const nameDocsDiffIndexFile = 'api-diff-index.json'; +export const pathDocsDiffIndexFile = resolve( + pathPublicDir, + nameDocsDiffIndexFile +); export const pathOutputDir = resolve(pathDocsDir, 'api'); // Functions @@ -22,3 +52,12 @@ export function mapByName( {} ); } + +/** + * Creates a diff hash for the given object. + * + * @param object The object to create a hash for. + */ +export function diffHash(object: unknown): string { + return createHash('md5').update(JSON.stringify(object)).digest('hex'); +} diff --git a/scripts/diff.ts b/scripts/diff.ts new file mode 100644 index 00000000000..09a0704615a --- /dev/null +++ b/scripts/diff.ts @@ -0,0 +1,30 @@ +import { existsSync } from 'node:fs'; +import { argv } from 'node:process'; +import { diff } from './apidoc/diff'; +import { pathDocsDiffIndexFile } from './apidoc/utils'; + +const [target, source] = argv.slice(2); + +if (!source && !existsSync(pathDocsDiffIndexFile)) { + throw new Error( + `Unable to find local diff index file at: ${pathDocsDiffIndexFile}\n + You can run \`pnpm run generate:api-docs\` to generate it.` + ); +} + +diff(target, source) + .then((delta) => { + if (Object.keys(delta).length === 0) { + console.log('No documentation changes detected'); + return; + } + + console.log('Documentation changes detected:'); + for (const [module, methods] of Object.entries(delta)) { + console.log(`- ${module}`); + for (const method of methods) { + console.log(` - ${method}`); + } + } + }) + .catch(console.error);