From 284d3b50292759d7c893cb79c5398c55005c34d1 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 27 Mar 2026 16:21:21 -0400 Subject: [PATCH] feat(plugin-jsdocs): add setup wizard binding --- packages/create-cli/README.md | 7 ++ packages/create-cli/src/index.ts | 3 +- packages/plugin-jsdocs/src/index.ts | 1 + packages/plugin-jsdocs/src/lib/binding.ts | 93 +++++++++++++++++++ .../src/lib/binding.unit.test.ts | 83 +++++++++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-jsdocs/src/lib/binding.ts create mode 100644 packages/plugin-jsdocs/src/lib/binding.unit.test.ts diff --git a/packages/create-cli/README.md b/packages/create-cli/README.md index 9ed8da78a6..d1c4bf8e13 100644 --- a/packages/create-cli/README.md +++ b/packages/create-cli/README.md @@ -72,6 +72,13 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen | **`--lighthouse.urls`** | `string \| string[]` | `http://localhost:4200` | Target URL(s) (comma-separated) | | **`--lighthouse.categories`** | `('performance'` \| `'a11y'` \| `'best-practices'` \| `'seo')[]` | all | Lighthouse categories | +#### JSDocs + +| Option | Type | Default | Description | +| ------------------------- | -------------------- | -------------------------------------------- | -------------------------------------- | +| **`--jsdocs.patterns`** | `string \| string[]` | `src/**/*.ts, src/**/*.js, !**/node_modules` | Source file patterns (comma-separated) | +| **`--jsdocs.categories`** | `boolean` | `true` | Add JSDocs categories | + #### Axe | Option | Type | Default | Description | diff --git a/packages/create-cli/src/index.ts b/packages/create-cli/src/index.ts index 99972c4584..5cd7243dd1 100755 --- a/packages/create-cli/src/index.ts +++ b/packages/create-cli/src/index.ts @@ -5,6 +5,7 @@ import { axeSetupBinding } from '@code-pushup/axe-plugin'; import { coverageSetupBinding } from '@code-pushup/coverage-plugin'; import { eslintSetupBinding } from '@code-pushup/eslint-plugin'; import { jsPackagesSetupBinding } from '@code-pushup/js-packages-plugin'; +import { jsDocsSetupBinding } from '@code-pushup/jsdocs-plugin'; import { lighthouseSetupBinding } from '@code-pushup/lighthouse-plugin'; import { typescriptSetupBinding } from '@code-pushup/typescript-plugin'; import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js'; @@ -16,7 +17,6 @@ import { } from './lib/setup/types.js'; import { runSetupWizard } from './lib/setup/wizard.js'; -// TODO: create, import and pass remaining plugin bindings (jsdocs) const bindings: PluginSetupBinding[] = [ eslintSetupBinding, coverageSetupBinding, @@ -24,6 +24,7 @@ const bindings: PluginSetupBinding[] = [ typescriptSetupBinding, lighthouseSetupBinding, axeSetupBinding, + jsDocsSetupBinding, ]; const argv = await yargs(hideBin(process.argv)) diff --git a/packages/plugin-jsdocs/src/index.ts b/packages/plugin-jsdocs/src/index.ts index f0b65ec1c5..3ecad97ce8 100644 --- a/packages/plugin-jsdocs/src/index.ts +++ b/packages/plugin-jsdocs/src/index.ts @@ -1,4 +1,5 @@ import { jsDocsPlugin } from './lib/jsdocs-plugin.js'; export default jsDocsPlugin; +export { jsDocsSetupBinding } from './lib/binding.js'; export type { JsDocsPluginConfig } from './lib/config.js'; diff --git a/packages/plugin-jsdocs/src/lib/binding.ts b/packages/plugin-jsdocs/src/lib/binding.ts new file mode 100644 index 0000000000..76988eb71a --- /dev/null +++ b/packages/plugin-jsdocs/src/lib/binding.ts @@ -0,0 +1,93 @@ +import { createRequire } from 'node:module'; +import type { + CategoryConfig, + PluginAnswer, + PluginSetupBinding, +} from '@code-pushup/models'; +import { + answerBoolean, + answerNonEmptyArray, + singleQuote, +} from '@code-pushup/utils'; +import { PLUGIN_SLUG, PLUGIN_TITLE } from './constants.js'; + +const { name: PACKAGE_NAME } = createRequire(import.meta.url)( + '../../package.json', +) as typeof import('../../package.json'); + +const DEFAULT_PATTERNS: [string, ...string[]] = [ + 'src/**/*.ts', + 'src/**/*.js', + '!**/node_modules', +]; + +const CATEGORY: CategoryConfig = { + slug: 'docs', + title: 'Documentation', + description: 'Measures how much of your code is **documented**.', + refs: [ + { + type: 'group', + plugin: PLUGIN_SLUG, + slug: 'documentation-coverage', + weight: 1, + }, + ], +}; + +type JsDocsOptions = { + patterns: [string, ...string[]]; + categories: boolean; +}; + +export const jsDocsSetupBinding = { + slug: PLUGIN_SLUG, + title: PLUGIN_TITLE, + packageName: PACKAGE_NAME, + prompts: async () => [ + { + key: 'jsdocs.patterns', + message: 'Source file patterns (comma-separated)', + type: 'input', + default: DEFAULT_PATTERNS.join(', '), + }, + { + key: 'jsdocs.categories', + message: 'Add JSDocs categories?', + type: 'confirm', + default: true, + }, + ], + generateConfig: (answers: Record) => { + const options = parseAnswers(answers); + return { + imports: [ + { moduleSpecifier: PACKAGE_NAME, defaultImport: 'jsDocsPlugin' }, + ], + pluginInit: formatPluginInit(options.patterns), + ...(options.categories ? { categories: [CATEGORY] } : {}), + }; + }, +} satisfies PluginSetupBinding; + +function parseAnswers(answers: Record): JsDocsOptions { + return { + patterns: answerNonEmptyArray( + answers, + 'jsdocs.patterns', + DEFAULT_PATTERNS[0], + ), + categories: answerBoolean(answers, 'jsdocs.categories'), + }; +} + +function formatPluginInit([first, ...rest]: [string, ...string[]]): string[] { + if (rest.length === 0) { + return [`jsDocsPlugin(${singleQuote(first)}),`]; + } + return [ + 'jsDocsPlugin([', + ...[first, ...rest].map(p => ` ${singleQuote(p)},`), + ']),', + ]; +} diff --git a/packages/plugin-jsdocs/src/lib/binding.unit.test.ts b/packages/plugin-jsdocs/src/lib/binding.unit.test.ts new file mode 100644 index 0000000000..30ea7c7453 --- /dev/null +++ b/packages/plugin-jsdocs/src/lib/binding.unit.test.ts @@ -0,0 +1,83 @@ +import type { PluginAnswer } from '@code-pushup/models'; +import { jsDocsSetupBinding as binding } from './binding.js'; + +const defaultAnswers: Record = { + 'jsdocs.patterns': 'src/**/*.ts, src/**/*.js, !**/node_modules', + 'jsdocs.categories': true, +}; + +const noCategoryAnswers: Record = { + ...defaultAnswers, + 'jsdocs.categories': false, +}; + +describe('jsDocsSetupBinding', () => { + describe('prompts', () => { + it('should default to common TypeScript and JavaScript source patterns', async () => { + await expect(binding.prompts!()).resolves.toIncludeAllPartialMembers([ + { + key: 'jsdocs.patterns', + type: 'input', + default: 'src/**/*.ts, src/**/*.js, !**/node_modules', + }, + ]); + }); + + it('should offer to add categories by default', async () => { + await expect(binding.prompts!()).resolves.toIncludeAllPartialMembers([ + { key: 'jsdocs.categories', type: 'confirm', default: true }, + ]); + }); + }); + + describe('generateConfig', () => { + it('should import from @code-pushup/jsdocs-plugin', () => { + const { imports } = binding.generateConfig(defaultAnswers); + expect(imports).toStrictEqual([ + expect.objectContaining({ + defaultImport: 'jsDocsPlugin', + }), + ]); + }); + + it('should pass multiple patterns as array to plugin call', () => { + const { pluginInit } = binding.generateConfig(defaultAnswers); + expect(pluginInit).toStrictEqual([ + 'jsDocsPlugin([', + " 'src/**/*.ts',", + " 'src/**/*.js',", + " '!**/node_modules',", + ']),', + ]); + }); + + it('should pass single pattern as string to plugin call', () => { + const { pluginInit } = binding.generateConfig({ + ...defaultAnswers, + 'jsdocs.patterns': 'src/**/*.ts', + }); + expect(pluginInit).toStrictEqual(["jsDocsPlugin('src/**/*.ts'),"]); + }); + + it('should generate Documentation category from documentation-coverage group', () => { + const { categories } = binding.generateConfig(defaultAnswers); + expect(categories).toStrictEqual([ + expect.objectContaining({ + slug: 'docs', + title: 'Documentation', + refs: [ + expect.objectContaining({ + plugin: 'jsdocs', + slug: 'documentation-coverage', + }), + ], + }), + ]); + }); + + it('should omit categories when declined', () => { + const { categories } = binding.generateConfig(noCategoryAnswers); + expect(categories).toBeUndefined(); + }); + }); +});