@rtorcato/repo-tooling 4.8.0 → 5.0.1
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.
- package/AGENTS.md +0 -1
- package/dist/base/checks.js +0 -75
- package/dist/base/fixers.js +0 -36
- package/dist/cli/commands/doctor.js +2 -8
- package/dist/cli/commands/fix-targets.js +0 -7
- package/dist/cli/commands/setup-presets.js +3 -5
- package/dist/cli/commands/setup.js +0 -9
- package/dist/cli/generators/index.js +0 -6
- package/dist/cli/index.js +9 -6
- package/dist/cli/utils/copy-preset.js +0 -20
- package/dist/cli/utils/lockfile.js +5 -3
- package/dist/languages/js/checks.js +0 -62
- package/dist/languages/js/fixers.js +1 -44
- package/package.json +1 -11
- package/tooling/semantic-release/github.mjs +1 -1
- package/dist/cli/generators/brand.js +0 -467
- package/dist/cli/generators/docs-site.js +0 -463
- package/tooling/docusaurus/docs-helpers.mjs +0 -109
- package/tooling/docusaurus/index.d.mts +0 -17
- package/tooling/docusaurus/index.mjs +0 -38
- package/tooling/docusaurus/sync-changelog.mjs +0 -106
- package/tooling/docusaurus/theme-tokens.css +0 -79
- package/tooling/docusaurus/theme.css +0 -390
|
@@ -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, '<')))
|
|
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
|
-
}
|