Skip to content

Commit 21f076d

Browse files
committed
Enforce documentation.yml toc coverage via ESLint
JSDoc documented items missing from the documentation.yml toc render at an arbitrary spot at the end of the generated docs. Since the docs workflow only runs on master, this used to go unnoticed until after publishing. Add a custom ESLint rule that reports documented top level items that are not listed in the toc, giving fast feedback from lint runs. The rule lives in the pageflow package's shared config directory, like the jest and webpack configs reused by entry type packages. Add toc entries for previously unlisted items. Document the error boundary property via the established underscored @name pattern: only an explicit @name suppresses the inferred memberof, which would otherwise prevent toc matching, and the docs theme renders the underscore as a dot (frontend.contentElementErrorBoundary).
1 parent e3e00dc commit 21f076d

8 files changed

Lines changed: 155 additions & 3 deletions

File tree

entry_types/scrolled/package/.eslintrc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ module.exports = {
4848
"patterns": ["**/entryState/**", "../**/entryState"]
4949
}]
5050
}
51+
},
52+
{
53+
// Directories passed to documentation.js in
54+
// .github/workflows/docs.yml.
55+
"files": ["src/**/*.js", "spec/support/**/*.js"],
56+
"rules": {
57+
"documented-in-toc": "error"
58+
}
5159
}
5260
]
5361
};

entry_types/scrolled/package/documentation.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ toc:
55
children:
66
- frontend_contentElementTypes
77
- frontend_widgetTypes
8+
- frontend_contentElementErrorBoundary
89
- name: Editor API
910
description: |
1011
Main entry point of editor API to register new content element types.
@@ -45,6 +46,7 @@ toc:
4546
- useLegalInfo
4647
- useMediaMuted
4748
- usePortraitOrientation
49+
- usePrivacyLink
4850
- useShareProviders
4951
- useShareUrl
5052
- useTheme
@@ -60,6 +62,7 @@ toc:
6062
- normalizeSeed
6163
- renderInEntry
6264
- renderInContentElement
65+
- renderInEntryWithContentElementLifecycle
6366
- renderHookInEntry
6467
- name: Storybook Support
6568
description: |

