refactor(docs): centralize JSDoc configuration to repository root - #9056
refactor(docs): centralize JSDoc configuration to repository root#9056quirogas wants to merge 3 commits into
Conversation
Replace 241 per-package `.jsdoc.js` and `.jsdoc.cjs` configuration copies across `packages/*` with a unified root-level `.jsdoc.js` file, and update package `"docs"` scripts to execute `jsdoc -c ../../.jsdoc.js`. Every client package previously maintained a structurally identical JSDoc configuration file where the only variation was the hardcoded `systemName` (package name) and proto include paths. Maintaining over 200 duplicate configuration files created high maintenance friction. The new centralized root `.jsdoc.js` dynamically evaluates `process.cwd()` to resolve `systemName` and existing include paths (`build/src`, `build/esm`, and `protos`). The file utilizes dual named and default module exports to guarantee seamless compatibility with both ES Module resolution (`"type": "module"` in root package.json) and JSDoc's loader. For googleapis/librarian#4584
There was a problem hiding this comment.
Code Review
This pull request centralizes JSDoc configuration by removing individual configuration files across packages and referencing a single, centralized .jsdoc.js file at the root. However, because the root package.json uses "type": "module", JSDoc will fail to load this configuration file due to an ESM loading error. To resolve this, the centralized configuration should be renamed to .jsdoc.cjs using CommonJS syntax, and all package template references should be updated accordingly.
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| let systemName = ''; | ||
| const packageJsonPath = path.join(process.cwd(), 'package.json'); | ||
| if (fs.existsSync(packageJsonPath)) { | ||
| try { | ||
| const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); | ||
| if (pkg.name) { | ||
| systemName = pkg.name; | ||
| } | ||
| } catch (err) { | ||
| // Ignore invalid JSON or read errors | ||
| } | ||
| } | ||
|
|
||
| const include = []; | ||
| if (fs.existsSync(path.join(process.cwd(), 'build/src'))) { | ||
| include.push('build/src'); | ||
| } else if (fs.existsSync(path.join(process.cwd(), 'build/esm/src'))) { | ||
| include.push('build/esm/src'); | ||
| } else if (fs.existsSync(path.join(process.cwd(), 'build/cjs/src'))) { | ||
| include.push('build/cjs/src'); | ||
| } else if (fs.existsSync(path.join(process.cwd(), 'src'))) { | ||
| include.push('src'); | ||
| } else { | ||
| include.push('build/src'); | ||
| } | ||
|
|
||
| if (fs.existsSync(path.join(process.cwd(), 'protos'))) { | ||
| include.push('protos'); | ||
| } else if (fs.existsSync(path.join(process.cwd(), 'build/protos'))) { | ||
| include.push('build/protos'); | ||
| } | ||
|
|
||
| export const opts = { | ||
| readme: './README.md', | ||
| package: './package.json', | ||
| template: './node_modules/jsdoc-fresh', | ||
| recurse: true, | ||
| verbose: true, | ||
| destination: './docs/', | ||
| }; | ||
|
|
||
| export const plugins = ['plugins/markdown', 'jsdoc-region-tag']; | ||
|
|
||
| export const source = { | ||
| excludePattern: '(^|\\/|\\\\)[._]', | ||
| include, | ||
| includePattern: '\\.(js|cjs|mjs)$', | ||
| }; | ||
|
|
||
| export const templates = { | ||
| copyright: 'Copyright 2026 Google LLC', | ||
| includeDate: false, | ||
| sourceFiles: false, | ||
| systemName, | ||
| theme: 'lumen', | ||
| default: { | ||
| outputSourceFiles: false, | ||
| }, | ||
| }; | ||
|
|
||
| export const markdown = { | ||
| idInHeadings: true, | ||
| }; | ||
|
|
||
| export default { | ||
| opts, | ||
| plugins, | ||
| source, | ||
| templates, | ||
| markdown, | ||
| }; |
There was a problem hiding this comment.
Since the root package.json has "type": "module", any .js file at the root is treated as an ES module by Node.js. However, JSDoc loads configuration files using CommonJS require(). When JSDoc attempts to load an ES module via require(), Node.js will throw an ERR_REQUIRE_ESM error. Conversely, if the root does not use "type": "module", loading this file via require() will throw a SyntaxError due to the ESM import/export syntax.
To ensure compatibility with JSDoc's CommonJS loader across all environments, the centralized configuration file should be written using CommonJS syntax (require and module.exports) and named with a .cjs extension (e.g., .jsdoc.cjs). This guarantees that Node.js always treats it as CommonJS.
Additionally, we can make the template path resolution more robust by using path.join(__dirname, 'node_modules/jsdoc-fresh') to resolve it to the root node_modules directory, regardless of the package directory from which JSDoc is executed.
const fs = require('fs');
const path = require('path');
let systemName = '';
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (pkg.name) {
systemName = pkg.name;
}
} catch (err) {
// Ignore invalid JSON or read errors
}
}
const include = [];
if (fs.existsSync(path.join(process.cwd(), 'build/src'))) {
include.push('build/src');
} else if (fs.existsSync(path.join(process.cwd(), 'build/esm/src'))) {
include.push('build/esm/src');
} else if (fs.existsSync(path.join(process.cwd(), 'build/cjs/src'))) {
include.push('build/cjs/src');
} else if (fs.existsSync(path.join(process.cwd(), 'src'))) {
include.push('src');
} else {
include.push('build/src');
}
if (fs.existsSync(path.join(process.cwd(), 'protos'))) {
include.push('protos');
} else if (fs.existsSync(path.join(process.cwd(), 'build/protos'))) {
include.push('build/protos');
}
const opts = {
readme: './README.md',
package: './package.json',
template: path.join(__dirname, 'node_modules/jsdoc-fresh'),
recurse: true,
verbose: true,
destination: './docs/',
};
const plugins = ['plugins/markdown', 'jsdoc-region-tag'];
const source = {
excludePattern: '(^|\\/|\\\\)[._]',
include,
includePattern: '\\.(js|cjs|mjs)$',
};
const templates = {
copyright: 'Copyright 2026 Google LLC',
includeDate: false,
sourceFiles: false,
systemName,
theme: 'lumen',
default: {
outputSourceFiles: false,
},
};
const markdown = {
idInHeadings: true,
};
module.exports = {
opts,
plugins,
source,
templates,
markdown,
};| "compile": "tsc -p . && cp -r protos build/{% if not api.legacyProtoLoad %} && minifyProtoJson{% endif %}", | ||
| "compile-protos": "compileProtos {% if api.legacyProtoLoad %}--skip-json {% endif %}src", | ||
| "docs": "jsdoc -c .jsdoc.js", | ||
| "docs": "jsdoc -c ../../.jsdoc.js", |
| "clean": "gts clean", | ||
| "compile-protos": "compileProtos esm/src --esm {% if api.legacyProtoLoad %}--skip-json {% endif %}", | ||
| "docs": "jsdoc -c .jsdoc.cjs", | ||
| "docs": "jsdoc -c ../../.jsdoc.js", |
| "compile": "tsc -p . && cp -r protos build/", | ||
| "compile-protos": "compileProtos src", | ||
| "docs": "jsdoc -c .jsdoc.js", | ||
| "docs": "jsdoc -c ../../.jsdoc.js", |
Replace 241 per-package
.jsdoc.jsand.jsdoc.cjsconfiguration copies acrosspackages/*with a unified root-level.jsdoc.jsfile, and update package"docs"scripts to executejsdoc -c ../../.jsdoc.js.Every client package previously maintained a structurally identical JSDoc configuration file where the only variation was the hardcoded
systemName(package name) and proto include paths. Maintaining over 200 duplicate configuration files created high maintenance friction. The new centralized root.jsdoc.jsdynamically evaluates
process.cwd()to resolvesystemNameand existing include paths (build/src,build/esm, andprotos). The file utilizes dual named and default module exports to guarantee seamless compatibility with both ES Module resolution ("type": "module"in root package.json) and JSDoc'sloader.
For googleapis/librarian#4584