@rtorcato/repo-tooling 4.8.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,463 +0,0 @@
1
- import path from 'node:path';
2
- import fs from 'fs-extra';
3
- import selfPackageJson from '../../../package.json' with { type: 'json' };
4
- import { CI_WORKFLOW_NAME } from '../../base/ci.js';
5
- import { coverageUploadWorkflow } from '../../base/checks.js';
6
- import { jsBadgeAudience } from '../../languages/js/checks.js';
7
- import { copyPreset, PRESETS } from '../utils/copy-preset.js';
8
- import { buildBadgeRow, parseRepository } from './badges.js';
9
- import { syncBrandToDocs } from './brand.js';
10
- import { DOCS_SITE_BUILDS, mergeAllowBuilds } from './pnpm-workspace.js';
11
- import { inferSubpathsFromExports } from './treeshake.js';
12
- /**
13
- * What a scaffolded docs site should depend on for *this* CLI — read from the
14
- * running version rather than written down, because a literal here goes stale
15
- * silently. It had drifted to `^2.47.0`, two majors behind, so every site
16
- * scaffolded since was handed a pre-rename version predating the peer-dependency
17
- * split.
18
- *
19
- * The floor is the exact running version, not `^<major>.0.0`: the config being
20
- * generated is the one this version emits, and claiming compatibility back to
21
- * the start of the major would be the same over-wide-range problem doctor's
22
- * `Config schema versions` check exists to catch (#330).
23
- */
24
- const SELF_RANGE = `^${selfPackageJson.version}`;
25
- /**
26
- * Docs-site (Docusaurus) generator — the Phase 2 counterpart to the shared
27
- * assets shipped in #54. Scaffolds a working Docusaurus site under `apps/docs`,
28
- * inferring name/org/repo from package.json (+ the shared design tokens and
29
- * sync-changelog script), matching the layout the `Docs site` doctor check
30
- * verifies. Every file is written only when absent, so `fix docs-site` is
31
- * idempotent and never clobbers a hand-edited site.
32
- */
33
- const DOCS_APP = 'apps/docs';
34
- /** Docusaurus's neutral green — the default accent, meant to be branded over. */
35
- const DEFAULT_ACCENT = { light: '#2e8555', dark: '#25c2a0' };
36
- // One range for every @docusaurus/* package; TypeScript tracks repo-tooling's own devDependency (tested).
37
- export const DOCUSAURUS_RANGE = '^3.10.2';
38
- export const TYPESCRIPT_RANGE = '~7.0.2';
39
- /**
40
- * Source modules to document with TypeDoc: single-segment subpath exports
41
- * (`./errors` → `errors`), which map to `src/<id>/index.ts`. Multi-segment
42
- * subpaths (`./typescript/base`) are config/asset exports, not source modules,
43
- * so they're filtered out.
44
- */
45
- function inferTypedocModules(pkg) {
46
- return inferSubpathsFromExports(pkg).allCandidates.filter((id) => !id.includes('/'));
47
- }
48
- /** Derive the docs package name from the consumer's own name (`<name>-docs`). */
49
- function docsPackageName(pkgName) {
50
- if (!pkgName)
51
- return 'docs';
52
- return `${pkgName}-docs`;
53
- }
54
- function inferSiteMeta(pkg) {
55
- const pkgName = pkg?.name;
56
- const parsed = parseRepository(pkg?.repository);
57
- const base = pkgName ? (pkgName.split('/').pop() ?? pkgName) : 'docs';
58
- return {
59
- docsPkgName: docsPackageName(pkgName),
60
- title: parsed?.repo ?? base,
61
- tagline: pkg?.description ?? 'Documentation',
62
- owner: parsed?.owner ?? null,
63
- repo: parsed?.repo ?? null,
64
- };
65
- }
66
- /** Write `contents` at `rel` under targetDir only if it doesn't already exist. */
67
- async function writeIfMissing(targetDir, rel, contents) {
68
- const file = path.join(targetDir, rel);
69
- if (await fs.pathExists(file))
70
- return null;
71
- await fs.ensureDir(path.dirname(file));
72
- await fs.writeFile(file, contents);
73
- return rel;
74
- }
75
- /**
76
- * Ensure `pnpm-workspace.yaml` lists `apps/*` and approves the site's build
77
- * scripts (idempotent).
78
- */
79
- async function ensureWorkspace(targetDir) {
80
- const rel = 'pnpm-workspace.yaml';
81
- const file = path.join(targetDir, rel);
82
- const body = (await fs.pathExists(file)) ? await fs.readFile(file, 'utf8') : '';
83
- let next = body;
84
- // Already a workspace covering apps/* (either `apps/*` or a broader glob).
85
- if (!/^\s*-\s*['"]?apps\/\*/m.test(body)) {
86
- next = /^packages:/m.test(body)
87
- ? body.replace(/^packages:\n/m, "packages:\n - 'apps/*'\n")
88
- : `packages:\n - 'apps/*'\n${body ? `\n${body}` : ''}`;
89
- }
90
- next = mergeAllowBuilds(next, DOCS_SITE_BUILDS);
91
- if (next === body)
92
- return null;
93
- await fs.writeFile(file, next);
94
- return rel;
95
- }
96
- /**
97
- * A JS string literal in the Biome preset's quote style: single quotes, unless
98
- * the value holds more single than double quotes — the same pick Biome makes.
99
- */
100
- function jsString(value) {
101
- const singles = value.split("'").length - 1;
102
- const doubles = value.split('"').length - 1;
103
- if (singles > doubles)
104
- return JSON.stringify(value);
105
- return `'${JSON.stringify(value).slice(1, -1).replaceAll('\\"', '"').replaceAll("'", "\\'")}'`;
106
- }
107
- function docusaurusConfig(meta, typedocModules, siblings = []) {
108
- const owner = meta.owner ?? 'your-org';
109
- const repo = meta.repo ?? meta.title;
110
- const ghUrl = `https://github.com/${owner}/${repo}`;
111
- // TypeDoc plugins (opt-in) generate docs/api/<id>; the autogenerated sidebar
112
- // picks the api/ folder up automatically, so no sidebar change is needed.
113
- const typedocImport = typedocModules.length
114
- ? "import { getTypedocPlugins } from '@rtorcato/repo-tooling/docusaurus'\n"
115
- : '';
116
- const typedocPlugins = typedocModules.length
117
- ? `\t\t...getTypedocPlugins([${typedocModules.map(jsString).join(', ')}]),\n`
118
- : '';
119
- const siblingNav = siblings
120
- .map((s) => `\t\t\t\t{ href: ${jsString(s.href)}, label: ${jsString(s.label)}, position: 'left' },\n`)
121
- .join('');
122
- const siblingFooter = siblings.length
123
- ? `\t\t\t\t{
124
- \t\t\t\t\ttitle: 'Projects',
125
- \t\t\t\t\titems: [
126
- ${siblings.map((s) => `\t\t\t\t\t\t{ label: ${jsString(s.label)}, href: ${jsString(s.href)} },\n`).join('')}\t\t\t\t\t],
127
- \t\t\t\t},
128
- `
129
- : '';
130
- return `import type * as Preset from '@docusaurus/preset-classic'
131
- import type { Config } from '@docusaurus/types'
132
- import { themes as prismThemes } from 'prism-react-renderer'
133
- ${typedocImport}
134
- const config: Config = {
135
- \ttitle: '${meta.title}',
136
- \ttagline: ${jsString(meta.tagline)},
137
- \tfavicon: 'img/favicon.ico',
138
-
139
- \turl: 'https://${owner}.github.io',
140
- \tbaseUrl: '/${repo}/',
141
-
142
- \torganizationName: '${owner}',
143
- \tprojectName: '${repo}',
144
-
145
- \tonBrokenLinks: 'warn',
146
-
147
- \tmarkdown: {
148
- \t\tformat: 'detect',
149
- \t\thooks: {
150
- \t\t\tonBrokenMarkdownLinks: 'warn',
151
- \t\t},
152
- \t},
153
-
154
- \ti18n: {
155
- \t\tdefaultLocale: 'en',
156
- \t\tlocales: ['en'],
157
- \t},
158
-
159
- \tpresets: [
160
- \t\t[
161
- \t\t\t'classic',
162
- \t\t\t{
163
- \t\t\t\tdocs: {
164
- \t\t\t\t\tsidebarPath: './sidebars.ts',
165
- \t\t\t\t\trouteBasePath: '/docs',
166
- \t\t\t\t\teditUrl: '${ghUrl}/edit/main/apps/docs/',
167
- \t\t\t\t},
168
- \t\t\t\tblog: false,
169
- \t\t\t\ttheme: {
170
- \t\t\t\t\tcustomCss: './src/css/custom.css',
171
- \t\t\t\t},
172
- \t\t\t} satisfies Preset.Options,
173
- \t\t],
174
- \t],
175
-
176
- \tplugins: [
177
- ${typedocPlugins}\t\t[
178
- \t\t\t'@easyops-cn/docusaurus-search-local',
179
- \t\t\t{
180
- \t\t\t\thashed: true,
181
- \t\t\t\tindexDocs: true,
182
- \t\t\t\tindexBlog: false,
183
- \t\t\t\tdocsRouteBasePath: '/docs',
184
- \t\t\t\thighlightSearchTermsOnTargetPage: true,
185
- \t\t\t\tsearchBarShortcutHint: false,
186
- \t\t\t},
187
- \t\t],
188
- \t],
189
-
190
- \tthemeConfig: {
191
- \t\timage: 'img/social-card.png',
192
- \t\tcolorMode: {
193
- \t\t\tdefaultMode: 'dark',
194
- \t\t\trespectPrefersColorScheme: true,
195
- \t\t},
196
- \t\tnavbar: {
197
- \t\t\ttitle: '${meta.title}',
198
- \t\t\titems: [
199
- \t\t\t\t{ to: '/docs', position: 'left', label: 'Docs' },
200
- ${siblingNav}\t\t\t\t{
201
- \t\t\t\t\thref: '${ghUrl}',
202
- \t\t\t\t\tlabel: 'GitHub',
203
- \t\t\t\t\tposition: 'right',
204
- \t\t\t\t},
205
- \t\t\t],
206
- \t\t},
207
- \t\tfooter: {
208
- \t\t\tstyle: 'dark',
209
- \t\t\tlinks: [
210
- \t\t\t\t{
211
- \t\t\t\t\ttitle: 'Docs',
212
- \t\t\t\t\titems: [{ label: 'Getting Started', to: '/docs' }],
213
- \t\t\t\t},
214
- \t\t\t\t{
215
- \t\t\t\t\ttitle: 'More',
216
- \t\t\t\t\titems: [
217
- \t\t\t\t\t\t{ label: 'GitHub', href: '${ghUrl}' },
218
- \t\t\t\t\t\t{ label: 'Issues', href: '${ghUrl}/issues' },
219
- \t\t\t\t\t],
220
- \t\t\t\t},
221
- ${siblingFooter}\t\t\t],
222
- \t\t\tcopyright: \`Copyright © \${new Date().getFullYear()} ${meta.title}. Built with Docusaurus.\`,
223
- \t\t},
224
- \t\t// \`theme\` is the LIGHT-mode Prism theme and \`darkTheme\` the dark one. Both
225
- \t\t// were vsDark here, which is why the shared stylesheet had to pin fenced
226
- \t\t// blocks dark in light mode too (#324). Keep this pairing and the CSS in
227
- \t\t// step — vsDark tokens on a light surface are unreadable.
228
- \t\tprism: {
229
- \t\t\ttheme: prismThemes.vsLight,
230
- \t\t\tdarkTheme: prismThemes.vsDark,
231
- \t\t\tadditionalLanguages: ['bash', 'json', 'typescript'],
232
- \t\t},
233
- \t} satisfies Preset.ThemeConfig,
234
- }
235
-
236
- export default config
237
- `;
238
- }
239
- const SIDEBARS = `import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'
240
-
241
- // Autogenerated from the docs/ folder structure — add markdown files and they
242
- // appear here. Swap for an explicit list when you want to control ordering.
243
- const sidebars: SidebarsConfig = {
244
- \tdocs: [{ type: 'autogenerated', dirName: '.' }],
245
- }
246
-
247
- export default sidebars
248
- `;
249
- const TSCONFIG = `// Improves IDE type-checking; not used by \`docusaurus start/build\`.
250
- {
251
- "compilerOptions": {
252
- "baseUrl": ".",
253
- "ignoreDeprecations": "6.0",
254
- "strict": true,
255
- "target": "ES2020",
256
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
257
- "jsx": "react-jsx",
258
- "module": "ESNext",
259
- "moduleResolution": "node",
260
- "resolveJsonModule": true,
261
- "allowJs": true,
262
- "esModuleInterop": true,
263
- "skipLibCheck": true,
264
- "forceConsistentCasingInFileNames": true
265
- },
266
- "exclude": [".docusaurus", "build"]
267
- }
268
- `;
269
- function customCss(accent) {
270
- // Import the shared tokens, then override only the accent (per #54's model).
271
- return `/* Site theme: the shared design tokens + this project's accent. */
272
- @import "./_jt-tokens.css";
273
-
274
- :root {
275
- \t--ifm-color-primary: ${accent.light};
276
- \t--jt-accent: ${accent.light};
277
- }
278
-
279
- [data-theme="dark"] {
280
- \t--ifm-color-primary: ${accent.dark};
281
- \t--jt-accent: ${accent.dark};
282
- }
283
- `;
284
- }
285
- function docsPackageJson(meta, typedoc) {
286
- const typedocDevDeps = typedoc
287
- ? {
288
- '@rtorcato/repo-tooling': SELF_RANGE,
289
- 'docusaurus-plugin-typedoc': '^1.4.0',
290
- typedoc: '^0.28.0',
291
- 'typedoc-plugin-markdown': '^4.9.0',
292
- }
293
- : {};
294
- const pkg = {
295
- name: meta.docsPkgName,
296
- version: '0.0.1',
297
- private: true,
298
- scripts: {
299
- docusaurus: 'docusaurus',
300
- 'sync-changelog': 'node ../../scripts/sync-changelog.mjs',
301
- // pnpm 8 doesn't run pre* hooks reliably — chain sync-changelog explicitly.
302
- start: 'pnpm run sync-changelog && docusaurus start',
303
- dev: 'pnpm run sync-changelog && docusaurus start',
304
- build: 'pnpm run sync-changelog && docusaurus build',
305
- serve: 'docusaurus serve',
306
- clear: 'docusaurus clear',
307
- typecheck: 'tsc --noEmit',
308
- },
309
- dependencies: {
310
- '@docusaurus/core': DOCUSAURUS_RANGE,
311
- '@docusaurus/preset-classic': DOCUSAURUS_RANGE,
312
- '@easyops-cn/docusaurus-search-local': '^0.55.2',
313
- '@mdx-js/react': '^3.1.0',
314
- clsx: '^2.1.1',
315
- 'prism-react-renderer': '^2.4.1',
316
- react: '^19.0.0',
317
- 'react-dom': '^19.0.0',
318
- },
319
- devDependencies: {
320
- '@docusaurus/module-type-aliases': DOCUSAURUS_RANGE,
321
- '@docusaurus/tsconfig': DOCUSAURUS_RANGE,
322
- '@docusaurus/types': DOCUSAURUS_RANGE,
323
- '@rtorcato/repo-tooling': SELF_RANGE,
324
- '@types/react': '^19.0.0',
325
- typescript: TYPESCRIPT_RANGE,
326
- ...typedocDevDeps,
327
- },
328
- browserslist: {
329
- production: ['>0.5%', 'not dead', 'not op_mini all'],
330
- development: ['last 3 chrome version', 'last 3 firefox version', 'last 5 safari version'],
331
- },
332
- engines: { node: '>=22.0' },
333
- };
334
- return `${JSON.stringify(pkg, null, 2)}\n`;
335
- }
336
- function introDoc(meta, badges, siblings = []) {
337
- const related = siblings.length
338
- ? `\n## Related projects\n\n${siblings.map((s) => `- [${s.label}](${s.href})`).join('\n')}\n`
339
- : '';
340
- return `---
341
- title: ${meta.title}
342
- slug: /
343
- sidebar_position: 0
344
- ---
345
-
346
- # ${meta.title}
347
- ${badges ? `\n${badges}\n` : ''}
348
- ${meta.tagline}
349
-
350
- Welcome to the docs. Edit \`apps/docs/docs/intro.md\` to get started, and add
351
- more markdown files under \`apps/docs/docs/\` — they appear in the sidebar
352
- automatically.
353
- ${related}`;
354
- }
355
- /** The per-repo workflow that drives the shared reusable deploy on push to main. */
356
- function docsWorkflow(meta) {
357
- return `name: 📚 Docs
358
- on:
359
- push:
360
- branches: [main]
361
- paths:
362
- - 'apps/docs/**'
363
- - '.github/workflows/docs.yml'
364
- # The changelog page is built from GitHub Releases. A release created with
365
- # GITHUB_TOKEN never fires \`release: published\`, so rebuild once CI (which
366
- # runs semantic-release) succeeds on main instead — no PAT needed (#691).
367
- workflow_run:
368
- workflows: ['${CI_WORKFLOW_NAME}']
369
- types: [completed]
370
- branches: [main]
371
- workflow_dispatch:
372
-
373
- jobs:
374
- docs:
375
- if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
376
- permissions:
377
- contents: read
378
- pages: write
379
- id-token: write
380
- uses: rtorcato/repo-tooling/.github/workflows/docs-deploy.yml@main
381
- with:
382
- build-filter: '${meta.docsPkgName}'
383
- `;
384
- }
385
- // routeBasePath is '/docs', so the site root has no page of its own and the
386
- // navbar logo links to a 404 on every page (#664). Redirect it to the docs.
387
- // Tabs/no semicolons to match the Biome preset the consuming repo is linted with.
388
- const HOME_PAGE = `import { Redirect } from '@docusaurus/router'
389
- import useBaseUrl from '@docusaurus/useBaseUrl'
390
-
391
- export default function Home() {
392
- \treturn <Redirect to={useBaseUrl('/docs')} />
393
- }
394
- `;
395
- /**
396
- * Scaffold the Docusaurus docs site. Writes each file only when missing and
397
- * returns the relative paths actually written, so `fix docs-site` is safe to
398
- * re-run. Also drops in the shared sync-changelog script + design tokens.
399
- */
400
- export async function generateDocsSite(pkg, targetDir, options = {}) {
401
- const meta = inferSiteMeta(pkg);
402
- const accent = options.primaryColor ?? DEFAULT_ACCENT;
403
- const siblings = options.siblings ?? [];
404
- const written = [];
405
- // Shared assets (only-if-missing copies of the shipped presets).
406
- written.push(...(await copyPresetIfMissing('docusaurus-sync-changelog', targetDir)));
407
- written.push(...(await copyPresetIfMissing('docusaurus-theme-tokens', targetDir)));
408
- // The docs homepage carries the same badge set as the README (#169), derived
409
- // from package.json + repo; visibility-aware (private repos drop npm/coverage).
410
- // Plain row (no upsert delimiters) to stay MDX-safe in the generated intro.
411
- // Bundlephobia only for a published library, Codecov only when CI uploads
412
- // coverage — the same rules doctor's badge/coverage checks use (#675).
413
- const badges = buildBadgeRow({
414
- name: pkg?.name,
415
- owner: meta.owner ?? undefined,
416
- repo: meta.repo ?? undefined,
417
- isPrivate: pkg?.private === true,
418
- bundled: jsBadgeAudience(pkg) === 'public',
419
- uploadsCoverage: (await coverageUploadWorkflow(targetDir)) !== null,
420
- });
421
- // Opt-in TypeDoc API section (#229): only wire it when enabled AND the
422
- // package actually exposes source modules to document.
423
- const typedocModules = options.typedoc ? inferTypedocModules(pkg) : [];
424
- // Project-specific scaffold.
425
- const files = [
426
- [`${DOCS_APP}/package.json`, docsPackageJson(meta, typedocModules.length > 0)],
427
- [`${DOCS_APP}/docusaurus.config.ts`, docusaurusConfig(meta, typedocModules, siblings)],
428
- [`${DOCS_APP}/sidebars.ts`, SIDEBARS],
429
- [`${DOCS_APP}/tsconfig.json`, TSCONFIG],
430
- [`${DOCS_APP}/src/css/custom.css`, customCss(accent)],
431
- [`${DOCS_APP}/src/pages/index.tsx`, HOME_PAGE],
432
- [`${DOCS_APP}/docs/intro.md`, introDoc(meta, badges, siblings)],
433
- ['.github/workflows/docs.yml', docsWorkflow(meta)],
434
- ];
435
- // TypeDoc emits docs/api/<id> on build — keep the generated tree out of git.
436
- if (typedocModules.length) {
437
- files.push([
438
- `${DOCS_APP}/.gitignore`,
439
- '# Generated by TypeDoc on build\ndocs/api/\n\n# Docusaurus build artifacts\nbuild/\n.docusaurus/\n',
440
- ]);
441
- }
442
- for (const [rel, contents] of files) {
443
- const w = await writeIfMissing(targetDir, rel, contents);
444
- if (w)
445
- written.push(w);
446
- }
447
- // static/img always exists (the config points at img/favicon.ico); the brand
448
- // assets go in when brand/ has them (#680).
449
- await fs.ensureDir(path.join(targetDir, DOCS_APP, 'static', 'img'));
450
- written.push(...(await syncBrandToDocs(targetDir)));
451
- const ws = await ensureWorkspace(targetDir);
452
- if (ws)
453
- written.push(ws);
454
- return written;
455
- }
456
- /** Copy a shipped preset only when its target file is absent. */
457
- async function copyPresetIfMissing(name, targetDir) {
458
- const rel = PRESETS[name].target;
459
- if (await fs.pathExists(path.join(targetDir, rel)))
460
- return [];
461
- const res = await copyPreset(name, targetDir);
462
- return [res.target];
463
- }
@@ -1,109 +0,0 @@
1
- // Canonical docs-generator helpers for @rtorcato/* repos (shipped by
2
- // @rtorcato/repo-tooling — copy via `repo-tooling copy docusaurus-docs-helpers`).
3
- //
4
- // The pure pieces every subpath-exports package's doc generator needs, so the
5
- // generator script above them stays small and project-specific:
6
- //
7
- // escapeForMarkdownTable(text) — make a JSDoc summary safe for a table cell
8
- // collectExportNames(file) — recursive `export` parser over a module graph
9
- // spliceGeneratedBlock(existing, block) — rewrite only the fenced generated region
10
- //
11
- // Zero-config and side-effect free: no paths, no package names, no I/O beyond
12
- // reading the files you hand it, so this file is copied unmodified.
13
- //
14
- // import {
15
- // collectExportNames,
16
- // escapeForMarkdownTable,
17
- // MARKER_END,
18
- // MARKER_START,
19
- // spliceGeneratedBlock,
20
- // } from './docs-helpers.mjs'
21
-
22
- import { existsSync, readFileSync } from 'node:fs'
23
- import { dirname, join, resolve } from 'node:path'
24
-
25
- const EXPORT_NAMED =
26
- /export\s+(?:async\s+)?(?:function|const|let|class|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g
27
- const EXPORT_BRACE = /export\s*\{\s*([^}]+)\}/g
28
- // Both re-export forms, any specifier — `export * from …` and
29
- // `export { … } from …`. Non-relative specifiers are filtered below rather than
30
- // in the pattern, so a bare-package re-export is recognised and then skipped
31
- // instead of silently parsed as nothing.
32
- const REEXPORT_FROM = /export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g
33
-
34
- /**
35
- * Escape `text` so it survives a markdown table cell.
36
- *
37
- * Neutralises raw HTML-ish tags outside code spans, which would otherwise
38
- * confuse Docusaurus's MDX parser — but keeps them inside backticks, where
39
- * `Success<T>` and `<br>` are the point. Then escapes pipes everywhere, since
40
- * one anywhere in the cell breaks the row.
41
- *
42
- * Escaping the `<` beats stripping `/<[^>]*>/`: a one-pass tag strip can leave
43
- * a tag behind on nested input (`<<b>>` -> `<b>`) and silently eats text like
44
- * `Success<T>` when the JSDoc forgot the backticks.
45
- *
46
- * Escape the backslash in the same pass as the pipe, not after it: escaping
47
- * only `|` turns the input `a\|b` into `a\\|b`, which markdown reads as an
48
- * escaped backslash followed by a live pipe, and the row breaks anyway.
49
- */
50
- export function escapeForMarkdownTable(text) {
51
- return text
52
- .split(/(`[^`]*`)/)
53
- .map((part, i) => (i % 2 === 1 ? part : part.replace(/</g, '&lt;')))
54
- .join('')
55
- .replace(/[\\|]/g, '\\$&')
56
- }
57
-
58
- /**
59
- * Collect every name `file` exports, following relative re-exports into the
60
- * files they name. Returns the accumulating `names` set; `seen` guards against
61
- * an import cycle re-entering a file.
62
- *
63
- * For an aliased export the *alias* is the exported name — `export { foo as
64
- * bar }` exports `bar`, which is what a consumer imports — so this takes the
65
- * last segment, not the first.
66
- */
67
- export function collectExportNames(file, names = new Set(), seen = new Set()) {
68
- if (seen.has(file) || !existsSync(file)) return names
69
- seen.add(file)
70
- const src = readFileSync(file, 'utf8')
71
-
72
- for (const m of src.matchAll(EXPORT_NAMED)) names.add(m[1])
73
- for (const m of src.matchAll(EXPORT_BRACE)) {
74
- for (const part of m[1].split(',')) {
75
- const name = part
76
- .trim()
77
- .split(/\s+as\s+/)
78
- .pop()
79
- ?.trim()
80
- if (name) names.add(name)
81
- }
82
- }
83
- for (const m of src.matchAll(REEXPORT_FROM)) {
84
- if (!m[1].startsWith('.')) continue
85
- const base = resolve(dirname(file), m[1].replace(/\.js$/, ''))
86
- for (const candidate of [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts')]) {
87
- if (existsSync(candidate)) {
88
- collectExportNames(candidate, names, seen)
89
- break
90
- }
91
- }
92
- }
93
- return names
94
- }
95
-
96
- export const MARKER_START =
97
- '<!-- generated:exports — do not edit; `pnpm docs:generate` rewrites this block -->'
98
- export const MARKER_END = '<!-- /generated:exports -->'
99
-
100
- /**
101
- * Splice `block` into `existing` between the markers. Returns null when the
102
- * page has no markers, which means "hand-written, leave it alone".
103
- */
104
- export function spliceGeneratedBlock(existing, block) {
105
- const start = existing.indexOf(MARKER_START)
106
- const end = existing.indexOf(MARKER_END)
107
- if (start === -1 || end === -1 || end < start) return null
108
- return existing.slice(0, start) + block + existing.slice(end + MARKER_END.length)
109
- }
@@ -1,17 +0,0 @@
1
- export interface TypedocPluginsOptions {
2
- /** Directory holding each module's `<id>/index.ts`, relative to the docs app. Default `../../src`. */
3
- srcDir?: string
4
- /** tsconfig passed to TypeDoc, relative to the docs app. Default `../../tsconfig.json`. */
5
- tsconfig?: string
6
- /** Extra `docusaurus-plugin-typedoc` options merged into every instance. */
7
- overrides?: Record<string, unknown>
8
- }
9
-
10
- /**
11
- * Build one `docusaurus-plugin-typedoc` instance per subpath module. Returns
12
- * docusaurus plugin tuples to spread into `plugins`.
13
- */
14
- export declare const getTypedocPlugins: (
15
- modules: string[],
16
- options?: TypedocPluginsOptions
17
- ) => Array<[string, Record<string, unknown>]>
@@ -1,38 +0,0 @@
1
- /**
2
- * Build one `docusaurus-plugin-typedoc` instance per subpath module, each
3
- * generating `docs/api/<id>/index.md` from that module's source JSDoc.
4
- *
5
- * Centralises the TypeDoc wiring the @rtorcato/* docs sites share. The
6
- * consuming docs app must have `docusaurus-plugin-typedoc`, `typedoc` and
7
- * `typedoc-plugin-markdown` installed.
8
- *
9
- * @param {string[]} modules - module ids, e.g. ['errors', 'env', 'kv']
10
- * @param {object} [options]
11
- * @param {string} [options.srcDir] - dir holding `<id>/index.ts`, relative to the docs app. Default '../../src'.
12
- * @param {string} [options.tsconfig] - tsconfig for TypeDoc, relative to the docs app. Default '../../tsconfig.json'.
13
- * @param {object} [options.overrides] - extra plugin options merged into every instance.
14
- * @returns {Array<[string, object]>} docusaurus plugin tuples to spread into `plugins`.
15
- */
16
- export const getTypedocPlugins = (modules, options = {}) => {
17
- const { srcDir = '../../src', tsconfig = '../../tsconfig.json', overrides = {} } = options
18
- return modules.map((id) => [
19
- 'docusaurus-plugin-typedoc',
20
- {
21
- id,
22
- entryPoints: [`${srcDir}/${id}/index.ts`],
23
- tsconfig,
24
- // The library typechecks on its own toolchain; the docs workspace may
25
- // pin a different TS. Skip TypeDoc's redundant semantic check.
26
- skipErrorChecking: true,
27
- out: `docs/api/${id}`,
28
- readme: 'none',
29
- includeVersion: false,
30
- excludePrivate: true,
31
- excludeInternal: true,
32
- excludeExternals: true,
33
- sort: ['source-order'],
34
- outputFileStrategy: 'modules',
35
- ...overrides,
36
- },
37
- ])
38
- }