Files
talk/scripts/generateEventDocs.ts
T
Vinh c9a0ab8848 [CORL-878] Upgrade dependencies (#2867)
* chore: upgrade fluent

* chore: upgrade metascraper

* chore: upgrade akismet-api

* chore: upgrade apollo-server-express

* chore: upgrade archiver

* chore: upgrade bull

* chore: upgrade express, cheerio, content-security-policy-builder

* chore: upgrade convict

* chore: upgrade cors, cron

* chore: upgrade csv-stringify

* chore: upgrade dompurify

* chore: upgrade dotenv

* chore: upgrade express-static-gzip

* chore: upgrade fs-extra

* chore: upgrade graphql-js

* chore: upgrade graphql packages

* chore: upgrade html-minifier

* chore: upgrade html-to-text

* chore: upgrade ioredis

* chore: upgrade joi

* chore: upgrade jsdom

* chore: upgrade jsonwebtoken

* chore: upgrade juice

* chore: upgrade jwks-rsa ad linkifyjs

* chore: upgrade lodash

* chore: upgrade luxon

* chore: upgrade metascraper

* chore: upgrade mongodb

* chore: upgrade ms

* chore: upgrade node and node-fetch types

* chore: upgrade nodemailer nunjucks and typescript-eslint

* chore: Upgrade passport

* upgrade: prom-client react-helmet source-map-support stack-utils

* chore: upgrade uuid

* chore: upgrade @babel packages

* chore: upgrade types

* chore: upgrade autoprefixer

* chore: upgrade jest

* chore: upgrade ts-jest

* chore: remove linkify.d.ts

* chore: upgrade bowser

* chore: case-sensitive-paths-webpack-plugin

* chore: upgrade classnames

* chore: upgrade commander

* chore: upgrade comment-json

* chore: upgrade cross-spawn compression-webpack-plugin del

* chore: upgrade build and watch related dependencies

* chore: upgrade css-vars-ponyfill

* chore: upgrade eslint and css-vars-ponyfill

* chore: upgrade enzyme and eventemitter2

* fix: form bug

* chore: upgrade farce

* chore: upgrade final form

* chore: upgrade react-popper

* chore: upgrade flat and fork-ts-checker-webpack-plugin

* chore: upgrade husky and gulp related, intersection observer

* chore: upgrade lint-staged

* chore: upgrade marked, loader-utils, mini-css-extract-plugin

* chore: upgrade postcss-nested, proxy-polyfill, pstree.remy

* chore: upgrade prettier

* chore: fix prettier changes, upgrade react

* chore: mute createFactory deprecated message

* chore: upgrade react-copy-to-clipbard, react-axe, react-dom, react-test-renderer, react-timeago

* chore: upgrade react-transistion-group, react-responsive

* chore: upgrade types

* chore: upgrade react-dev-utils, react-error-overlay regenerator-runtime

* chore: upgrade types, sinon, sockjs-client, strip-ansi

* chore: upgrade types, fonts

* chore: upgrade nunjucks, ts-node, typescript-snapshot-plugin, wait-for-expect

* chore: upgrade eslint packages

* chore: upgrade fluent, types

* chore: upgrade jsdom dep

* chore: upgrade mongo

* chore: upgrade deps

* chore: upgrade typescript, recompose

* chore: upgrade prettier

* chore: remove obsolete prettier config

* chore: upgrade jsdom types

* chore: upgrade typescript-eslint

* chore: upgrad deps

* chore: upgrade deps

* chore: upgrade relay related modules

* chore: upgrade docz WIP

* chore: upgrade docz

* chore: add guard

* chore: remove obsolete line

* chore: comment

* chore: refactors

* fix: hook count change error
2020-04-15 18:15:31 +02:00

278 lines
7.5 KiB
TypeScript

/* eslint-disable no-bitwise */
import { codeBlock, stripIndent } from "common-tags";
import fs from "fs";
import path from "path";
import ts from "typescript";
interface DocEntry {
name: string;
docs?: string;
type: "ViewerNetworkEvent" | "ViewerEvent";
text?: string;
}
/**
* We use this regexp to find a previous block that we
* are going to update in the readme file.
*/
const BLOCK_REGEXP = /<!-- START docs:events -->(.|\n)*<!-- END docs:events -->/gm;
/** Build flags that affects AST generation */
const buildFlags =
// Do not truncate output.
ts.NodeBuilderFlags.NoTruncation |
// Use multiline object literals format.
ts.NodeBuilderFlags.MultilineObjectLiterals;
/** Generate documentation for all classes in a set of .ts files */
function gatherEntries(
fileNames: string[],
options: ts.CompilerOptions
): DocEntry[] {
// Build a program using the set of root file names in fileNames
const program = ts.createProgram(fileNames, options);
const printer = ts.createPrinter({
noEmitHelpers: true,
omitTrailingSemicolon: true,
removeComments: false,
});
// Get the checker, we will use it to find more about classes
const checker = program.getTypeChecker();
const data: DocEntry[] = [];
/** Hold a pointer to the sourcefile we are currently processing. */
let currentSourceFile: ts.SourceFile;
// Visit every sourceFile in the program
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
currentSourceFile = sourceFile;
// Walk the tree to search for classes
ts.forEachChild(sourceFile, visit);
}
}
const sorted = data.sort((a, b) => {
if (a.name > b.name) {
return 1;
}
if (b.name > a.name) {
return -1;
}
return 0;
});
return sorted;
/** visit nodes finding exported events */
function visit(node: ts.Node) {
// Only consider exported nodes
if (!isNodeExported(node)) {
return;
}
if (ts.isVariableStatement(node)) {
if (
!node.getFullText().includes("createViewerNetworkEvent") &&
!node.getFullText().includes("createViewerEvent")
) {
return;
}
const firstChild = node.declarationList.declarations[0];
if (ts.isVariableDeclaration(firstChild)) {
const symbol = checker.getSymbolAtLocation(firstChild.name);
if (symbol) {
serializeEventSymbol(symbol);
}
}
}
}
function serializeEventSymbol(symbol: ts.Symbol) {
const type = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration
);
const typeNode = checker.typeToTypeNode(type, undefined, buildFlags)!;
const typeName = symbol.getName();
const entry: DocEntry = {
name: typeName,
docs: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
type: type.getSymbol()!.getName() as DocEntry["type"],
};
typeNode.forEachChild((ch) => {
if (ts.isTypeLiteralNode(ch)) {
const text = printer.printNode(
ts.EmitHint.Unspecified,
ch,
currentSourceFile
);
if (text !== "{}") {
entry.text = text;
}
/*
Go through each parameter.
ch.members.forEach(m => {
if (ts.isPropertySignature(m)) {
if (ts.isIdentifier(m.name)) {
data.parameters[m.name.text] = printer.printNode(
ts.EmitHint.Unspecified,
m.type!,
currentSourceFile
);
}
}
});
*/
}
});
data.push(entry);
}
/** True if this is visible outside this file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-unnecessary-type-assertion
(ts.getCombinedModifierFlags(node as ts.Declaration) &
ts.ModifierFlags.Export) !==
0 ||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
);
}
}
function prefixLines(text: string, prefix: string) {
return text.split("\n").join(`\n${prefix}`);
}
function getEventName(typeName: string) {
return (
typeName[0].toLocaleLowerCase() +
typeName.slice(1, typeName.length - "Event".length)
);
}
/**
* Removes "%future added value" from text. This is a placeholder type
* added by Relay to help with future proofness.
*/
function removeFutureAddedValue(text: string) {
return text
.replace(': "%future added value" | ', ": ")
.replace(' | "%future added value"', "");
}
/**
* Append or update previous documention in markdownFile.
*
* @param markdownFile The markdown file we want to inject the docs too.
* @param entries data as returned by gatherEntries.
*/
function emitDocs(markdownFile: string, entries: DocEntry[], verify = false) {
const previousContent = fs.existsSync(markdownFile)
? fs.readFileSync(markdownFile).toString()
: "";
const summary = stripIndent`
- ${entries
.map(
(e) => `<a href="#${getEventName(e.name)}">${getEventName(e.name)}</a>`
)
.join("\n - ")}
`;
const list = entries
.map(
(e) =>
codeBlock`
- ${
e.type === "ViewerEvent"
? `<a id="${getEventName(e.name)}">**${getEventName(e.name)}**</a>`
: `<a id="${getEventName(e.name)}">**${getEventName(
e.name
)}.success**, **${getEventName(e.name)}.error**</a>`
}: ${e.docs ? e.docs.replace("\n", " ") : ""}
${
e.text
? codeBlock`
\`\`\`ts
${removeFutureAddedValue(e.text)}
\`\`\`
`
: ""
}
`
)
.join("\n");
const output = stripIndent`
<!-- START docs:events -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:events -->
### Index
${prefixLines(summary, " ")}
### Events
${prefixLines(list, " ")}
<!-- END docs:events -->
`;
let newContent;
// Find previous block.
if (BLOCK_REGEXP.test(previousContent)) {
newContent = previousContent.replace(BLOCK_REGEXP, output);
} else {
newContent = previousContent + "\n" + output;
}
if (previousContent === newContent) {
// eslint-disable-next-line no-console
console.log(`${markdownFile} is up to date`);
return;
}
if (verify) {
// eslint-disable-next-line no-console
console.error(
`${markdownFile} is outdated, please run \`npm run docs:events\``
);
process.exit(1);
return;
}
fs.writeFileSync(markdownFile, newContent);
// eslint-disable-next-line no-console
console.log(`Successfully injected documentation into ${markdownFile}`);
}
function main() {
if (process.argv.length < 4) {
throw new Error("Must provide path to events and a markdown file.");
}
const eventFile = process.argv[2];
const markdownFile = process.argv[3];
// Find tsconfig file.
const configFile = ts.findConfigFile(eventFile, fs.existsSync);
if (!configFile) {
throw new Error("tsconfig file not found");
}
const configText = fs.readFileSync(configFile).toString();
const result = ts.parseConfigFileTextToJson(configFile, configText);
if (result.error) {
throw result.error;
}
// Parse the JSON raw data into actual consumable compiler options.
const config = ts.parseJsonConfigFileContent(
result.config,
ts.sys,
path.dirname(configFile)
);
const entries = gatherEntries([eventFile], config.options);
emitDocs(markdownFile, entries, process.argv[4] === "--verify");
}
main();