@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.
- package/AGENTS.md +113 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/cli/commands/doctor.js +336 -0
- package/dist/cli/commands/fix-targets.js +192 -0
- package/dist/cli/commands/fix.js +440 -0
- package/dist/cli/commands/setup-presets.js +281 -0
- package/dist/cli/commands/setup.js +501 -0
- package/dist/cli/generators/agent-rules.js +103 -0
- package/dist/cli/generators/badges.js +88 -0
- package/dist/cli/generators/build.js +216 -0
- package/dist/cli/generators/bun.js +25 -0
- package/dist/cli/generators/community-health.js +145 -0
- package/dist/cli/generators/docs-site.js +436 -0
- package/dist/cli/generators/git.js +164 -0
- package/dist/cli/generators/github-actions.js +35 -0
- package/dist/cli/generators/github-workflows.js +24 -0
- package/dist/cli/generators/gitlab-ci.js +10 -0
- package/dist/cli/generators/index.js +123 -0
- package/dist/cli/generators/linting.js +54 -0
- package/dist/cli/generators/misc.js +298 -0
- package/dist/cli/generators/nx.js +16 -0
- package/dist/cli/generators/package-json.js +317 -0
- package/dist/cli/generators/pnpm-workspace.js +126 -0
- package/dist/cli/generators/postcss.js +24 -0
- package/dist/cli/generators/readme.js +268 -0
- package/dist/cli/generators/security.js +156 -0
- package/dist/cli/generators/skills-install.js +70 -0
- package/dist/cli/generators/tailwind.js +34 -0
- package/dist/cli/generators/testing.js +88 -0
- package/dist/cli/generators/treeshake.js +148 -0
- package/dist/cli/generators/tsconfig.js +27 -0
- package/dist/cli/generators/turborepo.js +35 -0
- package/dist/cli/generators/typedoc.js +40 -0
- package/dist/cli/index.js +332 -0
- package/dist/cli/utils/copy-preset.js +98 -0
- package/dist/cli/utils/detect-language.js +22 -0
- package/dist/cli/utils/format.js +32 -0
- package/dist/cli/utils/install.js +28 -0
- package/dist/cli/utils/lockfile.js +84 -0
- package/dist/languages/js/checks.js +949 -0
- package/dist/languages/js/ci.js +251 -0
- package/dist/languages/js/fixers.js +735 -0
- package/dist/languages/registry.js +37 -0
- package/dist/languages/swift/checks.js +127 -0
- package/dist/languages/swift/ci.js +165 -0
- package/dist/languages/swift/fixers.js +102 -0
- package/dist/languages/swift/git-hooks.js +63 -0
- package/dist/languages/swift/gitignore.js +43 -0
- package/dist/languages/swift/scaffold.js +244 -0
- package/package.json +461 -0
- package/tooling/biome/README.md +90 -0
- package/tooling/biome/biome.json +63 -0
- package/tooling/bun/bunfig.toml +14 -0
- package/tooling/changesets/README.md +35 -0
- package/tooling/changesets/config.json +11 -0
- package/tooling/claude/repo-tooling.md +87 -0
- package/tooling/commitlint/commitlint.d.mts +4 -0
- package/tooling/commitlint/commitlint.mjs +40 -0
- package/tooling/cypress/cypress.config.d.mts +4 -0
- package/tooling/cypress/cypress.config.mjs +11 -0
- package/tooling/docusaurus/index.d.mts +17 -0
- package/tooling/docusaurus/index.mjs +38 -0
- package/tooling/docusaurus/sync-changelog.mjs +42 -0
- package/tooling/docusaurus/theme-tokens.css +79 -0
- package/tooling/docusaurus/theme.css +378 -0
- package/tooling/esbuild/index.d.mts +6 -0
- package/tooling/esbuild/index.mjs +102 -0
- package/tooling/eslint/base.d.mts +6 -0
- package/tooling/eslint/base.mjs +122 -0
- package/tooling/eslint/nextjs.d.mts +4 -0
- package/tooling/eslint/nextjs.mjs +22 -0
- package/tooling/eslint/types.d.ts +58 -0
- package/tooling/github-actions/workflows/cloudflare-pages.yml +42 -0
- package/tooling/github-actions/workflows/docker-publish.yml +44 -0
- package/tooling/github-actions/workflows/preview-deployments.yml +54 -0
- package/tooling/github-actions/workflows/vercel-deploy.yml +40 -0
- package/tooling/jest-presets/browser/jest-preset.d.mts +4 -0
- package/tooling/jest-presets/browser/jest-preset.mjs +14 -0
- package/tooling/jest-presets/node/jest-preset.d.mts +4 -0
- package/tooling/jest-presets/node/jest-preset.mjs +13 -0
- package/tooling/mcp/mcp.json.example +27 -0
- package/tooling/nx/nx.json +24 -0
- package/tooling/oxlint/README.md +25 -0
- package/tooling/oxlint/oxlintrc.json +28 -0
- package/tooling/playwright/playwright.config.d.mts +4 -0
- package/tooling/playwright/playwright.config.mjs +19 -0
- package/tooling/prettier/index.d.mts +4 -0
- package/tooling/prettier/index.mjs +36 -0
- package/tooling/release-please/.release-please-manifest.json +3 -0
- package/tooling/release-please/release-please-config.json +9 -0
- package/tooling/rolldown/rolldown.config.d.mts +18 -0
- package/tooling/rolldown/rolldown.config.mjs +59 -0
- package/tooling/rollup/rollup.config.d.mts +20 -0
- package/tooling/rollup/rollup.config.mjs +70 -0
- package/tooling/semantic-release/docker.d.mts +4 -0
- package/tooling/semantic-release/docker.mjs +59 -0
- package/tooling/semantic-release/github.d.mts +4 -0
- package/tooling/semantic-release/github.mjs +79 -0
- package/tooling/semantic-release/index.d.mts +4 -0
- package/tooling/semantic-release/index.mjs +80 -0
- package/tooling/swift/periphery.yml +4 -0
- package/tooling/swift/swiftlint.yml +23 -0
- package/tooling/tests/exports-resolution.d.mts +14 -0
- package/tooling/tests/exports-resolution.mjs +68 -0
- package/tooling/tests/ssr-safety.d.mts +14 -0
- package/tooling/tests/ssr-safety.mjs +53 -0
- package/tooling/tsup/index.d.mts +8 -0
- package/tooling/tsup/index.mjs +33 -0
- package/tooling/typedoc/typedoc.json +7 -0
- package/tooling/typescript/README.md +49 -0
- package/tooling/typescript/reset.d.ts +9 -0
- package/tooling/typescript/tsconfig.base.json +84 -0
- package/tooling/typescript/tsconfig.build.json +11 -0
- package/tooling/typescript/tsconfig.bun.json +9 -0
- package/tooling/typescript/tsconfig.express.json +9 -0
- package/tooling/typescript/tsconfig.next.json +20 -0
- package/tooling/typescript/tsconfig.node.json +9 -0
- package/tooling/typescript/tsconfig.react.json +15 -0
- package/tooling/typescript/tsconfig.test.json +8 -0
- package/tooling/typescript/v1/tsconfig.base.json +81 -0
- package/tooling/typescript/v1/tsconfig.express.json +9 -0
- package/tooling/typescript/v1/tsconfig.next.json +19 -0
- package/tooling/typescript/v1/tsconfig.node.json +9 -0
- package/tooling/typescript/v1/tsconfig.react.json +14 -0
- package/tooling/typescript/v1/tsconfig.test.json +8 -0
- package/tooling/vite/vite.config.d.mts +4 -0
- package/tooling/vite/vite.config.mjs +18 -0
- package/tooling/vitest/jsdom-shims.d.mts +1 -0
- package/tooling/vitest/jsdom-shims.mjs +58 -0
- package/tooling/vitest/vitest.config.d.mts +4 -0
- package/tooling/vitest/vitest.config.mjs +25 -0
- package/tooling/vitest/vitest.config.react.d.mts +4 -0
- package/tooling/vitest/vitest.config.react.mjs +27 -0
- package/tooling/vitest/vitest.setup.d.mts +1 -0
- 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
|
+
}
|