Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs): enrich type information for docs-json Output Target #4212

Merged
merged 1 commit into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ test/**/components.d.ts
test/end-to-end/screenshot/
test/end-to-end/docs.d.ts
test/end-to-end/docs.json
test/docs-json/docs.d.ts
test/docs-json/docs.json

# minified angular that exists in the test directory
test/karma/test-app/assets/angular.min.js
Expand Down
83 changes: 82 additions & 1 deletion BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Legacy Browser Support Removed](#legacy-browser-support-removed)
- [Legacy Cache Stats Config Flag Removed](#legacy-cache-stats-config-flag-removed)
- [Drop Node 14 Support](#drop-node-14-support)
- [Information included in JSON documentation expanded](#information-included-in-docs-json-expanded)

### General

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

#### Information included in `docs-json` expanded

For Stencil v4 the information included in the output of the `docs-json` output
target was expanded to include more information about the types of properties
and methods on Stencil components.

For more context on this change, see the [documentation for the new
`supplementalPublicTypes`](https://stenciljs.com/docs/docs-json#supplementalpublictypes)
option for the JSON documentation output target.

##### `JsonDocsEvent`

The JSON-formatted documentation for an `@Event` now includes a field called
`complexType` which includes more information about the types referenced in the
type declarations for that property.

Here's an example of what this looks like for the [ionBreakpointDidChange
event](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/modal/modal.tsx#L289-L292)
on the `Modal` component in Ionic Framework:

```json
{
"complexType": {
"original": "ModalBreakpointChangeEventDetail",
"resolved": "ModalBreakpointChangeEventDetail",
"references": {
"ModalBreakpointChangeEventDetail": {
"location": "import",
"path": "./modal-interface",
"id": "src/components/modal/modal.tsx::ModalBreakpointChangeEventDetail"
}
}
}
}
```

##### `JsonDocsMethod`

The JSON-formatted documentation for a `@Method` now includes a field called
`complexType` which includes more information about the types referenced in
the type declarations for that property.

Here's an example of what this looks like for the [open
method](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/select/select.tsx#L261-L313)
on the `Select` component in Ionic Framework:

```json
{
"complexType": {
"signature": "(event?: UIEvent) => Promise<any>",
"parameters": [
{
"tags": [
{
"name": "param",
"text": "event The user interface event that called the open."
}
],
"text": "The user interface event that called the open."
}
],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
},
"UIEvent": {
"location": "global",
"id": "global::UIEvent"
},
"HTMLElement": {
"location": "global",
"id": "global::HTMLElement"
}
},
"return": "Promise<any>"
}
}
```

## Stencil v3.0.0

* [General](#general)
Expand Down Expand Up @@ -1093,4 +1174,4 @@ When running Jest directly, previously most of Jest had to be manually configure
- "jsx"
- ]
}
```
```
44 changes: 40 additions & 4 deletions src/compiler/docs/generate-doc-data.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { flatOne, normalizePath, sortBy, unique } from '@utils';
import { flatOne, isOutputTargetDocsJson, normalizePath, sortBy, unique } from '@utils';
import { basename, dirname, join, relative } from 'path';

import type * as d from '../../declarations';
import { JsonDocsValue } from '../../declarations';
import { typescriptVersion, version } from '../../version';
import { getBuildTimestamp } from '../build/build-ctx';
import { addFileToLibrary, getTypeLibrary } from '../transformers/type-library';
import { AUTO_GENERATE_COMMENT } from './constants';

/**
* Generate metadata that will be used to generate any given documentation-related output target(s)
* Generate metadata that will be used to generate any given documentation-related
* output target(s)
*
* @param config the configuration associated with the current Stencil task run
* @param compilerCtx the current compiler context
* @param buildCtx the build context for the current Stencil task run
Expand All @@ -19,6 +22,18 @@ export const generateDocData = async (
compilerCtx: d.CompilerCtx,
buildCtx: d.BuildCtx
): Promise<d.JsonDocs> => {
const jsonOutputTargets = config.outputTargets.filter(isOutputTargetDocsJson);
const supplementalPublicTypes = findSupplementalPublicTypes(jsonOutputTargets);

if (supplementalPublicTypes !== '') {
// if supplementalPublicTypes is set then we want to add all the public
// types in that file to the type library so that output targets producing
// documentation can make use of that data later.
addFileToLibrary(config, supplementalPublicTypes);
}

const typeLibrary = getTypeLibrary();

return {
timestamp: getBuildTimestamp(),
compiler: {
Expand All @@ -27,11 +42,28 @@ export const generateDocData = async (
typescriptVersion,
},
components: await getDocsComponents(config, compilerCtx, buildCtx),
typeLibrary,
};
};

/**
* If the `supplementalPublicTypes` option is set on one output target, find that value and return it.
*
* @param outputTargets an array of docs-json output targets
* @returns the first value encountered for supplementalPublicTypes or an empty string
*/
function findSupplementalPublicTypes(outputTargets: d.OutputTargetDocsJson[]): string {
for (const docsJsonOT of outputTargets) {
if (docsJsonOT.supplementalPublicTypes) {
return docsJsonOT.supplementalPublicTypes;
}
}
return '';
}

/**
* Derive the metadata for each Stencil component
*
* @param config the configuration associated with the current Stencil task run
* @param compilerCtx the current compiler context
* @param buildCtx the build context for the current Stencil task run
Expand All @@ -50,11 +82,12 @@ const getDocsComponents = async (
const usagesDir = normalizePath(join(dirPath, 'usage'));
const readme = await getUserReadmeContent(compilerCtx, readmePath);
const usage = await generateUsages(compilerCtx, usagesDir);

return moduleFile.cmps
.filter((cmp: d.ComponentCompilerMeta) => !cmp.internal && !cmp.isCollectionDependency)
.map((cmp: d.ComponentCompilerMeta) => ({
dirPath,
filePath: relative(config.rootDir, filePath),
filePath: normalizePath(relative(config.rootDir, filePath), false),
fileName: basename(filePath),
readmePath,
usagesDir,
Expand Down Expand Up @@ -140,6 +173,7 @@ const getRealProperties = (properties: d.ComponentCompilerProperty[]): d.JsonDoc
.map((member) => ({
name: member.name,
type: member.complexType.resolved,
complexType: member.complexType,
mutable: member.mutable,
attr: member.attribute,
reflectToAttr: !!member.reflect,
Expand Down Expand Up @@ -228,6 +262,7 @@ const getDocsMethods = (methods: d.ComponentCompilerMethod[]): d.JsonDocsMethod[
.map((t) => t.text)
.join('\n'),
},
complexType: member.complexType,
signature: `${member.name}${member.complexType.signature}`,
parameters: [], // TODO
docs: member.docs.text,
Expand All @@ -243,6 +278,7 @@ const getDocsEvents = (events: d.ComponentCompilerEvent[]): d.JsonDocsEvent[] =>
event: eventMeta.name,
detail: eventMeta.complexType.resolved,
bubbles: eventMeta.bubbles,
complexType: eventMeta.complexType,
cancelable: eventMeta.cancelable,
composed: eventMeta.composed,
docs: eventMeta.docs.text,
Expand Down Expand Up @@ -341,7 +377,7 @@ const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: stri
* @param jsdoc the JSDoc associated with the component's declaration
* @returns the generated documentation
*/
const generateDocs = (readme: string, jsdoc: d.CompilerJsDoc): string => {
const generateDocs = (readme: string | undefined, jsdoc: d.CompilerJsDoc): string => {
const docs = jsdoc.text;
if (docs !== '' || !readme) {
// just return the existing docs if they exist. these would have been captured earlier in the compilation process.
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/sys/in-memory-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export const createInMemoryFs = (sys: d.CompilerSystem) => {
const emptyDirs = async (dirs: string[]): Promise<void> => {
dirs = dirs
.filter(isString)
.map(normalizePath)
.map((s) => normalizePath(s))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking, is this change needed? Doesn't what we have before do the same thing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: NVM - I found the change later in the review to normalizePath, and GH didn't delete it (or that's my story at least 😏 )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I wish this could stay as a unary function here, but I think the issue here is that .map will pass more than one argument and the type of the second argument won't match the second argument of normalizePath anymore, so arrow function it is

.reduce((dirs, dir) => {
if (!dirs.includes(dir)) {
dirs.push(dir);
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/sys/logger/test/terminal-logger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('terminal-logger', () => {
});

describe('basic logging functionality', () => {
const setupConsoleMocks = setupConsoleMocker();
const { setupConsoleMocks } = setupConsoleMocker();

function setup() {
const { logMock, warnMock, errorMock } = setupConsoleMocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ import { watchDecoratorsToStatic } from './watch-decorator';
export const convertDecoratorsToStatic = (
config: d.Config,
diagnostics: d.Diagnostic[],
typeChecker: ts.TypeChecker
typeChecker: ts.TypeChecker,
program: ts.Program
): ts.TransformerFactory<ts.SourceFile> => {
return (transformCtx) => {
const visit = (node: ts.Node): ts.VisitResult<ts.Node> => {
if (ts.isClassDeclaration(node)) {
return visitClassDeclaration(config, diagnostics, typeChecker, node);
return visitClassDeclaration(config, diagnostics, typeChecker, program, node);
}
return ts.visitEachChild(node, visit, transformCtx);
};
Expand All @@ -47,6 +48,7 @@ const visitClassDeclaration = (
config: d.Config,
diagnostics: d.Diagnostic[],
typeChecker: ts.TypeChecker,
program: ts.Program,
classNode: ts.ClassDeclaration
) => {
const componentDecorator = retrieveTsDecorators(classNode)?.find(isDecoratorNamed('Component'));
Expand All @@ -69,10 +71,18 @@ const visitClassDeclaration = (
const watchable = new Set<string>();
// parse member decorators (Prop, State, Listen, Event, Method, Element and Watch)
if (decoratedMembers.length > 0) {
propDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, watchable, filteredMethodsAndFields);
propDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, program, watchable, filteredMethodsAndFields);
stateDecoratorsToStatic(decoratedMembers, watchable, filteredMethodsAndFields);
eventDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, filteredMethodsAndFields);
methodDecoratorsToStatic(config, diagnostics, classNode, decoratedMembers, typeChecker, filteredMethodsAndFields);
eventDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, program, filteredMethodsAndFields);
methodDecoratorsToStatic(
config,
diagnostics,
classNode,
decoratedMembers,
typeChecker,
program,
filteredMethodsAndFields
);
elementDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, filteredMethodsAndFields);
watchDecoratorsToStatic(config, diagnostics, decoratedMembers, watchable, filteredMethodsAndFields);
listenDecoratorsToStatic(diagnostics, decoratedMembers, filteredMethodsAndFields);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ export const eventDecoratorsToStatic = (
diagnostics: d.Diagnostic[],
decoratedProps: ts.ClassElement[],
typeChecker: ts.TypeChecker,
program: ts.Program,
newMembers: ts.ClassElement[]
) => {
const events = decoratedProps
.filter(ts.isPropertyDeclaration)
.map((prop) => parseEventDecorator(diagnostics, typeChecker, prop))
.map((prop) => parseEventDecorator(diagnostics, typeChecker, program, prop))
.filter((ev) => !!ev);

if (events.length > 0) {
Expand All @@ -35,13 +36,15 @@ export const eventDecoratorsToStatic = (
* @param diagnostics a list of diagnostics used as a part of the parsing process. Any parse errors/warnings shall be
* added to this collection
* @param typeChecker an instance of the TypeScript type checker, used to generate information about the `@Event()` and
* @param program a {@link ts.Program} object
* its surrounding context in the AST
* @param prop the property on the Stencil component class that is decorated with `@Event()`
* @returns generated metadata for the class member decorated by `@Event()`, or `null` if none could be derived
*/
const parseEventDecorator = (
diagnostics: d.Diagnostic[],
typeChecker: ts.TypeChecker,
program: ts.Program,
prop: ts.PropertyDeclaration
): d.ComponentCompilerStaticEvent | null => {
const eventDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed('Event'));
Expand All @@ -68,7 +71,7 @@ const parseEventDecorator = (
cancelable: eventOpts && typeof eventOpts.cancelable === 'boolean' ? eventOpts.cancelable : true,
composed: eventOpts && typeof eventOpts.composed === 'boolean' ? eventOpts.composed : true,
docs: serializeSymbol(typeChecker, symbol),
complexType: getComplexType(typeChecker, prop),
complexType: getComplexType(typeChecker, program, prop),
};
validateReferences(diagnostics, eventMeta.complexType.references, prop.type);
return eventMeta;
Expand All @@ -85,19 +88,21 @@ export const getEventName = (eventOptions: d.EventOptions, memberName: string) =
/**
* Derive Stencil's class member type metadata from a node in the AST
* @param typeChecker the TypeScript type checker
* @param program a {@link ts.Program} object
* @param node the node in the AST to generate metadata for
* @returns the generated metadata
*/
const getComplexType = (
typeChecker: ts.TypeChecker,
program: ts.Program,
node: ts.PropertyDeclaration
): d.ComponentCompilerPropertyComplexType => {
const sourceFile = node.getSourceFile();
const eventType = node.type ? getEventType(node.type) : null;
return {
original: eventType ? eventType.getText() : 'any',
resolved: eventType ? resolveType(typeChecker, typeChecker.getTypeFromTypeNode(eventType)) : 'any',
references: eventType ? getAttributeTypeInfo(eventType, sourceFile) : {},
references: eventType ? getAttributeTypeInfo(eventType, sourceFile, typeChecker, program) : {},
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ export const methodDecoratorsToStatic = (
cmpNode: ts.ClassDeclaration,
decoratedProps: ts.ClassElement[],
typeChecker: ts.TypeChecker,
program: ts.Program,
newMembers: ts.ClassElement[]
) => {
const tsSourceFile = cmpNode.getSourceFile();
const methods = decoratedProps
.filter(ts.isMethodDeclaration)
.map((method) => parseMethodDecorator(config, diagnostics, tsSourceFile, typeChecker, method))
.map((method) => parseMethodDecorator(config, diagnostics, tsSourceFile, typeChecker, program, method))
.filter((method) => !!method);

if (methods.length > 0) {
Expand All @@ -41,6 +42,7 @@ const parseMethodDecorator = (
diagnostics: d.Diagnostic[],
tsSourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker,
program: ts.Program,
method: ts.MethodDeclaration
): ts.PropertyAssignment | null => {
const methodDecorator = retrieveTsDecorators(method)?.find(isDecoratorNamed('Method'));
Expand Down Expand Up @@ -92,8 +94,8 @@ const parseMethodDecorator = (
signature: signatureString,
parameters: signature.parameters.map((symbol) => serializeSymbol(typeChecker, symbol)),
references: {
...getAttributeTypeInfo(returnTypeNode, tsSourceFile),
...getAttributeTypeInfo(method, tsSourceFile),
...getAttributeTypeInfo(returnTypeNode, tsSourceFile, typeChecker, program),
...getAttributeTypeInfo(method, tsSourceFile, typeChecker, program),
},
return: returnString,
},
Expand Down
Loading