entry_types/scrolled/package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
},
8787
"scripts": {
8888
"test": "jest",
89-
"lint": "eslint",
89+
"lint": "eslint --rulesdir ../../../node_modules/pageflow/config/eslint-rules",
9090
"start-storybook": "storybook dev --port 8001",
9191
"build-storybook": "storybook build -o .storybook/out",
9292
"snapshot": "storybook build --quiet -o .storybook/out && PERCY_TOKEN=${PERCY_TOKEN:-$PT} percy storybook .storybook/out"

entry_types/scrolled/package/src/frontend/api/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export const api = {
1212
* typeName (string), configuration (object), fallback (function returning
1313
* default UI), and children (content element).
1414
*
15-
* @property {React.Component} contentElementErrorBoundary
15+
* @name frontend_contentElementErrorBoundary
16+
* @type {React.Component}
1617
*/
1718
contentElementErrorBoundary: undefined
1819
}

package/.eslintrc.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ module.exports = {
4646
{
4747
"files": ["spec/**/*.js", "src/testHelpers/**/*.js"],
4848
"extends": ["plugin:jest/recommended"]
49+
},
50+
{
51+
// Directories passed to documentation.js in
52+
// .github/workflows/docs.yml.
53+
"files": [
54+
"src/editor/**/*.js",
55+
"src/ui/**/*.js",
56+
"src/testHelpers/**/*.js"
57+
],
58+
"rules": {
59+
"documented-in-toc": "error"
60+
}
4961
}
5062
]
5163
};
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const yaml = require('js-yaml');
4+
5+
// JSDoc comments on these are either nested under another item or
6+
// excluded from the generated docs, so they do not need toc entries.
7+
const skippedTags = /@(private|internal|ignore|memberof)\b/;
8+
9+
module.exports = {
10+
meta: {
11+
type: 'suggestion',
12+
docs: {
13+
description:
14+
'Ensure JSDoc documented top level items are listed in the toc ' +
15+
'of documentation.yml. Otherwise they render at an arbitrary ' +
16+
'spot at the end of the generated docs.'
17+
},
18+
schema: []
19+
},
20+
21+
create(context) {
22+
const config = findTocConfig(path.dirname(context.getFilename()));
23+
24+
if (!config) {
25+
return {};
26+
}
27+
28+
const sourceCode = context.getSourceCode();
29+
30+
return {
31+
Program(program) {
32+
program.body.forEach(statement => {
33+
const comment = jsdocCommentBefore(statement);
34+
35+
if (!comment || skippedTags.test(comment.value)) {
36+
return;
37+
}
38+
39+
const name = documentedName(comment, statement);
40+
41+
if (name && !config.names.has(name)) {
42+
context.report({
43+
loc: comment.loc,
44+
message:
45+
`'${name}' has a JSDoc comment, but is not listed in the ` +
46+
`toc in ${config.relativePath}. Add it to the section ` +
47+
'where it should show up in the generated docs.'
48+
});
49+
}
50+
});
51+
}
52+
};
53+
54+
function jsdocCommentBefore(statement) {
55+
const comments = sourceCode.getCommentsBefore(statement);
56+
const comment = comments[comments.length - 1];
57+
58+
return comment &&
59+
comment.type === 'Block' &&
60+
comment.value.startsWith('*') ? comment : null;
61+
}
62+
}
63+
};
64+
65+
function documentedName(comment, statement) {
66+
const tagMatch = comment.value.match(/@(?:name|alias)\s+([\w.#]+)/);
67+
68+
if (tagMatch) {
69+
return tagMatch[1];
70+
}
71+
72+
return declaredName(statement);
73+
}
74+
75+
function declaredName(statement) {
76+
switch (statement.type) {
77+
case 'ExportNamedDeclaration':
78+
case 'ExportDefaultDeclaration':
79+
return statement.declaration && declaredName(statement.declaration);
80+
case 'FunctionDeclaration':
81+
case 'ClassDeclaration':
82+
return statement.id && statement.id.name;
83+
case 'VariableDeclaration':
84+
return statement.declarations[0].id.type === 'Identifier' ?
85+
statement.declarations[0].id.name : null;
86+
default:
87+
return null;
88+
}
89+
}
90+
91+
const configCache = new Map();
92+
93+
function findTocConfig(directory) {
94+
if (configCache.has(directory)) {
95+
return configCache.get(directory);
96+
}
97+
98+
const configPath = path.join(directory, 'documentation.yml');
99+
let config;
100+
101+
if (fs.existsSync(configPath)) {
102+
config = loadTocConfig(configPath);
103+
}
104+
else {
105+
const parent = path.dirname(directory);
106+
config = parent === directory ? null : findTocConfig(parent);
107+
}
108+
109+
configCache.set(directory, config);
110+
return config;
111+
}
112+
113+
function loadTocConfig(configPath) {
114+
const toc = yaml.safeLoad(fs.readFileSync(configPath, 'utf8')).toc || [];
115+
const names = new Set();
116+
117+
toc.forEach(section =>
118+
(section.children || []).forEach(child => names.add(child))
119+
);
120+
121+
return {
122+
names,
123+
relativePath: path.relative(process.cwd(), configPath)
124+
};
125+
}

package/documentation.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ toc:
5050
- EditConfigurationView
5151
- ReferenceInputView
5252
- FileInputView
53+
- OembedUrlInputView
5354

5455
- name: Editor - Misc Views
5556
description: |
5657
General purpose Backbone views and mixins exported by `pageflow/editor`.
5758
children:
5859
- modelLifecycleTrackingView
5960
- DropDownButtonView
61+
- DestroyMenuItem
6062
- ListView
6163
- ModelThumbnailView
6264

@@ -132,4 +134,5 @@ toc:
132134
children:
133135
- factories
134136
- setupGlobals
137+
- useFakeFeatures
135138
- useFakeTranslations

package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"scripts": {
2323
"test": "jest",
24-
"lint": "eslint"
24+
"lint": "eslint --rulesdir config/eslint-rules"
2525
},
2626
"dependencies": {
2727
"backbone-events-standalone": "^0.2.7",

0 commit comments

Comments
 (0)