@rtorcato/repo-tooling 2.59.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.
Files changed (136) hide show
  1. package/AGENTS.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +201 -0
  4. package/dist/cli/commands/doctor.js +336 -0
  5. package/dist/cli/commands/fix-targets.js +192 -0
  6. package/dist/cli/commands/fix.js +440 -0
  7. package/dist/cli/commands/setup-presets.js +281 -0
  8. package/dist/cli/commands/setup.js +501 -0
  9. package/dist/cli/generators/agent-rules.js +103 -0
  10. package/dist/cli/generators/badges.js +88 -0
  11. package/dist/cli/generators/build.js +216 -0
  12. package/dist/cli/generators/bun.js +25 -0
  13. package/dist/cli/generators/community-health.js +145 -0
  14. package/dist/cli/generators/docs-site.js +436 -0
  15. package/dist/cli/generators/git.js +164 -0
  16. package/dist/cli/generators/github-actions.js +35 -0
  17. package/dist/cli/generators/github-workflows.js +24 -0
  18. package/dist/cli/generators/gitlab-ci.js +10 -0
  19. package/dist/cli/generators/index.js +123 -0
  20. package/dist/cli/generators/linting.js +54 -0
  21. package/dist/cli/generators/misc.js +298 -0
  22. package/dist/cli/generators/nx.js +16 -0
  23. package/dist/cli/generators/package-json.js +317 -0
  24. package/dist/cli/generators/pnpm-workspace.js +126 -0
  25. package/dist/cli/generators/postcss.js +24 -0
  26. package/dist/cli/generators/readme.js +268 -0
  27. package/dist/cli/generators/security.js +156 -0
  28. package/dist/cli/generators/skills-install.js +70 -0
  29. package/dist/cli/generators/tailwind.js +34 -0
  30. package/dist/cli/generators/testing.js +88 -0
  31. package/dist/cli/generators/treeshake.js +148 -0
  32. package/dist/cli/generators/tsconfig.js +27 -0
  33. package/dist/cli/generators/turborepo.js +35 -0
  34. package/dist/cli/generators/typedoc.js +40 -0
  35. package/dist/cli/index.js +332 -0
  36. package/dist/cli/utils/copy-preset.js +98 -0
  37. package/dist/cli/utils/detect-language.js +22 -0
  38. package/dist/cli/utils/format.js +32 -0
  39. package/dist/cli/utils/install.js +28 -0
  40. package/dist/cli/utils/lockfile.js +84 -0
  41. package/dist/languages/js/checks.js +949 -0
  42. package/dist/languages/js/ci.js +251 -0
  43. package/dist/languages/js/fixers.js +735 -0
  44. package/dist/languages/registry.js +37 -0
  45. package/dist/languages/swift/checks.js +127 -0
  46. package/dist/languages/swift/ci.js +165 -0
  47. package/dist/languages/swift/fixers.js +102 -0
  48. package/dist/languages/swift/git-hooks.js +63 -0
  49. package/dist/languages/swift/gitignore.js +43 -0
  50. package/dist/languages/swift/scaffold.js +244 -0
  51. package/package.json +461 -0
  52. package/tooling/biome/README.md +90 -0
  53. package/tooling/biome/biome.json +63 -0
  54. package/tooling/bun/bunfig.toml +14 -0
  55. package/tooling/changesets/README.md +35 -0
  56. package/tooling/changesets/config.json +11 -0
  57. package/tooling/claude/repo-tooling.md +87 -0
  58. package/tooling/commitlint/commitlint.d.mts +4 -0
  59. package/tooling/commitlint/commitlint.mjs +40 -0
  60. package/tooling/cypress/cypress.config.d.mts +4 -0
  61. package/tooling/cypress/cypress.config.mjs +11 -0
  62. package/tooling/docusaurus/index.d.mts +17 -0
  63. package/tooling/docusaurus/index.mjs +38 -0
  64. package/tooling/docusaurus/sync-changelog.mjs +42 -0
  65. package/tooling/docusaurus/theme-tokens.css +79 -0
  66. package/tooling/docusaurus/theme.css +378 -0
  67. package/tooling/esbuild/index.d.mts +6 -0
  68. package/tooling/esbuild/index.mjs +102 -0
  69. package/tooling/eslint/base.d.mts +6 -0
  70. package/tooling/eslint/base.mjs +122 -0
  71. package/tooling/eslint/nextjs.d.mts +4 -0
  72. package/tooling/eslint/nextjs.mjs +22 -0
  73. package/tooling/eslint/types.d.ts +58 -0
  74. package/tooling/github-actions/workflows/cloudflare-pages.yml +42 -0
  75. package/tooling/github-actions/workflows/docker-publish.yml +44 -0
  76. package/tooling/github-actions/workflows/preview-deployments.yml +54 -0
  77. package/tooling/github-actions/workflows/vercel-deploy.yml +40 -0
  78. package/tooling/jest-presets/browser/jest-preset.d.mts +4 -0
  79. package/tooling/jest-presets/browser/jest-preset.mjs +14 -0
  80. package/tooling/jest-presets/node/jest-preset.d.mts +4 -0
  81. package/tooling/jest-presets/node/jest-preset.mjs +13 -0
  82. package/tooling/mcp/mcp.json.example +27 -0
  83. package/tooling/nx/nx.json +24 -0
  84. package/tooling/oxlint/README.md +25 -0
  85. package/tooling/oxlint/oxlintrc.json +28 -0
  86. package/tooling/playwright/playwright.config.d.mts +4 -0
  87. package/tooling/playwright/playwright.config.mjs +19 -0
  88. package/tooling/prettier/index.d.mts +4 -0
  89. package/tooling/prettier/index.mjs +36 -0
  90. package/tooling/release-please/.release-please-manifest.json +3 -0
  91. package/tooling/release-please/release-please-config.json +9 -0
  92. package/tooling/rolldown/rolldown.config.d.mts +18 -0
  93. package/tooling/rolldown/rolldown.config.mjs +59 -0
  94. package/tooling/rollup/rollup.config.d.mts +20 -0
  95. package/tooling/rollup/rollup.config.mjs +70 -0
  96. package/tooling/semantic-release/docker.d.mts +4 -0
  97. package/tooling/semantic-release/docker.mjs +59 -0
  98. package/tooling/semantic-release/github.d.mts +4 -0
  99. package/tooling/semantic-release/github.mjs +79 -0
  100. package/tooling/semantic-release/index.d.mts +4 -0
  101. package/tooling/semantic-release/index.mjs +80 -0
  102. package/tooling/swift/periphery.yml +4 -0
  103. package/tooling/swift/swiftlint.yml +23 -0
  104. package/tooling/tests/exports-resolution.d.mts +14 -0
  105. package/tooling/tests/exports-resolution.mjs +68 -0
  106. package/tooling/tests/ssr-safety.d.mts +14 -0
  107. package/tooling/tests/ssr-safety.mjs +53 -0
  108. package/tooling/tsup/index.d.mts +8 -0
  109. package/tooling/tsup/index.mjs +33 -0
  110. package/tooling/typedoc/typedoc.json +7 -0
  111. package/tooling/typescript/README.md +49 -0
  112. package/tooling/typescript/reset.d.ts +9 -0
  113. package/tooling/typescript/tsconfig.base.json +84 -0
  114. package/tooling/typescript/tsconfig.build.json +11 -0
  115. package/tooling/typescript/tsconfig.bun.json +9 -0
  116. package/tooling/typescript/tsconfig.express.json +9 -0
  117. package/tooling/typescript/tsconfig.next.json +20 -0
  118. package/tooling/typescript/tsconfig.node.json +9 -0
  119. package/tooling/typescript/tsconfig.react.json +15 -0
  120. package/tooling/typescript/tsconfig.test.json +8 -0
  121. package/tooling/typescript/v1/tsconfig.base.json +81 -0
  122. package/tooling/typescript/v1/tsconfig.express.json +9 -0
  123. package/tooling/typescript/v1/tsconfig.next.json +19 -0
  124. package/tooling/typescript/v1/tsconfig.node.json +9 -0
  125. package/tooling/typescript/v1/tsconfig.react.json +14 -0
  126. package/tooling/typescript/v1/tsconfig.test.json +8 -0
  127. package/tooling/vite/vite.config.d.mts +4 -0
  128. package/tooling/vite/vite.config.mjs +18 -0
  129. package/tooling/vitest/jsdom-shims.d.mts +1 -0
  130. package/tooling/vitest/jsdom-shims.mjs +58 -0
  131. package/tooling/vitest/vitest.config.d.mts +4 -0
  132. package/tooling/vitest/vitest.config.mjs +25 -0
  133. package/tooling/vitest/vitest.config.react.d.mts +4 -0
  134. package/tooling/vitest/vitest.config.react.mjs +27 -0
  135. package/tooling/vitest/vitest.setup.d.mts +1 -0
  136. package/tooling/vitest/vitest.setup.mjs +3 -0
