From 492f86af393cd11b534542fd76b3cddd8b7fe3ac Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 17 Nov 2023 23:26:48 +0100 Subject: [PATCH] refactor: minify api.json by shortening keys (#9971) * refactor: minify api.json by shortening keys * fix: links to other packages * refactor: get doclink from canonicalReference, not model * fix: types * fix: again * fix: @link tags with alt texts --- apps/website/src/components/ExcerptText.tsx | 22 +-- .../website/src/components/ParameterTable.tsx | 2 +- apps/website/src/components/Property.tsx | 4 +- apps/website/src/components/SignatureText.tsx | 6 +- .../website/src/components/TypeParamTable.tsx | 7 +- .../documentation/HierarchyText.tsx | 4 +- .../src/components/documentation/util.ts | 71 ++++++++- .../src/components/model/enum/EnumMember.tsx | 4 +- .../components/model/method/MethodHeader.tsx | 2 +- apps/website/src/util/addPackageToModel.ts | 23 +-- .../src/model/ApiPackage.ts | 140 +++++++++++++++++- .../src/model/DeserializerContext.ts | 9 +- .../src/generators/ApiModelGenerator.ts | 6 +- 13 files changed, 239 insertions(+), 61 deletions(-) diff --git a/apps/website/src/components/ExcerptText.tsx b/apps/website/src/components/ExcerptText.tsx index 68ee9c875..bcf552563 100644 --- a/apps/website/src/components/ExcerptText.tsx +++ b/apps/website/src/components/ExcerptText.tsx @@ -1,26 +1,22 @@ -import type { ApiModel, Excerpt } from '@discordjs/api-extractor-model'; +import type { Excerpt } from '@discordjs/api-extractor-model'; import { ExcerptTokenKind } from '@discordjs/api-extractor-model'; import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks'; import { DISCORD_API_TYPES_DOCS_URL } from '~/util/constants'; import { DocumentationLink } from './DocumentationLink'; import { ItemLink } from './ItemLink'; -import { resolveItemURI } from './documentation/util'; +import { resolveCanonicalReference, resolveItemURI } from './documentation/util'; export interface ExcerptTextProps { /** * The tokens to render. */ readonly excerpt: Excerpt; - /** - * The model to resolve item references from. - */ - readonly model: ApiModel; } /** * A component that renders excerpt tokens from an api item. */ -export function ExcerptText({ model, excerpt }: ExcerptTextProps) { +export function ExcerptText({ excerpt }: ExcerptTextProps) { return ( {excerpt.spannedTokens.map((token, idx) => { @@ -53,20 +49,18 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) { ); } - const item = token.canonicalReference - ? model.resolveDeclarationReference(token.canonicalReference!, model).resolvedApiItem - : null; + const resolved = token.canonicalReference ? resolveCanonicalReference(token.canonicalReference) : null; - if (!item) { + if (!resolved) { return token.text; } return ( {token.text} diff --git a/apps/website/src/components/ParameterTable.tsx b/apps/website/src/components/ParameterTable.tsx index 2ad0b8426..046a20d94 100644 --- a/apps/website/src/components/ParameterTable.tsx +++ b/apps/website/src/components/ParameterTable.tsx @@ -17,7 +17,7 @@ export function ParameterTable({ item }: { readonly item: ApiDocumentedItem & Ap () => params.map((param) => ({ Name: param.isRest ? `...${param.name}` : param.name, - Type: , + Type: , Optional: param.isOptional ? 'Yes' : 'No', Description: param.description ? : 'None', })), diff --git a/apps/website/src/components/Property.tsx b/apps/website/src/components/Property.tsx index 5a30d8860..0a5b6cedf 100644 --- a/apps/website/src/components/Property.tsx +++ b/apps/website/src/components/Property.tsx @@ -32,9 +32,7 @@ export function Property({ > {`${item.displayName}${item.isOptional ? '?' : ''}`} : - {item.propertyTypeExcerpt.text ? ( - - ) : null} + {item.propertyTypeExcerpt.text ? : null} {hasSummary || inheritedFrom ? ( diff --git a/apps/website/src/components/SignatureText.tsx b/apps/website/src/components/SignatureText.tsx index 58168c184..ab7227a5e 100644 --- a/apps/website/src/components/SignatureText.tsx +++ b/apps/website/src/components/SignatureText.tsx @@ -1,10 +1,10 @@ -import type { ApiModel, Excerpt } from '@discordjs/api-extractor-model'; +import type { Excerpt } from '@discordjs/api-extractor-model'; import { ExcerptText } from './ExcerptText'; -export function SignatureText({ excerpt, model }: { readonly excerpt: Excerpt; readonly model: ApiModel }) { +export function SignatureText({ excerpt }: { readonly excerpt: Excerpt }) { return (

- +

); } diff --git a/apps/website/src/components/TypeParamTable.tsx b/apps/website/src/components/TypeParamTable.tsx index e0601638f..9b5db7b2a 100644 --- a/apps/website/src/components/TypeParamTable.tsx +++ b/apps/website/src/components/TypeParamTable.tsx @@ -11,21 +11,20 @@ const rowElements = { }; export function TypeParamTable({ item }: { readonly item: ApiTypeParameterListMixin }) { - const model = item.getAssociatedModel()!; const rows = useMemo( () => item.typeParameters.map((typeParam) => ({ Name: typeParam.name, - Constraints: , + Constraints: , Optional: typeParam.isOptional ? 'Yes' : 'No', - Default: , + Default: , Description: typeParam.tsdocTypeParamBlock ? ( ) : ( 'None' ), })), - [item, model], + [item], ); return ( diff --git a/apps/website/src/components/documentation/HierarchyText.tsx b/apps/website/src/components/documentation/HierarchyText.tsx index 646b6e00f..13fc07c79 100644 --- a/apps/website/src/components/documentation/HierarchyText.tsx +++ b/apps/website/src/components/documentation/HierarchyText.tsx @@ -9,8 +9,6 @@ export function HierarchyText({ readonly item: ApiClass | ApiInterface; readonly type: 'Extends' | 'Implements'; }) { - const model = item.getAssociatedModel()!; - if ( (item.kind === ApiItemKind.Class && (item as ApiClass).extendsType === undefined && @@ -50,7 +48,7 @@ export function HierarchyText({

{type}

- +
))} diff --git a/apps/website/src/components/documentation/util.ts b/apps/website/src/components/documentation/util.ts index f7acd503e..b95912339 100644 --- a/apps/website/src/components/documentation/util.ts +++ b/apps/website/src/components/documentation/util.ts @@ -1,4 +1,4 @@ -import { ApiItemKind } from '@discordjs/api-extractor-model'; +import { ApiItemKind, Meaning } from '@discordjs/api-extractor-model'; import type { ApiItem, ApiItemContainerMixin, @@ -10,11 +10,25 @@ import type { ApiParameterListMixin, ApiEvent, } from '@discordjs/api-extractor-model'; +import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; import { METHOD_SEPARATOR, OVERLOAD_SEPARATOR } from '~/util/constants'; import { resolveMembers } from '~/util/members'; import { resolveParameters } from '~/util/model'; import type { TableOfContentsSerialized } from '../TableOfContentItems'; +export type ApiItemLike = { + [K in keyof ApiItem]?: K extends 'displayName' | 'kind' + ? ApiItem[K] + : K extends 'parent' + ? ApiItemLike | undefined + : ApiItem[K] | undefined; +}; + +interface ResolvedCanonicalReference { + item: ApiItemLike; + package: string; +} + export function hasProperties(item: ApiItemContainerMixin) { return resolveMembers(item, memberPredicate).some( ({ item: member }) => member.kind === ApiItemKind.Property || member.kind === ApiItemKind.PropertySignature, @@ -31,12 +45,65 @@ export function hasEvents(item: ApiItemContainerMixin) { return resolveMembers(item, memberPredicate).some(({ item: member }) => member.kind === ApiItemKind.Event); } -export function resolveItemURI(item: ApiItem): string { +export function resolveItemURI(item: ApiItemLike): string { return !item.parent || item.parent.kind === ApiItemKind.EntryPoint ? `${item.displayName}${OVERLOAD_SEPARATOR}${item.kind}` : `${item.parent.displayName}${OVERLOAD_SEPARATOR}${item.parent.kind}${METHOD_SEPARATOR}${item.displayName}`; } +export function resolveCanonicalReference(canonicalReference: DeclarationReference): ResolvedCanonicalReference | null { + if ( + canonicalReference.source && + 'packageName' in canonicalReference.source && + canonicalReference.symbol?.componentPath && + canonicalReference.symbol.meaning + ) + return { + package: canonicalReference.source.unscopedPackageName, + item: { + kind: mapMeaningToKind(canonicalReference.symbol.meaning as unknown as Meaning), + displayName: canonicalReference.symbol.componentPath.component.toString(), + containerKey: `|${ + canonicalReference.symbol.meaning + }|${canonicalReference.symbol.componentPath.component.toString()}`, + }, + }; + return null; +} + +function mapMeaningToKind(meaning: Meaning): ApiItemKind { + switch (meaning) { + case Meaning.CallSignature: + return ApiItemKind.CallSignature; + case Meaning.Class: + return ApiItemKind.Class; + case Meaning.ComplexType: + throw new Error('Not a valid canonicalReference: Meaning.ComplexType'); + case Meaning.ConstructSignature: + return ApiItemKind.ConstructSignature; + case Meaning.Constructor: + return ApiItemKind.Constructor; + case Meaning.Enum: + return ApiItemKind.Enum; + case Meaning.Event: + return ApiItemKind.Event; + case Meaning.Function: + return ApiItemKind.Function; + case Meaning.IndexSignature: + return ApiItemKind.IndexSignature; + case Meaning.Interface: + return ApiItemKind.Interface; + case Meaning.Member: + return ApiItemKind.Property; + case Meaning.Namespace: + return ApiItemKind.Namespace; + case Meaning.TypeAlias: + return ApiItemKind.TypeAlias; + case Meaning.Variable: + return ApiItemKind.Variable; + } +} + export function memberPredicate( item: ApiItem, ): item is ApiEvent | ApiMethod | ApiMethodSignature | ApiProperty | ApiPropertySignature { diff --git a/apps/website/src/components/model/enum/EnumMember.tsx b/apps/website/src/components/model/enum/EnumMember.tsx index 18d95991e..f2d50a18c 100644 --- a/apps/website/src/components/model/enum/EnumMember.tsx +++ b/apps/website/src/components/model/enum/EnumMember.tsx @@ -14,9 +14,7 @@ export function EnumMember({ member }: { readonly member: ApiEnumMember }) { > {member.name} = - {member.initializerExcerpt ? ( - - ) : null} + {member.initializerExcerpt ? : null} {member.tsdocComment ? : null} diff --git a/apps/website/src/components/model/method/MethodHeader.tsx b/apps/website/src/components/model/method/MethodHeader.tsx index 1edb2ccc4..785799bfb 100644 --- a/apps/website/src/components/model/method/MethodHeader.tsx +++ b/apps/website/src/components/model/method/MethodHeader.tsx @@ -22,7 +22,7 @@ export function MethodHeader({ method }: { readonly method: ApiMethod | ApiMetho > {`${method.name}(${parametersString(method)})`} : - + diff --git a/apps/website/src/util/addPackageToModel.ts b/apps/website/src/util/addPackageToModel.ts index bcfb55e58..2f25ae85c 100644 --- a/apps/website/src/util/addPackageToModel.ts +++ b/apps/website/src/util/addPackageToModel.ts @@ -1,25 +1,8 @@ -import type { ApiModel, ApiPackage } from '@discordjs/api-extractor-model'; -import { ApiItem } from '@discordjs/api-extractor-model'; -import { TSDocConfiguration } from '@microsoft/tsdoc'; -import { TSDocConfigFile } from '@microsoft/tsdoc-config'; +import { ApiPackage } from '@discordjs/api-extractor-model'; +import type { ApiModel } from '@discordjs/api-extractor-model'; export const addPackageToModel = (model: ApiModel, data: any) => { - let apiPackage: ApiPackage; - if (data.metadata) { - const tsdocConfiguration = new TSDocConfiguration(); - const tsdocConfigFile = TSDocConfigFile.loadFromObject(data.metadata.tsdocConfig); - tsdocConfigFile.configureParser(tsdocConfiguration); - - apiPackage = ApiItem.deserialize(data, { - apiJsonFilename: '', - toolPackage: data.metadata.toolPackage, - toolVersion: data.metadata.toolVersion, - versionToDeserialize: data.metadata.schemaVersion, - tsdocConfiguration, - }) as ApiPackage; - } else { - apiPackage = ApiItem.deserializeDocgen(data, 'discord.js') as ApiPackage; - } + const apiPackage = ApiPackage.loadFromJson(data); model.addMember(apiPackage); return model; diff --git a/packages/api-extractor-model/src/model/ApiPackage.ts b/packages/api-extractor-model/src/model/ApiPackage.ts index a648581f3..8cbc01928 100644 --- a/packages/api-extractor-model/src/model/ApiPackage.ts +++ b/packages/api-extractor-model/src/model/ApiPackage.ts @@ -2,6 +2,8 @@ // See LICENSE in the project root for license information. import { Buffer } from 'node:buffer'; +import path from 'node:path'; +import util from 'node:util'; import { TSDocConfiguration } from '@microsoft/tsdoc'; import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; import { TSDocConfigFile } from '@microsoft/tsdoc-config'; @@ -29,10 +31,58 @@ export interface IApiPackageOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiDocumentedItemOptions { + dependencies?: Record | undefined; projectFolderUrl?: string | undefined; tsdocConfiguration: TSDocConfiguration; } +const MinifyJSONMapping = { + canonicalReference: 'c', + constraintTokenRange: 'ctr', + dependencies: 'dp', + defaultTypeTokenRange: 'dtr', + docComment: 'd', + endIndex: 'en', + excerptTokens: 'ex', + extendsTokenRange: 'etr', + extendsTokenRanges: 'etrs', + fileColumn: 'co', + fileLine: 'l', + fileUrlPath: 'pat', + implementsTokenRanges: 'itrs', + initializerTokenRange: 'itr', + isAbstract: 'ab', + isOptional: 'op', + isProtected: 'pr', + isReadonly: 'ro', + isRest: 'rs', + isStatic: 'sta', + kind: 'k', + members: 'ms', + metadata: 'meta', + name: 'n', + oldestForwardsCompatibleVersion: 'ov', + overloadIndex: 'oi', + parameterName: 'pn', + parameterTypeTokenRange: 'ptr', + parameters: 'ps', + preserveMemberOrder: 'pmo', + projectFolderUrl: 'pdir', + propertyTypeTokenRange: 'prtr', + releaseTag: 'r', + returnTypeTokenRange: 'rtr', + schemaVersion: 'v', + startIndex: 'st', + text: 't', + toolPackage: 'tpk', + toolVersion: 'tv', + tsdocConfig: 'ts', + typeParameterName: 'tp', + typeParameters: 'tps', + typeTokenRange: 'ttr', + variableTypeTokenRange: 'vtr', +}; + export interface IApiPackageMetadataJson { /** * To support forwards compatibility, the `oldestForwardsCompatibleVersion` field tracks the oldest schema version @@ -77,10 +127,15 @@ export interface IApiPackageMetadataJson { * Normally this configuration is loaded from the project's tsdoc.json file. It is stored * in the .api.json file so that doc comments can be parsed accurately when loading the file. */ - tsdocConfig: JsonObject; + tsdocConfig?: JsonObject; } export interface IApiPackageJson extends IApiItemJson { + /** + * Names of packages in the same monorepo this one uses mapped to the version of said package. + */ + dependencies?: Record; + /** * A file header that stores metadata about the tool that wrote the *.api.json file. */ @@ -141,11 +196,31 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented private readonly _projectFolderUrl?: string | undefined; + private readonly _dependencies?: Record | undefined; + public constructor(options: IApiPackageOptions) { super(options); this._tsdocConfiguration = options.tsdocConfiguration; this._projectFolderUrl = options.projectFolderUrl; + + if (options.dependencies) { + this._dependencies = options.dependencies; + } else { + const packageJson = PackageJsonLookup.instance.tryLoadPackageJsonFor('.'); + if (packageJson?.dependencies) { + this._dependencies = {}; + for (const [pack, semVer] of Object.entries(packageJson.dependencies)) { + const pathToPackage = path.join('..', pack.includes('/') ? pack.slice(pack.lastIndexOf('/')) : pack); + if (semVer === 'workspace:^') { + this._dependencies[pack] = + PackageJsonLookup.instance.tryLoadPackageJsonFor(pathToPackage)?.version ?? 'unknown'; + } else if (FileSystem.exists(pathToPackage)) { + this._dependencies[pack] = semVer; + } + } + } + } } /** @@ -159,11 +234,17 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented super.onDeserializeInto(options, context, jsonObject); options.projectFolderUrl = jsonObject.projectFolderUrl; + options.dependencies = jsonObject.dependencies; } public static loadFromJsonFile(apiJsonFilename: string): ApiPackage { - const jsonObject: IApiPackageJson = JsonFile.load(apiJsonFilename); + return this.loadFromJson(JsonFile.load(apiJsonFilename), apiJsonFilename); + } + public static loadFromJson(rawJson: any, apiJsonFilename: string = ''): ApiPackage { + const jsonObject = + MinifyJSONMapping.metadata in rawJson ? this._mapFromMinified(rawJson) : (rawJson as IApiPackageJson); + if (!jsonObject?.metadata) throw new Error(util.inspect(rawJson, { depth: 2 })); if (!jsonObject?.metadata || typeof jsonObject.metadata.schemaVersion !== 'number') { throw new Error( `Error loading ${apiJsonFilename}:` + @@ -212,7 +293,7 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration(); - if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004) { + if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004 && 'tsdocConfiguration' in jsonObject) { const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig); if (tsdocConfigFile.hasErrors) { throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary()); @@ -251,6 +332,10 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented return this.members as readonly ApiEntryPoint[]; } + public get dependencies(): Record | undefined { + return this._dependencies; + } + /** * The TSDoc configuration that was used when analyzing the API for this package. * @@ -306,9 +391,13 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented jsonObject.projectFolderUrl = this.projectFolderUrl; } + if (this._dependencies) { + jsonObject.dependencies = this._dependencies; + } + this.serializeInto(jsonObject); if (ioptions.minify) { - FileSystem.writeFile(apiJsonFilename, Buffer.from(JSON.stringify(jsonObject), 'utf8'), { + FileSystem.writeFile(apiJsonFilename, Buffer.from(JSON.stringify(this._mapToMinified(jsonObject)), 'utf8'), { ensureFolderExists: ioptions.ensureFolderExists ?? true, }); } else { @@ -322,4 +411,47 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented public override buildCanonicalReference(): DeclarationReference { return DeclarationReference.package(this.name); } + + private _mapToMinified(jsonObject: IApiPackageJson) { + const mapper = (item: any): any => { + if (Array.isArray(item)) return item.map(mapper); + else { + const result: any = {}; + for (const key of Object.keys(item)) { + if (key === 'dependencies') { + result[MinifyJSONMapping.dependencies] = item.dependencies; + } else + result[MinifyJSONMapping[key as keyof typeof MinifyJSONMapping]] = + typeof item[key] === 'object' ? mapper(item[key]) : item[key]; + } + + return result; + } + }; + + return mapper(jsonObject); + } + + private static _mapFromMinified(jsonObject: any): IApiPackageJson { + const mapper = (item: any): any => { + if (Array.isArray(item)) return item.map(mapper); + else { + const result: any = {}; + for (const key of Object.keys(item)) { + if (key === MinifyJSONMapping.dependencies) { + result.dependencies = item[MinifyJSONMapping.dependencies]; + } else + result[ + Object.keys(MinifyJSONMapping).find( + (look) => MinifyJSONMapping[look as keyof typeof MinifyJSONMapping] === key, + )! + ] = typeof item[key] === 'object' ? mapper(item[key]) : item[key]; + } + + return result; + } + }; + + return mapper(jsonObject) as IApiPackageJson; + } } diff --git a/packages/api-extractor-model/src/model/DeserializerContext.ts b/packages/api-extractor-model/src/model/DeserializerContext.ts index b5778c8f9..49730e434 100644 --- a/packages/api-extractor-model/src/model/DeserializerContext.ts +++ b/packages/api-extractor-model/src/model/DeserializerContext.ts @@ -96,13 +96,18 @@ export enum ApiJsonSchemaVersion { */ V_1012 = 1_012, + /** + * Make tsdocConfiguration optional + */ + V_1013 = 1_013, + /** * The current latest .api.json schema version. * * IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE` * should be updated. */ - LATEST = V_1012, + LATEST = V_1013, /** * The oldest .api.json schema version that is still supported for backwards compatibility. @@ -119,7 +124,7 @@ export enum ApiJsonSchemaVersion { * if the older library would not be able to deserialize your new file format. Adding a nonessential field * is generally okay. Removing, modifying, or reinterpreting existing fields is NOT safe. */ - OLDEST_FORWARDS_COMPATIBLE = V_1001, + OLDEST_FORWARDS_COMPATIBLE = V_1013, } export class DeserializerContext { diff --git a/packages/api-extractor/src/generators/ApiModelGenerator.ts b/packages/api-extractor/src/generators/ApiModelGenerator.ts index 3152b3572..983ee49a9 100644 --- a/packages/api-extractor/src/generators/ApiModelGenerator.ts +++ b/packages/api-extractor/src/generators/ApiModelGenerator.ts @@ -212,7 +212,11 @@ interface IProcessAstEntityContext { const linkRegEx = /{@link\s(?\w+)#(?event:)?(?[\w()]+)(?\s[^}]*)?}/g; function fixLinkTags(input?: string): string | undefined { - return input?.replaceAll(linkRegEx, '{@link $.$$}'); + return input?.replaceAll( + linkRegEx, + (_match, _p1, _p2, _p3, _p4, _offset, _string, groups) => + `{@link ${groups.class}.${groups.prop}${groups.name ? ` |${groups.name}` : ''}}`, + ); } function filePathFromJson(meta: DocgenMetaJson): string {