'use strict';
const {
cpSync,
existsSync,
mkdtempSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} = require('node:fs');
const { spawnSync } = require('node:child_process');
const { createRequire } = require('node:module');
const { tmpdir } = require('node:os');
const { dirname, join, resolve } = require('node:path');
const repoRoot = resolve(__dirname, '..');
const websiteDir = __dirname;
const repoRequire = createRequire(join(repoRoot, 'package.json'));
const websiteRequire = createRequire(join(websiteDir, 'package.json'));
const typedocTemplatePath = join(__dirname, 'typedoc-api.json');
const tmpDir = mkdtempSync(join(tmpdir(), 'graphql-js-api-'));
const prettier = repoRequire('prettier');
const ts = websiteRequire('typescript');
const prettierConfig = prettier.resolveConfig.sync(repoRoot) ?? {};
const signaturePrettierOptions = {
...prettierConfig,
parser: 'typescript',
};
let generation = {
docsVersionLabel: 'api-docs',
};
const worktreeDirs = [];
const groupOrder = [
'Classes',
'Functions',
'Constants',
'Enumerations',
'Types',
];
const ReflectionKind = {
Namespace: 4,
Enum: 8,
Variable: 32,
Function: 64,
Class: 128,
Interface: 256,
Constructor: 512,
Property: 1024,
Method: 2048,
IndexSignature: 8192,
Parameter: 32768,
TypeAlias: 2097152,
Reference: 4194304,
};
const keywordLikeIdentifiers = new Set(['false', 'null', 'true', 'undefined']);
const literalTokenKinds = new Set([
ts.SyntaxKind.NumericLiteral,
ts.SyntaxKind.BigIntLiteral,
ts.SyntaxKind.StringLiteral,
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
]);
const typeNodeKeywordNames = new Map([
[ts.SyntaxKind.AnyKeyword, 'any'],
[ts.SyntaxKind.BigIntKeyword, 'bigint'],
[ts.SyntaxKind.BooleanKeyword, 'boolean'],
[ts.SyntaxKind.NeverKeyword, 'never'],
[ts.SyntaxKind.NullKeyword, 'null'],
[ts.SyntaxKind.NumberKeyword, 'number'],
[ts.SyntaxKind.ObjectKeyword, 'object'],
[ts.SyntaxKind.StringKeyword, 'string'],
[ts.SyntaxKind.SymbolKeyword, 'symbol'],
[ts.SyntaxKind.UndefinedKeyword, 'undefined'],
[ts.SyntaxKind.UnknownKeyword, 'unknown'],
[ts.SyntaxKind.VoidKeyword, 'void'],
]);
const apiCodeComponents = ['ApiSignature', 'ApiType'];
const deprecatedTagMarkup =
'<span aria-label="Deprecated" className="api-tag" title="Deprecated">Deprecated</span>';
const renderContext = emptyRenderContext();
let sourceContext = emptySourceContext();
const visibleChildrenCache = new WeakMap();
function run(command, args, cwd) {
const result = spawnSync(command, args, {
cwd,
stdio: 'inherit',
env: process.env,
});
const failure = spawnFailureMessage(command, args, cwd, result);
if (failure != null) {
throw new Error(failure);
}
}
function spawnFailureMessage(command, args, cwd, result) {
const commandText = [command, ...args].join(' ');
if (result.error != null) {
return `${commandText} failed to start in ${cwd}: ${result.error.message}`;
}
if (result.status !== 0) {
const reason =
result.signal == null
? `exit code ${result.status}`
: `signal ${result.signal}`;
return `${commandText} failed in ${cwd} with ${reason}`;
}
return null;
}
function checkoutSourceRef(ref, index) {
const dir = join(tmpDir, `source-${index}`);
run('git', ['worktree', 'add', '--detach', dir, ref], repoRoot);
worktreeDirs.push(dir);
return dir;
}
function removeSourceWorktrees() {
for (let i = worktreeDirs.length - 1; i >= 0; i--) {
const dir = worktreeDirs[i];
const result = spawnSync('git', ['worktree', 'remove', '--force', dir], {
cwd: repoRoot,
stdio: 'inherit',
env: process.env,
});
const failure = spawnFailureMessage(
'git',
['worktree', 'remove', '--force', dir],
repoRoot,
result,
);
if (failure != null) {
console.error(`[api-docs] ${failure}`);
}
}
}
function readJson(path) {
try {
return JSON.parse(readFileSync(path, 'utf8'));
} catch (error) {
fail(`Cannot parse JSON ${path}: ${error.message}`);
}
}
function readTsConfig(path) {
const parsed = ts.parseConfigFileTextToJson(path, readFileSync(path, 'utf8'));
if (parsed.error != null) {
const message = ts.flattenDiagnosticMessageText(
parsed.error.messageText,
'\n',
);
fail(`Cannot parse ${path}: ${message}`);
}
return parsed.config;
}
function writeJson(path, value) {
writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
}
function fail(message) {
throw new Error(`[${generation.docsVersionLabel}] ${message}`);
}
function sourceFile(path, content) {
return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
}
function assertSourceRoot(sourceDir) {
if (!existsSync(join(sourceDir, 'src/index.ts'))) {
fail(`Source directory does not look like graphql-js root: ${sourceDir}`);
}
}
function sourceMajorVersion(sourceDir) {
const version = readJson(join(sourceDir, 'package.json')).version;
const match = typeof version === 'string' ? /^(\d+)\./.exec(version) : null;
if (match == null) {
fail(`Cannot infer major version from package version: ${version}`);
}
return Number(match[1]);
}
function configureGeneration(ref, dir) {
generation = {
docsVersionLabel: ref,
};
assertSourceRoot(dir);
const majorVersion = sourceMajorVersion(dir);
const apiVersion = `api-v${majorVersion}`;
generation = {
apiVersion,
docsBasePath: `/${apiVersion}`,
docsVersionLabel: apiVersion,
jsonPath: join(tmpDir, `${apiVersion}.json`),
majorVersion,
outputDir: join(websiteDir, `pages/${apiVersion}`),
sourceDir: dir,
tmpSourceDir: join(tmpDir, `${apiVersion}-source`),
typedocOptionsPath: join(tmpDir, `${apiVersion}-typedoc.json`),
};
return majorVersion;
}
function walkFiles(dir, fn) {
const entries = readdirSync(dir, { withFileTypes: true }).sort(
(left, right) => left.name.localeCompare(right.name),
);
for (const entry of entries) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
walkFiles(path, fn);
} else if (entry.isFile()) {
fn(path);
}
}
}
function collectRootExportNames(sourceRootDir) {
const path = join(sourceRootDir, 'src/index.ts');
const ast = sourceFile(path, readFileSync(path, 'utf8'));
const names = new Set();
for (const statement of ast.statements) {
if (ts.isExportDeclaration(statement)) {
const specifier = statement.moduleSpecifier;
if (
specifier == null ||
!ts.isStringLiteral(specifier) ||
!isRootSpecifier(specifier.text) ||
!statement.exportClause ||
!ts.isNamedExports(statement.exportClause)
) {
continue;
}
for (const element of statement.exportClause.elements) {
names.add((element.propertyName ?? element.name).text);
}
continue;
}
if (isExported(statement)) {
const name = statement.name?.text;
if (name != null) {
names.add(name);
} else if (ts.isVariableStatement(statement)) {
for (const declaration of statement.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)) {
names.add(declaration.name.text);
}
}
}
}
}
return names;
}
function emptySourceMetadata() {
return {
defaultValuesByRef: new Map(),
importsByRef: new Map(),
typesByRef: new Map(),
};
}
function emptyDocsIndex() {
return {
docsById: new Map(),
docsBySymbol: new Map(),
typeParameterDefaultsById: new Map(),
};
}
function emptyRenderContext() {
return {
docsBasePath: '',
docsIndex: emptyDocsIndex(),
};
}
function emptySourceContext() {
return {
metadata: emptySourceMetadata(),
rootExportNames: new Set(),
};
}
function collectSourceMetadata(sourceRootDir) {
const metadata = emptySourceMetadata();
walkFiles(join(sourceRootDir, 'src'), (path) => {
if (!path.endsWith('.ts')) {
return;
}
const ast = sourceFile(path, readFileSync(path, 'utf8'));
const packagePath = path.slice(sourceRootDir.length + 1);
for (const statement of ast.statements) {
collectTypeDefinition(metadata, statement, packagePath);
collectImportedTypes(
metadata,
sourceRootDir,
path,
statement,
packagePath,
);
collectDeclarationDefaults(metadata, statement, packagePath);
}
});
return metadata;
}
function analyzeSourceSnapshot(sourceRootDir) {
return {
metadata: collectSourceMetadata(sourceRootDir),
rootExportNames: collectRootExportNames(sourceRootDir),
};
}
function collectTypeDefinition(metadata, statement, packagePath) {
if (
(ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement)) &&
statement.name != null
) {
metadata.typesByRef.set(sourceTypeKey(packagePath, statement.name.text), {
isPublic: isExported(statement) || hasJSDocTag(statement, 'public'),
packagePath,
node: statement,
});
}
}
function collectImportedTypes(
metadata,
sourceRootDir,
path,
statement,
packagePath,
) {
if (!ts.isImportDeclaration(statement)) {
return;
}
const targetPackagePath = importPackagePath(sourceRootDir, path, statement);
const bindings = statement.importClause?.namedBindings;
if (
targetPackagePath == null ||
bindings == null ||
!ts.isNamedImports(bindings)
) {
return;
}
for (const element of bindings.elements) {
metadata.importsByRef.set(sourceTypeKey(packagePath, element.name.text), {
packagePath: targetPackagePath,
qualifiedName: (element.propertyName ?? element.name).text,
});
}
}
function collectDeclarationDefaults(metadata, statement, packagePath) {
if (ts.isFunctionDeclaration(statement) && statement.name != null) {
collectParameterDefaults(
metadata,
packagePath,
statement.name.text,
statement,
);
return;
}
if (!ts.isClassDeclaration(statement) || statement.name == null) {
return;
}
for (const member of statement.members) {
if (ts.isConstructorDeclaration(member)) {
collectParameterDefaults(
metadata,
packagePath,
statement.name.text,
member,
);
} else if (ts.isMethodDeclaration(member)) {
const name = propertyNameText(member.name);
if (name != null) {
collectParameterDefaults(metadata, packagePath, name, member);
}
}
}
}
function importPackagePath(sourceRootDir, path, statement) {
if (!ts.isStringLiteral(statement.moduleSpecifier)) {
return null;
}
const specifier = statement.moduleSpecifier.text;
if (!specifier.startsWith('.')) {
return null;
}
const resolvedPath = resolve(dirname(path), specifier);
const candidates = [`${resolvedPath}.ts`, join(resolvedPath, 'index.ts')];
const targetPath = candidates.find((candidate) => existsSync(candidate));
return targetPath == null ? null : targetPath.slice(sourceRootDir.length + 1);
}
function collectParameterDefaults(
metadata,
packagePath,
declarationName,
declaration,
) {
for (const parameter of declaration.parameters ?? []) {
if (!ts.isIdentifier(parameter.name) || parameter.initializer == null) {
continue;
}
metadata.defaultValuesByRef.set(
sourceDefaultKey(packagePath, declarationName, parameter.name.text),
parameter.initializer.getText(),
);
}
}
function propertyNameText(name) {
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
}
function hasJSDocTag(node, tagName) {
return ts
.getJSDocTags(node)
.some((jsDocTag) => jsDocTag.tagName.text === tagName);
}
function sourceTypeKey(packagePath, qualifiedName) {
return `${packagePath}:${qualifiedName}`;
}
function sourceDefaultKey(packagePath, declarationName, parameterName) {
return `${packagePath}:${declarationName}:${parameterName}`;
}
function isRootSpecifier(specifier) {
return specifier.startsWith('./') && !specifier.slice(2).includes('/');
}
function writeMeta(dir, entries) {
const content = [
'const meta = {',
entries.map(metaEntry).join('\n'),
'};',
'',
'export default meta;',
'',
].join('\n');
writeFileSync(join(dir, '_meta.ts'), content);
}
function metaEntry([key, value]) {
const property = metaKey(key);
if (typeof value === 'string') {
return ` ${property}: '${value}',`;
}
return [
` ${property}: {`,
` title: '${value.title}',`,
` href: '${value.href}',`,
' },',
].join('\n');
}
function metaKey(value) {
return /^[A-Za-z_$][\w$]*$/.test(value) ? value : `'${value}'`;
}
function slug(text) {
return text
.replace(/\\/g, '')
.replace(/`|\(\)$/g, '')
.toLowerCase()
.replace(/[^\w]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function commentBlockTag(comment, name) {
return comment?.blockTags?.find((block) => block.tag === name);
}
function hasCommentTag(comment, name) {
return (
comment?.modifierTags?.includes(name) === true ||
comment?.blockTags?.some((block) => block.tag === name) === true
);
}
function hasReflectionTag(node, name) {
return (
hasCommentTag(node.comment, name) ||
node.signatures?.some((signature) =>
hasCommentTag(signature.comment, name),
) === true
);
}
function tagText(comment, name, options) {
const block = commentBlockTag(comment, name);
return block == null ? '' : renderParts(block.content, options).trim();
}
function defaultText(node, parent, options = {}) {
const value = rawDefaultValue(node, parent, options);
return value == null ? '' : apiCode(value);
}
function rawDefaultValue(node, parent, options = {}) {
return (
sourceDefaultValue(node, parent, options) ??
(node.defaultValue == null || node.defaultValue === '...'
? null
: node.defaultValue)
);
}
function sourceDefaultValue(node, parent, options = {}) {
if (
node.kind !== ReflectionKind.Parameter ||
parent?.name == null ||
node?.name == null
) {
return null;
}
if (options.sourcePackagePath == null) {
return null;
}
return (
sourceContext.metadata.defaultValuesByRef.get(
sourceDefaultKey(options.sourcePackagePath, parent.name, node.name),
) ?? null
);
}
function summary(node) {
return renderParts(node.comment?.summary ?? [], {
linkCodeSpans: true,
}).trim();
}
function directCategory(node) {
return (
tagText(node.comment, '@category') ||
tagText(node.signatures?.[0]?.comment, '@category') ||
null
);
}
function resolveItemCategory(node, siblings = []) {
const ownCategory = directCategory(node);
if (ownCategory != null && ownCategory !== '') {
return ownCategory;
}
if (isEnumNamespace(node)) {
return commonCategory(enumLikeMembers(node).map(directCategory));
}
for (const sibling of siblings) {
if (sibling === node || sibling.name !== node.name) {
continue;
}
const siblingCategory = directCategory(sibling);
if (siblingCategory != null) {
return siblingCategory;
}
}
return null;
}
function commonCategory(categories) {
const visibleCategories = categories.filter(Boolean);
if (visibleCategories.length === 0) {
return null;
}
const [first] = visibleCategories;
return visibleCategories.every((item) => item === first) ? first : null;
}
function sanitizeTsConfig(tsconfig) {
const compilerOptions = tsconfig.compilerOptions ?? {};
delete compilerOptions.importsNotUsedAsValues;
delete compilerOptions.rewriteRelativeImportExtensions;
delete compilerOptions.erasableSyntaxOnly;
if (Array.isArray(compilerOptions.lib)) {
compilerOptions.lib = compilerOptions.lib.map((lib) =>
String(lib).toLowerCase() === 'es2024' ? 'esnext' : lib,
);
}
return tsconfig;
}
function renderParts(parts, options = {}) {
return parts
.map((part) => {
if (part.kind === 'code') {
return options.linkCodeSpans ? linkCodeSpan(part.text) : part.text;
}
return part.text ?? '';
})
.join('');
}
function linkCodeSpan(value) {
const symbol = inlineCodeText(value);
const doc = symbol == null ? null : singleSymbolDoc(symbol);
if (doc == null) {
return value;
}
return `[${code(symbol)}](${docHref(doc)})`;
}
function inlineCodeText(value) {
const text = String(value);
return text.startsWith('`') && text.endsWith('`') && !text.includes('\n')
? text.slice(1, -1)
: null;
}
function targetDoc(target) {
return renderContext.docsIndex.docsById.get(target) ?? null;
}
function singleSymbolDoc(symbol) {
const docs = renderContext.docsIndex.docsBySymbol.get(symbol);
return docs?.length === 1 ? docs[0] : null;
}
function docHref(doc) {
return `${renderContext.docsBasePath}/${doc.page}${
doc.anchor == null ? '' : `#${doc.anchor}`
}`;
}
function heading(level, label) {
return `${'#'.repeat(level)} ${label}`;
}
function isDeprecated(node) {
return hasCommentTag(node?.comment, '@deprecated');
}
function deprecatedTag(node) {
return isDeprecated(node) ? ` ${deprecatedTagMarkup}` : '';
}
function callableDeprecatedTag(node, signatures) {
return isDeprecated(node) ||
(signatures.length === 1 && isDeprecated(signatures[0]))
? ` ${deprecatedTagMarkup}`
: '';
}
function code(value) {
const normalizedText = String(value).replace(/\r?\n|\r/g, ' ');
const longestBacktickRun = Math.max(
0,
...Array.from(normalizedText.matchAll(/`+/g), (match) => match[0].length),
);
const delimiter = '`'.repeat(longestBacktickRun + 1);
const padding =
normalizedText.startsWith('`') || normalizedText.endsWith('`') ? ' ' : '';
return `${delimiter}${padding}${normalizedText}${padding}${delimiter}`;
}
function htmlText(value) {
return String(value).replace(/[<>&]/g, (char) => {
switch (char) {
case '<':
return '<';
case '>':
return '>';
case '&':
return '&';
}
return char;
});
}
function jsxText(value) {
return String(value).replace(/[&{}<>]/g, (char) => {
switch (char) {
case '&':
return '&';
case '{':
return '{';
case '}':
return '}';
case '<':
return '<';
case '>':
return '>';
}
return char;
});
}
function jsxAttribute(value) {
return htmlText(value).replace(/"/g, '"');
}
function mdxText(value) {
return String(value).replace(/[{}<]/g, (char) => {
switch (char) {
case '{':
return '{';
case '}':
return '}';
case '<':
return '<';
}
return char;
});
}
function mdxMarkdown(value) {
return mapInlineCodeSpans(
String(value),
mdxText,
(rawCode, delimiter) => `${delimiter}${rawCode}${delimiter}`,
);
}
function table(rows) {
if (rows.length === 0) {
return [];
}
const headerCells = rows[0]
.map((cell) => ` <th>${jsxText(cell)}</th>`)
.join('\n');
const bodyRows = rows
.slice(1)
.map((row) =>
[
' <tr>',
...row.map((cell) => ` <td>${tableCell(cell)}</td>`),
' </tr>',
].join('\n'),
)
.join('\n');
return [
[
'<table>',
' <thead>',
' <tr>',
headerCells,
' </tr>',
' </thead>',
' <tbody>',
bodyRows,
' </tbody>',
'</table>',
].join('\n'),
];
}
function tableCell(value) {
const cell = String(value);
if (isApiCodeComponentMarkup(cell)) {
return cell;
}
return mapInlineCodeSpans(cell, tableText, tableCode);
}
function isApiCodeComponentMarkup(value) {
return apiCodeComponents.some((component) =>
value.startsWith(`<${component} `),
);
}
function tableText(value) {
return jsxText(value).replace(/\\/g, '\').replace(/\n+/g, '<br />\n');
}
function jsString(value) {
return JSON.stringify(value)
.replace(/\|/g, '\\u007c')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
}
function jsxCode(value) {
return `<code>{${jsString(value)}}</code>`;
}
function tableCode(rawCode) {
const value = normalizeCodeSpan(rawCode);
return jsxCode(value);
}
function normalizeCodeSpan(value) {
const text = value.replace(/\r?\n|\r/g, ' ');
return text.startsWith(' ') &&
text.endsWith(' ') &&
/\S/.test(text.slice(1, -1))
? text.slice(1, -1)
: text;
}
function mapInlineCodeSpans(value, textFn, codeFn) {
let result = '';
let index = 0;
while (index < value.length) {
const start = value.indexOf('`', index);
if (start === -1) {
result += textFn(value.slice(index));
break;
}
const delimiter = /^`+/.exec(value.slice(start))[0];
const end = value.indexOf(delimiter, start + delimiter.length);
if (end === -1) {
result += textFn(value.slice(index));
break;
}
result += textFn(value.slice(index, start));
result += codeFn(value.slice(start + delimiter.length, end), delimiter);
index = end + delimiter.length;
}
return result;
}
function targetId(type) {
return typeof type.target === 'number'
? type.target
: typeof type.target?.id === 'number'
? type.target.id
: null;
}
function rawTypeName(type) {
return typeName(type, { keepDefaultTypeArguments: true });
}
function typeArguments(type, options) {
const args = type.typeArguments ?? [];
const target = targetId(type);
const defaults =
target == null
? null
: renderContext.docsIndex.typeParameterDefaultsById.get(target);
if (options.keepDefaultTypeArguments || defaults == null) {
return args;
}
let end = args.length;
while (end > 0) {
const defaultType = defaults[end - 1];
if (
defaultType == null ||
rawTypeName(args[end - 1]) !== rawTypeName(defaultType)
) {
break;
}
end--;
}
return args.slice(0, end);
}
function referenceName(type) {
return type.name ?? type.qualifiedName;
}
function typeName(type, options = {}) {
if (type == null) {
return 'unknown';
}
switch (type.type) {
case 'array':
return `${arrayElementTypeName(type.elementType, options)}[]`;
case 'conditional':
return `${typeName(type.checkType, options)} extends ${typeName(
type.extendsType,
options,
)} ? ${typeName(type.trueType, options)} : ${typeName(
type.falseType,
options,
)}`;
case 'indexedAccess':
return `${typeName(type.objectType, options)}[${typeName(
type.indexType,
options,
)}]`;
case 'inferred':
case 'intrinsic':
return type.name;
case 'intersection':
return type.types.map((item) => typeName(item, options)).join(' & ');
case 'literal':
return JSON.stringify(type.value);
case 'mapped':
return 'mapped object';
case 'optional':
return `${typeName(type.elementType, options)}?`;
case 'predicate':
return type.asserts
? `asserts ${type.name}`
: `${type.name} is ${typeName(type.targetType, options)}`;
case 'query':
return `typeof ${typeName(type.queryType, options)}`;
case 'reference': {
const expanded = sourceTypeEquivalent(type, options);
if (expanded != null) {
return expanded;
}
const args = typeArguments(type, options);
const typeArgs =
args.length === 0
? ''
: `<${args.map((arg) => typeName(arg, options)).join(', ')}>`;
return `${referenceName(type)}${typeArgs}`;
}
case 'reflection':
return reflectionType(type.declaration, options);
case 'rest':
return `...${typeName(type.elementType, options)}`;
case 'templateLiteral':
return 'template literal';
case 'tuple':
return `[${type.elements
.map((item) => typeName(item, options))
.join(', ')}]`;
case 'typeOperator':
if (type.operator === 'readonly' && type.target?.type === 'array') {
return `readonly ${arrayElementTypeName(
type.target.elementType,
options,
)}[]`;
}
return `${type.operator} ${typeName(type.target, options)}`;
case 'union':
return type.types.map((item) => typeName(item, options)).join(' | ');
case 'unknown':
return 'unknown';
}
return type.name ?? type.type ?? 'unknown';
}
function sourceTypeEquivalent(type, options = {}) {
const qualifiedName = type.target?.qualifiedName ?? type.qualifiedName;
const packagePath = options.sourcePackagePath;
if (qualifiedName == null || packagePath == null) {
return null;
}
const renderedTypeArguments = (type.typeArguments ?? []).map((arg) =>
typeName(arg, options),
);
return sourceTypeName(
packagePath,
qualifiedName,
options,
renderedTypeArguments,
);
}
function sourceTypeName(
packagePath,
qualifiedName,
options = {},
typeArgs = [],
) {
if (options.typeSubstitutions?.has(qualifiedName) && typeArgs.length === 0) {
return options.typeSubstitutions.get(qualifiedName);
}
const key = sourceTypeKey(packagePath, qualifiedName);
const seen = options.seenSourceTypes ?? new Set();
if (seen.has(key)) {
return null;
}
const importedType = sourceContext.metadata.importsByRef.get(key);
if (importedType != null) {
return sourceTypeName(
importedType.packagePath,
importedType.qualifiedName,
{
...options,
seenSourceTypes: new Set([...seen, key]),
},
typeArgs,
);
}
const definition = sourceContext.metadata.typesByRef.get(key);
if (definition == null || definition.isPublic) {
return null;
}
const typeSubstitutions = new Map(options.typeSubstitutions);
for (const [index, parameter] of (
definition.node.typeParameters ?? []
).entries()) {
const typeArgument = typeArgs[index];
if (typeArgument != null) {
typeSubstitutions.set(parameter.name.text, typeArgument);
}
}
const nextOptions = {
...options,
seenSourceTypes: new Set([...seen, key]),
typeSubstitutions,
};
const { node } = definition;
if (ts.isInterfaceDeclaration(node)) {
return interfaceTypeName(node, definition.packagePath, nextOptions);
}
if (ts.isTypeAliasDeclaration(node)) {
return renderSourceTypeNode(node.type, definition.packagePath, nextOptions);
}
return null;
}
function interfaceTypeName(node, packagePath, options) {
const members = node.members
.map((member) => interfaceMemberTypeName(member, packagePath, options))
.filter(Boolean);
return objectTypeName(members);
}
function renderSourceTypeNode(node, packagePath, options) {
return typeNodeName(node, packagePath, options);
}
function objectTypeName(members) {
return members.length === 0 ? 'object' : `{ ${members.join('; ')} }`;
}
function interfaceMemberTypeName(member, packagePath, options) {
if (ts.isIndexSignatureDeclaration(member)) {
const parameter = member.parameters[0];
if (parameter?.type == null || member.type == null) {
return null;
}
const parameterType = renderSourceTypeNode(
parameter.type,
packagePath,
options,
);
const type = renderSourceTypeNode(member.type, packagePath, options);
return indexSignatureText(
hasReadonlyModifier(member),
parameter.name.getText(),
parameterType,
type,
);
}
if (ts.isPropertySignature(member) && member.type != null) {
const name = propertyNameText(member.name);
if (name == null) {
return null;
}
const type = renderSourceTypeNode(member.type, packagePath, options);
return `${name}${member.questionToken == null ? '' : '?'}: ${type}`;
}
return null;
}
function typeLiteralIndexSignature(signature, options = {}) {
const param = signature.parameters?.[0];
if (param == null) {
return null;
}
return indexSignatureText(
hasReadonlyFlag(signature),
param.name,
typeName(param.type, options),
typeName(signature.type, options),
);
}
function indexSignatureText(isReadonly, name, parameterType, valueType) {
const readonlyText = isReadonly ? 'readonly ' : '';
return `${readonlyText}[${name}: ${parameterType}]: ${valueType}`;
}
function hasReadonlyFlag(node) {
return node.flags?.isReadonly === true;
}
function hasReadonlyModifier(node) {
return (
node.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.ReadonlyKeyword,
) === true
);
}
function indexSignatures(node) {
return [
node?.indexSignature,
...(node?.indexSignatures ?? []),
...(node?.children ?? []).filter(
(child) => child.kind === ReflectionKind.IndexSignature,
),
].filter(Boolean);
}
function typeNodeName(node, packagePath, options) {
if (ts.isArrayTypeNode(node)) {
const element = typeNodeName(node.elementType, packagePath, options);
return ts.isUnionTypeNode(node.elementType) ||
ts.isIntersectionTypeNode(node.elementType)
? `(${element})[]`
: `${element}[]`;
}
if (ts.isFunctionTypeNode(node)) {
const params = node.parameters
.map(
(param) =>
`${param.name.getText()}${param.questionToken == null ? '' : '?'}: ${
param.type == null
? 'unknown'
: typeNodeName(param.type, packagePath, options)
}`,
)
.join(', ');
return `(${params}) => ${typeNodeName(node.type, packagePath, options)}`;
}
if (ts.isLiteralTypeNode(node)) {
return node.literal.getText();
}
if (ts.isParenthesizedTypeNode(node)) {
return `(${typeNodeName(node.type, packagePath, options)})`;
}
if (ts.isTypeLiteralNode(node)) {
return interfaceTypeName(node, packagePath, options);
}
if (ts.isTypeOperatorNode(node)) {
return `${
node.operator === ts.SyntaxKind.ReadonlyKeyword
? 'readonly'
: node.operator
} ${typeNodeName(node.type, packagePath, options)}`;
}
if (ts.isTypeReferenceNode(node)) {
const name = node.typeName.getText();
const typeArgs = (node.typeArguments ?? []).map((arg) =>
typeNodeName(arg, packagePath, options),
);
const expanded = sourceTypeName(packagePath, name, options, typeArgs);
const typeArgsText =
typeArgs.length === 0 ? '' : `<${typeArgs.join(', ')}>`;
return expanded ?? `${name}${typeArgsText}`;
}
if (ts.isTupleTypeNode(node)) {
return `[${node.elements
.map((element) => typeNodeName(element, packagePath, options))
.join(', ')}]`;
}
if (ts.isUnionTypeNode(node)) {
return node.types
.map((item) => typeNodeName(item, packagePath, options))
.join(' | ');
}
if (ts.isIntersectionTypeNode(node)) {
return node.types
.map((item) => typeNodeName(item, packagePath, options))
.join(' & ');
}
return node.getText();
}
function signatureText(value) {
return jsxText(value);
}
function signatureToken(value, kind) {
return `<span class="api-signature-${kind}">${jsxText(value)}</span>`;
}
function signatureKeyword(value) {
return signatureToken(value, 'keyword');
}
function signatureName(value) {
return signatureToken(value, 'name');
}
function signatureTypeToken(value) {
return signatureToken(value, 'type');
}
function signatureParameter(value) {
return signatureToken(value, 'parameter');
}
function signatureProperty(value) {
return signatureToken(value, 'property');
}
function signatureLiteralToken(value) {
return signatureToken(value, 'literal');
}
function signatureLink(label, href) {
return `<a class="api-signature-type" href="${jsxAttribute(href)}">${jsxText(
label,
)}</a>`;
}
function signaturePartsExpression(value) {
const parts = parseSignatureParts(String(value));
if (parts.length === 0) {
return '[]';
}
return `[${parts.map(signaturePartExpression).join(', ')}]`;
}
function signaturePartExpression(part) {
if (typeof part === 'string') {
return jsPropString(part);
}
if (part.kind === 'link') {
return `[${jsPropString('link')}, ${jsPropString(
part.value,
)}, ${jsPropString(part.href)}]`;
}
return `[${jsPropString(part.kind)}, ${jsPropString(part.value)}]`;
}
function jsPropString(value) {
return jsString(value).replace(/[<>{}]/g, (char) => {
switch (char) {
case '<':
return '\\u003c';
case '>':
return '\\u003e';
case '{':
return '\\u007b';
case '}':
return '\\u007d';
}
return char;
});
}
function parseSignatureParts(value) {
const parts = [];
let position = 0;
while (position < value.length) {
if (value.startsWith('<span class="api-signature-', position)) {
position = readSignatureSpan(value, position, parts);
continue;
}
if (value.startsWith('<a class="api-signature-type" href="', position)) {
position = readSignatureLink(value, position, parts);
continue;
}
const nextTag = minPositiveIndex(
value.indexOf('<span class="api-signature-', position),
value.indexOf('<a class="api-signature-type" href="', position),
);
const end = nextTag ?? value.length;
pushSignatureTextPart(parts, value.slice(position, end));
position = end;
}
return parts;
}
function minPositiveIndex(...indexes) {
const positiveIndexes = indexes.filter((index) => index >= 0);
return positiveIndexes.length === 0 ? null : Math.min(...positiveIndexes);
}
function readSignatureSpan(value, position, parts) {
const start = /^<span class="api-signature-([a-z]+)">/.exec(
value.slice(position),
);
if (start == null) {
fail(`Cannot parse API signature span: ${value.slice(position)}`);
}
const contentStart = position + start[0].length;
const contentEnd = value.indexOf('</span>', contentStart);
if (contentEnd === -1) {
fail(`Unclosed API signature span: ${value.slice(position)}`);
}
parts.push({
kind: start[1],
value: decodeSignatureHtml(value.slice(contentStart, contentEnd)),
});
return contentEnd + '</span>'.length;
}
function readSignatureLink(value, position, parts) {
const prefix = '<a class="api-signature-type" href="';
const hrefStart = position + prefix.length;
const hrefEnd = value.indexOf('">', hrefStart);
if (hrefEnd === -1) {
fail(`Cannot parse API signature link: ${value.slice(position)}`);
}
const contentStart = hrefEnd + 2;
const contentEnd = value.indexOf('</a>', contentStart);
if (contentEnd === -1) {
fail(`Unclosed API signature link: ${value.slice(position)}`);
}
parts.push({
kind: 'link',
href: decodeSignatureHtml(value.slice(hrefStart, hrefEnd)),
value: decodeSignatureHtml(value.slice(contentStart, contentEnd)),
});
return contentEnd + '</a>'.length;
}
function pushSignatureTextPart(parts, value) {
const textPart = decodeSignatureHtml(value);
if (textPart.length === 0) {
return;
}
const previousPart = parts.at(-1);
if (typeof previousPart === 'string') {
parts[parts.length - 1] = previousPart + textPart;
} else {
parts.push(textPart);
}
}
function decodeSignatureHtml(value) {
return value.replace(
/&(?:#(\d+)|#x([0-9a-fA-F]+)|amp|apos|gt|lt|quot);/g,
(entity, decimal, hexadecimal) => {
if (decimal != null) {
return String.fromCodePoint(Number(decimal));
}
if (hexadecimal != null) {
return String.fromCodePoint(Number.parseInt(hexadecimal, 16));
}
switch (entity) {
case '&':
return '&';
case ''':
return "'";
case '>':
return '>';
case '<':
return '<';
case '"':
return '"';
}
return entity;
},
);
}
function apiSignature(value) {
return `<ApiSignature parts={${signaturePartsExpression(value)}} />`;
}
function apiCode(value) {
return `<ApiType parts={${signaturePartsExpression(
renderSignatureCode(value),
)}} />`;
}
function renderSignatureCode(value) {
return renderScannedSignatureSource(String(value), renderSignatureToken);
}
function renderSignatureToken(token, tokenText) {
if (isKeywordToken(token) || keywordLikeIdentifier(token, tokenText)) {
return signatureKeyword(tokenText);
}
if (isLiteralToken(token)) {
return signatureLiteralToken(tokenText);
}
return signatureText(tokenText);
}
function renderScannedSignatureSource(source, renderToken) {
const scanner = ts.createScanner(
ts.ScriptTarget.Latest,
false,
ts.LanguageVariant.Standard,
source,
);
let result = '';
let position = 0;
let atLineStart = true;
for (
let token = scanner.scan();
token !== ts.SyntaxKind.EndOfFileToken;
token = scanner.scan()
) {
const tokenStart = scanner.getTokenPos();
const tokenEnd = scanner.getTextPos();
const tokenText = scanner.getTokenText();
result += signatureSourceText(source.slice(position, tokenStart));
result +=
renderLeadingWhitespaceToken(token, tokenText, atLineStart) ??
renderToken(token, tokenText, atLineStart);
atLineStart =
token === ts.SyntaxKind.NewLineTrivia ||
(atLineStart && token === ts.SyntaxKind.WhitespaceTrivia);
position = tokenEnd;
}
return result + signatureSourceText(source.slice(position));
}
function renderLeadingWhitespaceToken(token, tokenText, atLineStart) {
if (atLineStart && token === ts.SyntaxKind.WhitespaceTrivia) {
return tokenText.replace(/ /g, ' ').replace(/\t/g, '  ');
}
return null;
}
function createSignatureSourceContext(options = {}) {
return {
...options,
seenSourceTypes: options.seenSourceTypes ?? new Set(),
state: {
nextPlaceholderId: 0,
placeholders: new Map(),
},
typeSubstitutions: options.typeSubstitutions ?? new Map(),
};
}
function withSignatureSourceContext(ctx, overrides) {
return {
...ctx,
...overrides,
state: ctx.state,
};
}
function signaturePlaceholder(ctx, html) {
const placeholder = `__API_SIGNATURE_${ctx.state.nextPlaceholderId++}__`;
ctx.state.placeholders.set(placeholder, html);
return placeholder;
}
function signatureNameSource(ctx, name) {
return signaturePlaceholder(ctx, signatureName(name));
}
function signatureTypeTokenSource(ctx, name) {
return signaturePlaceholder(ctx, signatureTypeToken(name));
}
function signatureParameterSource(ctx, name) {
return signaturePlaceholder(ctx, signatureParameter(name));
}
function signaturePropertySource(ctx, name) {
return signaturePlaceholder(ctx, signatureProperty(name));
}
function signatureTypeLinkSource(ctx, label, target) {
const doc = targetDoc(target);
return signaturePlaceholder(
ctx,
doc == null
? signatureTypeToken(label)
: signatureLink(label, docHref(doc)),
);
}
function signatureSourceTypeLink(ctx, name) {
const doc = singleSymbolDoc(name);
return signaturePlaceholder(
ctx,
doc == null ? signatureTypeToken(name) : signatureLink(name, docHref(doc)),
);
}
function formatSignatureSource(source) {
try {
return prettier.format(source, signaturePrettierOptions).trimEnd();
} catch (error) {
fail(`Cannot format API signature source:\n${source}\n\n${error.message}`);
}
}
function renderFormattedSignatureSource(source, ctx) {
return renderScannedSignatureSource(source, (token, tokenText) =>
renderFormattedSignatureToken(token, tokenText, ctx),
);
}
function renderFormattedSignatureToken(token, tokenText, ctx) {
const placeholder = ctx.state.placeholders.get(tokenText);
if (placeholder != null) {
return placeholder;
}
return renderSignatureToken(token, tokenText);
}
function signatureSourceText(value) {
return signatureText(value).replace(
/(^|\n)( +)/g,
(_, lineStart, spaces) => `${lineStart}${' '.repeat(spaces.length)}`,
);
}
function formatInterfaceMemberSource(memberSource) {
return formatDeclarationBody(
formatSignatureSource(`interface __ApiSignature {\n${memberSource}\n}`),
);
}
function formatClassMemberSource(memberSource) {
return formatDeclarationBody(
formatSignatureSource(`declare class __ApiSignature {\n${memberSource}\n}`),
);
}
function formatDeclarationBody(source) {
const lines = source.split('\n');
return lines.slice(1, -1).join('\n').replace(/^ {2}/gm, '').trim();
}
function formatTypeSource(type, options = {}) {
const ctx = createSignatureSourceContext(options);
const source = signatureTypeSource(type, ctx);
const formatted = formatInterfaceMemberSource(`__api(): ${source};`);
const body = extractReturnTypeBody(formatted);
return renderFormattedSignatureSource(body, ctx);
}
function extractReturnTypeBody(source) {
const prefix = '__api():';
const body = source.slice(prefix.length).replace(/;$/, '').trim();
return body.replace(/\n {2}([|&] )/g, '\n$1');
}
function signatureTypeParametersSource(node, ctx) {
const typeParameters = node.typeParameters ?? [];
if (typeParameters.length === 0) {
return '';
}
return `<${typeParameters
.map((param) => signatureTypeParameterSource(param, ctx))
.join(', ')}>`;
}
function signatureTypeParameterSource(param, ctx) {
const constraint =
param.type == null
? ''
: ` extends ${signatureTypeSource(param.type, ctx)}`;
const defaultType =
param.default == null
? ''
: ` = ${signatureTypeSource(param.default, ctx)}`;
return `${signatureTypeTokenSource(
ctx,
param.name,
)}${constraint}${defaultType}`;
}
function signatureParametersSource(signature, ctx, options = {}) {
return (signature.parameters ?? [])
.map((param) =>
signatureParameterDeclarationSource(param, signature, ctx, options),
)
.join(', ');
}
function signatureParameterDeclarationSource(
param,
signature,
ctx,
options = {},
) {
const defaultValue = options.includeDefault
? rawDefaultValue(param, signature, ctx)
: null;
return `${signatureParameterSource(ctx, param.name)}${
param.flags?.isOptional === true ? '?' : ''
}: ${signatureTypeSource(param.type, ctx)}${
defaultValue == null ? '' : ` = ${defaultValue}`
}`;
}
function signatureFunctionTypeSource(signature, ctx) {
return `(${signatureParametersSource(
signature,
ctx,
)}) => ${signatureTypeSource(signature.type, ctx)}`;
}
function signatureTypeSource(type, ctx) {
if (type == null) {
return 'unknown';
}
switch (type.type) {
case 'array':
return `${signatureArrayElementTypeSource(type.elementType, ctx)}[]`;
case 'conditional':
return `${signatureTypeSource(
type.checkType,
ctx,
)} extends ${signatureTypeSource(
type.extendsType,
ctx,
)} ? ${signatureTypeSource(type.trueType, ctx)} : ${signatureTypeSource(
type.falseType,
ctx,
)}`;
case 'indexedAccess':
return `${signatureTypeSource(
type.objectType,
ctx,
)}[${signatureTypeSource(type.indexType, ctx)}]`;
case 'inferred':
case 'intrinsic':
return type.name;
case 'intersection':
return type.types
.map((item) => signatureTypeSource(item, ctx))
.join(' & ');
case 'literal':
return JSON.stringify(type.value);
case 'mapped':
return signatureTypeTokenSource(ctx, 'mapped object');
case 'optional':
return `${signatureTypeSource(type.elementType, ctx)}?`;
case 'predicate':
return type.asserts
? `asserts ${signatureParameterSource(ctx, type.name)}`
: `${signatureParameterSource(ctx, type.name)} is ${signatureTypeSource(
type.targetType,
ctx,
)}`;
case 'query':
return `typeof ${signatureTypeSource(type.queryType, ctx)}`;
case 'reference': {
const expanded = signatureSourceTypeEquivalent(type, ctx);
if (expanded != null) {
return expanded;
}
const name = referenceName(type);
const target = targetId(type);
const base =
target == null
? signatureTypeTokenSource(ctx, name)
: signatureTypeLinkSource(ctx, name, target);
return `${base}${signatureTypeArgumentsSource(type, ctx)}`;
}
case 'reflection':
return signatureReflectionTypeSource(type.declaration, ctx);
case 'rest':
return `...${signatureTypeSource(type.elementType, ctx)}`;
case 'templateLiteral':
return signatureTypeTokenSource(ctx, 'template literal');
case 'tuple':
return `[${type.elements
.map((item) => signatureTypeSource(item, ctx))
.join(', ')}]`;
case 'typeOperator':
if (type.operator === 'readonly' && type.target?.type === 'array') {
return `readonly ${signatureArrayElementTypeSource(
type.target.elementType,
ctx,
)}[]`;
}
return `${type.operator} ${signatureTypeSource(type.target, ctx)}`;
case 'union':
return type.types
.map((item) => signatureTypeSource(item, ctx))
.join(' | ');
case 'unknown':
return 'unknown';
}
return signatureTypeTokenSource(
ctx,
referenceName(type) ?? type.type ?? 'unknown',
);
}
function signatureArrayElementTypeSource(type, ctx) {
const source = signatureTypeSource(type, ctx);
return type?.type === 'union' ||
type?.type === 'intersection' ||
(type?.type === 'reflection' && type.declaration?.signatures?.length)
? `(${source})`
: source;
}
function signatureTypeArgumentsSource(type, ctx) {
const args = typeArguments(type, ctx);
return args.length === 0
? ''
: `<${args.map((arg) => signatureTypeSource(arg, ctx)).join(', ')}>`;
}
function signatureSourceTypeEquivalent(type, ctx) {
const qualifiedName = type.target?.qualifiedName ?? type.qualifiedName;
const packagePath = ctx.sourcePackagePath;
if (qualifiedName == null || packagePath == null) {
return null;
}
const renderedTypeArguments = (type.typeArguments ?? []).map((arg) =>
signatureTypeSource(arg, ctx),
);
return signatureSourceTypeName(
packagePath,
qualifiedName,
ctx,
renderedTypeArguments,
);
}
function signatureSourceTypeName(
packagePath,
qualifiedName,
ctx,
renderedTypeArguments = [],
) {
if (
ctx.typeSubstitutions?.has(qualifiedName) &&
renderedTypeArguments.length === 0
) {
return ctx.typeSubstitutions.get(qualifiedName);
}
const key = sourceTypeKey(packagePath, qualifiedName);
if (ctx.seenSourceTypes.has(key)) {
return null;
}
const importedType = sourceContext.metadata.importsByRef.get(key);
if (importedType != null) {
return signatureSourceTypeName(
importedType.packagePath,
importedType.qualifiedName,
withSignatureSourceContext(ctx, {
seenSourceTypes: new Set([...ctx.seenSourceTypes, key]),
}),
renderedTypeArguments,
);
}
const definition = sourceContext.metadata.typesByRef.get(key);
if (definition == null || definition.isPublic) {
return null;
}
const typeSubstitutions = new Map(ctx.typeSubstitutions);
for (const [index, parameter] of (
definition.node.typeParameters ?? []
).entries()) {
const typeArgument = renderedTypeArguments[index];
if (typeArgument != null) {
typeSubstitutions.set(parameter.name.text, typeArgument);
}
}
const nextCtx = withSignatureSourceContext(ctx, {
seenSourceTypes: new Set([...ctx.seenSourceTypes, key]),
typeSubstitutions,
});
const { node } = definition;
if (ts.isInterfaceDeclaration(node)) {
return signatureInterfaceTypeSource(node, definition.packagePath, nextCtx);
}
if (ts.isTypeAliasDeclaration(node)) {
return signatureTypeNodeSource(node.type, definition.packagePath, nextCtx);
}
return null;
}
function signatureInterfaceTypeSource(node, packagePath, ctx) {
const members = node.members
.map((member) => signatureInterfaceMemberSource(member, packagePath, ctx))
.filter(Boolean);
return signatureObjectTypeSource(members, ctx);
}
function signatureObjectTypeSource(members, ctx) {
return members.length === 0
? signatureTypeTokenSource(ctx, 'object')
: `{ ${members.join('; ')} }`;
}
function signatureInterfaceMemberSource(member, packagePath, ctx) {
if (ts.isIndexSignatureDeclaration(member)) {
const parameter = member.parameters[0];
if (parameter?.type == null || member.type == null) {
return null;
}
return signatureIndexSignatureSource(
hasReadonlyModifier(member),
parameter.name.getText(),
signatureTypeNodeSource(parameter.type, packagePath, ctx),
signatureTypeNodeSource(member.type, packagePath, ctx),
ctx,
);
}
if (ts.isPropertySignature(member) && member.type != null) {
const name = propertyNameText(member.name);
if (name == null) {
return null;
}
return signatureTypedPropertySource(
name,
member.questionToken != null,
signatureTypeNodeSource(member.type, packagePath, ctx),
ctx,
);
}
return null;
}
function signatureReflectionTypeSource(node, ctx) {
if (node?.signatures?.length) {
return node.signatures
.map((signature) => `(${signatureFunctionTypeSource(signature, ctx)})`)
.join(' | ');
}
const members = typeLiteralMembers(
node,
ctx,
signatureTypeLiteralIndexSignatureSource,
(child) =>
signatureTypedPropertySource(
child.name,
child.flags?.isOptional === true,
signatureTypeSource(child.type, ctx),
ctx,
),
);
return signatureObjectTypeSource(members, ctx);
}
function signatureTypeLiteralIndexSignatureSource(signature, ctx) {
const param = signature.parameters?.[0];
if (param == null) {
return null;
}
return signatureIndexSignatureSource(
hasReadonlyFlag(signature),
param.name,
signatureTypeSource(param.type, ctx),
signatureTypeSource(signature.type, ctx),
ctx,
);
}
function signatureIndexSignatureSource(
isReadonly,
name,
parameterType,
valueType,
ctx,
) {
return `${isReadonly ? 'readonly ' : ''}[${signatureParameterSource(
ctx,
name,
)}: ${parameterType}]: ${valueType}`;
}
function signatureTypedPropertySource(name, optional, type, ctx) {
return `${signaturePropertyNameSource(name, ctx)}${
optional ? '?' : ''
}: ${type}`;
}
function signaturePropertyNameSource(name, ctx) {
return /^[A-Za-z_$][\w$]*$/.test(name)
? signaturePropertySource(ctx, name)
: JSON.stringify(name);
}
function signatureTypeNodeSource(node, packagePath, ctx) {
const keyword = typeNodeKeywordNames.get(node.kind);
if (keyword != null) {
return keyword;
}
if (ts.isArrayTypeNode(node)) {
const element = signatureTypeNodeSource(node.elementType, packagePath, ctx);
return ts.isUnionTypeNode(node.elementType) ||
ts.isIntersectionTypeNode(node.elementType) ||
ts.isFunctionTypeNode(node.elementType)
? `(${element})[]`
: `${element}[]`;
}
if (ts.isConditionalTypeNode(node)) {
return `${signatureTypeNodeSource(
node.checkType,
packagePath,
ctx,
)} extends ${signatureTypeNodeSource(
node.extendsType,
packagePath,
ctx,
)} ? ${signatureTypeNodeSource(
node.trueType,
packagePath,
ctx,
)} : ${signatureTypeNodeSource(node.falseType, packagePath, ctx)}`;
}
if (ts.isFunctionTypeNode(node)) {
return `(${node.parameters
.map((param) => signatureTypeNodeParameterSource(param, packagePath, ctx))
.join(', ')}) => ${signatureTypeNodeSource(node.type, packagePath, ctx)}`;
}
if (ts.isIndexedAccessTypeNode(node)) {
return `${signatureTypeNodeSource(
node.objectType,
packagePath,
ctx,
)}[${signatureTypeNodeSource(node.indexType, packagePath, ctx)}]`;
}
if (ts.isLiteralTypeNode(node)) {
return node.literal.getText();
}
if (ts.isParenthesizedTypeNode(node)) {
return `(${signatureTypeNodeSource(node.type, packagePath, ctx)})`;
}
if (ts.isTypeLiteralNode(node)) {
return signatureInterfaceTypeSource(node, packagePath, ctx);
}
if (ts.isTypeOperatorNode(node)) {
return `${
node.operator === ts.SyntaxKind.ReadonlyKeyword
? 'readonly'
: node.operator
} ${signatureTypeNodeSource(node.type, packagePath, ctx)}`;
}
if (ts.isTypeReferenceNode(node)) {
const name = node.typeName.getText();
const typeArgs = (node.typeArguments ?? []).map((arg) =>
signatureTypeNodeSource(arg, packagePath, ctx),
);
const expanded = signatureSourceTypeName(packagePath, name, ctx, typeArgs);
const typeArgsText =
typeArgs.length === 0 ? '' : `<${typeArgs.join(', ')}>`;
return expanded ?? `${signatureSourceTypeLink(ctx, name)}${typeArgsText}`;
}
if (ts.isTupleTypeNode(node)) {
return `[${node.elements
.map((element) => signatureTypeNodeSource(element, packagePath, ctx))
.join(', ')}]`;
}
if (ts.isUnionTypeNode(node)) {
return node.types
.map((item) => signatureTypeNodeSource(item, packagePath, ctx))
.join(' | ');
}
if (ts.isIntersectionTypeNode(node)) {
return node.types
.map((item) => signatureTypeNodeSource(item, packagePath, ctx))
.join(' & ');
}
return node.getText();
}
function signatureTypeNodeParameterSource(param, packagePath, ctx) {
return `${signatureParameterSource(ctx, param.name.getText())}${
param.questionToken == null ? '' : '?'
}: ${
param.type == null
? 'unknown'
: signatureTypeNodeSource(param.type, packagePath, ctx)
}`;
}
function isKeywordToken(token) {
return (
token >= ts.SyntaxKind.FirstKeyword && token <= ts.SyntaxKind.LastKeyword
);
}
function keywordLikeIdentifier(token, tokenText) {
return (
token === ts.SyntaxKind.Identifier && keywordLikeIdentifiers.has(tokenText)
);
}
function isLiteralToken(token) {
return literalTokenKinds.has(token);
}
function arrayElementTypeName(type, options = {}) {
const name = typeName(type, options);
return type?.type === 'union' || type?.type === 'intersection'
? `(${name})`
: name;
}
function reflectionType(node, options = {}) {
if (node?.signatures?.length) {
return node.signatures
.map((signature) => signatureType(signature, options))
.join(' | ');
}
const members = typeLiteralMembers(
node,
options,
typeLiteralIndexSignature,
(child) =>
`${child.name}${child.flags?.isOptional ? '?' : ''}: ${typeName(
child.type,
options,
)}`,
);
return objectTypeName(members);
}
function signatureType(signature, options = {}) {
const params = (signature.parameters ?? [])
.map(
(param) =>
`${param.name}${param.flags?.isOptional ? '?' : ''}: ${typeName(
param.type,
options,
)}`,
)
.join(', ');
return `(${params}): ${typeName(signature.type, options)}`;
}
function typeLiteralMembers(node, options, renderIndexSignature, renderChild) {
return [
...indexSignatures(node)
.map((signature) => renderIndexSignature(signature, options))
.filter(Boolean),
...visibleChildren(node).map(renderChild),
];
}
function renderApiType(type, options = {}) {
return `<ApiType parts={${signaturePartsExpression(
formatTypeSource(type, options),
)}} />`;
}
function renderSignatureDeclaration(
signature,
options = {},
name = signature.name,
) {
const ctx = createSignatureSourceContext(options);
const source = `${signatureNameSource(
ctx,
name,
)}${signatureTypeParametersSource(
signature,
ctx,
)}(${signatureParametersSource(signature, ctx, {
includeDefault: true,
})}): ${signatureTypeSource(signature.type, ctx)};`;
return apiSignature(
renderFormattedSignatureSource(formatInterfaceMemberSource(source), ctx),
);
}
function renderConstructorDeclaration(signature, options = {}) {
const ctx = createSignatureSourceContext(options);
const source = `constructor(${signatureParametersSource(signature, ctx, {
includeDefault: true,
})});`;
const constructorSource = signaturePlaceholder(
ctx,
`${signatureKeyword('new')} ${signatureName(signature.name)}`,
);
return apiSignature(
renderFormattedSignatureSource(
formatClassMemberSource(source).replace(
/^constructor/,
constructorSource,
),
ctx,
),
);
}
function renderTypeAliasDeclaration(node, options = {}) {
const ctx = createSignatureSourceContext(options);
const source = `type ${signatureNameSource(
ctx,
node.name,
)}${signatureTypeParametersSource(node, ctx)} = ${signatureTypeSource(
node.type,
ctx,
)};`;
return apiSignature(
renderFormattedSignatureSource(formatSignatureSource(source), ctx),
);
}
function declarationKind(node, siblings = []) {
if (isEnumLikeDeclaration(node, siblings)) {
return 'Enumerations';
}
if (node.kind === ReflectionKind.Class) {
return 'Classes';
}
if (node.kind === ReflectionKind.Function) {
return 'Functions';
}
if (node.kind === ReflectionKind.Variable) {
return 'Constants';
}
if (node.kind === ReflectionKind.Enum) {
return 'Enumerations';
}
if (
node.kind === ReflectionKind.TypeAlias ||
node.kind === ReflectionKind.Interface ||
(node.kind === ReflectionKind.Reference && node.variant === 'declaration')
) {
return 'Types';
}
return null;
}
function isEnumLikeDeclaration(node, siblings = []) {
return isEnumNamespace(node) || isEnumLikeConstObject(node, siblings);
}
function isEnumLikeConstObject(node, siblings = []) {
return (
node.kind === ReflectionKind.Variable &&
hasMatchingTypeAlias(node, siblings) &&
enumLikeMembers(node).length > 0
);
}
function isEnumLikeTypeAlias(node, siblings = []) {
return (
node.kind === ReflectionKind.TypeAlias &&
siblings.some(
(sibling) =>
sibling !== node &&
sibling.name === node.name &&
isEnumLikeConstObject(sibling, siblings),
)
);
}
function hasMatchingTypeAlias(node, siblings = []) {
return siblings.some(
(sibling) =>
sibling !== node &&
sibling.name === node.name &&
sibling.kind === ReflectionKind.TypeAlias,
);
}
function isEnumNamespace(node) {
if (node.kind !== ReflectionKind.Namespace) {
return false;
}
const children = visibleChildren(node);
const valueNames = new Set(
children
.filter((child) => child.kind === ReflectionKind.Variable)
.map((child) => child.name),
);
return children.some(
(child) =>
child.kind === ReflectionKind.TypeAlias && valueNames.has(child.name),
);
}
function enumLikeMembers(node) {
if (node.kind === ReflectionKind.Namespace) {
return visibleChildren(node).filter(
(child) => child.kind === ReflectionKind.Variable,
);
}
return visibleChildren(node.type?.declaration);
}
function visibleChildren(node) {
if (node == null) {
return [];
}
let children = visibleChildrenCache.get(node);
if (children == null) {
children = (node.children ?? []).filter(isVisibleChild);
visibleChildrenCache.set(node, children);
}
return children;
}
function isVisibleChild(child) {
return (
child.variant !== 'reference' &&
child.kind !== ReflectionKind.IndexSignature &&
!child.flags?.isExternal &&
!child.flags?.isInherited &&
!child.flags?.isPrivate &&
!hasReflectionTag(child, '@internal') &&
!hasReflectionTag(child, '@private')
);
}
function renderComment(node) {
const parts = [];
const summaryText = summary(node);
if (summaryText) {
parts.push(mdxMarkdown(summaryText));
}
const remarks = tagText(node.comment, '@remarks', {
linkCodeSpans: true,
});
if (remarks) {
parts.push(`**Remarks:** ${mdxMarkdown(remarks)}`);
}
return parts.join('\n\n');
}
function renderFields(parent, level, options = {}) {
const children = visibleChildren(parent).filter(
(child) =>
child.kind === ReflectionKind.Property ||
child.kind === ReflectionKind.Method,
);
if (children.length === 0) {
return [];
}
const lines = [];
const rows = [];
for (const child of children) {
if (child.kind === ReflectionKind.Method) {
if (rows.length > 0 || lines.length > 0) {
lines.push('<hr className="api-subsection-divider" />');
}
lines.push(...renderCallable(child, level, child.name));
continue;
}
const defaultValue = defaultText(child, parent, options);
rows.push([
`${htmlText(child.name)}${child.flags?.isOptional ? '?' : ''}`,
renderApiType(child.type, options),
defaultValue,
summary(child),
]);
lines.push(...renderExamples(child.comment, `${child.name} Example`));
}
const members = tableWithOptionalDefault(rows);
if (rows.length === 0) {
return lines;
}
return options.heading
? [...headingSubsection('Members', level, members), ...lines]
: [...subsection('Members', members), ...lines];
}
function renderParams(signature, options = {}) {
const params = signature.parameters ?? [];
if (params.length === 0) {
return [];
}
const rows = [];
for (const param of signature.parameters ?? []) {
const defaultValue = defaultText(param, signature, options);
rows.push([
`${htmlText(param.name)}${param.flags?.isOptional ? '?' : ''}`,
renderApiType(param.type, options),
defaultValue,
summary(param),
]);
}
return subsection('Arguments', tableWithOptionalDefault(rows));
}
function tableWithOptionalDefault(rows) {
const hasDefault = rows.some(([, , defaultValue]) => defaultValue !== '');
const headers = hasDefault
? ['Name', 'Type', 'Default', 'Description']
: ['Name', 'Type', 'Description'];
const visibleRows = hasDefault
? rows
: rows.map(([name, type, , description]) => [name, type, description]);
return table([headers, ...visibleRows]);
}
function tableWithOptionalDescription(headers, rows) {
const descriptionIndex = headers.indexOf('Description');
if (
descriptionIndex === -1 ||
rows.some((row) => row[descriptionIndex] !== '')
) {
return table([headers, ...rows]);
}
return table([
headers.filter((_, index) => index !== descriptionIndex),
...rows.map((row) => row.filter((_, index) => index !== descriptionIndex)),
]);
}
function renderExamples(comment, title = 'Example') {
const examples = (comment?.blockTags ?? []).filter(
(block) => block.tag === '@example',
);
if (examples.length === 0) {
return [];
}
return examples.flatMap((example, index) =>
subsection(examples.length === 1 ? title : `${title} ${index + 1}`, [
renderParts(example.content).trim(),
]),
);
}
function renderReturns(signature, options = {}) {
if (signature.type == null || typeName(signature.type, options) === 'void') {
return [];
}
const returns = tagText(signature.comment, '@returns', {
linkCodeSpans: true,
});
if (!returns) {
return [];
}
return subsection('Returns', [
...table([
['Type', 'Description'],
[renderApiType(signature.type, options), returns],
]),
]);
}
function renderTypeParameters(node, options = {}) {
const typeParameters = node.typeParameters ?? [];
if (typeParameters.length === 0) {
return [];
}
const rows = typeParameters.map((param) => [
param.name,
param.type == null ? '' : renderApiType(param.type, options),
param.default == null ? '' : renderApiType(param.default, options),
summary(param),
]);
return subsection('Type Parameters', [
...table([['Name', 'Constraint', 'Default', 'Description'], ...rows]),
]);
}
function publishedExtendedTypes(node) {
if (node.kind !== ReflectionKind.Interface) {
return [];
}
return (node.extendedTypes ?? []).filter((type) => {
const target = targetId(type);
return target != null && targetDoc(target) != null;
});
}
function renderInterfaceDeclaration(node, options = {}) {
const extendedTypes = publishedExtendedTypes(node);
if (extendedTypes.length === 0) {
return '';
}
const ctx = createSignatureSourceContext(options);
const source = `interface ${signatureNameSource(
ctx,
node.name,
)}${signatureTypeParametersSource(node, ctx)} extends ${extendedTypes
.map((type) => signatureTypeSource(type, ctx))
.join(', ')} {}`;
return apiSignature(
renderFormattedSignatureSource(
formatSignatureSource(source).replace(/\s*\{\}$/, ''),
ctx,
),
);
}
function renderCallable(
node,
level,
label = `${node.name}()`,
options = sourceOptions(node),
) {
const signatures = node.signatures ?? [node];
const headingLabel = label.endsWith(')') ? label : `${label}()`;
const lines = [
heading(level, `${headingLabel}${callableDeprecatedTag(node, signatures)}`),
];
for (const [index, signature] of signatures.entries()) {
const overloadLabel =
signatures.length > 1 ? `Overload ${index + 1}` : null;
if (overloadLabel) {
lines.push(
...subsection(`${overloadLabel}${deprecatedTag(signature)}`, []),
);
}
const comment = renderComment(signature);
if (comment) {
lines.push(comment);
}
lines.push(...renderTypeParameters(signature, options));
lines.push(
'**Signature:**',
renderSignatureDeclaration(signature, options),
);
lines.push(...renderParams(signature, options));
lines.push(...renderReturns(signature, options));
lines.push(...renderExamples(signature.comment));
}
return lines;
}
function renderDeclaration(node, level = 3, siblings = []) {
const lines = [];
const options = sourceOptions(node);
const title =
node.kind === ReflectionKind.Function ? `${node.name}()` : node.name;
if (node.kind === ReflectionKind.Function) {
return renderCallable(node, level, title, options);
}
lines.push(heading(level, `${title}${deprecatedTag(node)}`));
const comment = renderComment(node);
const label = typeLabel(node, siblings);
if (label != null) {
lines.push(comment ? `**${label}.** ${comment}` : `**${label}.**`);
} else if (comment) {
lines.push(comment);
}
lines.push(...renderTypeParameters(node, options));
const examples = renderExamples(node.comment);
if (isEnumLikeDeclaration(node, siblings)) {
lines.push(enumLikeNote(node));
lines.push(...examples);
lines.push(...renderEnumMembers(node, options));
return lines;
}
const interfaceDeclaration = renderInterfaceDeclaration(node, options);
if (interfaceDeclaration) {
lines.push(interfaceDeclaration);
}
if (node.kind === ReflectionKind.Variable) {
lines.push(...subsection('Type', [renderApiType(node.type, options)]));
lines.push(...examples);
return lines;
}
if (
(node.kind === ReflectionKind.TypeAlias ||
node.kind === ReflectionKind.Reference) &&
node.type != null
) {
lines.push(renderTypeAliasDeclaration(node, options));
}
if (node.kind === ReflectionKind.Enum) {
lines.push(...examples);
const rows = visibleChildren(node).map((child) => [
code(child.name),
code(typeName(child.type, options)),
summary(child),
]);
lines.push(
...subsection('Members', [
...tableWithOptionalDescription(['Name', 'Value', 'Description'], rows),
]),
);
return lines;
}
if (node.kind === ReflectionKind.Class) {
lines.push(...examples);
const constructors = visibleChildren(node).filter(
(child) => child.kind === ReflectionKind.Constructor,
);
for (const constructor of constructors) {
for (const signature of constructor.signatures ?? []) {
lines.push(
...headingSubsection(
`Constructor${deprecatedTag(signature)}`,
level + 1,
),
);
const signatureComment = renderComment(signature);
if (signatureComment) {
lines.push(signatureComment);
}
lines.push(
'**Signature:**',
renderConstructorDeclaration(signature, options),
);
lines.push(...renderParams(signature, options));
}
}
} else {
lines.push(...examples);
}
lines.push(
...renderFields(node, level + 1, {
...options,
heading: node.kind === ReflectionKind.Class,
}),
);
return lines;
}
function enumLikeNote(node) {
const runtimeShape =
node.kind === ReflectionKind.Namespace
? 'namespace object'
: 'const object';
return `> This is not a TypeScript ${code('enum')}. GraphQL.js exports ${code(
node.name,
)} as both a runtime ${runtimeShape} of literal values and a TypeScript type alias for those values.`;
}
function renderEnumMembers(node, options = {}) {
const rows = enumLikeMembers(node).map((child) => [
code(child.name),
code(typeName(child.type, options)),
summary(child),
]);
if (rows.length === 0) {
return [];
}
return subsection('Members', [
...tableWithOptionalDescription(['Name', 'Value', 'Description'], rows),
]);
}
function sourceOptions(node) {
const sourcePackagePath = sourceFileName(node);
return sourcePackagePath == null ? {} : { sourcePackagePath };
}
function sourceFileName(node) {
const fileName = node?.sources?.[0]?.fileName;
if (fileName == null) {
return null;
}
if (fileName.startsWith('src/')) {
return fileName;
}
const srcSegment = '/src/';
const srcIndex = fileName.lastIndexOf(srcSegment);
return srcIndex === -1
? `src/${fileName.replace(/^\.\//, '')}`
: fileName.slice(srcIndex + 1);
}
function typeLabel(node, siblings = []) {
if (
node.kind === ReflectionKind.Enum ||
isEnumLikeDeclaration(node, siblings)
) {
return 'Enumeration';
}
if (node.kind === ReflectionKind.Interface) {
return 'Interface';
}
if (
node.kind === ReflectionKind.TypeAlias ||
(node.kind === ReflectionKind.Reference && node.variant === 'declaration')
) {
return 'Type alias';
}
return null;
}
function apiModuleName(module) {
return module.name === 'index' ? 'graphql' : module.name;
}
function moduleTitle(name) {
return name === 'graphql' ? 'graphql' : `graphql/${name}`;
}
function moduleItems(module, name) {
return visibleChildren(module).filter(
(item) =>
name !== 'graphql' || sourceContext.rootExportNames.has(item.name),
);
}
function categorizedItems(items) {
const categories = [];
const byCategory = new Map();
const categoryByItem = new Map();
const leftovers = [];
for (const item of items) {
if (isEnumLikeTypeAlias(item, items)) {
continue;
}
const itemCategory = resolveItemCategory(item, items);
categoryByItem.set(item, itemCategory);
if (itemCategory != null && !byCategory.has(itemCategory)) {
categories.push(itemCategory);
byCategory.set(itemCategory, []);
}
}
for (const item of items) {
const itemCategory = categoryByItem.has(item)
? categoryByItem.get(item)
: resolveItemCategory(item, items);
const categoryItems = byCategory.get(itemCategory);
if (categoryItems == null) {
leftovers.push(item);
continue;
}
categoryItems.push(item);
}
return { categories, byCategory, leftovers };
}
function moduleDocs(module) {
const name = apiModuleName(module);
const items = moduleItems(module, name);
const { categories, byCategory, leftovers } = categorizedItems(items);
return {
module,
name,
title: moduleTitle(name),
items,
categories,
byCategory,
leftovers,
};
}
function createDocsIndex(modules) {
const index = emptyDocsIndex();
for (const docs of modules) {
addSymbolDoc(index, docs.name, { page: docs.name });
for (const child of docs.items) {
const childDoc = { page: docs.name, anchor: slug(child.name) };
index.docsById.set(child.id, childDoc);
addSymbolDoc(index, child.name, childDoc);
index.typeParameterDefaultsById.set(
child.id,
(child.typeParameters ?? []).map((param) => param.default ?? null),
);
for (const member of visibleChildren(child)) {
const memberDoc = { page: docs.name, anchor: slug(member.name) };
index.docsById.set(member.id, memberDoc);
addSymbolDoc(index, `${child.name}.${member.name}`, memberDoc);
}
for (const signature of child.signatures ?? []) {
index.docsById.set(signature.id, childDoc);
}
}
}
return index;
}
function addSymbolDoc(index, symbol, doc) {
const docs = index.docsBySymbol.get(symbol);
if (docs == null) {
index.docsBySymbol.set(symbol, [doc]);
return;
}
if (!docs.some((existing) => sameDoc(existing, doc))) {
docs.push(doc);
}
}
function sameDoc(left, right) {
return left.page === right.page && left.anchor === right.anchor;
}
function renderGroup(title, items, level, allItems) {
if (items.length === 0) {
return [];
}
const lines = [heading(level, title)];
for (const [index, item] of items.entries()) {
if (index > 0) {
lines.push('<hr className="api-item-divider" />');
}
lines.push(...renderDeclaration(item, level + 1, allItems));
}
return lines;
}
function grouped(items) {
const map = new Map(groupOrder.map((name) => [name, []]));
for (const item of items) {
if (isEnumLikeTypeAlias(item, items)) {
continue;
}
const kind = declarationKind(item, items);
if (kind != null) {
map.get(kind).push(item);
}
}
return map;
}
function renderItems(items, page, level = 2) {
const groups = grouped(items);
const lines = [renderItemToc(groups, page)];
for (const group of groupOrder) {
lines.push(...renderGroup(group, groups.get(group), level, items));
}
return lines.filter(Boolean).join('\n\n').trimEnd() + '\n';
}
function renderItemToc(groups, page) {
const lines = [];
for (const group of groupOrder) {
const groupItems = groups.get(group);
if (groupItems.length === 0) {
continue;
}
const tocItems = groupItems
.map((item) => tocLink(item, page))
.join('\n <span aria-hidden="true">·</span>\n ');
lines.push(
` <p>\n <strong>${group}:</strong><br />\n ${tocItems}\n </p>`,
);
}
return lines.length === 0
? ''
: `<div className="api-category-toc">\n${lines.join('\n')}\n</div>`;
}
function tocLink(item, page) {
const label =
item.kind === ReflectionKind.Function ? `${item.name}()` : item.name;
return `<a href="${jsxAttribute(
docHref({ page, anchor: slug(item.name) }),
)}">${jsxText(label)}</a>`;
}
function subsection(title, lines) {
return [
'<hr className="api-subsection-divider" />',
`<div className="api-subsection-title">${title}</div>`,
...lines,
];
}
function headingSubsection(title, level, lines = []) {
return [
'<hr className="api-subsection-divider" />',
heading(level, title),
...lines,
];
}
function addApiCodeImport(page, content) {
const imports = apiCodeComponents.filter((component) =>
content.includes(`<${component}`),
);
if (imports.length === 0) {
return content;
}
const importPath = page.includes('/')
? '../../../components/ApiCode'
: '../../components/ApiCode';
return `import { ${imports.join(', ')} } from '${importPath}';\n\n${content}`;
}
function writePage(page, content) {
const path = join(generation.outputDir, `${page}.mdx`);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, stripTrailingWhitespace(addApiCodeImport(page, content)));
}
function stripTrailingWhitespace(value) {
return value.replace(/[ \t]+$/gm, '');
}
function categoryLinks(visibleCategories, moduleName) {
const links = visibleCategories
.map((name) => `- [${name}](${categoryHref(moduleName, name)})`)
.join('\n');
return [
'For documentation purposes, these exports are grouped into the following categories:',
links,
].join('\n\n');
}
function categoryHref(moduleName, categoryName) {
return `${renderContext.docsBasePath}/${moduleName}#${slug(
categoryHeading(categoryName),
)}`;
}
function categoryHeading(categoryName) {
return `Category: ${categoryName}`;
}
function categorySection(name, items, moduleName) {
return [
heading(2, categoryHeading(name)),
renderItems(items, moduleName, 3).trimEnd(),
].join('\n\n');
}
function renderModulePage(docs) {
const content = [summary(docs.module)];
if (docs.categories.length === 1) {
content.push(
renderItems(docs.byCategory.get(docs.categories[0]), docs.name).trimEnd(),
);
} else {
content.push(categoryLinks(docs.categories, docs.name));
content.push(
...docs.categories.map((name) =>
categorySection(name, docs.byCategory.get(name), docs.name),
),
);
}
return content.filter(Boolean).join('\n\n') + '\n';
}
function addModuleMeta(meta, docs) {
const entry = [docs.name, docs.title];
if (docs.name === 'graphql') {
meta.unshift(entry);
} else {
meta.push(entry);
}
}
function assertAllItemsCategorized(docs) {
if (docs.leftovers.length === 0) {
return;
}
fail(
`Missing @category in ${docs.title}: ` +
docs.leftovers.map((item) => item.name).join(', '),
);
}
function writeCategoryMeta(docs) {
if (docs.categories.length <= 1) {
return;
}
const dir = join(generation.outputDir, docs.name);
mkdirSync(dir, { recursive: true });
writeMeta(
dir,
docs.categories.map((name) => [
slug(name),
{ title: categoryHeading(name), href: categoryHref(docs.name, name) },
]),
);
}
function buildApiReference(doc) {
const modules = (doc.children ?? []).map(moduleDocs);
return {
index: createDocsIndex(modules),
modules,
};
}
function writeApiReference(reference) {
renderContext.docsBasePath = generation.docsBasePath;
renderContext.docsIndex = reference.index;
rmSync(generation.outputDir, { recursive: true, force: true });
mkdirSync(generation.outputDir, { recursive: true });
const meta = [];
for (const docs of reference.modules) {
addModuleMeta(meta, docs);
assertAllItemsCategorized(docs);
writePage(docs.name, renderModulePage(docs));
writeCategoryMeta(docs);
}
writeMeta(generation.outputDir, meta);
}
function renderDocs(doc) {
writeApiReference(buildApiReference(doc));
}
function addCategory(comment, category) {
if (/@category\b/.test(comment)) {
return comment;
}
const trailing = comment.match(/\s*$/)?.[0] ?? '';
const body = comment.slice(0, comment.length - trailing.length);
const oneLine = /^(\s*)\/\*\*\s*(.*?)\s*\*\/$/.exec(body);
if (oneLine != null) {
const [, indent, text] = oneLine;
return `${indent}/**\n${indent} * ${text}\n${indent} *\n${indent} * @category ${category}\n${indent} */${trailing}`;
}
return (
body.replace(/\n\s*\*\/$/, `\n *\n * @category ${category}\n */`) + trailing
);
}
function isLeadingLineCommentTrivia(value) {
return value.replace(/\/\/[^\n\r]*(?:\r?\n|$)/g, '').trim() === '';
}
function isExported(node) {
return node.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
);
}
function localExportNames(ast) {
const names = new Set();
for (const statement of ast.statements) {
if (
ts.isExportDeclaration(statement) &&
statement.moduleSpecifier == null &&
statement.exportClause != null &&
ts.isNamedExports(statement.exportClause)
) {
for (const element of statement.exportClause.elements) {
names.add((element.propertyName ?? element.name).text);
}
}
}
return names;
}
function declarationNames(statement) {
if (ts.isVariableStatement(statement)) {
return statement.declarationList.declarations
.map((declaration) =>
ts.isIdentifier(declaration.name) ? declaration.name.text : null,
)
.filter(Boolean);
}
return statement.name?.text == null ? [] : [statement.name.text];
}
function exportedDeclarations(ast) {
const localExports = localExportNames(ast);
return ast.statements.filter(
(statement) =>
(ts.isClassDeclaration(statement) ||
ts.isEnumDeclaration(statement) ||
ts.isFunctionDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
ts.isVariableStatement(statement)) &&
(isExported(statement) ||
declarationNames(statement).some((name) => localExports.has(name))),
);
}
function inheritFileCategories(dir) {
walkFiles(dir, (path) => {
if (!path.endsWith('.ts')) {
return;
}
let content = readFileSync(path, 'utf8');
const category = content
.match(/^\/\*\*([\s\S]*?)\*\//)?.[1]
.match(/@category\s+([^\n*]+)/)?.[1]
.trim();
if (category == null) {
return;
}
const declarations = exportedDeclarations(sourceFile(path, content));
for (let i = declarations.length - 1; i >= 0; i--) {
const index = declarations[i].getStart();
const before = content.slice(0, index);
const start = before.lastIndexOf('/**');
const end = start === -1 ? -1 : before.indexOf('*/', start);
const jsdocEnd = end === -1 ? -1 : end + 2;
if (
start === -1 ||
jsdocEnd < start ||
!isLeadingLineCommentTrivia(before.slice(jsdocEnd))
) {
content =
content.slice(0, index) +
`/**\n * @category ${category}\n */\n` +
content.slice(index);
} else {
content =
content.slice(0, start) +
addCategory(before.slice(start, jsdocEnd), category) +
before.slice(jsdocEnd) +
content.slice(index);
}
}
writeFileSync(path, content);
});
}
function prepareSourceSnapshot() {
copySourceSnapshot(generation.sourceDir, generation.tmpSourceDir);
writeSnapshotTsConfig(generation.sourceDir, generation.tmpSourceDir);
copyOptionalTsdoc(generation.sourceDir, generation.tmpSourceDir);
writeTypedocOptions();
}
function copySourceSnapshot(sourceDir, tmpSourceDir) {
mkdirSync(tmpSourceDir, { recursive: true });
cpSync(join(sourceDir, 'src'), join(tmpSourceDir, 'src'), {
recursive: true,
});
stripCoverageIgnoreComments(join(tmpSourceDir, 'src'));
inheritFileCategories(join(tmpSourceDir, 'src'));
}
function stripCoverageIgnoreComments(dir) {
walkFiles(dir, (path) => {
if (!path.endsWith('.ts')) {
return;
}
const content = readFileSync(path, 'utf8');
const nextContent = content.replace(
/^[ \t]*\/\*\s*c8 ignore [^*]*\*\/\r?\n/gm,
'',
);
if (nextContent !== content) {
writeFileSync(path, nextContent);
}
});
}
function writeSnapshotTsConfig(sourceDir, tmpSourceDir) {
const tsconfig = sanitizeTsConfig(
readTsConfig(join(sourceDir, 'tsconfig.json')),
);
writeJson(join(tmpSourceDir, 'tsconfig.json'), tsconfig);
}
function copyOptionalTsdoc(sourceDir, tmpSourceDir) {
const tsdocPath = join(sourceDir, 'tsdoc.json');
if (existsSync(tsdocPath)) {
cpSync(tsdocPath, join(tmpSourceDir, 'tsdoc.json'));
}
}
function writeTypedocOptions() {
const typedocOptions = readJson(typedocTemplatePath);
typedocOptions.name = `GraphQL.js v${generation.docsVersionLabel.slice(
5,
)} API`;
typedocOptions.entryPoints = typedocEntryPoints(generation.tmpSourceDir);
typedocOptions.json = generation.jsonPath;
typedocOptions.tsconfig = join(generation.tmpSourceDir, 'tsconfig.json');
typedocOptions.disableSources = false;
writeJson(generation.typedocOptionsPath, typedocOptions);
}
function typedocEntryPoints(sourceRootDir) {
return [
'src/error/index.ts',
'src/execution/index.ts',
'src/language/index.ts',
'src/subscription/index.ts',
'src/type/index.ts',
'src/utilities/index.ts',
'src/validation/index.ts',
'src/index.ts',
]
.map((path) => join(sourceRootDir, path))
.filter((path) => existsSync(path));
}
function rememberGeneratedMajor(generatedMajors, majorVersion) {
if (generatedMajors.has(majorVersion)) {
fail(
`Multiple refs resolve to v${majorVersion}; refusing to overwrite docs.`,
);
}
generatedMajors.add(majorVersion);
}
function runTypedoc(ref) {
console.log(
`[${generation.docsVersionLabel}] Copied source snapshot from:`,
ref,
);
run(
'npm',
['exec', '--', 'typedoc', '--options', generation.typedocOptionsPath],
websiteDir,
);
}
function readTypedocOutput() {
if (!existsSync(generation.jsonPath)) {
fail('TypeDoc did not emit JSON docs.');
}
return readJson(generation.jsonPath);
}
function generateForRef(ref, index, generatedMajors) {
const sourceCheckoutDir = checkoutSourceRef(ref, index);
const majorVersion = configureGeneration(ref, sourceCheckoutDir);
rememberGeneratedMajor(generatedMajors, majorVersion);
prepareSourceSnapshot();
sourceContext = analyzeSourceSnapshot(generation.tmpSourceDir);
runTypedoc(ref);
renderDocs(readTypedocOutput());
}
function generateRefs(refs) {
if (refs.length === 0) {
fail('Usage: npm run generate:docs <branch-or-ref> [...branch-or-ref]');
}
const generatedMajors = new Set();
for (const [index, ref] of refs.entries()) {
generateForRef(ref, index, generatedMajors);
}
}
try {
generateRefs(process.argv.slice(2));
} catch (error) {
console.error(error.message);
process.exitCode = 1;
} finally {
removeSourceWorktrees();
if (process.env.GRAPHQL_JS_API_KEEP_TMP === '1') {
console.error('[api-docs] Kept temporary directory:', tmpDir);
} else {
rmSync(tmpDir, { recursive: true, force: true });
}
}