Skip to content

Commit 2db4f4d

Browse files
authoredOct 5, 2022
feat(docs-readme): add overview to readme (#3635)
this commit adds an 'Overview' section to the generated `README.md` file for a component. this implementation is simple (and arguably naive) intentionally. there is quite a bit that folks could do in the context of a JSDoc. rather than attempting to capture every use case and potentially overcomplicate our solution, we start simple and can iterate from there. this commit also adds JSDoc to a portion of the codebase that is related to the generation of documentation
1 parent cb4ba58 commit 2db4f4d

File tree

9 files changed

+207
-9
lines changed

9 files changed

+207
-9
lines changed
 

‎src/compiler/build/build-ctx.ts

+5
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ export class BuildContext implements d.BuildCtx {
196196
}
197197
}
198198

199+
/**
200+
* Generate a timestamp of the format `YYYY-MM-DDThh:mm:ss`, using the number of seconds that have elapsed since
201+
* January 01, 1970, and the time this function was called
202+
* @returns the generated timestamp
203+
*/
199204
export const getBuildTimestamp = () => {
200205
const d = new Date();
201206

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

+79-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import { typescriptVersion, version } from '../../version';
77
import { getBuildTimestamp } from '../build/build-ctx';
88
import { AUTO_GENERATE_COMMENT } from './constants';
99

10+
/**
11+
* Generate metadata that will be used to generate any given documentation-related output target(s)
12+
* @param config the configuration associated with the current Stencil task run
13+
* @param compilerCtx the current compiler context
14+
* @param buildCtx the build context for the current Stencil task run
15+
* @returns the generated metadata
16+
*/
1017
export const generateDocData = async (
1118
config: d.ValidatedConfig,
1219
compilerCtx: d.CompilerCtx,
@@ -23,6 +30,13 @@ export const generateDocData = async (
2330
};
2431
};
2532

33+
/**
34+
* Derive the metadata for each Stencil component
35+
* @param config the configuration associated with the current Stencil task run
36+
* @param compilerCtx the current compiler context
37+
* @param buildCtx the build context for the current Stencil task run
38+
* @returns the derived metadata
39+
*/
2640
const getDocsComponents = async (
2741
config: d.ValidatedConfig,
2842
compilerCtx: d.CompilerCtx,
@@ -37,8 +51,8 @@ const getDocsComponents = async (
3751
const readme = await getUserReadmeContent(compilerCtx, readmePath);
3852
const usage = await generateUsages(compilerCtx, usagesDir);
3953
return moduleFile.cmps
40-
.filter((cmp) => !cmp.internal && !cmp.isCollectionDependency)
41-
.map((cmp) => ({
54+
.filter((cmp: d.ComponentCompilerMeta) => !cmp.internal && !cmp.isCollectionDependency)
55+
.map((cmp: d.ComponentCompilerMeta) => ({
4256
dirPath,
4357
filePath: relative(config.rootDir, filePath),
4458
fileName: basename(filePath),
@@ -69,9 +83,12 @@ const getDocsComponents = async (
6983
return sortBy(flatOne(results), (cmp) => cmp.tag);
7084
};
7185

72-
const buildDocsDepGraph = (cmp: d.ComponentCompilerMeta, cmps: d.ComponentCompilerMeta[]) => {
86+
const buildDocsDepGraph = (
87+
cmp: d.ComponentCompilerMeta,
88+
cmps: d.ComponentCompilerMeta[]
89+
): d.JsonDocsDependencyGraph => {
7390
const dependencies: d.JsonDocsDependencyGraph = {};
74-
function walk(tagName: string) {
91+
function walk(tagName: string): void {
7592
if (!dependencies[tagName]) {
7693
const cmp = cmps.find((c) => c.tagName === tagName);
7794
const deps = cmp.directDependencies;
@@ -94,6 +111,11 @@ const buildDocsDepGraph = (cmp: d.ComponentCompilerMeta, cmps: d.ComponentCompil
94111
return dependencies;
95112
};
96113

114+
/**
115+
* Determines the encapsulation string to use, based on the provided compiler metadata
116+
* @param cmp the metadata for a single component
117+
* @returns the encapsulation level, expressed as a string
118+
*/
97119
const getDocsEncapsulation = (cmp: d.ComponentCompilerMeta): 'shadow' | 'scoped' | 'none' => {
98120
if (cmp.encapsulation === 'shadow') {
99121
return 'shadow';
@@ -251,7 +273,13 @@ const getDocsListeners = (listeners: d.ComponentCompilerListener[]): d.JsonDocsL
251273
}));
252274
};
253275

254-
const getDocsDeprecationText = (tags: d.JsonDocsTag[]) => {
276+
/**
277+
* Get the text associated with a `@deprecated` tag, if one exists
278+
* @param tags the tags associated with a JSDoc block on a node in the AST
279+
* @returns the text associated with the first found `@deprecated` tag. If a `@deprecated` tag exists but does not
280+
* have associated text, an empty string is returned. If no such tag is found, return `undefined`
281+
*/
282+
const getDocsDeprecationText = (tags: d.JsonDocsTag[]): string | undefined => {
255283
const deprecation = tags.find((t) => t.name === 'deprecated');
256284
if (deprecation) {
257285
return deprecation.text || '';
@@ -284,9 +312,20 @@ export const getNameText = (name: string, tags: d.JsonDocsTag[]) => {
284312
});
285313
};
286314

287-
const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: string) => {
315+
/**
316+
* Attempts to read a pre-existing README.md file from disk, returning any content generated by the user.
317+
*
318+
* For simplicity's sake, it is assumed that all user-generated content will fall before {@link AUTO_GENERATE_COMMENT}
319+
*
320+
* @param compilerCtx the current compiler context
321+
* @param readmePath the path to the README file to read
322+
* @returns the user generated content that occurs before {@link AUTO_GENERATE_COMMENT}. If no user generated content
323+
* exists, or if there was an issue reading the file, return `undefined`
324+
*/
325+
const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: string): Promise<string | undefined> => {
288326
try {
289327
const existingContent = await compilerCtx.fs.readFile(readmePath);
328+
// subtract one to get everything up to, but not including the auto generated comment
290329
const userContentIndex = existingContent.indexOf(AUTO_GENERATE_COMMENT) - 1;
291330
if (userContentIndex >= 0) {
292331
return existingContent.substring(0, userContentIndex);
@@ -295,31 +334,63 @@ const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: stri
295334
return undefined;
296335
};
297336

298-
const generateDocs = (readme: string, jsdoc: d.CompilerJsDoc) => {
337+
/**
338+
* Generate documentation for a given component based on the provided JSDoc and README contents
339+
* @param readme the contents of a component's README file, without any autogenerated contents
340+
* @param jsdoc the JSDoc associated with the component's declaration
341+
* @returns the generated documentation
342+
*/
343+
const generateDocs = (readme: string, jsdoc: d.CompilerJsDoc): string => {
299344
const docs = jsdoc.text;
300345
if (docs !== '' || !readme) {
346+
// just return the existing docs if they exist. these would have been captured earlier in the compilation process.
347+
// if they don't exist, and there's no README to process, return an empty string.
301348
return docs;
302349
}
303350

351+
/**
352+
* Parse the README, storing the first section of content.
353+
* Content is defined as the area between two non-consecutive lines that start with a '#':
354+
* ```
355+
* # Header 1
356+
* This is some content
357+
* # Header 2
358+
* This is more content
359+
* # Header 3
360+
* Again, content
361+
* ```
362+
* In the example above, this chunk of code is designed to capture "This is some content"
363+
*/
304364
let isContent = false;
305365
const lines = readme.split('\n');
306366
const contentLines = [];
307367
for (const line of lines) {
308368
const isHeader = line.startsWith('#');
309369
if (isHeader && isContent) {
370+
// we were actively parsing content, but found a new header, break out
310371
break;
311372
}
312373
if (!isHeader && !isContent) {
374+
// we've found content for the first time, set this sentinel to `true`
313375
isContent = true;
314376
}
315377
if (isContent) {
378+
// we're actively parsing the first found block of content, add it to our list for later
316379
contentLines.push(line);
317380
}
318381
}
319382
return contentLines.join('\n').trim();
320383
};
321384

322-
const generateUsages = async (compilerCtx: d.CompilerCtx, usagesDir: string) => {
385+
/**
386+
* This function is responsible for reading the contents of all markdown files in a provided `usage` directory and
387+
* returning their contents
388+
* @param compilerCtx the current compiler context
389+
* @param usagesDir the directory to read usage markdown files from
390+
* @returns an object that maps the filename containing the usage example, to the file's contents. If an error occurs,
391+
* an empty object is returned.
392+
*/
393+
const generateUsages = async (compilerCtx: d.CompilerCtx, usagesDir: string): Promise<d.JsonDocsUsage> => {
323394
const rtn: d.JsonDocsUsage = {};
324395

325396
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Generate an 'Overview' section for a markdown file
3+
* @param overview a component-level comment string to place in a markdown file
4+
* @returns The generated Overview section. If the provided overview is empty, return an empty list
5+
*/
6+
export const overviewToMarkdown = (overview: string): ReadonlyArray<string> => {
7+
if (!overview) {
8+
return [];
9+
}
10+
11+
const content: string[] = [];
12+
content.push(`## Overview`);
13+
content.push('');
14+
content.push(`${overview.trim()}`);
15+
content.push('');
16+
17+
return content;
18+
};

‎src/compiler/docs/readme/output-docs.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { stylesToMarkdown } from './markdown-css-props';
66
import { depsToMarkdown } from './markdown-dependencies';
77
import { eventsToMarkdown } from './markdown-events';
88
import { methodsToMarkdown } from './markdown-methods';
9+
import { overviewToMarkdown } from './markdown-overview';
910
import { partsToMarkdown } from './markdown-parts';
1011
import { propsToMarkdown } from './markdown-props';
1112
import { slotsToMarkdown } from './markdown-slots';
@@ -54,6 +55,7 @@ export const generateMarkdown = (
5455
'',
5556
'',
5657
...getDocsDeprecation(cmp),
58+
...overviewToMarkdown(cmp.docs),
5759
...usageToMarkdown(cmp.usage),
5860
...propsToMarkdown(cmp.props),
5961
...eventsToMarkdown(cmp.events),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { overviewToMarkdown } from '../readme/markdown-overview';
2+
3+
describe('markdown-overview', () => {
4+
describe('overviewToMarkdown', () => {
5+
it('returns no overview if no docs exist', () => {
6+
const generatedOverview = overviewToMarkdown('').join('\n');
7+
8+
expect(generatedOverview).toBe('');
9+
});
10+
11+
it('generates a single line overview', () => {
12+
const generatedOverview = overviewToMarkdown('This is a custom button component').join('\n');
13+
14+
expect(generatedOverview).toBe(`## Overview
15+
16+
This is a custom button component
17+
`);
18+
});
19+
20+
it('generates a multi-line overview', () => {
21+
const description = `This is a custom button component.
22+
It is to be used throughout the design system.
23+
24+
This is a comment followed by a newline.
25+
`;
26+
const generatedOverview = overviewToMarkdown(description).join('\n');
27+
28+
expect(generatedOverview).toBe(`## Overview
29+
30+
This is a custom button component.
31+
It is to be used throughout the design system.
32+
33+
This is a comment followed by a newline.
34+
`);
35+
});
36+
37+
it('trims all leading newlines & leaves one at the end', () => {
38+
const description = `
39+
This is a custom button component.
40+
41+
`;
42+
const generatedOverview = overviewToMarkdown(description).join('\n');
43+
44+
expect(generatedOverview).toBe(`## Overview
45+
46+
This is a custom button component.
47+
`);
48+
});
49+
});
50+
});

‎src/compiler/output-targets/output-docs.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,17 @@ import {
1313
isOutputTargetDocsVscode,
1414
} from './output-utils';
1515

16-
export const outputDocs = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => {
16+
/**
17+
* Generate documentation-related output targets
18+
* @param config the configuration associated with the current Stencil task run
19+
* @param compilerCtx the current compiler context
20+
* @param buildCtx the build context for the current Stencil task run
21+
*/
22+
export const outputDocs = async (
23+
config: d.ValidatedConfig,
24+
compilerCtx: d.CompilerCtx,
25+
buildCtx: d.BuildCtx
26+
): Promise<void> => {
1727
if (!config.buildDocs) {
1828
return;
1929
}

‎src/declarations/stencil-private.ts

+18
Original file line numberDiff line numberDiff line change
@@ -935,13 +935,31 @@ export interface ComponentCompilerState {
935935
name: string;
936936
}
937937

938+
/**
939+
* Representation of JSDoc that is pulled off a node in the AST
940+
*/
938941
export interface CompilerJsDoc {
942+
/**
943+
* The text associated with the JSDoc
944+
*/
939945
text: string;
946+
/**
947+
* Tags included in the JSDoc
948+
*/
940949
tags: CompilerJsDocTagInfo[];
941950
}
942951

952+
/**
953+
* Representation of a tag that exists in a JSDoc
954+
*/
943955
export interface CompilerJsDocTagInfo {
956+
/**
957+
* The name of the tag - e.g. `@deprecated`
958+
*/
944959
name: string;
960+
/**
961+
* Additional text that is associated with the tag - e.g. `@deprecated use v2 of this API`
962+
*/
945963
text?: string;
946964
}
947965

‎src/declarations/stencil-public-docs.ts

+20
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ export interface JsonDocsValue {
4747
type: string;
4848
}
4949

50+
/**
51+
* A mapping of file names to their contents.
52+
*
53+
* This type is meant to be used when reading one or more usage markdown files associated with a component. For the
54+
* given directory structure:
55+
* ```
56+
* src/components/my-component
57+
* ├── my-component.tsx
58+
* └── usage
59+
* ├── bar.md
60+
* └── foo.md
61+
* ```
62+
* an instance of this type would include the name of the markdown file, mapped to its contents:
63+
* ```ts
64+
* {
65+
* 'bar': STRING_CONTENTS_OF_BAR.MD
66+
* 'foo': STRING_CONTENTS_OF_FOO.MD
67+
* }
68+
* ```
69+
*/
5070
export interface JsonDocsUsage {
5171
[key: string]: string;
5272
}

‎test/end-to-end/src/car-list/readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
<!-- Auto Generated Below -->
66

77

8+
## Overview
9+
10+
Component that helps display a list of cars
11+
812
## Properties
913

1014
| Property | Attribute | Description | Type | Default |

0 commit comments

Comments
 (0)
Please sign in to comment.