Skip to content

Commit

Permalink
Merge pull request #9445 from backstage/rugvip/api-doc-tweaks
Browse files Browse the repository at this point in the history
api-extractor: add doc transforms for arrow components and func decls
  • Loading branch information
Rugvip committed Feb 10, 2022
2 parents d2a24dc + 54c382c commit 9c17a6c
Showing 1 changed file with 218 additions and 9 deletions.
227 changes: 218 additions & 9 deletions scripts/api-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 9c17a6c

Please sign in to comment.