Skip to content

Commit b20f2e9

Browse files
authored
Merge branch 'main' into fix-various-todos
2 parents 298b590 + 1b08de8 commit b20f2e9

File tree

3 files changed

+170
-1
lines changed

3 files changed

+170
-1
lines changed

scripts/apidoc.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import * as TypeDoc from 'typedoc';
33
import { writeApiPagesIndex } from './apidoc/apiDocsWriter';
44
import { processDirectMethods } from './apidoc/directMethods';
55
import { processModuleMethods } from './apidoc/moduleMethods';
6+
import {
7+
DefaultParameterAwareSerializer,
8+
parameterDefaultReader,
9+
patchProjectParameterDefaults,
10+
} from './apidoc/parameterDefaults';
611
import type { PageIndex } from './apidoc/utils';
712
import { pathOutputDir } from './apidoc/utils';
813

@@ -15,6 +20,14 @@ async function build(): Promise<void> {
1520
// If you want TypeDoc to load typedoc.json files
1621
//app.options.addReader(new TypeDoc.TypeDocReader());
1722

23+
// Read parameter defaults
24+
app.converter.on(
25+
TypeDoc.Converter.EVENT_CREATE_DECLARATION,
26+
parameterDefaultReader
27+
);
28+
// Add to debug json output
29+
app.serializer.addSerializer(new DefaultParameterAwareSerializer(undefined));
30+
1831
app.bootstrap({
1932
entryPoints: ['src/index.ts'],
2033
pretty: true,
@@ -31,6 +44,8 @@ async function build(): Promise<void> {
3144
await app.generateJson(project, pathOutputJson);
3245
console.log(pathOutputDir);
3346

47+
patchProjectParameterDefaults(project);
48+
3449
const modulesPages: PageIndex = [];
3550
modulesPages.push({ text: 'Localization', link: '/api/localization.html' });
3651
modulesPages.push(...processModuleMethods(project));

scripts/apidoc/parameterDefaults.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type {
2+
Context,
3+
DeclarationReflection,
4+
EventCallback,
5+
JSONOutput,
6+
ProjectReflection,
7+
SignatureReflection,
8+
} from 'typedoc';
9+
import {
10+
Reflection,
11+
ReflectionKind,
12+
SerializerComponent,
13+
TypeScript,
14+
} from 'typedoc';
15+
16+
const reflectionKindFunctionOrMethod =
17+
ReflectionKind.Function | ReflectionKind.Method;
18+
19+
interface ParameterDefaultsAware extends Reflection {
20+
implementationDefaultParameters: string[];
21+
}
22+
23+
/**
24+
* TypeDoc EventCallback for EVENT_CREATE_DECLARATION events that reads the default parameters from the implementation.
25+
*/
26+
export const parameterDefaultReader: EventCallback = (
27+
context: Context,
28+
reflection: Reflection
29+
): void => {
30+
const symbol = context.project.getSymbolFromReflection(reflection);
31+
if (!symbol) return;
32+
33+
if (
34+
reflection.kindOf(reflectionKindFunctionOrMethod) &&
35+
symbol.declarations?.length
36+
) {
37+
const lastDeclaration = symbol.declarations[symbol.declarations.length - 1];
38+
if (TypeScript.isFunctionLike(lastDeclaration)) {
39+
(reflection as ParameterDefaultsAware).implementationDefaultParameters =
40+
lastDeclaration.parameters.map((param) =>
41+
cleanParameterDefault(param.initializer?.getText())
42+
);
43+
}
44+
}
45+
};
46+
47+
/**
48+
* Removes compile expressions that don't add any value for readers.
49+
*
50+
* @param value The default value to clean.
51+
* @returns The cleaned default value.
52+
*/
53+
function cleanParameterDefault(value?: string): string {
54+
if (value == null) {
55+
return undefined;
56+
}
57+
// Strip type casts: "'foobar' as unknown as T" => "'foobar'"
58+
return value.replace(/ as unknown as [A-Za-z<>]+/, '');
59+
}
60+
61+
/**
62+
* Serializer that adds the `implementationDefaultParameters` to the JSON output.
63+
*/
64+
export class DefaultParameterAwareSerializer extends SerializerComponent<Reflection> {
65+
serializeGroup(instance: unknown): boolean {
66+
return instance instanceof Reflection;
67+
}
68+
69+
supports(item: unknown): boolean {
70+
return true;
71+
}
72+
73+
toObject(item: Reflection, obj?: object): Partial<JSONOutput.Reflection> {
74+
(obj as ParameterDefaultsAware).implementationDefaultParameters = (
75+
item as ParameterDefaultsAware
76+
).implementationDefaultParameters;
77+
return obj;
78+
}
79+
}
80+
81+
/**
82+
* Replaces all methods' last signature's parameter's default value with the default value read from the implementation.
83+
*
84+
* @param project The project to patch.
85+
*/
86+
export function patchProjectParameterDefaults(
87+
project: ProjectReflection
88+
): void {
89+
const functionOrMethods = project.getReflectionsByKind(
90+
reflectionKindFunctionOrMethod
91+
) as DeclarationReflection[];
92+
for (const functionOrMethod of functionOrMethods) {
93+
patchMethodParameterDefaults(functionOrMethod);
94+
}
95+
}
96+
97+
/**
98+
* Replaces the last signature's parameter's default value with the default value read from the implementation.
99+
*
100+
* @param method The method to patch.
101+
*/
102+
function patchMethodParameterDefaults(method: DeclarationReflection): void {
103+
const signatures = method.signatures;
104+
const signature = signatures[signatures.length - 1];
105+
const parameterDefaults = (method as unknown as ParameterDefaultsAware)
106+
.implementationDefaultParameters;
107+
if (parameterDefaults) {
108+
patchSignatureParameterDefaults(signature, parameterDefaults);
109+
}
110+
}
111+
112+
/**
113+
* Replaces the given signature's parameter's default value with the given default values.
114+
*
115+
* @param signature The signature to patch.
116+
* @param parameterDefaults The defaults to add.
117+
*/
118+
function patchSignatureParameterDefaults(
119+
signature: SignatureReflection,
120+
parameterDefaults: string[]
121+
): void {
122+
const signatureParameters = signature.parameters;
123+
if (signatureParameters.length !== parameterDefaults.length) {
124+
throw new Error('Unexpected parameter length mismatch');
125+
}
126+
signatureParameters.forEach(
127+
(param, index) =>
128+
(param.defaultValue = parameterDefaults[index] || param.defaultValue)
129+
);
130+
}

scripts/apidoc/signature.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ function analyzeParameter(parameter: ParameterReflection): {
162162
const name = parameter.name;
163163
const declarationName = name + (isOptional(parameter) ? '?' : '');
164164
const type = parameter.type;
165-
const defaultValue = parameter.defaultValue;
165+
const commentDefault = extractDefaultFromComment(parameter.comment);
166+
const defaultValue = parameter.defaultValue ?? commentDefault;
166167

167168
let signatureText = '';
168169
if (defaultValue) {
@@ -200,6 +201,7 @@ function analyzeParameterOptions(
200201
return properties.map((property) => ({
201202
name: `${name}.${property.name}${isOptional(property) ? '?' : ''}`,
202203
type: declarationTypeToText(property),
204+
default: extractDefaultFromComment(property.comment),
203205
description: mdToHtml(
204206
toBlock(property.comment ?? property.signatures?.[0].comment)
205207
),
@@ -284,3 +286,25 @@ function signatureTypeToText(signature: SignatureReflection): string {
284286
.map((p) => `${p.name}: ${typeToText(p.type)}`)
285287
.join(', ')}) => ${typeToText(signature.type)}`;
286288
}
289+
290+
/**
291+
* Extracts and removed the parameter default from the comments.
292+
*
293+
* @param comment The comment to extract the default from.
294+
* @returns The extracted default value.
295+
*/
296+
function extractDefaultFromComment(comment?: Comment): string {
297+
if (!comment) {
298+
return;
299+
}
300+
const text = comment.shortText;
301+
if (!text || text.trim() === '') {
302+
return;
303+
}
304+
const result = /(.*)[ \n]Defaults to `([^`]+)`./.exec(text);
305+
if (!result) {
306+
return;
307+
}
308+
comment.shortText = result[1];
309+
return result[2];
310+
}

0 commit comments

Comments
 (0)