Skip to content

Commit 7c0511e

Browse files
alicewriteswrongsrwaskiewicz
authored andcommitted
feat(docs): enrich type information for docs-json Output Target (#4212)
This enriches the type information supplied in the `docs-json` output target in order to make it easier to deliver a rich documentation experience for a Stencil project. This involves three significant changes. Firstly, the information which the `docs-json` OT gathers about the properties which comprise the API of a Stencil component is expanded to include more information about the types used in the declaration of the property. In particular, this means that if you have, say, a `@Method` on a component whose signature involves several types imported from other modules the JSON blob for that method will now include more detailed information about all the types used in that declaration. This information is stored under the `complexType` key for each field on a component. This `complexType` object includes a `references` object which now includes a new, globally-unique ID for each type in a Stencil project which looks like: ```ts const typeID = `${pathToTypeHomeModule}::${typeName}`; ``` where `pathToTypeHomeModule` is the path to the type's "home" module, i.e. where it was originally declared, and `typeName` is the _original_ name it is declared under in that module. This if you had a type like the following: ```ts src/shared-interfaces.ts export interface SharedInterface { property: "value"; } ``` the `SharedInterface` type would have the following ID: ```ts "src/shared-interfaces.ts::SharedInterface" ``` This unique identifier allows cross-references to be made between the _usage sites_ of a type (as documented in the JSON blob for, say, a `@Method` or `@Prop`) and canonical information about that type. The second major addition is the creation of a new canonical reference point for all exported types used within a Stencil project. This reference is called the _type library_ and it is included in the `docs-json` output target under the `"typeLibrary"` key. It's an object which maps the unique type IDs described above to an object containing information about those types, including their original declaration rendered as a string, the path to their home module, and documentation, if present > Note that global types, types imported from `node_modules`, and types marked as private with the `@private` JSDoc tag are not included in the type library, even if they are used in the declaration of a property decorated with a Stencil decorator. The combination of globally-unique type IDs and the type library will allow Stencil component authors to create richer documentation experiences where the usage sites of a widely imported type can easily be linked to a canonical reference point for that type. The third addition is a new configuration option for the `docs-json` output target called `supplementalPublicTypes`. This option takes a filepath which points to a file exporting types and interfaces which should be included in the type library regardless of whether they are used in the API of any component included in the Stencil project. The motivation for `supplementalPublicTypes` is to facilitate the documentation of types and interfaces which are important for a given project but which may not be included in the type library because they're not used by a `@Prop()`, `@Event()`, or another Stencil decorator.
1 parent 5c34609 commit 7c0511e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1511
-56
lines changed

.prettierignore

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ test/**/components.d.ts
4545
test/end-to-end/screenshot/
4646
test/end-to-end/docs.d.ts
4747
test/end-to-end/docs.json
48+
test/docs-json/docs.d.ts
49+
test/docs-json/docs.json
4850

4951
# minified angular that exists in the test directory
5052
test/karma/test-app/assets/angular.min.js

BREAKING_CHANGES.md

+82-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
1717
- [Legacy Browser Support Removed](#legacy-browser-support-removed)
1818
- [Legacy Cache Stats Config Flag Removed](#legacy-cache-stats-config-flag-removed)
1919
- [Drop Node 14 Support](#drop-node-14-support)
20+
- [Information included in JSON documentation expanded](#information-included-in-docs-json-expanded)
2021

2122
### General
2223

@@ -81,6 +82,86 @@ Stencil no longer supports Node 14.
8182
Please upgrade local development machines, continuous integration pipelines, etc. to use Node v16 or higher.
8283
For the full list of supported runtimes, please see [our Support Policy](../reference/support-policy.md#javascript-runtime).
8384

85+
#### Information included in `docs-json` expanded
86+
87+
For Stencil v4 the information included in the output of the `docs-json` output
88+
target was expanded to include more information about the types of properties
89+
and methods on Stencil components.
90+
91+
For more context on this change, see the [documentation for the new
92+
`supplementalPublicTypes`](https://stenciljs.com/docs/docs-json#supplementalpublictypes)
93+
option for the JSON documentation output target.
94+
95+
##### `JsonDocsEvent`
96+
97+
The JSON-formatted documentation for an `@Event` now includes a field called
98+
`complexType` which includes more information about the types referenced in the
99+
type declarations for that property.
100+
101+
Here's an example of what this looks like for the [ionBreakpointDidChange
102+
event](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/modal/modal.tsx#L289-L292)
103+
on the `Modal` component in Ionic Framework:
104+
105+
```json
106+
{
107+
"complexType": {
108+
"original": "ModalBreakpointChangeEventDetail",
109+
"resolved": "ModalBreakpointChangeEventDetail",
110+
"references": {
111+
"ModalBreakpointChangeEventDetail": {
112+
"location": "import",
113+
"path": "./modal-interface",
114+
"id": "src/components/modal/modal.tsx::ModalBreakpointChangeEventDetail"
115+
}
116+
}
117+
}
118+
}
119+
```
120+
121+
##### `JsonDocsMethod`
122+
123+
The JSON-formatted documentation for a `@Method` now includes a field called
124+
`complexType` which includes more information about the types referenced in
125+
the type declarations for that property.
126+
127+
Here's an example of what this looks like for the [open
128+
method](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/select/select.tsx#L261-L313)
129+
on the `Select` component in Ionic Framework:
130+
131+
```json
132+
{
133+
"complexType": {
134+
"signature": "(event?: UIEvent) => Promise<any>",
135+
"parameters": [
136+
{
137+
"tags": [
138+
{
139+
"name": "param",
140+
"text": "event The user interface event that called the open."
141+
}
142+
],
143+
"text": "The user interface event that called the open."
144+
}
145+
],
146+
"references": {
147+
"Promise": {
148+
"location": "global",
149+
"id": "global::Promise"
150+
},
151+
"UIEvent": {
152+
"location": "global",
153+
"id": "global::UIEvent"
154+
},
155+
"HTMLElement": {
156+
"location": "global",
157+
"id": "global::HTMLElement"
158+
}
159+
},
160+
"return": "Promise<any>"
161+
}
162+
}
163+
```
164+
84165
## Stencil v3.0.0
85166

86167
* [General](#general)
@@ -1093,4 +1174,4 @@ When running Jest directly, previously most of Jest had to be manually configure
10931174
- "jsx"
10941175
- ]
10951176
}
1096-
```
1177+
```

src/compiler/docs/generate-doc-data.ts

+40-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { flatOne, normalizePath, sortBy, unique } from '@utils';
1+
import { flatOne, isOutputTargetDocsJson, normalizePath, sortBy, unique } from '@utils';
22
import { basename, dirname, join, relative } from 'path';
33

44
import type * as d from '../../declarations';
55
import { JsonDocsValue } from '../../declarations';
66
import { typescriptVersion, version } from '../../version';
77
import { getBuildTimestamp } from '../build/build-ctx';
8+
import { addFileToLibrary, getTypeLibrary } from '../transformers/type-library';
89
import { AUTO_GENERATE_COMMENT } from './constants';
910

1011
/**
11-
* Generate metadata that will be used to generate any given documentation-related output target(s)
12+
* Generate metadata that will be used to generate any given documentation-related
13+
* output target(s)
14+
*
1215
* @param config the configuration associated with the current Stencil task run
1316
* @param compilerCtx the current compiler context
1417
* @param buildCtx the build context for the current Stencil task run
@@ -19,6 +22,18 @@ export const generateDocData = async (
1922
compilerCtx: d.CompilerCtx,
2023
buildCtx: d.BuildCtx
2124
): Promise<d.JsonDocs> => {
25+
const jsonOutputTargets = config.outputTargets.filter(isOutputTargetDocsJson);
26+
const supplementalPublicTypes = findSupplementalPublicTypes(jsonOutputTargets);
27+
28+
if (supplementalPublicTypes !== '') {
29+
// if supplementalPublicTypes is set then we want to add all the public
30+
// types in that file to the type library so that output targets producing
31+
// documentation can make use of that data later.
32+
addFileToLibrary(config, supplementalPublicTypes);
33+
}
34+
35+
const typeLibrary = getTypeLibrary();
36+
2237
return {
2338
timestamp: getBuildTimestamp(),
2439
compiler: {
@@ -27,11 +42,28 @@ export const generateDocData = async (
2742
typescriptVersion,
2843
},
2944
components: await getDocsComponents(config, compilerCtx, buildCtx),
45+
typeLibrary,
3046
};
3147
};
3248

49+
/**
50+
* If the `supplementalPublicTypes` option is set on one output target, find that value and return it.
51+
*
52+
* @param outputTargets an array of docs-json output targets
53+
* @returns the first value encountered for supplementalPublicTypes or an empty string
54+
*/
55+
function findSupplementalPublicTypes(outputTargets: d.OutputTargetDocsJson[]): string {
56+
for (const docsJsonOT of outputTargets) {
57+
if (docsJsonOT.supplementalPublicTypes) {
58+
return docsJsonOT.supplementalPublicTypes;
59+
}
60+
}
61+
return '';
62+
}
63+
3364
/**
3465
* Derive the metadata for each Stencil component
66+
*
3567
* @param config the configuration associated with the current Stencil task run
3668
* @param compilerCtx the current compiler context
3769
* @param buildCtx the build context for the current Stencil task run
@@ -50,11 +82,12 @@ const getDocsComponents = async (
5082
const usagesDir = normalizePath(join(dirPath, 'usage'));
5183
const readme = await getUserReadmeContent(compilerCtx, readmePath);
5284
const usage = await generateUsages(compilerCtx, usagesDir);
85+
5386
return moduleFile.cmps
5487
.filter((cmp: d.ComponentCompilerMeta) => !cmp.internal && !cmp.isCollectionDependency)
5588
.map((cmp: d.ComponentCompilerMeta) => ({
5689
dirPath,
57-
filePath: relative(config.rootDir, filePath),
90+
filePath: normalizePath(relative(config.rootDir, filePath), false),
5891
fileName: basename(filePath),
5992
readmePath,
6093
usagesDir,
@@ -150,6 +183,7 @@ const getRealProperties = (properties: d.ComponentCompilerProperty[]): d.JsonDoc
150183
.map((member) => ({
151184
name: member.name,
152185
type: member.complexType.resolved,
186+
complexType: member.complexType,
153187
mutable: member.mutable,
154188
attr: member.attribute,
155189
reflectToAttr: !!member.reflect,
@@ -243,6 +277,7 @@ const getDocsMethods = (methods: d.ComponentCompilerMethod[]): d.JsonDocsMethod[
243277
.map((t) => t.text)
244278
.join('\n'),
245279
},
280+
complexType: member.complexType,
246281
signature: `${member.name}${member.complexType.signature}`,
247282
parameters: [], // TODO
248283
docs: member.docs.text,
@@ -258,6 +293,7 @@ const getDocsEvents = (events: d.ComponentCompilerEvent[]): d.JsonDocsEvent[] =>
258293
event: eventMeta.name,
259294
detail: eventMeta.complexType.resolved,
260295
bubbles: eventMeta.bubbles,
296+
complexType: eventMeta.complexType,
261297
cancelable: eventMeta.cancelable,
262298
composed: eventMeta.composed,
263299
docs: eventMeta.docs.text,
@@ -356,7 +392,7 @@ const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: stri
356392
* @param jsdoc the JSDoc associated with the component's declaration
357393
* @returns the generated documentation
358394
*/
359-
const generateDocs = (readme: string, jsdoc: d.CompilerJsDoc): string => {
395+
const generateDocs = (readme: string | undefined, jsdoc: d.CompilerJsDoc): string => {
360396
const docs = jsdoc.text;
361397
if (docs !== '' || !readme) {
362398
// just return the existing docs if they exist. these would have been captured earlier in the compilation process.

src/compiler/sys/in-memory-fs.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export const createInMemoryFs = (sys: d.CompilerSystem) => {
221221
const emptyDirs = async (dirs: string[]): Promise<void> => {
222222
dirs = dirs
223223
.filter(isString)
224-
.map(normalizePath)
224+
.map((s) => normalizePath(s))
225225
.reduce((dirs, dir) => {
226226
if (!dirs.includes(dir)) {
227227
dirs.push(dir);

src/compiler/sys/logger/test/terminal-logger.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('terminal-logger', () => {
4040
});
4141

4242
describe('basic logging functionality', () => {
43-
const setupConsoleMocks = setupConsoleMocker();
43+
const { setupConsoleMocks } = setupConsoleMocker();
4444

4545
function setup() {
4646
const { logMock, warnMock, errorMock } = setupConsoleMocks();

src/compiler/transformers/decorators-to-static/convert-decorators.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ import { watchDecoratorsToStatic } from './watch-decorator';
2727
export const convertDecoratorsToStatic = (
2828
config: d.Config,
2929
diagnostics: d.Diagnostic[],
30-
typeChecker: ts.TypeChecker
30+
typeChecker: ts.TypeChecker,
31+
program: ts.Program
3132
): ts.TransformerFactory<ts.SourceFile> => {
3233
return (transformCtx) => {
3334
const visit = (node: ts.Node): ts.VisitResult<ts.Node> => {
3435
if (ts.isClassDeclaration(node)) {
35-
return visitClassDeclaration(config, diagnostics, typeChecker, node);
36+
return visitClassDeclaration(config, diagnostics, typeChecker, program, node);
3637
}
3738
return ts.visitEachChild(node, visit, transformCtx);
3839
};
@@ -47,6 +48,7 @@ const visitClassDeclaration = (
4748
config: d.Config,
4849
diagnostics: d.Diagnostic[],
4950
typeChecker: ts.TypeChecker,
51+
program: ts.Program,
5052
classNode: ts.ClassDeclaration
5153
) => {
5254
const componentDecorator = retrieveTsDecorators(classNode)?.find(isDecoratorNamed('Component'));
@@ -69,10 +71,18 @@ const visitClassDeclaration = (
6971
const watchable = new Set<string>();
7072
// parse member decorators (Prop, State, Listen, Event, Method, Element and Watch)
7173
if (decoratedMembers.length > 0) {
72-
propDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, watchable, filteredMethodsAndFields);
74+
propDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, program, watchable, filteredMethodsAndFields);
7375
stateDecoratorsToStatic(decoratedMembers, watchable, filteredMethodsAndFields);
74-
eventDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, filteredMethodsAndFields);
75-
methodDecoratorsToStatic(config, diagnostics, classNode, decoratedMembers, typeChecker, filteredMethodsAndFields);
76+
eventDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, program, filteredMethodsAndFields);
77+
methodDecoratorsToStatic(
78+
config,
79+
diagnostics,
80+
classNode,
81+
decoratedMembers,
82+
typeChecker,
83+
program,
84+
filteredMethodsAndFields
85+
);
7686
elementDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, filteredMethodsAndFields);
7787
watchDecoratorsToStatic(config, diagnostics, decoratedMembers, watchable, filteredMethodsAndFields);
7888
listenDecoratorsToStatic(diagnostics, decoratedMembers, filteredMethodsAndFields);

src/compiler/transformers/decorators-to-static/event-decorator.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ export const eventDecoratorsToStatic = (
1717
diagnostics: d.Diagnostic[],
1818
decoratedProps: ts.ClassElement[],
1919
typeChecker: ts.TypeChecker,
20+
program: ts.Program,
2021
newMembers: ts.ClassElement[]
2122
) => {
2223
const events = decoratedProps
2324
.filter(ts.isPropertyDeclaration)
24-
.map((prop) => parseEventDecorator(diagnostics, typeChecker, prop))
25+
.map((prop) => parseEventDecorator(diagnostics, typeChecker, program, prop))
2526
.filter((ev) => !!ev);
2627

2728
if (events.length > 0) {
@@ -35,13 +36,15 @@ export const eventDecoratorsToStatic = (
3536
* @param diagnostics a list of diagnostics used as a part of the parsing process. Any parse errors/warnings shall be
3637
* added to this collection
3738
* @param typeChecker an instance of the TypeScript type checker, used to generate information about the `@Event()` and
39+
* @param program a {@link ts.Program} object
3840
* its surrounding context in the AST
3941
* @param prop the property on the Stencil component class that is decorated with `@Event()`
4042
* @returns generated metadata for the class member decorated by `@Event()`, or `null` if none could be derived
4143
*/
4244
const parseEventDecorator = (
4345
diagnostics: d.Diagnostic[],
4446
typeChecker: ts.TypeChecker,
47+
program: ts.Program,
4548
prop: ts.PropertyDeclaration
4649
): d.ComponentCompilerStaticEvent | null => {
4750
const eventDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed('Event'));
@@ -68,7 +71,7 @@ const parseEventDecorator = (
6871
cancelable: eventOpts && typeof eventOpts.cancelable === 'boolean' ? eventOpts.cancelable : true,
6972
composed: eventOpts && typeof eventOpts.composed === 'boolean' ? eventOpts.composed : true,
7073
docs: serializeSymbol(typeChecker, symbol),
71-
complexType: getComplexType(typeChecker, prop),
74+
complexType: getComplexType(typeChecker, program, prop),
7275
};
7376
validateReferences(diagnostics, eventMeta.complexType.references, prop.type);
7477
return eventMeta;
@@ -85,19 +88,21 @@ export const getEventName = (eventOptions: d.EventOptions, memberName: string) =
8588
/**
8689
* Derive Stencil's class member type metadata from a node in the AST
8790
* @param typeChecker the TypeScript type checker
91+
* @param program a {@link ts.Program} object
8892
* @param node the node in the AST to generate metadata for
8993
* @returns the generated metadata
9094
*/
9195
const getComplexType = (
9296
typeChecker: ts.TypeChecker,
97+
program: ts.Program,
9398
node: ts.PropertyDeclaration
9499
): d.ComponentCompilerPropertyComplexType => {
95100
const sourceFile = node.getSourceFile();
96101
const eventType = node.type ? getEventType(node.type) : null;
97102
return {
98103
original: eventType ? eventType.getText() : 'any',
99104
resolved: eventType ? resolveType(typeChecker, typeChecker.getTypeFromTypeNode(eventType)) : 'any',
100-
references: eventType ? getAttributeTypeInfo(eventType, sourceFile) : {},
105+
references: eventType ? getAttributeTypeInfo(eventType, sourceFile, typeChecker, program) : {},
101106
};
102107
};
103108

src/compiler/transformers/decorators-to-static/method-decorator.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ export const methodDecoratorsToStatic = (
2323
cmpNode: ts.ClassDeclaration,
2424
decoratedProps: ts.ClassElement[],
2525
typeChecker: ts.TypeChecker,
26+
program: ts.Program,
2627
newMembers: ts.ClassElement[]
2728
) => {
2829
const tsSourceFile = cmpNode.getSourceFile();
2930
const methods = decoratedProps
3031
.filter(ts.isMethodDeclaration)
31-
.map((method) => parseMethodDecorator(config, diagnostics, tsSourceFile, typeChecker, method))
32+
.map((method) => parseMethodDecorator(config, diagnostics, tsSourceFile, typeChecker, program, method))
3233
.filter((method) => !!method);
3334

3435
if (methods.length > 0) {
@@ -41,6 +42,7 @@ const parseMethodDecorator = (
4142
diagnostics: d.Diagnostic[],
4243
tsSourceFile: ts.SourceFile,
4344
typeChecker: ts.TypeChecker,
45+
program: ts.Program,
4446
method: ts.MethodDeclaration
4547
): ts.PropertyAssignment | null => {
4648
const methodDecorator = retrieveTsDecorators(method)?.find(isDecoratorNamed('Method'));
@@ -92,8 +94,8 @@ const parseMethodDecorator = (
9294
signature: signatureString,
9395
parameters: signature.parameters.map((symbol) => serializeSymbol(typeChecker, symbol)),
9496
references: {
95-
...getAttributeTypeInfo(returnTypeNode, tsSourceFile),
96-
...getAttributeTypeInfo(method, tsSourceFile),
97+
...getAttributeTypeInfo(returnTypeNode, tsSourceFile, typeChecker, program),
98+
...getAttributeTypeInfo(method, tsSourceFile, typeChecker, program),
9799
},
98100
return: returnString,
99101
},

0 commit comments

Comments
 (0)