Merge pull request #9445 from backstage/rugvip/api-doc-tweaks
api-extractor: add doc transforms for arrow components and func decls
This commit is contained in:
+218
-9
@@ -577,6 +577,216 @@ forking rushstash to modify the api-documenter markdown generation,
|
||||
which otherwise is the recommended way to do customizations.
|
||||
*/
|
||||
|
||||
type ExcerptToken = {
|
||||
kind: string;
|
||||
text: string;
|
||||
canonicalReference?: string;
|
||||
};
|
||||
|
||||
class ExcerptTokenMatcher {
|
||||
readonly #tokens: ExcerptToken[];
|
||||
|
||||
constructor(tokens: ExcerptToken[]) {
|
||||
this.#tokens = tokens.slice();
|
||||
}
|
||||
|
||||
nextContent() {
|
||||
const token = this.#tokens.shift();
|
||||
if (token?.kind === 'Content') {
|
||||
return token.text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
matchContent(expectedText: string) {
|
||||
const text = this.nextContent();
|
||||
return text !== expectedText;
|
||||
}
|
||||
|
||||
getTokensUntilArrow() {
|
||||
const tokens = [];
|
||||
for (;;) {
|
||||
const token = this.#tokens.shift();
|
||||
if (token === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (token.kind === 'Content' && token.text === ') => ') {
|
||||
return tokens;
|
||||
}
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
getComponentReturnTokens() {
|
||||
const first = this.#tokens.shift();
|
||||
if (!first) {
|
||||
return undefined;
|
||||
}
|
||||
const second = this.#tokens.shift();
|
||||
|
||||
if (this.#tokens.length !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (first.kind !== 'Reference' || first.text !== 'JSX.Element') {
|
||||
return undefined;
|
||||
}
|
||||
if (!second) {
|
||||
return [first];
|
||||
} else if (second.kind === 'Content' && second.text === ' | null') {
|
||||
return [first, second];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class ApiModelTransforms {
|
||||
static deserializeWithTransforms(
|
||||
serialized: any,
|
||||
transforms: Array<(member: any) => any>,
|
||||
): ApiPackage {
|
||||
if (serialized.kind !== 'Package') {
|
||||
throw new Error(
|
||||
`Unexpected root kind in serialized ApiPackage, ${serialized.kind}`,
|
||||
);
|
||||
}
|
||||
if (serialized.members.length !== 1) {
|
||||
throw new Error(
|
||||
`Unexpected members in serialized ApiPackage, [${serialized.members
|
||||
.map(m => m.kind)
|
||||
.join(' ')}]`,
|
||||
);
|
||||
}
|
||||
const [entryPoint] = serialized.members;
|
||||
if (entryPoint.kind !== 'EntryPoint') {
|
||||
throw new Error(
|
||||
`Unexpected kind in serialized ApiPackage member, ${entryPoint.kind}`,
|
||||
);
|
||||
}
|
||||
|
||||
const transformed = {
|
||||
...serialized,
|
||||
members: [
|
||||
{
|
||||
...entryPoint,
|
||||
members: entryPoint.members.map(member =>
|
||||
transforms.reduce((m, t) => t(m), member),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return ApiPackage.deserialize(
|
||||
transformed,
|
||||
transformed.metadata,
|
||||
) as ApiPackage;
|
||||
}
|
||||
|
||||
static transformArrowComponents = (member: any) => {
|
||||
if (member.kind !== 'Variable') {
|
||||
return member;
|
||||
}
|
||||
|
||||
const { name, excerptTokens } = member;
|
||||
|
||||
// First letter in name must be uppercase
|
||||
const [firstChar] = name;
|
||||
if (firstChar.toLocaleUpperCase('en-US') !== firstChar) {
|
||||
return member;
|
||||
}
|
||||
|
||||
// First content must match expected declaration format
|
||||
const tokens = new ExcerptTokenMatcher(excerptTokens);
|
||||
if (tokens.nextContent() !== `${name}: `) {
|
||||
return member;
|
||||
}
|
||||
|
||||
// Next needs to be an arrow with `props` parameters or no parameters
|
||||
// followed by a return type of `JSX.Element | null` or just `JSX.Element`
|
||||
const declStart = tokens.nextContent();
|
||||
if (declStart === '(props: ' || declStart === '(_props: ') {
|
||||
const props = tokens.getTokensUntilArrow();
|
||||
const ret = tokens.getComponentReturnTokens();
|
||||
if (props && ret) {
|
||||
return this.makeComponentMember(member, ret, props);
|
||||
}
|
||||
} else if (declStart === '() => ') {
|
||||
const ret = tokens.getComponentReturnTokens();
|
||||
if (ret) {
|
||||
return this.makeComponentMember(member, ret);
|
||||
}
|
||||
}
|
||||
return member;
|
||||
};
|
||||
|
||||
static makeComponentMember(
|
||||
member: any,
|
||||
ret: ExcerptToken[],
|
||||
props?: ExcerptToken[],
|
||||
) {
|
||||
const declTokens = props
|
||||
? [
|
||||
{
|
||||
kind: 'Content',
|
||||
text: `export declare function ${member.name}(props: `,
|
||||
},
|
||||
...props,
|
||||
{
|
||||
kind: 'Content',
|
||||
text: '): ',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
kind: 'Content',
|
||||
text: `export declare function ${member.name}(): `,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
kind: 'Function',
|
||||
name: member.name,
|
||||
releaseTag: member.releaseTag,
|
||||
docComment: member.docComment ?? '',
|
||||
canonicalReference: member.canonicalReference,
|
||||
excerptTokens: [...declTokens, ...ret],
|
||||
returnTypeTokenRange: {
|
||||
startIndex: declTokens.length,
|
||||
endIndex: declTokens.length + ret.length,
|
||||
},
|
||||
parameters: props
|
||||
? [
|
||||
{
|
||||
parameterName: 'props',
|
||||
parameterTypeTokenRange: {
|
||||
startIndex: 1,
|
||||
endIndex: 1 + props.length,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
overloadIndex: 1,
|
||||
};
|
||||
}
|
||||
|
||||
static transformTrimDeclare = (member: any) => {
|
||||
const { excerptTokens } = member;
|
||||
const firstContent = new ExcerptTokenMatcher(excerptTokens).nextContent();
|
||||
if (firstContent && firstContent.startsWith('export declare ')) {
|
||||
return {
|
||||
...member,
|
||||
excerptTokens: [
|
||||
{
|
||||
kind: 'Content',
|
||||
text: firstContent.slice('export declare '.length),
|
||||
},
|
||||
...excerptTokens.slice(1),
|
||||
],
|
||||
};
|
||||
}
|
||||
return member;
|
||||
};
|
||||
}
|
||||
|
||||
async function buildDocs({
|
||||
inputDir,
|
||||
outputDir,
|
||||
@@ -600,13 +810,12 @@ async function buildDocs({
|
||||
|
||||
const newModel = new ApiModel();
|
||||
for (const serialized of serializedPackages) {
|
||||
// Add any docs filtering logic here
|
||||
|
||||
const pkg = ApiPackage.deserialize(
|
||||
serialized,
|
||||
serialized.metadata,
|
||||
) as ApiPackage;
|
||||
newModel.addMember(pkg);
|
||||
newModel.addMember(
|
||||
ApiModelTransforms.deserializeWithTransforms(serialized, [
|
||||
ApiModelTransforms.transformArrowComponents,
|
||||
ApiModelTransforms.transformTrimDeclare,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// The doc AST need to be extended with custom nodes if we want to
|
||||
@@ -822,9 +1031,9 @@ async function main() {
|
||||
const selectedPackageDirs = await findSpecificPackageDirs(
|
||||
process.argv.slice(2).filter(arg => !arg.startsWith('--')),
|
||||
);
|
||||
if (selectedPackageDirs && (isCiBuild || isDocsBuild)) {
|
||||
if (selectedPackageDirs && isCiBuild) {
|
||||
throw new Error(
|
||||
'Package path arguments are not supported for the --ci and --docs flags',
|
||||
'Package path arguments are not supported together with the --ci flag',
|
||||
);
|
||||
}
|
||||
if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) {
|
||||
|
||||
Reference in New Issue
Block a user