diff --git a/docgen/api-extractor.v1.json b/docgen/api-extractor.v1.json index f03058f13..880990ee1 100644 --- a/docgen/api-extractor.v1.json +++ b/docgen/api-extractor.v1.json @@ -1,7 +1,7 @@ { "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "extends": "./api-extractor.base.json", - "mainEntryPointFilePath": "/lib/index.d.ts", + "mainEntryPointFilePath": "/lib/v1/index.d.ts", "docModel": { "enabled": true, "apiJsonFilePath": "/docgen/v1/firebase-functions.api.json" diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js deleted file mode 100644 index bf07f3881..000000000 --- a/docgen/generate-docs.js +++ /dev/null @@ -1,383 +0,0 @@ -/** - * @license - * Copyright 2019 Google Inc. - * - * 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. - */ - -const { exec } = require('child-process-promise'); -const fs = require('mz/fs'); -const path = require('path'); -const yargs = require('yargs'); -const yaml = require('js-yaml'); - -const repoPath = path.resolve(`${__dirname}/..`); - -// Command-line options. -const { api: apiVersion } = yargs - .option('api', { - default: 'v1', - describe: 'Typescript source file(s)', - type: 'string', - }) - .version(false) - .help().argv; - -let sourceFile, devsitePath, exclude; -switch (apiVersion) { - case 'v1': - sourceFile = `${repoPath}/src`; - devsitePath = '/docs/reference/functions/'; - exclude = ['"**/v2/**/*.ts"', '"src/index.ts"']; - break; - case 'v2': - sourceFile = `${repoPath}/src/{v2,logger}`; - devsitePath = '/docs/functions/alpha/'; - exclude = []; - break; - default: - throw new Error( - `Unrecognized version ${apiVersion}, must be one of v1 or v2` - ); -} - -const docPath = path.resolve(`${__dirname}/html`); -const contentPath = path.resolve(`${__dirname}/content-sources/${apiVersion}`); -const tempHomePath = path.resolve(`${contentPath}/HOME_TEMP.md`); - -const { JSDOM } = require('jsdom'); - -const typeMap = require('./type-aliases.json'); -const { existsSync } = require('fs'); - -/** - * Strips path prefix and returns only filename. - * @param {string} path - */ -function stripPath(path) { - const parts = path.split('/'); - return parts[parts.length - 1]; -} - -/** - * Runs Typedoc command. - * - * Additional config options come from ./typedoc.js - */ -function runTypedoc() { - const command = `${repoPath}/node_modules/.bin/typedoc ${sourceFile} \ - --out ${docPath} \ - ${exclude.map(ex => "--exclude " + ex).join(" ")} \ - --readme ${tempHomePath} \ - --options ${__dirname}/typedoc.js \ - --theme ${__dirname}/theme`; - - console.log('Running command:\n', command); - return exec(command); -} - -/** - * Moves files from subdir to root. - * @param {string} subdir Subdir to move files out of. - */ -async function moveFilesToRoot(subdir) { - if (existsSync(`${docPath}/${subdir}`)) { - await exec(`mv ${docPath}/${subdir}/* ${docPath}`); - await exec(`rmdir ${docPath}/${subdir}`); - } -} - -/** - * Renames files to remove the leading underscores. - * We need to do this because devsite hides these files. - * Example: - * _cloud_functions_.resource.html => cloud_functions_.resource.html - */ -async function renameFiles() { - const files = await fs.readdir(docPath); - const renames = []; - for (const file of files) { - if (file.startsWith('_') && file.endsWith('html')) { - let newFileName = file.substring(1); - renames.push( - fs.rename(`${docPath}/${file}`, `${docPath}/${newFileName}`) - ); - } - } - await Promise.all(renames); -} - -/** - * Reformat links to match flat structure. - * @param {string} file File to fix links in. - */ -async function fixLinks(file) { - let data = await fs.readFile(file, 'utf8'); - data = addTypeAliasLinks(data); - const flattenedLinks = data - .replace(/\.\.\//g, '') - .replace(/(modules|interfaces|classes)\//g, '') - .replace(/\"_/g, '"'); - let caseFixedLinks = flattenedLinks; - for (const lower in lowerToUpperLookup) { - const re = new RegExp(lower, 'g'); - caseFixedLinks = caseFixedLinks.replace(re, lowerToUpperLookup[lower]); - } - return fs.writeFile(file, caseFixedLinks); -} - -/** - * Adds links to external documentation for type aliases that - * reference an external library. - * - * @param data File data to add external library links to. - */ -function addTypeAliasLinks(data) { - const htmlDom = new JSDOM(data); - /** - * Select .tsd-signature-type because all potential external - * links will have this identifier. - */ - const fileTags = htmlDom.window.document.querySelectorAll( - '.tsd-signature-type' - ); - for (const tag of fileTags) { - const mapping = typeMap[tag.textContent]; - if (mapping) { - console.log('Adding link to ' + tag.textContent + ' documentation.'); - - // Add the corresponding document link to this type - const linkChild = htmlDom.window.document.createElement('a'); - linkChild.setAttribute('href', mapping); - linkChild.textContent = tag.textContent; - tag.textContent = null; - tag.appendChild(linkChild); - } - } - return htmlDom.serialize(); -} - -/** - * Generates temporary markdown file that will be sourced by Typedoc to - * create index.html. - * - * @param {string} tocRaw - * @param {string} homeRaw - */ -function generateTempHomeMdFile(tocRaw, homeRaw) { - const { toc } = yaml.safeLoad(tocRaw); - let tocPageLines = [homeRaw, '# API Reference']; - for (const group of toc) { - tocPageLines.push(`\n## [${group.title}](${stripPath(group.path)})`); - const section = group.section || []; - for (const item of section) { - tocPageLines.push(`- [${item.title}](${stripPath(item.path)})`); - } - } - return fs.writeFile(tempHomePath, tocPageLines.join('\n')); -} - -/** - * Mapping between lowercase file name and correctly cased name. - * Used to update links when filenames are capitalized. - */ -const lowerToUpperLookup = {}; - -/** - * Checks to see if any files listed in toc.yaml were not generated. - * If files exist, fixes filename case to match toc.yaml version. - */ -async function checkForMissingFilesAndFixFilenameCase(tocText) { - // Get filenames from toc.yaml. - const filenames = tocText - .split('\n') - .filter((line) => line.includes('path:')) - .map((line) => { - parts = line.split('/'); - return parts[parts.length - 1].replace(/#.*$/, ''); - }); - // Logs warning to console if a file from TOC is not found. - const fileCheckPromises = filenames.map(async (filename) => { - // Warns if file does not exist, fixes filename case if it does. - // Preferred filename for devsite should be capitalized and taken from - // toc.yaml. - const tocFilePath = `${docPath}/${filename}`; - // Generated filename from Typedoc will be lowercase. - const generatedFilePath = `${docPath}/${filename.toLowerCase()}`; - if (await fs.exists(generatedFilePath)) { - // Store in a lookup table for link fixing. - lowerToUpperLookup[filename.toLowerCase()] = filename; - return fs.rename(generatedFilePath, tocFilePath); - } else { - console.warn( - `Missing file: ${filename} requested ` + - `in toc.yaml but not found in ${docPath}` - ); - } - }); - await Promise.all(fileCheckPromises); - return filenames; -} - -/** - * Gets a list of html files in generated dir and checks if any are not - * found in toc.yaml. - * Option to remove the file if not found (used for node docs). - * - * @param {Array} filenamesFromToc Filenames pulled from toc.yaml - * @param {boolean} shouldRemove Should just remove the file - */ -async function checkForUnlistedFiles(filenamesFromToc, shouldRemove) { - const files = await fs.readdir(docPath); - const htmlFiles = files.filter((filename) => filename.slice(-4) === 'html'); - const removePromises = []; - const filesToRemove = htmlFiles - .filter((filename) => !filenamesFromToc.includes(filename)) - .filter((filename) => filename !== 'index' && filename != 'globals'); - if (filesToRemove.length && !shouldRemove) { - // This is just a warning, it doesn't need to finish before - // the process continues. - console.warn( - `Unlisted files: ${filesToRemove.join(', ')} generated ` + - `but not listed in toc.yaml.` - ); - return htmlFiles; - } - - await Promise.all( - filesToRemove.map((filename) => { - console.log(`REMOVING ${docPath}/${filename} - not listed in toc.yaml.`); - return fs.unlink(`${docPath}/${filename})`); - }) - ); - return htmlFiles.filter((filename) => filenamesFromToc.includes(filename)); -} - -/** - * Writes a _toc_autogenerated.yaml as a record of all files that were - * autogenerated. Helpful to tech writers. - * - * @param {Array} htmlFiles List of html files found in generated dir. - */ -async function writeGeneratedFileList(htmlFiles) { - const fileList = htmlFiles.map((filename) => { - return { - title: filename, - path: `${devsitePath}${filename}`, - }; - }); - const generatedTocYAML = yaml.safeDump({ toc: fileList }); - await fs.writeFile(`${docPath}/_toc_autogenerated.yaml`, generatedTocYAML); - return htmlFiles; -} - -/** - * Fix all links in generated files to other generated files to point to top - * level of generated docs dir. - * - * @param {Array} htmlFiles List of html files found in generated dir. - */ -function fixAllLinks(htmlFiles) { - const writePromises = []; - for (const file of htmlFiles) { - // Update links in each html file to match flattened file structure. - writePromises.push(fixLinks(`${docPath}/${file}`)); - } - return Promise.all(writePromises); -} - -/** - * Main document generation process. - * - * Steps for generating documentation: - * 1) Create temporary md file as source of homepage. - * 2) Run Typedoc, sourcing index.d.ts for API content and temporary md file - * for index.html content. - * 3) Write table of contents file. - * 4) Flatten file structure by moving all items up to root dir and fixing - * links as needed. - * 5) Check for mismatches between TOC list and generated file list. - */ -(async function () { - try { - const [tocRaw, homeRaw] = await Promise.all([ - fs.readFile(`${contentPath}/toc.yaml`, 'utf8'), - fs.readFile(`${contentPath}/HOME.md`, 'utf8'), - ]); - - // Run main Typedoc process (uses index.d.ts and generated temp file above). - await generateTempHomeMdFile(tocRaw, homeRaw); - const output = await runTypedoc(); - - // Typedoc output. - console.log(output.stdout); - await Promise.all([ - // Clean up temp home markdown file. (Nothing needs to wait for this.) - fs.unlink(tempHomePath), - - // Devsite doesn't like css.map files. - // NOTE: This doesn't seem to actually get generated anymore, but we'll keep this here just in case. - async () => { - const cssMap = `${docPath}/assets/css/main.css.map`; - if (await fs.exists(cssMap)) { - await fs.unlink(); - } - }, - - // Write out TOC file. Do this after Typedoc step to prevent Typedoc - // erroring when it finds an unexpected file in the target dir. - fs.writeFile(`${docPath}/_toc.yaml`, tocRaw), - ]); - - // Flatten file structure. These categories don't matter to us and it makes - // it easier to manage the docs directory. - await Promise.all([ - moveFilesToRoot('classes'), - moveFilesToRoot('modules'), - moveFilesToRoot('interfaces'), - ]); - // Rename files to remove the underscores since devsite hides those. - await renameFiles(); - - // Check for files listed in TOC that are missing and warn if so. - // Not blocking. - const filenamesFromToc = await checkForMissingFilesAndFixFilenameCase( - tocRaw - ); - - // Check for files that exist but aren't listed in the TOC and warn. - // (If API is node, actually remove the file.) - // Removal is blocking, warnings aren't. - const htmlFiles = await checkForUnlistedFiles(filenamesFromToc, false); - - // Write a _toc_autogenerated.yaml to record what files were created. - const fileList = await writeGeneratedFileList(htmlFiles); - - // Correct the links in all the generated html files now that files have - // all been moved to top level. - await fixAllLinks(fileList); - const data = await fs.readFile(`${docPath}/index.html`, 'utf8'); - // String to include devsite local variables. - const localVariablesIncludeString = `{% include "docs/web/_local_variables.html" %}\n`; - await fs.writeFile( - `${docPath}/index.html`, - localVariablesIncludeString + data - ); - } catch (err) { - if (err.stdout) { - console.error(err.stdout); - } else { - console.error(err); - } - } -})(); diff --git a/docgen/toc.ts b/docgen/toc.ts index 24cc58092..b359092d8 100644 --- a/docgen/toc.ts +++ b/docgen/toc.ts @@ -1,95 +1,115 @@ -/** - * @license - * Copyright 2022 Google LLC - * - * 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. - */ - /** * Forked of https://github.com/firebase/firebase-js-sdk/blob/5ce06766303b92fea969c58172a7c1ab8695e21e/repo-scripts/api-documenter/src/toc.ts. + * + * Firebase Functions SDK uses namespaces as primary entry points but the theoriginal Firebase api-documenter ignores + * them when generating toc.yaml. A small modification is made to include namespaces and exclude classes when walking + * down the api model. */ -import { writeFileSync } from 'fs'; -import { resolve } from 'path'; - -import { FileSystem } from '@rushstack/node-core-library'; import * as yaml from 'js-yaml'; +import { + ApiPackage, + ApiItem, + ApiItemKind, + ApiParameterListMixin, + ApiModel, +} from 'api-extractor-model-me'; +import { ModuleSource } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; +import { FileSystem, PackageName } from '@rushstack/node-core-library'; import yargs from 'yargs'; +import { writeFileSync } from 'fs'; +import { resolve, join } from 'path'; -export interface TocGenerationOptions { +function getSafeFileName(f: string): string { + return f.replace(/[^a-z0-9_\-\.]/gi, '_').toLowerCase(); +} + +export function getFilenameForApiItem( + apiItem: ApiItem, + addFileNameSuffix: boolean +): string { + if (apiItem.kind === ApiItemKind.Model) { + return 'index.md'; + } + + let baseName: string = ''; + let multipleEntryPoints: boolean = false; + for (const hierarchyItem of apiItem.getHierarchy()) { + // For overloaded methods, add a suffix such as "MyClass.myMethod_2". + let qualifiedName = getSafeFileName(hierarchyItem.displayName); + if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { + if (hierarchyItem.overloadIndex > 1) { + // Subtract one for compatibility with earlier releases of API Documenter. + // (This will get revamped when we fix GitHub issue #1308) + qualifiedName += `_${hierarchyItem.overloadIndex - 1}`; + } + } + + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + break; + case ApiItemKind.EntryPoint: + const packageName: string = hierarchyItem.parent!.displayName; + let entryPointName: string = PackageName.getUnscopedName(packageName); + if (multipleEntryPoints) { + entryPointName = `${PackageName.getUnscopedName(packageName)}/${ + hierarchyItem.displayName + }`; + } + baseName = getSafeFileName(entryPointName); + break; + case ApiItemKind.Package: + baseName = getSafeFileName( + PackageName.getUnscopedName(hierarchyItem.displayName) + ); + if ((hierarchyItem as ApiPackage).entryPoints.length > 1) { + multipleEntryPoints = true; + } + break; + case ApiItemKind.Namespace: + baseName += '.' + qualifiedName; + if (addFileNameSuffix) { + baseName += '_n'; + } + break; + case ApiItemKind.Class: + case ApiItemKind.Interface: + baseName += '.' + qualifiedName; + break; + } + } + return baseName + '.md'; +} + +export interface ITocGenerationOptions { inputFolder: string; - outputFolder: string; g3Path: string; + outputFolder: string; + addFileNameSuffix: boolean; } -interface TocItem { +interface ITocItem { title: string; path: string; - section?: TocItem[]; - status?: 'experimental'; -} - -function fileExt(f: string) { - const parts = f.split('.'); - if (parts.length < 2) { - return ''; - } - return parts.pop(); + section?: ITocItem[]; } export function generateToc({ inputFolder, g3Path, outputFolder, -}: TocGenerationOptions) { - const asObj = FileSystem.readFolder(inputFolder) - .filter((f) => fileExt(f) === 'md') - .reduce((acc, f) => { - const parts = f.split('.'); - parts.pop(); // Get rid of file extenion (.md) - - let cursor = acc; - for (const p of parts) { - cursor[p] = cursor[p] || {}; - cursor = cursor[p]; - } - return acc; - }, {} as any); + addFileNameSuffix, +}: ITocGenerationOptions) { + const apiModel: ApiModel = new ApiModel(); - function toToc(obj, prefix = ''): TocItem[] { - const toc: TocItem[] = []; - for (const key of Object.keys(obj)) { - const path = prefix?.length ? `${prefix}.${key}` : key; - const section = toToc(obj[key], path); - const tic: TocItem = { - title: key, - path: `${g3Path}/${path}.md`, - }; - if (section.length > 0) { - tic.section = section; - } - toc.push(tic); + for (const filename of FileSystem.readFolder(inputFolder)) { + if (filename.match(/\.api\.json$/i)) { + const filenamePath = join(inputFolder, filename); + apiModel.loadPackage(filenamePath); } - return toc; } - const toc: TocItem[] = [ - { - title: 'firebase-functions', - status: 'experimental', - path: `${g3Path}/firebase-functions.md`, - }, - ...toToc(asObj['firebase-functions'], 'firebase-functions'), - ]; + const toc = []; + generateTocRecursively(apiModel, g3Path, addFileNameSuffix, toc); writeFileSync( resolve(outputFolder, 'toc.yaml'), @@ -102,6 +122,49 @@ export function generateToc({ ); } +function generateTocRecursively( + apiItem: ApiItem, + g3Path: string, + addFileNameSuffix: boolean, + toc: ITocItem[] +) { + // generate toc item only for entry points + if (apiItem.kind === ApiItemKind.EntryPoint) { + // Entry point + const entryPointName = ( + apiItem.canonicalReference.source! as ModuleSource + ).escapedPath.replace('@firebase/', ''); + const entryPointToc: ITocItem = { + title: entryPointName, + path: `${g3Path}/${getFilenameForApiItem(apiItem, addFileNameSuffix)}`, + section: [], + }; + + for (const member of apiItem.members) { + // only classes and interfaces have dedicated pages + if ( + member.kind === ApiItemKind.Interface || + member.kind === ApiItemKind.Namespace + ) { + const fileName = getFilenameForApiItem(member, addFileNameSuffix); + const title = + member.displayName[0].toUpperCase() + member.displayName.slice(1); + entryPointToc.section!.push({ + title, + path: `${g3Path}/${fileName}`, + }); + } + } + + toc.push(entryPointToc); + } else { + // travel the api tree to find the next entry point + for (const member of apiItem.members) { + generateTocRecursively(member, g3Path, addFileNameSuffix, toc); + } + } +} + const { input, output, path } = yargs(process.argv.slice(2)) .option('input', { alias: 'i', @@ -121,4 +184,9 @@ const { input, output, path } = yargs(process.argv.slice(2)) .help().argv; FileSystem.ensureFolder(output); -generateToc({ inputFolder: input, g3Path: path, outputFolder: output }); +generateToc({ + inputFolder: input, + g3Path: path, + outputFolder: output, + addFileNameSuffix: false, +}); diff --git a/docgen/type-aliases.json b/docgen/type-aliases.json deleted file mode 100644 index 240c50269..000000000 --- a/docgen/type-aliases.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "firebase.firestore.DocumentSnapshot": "https://googleapis.dev/nodejs/firestore/latest/DocumentSnapshot.html", - "firebase.auth.UserRecord": "https://firebase.google.com/docs/reference/admin/node/admin.auth.UserRecord.html", - "firebase.auth.UserInfo": "https://firebase.google.com/docs/reference/admin/node/admin.auth.UserInfo.html" -} diff --git a/docgen/typedoc.js b/docgen/typedoc.js deleted file mode 100644 index 332e19b0f..000000000 --- a/docgen/typedoc.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2019 Google Inc. - * - * 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. - */ - -const options = { - tsconfig: '../tsconfig.release.json', - excludeExternals: true, - exclude: ['../spec/**/*.ts', '../integration_test/**/*.ts'], - name: 'Firebase Functions SDK', - hideGenerator: true, -}; - -module.exports = options; diff --git a/package.json b/package.json index defe43758..5a49afe70 100644 --- a/package.json +++ b/package.json @@ -145,14 +145,13 @@ "registry": "https://wombat-dressing-room.appspot.com" }, "scripts": { - "apidocs": "node docgen/generate-docs.js", "docgen:v1:extract": "api-extractor run -c docgen/api-extractor.v1.json --local", - "docgen:v1:toc": "ts-node docgen/toc.ts --input docgen/v1/markdown --output docgen/v1/markdown/toc --path /docs/reference/functions", + "docgen:v1:toc": "ts-node docgen/toc.ts --input docgen/v1 --output docgen/v1/markdown/toc --path /docs/reference/functions", "docgen:v1:gen": "api-documenter-fire markdown -i docgen/v1 -o docgen/v1/markdown --project functions && npm run docgen:v1:toc", "docgen:v1": "npm run build && npm run docgen:v1:extract && npm run docgen:v1:gen", "docgen:v2:extract": "api-extractor run -c docgen/api-extractor.v2.json --local", - "docgen:v2:toc": "ts-node docgen/toc.ts --input docgen/v2/markdown --output docgen/v2/markdown/toc --path /docs/functions/beta/reference", - "docgen:v2:gen": "api-documenter-fire markdown -i docgen/v2 -o docgen/v2/markdown && npm run docgen:v2:toc", + "docgen:v2:toc": "ts-node docgen/toc.ts --input docgen/v2 --output docgen/v2/markdown/toc --path /docs/functions/beta/reference", + "docgen:v2:gen": "api-documenter-fire markdown -i docgen/v2 -e docgen/v2/markdown --project functions && npm run docgen:v2:toc", "docgen:v2": "npm run build && npm run docgen:v2:extract && npm run docgen:v2:gen", "build:pack": "rm -rf lib && npm install && tsc -p tsconfig.release.json && npm pack", "build:release": "npm ci --production && npm install --no-save typescript firebase-admin && tsc -p tsconfig.release.json", @@ -215,7 +214,6 @@ "semver": "^7.3.5", "sinon": "^7.3.2", "ts-node": "^10.4.0", - "typedoc": "0.23.7", "typescript": "^4.3.5", "yargs": "^15.3.1" },