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",