-
Notifications
You must be signed in to change notification settings - Fork 790
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-readme): add overview to readme #3635
Changes from all commits
61ac6ea
5aa96f2
de1e73f
9e41a13
ed21aac
a099df2
89620f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,13 @@ import { typescriptVersion, version } from '../../version'; | |
import { getBuildTimestamp } from '../build/build-ctx'; | ||
import { AUTO_GENERATE_COMMENT } from './constants'; | ||
|
||
/** | ||
* 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 | ||
* @returns the generated metadata | ||
*/ | ||
export const generateDocData = async ( | ||
config: d.ValidatedConfig, | ||
compilerCtx: d.CompilerCtx, | ||
|
@@ -23,6 +30,13 @@ export const generateDocData = async ( | |
}; | ||
}; | ||
|
||
/** | ||
* 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 | ||
* @returns the derived metadata | ||
*/ | ||
const getDocsComponents = async ( | ||
config: d.ValidatedConfig, | ||
compilerCtx: d.CompilerCtx, | ||
|
@@ -37,8 +51,8 @@ const getDocsComponents = async ( | |
const readme = await getUserReadmeContent(compilerCtx, readmePath); | ||
const usage = await generateUsages(compilerCtx, usagesDir); | ||
return moduleFile.cmps | ||
.filter((cmp) => !cmp.internal && !cmp.isCollectionDependency) | ||
.map((cmp) => ({ | ||
.filter((cmp: d.ComponentCompilerMeta) => !cmp.internal && !cmp.isCollectionDependency) | ||
.map((cmp: d.ComponentCompilerMeta) => ({ | ||
dirPath, | ||
filePath: relative(config.rootDir, filePath), | ||
fileName: basename(filePath), | ||
|
@@ -69,9 +83,12 @@ const getDocsComponents = async ( | |
return sortBy(flatOne(results), (cmp) => cmp.tag); | ||
}; | ||
|
||
const buildDocsDepGraph = (cmp: d.ComponentCompilerMeta, cmps: d.ComponentCompilerMeta[]) => { | ||
const buildDocsDepGraph = ( | ||
cmp: d.ComponentCompilerMeta, | ||
cmps: d.ComponentCompilerMeta[] | ||
): d.JsonDocsDependencyGraph => { | ||
const dependencies: d.JsonDocsDependencyGraph = {}; | ||
function walk(tagName: string) { | ||
function walk(tagName: string): void { | ||
if (!dependencies[tagName]) { | ||
const cmp = cmps.find((c) => c.tagName === tagName); | ||
const deps = cmp.directDependencies; | ||
|
@@ -94,6 +111,11 @@ const buildDocsDepGraph = (cmp: d.ComponentCompilerMeta, cmps: d.ComponentCompil | |
return dependencies; | ||
}; | ||
|
||
/** | ||
* Determines the encapsulation string to use, based on the provided compiler metadata | ||
* @param cmp the metadata for a single component | ||
* @returns the encapsulation level, expressed as a string | ||
*/ | ||
const getDocsEncapsulation = (cmp: d.ComponentCompilerMeta): 'shadow' | 'scoped' | 'none' => { | ||
if (cmp.encapsulation === 'shadow') { | ||
return 'shadow'; | ||
|
@@ -251,7 +273,13 @@ const getDocsListeners = (listeners: d.ComponentCompilerListener[]): d.JsonDocsL | |
})); | ||
}; | ||
|
||
const getDocsDeprecationText = (tags: d.JsonDocsTag[]) => { | ||
/** | ||
* Get the text associated with a `@deprecated` tag, if one exists | ||
* @param tags the tags associated with a JSDoc block on a node in the AST | ||
* @returns the text associated with the first found `@deprecated` tag. If a `@deprecated` tag exists but does not | ||
* have associated text, an empty string is returned. If no such tag is found, return `undefined` | ||
*/ | ||
const getDocsDeprecationText = (tags: d.JsonDocsTag[]): string | undefined => { | ||
const deprecation = tags.find((t) => t.name === 'deprecated'); | ||
if (deprecation) { | ||
return deprecation.text || ''; | ||
|
@@ -284,9 +312,20 @@ export const getNameText = (name: string, tags: d.JsonDocsTag[]) => { | |
}); | ||
}; | ||
|
||
const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: string) => { | ||
/** | ||
* Attempts to read a pre-existing README.md file from disk, returning any content generated by the user. | ||
* | ||
* For simplicity's sake, it is assumed that all user-generated content will fall before {@link AUTO_GENERATE_COMMENT} | ||
* | ||
* @param compilerCtx the current compiler context | ||
* @param readmePath the path to the README file to read | ||
* @returns the user generated content that occurs before {@link AUTO_GENERATE_COMMENT}. If no user generated content | ||
* exists, or if there was an issue reading the file, return `undefined` | ||
*/ | ||
const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: string): Promise<string | undefined> => { | ||
try { | ||
const existingContent = await compilerCtx.fs.readFile(readmePath); | ||
// subtract one to get everything up to, but not including the auto generated comment | ||
const userContentIndex = existingContent.indexOf(AUTO_GENERATE_COMMENT) - 1; | ||
if (userContentIndex >= 0) { | ||
return existingContent.substring(0, userContentIndex); | ||
|
@@ -295,31 +334,63 @@ const getUserReadmeContent = async (compilerCtx: d.CompilerCtx, readmePath: stri | |
return undefined; | ||
}; | ||
|
||
const generateDocs = (readme: string, jsdoc: d.CompilerJsDoc) => { | ||
/** | ||
* Generate documentation for a given component based on the provided JSDoc and README contents | ||
* @param readme the contents of a component's README file, without any autogenerated contents | ||
* @param jsdoc the JSDoc associated with the component's declaration | ||
* @returns the generated documentation | ||
*/ | ||
const generateDocs = (readme: string, 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. | ||
// if they don't exist, and there's no README to process, return an empty string. | ||
return docs; | ||
} | ||
|
||
/** | ||
* Parse the README, storing the first section of content. | ||
* Content is defined as the area between two non-consecutive lines that start with a '#': | ||
* ``` | ||
* # Header 1 | ||
* This is some content | ||
* # Header 2 | ||
* This is more content | ||
* # Header 3 | ||
* Again, content | ||
* ``` | ||
* In the example above, this chunk of code is designed to capture "This is some content" | ||
*/ | ||
let isContent = false; | ||
const lines = readme.split('\n'); | ||
const contentLines = []; | ||
for (const line of lines) { | ||
const isHeader = line.startsWith('#'); | ||
if (isHeader && isContent) { | ||
// we were actively parsing content, but found a new header, break out | ||
break; | ||
} | ||
if (!isHeader && !isContent) { | ||
// we've found content for the first time, set this sentinel to `true` | ||
isContent = true; | ||
} | ||
if (isContent) { | ||
// we're actively parsing the first found block of content, add it to our list for later | ||
contentLines.push(line); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice, state machine parser! kind of neat to find |
||
} | ||
return contentLines.join('\n').trim(); | ||
}; | ||
|
||
const generateUsages = async (compilerCtx: d.CompilerCtx, usagesDir: string) => { | ||
/** | ||
* This function is responsible for reading the contents of all markdown files in a provided `usage` directory and | ||
* returning their contents | ||
* @param compilerCtx the current compiler context | ||
* @param usagesDir the directory to read usage markdown files from | ||
* @returns an object that maps the filename containing the usage example, to the file's contents. If an error occurs, | ||
* an empty object is returned. | ||
*/ | ||
const generateUsages = async (compilerCtx: d.CompilerCtx, usagesDir: string): Promise<d.JsonDocsUsage> => { | ||
const rtn: d.JsonDocsUsage = {}; | ||
|
||
try { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* Generate an 'Overview' section for a markdown file | ||
* @param overview a component-level comment string to place in a markdown file | ||
* @returns The generated Overview section. If the provided overview is empty, return an empty list | ||
*/ | ||
export const overviewToMarkdown = (overview: string): ReadonlyArray<string> => { | ||
if (!overview) { | ||
return []; | ||
} | ||
|
||
const content: string[] = []; | ||
content.push(`## Overview`); | ||
content.push(''); | ||
content.push(`${overview.trim()}`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any escaping / illegal character type stuff we should be concerned about here? I know that a lot of Markdown syntax is supported in a lot of JSDoc ecosystem stuff, but not sure if there are some edge cases we need to be aware of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah missed this the first time around. Perhaps? Although I don't think there's any sanitization done for any MD files yet. It's probably best served as a separate task, but I think the risk some someone doing something super funky is pretty low here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah agreed, and it would just mainly be ending up in their own markdown file so 🤷♀️ I think we don't need to worry about it |
||
content.push(''); | ||
|
||
return content; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { overviewToMarkdown } from '../readme/markdown-overview'; | ||
|
||
describe('markdown-overview', () => { | ||
describe('overviewToMarkdown', () => { | ||
it('returns no overview if no docs exist', () => { | ||
const generatedOverview = overviewToMarkdown('').join('\n'); | ||
|
||
expect(generatedOverview).toBe(''); | ||
}); | ||
|
||
it('generates a single line overview', () => { | ||
const generatedOverview = overviewToMarkdown('This is a custom button component').join('\n'); | ||
|
||
expect(generatedOverview).toBe(`## Overview | ||
|
||
This is a custom button component | ||
`); | ||
}); | ||
|
||
it('generates a multi-line overview', () => { | ||
const description = `This is a custom button component. | ||
It is to be used throughout the design system. | ||
|
||
This is a comment followed by a newline. | ||
`; | ||
const generatedOverview = overviewToMarkdown(description).join('\n'); | ||
|
||
expect(generatedOverview).toBe(`## Overview | ||
|
||
This is a custom button component. | ||
It is to be used throughout the design system. | ||
|
||
This is a comment followed by a newline. | ||
`); | ||
}); | ||
|
||
it('trims all leading newlines & leaves one at the end', () => { | ||
const description = ` | ||
This is a custom button component. | ||
|
||
`; | ||
const generatedOverview = overviewToMarkdown(description).join('\n'); | ||
|
||
expect(generatedOverview).toBe(`## Overview | ||
|
||
This is a custom button component. | ||
`); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,6 +47,26 @@ export interface JsonDocsValue { | |
type: string; | ||
} | ||
|
||
/** | ||
* A mapping of file names to their contents. | ||
* | ||
* This type is meant to be used when reading one or more usage markdown files associated with a component. For the | ||
* given directory structure: | ||
* ``` | ||
* src/components/my-component | ||
* ├── my-component.tsx | ||
* └── usage | ||
* ├── bar.md | ||
* └── foo.md | ||
* ``` | ||
* an instance of this type would include the name of the markdown file, mapped to its contents: | ||
* ```ts | ||
* { | ||
* 'bar': STRING_CONTENTS_OF_BAR.MD | ||
* 'foo': STRING_CONTENTS_OF_FOO.MD | ||
* } | ||
* ``` | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comments like these 🙌 |
||
export interface JsonDocsUsage { | ||
[key: string]: string; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
am I missing something, or does this produce the same output as
(new Date()).toISOString().split(".")[0]
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it does (create the same string)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤷♀️ seems to work so...idk! let's keep it