-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Components: Set up README auto-generator #66035
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
f31de7c
Add components readme generator
mirka 82cafc7
Move
mirka 04f28a7
AlignmentMatrixControl: Add missing `@deprecated` tag
mirka bbdf0fb
Add docs manifest for AlignmentMatrixControl
mirka a0d09d0
Handle case with no subcomponents
mirka 5a524a3
Add JSON schema
mirka b363002
Commit AlignmentMatrixControl readme changes
mirka 5d69c1b
Fixup: Handle case with no subcomponents
mirka 680a420
Add manifest for AnglePickerControl
mirka 1d01ecf
Simplify
mirka 9bf7678
Improve schema descriptions
mirka 8fe2ca8
Handle docgen errors
mirka 7c66c58
Convert to async
mirka 38b6d34
Move glob further up
mirka 60f9cc9
Handle unparseable JSON
mirka eb4cb40
Handle write file progress
mirka d2a2487
Fixup
mirka 752792d
Apply feedback in markdown props handling
mirka 6fbf877
Simplify
mirka 0623f5b
Handle cases when `displayName` in manifest is wrong
mirka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import docgen from 'react-docgen-typescript'; | ||
import glob from 'glob'; | ||
import fs from 'node:fs/promises'; | ||
import path from 'path'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { generateMarkdownDocs } from './markdown/index.mjs'; | ||
|
||
const MANIFEST_GLOB = 'packages/components/src/**/docs-manifest.json'; | ||
|
||
// For consistency, options should generally match the options used in Storybook. | ||
const OPTIONS = { | ||
shouldExtractLiteralValuesFromEnum: true, | ||
shouldRemoveUndefinedFromOptional: true, | ||
propFilter: ( prop ) => | ||
prop.parent ? ! /node_modules/.test( prop.parent.fileName ) : true, | ||
savePropValueAsString: true, | ||
}; | ||
|
||
function getTypeDocsForComponent( { | ||
manifestPath, | ||
componentFilePath, | ||
displayName, | ||
} ) { | ||
const resolvedPath = path.resolve( | ||
path.dirname( manifestPath ), | ||
componentFilePath | ||
); | ||
|
||
const typeDocs = docgen.parse( resolvedPath, OPTIONS ); | ||
|
||
if ( typeDocs.length === 0 ) { | ||
throw new Error( | ||
`react-docgen-typescript could not generate any type docs from ${ resolvedPath }` | ||
); | ||
} | ||
|
||
const matchingTypeDoc = typeDocs.find( | ||
( obj ) => obj.displayName === displayName | ||
); | ||
|
||
if ( typeof matchingTypeDoc === 'undefined' ) { | ||
const unmatchedTypeDocs = typeDocs | ||
.map( ( obj ) => `\`${ obj.displayName }\`` ) | ||
.join( ', ' ); | ||
|
||
throw new Error( | ||
`react-docgen-typescript could not find type docs for ${ displayName } in ${ resolvedPath }. (Found ${ unmatchedTypeDocs })` | ||
); | ||
} | ||
|
||
return matchingTypeDoc; | ||
} | ||
|
||
async function parseManifest( manifestPath ) { | ||
try { | ||
return JSON.parse( await fs.readFile( manifestPath, 'utf8' ) ); | ||
} catch ( e ) { | ||
throw new Error( | ||
`Error parsing docs manifest at ${ manifestPath }: ${ e.message }` | ||
); | ||
} | ||
} | ||
|
||
const manifests = glob.sync( MANIFEST_GLOB ); | ||
|
||
await Promise.all( | ||
manifests.map( async ( manifestPath ) => { | ||
const manifest = await parseManifest( manifestPath ); | ||
|
||
const typeDocs = getTypeDocsForComponent( { | ||
manifestPath, | ||
componentFilePath: manifest.filePath, | ||
displayName: manifest.displayName, | ||
} ); | ||
|
||
const subcomponentTypeDocs = manifest.subcomponents?.map( | ||
( subcomponent ) => { | ||
const docs = getTypeDocsForComponent( { | ||
manifestPath, | ||
componentFilePath: subcomponent.filePath, | ||
displayName: subcomponent.displayName, | ||
} ); | ||
|
||
if ( subcomponent.preferredDisplayName ) { | ||
docs.displayName = subcomponent.preferredDisplayName; | ||
} | ||
|
||
return docs; | ||
} | ||
); | ||
const docs = generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ); | ||
const outputFile = path.resolve( | ||
path.dirname( manifestPath ), | ||
'./README.md' | ||
); | ||
|
||
try { | ||
console.log( `Writing docs to ${ outputFile }` ); | ||
return fs.writeFile( outputFile, docs ); | ||
} catch ( e ) { | ||
throw new Error( | ||
`Error writing docs to ${ outputFile }: ${ e.message }` | ||
); | ||
} | ||
} ) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import json2md from 'json2md'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { generateMarkdownPropsJson } from './props.mjs'; | ||
|
||
export function generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ) { | ||
const mainDocsJson = [ | ||
'<!-- This file is generated automatically and cannot be edited directly. -->\n', | ||
{ h1: typeDocs.displayName }, | ||
{ | ||
p: `<p class="callout callout-info">See the <a href="https://wordpress.github.io/gutenberg/?path=/docs/components-${ typeDocs.displayName.toLowerCase() }--docs">WordPress Storybook</a> for more detailed, interactive documentation.</p>`, | ||
}, | ||
typeDocs.description, | ||
...generateMarkdownPropsJson( typeDocs.props ), | ||
]; | ||
|
||
const subcomponentDocsJson = subcomponentTypeDocs?.length | ||
? [ | ||
{ h2: 'Subcomponents' }, | ||
...subcomponentTypeDocs.flatMap( ( subcomponentTypeDoc ) => [ | ||
{ | ||
h3: subcomponentTypeDoc.displayName, | ||
}, | ||
subcomponentTypeDoc.description, | ||
...generateMarkdownPropsJson( subcomponentTypeDoc.props, { | ||
headingLevel: 4, | ||
} ), | ||
] ), | ||
] | ||
: []; | ||
|
||
return json2md( | ||
[ ...mainDocsJson, ...subcomponentDocsJson ].filter( Boolean ) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
function renderPropType( type ) { | ||
const MAX_ENUM_VALUES = 10; | ||
|
||
switch ( type.name ) { | ||
case 'enum': { | ||
const string = type.value | ||
.slice( 0, MAX_ENUM_VALUES ) | ||
.map( ( { value } ) => value ) | ||
.join( ' | ' ); | ||
|
||
if ( type.value.length > MAX_ENUM_VALUES ) { | ||
return `${ string } | ...`; | ||
} | ||
return string; | ||
} | ||
default: | ||
return type.name; | ||
} | ||
} | ||
|
||
export function generateMarkdownPropsJson( props, { headingLevel = 2 } = {} ) { | ||
const sortedKeys = Object.keys( props ).sort( ( [ a ], [ b ] ) => | ||
a.localeCompare( b ) | ||
); | ||
|
||
const propsJson = sortedKeys | ||
.flatMap( ( key ) => { | ||
const prop = props[ key ]; | ||
|
||
if ( prop.description?.includes( '@ignore' ) ) { | ||
return null; | ||
} | ||
|
||
return [ | ||
{ [ `h${ headingLevel + 1 }` ]: `\`${ key }\`` }, | ||
prop.description, | ||
{ | ||
ul: [ | ||
`Type: \`${ renderPropType( prop.type ) }\``, | ||
`Required: ${ prop.required ? 'Yes' : 'No' }`, | ||
prop.defaultValue && | ||
`Default: \`${ prop.defaultValue.value }\``, | ||
].filter( Boolean ), | ||
}, | ||
]; | ||
} ) | ||
.filter( Boolean ); | ||
|
||
return [ { [ `h${ headingLevel }` ]: 'Props' }, ...propsJson ]; | ||
} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"title": "JSON schema for @wordpress/components README manifests", | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "object", | ||
"properties": { | ||
"displayName": { | ||
"type": "string", | ||
"description": "The `displayName` of the component, as determined in code. Used to identify the component in the specified source file." | ||
}, | ||
"filePath": { | ||
"type": "string", | ||
"description": "The file path where the component is located." | ||
}, | ||
"subcomponents": { | ||
"type": "array", | ||
"description": "List of subcomponents related to the component.", | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"displayName": { | ||
"type": "string", | ||
"description": "The `displayName` of the subcomponent, as determined in code. Used to identify the component in the specified source file." | ||
}, | ||
"preferredDisplayName": { | ||
"type": "string", | ||
"description": "The display name to use in the README, if it is different from the `displayName` as determined in code." | ||
}, | ||
"filePath": { | ||
"type": "string", | ||
"description": "The file path where the subcomponent is located." | ||
} | ||
}, | ||
"required": [ "displayName", "filePath" ] | ||
} | ||
} | ||
}, | ||
"required": [ "displayName", "filePath" ] | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
getTypeDocsForComponent()
is designed to throw errors, but what happens if we throw in this async function? Is anything going to handle this error?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.
My intent is to immediately fail exit the entire script whenever
getTypeDocsForComponent()
throws, so that it's very obvious when something went wrong. Were you thinking of something else?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.
Thanks for confirming, I think that works well 👍