From 21f076d3783aedfb46eae076bad71411894a9522 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 9 Jun 2026 22:33:16 +0200 Subject: [PATCH] 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). --- entry_types/scrolled/package/.eslintrc.js | 8 ++ .../scrolled/package/documentation.yml | 3 + entry_types/scrolled/package/package.json | 2 +- .../package/src/frontend/api/index.js | 3 +- package/.eslintrc.js | 12 ++ .../config/eslint-rules/documented-in-toc.js | 125 ++++++++++++++++++ package/documentation.yml | 3 + package/package.json | 2 +- 8 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 package/config/eslint-rules/documented-in-toc.js diff --git a/entry_types/scrolled/package/.eslintrc.js b/entry_types/scrolled/package/.eslintrc.js index ad8234c9b1..01d6d9bc11 100644 --- a/entry_types/scrolled/package/.eslintrc.js +++ b/entry_types/scrolled/package/.eslintrc.js @@ -48,6 +48,14 @@ module.exports = { "patterns": ["**/entryState/**", "../**/entryState"] }] } + }, + { + // Directories passed to documentation.js in + // .github/workflows/docs.yml. + "files": ["src/**/*.js", "spec/support/**/*.js"], + "rules": { + "documented-in-toc": "error" + } } ] }; diff --git a/entry_types/scrolled/package/documentation.yml b/entry_types/scrolled/package/documentation.yml index 327b66b117..7aa12b99f6 100644 --- a/entry_types/scrolled/package/documentation.yml +++ b/entry_types/scrolled/package/documentation.yml @@ -5,6 +5,7 @@ toc: children: - frontend_contentElementTypes - frontend_widgetTypes + - frontend_contentElementErrorBoundary - name: Editor API description: | Main entry point of editor API to register new content element types. @@ -45,6 +46,7 @@ toc: - useLegalInfo - useMediaMuted - usePortraitOrientation + - usePrivacyLink - useShareProviders - useShareUrl - useTheme @@ -60,6 +62,7 @@ toc: - normalizeSeed - renderInEntry - renderInContentElement + - renderInEntryWithContentElementLifecycle - renderHookInEntry - name: Storybook Support description: | diff --git a/entry_types/scrolled/package/package.json b/entry_types/scrolled/package/package.json index f6747f7568..c4ed8b2ae4 100644 --- a/entry_types/scrolled/package/package.json +++ b/entry_types/scrolled/package/package.json @@ -86,7 +86,7 @@ }, "scripts": { "test": "jest", - "lint": "eslint", + "lint": "eslint --rulesdir ../../../node_modules/pageflow/config/eslint-rules", "start-storybook": "storybook dev --port 8001", "build-storybook": "storybook build -o .storybook/out", "snapshot": "storybook build --quiet -o .storybook/out && PERCY_TOKEN=${PERCY_TOKEN:-$PT} percy storybook .storybook/out" diff --git a/entry_types/scrolled/package/src/frontend/api/index.js b/entry_types/scrolled/package/src/frontend/api/index.js index 69eddbd68c..c750f54a53 100644 --- a/entry_types/scrolled/package/src/frontend/api/index.js +++ b/entry_types/scrolled/package/src/frontend/api/index.js @@ -12,7 +12,8 @@ export const api = { * typeName (string), configuration (object), fallback (function returning * default UI), and children (content element). * - * @property {React.Component} contentElementErrorBoundary + * @name frontend_contentElementErrorBoundary + * @type {React.Component} */ contentElementErrorBoundary: undefined } diff --git a/package/.eslintrc.js b/package/.eslintrc.js index 2d8fe72765..bbf0f6d67e 100644 --- a/package/.eslintrc.js +++ b/package/.eslintrc.js @@ -46,6 +46,18 @@ module.exports = { { "files": ["spec/**/*.js", "src/testHelpers/**/*.js"], "extends": ["plugin:jest/recommended"] + }, + { + // Directories passed to documentation.js in + // .github/workflows/docs.yml. + "files": [ + "src/editor/**/*.js", + "src/ui/**/*.js", + "src/testHelpers/**/*.js" + ], + "rules": { + "documented-in-toc": "error" + } } ] }; diff --git a/package/config/eslint-rules/documented-in-toc.js b/package/config/eslint-rules/documented-in-toc.js new file mode 100644 index 0000000000..72002859a1 --- /dev/null +++ b/package/config/eslint-rules/documented-in-toc.js @@ -0,0 +1,125 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +// JSDoc comments on these are either nested under another item or +// excluded from the generated docs, so they do not need toc entries. +const skippedTags = /@(private|internal|ignore|memberof)\b/; + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'Ensure JSDoc documented top level items are listed in the toc ' + + 'of documentation.yml. Otherwise they render at an arbitrary ' + + 'spot at the end of the generated docs.' + }, + schema: [] + }, + + create(context) { + const config = findTocConfig(path.dirname(context.getFilename())); + + if (!config) { + return {}; + } + + const sourceCode = context.getSourceCode(); + + return { + Program(program) { + program.body.forEach(statement => { + const comment = jsdocCommentBefore(statement); + + if (!comment || skippedTags.test(comment.value)) { + return; + } + + const name = documentedName(comment, statement); + + if (name && !config.names.has(name)) { + context.report({ + loc: comment.loc, + message: + `'${name}' has a JSDoc comment, but is not listed in the ` + + `toc in ${config.relativePath}. Add it to the section ` + + 'where it should show up in the generated docs.' + }); + } + }); + } + }; + + function jsdocCommentBefore(statement) { + const comments = sourceCode.getCommentsBefore(statement); + const comment = comments[comments.length - 1]; + + return comment && + comment.type === 'Block' && + comment.value.startsWith('*') ? comment : null; + } + } +}; + +function documentedName(comment, statement) { + const tagMatch = comment.value.match(/@(?:name|alias)\s+([\w.#]+)/); + + if (tagMatch) { + return tagMatch[1]; + } + + return declaredName(statement); +} + +function declaredName(statement) { + switch (statement.type) { + case 'ExportNamedDeclaration': + case 'ExportDefaultDeclaration': + return statement.declaration && declaredName(statement.declaration); + case 'FunctionDeclaration': + case 'ClassDeclaration': + return statement.id && statement.id.name; + case 'VariableDeclaration': + return statement.declarations[0].id.type === 'Identifier' ? + statement.declarations[0].id.name : null; + default: + return null; + } +} + +const configCache = new Map(); + +function findTocConfig(directory) { + if (configCache.has(directory)) { + return configCache.get(directory); + } + + const configPath = path.join(directory, 'documentation.yml'); + let config; + + if (fs.existsSync(configPath)) { + config = loadTocConfig(configPath); + } + else { + const parent = path.dirname(directory); + config = parent === directory ? null : findTocConfig(parent); + } + + configCache.set(directory, config); + return config; +} + +function loadTocConfig(configPath) { + const toc = yaml.safeLoad(fs.readFileSync(configPath, 'utf8')).toc || []; + const names = new Set(); + + toc.forEach(section => + (section.children || []).forEach(child => names.add(child)) + ); + + return { + names, + relativePath: path.relative(process.cwd(), configPath) + }; +} diff --git a/package/documentation.yml b/package/documentation.yml index 046c35915f..3a8bd6dcd3 100644 --- a/package/documentation.yml +++ b/package/documentation.yml @@ -50,6 +50,7 @@ toc: - EditConfigurationView - ReferenceInputView - FileInputView + - OembedUrlInputView - name: Editor - Misc Views description: | @@ -57,6 +58,7 @@ toc: children: - modelLifecycleTrackingView - DropDownButtonView + - DestroyMenuItem - ListView - ModelThumbnailView @@ -132,4 +134,5 @@ toc: children: - factories - setupGlobals + - useFakeFeatures - useFakeTranslations diff --git a/package/package.json b/package/package.json index fe77bdc3eb..d6a947a3f8 100644 --- a/package/package.json +++ b/package/package.json @@ -21,7 +21,7 @@ }, "scripts": { "test": "jest", - "lint": "eslint" + "lint": "eslint --rulesdir config/eslint-rules" }, "dependencies": { "backbone-events-standalone": "^0.2.7",