snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
1
docs/site/scripts/_remaining-tab-div-files.snapshot.txt
Normal file
1
docs/site/scripts/_remaining-tab-div-files.snapshot.txt
Normal file
@@ -0,0 +1 @@
|
||||
(none — all `<div class="tab">` blocks converted to Docusaurus <Tabs>/<TabItem> in this session)
|
||||
85
docs/site/scripts/build-openapi.mjs
Normal file
85
docs/site/scripts/build-openapi.mjs
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Builds the merged OpenAPI spec by delegating to the canonical api/Makefile,
|
||||
* then sanitizes the output for MDX compatibility.
|
||||
*
|
||||
* Sanitization (description / summary / title strings):
|
||||
* 1) Embedded "..." breaks docusaurus-plugin-openapi-docs frontmatter quoting.
|
||||
* Replace with curly quotes (“ / ”).
|
||||
* 2) Markdown autolinks like </api/v4/foo> are parsed as JSX closing tags by MDX.
|
||||
* Escape bare < not followed by a letter or ! to <.
|
||||
*
|
||||
* Usage: node scripts/build-openapi.mjs (from docs/site/)
|
||||
* Output: docs/site/openapi/mattermost-openapi-v4.yaml
|
||||
*/
|
||||
|
||||
import {execSync} from 'node:child_process';
|
||||
import {readFileSync, writeFileSync, mkdirSync, existsSync} from 'node:fs';
|
||||
import {dirname, resolve} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {parse, stringify} from 'yaml';
|
||||
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); // docs/site/scripts
|
||||
const DOCS_SITE_DIR = resolve(SCRIPT_DIR, '..'); // docs/site
|
||||
const MONOREPO_ROOT = resolve(SCRIPT_DIR, '../../..'); // mattermost/
|
||||
const API_ROOT = resolve(MONOREPO_ROOT, 'api'); // mattermost/api
|
||||
const RAW_SPEC_PATH = resolve(API_ROOT, 'v4/html/static/mattermost-openapi-v4.yaml'); // make's own output
|
||||
const SANITIZED_SPEC_PATH = resolve(DOCS_SITE_DIR, 'openapi/mattermost-openapi-v4.yaml'); // what Docusaurus reads
|
||||
|
||||
function walkStrings(node, fn, key) {
|
||||
if (typeof node === 'string') return fn(node, key);
|
||||
if (Array.isArray(node)) {
|
||||
for (let i = 0; i < node.length; i++) node[i] = walkStrings(node[i], fn, key);
|
||||
return node;
|
||||
}
|
||||
if (node && typeof node === 'object') {
|
||||
for (const [k, v] of Object.entries(node)) node[k] = walkStrings(v, fn, k);
|
||||
return node;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!existsSync(API_ROOT)) {
|
||||
console.error(`[openapi] api/ directory not found at ${API_ROOT}`);
|
||||
console.error('[openapi] Ensure you are running from within the mattermost monorepo.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[openapi] Building OpenAPI spec via make -C ${API_ROOT} build`);
|
||||
execSync(`make -C ${API_ROOT} build`, {stdio: 'inherit'});
|
||||
|
||||
if (!existsSync(RAW_SPEC_PATH)) {
|
||||
console.error(`[openapi] Expected make output not found: ${RAW_SPEC_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('[openapi] Sanitizing for MDX compatibility ...');
|
||||
const doc = parse(readFileSync(RAW_SPEC_PATH, 'utf8'));
|
||||
|
||||
let quoteFixes = 0;
|
||||
let autolinkFixes = 0;
|
||||
walkStrings(doc, (value, key) => {
|
||||
if (key !== 'description' && key !== 'summary' && key !== 'title') return value;
|
||||
let v = value;
|
||||
if (v.includes('"')) {
|
||||
const fixed = v.replace(/"([^"]*)"/g, '“$1”');
|
||||
if (fixed !== v) { quoteFixes++; v = fixed; }
|
||||
}
|
||||
if (/<[^a-zA-Z!]/.test(v)) {
|
||||
const fixed = v.replace(/<(?=[^a-zA-Z!])/g, '<');
|
||||
if (fixed !== v) { autolinkFixes++; v = fixed; }
|
||||
}
|
||||
return v;
|
||||
});
|
||||
|
||||
console.log(`[openapi] Sanitized: quotes=${quoteFixes}, autolinks=${autolinkFixes}`);
|
||||
|
||||
mkdirSync(dirname(SANITIZED_SPEC_PATH), {recursive: true});
|
||||
writeFileSync(SANITIZED_SPEC_PATH, stringify(doc, {lineWidth: 0}));
|
||||
|
||||
const sizeMb = (readFileSync(SANITIZED_SPEC_PATH).length / 1024 / 1024).toFixed(2);
|
||||
console.log(`[openapi] Wrote ${sizeMb} MB → ${SANITIZED_SPEC_PATH}`);
|
||||
}
|
||||
|
||||
main();
|
||||
67
docs/site/scripts/duplicate-redirect-stubs.mjs
Normal file
67
docs/site/scripts/duplicate-redirect-stubs.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
// Copy every plugin-client-redirects stub from `build/<from>/index.html` to
|
||||
// `build/<from>.html` so the docs CloudFront function's `<uri>.html` fallback
|
||||
// branch finds it (see mattermost-iac-docs-prod → 05-cdn →
|
||||
// clean-url-rewrite.js).
|
||||
|
||||
import {readFileSync, writeFileSync, existsSync, statSync} from 'node:fs';
|
||||
import {join, resolve, dirname, sep} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SITE_ROOT = resolve(HERE, '..');
|
||||
const BUILD_DIR = join(SITE_ROOT, 'build');
|
||||
const REDIRECTS_JSON = join(SITE_ROOT, 'sidebars', 'active-redirects.json');
|
||||
|
||||
if (!existsSync(BUILD_DIR)) {
|
||||
console.error(`[duplicate-redirect-stubs] ${BUILD_DIR} not found; run \`docusaurus build\` first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const {redirects} = JSON.parse(readFileSync(REDIRECTS_JSON, 'utf8'));
|
||||
const BUILD_DIR_PREFIX = BUILD_DIR + sep;
|
||||
|
||||
let duplicated = 0;
|
||||
let skippedExisting = 0;
|
||||
let skippedMissingStub = 0;
|
||||
let skippedHtmlSuffix = 0;
|
||||
let skippedOutsideBuild = 0;
|
||||
|
||||
for (const {from} of redirects) {
|
||||
// CFF Phase A already 301s `.html` to the extensionless form.
|
||||
if (from.endsWith('.html')) {
|
||||
skippedHtmlSuffix++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const rel = from.replace(/^\/+/, '');
|
||||
const stubPath = resolve(BUILD_DIR, rel, 'index.html');
|
||||
const flatPath = resolve(BUILD_DIR, `${rel}.html`);
|
||||
if (!stubPath.startsWith(BUILD_DIR_PREFIX) || !flatPath.startsWith(BUILD_DIR_PREFIX)) {
|
||||
skippedOutsideBuild++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existsSync(stubPath) || !statSync(stubPath).isFile()) {
|
||||
skippedMissingStub++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Never shadow a real content page at the flat key.
|
||||
if (existsSync(flatPath)) {
|
||||
skippedExisting++;
|
||||
continue;
|
||||
}
|
||||
|
||||
writeFileSync(flatPath, readFileSync(stubPath));
|
||||
duplicated++;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[duplicate-redirect-stubs] duplicated=${duplicated} ` +
|
||||
`skipped-existing=${skippedExisting} ` +
|
||||
`skipped-missing-stub=${skippedMissingStub} ` +
|
||||
`skipped-html-suffix=${skippedHtmlSuffix} ` +
|
||||
`skipped-outside-build=${skippedOutsideBuild} ` +
|
||||
`total=${redirects.length}`,
|
||||
);
|
||||
104
docs/site/scripts/gen-active-redirects.mjs
Normal file
104
docs/site/scripts/gen-active-redirects.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
// Filter the raw redirects.json (extracted from the legacy Sphinx
|
||||
// redirects.py) to the subset whose target MDX page actually exists in
|
||||
// the migrated `docs/` tree. Output:
|
||||
// docs-site/sidebars/active-redirects.json
|
||||
// (kept in the sidebars/ folder since that's already gitignore-aware and
|
||||
// regenerated). Consumed by docusaurus.config.ts via
|
||||
// @docusaurus/plugin-client-redirects.
|
||||
//
|
||||
// Internal redirects whose destination is still missing (target page not
|
||||
// yet migrated, or marked draft) are dropped silently — they'll start
|
||||
// working as soon as the destination lands. External redirects (to
|
||||
// github.com, other-host) need server-side rules and aren't emitted here.
|
||||
//
|
||||
// Usage: node docs-site/scripts/gen-active-redirects.mjs
|
||||
|
||||
import {readFileSync, writeFileSync, readdirSync, statSync, existsSync} from 'node:fs';
|
||||
import {join, resolve, basename, dirname} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SITE_ROOT = resolve(HERE, '..');
|
||||
const REPO_ROOT = resolve(SITE_ROOT, '..');
|
||||
const DOCS = join(REPO_ROOT, 'main');
|
||||
const SRC = join(SITE_ROOT, 'scripts', 'migrate-main-docs', 'redirects.json');
|
||||
const OUT = join(SITE_ROOT, 'sidebars', 'active-redirects.json');
|
||||
|
||||
function isDraft(filePath) {
|
||||
try {
|
||||
const head = readFileSync(filePath, 'utf8').slice(0, 800);
|
||||
return /^draft:\s*true/m.test(head);
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function urlFromMdx(absPath) {
|
||||
// docs/foo/bar.mdx → /foo/bar (index.mdx → /foo)
|
||||
let rel = absPath.slice(DOCS.length).replace(/\\/g, '/');
|
||||
rel = rel.replace(/\.mdx$/, '');
|
||||
if (rel.endsWith('/index')) rel = rel.slice(0, -'/index'.length);
|
||||
return rel || '/';
|
||||
}
|
||||
|
||||
function collectExistingUrls() {
|
||||
const urls = new Set();
|
||||
(function walk(dir) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const p = join(dir, name);
|
||||
const s = statSync(p);
|
||||
if (s.isDirectory()) walk(p);
|
||||
else if (p.endsWith('.mdx') && !isDraft(p)) urls.add(urlFromMdx(p));
|
||||
}
|
||||
})(DOCS);
|
||||
return urls;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const raw = JSON.parse(readFileSync(SRC, 'utf8'));
|
||||
const existing = collectExistingUrls();
|
||||
const active = [];
|
||||
const missing = [];
|
||||
const droppedAnchoredFrom = [];
|
||||
for (const r of raw.internal) {
|
||||
// @docusaurus/plugin-client-redirects requires `from` paths without
|
||||
// anchors. Sphinx allowed anchored sources for mid-page redirects —
|
||||
// those need server-side rules, not client-side. Drop them.
|
||||
if (r.from.includes('#')) { droppedAnchoredFrom.push(r); continue; }
|
||||
// Strip anchor when checking existence; preserve it in the emitted to.
|
||||
const target = r.to.split('#')[0];
|
||||
if (existing.has(target)) active.push({from: r.from, to: r.to});
|
||||
else missing.push(r);
|
||||
}
|
||||
// De-duplicate: the plugin refuses duplicate FROM values.
|
||||
const seenFrom = new Map();
|
||||
const final = [];
|
||||
for (const r of active) {
|
||||
if (seenFrom.has(r.from)) continue;
|
||||
seenFrom.set(r.from, true);
|
||||
final.push(r);
|
||||
}
|
||||
writeFileSync(
|
||||
OUT,
|
||||
JSON.stringify(
|
||||
{
|
||||
_meta: {
|
||||
source: 'docs-site/scripts/migrate-main-docs/redirects.json',
|
||||
active: final.length,
|
||||
missing_target: missing.length,
|
||||
dropped_anchored_from: droppedAnchoredFrom.length,
|
||||
total_internal: raw.internal.length,
|
||||
},
|
||||
redirects: final,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
console.log(`[redirects] wrote ${OUT}`);
|
||||
console.log(` active: ${final.length}`);
|
||||
console.log(` missing target: ${missing.length}`);
|
||||
console.log(` dropped (anchored): ${droppedAnchoredFrom.length}`);
|
||||
console.log(` total internal: ${raw.internal.length}`);
|
||||
}
|
||||
|
||||
main();
|
||||
122
docs/site/scripts/gen-developer-sidebar.mjs
Normal file
122
docs/site/scripts/gen-developer-sidebar.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env node
|
||||
// Generate the Developers sidebar from the migrated content tree under
|
||||
// develop/. Output: docs-site/sidebars/developers.generated.json
|
||||
//
|
||||
// Usage: node docs-site/scripts/gen-developer-sidebar.mjs
|
||||
|
||||
import {readFileSync, writeFileSync, readdirSync, statSync, existsSync} from 'node:fs';
|
||||
import {join, resolve, basename, dirname} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SITE_ROOT = resolve(HERE, '..');
|
||||
const REPO_ROOT = resolve(SITE_ROOT, '..');
|
||||
const SRC = join(REPO_ROOT, 'develop');
|
||||
const OUT = join(SITE_ROOT, 'sidebars', 'developers.generated.json');
|
||||
|
||||
const TOP_LEVEL = [
|
||||
{dir: 'contribute', label: 'Contribute'},
|
||||
{dir: 'integrate', label: 'Integrate & Extend'},
|
||||
{dir: 'internal', label: 'Internal'},
|
||||
];
|
||||
|
||||
function humanize(name) {
|
||||
return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function readFm(filePath, key) {
|
||||
try {
|
||||
const text = readFileSync(filePath, 'utf8').slice(0, 2000);
|
||||
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!m) return null;
|
||||
const r = new RegExp(`^${key}:\\s*"?([^"\\n]+)"?`, 'm');
|
||||
const x = m[1].match(r);
|
||||
return x ? x[1].trim().replace(/^"|"$/g, '') : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function pathToDocId(relPath) { return relPath.replace(/\.(md|mdx)$/, ''); }
|
||||
|
||||
function buildCategory(absDir, devRelDir) {
|
||||
const items = [];
|
||||
const entries = readdirSync(absDir);
|
||||
const indexFile = entries.find((e) => /^index\.(md|mdx)$/.test(e));
|
||||
let categoryLink = null;
|
||||
if (indexFile) {
|
||||
categoryLink = {type: 'doc', id: pathToDocId(join(devRelDir, indexFile))};
|
||||
}
|
||||
|
||||
const subDirs = [];
|
||||
const leafDocs = [];
|
||||
for (const name of entries) {
|
||||
if (/^index\.(md|mdx)$/.test(name)) continue;
|
||||
const abs = join(absDir, name);
|
||||
const st = statSync(abs);
|
||||
if (st.isDirectory()) subDirs.push(name);
|
||||
else if (st.isFile() && /\.(md|mdx)$/.test(name)) leafDocs.push(name);
|
||||
}
|
||||
|
||||
function key(name, abs) {
|
||||
const p = readFm(abs, 'sidebar_position');
|
||||
return [p ? Number(p) : 9999, name.toLowerCase()];
|
||||
}
|
||||
leafDocs.sort((a, b) => {
|
||||
const ka = key(a, join(absDir, a));
|
||||
const kb = key(b, join(absDir, b));
|
||||
return ka[0] - kb[0] || ka[1].localeCompare(kb[1]);
|
||||
});
|
||||
subDirs.sort((a, b) => {
|
||||
const ka = key(a, join(absDir, a, 'index.md'));
|
||||
const kb = key(b, join(absDir, b, 'index.md'));
|
||||
return ka[0] - kb[0] || ka[1].localeCompare(kb[1]);
|
||||
});
|
||||
|
||||
for (const name of leafDocs) {
|
||||
const id = pathToDocId(join(devRelDir, name));
|
||||
const label = readFm(join(absDir, name), 'title') || humanize(basename(name, /\.(md|mdx)$/.exec(name)[0]));
|
||||
items.push({type: 'doc', id, label});
|
||||
}
|
||||
for (const name of subDirs) {
|
||||
const sub = buildCategory(join(absDir, name), join(devRelDir, name));
|
||||
// Skip empty subcategories — Docusaurus refuses sidebar categories that
|
||||
// have neither a link nor any items.
|
||||
if (sub) items.push(sub);
|
||||
}
|
||||
|
||||
const label =
|
||||
readFm(join(absDir, 'index.md'), 'title') ||
|
||||
readFm(join(absDir, 'index.mdx'), 'title') ||
|
||||
humanize(basename(absDir));
|
||||
|
||||
if (!categoryLink && items.length === 0) return null;
|
||||
|
||||
return {type: 'category', label, collapsed: true, ...(categoryLink ? {link: categoryLink} : {}), items};
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!existsSync(SRC)) { console.error(`develop/ not found at ${SRC}`); process.exit(1); }
|
||||
|
||||
const sidebar = [{type: 'doc', id: 'index', label: 'Welcome'}];
|
||||
for (const {dir, label} of TOP_LEVEL) {
|
||||
const abs = join(SRC, dir);
|
||||
if (!existsSync(abs)) { console.warn(`[sidebar] skip ${dir}: missing`); continue; }
|
||||
const cat = buildCategory(abs, dir);
|
||||
cat.label = label;
|
||||
cat.collapsed = true;
|
||||
sidebar.push(cat);
|
||||
}
|
||||
writeFileSync(OUT, JSON.stringify(sidebar, null, 2));
|
||||
|
||||
let cats = 0, docs = 0;
|
||||
(function walk(n) {
|
||||
if (Array.isArray(n)) n.forEach(walk);
|
||||
else if (n && typeof n === 'object') {
|
||||
if (n.type === 'category') cats++;
|
||||
if (n.type === 'doc') docs++;
|
||||
if (n.items) walk(n.items);
|
||||
}
|
||||
})(sidebar);
|
||||
console.log(`[sidebar] wrote ${OUT}: ${cats} categories, ${docs} docs`);
|
||||
}
|
||||
|
||||
main();
|
||||
2124
docs/site/scripts/gen-documentation-sidebar.mjs
Normal file
2124
docs/site/scripts/gen-documentation-sidebar.mjs
Normal file
File diff suppressed because it is too large
Load Diff
99
docs/site/scripts/gen-hex-camo.mjs
Normal file
99
docs/site/scripts/gen-hex-camo.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Generates a tileable hexagonal camouflage SVG, matching the Mattermost
|
||||
* brand book "Hexagonal Camouflage" pattern (denim variant, primary
|
||||
* brand background).
|
||||
*
|
||||
* Output: docs-site/static/img/patterns/hex-camo-denim.svg
|
||||
*
|
||||
* Re-run if you want to tweak the shade mix / density / cell size.
|
||||
*/
|
||||
|
||||
import {writeFileSync, mkdirSync} from 'node:fs';
|
||||
import {dirname, join, resolve} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT = resolve(HERE, '../static/img/patterns/hex-camo-denim.svg');
|
||||
|
||||
// Tile dimensions. Side=9 → ~3x smaller cells than v1 (was side=24).
|
||||
// Reads as fine grain / texture rather than chunky mosaic.
|
||||
const SIDE = 9;
|
||||
const HEX_W = 2 * SIDE;
|
||||
const HEX_H = Math.sqrt(3) * SIDE;
|
||||
const COL_STEP = 1.5 * SIDE;
|
||||
const ROW_STEP = HEX_H;
|
||||
|
||||
const COLS = 8; // 8 cols × 8 rows ≈ 108×125 tile
|
||||
const ROWS = 8;
|
||||
const TILE_W = COL_STEP * COLS;
|
||||
const TILE_H = ROW_STEP * ROWS;
|
||||
|
||||
// Tight shade band — every cell is within a narrow window of denim-500.
|
||||
// The base color matches the hero's solid bg, so most cells visually
|
||||
// disappear; only the slightly-lighter cells provide the texture beat.
|
||||
// No marigold / no high-contrast outliers.
|
||||
const SHADES = [
|
||||
{color: '#1E325C', weight: 14}, // denim-500 (matches hero bg — invisible cells)
|
||||
{color: '#22386A', weight: 5}, // +4 lightness — faintly visible
|
||||
{color: '#172A4F', weight: 3}, // -2 lightness — adds depth shadow
|
||||
];
|
||||
|
||||
// Stable PRNG so the SVG is deterministic and reproducible across runs.
|
||||
function mulberry32(seed) {
|
||||
return () => {
|
||||
seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
|
||||
let t = seed;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
const rand = mulberry32(0xCAFE);
|
||||
|
||||
function pickShade() {
|
||||
const total = SHADES.reduce((s, x) => s + x.weight, 0);
|
||||
let r = rand() * total;
|
||||
for (const s of SHADES) {
|
||||
if (r < s.weight) return s.color;
|
||||
r -= s.weight;
|
||||
}
|
||||
return SHADES[0].color;
|
||||
}
|
||||
|
||||
function hexPath(cx, cy) {
|
||||
const pts = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const a = Math.PI / 3 * i;
|
||||
pts.push(`${(cx + SIDE * Math.cos(a)).toFixed(2)},${(cy + SIDE * Math.sin(a)).toFixed(2)}`);
|
||||
}
|
||||
return `M${pts.join(' L')}Z`;
|
||||
}
|
||||
|
||||
const polygons = [];
|
||||
// Render extra cells beyond the tile edge so the seam is hidden when tiled.
|
||||
for (let col = -1; col <= COLS + 1; col++) {
|
||||
for (let row = -1; row <= ROWS + 1; row++) {
|
||||
const cx = col * COL_STEP;
|
||||
const cy = row * ROW_STEP + (col % 2 ? ROW_STEP / 2 : 0);
|
||||
polygons.push(`<path d="${hexPath(cx, cy)}" fill="${pickShade()}"/>`);
|
||||
}
|
||||
}
|
||||
|
||||
const svg = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${TILE_W}" height="${TILE_H}"
|
||||
viewBox="0 0 ${TILE_W} ${TILE_H}"
|
||||
preserveAspectRatio="xMidYMid slice">
|
||||
<defs>
|
||||
<clipPath id="tile"><rect width="${TILE_W}" height="${TILE_H}"/></clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#tile)">
|
||||
<rect width="${TILE_W}" height="${TILE_H}" fill="#1E325C"/>
|
||||
${polygons.join('\n ')}
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
mkdirSync(dirname(OUT), {recursive: true});
|
||||
writeFileSync(OUT, svg);
|
||||
console.log(`wrote ${OUT} (${TILE_W}×${TILE_H.toFixed(1)})`);
|
||||
3
docs/site/scripts/gen-plugin-godocs/go.mod
Normal file
3
docs/site/scripts/gen-plugin-godocs/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/mattermost/mattermost/docs/site/scripts/gen-plugin-godocs
|
||||
|
||||
go 1.26
|
||||
324
docs/site/scripts/gen-plugin-godocs/main.go
Normal file
324
docs/site/scripts/gen-plugin-godocs/main.go
Normal file
@@ -0,0 +1,324 @@
|
||||
// Command gen-plugin-godocs generates docs/site/data/plugin-godocs.json, the data source consumed
|
||||
// by the <PluginGoDocs /> and <PluginGoExample /> React components that render the server plugin
|
||||
// SDK reference (docs/develop/integrate/reference/server/index.md).
|
||||
//
|
||||
// It reads the server/public/plugin package directly from this monorepo. It deliberately parses
|
||||
// the package with go/parser + go/doc rather than type-checking it via golang.org/x/tools/go/packages,
|
||||
// so it has no dependency on the Go toolchain version required by server/public/go.mod (the docs
|
||||
// site's build environment may lag behind it).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const pluginImportPath = "github.com/mattermost/mattermost/server/public/plugin"
|
||||
|
||||
type Field struct {
|
||||
Names []string `json:"Names,omitempty"`
|
||||
Type string
|
||||
}
|
||||
|
||||
type MethodDocs struct {
|
||||
Name string
|
||||
Tags []string `json:"Tags,omitempty"`
|
||||
HTML string
|
||||
Parameters []*Field `json:"Parameters,omitempty"`
|
||||
Results []*Field `json:"Results,omitempty"`
|
||||
}
|
||||
|
||||
type InterfaceDocs struct {
|
||||
HTML string
|
||||
Tags []string `json:"Tags,omitempty"`
|
||||
Methods []*MethodDocs
|
||||
}
|
||||
|
||||
type ExampleDocs struct {
|
||||
HTML string
|
||||
Code string
|
||||
}
|
||||
|
||||
type Docs struct {
|
||||
HTML string
|
||||
API InterfaceDocs
|
||||
Hooks InterfaceDocs
|
||||
Helpers InterfaceDocs
|
||||
Examples map[string]*ExampleDocs
|
||||
}
|
||||
|
||||
func pluginPackageDir() string {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
log.Fatal("unable to determine source file location")
|
||||
}
|
||||
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", ".."))
|
||||
return filepath.Join(repoRoot, "server", "public", "plugin")
|
||||
}
|
||||
|
||||
func docHTML(text string) string {
|
||||
buf := &bytes.Buffer{}
|
||||
doc.ToHTML(buf, text, nil)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func removeDuplicates(items []string) []string {
|
||||
seen := make(map[string]bool, len(items))
|
||||
out := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
if !seen[item] {
|
||||
seen[item] = true
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var tagRegexp = regexp.MustCompile(`@tag\s+(\w+)\s*`)
|
||||
|
||||
func tags(text string) []string {
|
||||
submatches := tagRegexp.FindAllStringSubmatch(text, -1)
|
||||
out := make([]string, len(submatches))
|
||||
for i, submatch := range submatches {
|
||||
out[i] = submatch[1]
|
||||
}
|
||||
return removeDuplicates(out)
|
||||
}
|
||||
|
||||
// builtinTypes is the set of predeclared Go types documented at https://pkg.go.dev/builtin.
|
||||
var builtinTypes = map[string]bool{
|
||||
"bool": true, "byte": true, "complex128": true, "complex64": true, "error": true,
|
||||
"float32": true, "float64": true, "int": true, "int16": true, "int32": true, "int64": true,
|
||||
"int8": true, "rune": true, "string": true, "uint": true, "uint16": true, "uint32": true,
|
||||
"uint64": true, "uint8": true, "uintptr": true, "any": true,
|
||||
}
|
||||
|
||||
// importAliases maps the local identifier a file uses for an imported package (its alias, or the
|
||||
// last path segment when unaliased) to that package's full import path.
|
||||
func importAliases(file *ast.File) map[string]string {
|
||||
aliases := make(map[string]string)
|
||||
for _, imp := range file.Imports {
|
||||
path := strings.Trim(imp.Path.Value, `"`)
|
||||
alias := path[strings.LastIndex(path, "/")+1:]
|
||||
if imp.Name != nil {
|
||||
alias = imp.Name.Name
|
||||
}
|
||||
aliases[alias] = path
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
// typeString renders a field's type expression as a dotted, fully-qualified string, e.g.
|
||||
// "[]*github.com/mattermost/mattermost/server/public/model.Manifest", the same shape the old
|
||||
// go/types-based generator produced. Types local to the plugin package are qualified with
|
||||
// pluginImportPath so the renderer can link to them the same way it links to imported types.
|
||||
func typeString(expr ast.Expr, aliases map[string]string) string {
|
||||
switch x := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
return "*" + typeString(x.X, aliases)
|
||||
case *ast.ArrayType:
|
||||
return "[]" + typeString(x.Elt, aliases)
|
||||
case *ast.Ellipsis:
|
||||
return "[]" + typeString(x.Elt, aliases)
|
||||
case *ast.MapType:
|
||||
return "map[" + typeString(x.Key, aliases) + "]" + typeString(x.Value, aliases)
|
||||
case *ast.ChanType:
|
||||
return "chan " + typeString(x.Value, aliases)
|
||||
case *ast.InterfaceType:
|
||||
if x.Methods == nil || len(x.Methods.List) == 0 {
|
||||
return "interface{}"
|
||||
}
|
||||
return "interface{ ... }"
|
||||
case *ast.SelectorExpr:
|
||||
pkgIdent, ok := x.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return x.Sel.Name
|
||||
}
|
||||
if path, ok := aliases[pkgIdent.Name]; ok {
|
||||
return path + "." + x.Sel.Name
|
||||
}
|
||||
return pkgIdent.Name + "." + x.Sel.Name
|
||||
case *ast.Ident:
|
||||
if builtinTypes[x.Name] || x.Name == "byte" || x.Name == "rune" {
|
||||
return x.Name
|
||||
}
|
||||
// A bare identifier that isn't a builtin must refer to a type declared in this same
|
||||
// package (plugin), since any imported type is always qualified with a selector.
|
||||
return pluginImportPath + "." + x.Name
|
||||
default:
|
||||
buf := &bytes.Buffer{}
|
||||
_ = printer.Fprint(buf, token.NewFileSet(), expr)
|
||||
return buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
func fields(list *ast.FieldList, aliases map[string]string) (out []*Field) {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
for _, x := range list.List {
|
||||
field := &Field{}
|
||||
for _, name := range x.Names {
|
||||
field.Names = append(field.Names, name.Name)
|
||||
}
|
||||
|
||||
t := typeString(x.Type, aliases)
|
||||
if _, ok := x.Type.(*ast.Ellipsis); ok {
|
||||
t = "..." + strings.TrimPrefix(t, "[]")
|
||||
}
|
||||
field.Type = t
|
||||
|
||||
out = append(out, field)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fileForPos(fset *token.FileSet, files map[string]*ast.File, pos token.Pos) *ast.File {
|
||||
name := fset.Position(pos).Filename
|
||||
return files[name]
|
||||
}
|
||||
|
||||
func generateDocs() (*Docs, error) {
|
||||
pluginDir := pluginPackageDir()
|
||||
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, pluginDir, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docs := &Docs{
|
||||
Examples: make(map[string]*ExampleDocs),
|
||||
}
|
||||
|
||||
// Files keyed by absolute path, across both the "plugin" and "plugin_test" (external test)
|
||||
// packages, so we can recover which file a given interface method came from and resolve its
|
||||
// imports, and so examples defined in either package are picked up.
|
||||
filesByPath := make(map[string]*ast.File)
|
||||
var allFiles []*ast.File
|
||||
for _, pkg := range pkgs {
|
||||
for path, file := range pkg.Files {
|
||||
filesByPath[path] = file
|
||||
allFiles = append(allFiles, file)
|
||||
}
|
||||
}
|
||||
|
||||
for _, example := range doc.Examples(allFiles...) {
|
||||
// Play is a synthesized, standalone runnable program and is preferred when available;
|
||||
// it's nil when go/doc can't build one (e.g. the example can't be wrapped as a whole
|
||||
// program), in which case Code — the example function's body, always non-nil — is used
|
||||
// instead.
|
||||
var node ast.Node = example.Play
|
||||
if example.Play == nil {
|
||||
node = example.Code
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := printer.Fprint(buf, fset, node); err != nil {
|
||||
return nil, fmt.Errorf("failed to print example %q: %w", example.Name, err)
|
||||
}
|
||||
docs.Examples[example.Name] = &ExampleDocs{
|
||||
HTML: docHTML(example.Doc),
|
||||
Code: buf.String(),
|
||||
}
|
||||
}
|
||||
|
||||
pluginPkg, ok := pkgs["plugin"]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
godocs := doc.New(pluginPkg, pluginImportPath, doc.Mode(0))
|
||||
|
||||
if godocs.Name == "plugin" && godocs.Doc != "" {
|
||||
docs.HTML = docHTML(godocs.Doc)
|
||||
}
|
||||
|
||||
for _, t := range godocs.Types {
|
||||
var interfaceDocs *InterfaceDocs
|
||||
switch t.Name {
|
||||
case "API":
|
||||
interfaceDocs = &docs.API
|
||||
case "Hooks":
|
||||
interfaceDocs = &docs.Hooks
|
||||
case "Helpers":
|
||||
interfaceDocs = &docs.Helpers
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if t.Doc != "" {
|
||||
interfaceDocs.HTML = docHTML(t.Doc)
|
||||
}
|
||||
|
||||
for _, spec := range t.Decl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
iface, ok := typeSpec.Type.(*ast.InterfaceType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
file := fileForPos(fset, filesByPath, typeSpec.Pos())
|
||||
var aliases map[string]string
|
||||
if file != nil {
|
||||
aliases = importAliases(file)
|
||||
}
|
||||
|
||||
allTags := make([]string, 0)
|
||||
for _, method := range iface.Methods.List {
|
||||
funcType, ok := method.Type.(*ast.FuncType)
|
||||
if !ok || len(method.Names) == 0 {
|
||||
continue
|
||||
}
|
||||
methodDocs := &MethodDocs{
|
||||
Name: method.Names[0].Name,
|
||||
Tags: tags(method.Doc.Text()),
|
||||
HTML: docHTML(method.Doc.Text()),
|
||||
Parameters: fields(funcType.Params, aliases),
|
||||
Results: fields(funcType.Results, aliases),
|
||||
}
|
||||
interfaceDocs.Methods = append(interfaceDocs.Methods, methodDocs)
|
||||
allTags = append(allTags, methodDocs.Tags...)
|
||||
}
|
||||
allTags = removeDuplicates(allTags)
|
||||
sort.Strings(allTags)
|
||||
interfaceDocs.Tags = allTags
|
||||
}
|
||||
}
|
||||
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
docs, err := generateDocs()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(docs, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stdout.Write(b); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stdout.Write([]byte("\n")); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
113
docs/site/scripts/gen-plugin-jsdocs.mjs
Normal file
113
docs/site/scripts/gen-plugin-jsdocs.mjs
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/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();
|
||||
3
docs/site/scripts/gen-plugin-manifest-docs/go.mod
Normal file
3
docs/site/scripts/gen-plugin-manifest-docs/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/mattermost/mattermost/docs/site/scripts/gen-plugin-manifest-docs
|
||||
|
||||
go 1.26
|
||||
278
docs/site/scripts/gen-plugin-manifest-docs/main.go
Normal file
278
docs/site/scripts/gen-plugin-manifest-docs/main.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// Command gen-plugin-manifest-docs generates docs/site/data/plugin-manifest-docs.json, the data
|
||||
// source consumed by the <PluginManifestDocs /> component that renders the plugin manifest
|
||||
// reference (docs/develop/integrate/plugins/manifest-reference.md).
|
||||
//
|
||||
// It reads server/public/model directly from this monorepo, walking the AST with go/parser +
|
||||
// go/doc instead of type-checking with golang.org/x/tools/go/packages (see gen-plugin-godocs for
|
||||
// why: it avoids requiring a Go toolchain matching server/public/go.mod's declared version).
|
||||
// Because of that, type resolution only follows types declared inside the model package's own
|
||||
// source (which is all model.Manifest ever references) — it doesn't resolve identifiers to types
|
||||
// from other packages.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SchemaType string
|
||||
|
||||
const (
|
||||
Array SchemaType = "array"
|
||||
Bool SchemaType = "bool"
|
||||
Dict SchemaType = "dict"
|
||||
Number SchemaType = "number"
|
||||
Object SchemaType = "object"
|
||||
String SchemaType = "string"
|
||||
Interface SchemaType = "interface"
|
||||
)
|
||||
|
||||
const modelImportPath = "github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
type ObjectProperty struct {
|
||||
Name string
|
||||
DocHTML string `json:"DocHTML,omitempty"`
|
||||
Schema *TypeDocs `json:"Schema,omitempty"`
|
||||
}
|
||||
|
||||
type TypeDocs struct {
|
||||
Type SchemaType
|
||||
DocHTML string `json:"DocHTML,omitempty"`
|
||||
ObjectProperties []*ObjectProperty `json:"ObjectProperties,omitempty"`
|
||||
ValueSchema *TypeDocs `json:"ValueSchema,omitempty"`
|
||||
}
|
||||
|
||||
type Docs struct {
|
||||
Schema *TypeDocs
|
||||
}
|
||||
|
||||
var basicTypeKinds = map[string]SchemaType{
|
||||
"string": String,
|
||||
"bool": Bool,
|
||||
"byte": Number,
|
||||
"rune": Number,
|
||||
"int": Number, "int8": Number, "int16": Number, "int32": Number, "int64": Number,
|
||||
"uint": Number, "uint8": Number, "uint16": Number, "uint32": Number, "uint64": Number,
|
||||
"float32": Number, "float64": Number,
|
||||
}
|
||||
|
||||
// fset and filesByPath let namedTypeDocs recover which file (and therefore which imports) a given
|
||||
// model package type was declared in, so SelectorExpr fields can be resolved against the model
|
||||
// package's imports rather than by identifier name alone. Populated once in generateDocs.
|
||||
var (
|
||||
fset *token.FileSet
|
||||
filesByPath map[string]*ast.File
|
||||
)
|
||||
|
||||
// importAliases maps the local identifier a file uses for an imported package (its alias, or the
|
||||
// last path segment when unaliased) to that package's full import path.
|
||||
func importAliases(file *ast.File) map[string]string {
|
||||
aliases := make(map[string]string)
|
||||
for _, imp := range file.Imports {
|
||||
path := strings.Trim(imp.Path.Value, `"`)
|
||||
alias := path[strings.LastIndex(path, "/")+1:]
|
||||
if imp.Name != nil {
|
||||
alias = imp.Name.Name
|
||||
}
|
||||
aliases[alias] = path
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
func importsForPos(pos token.Pos) map[string]string {
|
||||
file := filesByPath[fset.Position(pos).Filename]
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
return importAliases(file)
|
||||
}
|
||||
|
||||
func modelPackageDir() string {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
log.Fatal("unable to determine source file location")
|
||||
}
|
||||
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", ".."))
|
||||
return filepath.Join(repoRoot, "server", "public", "model")
|
||||
}
|
||||
|
||||
func docHTML(text string) string {
|
||||
buf := &bytes.Buffer{}
|
||||
doc.ToHTML(buf, text, nil)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func typeSpecOf(t *doc.Type) *ast.TypeSpec {
|
||||
for _, spec := range t.Decl.Specs {
|
||||
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
|
||||
return typeSpec
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func namedTypeDocs(t *doc.Type, typesByName map[string]*doc.Type) *TypeDocs {
|
||||
spec := typeSpecOf(t)
|
||||
if spec == nil {
|
||||
return nil
|
||||
}
|
||||
ret := exprTypeDocs(spec.Type, typesByName, importsForPos(spec.Pos()))
|
||||
if ret != nil {
|
||||
ret.DocHTML = docHTML(t.Doc)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// jsonFieldName returns the field's JSON key, mirroring encoding/json's own rules: unexported
|
||||
// fields and those tagged `json:"-"` are never included; otherwise the tag's name component wins
|
||||
// if present (including when a comma-only tag like `json:",omitempty"` leaves it empty, in which
|
||||
// case the Go field name is used, same as an absent tag).
|
||||
func jsonFieldName(field *ast.Field) string {
|
||||
if len(field.Names) == 0 {
|
||||
// Anonymous (embedded) fields aren't handled by this generator — none exist in the
|
||||
// reachable model.Manifest schema.
|
||||
return ""
|
||||
}
|
||||
fieldName := field.Names[0].Name
|
||||
if !ast.IsExported(fieldName) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if field.Tag == nil {
|
||||
return fieldName
|
||||
}
|
||||
tagValue := strings.Trim(field.Tag.Value, "`")
|
||||
jsonTag := reflect.StructTag(tagValue).Get("json")
|
||||
if jsonTag == "-" {
|
||||
return ""
|
||||
}
|
||||
name := strings.SplitN(jsonTag, ",", 2)[0]
|
||||
if name == "" {
|
||||
return fieldName
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func exprTypeDocs(expr ast.Expr, typesByName map[string]*doc.Type, imports map[string]string) *TypeDocs {
|
||||
switch x := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
return exprTypeDocs(x.X, typesByName, imports)
|
||||
case *ast.ArrayType:
|
||||
return &TypeDocs{
|
||||
Type: Array,
|
||||
ValueSchema: exprTypeDocs(x.Elt, typesByName, imports),
|
||||
}
|
||||
case *ast.MapType:
|
||||
return &TypeDocs{
|
||||
Type: Dict,
|
||||
ValueSchema: exprTypeDocs(x.Value, typesByName, imports),
|
||||
}
|
||||
case *ast.StructType:
|
||||
ret := &TypeDocs{Type: Object}
|
||||
for _, field := range x.Fields.List {
|
||||
name := jsonFieldName(field)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
ret.ObjectProperties = append(ret.ObjectProperties, &ObjectProperty{
|
||||
Name: name,
|
||||
DocHTML: docHTML(field.Doc.Text()),
|
||||
Schema: exprTypeDocs(field.Type, typesByName, imports),
|
||||
})
|
||||
}
|
||||
return ret
|
||||
case *ast.SelectorExpr:
|
||||
pkgIdent, ok := x.X.(*ast.Ident)
|
||||
if !ok || imports[pkgIdent.Name] != modelImportPath {
|
||||
log.Printf("unrecognized qualified type %v (not a reference to the model package)", x)
|
||||
return nil
|
||||
}
|
||||
// A qualified reference to the model package's own import path (i.e. a self-import
|
||||
// alias) still names a type declared in this same package, so resolve it the same way
|
||||
// an unqualified identifier would be.
|
||||
return exprTypeDocs(x.Sel, typesByName, imports)
|
||||
case *ast.InterfaceType:
|
||||
return &TypeDocs{Type: Interface}
|
||||
case *ast.Ident:
|
||||
if x.Name == "any" {
|
||||
return &TypeDocs{Type: Interface}
|
||||
}
|
||||
if kind, ok := basicTypeKinds[x.Name]; ok {
|
||||
return &TypeDocs{Type: kind}
|
||||
}
|
||||
if t, ok := typesByName[x.Name]; ok {
|
||||
return namedTypeDocs(t, typesByName)
|
||||
}
|
||||
log.Printf("unrecognized identifier %q (not a builtin or a model package type)", x.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("unrecognized ast.Expr: %T", expr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateDocs() (*Docs, error) {
|
||||
modelDir := modelPackageDir()
|
||||
|
||||
fset = token.NewFileSet()
|
||||
notTest := func(info os.FileInfo) bool { return !strings.HasSuffix(info.Name(), "_test.go") }
|
||||
pkgs, err := parser.ParseDir(fset, modelDir, notTest, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modelPkg, ok := pkgs["model"]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
filesByPath = make(map[string]*ast.File, len(modelPkg.Files))
|
||||
for path, file := range modelPkg.Files {
|
||||
filesByPath[path] = file
|
||||
}
|
||||
|
||||
godocs := doc.New(modelPkg, "github.com/mattermost/mattermost/server/public/model", doc.Mode(0))
|
||||
|
||||
typesByName := make(map[string]*doc.Type, len(godocs.Types))
|
||||
for _, t := range godocs.Types {
|
||||
typesByName[t.Name] = t
|
||||
}
|
||||
|
||||
manifestType, ok := typesByName["Manifest"]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
return &Docs{
|
||||
Schema: namedTypeDocs(manifestType, typesByName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
docs, err := generateDocs()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(docs, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stdout.Write(b); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stdout.Write([]byte("\n")); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
180
docs/site/scripts/stage-agents-docs.mjs
Normal file
180
docs/site/scripts/stage-agents-docs.mjs
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
// Stage the mattermost-plugin-agents submodule's docs/ subfolder into
|
||||
// main/agents/docs/ so the two curated Agents pages (administration-guide/
|
||||
// configure/agents-admin-guide.mdx and end-user-guide/agents.mdx) and a
|
||||
// handful of nested reference pages have real content to link to.
|
||||
//
|
||||
// Why staging instead of checking out the submodule directly under docs/:
|
||||
// submodules can't do a sparse "just this subfolder" checkout, and the repo
|
||||
// also has README/LICENSE/Go source/etc. we don't want as pages. So it's
|
||||
// vendored out-of-tree at vendor/mattermost-plugin-agents, and this script
|
||||
// copies+transforms only its docs/** into main/agents/docs/ before the
|
||||
// sidebar is generated — same effect as Sphinx's conf.py exclude/redirect
|
||||
// rules on its own full-repo submodule checkout.
|
||||
//
|
||||
// Navigation mirrors Sphinx exactly (source/end-user-guide/agents.rst,
|
||||
// source/administration-guide/configure/agents-admin-guide.rst): no
|
||||
// top-level Agents nav section. admin_guide.md/user_guide.md are `..
|
||||
// include::`d into the two curated pages; providers/aws_bedrock_setup/
|
||||
// sovereign_ai/usage_tips get their own nested pages via a small hidden
|
||||
// toctree; everything else (load-testing, upgrading_to_2.0, features/*) is
|
||||
// direct-URL-only, never in any toctree. Reproduced as a three-way split:
|
||||
// - INLINE_PARTIALS: staged as an unlisted doc (direct-link parity) AND
|
||||
// as a Markdown partial (leading underscore) the curated pages
|
||||
// `import` and render inline, reproducing `.. include::`.
|
||||
// - NAV_CHILDREN: normal listed docs, nested under the curated pages by
|
||||
// gen-documentation-sidebar.mjs via cross-directory {doc: ...} items.
|
||||
// - Everything else: unlisted docs — built and linkable, absent from
|
||||
// the sidebar.
|
||||
//
|
||||
// Re-run safely at any time (e.g. after `git submodule update --remote`) —
|
||||
// it fully regenerates its output directories.
|
||||
//
|
||||
// Usage: node docs/site/scripts/stage-agents-docs.mjs
|
||||
|
||||
import {
|
||||
readFileSync, writeFileSync, mkdirSync, readdirSync, statSync,
|
||||
existsSync, rmSync, copyFileSync,
|
||||
} from 'node:fs';
|
||||
import {join, resolve, relative, dirname, extname, posix} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SITE_ROOT = resolve(HERE, '..');
|
||||
const REPO_ROOT = resolve(SITE_ROOT, '..');
|
||||
|
||||
const VENDOR_DOCS = join(REPO_ROOT, 'vendor', 'mattermost-plugin-agents', 'docs');
|
||||
const DEST_DOCS = join(REPO_ROOT, 'main', 'agents', 'docs');
|
||||
const DEST_IMAGES = join(SITE_ROOT, 'static', 'images', 'agents');
|
||||
|
||||
// Files whose body is inlined into a curated page via a Markdown partial
|
||||
// import, in addition to being staged as their own unlisted page.
|
||||
const INLINE_PARTIALS = new Set(['admin_guide', 'user_guide']);
|
||||
|
||||
// Files that get their own listed page, nested under a curated page by
|
||||
// gen-documentation-sidebar.mjs. Everything else staged ends up unlisted.
|
||||
const NAV_CHILDREN = new Set(['providers', 'aws_bedrock_setup', 'sovereign_ai', 'usage_tips']);
|
||||
|
||||
function rmrf(p) {
|
||||
if (existsSync(p)) rmSync(p, {recursive: true, force: true});
|
||||
}
|
||||
|
||||
function walk(dir, exclude = []) {
|
||||
const out = [];
|
||||
for (const name of readdirSync(dir)) {
|
||||
if (exclude.includes(name)) continue;
|
||||
const abs = join(dir, name);
|
||||
if (statSync(abs).isDirectory()) out.push(...walk(abs, exclude));
|
||||
else out.push(abs);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Several vendored files lead with a raw `<!-- Copyright ... -->` HTML
|
||||
// comment. Plain Markdown tolerates that, but MDX doesn't parse bare HTML
|
||||
// comments the same way (it wants `{/* ... */}`), so strip it before any
|
||||
// further processing rather than trying to convert it — license headers
|
||||
// aren't meaningful on rendered docs pages anyway.
|
||||
function stripLeadingLicenseComment(body) {
|
||||
return body.replace(/^<!--[\s\S]*?-->\r?\n\r?\n?/, '');
|
||||
}
|
||||
|
||||
// Extract a leading `# Title` line as Docusaurus frontmatter `title`, since
|
||||
// migrated pages elsewhere in main/ carry the title in frontmatter rather
|
||||
// than as an in-body H1 (Docusaurus renders the frontmatter title as the
|
||||
// page's H1 automatically).
|
||||
function extractTitle(body) {
|
||||
const m = body.match(/^# (.+)\r?\n\r?\n?/);
|
||||
if (!m) return {title: null, body};
|
||||
return {title: m[1].trim(), body: body.slice(m[0].length)};
|
||||
}
|
||||
|
||||
// Vendored markdown references sibling images as `img/foo.png` or
|
||||
// `../img/foo.png` (relative to its own location under docs/). Those PNGs
|
||||
// are copied to static/images/agents/, so rewrite refs to the site's
|
||||
// standard absolute `/images/<dir>/<file>` convention.
|
||||
function rewriteImagePaths(body) {
|
||||
return body.replace(/(!\[[^\]]*]\()(?:\.\.\/)?img\/([^)\s]+)(\))/g, '$1/images/agents/$2$3');
|
||||
}
|
||||
|
||||
// Vendored markdown cross-links sibling docs with relative paths, e.g.
|
||||
// `[...](../admin_guide.md#license-requirements)` or
|
||||
// `[...](features/channel_summaries.md)`. Resolve those relative to the
|
||||
// linking file's own directory under docs/ and rewrite to the site's
|
||||
// absolute `/agents/docs/<id>` doc convention, so links keep working
|
||||
// regardless of where the linking content ends up rendered (including
|
||||
// when inlined into a curated page in a completely different directory).
|
||||
function rewriteRelativeMdLinks(body, fileRelDir) {
|
||||
return body.replace(/(\[[^\]]*]\()(?!https?:\/\/|\/|#)([^)\s#]+)\.md(#[^)\s]+)?(\))/g, (_m, pre, target, anchor, post) => {
|
||||
const resolved = posix.normalize(posix.join(fileRelDir, target));
|
||||
return `${pre}/agents/docs/${resolved}${anchor || ''}${post}`;
|
||||
});
|
||||
}
|
||||
|
||||
function stageDocs() {
|
||||
// Clear previous output unconditionally, even on failure below, so a
|
||||
// reused workspace (stale checkout, missing submodule init) never ships
|
||||
// docs left over from a prior run instead of failing loudly.
|
||||
rmrf(DEST_DOCS);
|
||||
rmrf(DEST_IMAGES);
|
||||
|
||||
if (!existsSync(VENDOR_DOCS)) {
|
||||
throw new Error(
|
||||
`[stage-agents-docs] submodule content not found at ${VENDOR_DOCS}. ` +
|
||||
'Run `git submodule update --init --remote docs/vendor/mattermost-plugin-agents` first.',
|
||||
);
|
||||
}
|
||||
|
||||
const mdFiles = walk(VENDOR_DOCS, ['img']).filter((f) => extname(f) === '.md');
|
||||
let partialCount = 0;
|
||||
for (const src of mdFiles) {
|
||||
const rel = relative(VENDOR_DOCS, src); // e.g. "admin_guide.md" or "features/channel_summaries.md"
|
||||
const relDir = posix.dirname(rel.replace(/\\/g, '/'));
|
||||
const baseName = basenameNoExt(rel);
|
||||
|
||||
const raw = stripLeadingLicenseComment(readFileSync(src, 'utf8'));
|
||||
const {title, body} = extractTitle(raw);
|
||||
const transformed = rewriteRelativeMdLinks(rewriteImagePaths(body), relDir === '.' ? '' : relDir);
|
||||
|
||||
const dest = join(DEST_DOCS, rel);
|
||||
mkdirSync(dirname(dest), {recursive: true});
|
||||
|
||||
const listed = NAV_CHILDREN.has(baseName);
|
||||
const inlined = INLINE_PARTIALS.has(baseName);
|
||||
const frontmatterLines = [];
|
||||
if (title) frontmatterLines.push(`title: "${title.replace(/"/g, '\\"')}"`);
|
||||
if (!listed) frontmatterLines.push('unlisted: true');
|
||||
const frontmatter = frontmatterLines.length ? `---\n${frontmatterLines.join('\n')}\n---\n\n` : '';
|
||||
writeFileSync(dest, frontmatter + transformed);
|
||||
|
||||
if (inlined) {
|
||||
// Markdown partials (leading underscore) are auto-excluded from
|
||||
// Docusaurus routing/sidebars and can be `import`ed + rendered
|
||||
// inline elsewhere — this is what reproduces Sphinx's
|
||||
// `.. include:: /agents/docs/*.md` behavior.
|
||||
const partialDest = join(dirname(dest), `_${baseName}_partial.mdx`);
|
||||
writeFileSync(partialDest, transformed);
|
||||
partialCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const imgDir = join(VENDOR_DOCS, 'img');
|
||||
let imageCount = 0;
|
||||
if (existsSync(imgDir)) {
|
||||
mkdirSync(DEST_IMAGES, {recursive: true});
|
||||
for (const name of readdirSync(imgDir)) {
|
||||
copyFileSync(join(imgDir, name), join(DEST_IMAGES, name));
|
||||
imageCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {files: mdFiles.length, partials: partialCount, images: imageCount};
|
||||
}
|
||||
|
||||
function basenameNoExt(relPath) {
|
||||
const base = posix.basename(relPath.replace(/\\/g, '/'));
|
||||
return base.replace(/\.md$/, '');
|
||||
}
|
||||
|
||||
const {files, partials, images} = stageDocs();
|
||||
console.log(`[stage-agents-docs] staged ${files} doc(s) (${partials} with inline partials), ${images} image(s) into ${relative(REPO_ROOT, DEST_DOCS)}`);
|
||||
Reference in New Issue
Block a user