@@ -0,0 +1,436 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { copyPreset, PRESETS } from '../utils/copy-preset.js';
4
+ import { buildBadgeRow, parseRepository } from './badges.js';
5
+ import { inferSubpathsFromExports } from './treeshake.js';
6
+ /**
7
+ * Docs-site (Docusaurus) generator — the Phase 2 counterpart to the shared
8
+ * assets shipped in #54. Scaffolds a working Docusaurus site under `apps/docs`,
9
+ * inferring name/org/repo from package.json (+ the shared design tokens and
10
+ * sync-changelog script), matching the layout the `Docs site` doctor check
11
+ * verifies. Every file is written only when absent, so `fix docs-site` is
12
+ * idempotent and never clobbers a hand-edited site.
13
+ */
14
+ const DOCS_APP = 'apps/docs';
15
+ /** Docusaurus's neutral green — the default accent, meant to be branded over. */
16
+ const DEFAULT_ACCENT = { light: '#2e8555', dark: '#25c2a0' };
17
+ /**
18
+ * Source modules to document with TypeDoc: single-segment subpath exports
19
+ * (`./errors` → `errors`), which map to `src/<id>/index.ts`. Multi-segment
20
+ * subpaths (`./typescript/base`) are config/asset exports, not source modules,
21
+ * so they're filtered out.
22
+ */
23
+ function inferTypedocModules(pkg) {
24
+ return inferSubpathsFromExports(pkg).allCandidates.filter((id) => !id.includes('/'));
25
+ }
26
+ /** Derive the docs package name from the consumer's own name (`<name>-docs`). */
27
+ function docsPackageName(pkgName) {
28
+ if (!pkgName)
29
+ return 'docs';
30
+ return `${pkgName}-docs`;
31
+ }
32
+ function inferSiteMeta(pkg) {
33
+ const pkgName = pkg?.name;
34
+ const parsed = parseRepository(pkg?.repository);
35
+ const base = pkgName ? (pkgName.split('/').pop() ?? pkgName) : 'docs';
36
+ return {
37
+ docsPkgName: docsPackageName(pkgName),
38
+ title: parsed?.repo ?? base,
39
+ tagline: pkg?.description ?? 'Documentation',
40
+ owner: parsed?.owner ?? null,
41
+ repo: parsed?.repo ?? null,
42
+ };
43
+ }
44
+ /** Write `contents` at `rel` under targetDir only if it doesn't already exist. */
45
+ async function writeIfMissing(targetDir, rel, contents) {
46
+ const file = path.join(targetDir, rel);
47
+ if (await fs.pathExists(file))
48
+ return null;
49
+ await fs.ensureDir(path.dirname(file));
50
+ await fs.writeFile(file, contents);
51
+ return rel;
52
+ }
53
+ /** Ensure `pnpm-workspace.yaml` lists `apps/*` (idempotent). */
54
+ async function ensureWorkspace(targetDir) {
55
+ const rel = 'pnpm-workspace.yaml';
56
+ const file = path.join(targetDir, rel);
57
+ if (await fs.pathExists(file)) {
58
+ const body = await fs.readFile(file, 'utf8');
59
+ // Already a workspace covering apps/* (either `apps/*` or a broader glob).
60
+ if (/^\s*-\s*['"]?apps\/\*/m.test(body))
61
+ return null;
62
+ if (/^packages:/m.test(body)) {
63
+ const next = body.replace(/^packages:\n/m, "packages:\n - 'apps/*'\n");
64
+ await fs.writeFile(file, next);
65
+ return rel;
66
+ }
67
+ await fs.writeFile(file, `packages:\n - 'apps/*'\n\n${body}`);
68
+ return rel;
69
+ }
70
+ await fs.writeFile(file, "packages:\n - 'apps/*'\n");
71
+ return rel;
72
+ }
73
+ /** The GitHub Pages base path Docusaurus serves under, e.g. `/repo-tooling/`. */
74
+ function siteBaseUrl(meta) {
75
+ return `/${meta.repo ?? meta.title}/`;
76
+ }
77
+ function docusaurusConfig(meta, typedocModules) {
78
+ const owner = meta.owner ?? 'your-org';
79
+ const repo = meta.repo ?? meta.title;
80
+ const ghUrl = `https://github.com/${owner}/${repo}`;
81
+ // TypeDoc plugins (opt-in) generate docs/api/<id>; the autogenerated sidebar
82
+ // picks the api/ folder up automatically, so no sidebar change is needed.
83
+ const typedocImport = typedocModules.length
84
+ ? "import { getTypedocPlugins } from '@rtorcato/repo-tooling/docusaurus'\n"
85
+ : '';
86
+ const typedocPlugins = typedocModules.length
87
+ ? ` ...getTypedocPlugins(${JSON.stringify(typedocModules)}),\n`
88
+ : '';
89
+ return `import type * as Preset from '@docusaurus/preset-classic'
90
+ import type { Config } from '@docusaurus/types'
91
+ import { themes as prismThemes } from 'prism-react-renderer'
92
+ ${typedocImport}
93
+ const config: Config = {
94
+ title: '${meta.title}',
95
+ tagline: ${JSON.stringify(meta.tagline)},
96
+ favicon: 'img/favicon.ico',
97
+
98
+ url: 'https://${owner}.github.io',
99
+ baseUrl: '/${repo}/',
100
+
101
+ organizationName: '${owner}',
102
+ projectName: '${repo}',
103
+
104
+ onBrokenLinks: 'warn',
105
+
106
+ markdown: {
107
+ format: 'detect',
108
+ hooks: {
109
+ onBrokenMarkdownLinks: 'warn',
110
+ },
111
+ },
112
+
113
+ i18n: {
114
+ defaultLocale: 'en',
115
+ locales: ['en'],
116
+ },
117
+
118
+ presets: [
119
+ [
120
+ 'classic',
121
+ {
122
+ docs: {
123
+ sidebarPath: './sidebars.ts',
124
+ routeBasePath: '/docs',
125
+ editUrl: '${ghUrl}/edit/main/apps/docs/',
126
+ },
127
+ blog: false,
128
+ theme: {
129
+ customCss: './src/css/custom.css',
130
+ },
131
+ } satisfies Preset.Options,
132
+ ],
133
+ ],
134
+
135
+ plugins: [
136
+ ${typedocPlugins} [
137
+ '@easyops-cn/docusaurus-search-local',
138
+ {
139
+ hashed: true,
140
+ indexDocs: true,
141
+ indexBlog: false,
142
+ docsRouteBasePath: '/docs',
143
+ highlightSearchTermsOnTargetPage: true,
144
+ searchBarShortcutHint: false,
145
+ },
146
+ ],
147
+ ],
148
+
149
+ themeConfig: {
150
+ colorMode: {
151
+ defaultMode: 'dark',
152
+ respectPrefersColorScheme: true,
153
+ },
154
+ navbar: {
155
+ title: '${meta.title}',
156
+ items: [
157
+ { to: '/docs', position: 'left', label: 'Docs' },
158
+ {
159
+ href: '${ghUrl}',
160
+ label: 'GitHub',
161
+ position: 'right',
162
+ },
163
+ ],
164
+ },
165
+ footer: {
166
+ style: 'dark',
167
+ links: [
168
+ {
169
+ title: 'Docs',
170
+ items: [{ label: 'Getting Started', to: '/docs' }],
171
+ },
172
+ {
173
+ title: 'More',
174
+ items: [
175
+ { label: 'GitHub', href: '${ghUrl}' },
176
+ { label: 'Issues', href: '${ghUrl}/issues' },
177
+ ],
178
+ },
179
+ ],
180
+ copyright: \`Copyright © \${new Date().getFullYear()} ${meta.title}. Built with Docusaurus.\`,
181
+ },
182
+ prism: {
183
+ theme: prismThemes.vsDark,
184
+ darkTheme: prismThemes.vsDark,
185
+ additionalLanguages: ['bash', 'json', 'typescript'],
186
+ },
187
+ } satisfies Preset.ThemeConfig,
188
+ }
189
+
190
+ export default config
191
+ `;
192
+ }
193
+ const SIDEBARS = `import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'
194
+
195
+ // Autogenerated from the docs/ folder structure — add markdown files and they
196
+ // appear here. Swap for an explicit list when you want to control ordering.
197
+ const sidebars: SidebarsConfig = {
198
+ docs: [{ type: 'autogenerated', dirName: '.' }],
199
+ }
200
+
201
+ export default sidebars
202
+ `;
203
+ const TSCONFIG = `// Improves IDE type-checking; not used by \`docusaurus start/build\`.
204
+ {
205
+ "compilerOptions": {
206
+ "baseUrl": ".",
207
+ "ignoreDeprecations": "6.0",
208
+ "strict": true,
209
+ "target": "ES2020",
210
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
211
+ "jsx": "react-jsx",
212
+ "module": "ESNext",
213
+ "moduleResolution": "node",
214
+ "resolveJsonModule": true,
215
+ "allowJs": true,
216
+ "esModuleInterop": true,
217
+ "skipLibCheck": true,
218
+ "forceConsistentCasingInFileNames": true
219
+ },
220
+ "exclude": [".docusaurus", "build"]
221
+ }
222
+ `;
223
+ function customCss(accent) {
224
+ // Import the shared tokens, then override only the accent (per #54's model).
225
+ return `/* Site theme: shared @rtorcato tokens + this project's accent. */
226
+ @import "./_jt-tokens.css";
227
+
228
+ :root {
229
+ --ifm-color-primary: ${accent.light};
230
+ --jt-accent: ${accent.light};
231
+ }
232
+
233
+ [data-theme="dark"] {
234
+ --ifm-color-primary: ${accent.dark};
235
+ --jt-accent: ${accent.dark};
236
+ }
237
+ `;
238
+ }
239
+ function docsPackageJson(meta, typedoc) {
240
+ const typedocDevDeps = typedoc
241
+ ? {
242
+ '@rtorcato/repo-tooling': '^2.47.0',
243
+ 'docusaurus-plugin-typedoc': '^1.4.0',
244
+ typedoc: '^0.28.0',
245
+ 'typedoc-plugin-markdown': '^4.9.0',
246
+ }
247
+ : {};
248
+ const pkg = {
249
+ name: meta.docsPkgName,
250
+ version: '0.0.1',
251
+ private: true,
252
+ scripts: {
253
+ docusaurus: 'docusaurus',
254
+ 'sync-changelog': 'node ../../scripts/sync-changelog.mjs',
255
+ // pnpm 8 doesn't run pre* hooks reliably — chain sync-changelog explicitly.
256
+ start: 'pnpm run sync-changelog && docusaurus start',
257
+ dev: 'pnpm run sync-changelog && docusaurus start',
258
+ build: 'pnpm run sync-changelog && docusaurus build',
259
+ serve: 'docusaurus serve',
260
+ clear: 'docusaurus clear',
261
+ typecheck: 'tsc --noEmit',
262
+ // Opt-in smoke test — builds, serves, and checks the site renders. Heavy
263
+ // browser install, so it's a manual/CI-gated run, not part of `build`.
264
+ 'test:e2e': 'playwright test',
265
+ },
266
+ dependencies: {
267
+ '@docusaurus/core': '^3.10.2',
268
+ '@docusaurus/preset-classic': '^3.8.1',
269
+ '@easyops-cn/docusaurus-search-local': '^0.55.2',
270
+ '@mdx-js/react': '^3.1.0',
271
+ clsx: '^2.1.1',
272
+ 'prism-react-renderer': '^2.4.1',
273
+ react: '^19.0.0',
274
+ 'react-dom': '^19.0.0',
275
+ },
276
+ devDependencies: {
277
+ '@docusaurus/module-type-aliases': '^3.10.2',
278
+ '@docusaurus/tsconfig': '^3.8.1',
279
+ '@docusaurus/types': '^3.10.2',
280
+ '@playwright/test': '^1.49.0',
281
+ '@rtorcato/repo-tooling': '^2.47.0',
282
+ '@types/react': '^19.0.0',
283
+ typescript: '~5.6.3',
284
+ ...typedocDevDeps,
285
+ },
286
+ browserslist: {
287
+ production: ['>0.5%', 'not dead', 'not op_mini all'],
288
+ development: ['last 3 chrome version', 'last 3 firefox version', 'last 5 safari version'],
289
+ },
290
+ engines: { node: '>=22.0' },
291
+ };
292
+ return `${JSON.stringify(pkg, null, 2)}\n`;
293
+ }
294
+ function introDoc(meta, badges) {
295
+ return `---
296
+ title: ${meta.title}
297
+ slug: /
298
+ sidebar_position: 0
299
+ ---
300
+
301
+ # ${meta.title}
302
+ ${badges ? `\n${badges}\n` : ''}
303
+ ${meta.tagline}
304
+
305
+ Welcome to the docs. Edit \`apps/docs/docs/intro.md\` to get started, and add
306
+ more markdown files under \`apps/docs/docs/\` — they appear in the sidebar
307
+ automatically.
308
+ `;
309
+ }
310
+ /** The per-repo workflow that drives the shared reusable deploy on push to main. */
311
+ function docsWorkflow(meta) {
312
+ return `name: 📚 Docs
313
+ on:
314
+ push:
315
+ branches: [main]
316
+ paths:
317
+ - 'apps/docs/**'
318
+ - 'CHANGELOG.md'
319
+ - '.github/workflows/docs.yml'
320
+ workflow_dispatch:
321
+
322
+ jobs:
323
+ docs:
324
+ permissions:
325
+ contents: read
326
+ pages: write
327
+ id-token: write
328
+ uses: rtorcato/repo-tooling/.github/workflows/docs-deploy.yml@main
329
+ with:
330
+ build-filter: '${meta.docsPkgName}'
331
+ `;
332
+ }
333
+ /**
334
+ * Playwright config for the docs smoke test. Reuses the shipped preset, then
335
+ * builds + serves the site on :3000 and points the base URL at the site's
336
+ * GitHub Pages base path so routes resolve exactly as in production. One
337
+ * browser keeps the CI browser install light.
338
+ */
339
+ function playwrightConfig(meta) {
340
+ const url = `http://localhost:3000${siteBaseUrl(meta)}`;
341
+ return `import { defineConfig, devices } from '@playwright/test'
342
+ import base from '@rtorcato/repo-tooling/playwright'
343
+
344
+ export default defineConfig({
345
+ ...base,
346
+ testDir: './tests',
347
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
348
+ use: {
349
+ ...base.use,
350
+ baseURL: process.env.PLAYWRIGHT_BASE_URL ?? '${url}',
351
+ },
352
+ webServer: {
353
+ command: 'pnpm run build && pnpm exec docusaurus serve --port 3000',
354
+ url: process.env.PLAYWRIGHT_BASE_URL ?? '${url}',
355
+ reuseExistingServer: !process.env.CI,
356
+ timeout: 180_000,
357
+ },
358
+ })
359
+ `;
360
+ }
361
+ const SMOKE_SPEC = `import { expect, test } from '@playwright/test'
362
+
363
+ // Smoke test: assert the built site serves and its core UI renders. Deliberately
364
+ // content-agnostic — it validates "the site builds and boots", not copy.
365
+ test('homepage responds and renders the shell', async ({ page }) => {
366
+ const res = await page.goto('./')
367
+ expect(res?.ok()).toBeTruthy()
368
+ await expect(page.locator('.navbar')).toBeVisible()
369
+ })
370
+
371
+ test('the starter doc renders a heading', async ({ page }) => {
372
+ await page.goto('./')
373
+ await expect(page.locator('h1')).toBeVisible()
374
+ })
375
+ `;
376
+ /**
377
+ * Scaffold the Docusaurus docs site. Writes each file only when missing and
378
+ * returns the relative paths actually written, so `fix docs-site` is safe to
379
+ * re-run. Also drops in the shared sync-changelog script + design tokens.
380
+ */
381
+ export async function generateDocsSite(pkg, targetDir, options = {}) {
382
+ const meta = inferSiteMeta(pkg);
383
+ const accent = options.primaryColor ?? DEFAULT_ACCENT;
384
+ const written = [];
385
+ // Shared assets (only-if-missing copies of the shipped presets).
386
+ written.push(...(await copyPresetIfMissing('docusaurus-sync-changelog', targetDir)));
387
+ written.push(...(await copyPresetIfMissing('docusaurus-theme-tokens', targetDir)));
388
+ // The docs homepage carries the same badge set as the README (#169), derived
389
+ // from package.json + repo; visibility-aware (private repos drop npm/coverage).
390
+ // Plain row (no upsert delimiters) to stay MDX-safe in the generated intro.
391
+ const badges = buildBadgeRow({
392
+ name: pkg?.name,
393
+ owner: meta.owner ?? undefined,
394
+ repo: meta.repo ?? undefined,
395
+ isPrivate: pkg?.private === true,
396
+ });
397
+ // Opt-in TypeDoc API section (#229): only wire it when enabled AND the
398
+ // package actually exposes source modules to document.
399
+ const typedocModules = options.typedoc ? inferTypedocModules(pkg) : [];
400
+ // Project-specific scaffold.
401
+ const files = [
402
+ [`${DOCS_APP}/package.json`, docsPackageJson(meta, typedocModules.length > 0)],
403
+ [`${DOCS_APP}/docusaurus.config.ts`, docusaurusConfig(meta, typedocModules)],
404
+ [`${DOCS_APP}/sidebars.ts`, SIDEBARS],
405
+ [`${DOCS_APP}/tsconfig.json`, TSCONFIG],
406
+ [`${DOCS_APP}/src/css/custom.css`, customCss(accent)],
407
+ [`${DOCS_APP}/docs/intro.md`, introDoc(meta, badges)],
408
+ [`${DOCS_APP}/playwright.config.ts`, playwrightConfig(meta)],
409
+ [`${DOCS_APP}/tests/smoke.spec.ts`, SMOKE_SPEC],
410
+ ['.github/workflows/docs.yml', docsWorkflow(meta)],
411
+ ];
412
+ // TypeDoc emits docs/api/<id> on build — keep the generated tree out of git.
413
+ if (typedocModules.length) {
414
+ files.push([
415
+ `${DOCS_APP}/.gitignore`,
416
+ '# Generated by TypeDoc on build\ndocs/api/\n\n# Docusaurus build artifacts\nbuild/\n.docusaurus/\n',
417
+ ]);
418
+ }
419
+ for (const [rel, contents] of files) {
420
+ const w = await writeIfMissing(targetDir, rel, contents);
421
+ if (w)
422
+ written.push(w);
423
+ }
424
+ const ws = await ensureWorkspace(targetDir);
425
+ if (ws)
426
+ written.push(ws);
427
+ return written;
428
+ }
429
+ /** Copy a shipped preset only when its target file is absent. */
430
+ async function copyPresetIfMissing(name, targetDir) {
431
+ const rel = PRESETS[name].target;
432
+ if (await fs.pathExists(path.join(targetDir, rel)))
433
+ return [];
434
+ const res = await copyPreset(name, targetDir);
435
+ return [res.target];
436
+ }
@@ -0,0 +1,164 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ export const PRE_PUSH_HOOK_CONTENT = `echo "🔍 Running pre-push verify..."
4
+ pnpm verify
5
+ STATUS=$?
6
+ if [ $STATUS -ne 0 ]; then
7
+ echo "❌ Verify failed — push aborted."
8
+ exit 1
9
+ fi
10
+ echo "✅ Verify passed — pushing."
11
+ `;
12
+ export async function generateGitConfigs(config, targetDir) {
13
+ if (config.gitHooks) {
14
+ await generateHuskyConfig(config, targetDir);
15
+ }
16
+ if (config.commitLint) {
17
+ await generateCommitlintConfig(targetDir);
18
+ }
19
+ // Generate .gitignore
20
+ await generateGitignore(config, targetDir);
21
+ }
22
+ export async function generateHuskyConfig(config, targetDir) {
23
+ const huskyDir = path.join(targetDir, '.husky');
24
+ await fs.ensureDir(huskyDir);
25
+ // Pre-commit hook. husky v10 format: just the command — the v9 shebang +
26
+ // `. "$(dirname ...)/_/husky.sh"` bootstrap is deprecated (warns on every
27
+ // hook run in v9, fails outright in v10).
28
+ const preCommitPath = path.join(huskyDir, 'pre-commit');
29
+ const preCommitContent = 'npx lint-staged\n';
30
+ await fs.writeFile(preCommitPath, preCommitContent);
31
+ await fs.chmod(preCommitPath, 0o755);
32
+ // Pre-push hook — only when the package.json already has a `verify` script.
33
+ // In the setup flow, generatePackageJson runs before this and writes verify
34
+ // when 2+ tools are enabled. In the `fix husky` path, a pre-existing verify
35
+ // script is what unlocks the hook.
36
+ const pkgPath = path.join(targetDir, 'package.json');
37
+ if (await fs.pathExists(pkgPath)) {
38
+ const pkg = (await fs.readJson(pkgPath));
39
+ const scripts = pkg.scripts ?? {};
40
+ if (scripts.verify) {
41
+ await generatePrePushHook(targetDir);
42
+ }
43
+ }
44
+ // Commit-msg hook (if commitlint is enabled). husky v10 format — no v9
45
+ // bootstrap. $1 is still the commit-message file path git passes through.
46
+ if (config.commitLint) {
47
+ const commitMsgPath = path.join(huskyDir, 'commit-msg');
48
+ const commitMsgContent = 'npx --no -- commitlint --edit $1\n';
49
+ await fs.writeFile(commitMsgPath, commitMsgContent);
50
+ await fs.chmod(commitMsgPath, 0o755);
51
+ }
52
+ // lint-staged configuration in package.json
53
+ const packageJsonPath = path.join(targetDir, 'package.json');
54
+ const packageJson = await fs.readJson(packageJsonPath);
55
+ // No explicit `git add` — lint-staged stages tool output itself, and the
56
+ // extra add races its index lock. `--no-errors-on-unmatched` keeps biome
57
+ // from failing a commit when every matched file is biome-ignored.
58
+ const useBiome = config.linting.tool === 'biome' || config.linting.tool === 'both';
59
+ packageJson['lint-staged'] = {
60
+ '*.{js,ts,jsx,tsx}': useBiome ? 'biome check --fix --no-errors-on-unmatched' : 'eslint --fix',
61
+ '*.{json,md,yml,yaml}': useBiome
62
+ ? 'biome format --write --no-errors-on-unmatched'
63
+ : 'prettier --write',
64
+ };
65
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
66
+ }
67
+ export async function generatePrePushHook(targetDir) {
68
+ const huskyDir = path.join(targetDir, '.husky');
69
+ await fs.ensureDir(huskyDir);
70
+ const prePushPath = path.join(huskyDir, 'pre-push');
71
+ await fs.writeFile(prePushPath, PRE_PUSH_HOOK_CONTENT);
72
+ await fs.chmod(prePushPath, 0o755);
73
+ }
74
+ export async function generateCommitlintConfig(targetDir) {
75
+ const commitlintConfigPath = path.join(targetDir, 'commitlint.config.mjs');
76
+ const commitlintConfig = `export { default } from '@rtorcato/repo-tooling/commitlint/config'
77
+ `;
78
+ await fs.writeFile(commitlintConfigPath, commitlintConfig);
79
+ }
80
+ async function generateGitignore(config, targetDir) {
81
+ const gitignorePath = path.join(targetDir, '.gitignore');
82
+ let gitignoreContent = `# Dependencies
83
+ node_modules/
84
+ .pnpm-store
85
+
86
+ # Build outputs
87
+ dist/
88
+ build/
89
+ out/
90
+
91
+ # Environment files
92
+ .env
93
+ .env.local
94
+ .env.development.local
95
+ .env.test.local
96
+ .env.production.local
97
+
98
+ # IDE
99
+ .vscode/
100
+ .idea/
101
+ *.swp
102
+ *.swo
103
+
104
+ # Claude Code (worktrees + local settings are per-machine; agents/commands are shared)
105
+ .claude/worktrees/
106
+ .claude/settings.local.json
107
+
108
+ # OS
109
+ .DS_Store
110
+ Thumbs.db
111
+
112
+ # Logs
113
+ logs
114
+ *.log
115
+ npm-debug.log*
116
+ yarn-debug.log*
117
+ yarn-error.log*
118
+ pnpm-debug.log*
119
+
120
+ # Runtime data
121
+ pids
122
+ *.pid
123
+ *.seed
124
+ *.pid.lock
125
+
126
+ # Coverage directory used by tools like istanbul
127
+ coverage/
128
+ *.lcov
129
+
130
+ # TypeScript
131
+ *.tsbuildinfo
132
+ `;
133
+ // Add framework-specific ignores
134
+ if (config.projectType === 'nextjs-app') {
135
+ gitignoreContent += `
136
+ # Next.js
137
+ .next/
138
+ .vercel
139
+ `;
140
+ }
141
+ if (config.bundler === 'vite') {
142
+ gitignoreContent += `
143
+ # Vite
144
+ .vite/
145
+ `;
146
+ }
147
+ if (config.testing.framework === 'playwright') {
148
+ gitignoreContent += `
149
+ # Playwright
150
+ /test-results/
151
+ /playwright-report/
152
+ /playwright/.cache/
153
+ `;
154
+ }
155
+ if (config.testing.framework === 'cypress') {
156
+ gitignoreContent += `
157
+ # Cypress
158
+ /cypress/videos/
159
+ /cypress/screenshots/
160
+ /cypress/downloads/
161
+ `;
162
+ }
163
+ await fs.writeFile(gitignorePath, gitignoreContent);
164
+ }
@@ -0,0 +1,35 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { renderGitHubWorkflow } from '../../base/ci.js';
4
+ import { githubJobs, usesCoverage } from '../../languages/js/ci.js';
5
+ // Minimal Codecov config — auto targets keep it from failing a fresh repo that
6
+ // has no baseline yet, while the 1% threshold tolerates rounding noise.
7
+ // https://docs.codecov.com/docs/codecov-yaml
8
+ const CODECOV_YML = `coverage:
9
+ status:
10
+ project:
11
+ default:
12
+ target: auto
13
+ threshold: 1%
14
+ patch:
15
+ default:
16
+ target: auto
17
+ threshold: 1%
18
+ `;
19
+ export async function generateGitHubActions(config, targetDir) {
20
+ const workflowsDir = path.join(targetDir, '.github', 'workflows');
21
+ await fs.ensureDir(workflowsDir);
22
+ // This is the JS path specifically. Swift (#287) renders its own workflow
23
+ // from `src/languages/swift/ci.ts` rather than dispatching through here: it
24
+ // takes no ProjectConfig at all (its jobs derive from Package.swift), so a
25
+ // shared entry point would mean inventing a fake config to pass in. Both
26
+ // paths meet at renderGitHubWorkflow() in src/base/ci.ts, which is the seam
27
+ // that actually matters.
28
+ const workflow = renderGitHubWorkflow(githubJobs(config));
29
+ await fs.writeFile(path.join(workflowsDir, 'ci.yml'), workflow);
30
+ // codecov.yml is the CI's coverage-upload companion — emit it alongside ci.yml
31
+ // whenever the workflow uploads coverage, so the codecov badge isn't red.
32
+ if (usesCoverage(config)) {
33
+ await fs.writeFile(path.join(targetDir, 'codecov.yml'), CODECOV_YML);
34
+ }
35
+ }
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { getPackageRoot } from '../utils/copy-preset.js';
4
+ /**
5
+ * Optional GitHub Actions deploy workflows, scaffolded on demand via
6
+ * `fix <name>`. They're static templates (no config-driven branching) copied
7
+ * verbatim from tooling/, unlike the config-aware ci.yml generator. Setup does
8
+ * not scaffold these — they're too deploy-target-specific to assume.
9
+ */
10
+ export const GH_WORKFLOWS = [
11
+ 'docker-publish',
12
+ 'vercel-deploy',
13
+ 'cloudflare-pages',
14
+ 'preview-deployments',
15
+ ];
16
+ export async function generateGhWorkflow(name, targetDir) {
17
+ const source = path.join(getPackageRoot(), 'tooling/github-actions/workflows', `${name}.yml`);
18
+ const relTarget = path.join('.github', 'workflows', `${name}.yml`);
19
+ // Self-enforced safe-add: these fixers have no doctor check, so the fix
20
+ // command's own safe-add guard (which keys off an `ok` check) can't kick in.
21
+ // overwrite:false keeps a user-customized workflow of the same name intact.
22
+ await fs.copy(source, path.join(targetDir, relTarget), { overwrite: false, errorOnExist: false });
23
+ return relTarget;
24
+ }
@@ -0,0 +1,10 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { renderGitLabCI } from '../../base/ci.js';
4
+ import { gitlabSpec } from '../../languages/js/ci.js';
5
+ export async function generateGitLabCI(config, targetDir) {
6
+ const yamlPath = path.join(targetDir, '.gitlab-ci.yml');
7
+ // ponytail: direct JS import until a second language module ships CI (#287).
8
+ await fs.writeFile(yamlPath, renderGitLabCI(gitlabSpec(config)));
9
+ return '.gitlab-ci.yml';
10
+ }