mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-27 05:37:15 -05:00
* docs: restore generated plugin SDK reference pages Reimplements the Hugo-era plugingodocs/pluginjsdocs/pluginmanifestdocs shortcode pipeline natively in Docusaurus, so the server plugin SDK, web app plugin SDK, and manifest reference pages render full generated content again instead of "Generated content (migrating)" placeholders. Two new Go generators (gen-plugin-godocs, gen-plugin-manifest-docs) and one Node generator (gen-plugin-jsdocs.mjs) read server/public/plugin, server/public/model, and webapp/channels/src/plugins/registry.ts directly from this monorepo and emit gitignored JSON consumed by new PluginGoDocs/PluginGoExample/PluginJsDocs/PluginManifestDocs React components, wired into prestart/prebuild alongside the existing sidebar/OpenAPI generators. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: remove migration plan doc from this PR Was a handoff/status doc for the implementing agent, not meant to ship as part of the change itself. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: make plugin-godocs/manifest-docs builds atomic build:plugin-godocs and build:plugin-manifest-docs redirected straight into data/plugin-*.json, which doesn't exist on a clean checkout (the redirection itself would fail before the generator ever ran) and, on a subsequent failed run, would truncate a previously-good JSON file before failing. Create data/ up front, write to a .tmp file, and only mv it into place once the generator exits successfully. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: fix review findings in plugin doc generators gen-plugin-manifest-docs: - jsonFieldName now excludes unexported fields (matching encoding/json) and falls back to the Go field name when there's no tag or the tag's name component is empty (e.g. json:",omitempty"), instead of dropping the field entirely. - exprTypeDocs's SelectorExpr case now resolves a qualified type only when its package qualifier actually points at the model package (checked against the declaring file's imports), instead of matching any x.Sel identifier by name alone regardless of which package it qualifies. gen-plugin-godocs: - The example-code loop now falls back to example.Code when example.Play is nil (go/doc leaves Play nil when it can't synthesize a whole runnable program), and propagates printer.Fprint errors instead of discarding them. No output change for the current server/public/plugin or server/public/model content — verified via a full regen. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: drop legacy-repo comment references, align jsdocs generator parser Remove "port of the old mattermost-developer-documentation Hugo shortcode" comments from the plugin doc generators/components now that they're fully native to this monorepo, and switch gen-plugin-jsdocs.mjs from the TypeScript compiler API to @typescript-eslint/typescript-estree to match the old script's parser/shape, while keeping the more robust reArg-declared-parameter-name and node-scoped comment-attachment logic. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: bump plugin doc generators' go.mod to go 1.26 go 1.23 was arbitrarily low; align with a current Go version. go run auto-downloads a matching toolchain on older local installs, so this doesn't reintroduce a dependency on server/public/go.mod's version. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: handle RestElement in gen-plugin-jsdocs parameter extraction paramNamesFromPattern silently dropped rest parameters/destructured rest members (e.g. (a, ...rest) or {a, ...rest}), since RestElement matched none of its type checks. No effect on today's registry.ts output (no rest patterns currently used there), but keeps the extraction correct if one is ever introduced. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: simplify plugin doc generators README note Co-authored-by: Cursor <cursoragent@cursor.com> * docs: check stdout write errors in plugin doc generators os.Stdout.Write's return errors were discarded, so a failed/partial write (e.g. broken pipe, disk full) would still exit 0. Combined with the npm scripts' > file.tmp && mv pattern, a truncated write could be treated as a successful generation. Fatal on either write failing. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: flatten Server/Webapp plugin SDK reference sidebar nesting server/server-reference.md and webapp/webapp-reference.md were the only file in their respective folders, so the sidebar generator (which only collapses a folder into a single entry when it has an index.md) rendered them as a "Server" > "Server plugin SDK reference" category with one child instead of one flat entry, unlike every sibling reference folder (rest-api/index.md, bot-accounts/index.md, etc.). Rename both to index.md to match that convention, and update the ~20 cross-referencing links (many with #anchor fragments) that pointed at the old /reference/server/server-reference and /reference/webapp/webapp- reference paths. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
114 lines
4.6 KiB
JavaScript
114 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Generates docs/site/data/plugin-jsdocs.json, the data source consumed by the <PluginJsDocs />
|
|
// component that renders the web app plugin SDK reference
|
|
// (docs/develop/integrate/reference/webapp/index.md).
|
|
//
|
|
// Reads webapp/channels/src/plugins/registry.ts directly from this monorepo instead of fetching it
|
|
// from GitHub over HTTP.
|
|
//
|
|
// Usage: node scripts/gen-plugin-jsdocs.mjs (from docs/site/)
|
|
|
|
import {parse} from '@typescript-eslint/typescript-estree';
|
|
import {readFileSync, writeFileSync, mkdirSync} from 'node:fs';
|
|
import {resolve, dirname} from 'node:path';
|
|
import {fileURLToPath} from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url)); // docs/site/scripts
|
|
const SITE_ROOT = resolve(HERE, '..'); // docs/site
|
|
const REPO_ROOT = resolve(SITE_ROOT, '../..'); // mattermost/
|
|
const REGISTRY_PATH = resolve(REPO_ROOT, 'webapp/channels/src/plugins/registry.ts');
|
|
const OUT_PATH = resolve(SITE_ROOT, 'data/plugin-jsdocs.json');
|
|
|
|
function paramNamesFromPattern(pattern) {
|
|
if (pattern.type === 'Identifier') return [pattern.name];
|
|
if (pattern.type === 'ObjectPattern') {
|
|
return pattern.properties.flatMap((prop) => (prop.type === 'Property' ? paramNamesFromPattern(prop.value) : paramNamesFromPattern(prop)));
|
|
}
|
|
if (pattern.type === 'ArrayPattern') {
|
|
return pattern.elements.flatMap((el) => (el ? paramNamesFromPattern(el) : []));
|
|
}
|
|
if (pattern.type === 'AssignmentPattern') return paramNamesFromPattern(pattern.left);
|
|
if (pattern.type === 'RestElement') return paramNamesFromPattern(pattern.argument);
|
|
return [];
|
|
}
|
|
|
|
// Comments attached to `node`: walk backward from its start, collecting comments as long as only
|
|
// whitespace separates them from each other and from the node. Node-scoped, so unlike a global
|
|
// "which comments sit on consecutive lines" pass, it can't attribute a comment to the wrong member.
|
|
function leadingComments(sourceText, comments, node) {
|
|
const attached = [];
|
|
let cursor = node.range[0];
|
|
for (let i = comments.length - 1; i >= 0; i--) {
|
|
const comment = comments[i];
|
|
if (comment.range[1] > cursor) continue;
|
|
if (!/^\s*$/.test(sourceText.slice(comment.range[1], cursor))) break;
|
|
attached.unshift(comment);
|
|
cursor = comment.range[0];
|
|
}
|
|
return attached.flatMap((comment) =>
|
|
comment.value
|
|
.split('\n')
|
|
.map((line) => line.replace(/^\s*\*\s?/, '').trimEnd())
|
|
.filter((line) => line.length > 0),
|
|
);
|
|
}
|
|
|
|
// reArg(['name', ...], handler) documents its public parameter names explicitly in that array —
|
|
// that's the contract callers see, so prefer it over inferring names from the handler's own
|
|
// (possibly renamed or destructured) parameters.
|
|
function reArgParameterNames(callExpr) {
|
|
const [firstArg, ...rest] = callExpr.arguments;
|
|
if (callExpr.callee.type === 'Identifier' && callExpr.callee.name === 'reArg' && firstArg?.type === 'ArrayExpression') {
|
|
return firstArg.elements.filter((el) => el?.type === 'Literal' && typeof el.value === 'string').map((el) => el.value);
|
|
}
|
|
const handler = rest.find((arg) => arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression');
|
|
return handler ? handler.params.flatMap(paramNamesFromPattern) : [];
|
|
}
|
|
|
|
function findPluginRegistryClass(program) {
|
|
return program.body.find(
|
|
(statement) =>
|
|
statement.type === 'ExportDefaultDeclaration' &&
|
|
statement.declaration.type === 'ClassDeclaration' &&
|
|
statement.declaration.id?.name === 'PluginRegistry',
|
|
)?.declaration;
|
|
}
|
|
|
|
function main() {
|
|
const sourceText = readFileSync(REGISTRY_PATH, 'utf8');
|
|
const ast = parse(sourceText, {comment: true, range: true});
|
|
|
|
const classDecl = findPluginRegistryClass(ast);
|
|
if (!classDecl) {
|
|
throw new Error(`Could not find "export default class PluginRegistry" in ${REGISTRY_PATH}`);
|
|
}
|
|
|
|
const methods = [];
|
|
for (const member of classDecl.body.body) {
|
|
if (member.key?.type !== 'Identifier') continue;
|
|
|
|
let params;
|
|
if (member.type === 'MethodDefinition' && member.kind !== 'constructor') {
|
|
params = member.value.params.flatMap(paramNamesFromPattern);
|
|
} else if (member.type === 'PropertyDefinition' && member.value?.type === 'CallExpression') {
|
|
params = reArgParameterNames(member.value);
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
methods.push({
|
|
Name: member.key.name,
|
|
Parameters: params,
|
|
Comments: leadingComments(sourceText, ast.comments, member),
|
|
});
|
|
}
|
|
|
|
const output = {Interface: {Methods: methods}};
|
|
|
|
mkdirSync(dirname(OUT_PATH), {recursive: true});
|
|
writeFileSync(OUT_PATH, JSON.stringify(output, null, 2));
|
|
console.log(`[plugin-jsdocs] wrote ${methods.length} methods to ${OUT_PATH}`);
|
|
}
|
|
|
|
main();
|