@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,148 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
/**
|
|
4
|
+
* Inspect package.exports and return non-root subpath candidates (e.g. './foo' ā 'foo').
|
|
5
|
+
* Skips '.', types-only entries, and wildcard patterns ('./*').
|
|
6
|
+
*/
|
|
7
|
+
export function inferSubpathsFromExports(pkg) {
|
|
8
|
+
const exports = pkg?.exports;
|
|
9
|
+
if (!exports || typeof exports !== 'object') {
|
|
10
|
+
return { allCandidates: [], defaultAllowed: null };
|
|
11
|
+
}
|
|
12
|
+
const candidates = [];
|
|
13
|
+
for (const key of Object.keys(exports)) {
|
|
14
|
+
if (key === '.' || !key.startsWith('./'))
|
|
15
|
+
continue;
|
|
16
|
+
if (key.includes('*'))
|
|
17
|
+
continue;
|
|
18
|
+
const trimmed = key.replace(/^\.\//, '');
|
|
19
|
+
if (!trimmed)
|
|
20
|
+
continue;
|
|
21
|
+
candidates.push(trimmed);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
allCandidates: candidates,
|
|
25
|
+
defaultAllowed: candidates[0] ?? null,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function renderCheckScript(opts) {
|
|
29
|
+
const allowed = JSON.stringify([opts.allowedSubpath]);
|
|
30
|
+
const forbidden = JSON.stringify(opts.forbiddenSubpaths, null, '\t');
|
|
31
|
+
return `import { build } from 'esbuild'
|
|
32
|
+
|
|
33
|
+
const ALLOWED_MODULES = ${allowed}
|
|
34
|
+
|
|
35
|
+
const FORBIDDEN_MODULES = ${forbidden}
|
|
36
|
+
|
|
37
|
+
const result = await build({
|
|
38
|
+
\tentryPoints: ['src/entry.ts'],
|
|
39
|
+
\tbundle: true,
|
|
40
|
+
\tformat: 'esm',
|
|
41
|
+
\tplatform: 'browser',
|
|
42
|
+
\tconditions: ['import', 'browser'],
|
|
43
|
+
\twrite: false,
|
|
44
|
+
\tmetafile: true,
|
|
45
|
+
\tminify: false,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const inputs = Object.keys(result.metafile.inputs)
|
|
49
|
+
const distInputs = inputs.filter((p) => p.includes('/dist/'))
|
|
50
|
+
const leaks = distInputs.filter((p) =>
|
|
51
|
+
\tFORBIDDEN_MODULES.some((m) => new RegExp(\`/dist/\${m}/\`).test(p))
|
|
52
|
+
)
|
|
53
|
+
const allowed = distInputs.filter((p) =>
|
|
54
|
+
\tALLOWED_MODULES.some((m) => new RegExp(\`/dist/\${m}/\`).test(p))
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const bundleBytes = result.outputFiles?.[0]?.contents.byteLength ?? 0
|
|
58
|
+
|
|
59
|
+
console.log(\`Bundle size: \${bundleBytes} bytes\`)
|
|
60
|
+
console.log(\`dist inputs in bundle (\${distInputs.length}):\`)
|
|
61
|
+
for (const p of distInputs) console.log(\` \${p}\`)
|
|
62
|
+
|
|
63
|
+
if (leaks.length > 0) {
|
|
64
|
+
\tconsole.error('\\nā Tree-shaking leak ā forbidden modules in the bundle:')
|
|
65
|
+
\tfor (const p of leaks) console.error(\` \${p}\`)
|
|
66
|
+
\tprocess.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (allowed.length === 0) {
|
|
70
|
+
\tconsole.error(
|
|
71
|
+
\t\t'\\nā Expected at least one allowed-subpath input ā entry may have failed to resolve.'
|
|
72
|
+
\t)
|
|
73
|
+
\tprocess.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('\\nā
Tree-shaking OK ā only allowed inputs present.')
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
function renderEntry(workspaceName, allowedSubpath) {
|
|
80
|
+
return `export * from '${workspaceName}/${allowedSubpath}'\n`;
|
|
81
|
+
}
|
|
82
|
+
// The treeshake-check app lives under apps/ and depends on the root package via
|
|
83
|
+
// `workspace:*`, which only resolves when a pnpm-workspace.yaml declares apps/*.
|
|
84
|
+
// pnpm 11 fails install (ERR_PNPM_IGNORED_BUILDS) on any dependency with an
|
|
85
|
+
// unapproved build script, approved via the `allowBuilds` map (package ā
|
|
86
|
+
// boolean ā pnpm 11 ignores the older `onlyBuiltDependencies` list). Approve
|
|
87
|
+
// the ones this monorepo shape needs:
|
|
88
|
+
// - esbuild: the tree-shake check builds with it
|
|
89
|
+
// - sharp: pulled in by the common apps/docs (Docusaurus) path; built
|
|
90
|
+
// - core-js: also pulled by apps/docs, but its postinstall is unwanted ā false
|
|
91
|
+
// Naming a package that isn't installed is a harmless no-op, so seeding the
|
|
92
|
+
// docs-path entries keeps `pnpm install` green the moment an apps/docs is added.
|
|
93
|
+
const WORKSPACE_YAML = `packages:
|
|
94
|
+
- 'apps/*'
|
|
95
|
+
|
|
96
|
+
allowBuilds:
|
|
97
|
+
esbuild: true
|
|
98
|
+
sharp: true
|
|
99
|
+
core-js: false
|
|
100
|
+
`;
|
|
101
|
+
/**
|
|
102
|
+
* Ensure a pnpm-workspace.yaml exists that globs apps/* (needed for the
|
|
103
|
+
* treeshake-check app's `workspace:*` dep to resolve). Never clobbers an
|
|
104
|
+
* existing file. Returns the relative path if written, else null.
|
|
105
|
+
*/
|
|
106
|
+
async function ensureWorkspaceYaml(targetDir) {
|
|
107
|
+
const wsPath = path.join(targetDir, 'pnpm-workspace.yaml');
|
|
108
|
+
if (await fs.pathExists(wsPath))
|
|
109
|
+
return null;
|
|
110
|
+
await fs.writeFile(wsPath, WORKSPACE_YAML);
|
|
111
|
+
return 'pnpm-workspace.yaml';
|
|
112
|
+
}
|
|
113
|
+
export async function generateTreeshakeCheck(targetDir, opts) {
|
|
114
|
+
const appDir = opts.appDir ?? 'apps/treeshake-check';
|
|
115
|
+
const root = path.join(targetDir, appDir);
|
|
116
|
+
await fs.ensureDir(path.join(root, 'src'));
|
|
117
|
+
const pkgName = `${opts.workspaceName.replace(/^@/, '').replace(/\//g, '-')}-treeshake-check`;
|
|
118
|
+
const packageJson = {
|
|
119
|
+
name: pkgName,
|
|
120
|
+
version: '0.0.0',
|
|
121
|
+
private: true,
|
|
122
|
+
type: 'module',
|
|
123
|
+
scripts: {
|
|
124
|
+
check: 'node check.mjs',
|
|
125
|
+
},
|
|
126
|
+
dependencies: {
|
|
127
|
+
[opts.workspaceName]: 'workspace:*',
|
|
128
|
+
},
|
|
129
|
+
devDependencies: {
|
|
130
|
+
esbuild: '^0.28.0',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const checkPath = path.join(root, 'check.mjs');
|
|
134
|
+
const entryPath = path.join(root, 'src', 'entry.ts');
|
|
135
|
+
const pkgPath = path.join(root, 'package.json');
|
|
136
|
+
await fs.writeJson(pkgPath, packageJson, { spaces: 2 });
|
|
137
|
+
await fs.writeFile(checkPath, renderCheckScript(opts));
|
|
138
|
+
await fs.writeFile(entryPath, renderEntry(opts.workspaceName, opts.allowedSubpath));
|
|
139
|
+
const written = [
|
|
140
|
+
path.join(appDir, 'package.json'),
|
|
141
|
+
path.join(appDir, 'check.mjs'),
|
|
142
|
+
path.join(appDir, 'src', 'entry.ts'),
|
|
143
|
+
];
|
|
144
|
+
const workspaceFile = await ensureWorkspaceYaml(targetDir);
|
|
145
|
+
if (workspaceFile)
|
|
146
|
+
written.push(workspaceFile);
|
|
147
|
+
return written;
|
|
148
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function generateTSConfig(config, targetDir) {
|
|
4
|
+
const tsconfigPath = path.join(targetDir, 'tsconfig.json');
|
|
5
|
+
// Base configuration extends our tooling. The shared presets anchor `paths`
|
|
6
|
+
// with ${configDir}, so no local baseUrl/paths is needed (baseUrl is
|
|
7
|
+
// deprecated in TS 5.9, removed in TS 7.0). Refs: https://aka.ms/ts6
|
|
8
|
+
// Targeting Bun (#225) means the Bun-typed preset (adds `types: ['bun']`),
|
|
9
|
+
// regardless of the chosen project config ā Bun is a runtime flag, not a type.
|
|
10
|
+
const preset = config.bun ? 'bun' : config.typescript.config;
|
|
11
|
+
const tsconfig = {
|
|
12
|
+
extends: `@rtorcato/repo-tooling/typescript/${preset}`,
|
|
13
|
+
compilerOptions: {},
|
|
14
|
+
include: ['src/**/*', 'reset.d.ts'],
|
|
15
|
+
exclude: ['node_modules', 'dist', 'build'],
|
|
16
|
+
};
|
|
17
|
+
// Adjust for project type
|
|
18
|
+
if (config.projectType === 'library') {
|
|
19
|
+
tsconfig.compilerOptions.outDir = './dist';
|
|
20
|
+
tsconfig.compilerOptions.rootDir = './src';
|
|
21
|
+
}
|
|
22
|
+
if (config.projectType === 'nextjs-app') {
|
|
23
|
+
tsconfig.include = ['next-env.d.ts', 'src', 'app', 'pages', 'components', 'reset.d.ts'];
|
|
24
|
+
tsconfig.exclude.push('.next');
|
|
25
|
+
}
|
|
26
|
+
await fs.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
/**
|
|
4
|
+
* Scaffold a starter turbo.json for a pnpm-workspace monorepo. Turborepo 2.x
|
|
5
|
+
* (`tasks`, not the pre-2.0 `pipeline`). The defaults cover the common
|
|
6
|
+
* build/lint/typecheck/test/dev pipeline; teams tune outputs per repo.
|
|
7
|
+
*/
|
|
8
|
+
export async function generateTurborepo(targetDir) {
|
|
9
|
+
const filepath = path.join(targetDir, 'turbo.json');
|
|
10
|
+
await fs.writeJson(filepath, TURBO_CONFIG, { spaces: 2 });
|
|
11
|
+
return 'turbo.json';
|
|
12
|
+
}
|
|
13
|
+
const TURBO_CONFIG = {
|
|
14
|
+
$schema: 'https://turborepo.com/schema.json',
|
|
15
|
+
tasks: {
|
|
16
|
+
build: {
|
|
17
|
+
dependsOn: ['^build'],
|
|
18
|
+
// Covers both plain bundlers (dist) and Next.js apps; cache is invalidated
|
|
19
|
+
// on source changes. Drop what your packages don't emit.
|
|
20
|
+
outputs: ['dist/**', '.next/**', '!.next/cache/**'],
|
|
21
|
+
},
|
|
22
|
+
lint: {},
|
|
23
|
+
typecheck: {
|
|
24
|
+
dependsOn: ['^build'],
|
|
25
|
+
},
|
|
26
|
+
test: {
|
|
27
|
+
dependsOn: ['^build'],
|
|
28
|
+
outputs: ['coverage/**'],
|
|
29
|
+
},
|
|
30
|
+
dev: {
|
|
31
|
+
cache: false,
|
|
32
|
+
persistent: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
const DOCS_WORKFLOW = `name: š Docs
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
jobs:
|
|
8
|
+
docs:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v7
|
|
14
|
+
- uses: pnpm/action-setup@v6
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
cache: pnpm
|
|
19
|
+
- run: pnpm install --frozen-lockfile
|
|
20
|
+
- run: pnpm docs
|
|
21
|
+
- uses: peaceiris/actions-gh-pages@v4
|
|
22
|
+
with:
|
|
23
|
+
github_token: \${{ secrets.GITHUB_TOKEN }}
|
|
24
|
+
publish_dir: ./docs
|
|
25
|
+
`;
|
|
26
|
+
export async function generateTypedocConfig(pkg, targetDir) {
|
|
27
|
+
const name = pkg?.name ?? 'My Library';
|
|
28
|
+
const config = {
|
|
29
|
+
extends: ['@rtorcato/repo-tooling/typedoc'],
|
|
30
|
+
entryPoints: ['./src/index.ts'],
|
|
31
|
+
name,
|
|
32
|
+
};
|
|
33
|
+
await fs.writeJson(path.join(targetDir, 'typedoc.json'), config, { spaces: 2 });
|
|
34
|
+
}
|
|
35
|
+
export async function generateTypedocWorkflow(targetDir) {
|
|
36
|
+
const workflowsDir = path.join(targetDir, '.github', 'workflows');
|
|
37
|
+
await fs.ensureDir(workflowsDir);
|
|
38
|
+
await fs.writeFile(path.join(workflowsDir, 'docs.yml'), DOCS_WORKFLOW);
|
|
39
|
+
return '.github/workflows/docs.yml';
|
|
40
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import packageJson from '../../package.json' with { type: 'json' };
|
|
7
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
8
|
+
import { fixCommand } from './commands/fix.js';
|
|
9
|
+
import { setupProject } from './commands/setup.js';
|
|
10
|
+
import { copyPreset, PRESETS } from './utils/copy-preset.js';
|
|
11
|
+
async function isSelfRepo(dir) {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = await fs.readJson(path.join(dir, 'package.json'));
|
|
14
|
+
return pkg.name === '@rtorcato/repo-tooling';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name('@rtorcato/repo-tooling')
|
|
23
|
+
.description('š ļø JavaScript and TypeScript tooling setup for modern projects')
|
|
24
|
+
.version(packageJson.version);
|
|
25
|
+
program
|
|
26
|
+
.command('setup')
|
|
27
|
+
.alias('init')
|
|
28
|
+
.description('š Setup tooling for a new or existing project')
|
|
29
|
+
.option('-d, --directory <path>', 'Target directory for setup', process.cwd())
|
|
30
|
+
.option('--skip-install', 'Skip installing dependencies')
|
|
31
|
+
.option('--preset <name>', 'Skip prompts; use defaults for a project type')
|
|
32
|
+
.option('--config <path>', 'Skip prompts; read a ProjectConfig or .repo-tooling.json lockfile from <path>')
|
|
33
|
+
.option('--dry-run', 'Print the resolved config and file list, write nothing')
|
|
34
|
+
.option('--config-schema', 'Print the JSON Schema for ProjectConfig and exit')
|
|
35
|
+
.action(setupProject);
|
|
36
|
+
program
|
|
37
|
+
.command('copy <config>')
|
|
38
|
+
.description('š Copy a specific configuration file to current directory')
|
|
39
|
+
.action(async (config) => {
|
|
40
|
+
if (!(config in PRESETS)) {
|
|
41
|
+
console.error(chalk.red(`\nā Unknown configuration: ${config}`));
|
|
42
|
+
console.log(chalk.gray('Available configurations:'));
|
|
43
|
+
for (const [key, { desc }] of Object.entries(PRESETS)) {
|
|
44
|
+
console.log(` ${chalk.green('ā')} ${chalk.bold(key)}: ${chalk.gray(desc)}`);
|
|
45
|
+
}
|
|
46
|
+
console.log();
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const result = await copyPreset(config);
|
|
51
|
+
console.log(chalk.green(`\nā
Copied ${result.desc}`));
|
|
52
|
+
console.log(chalk.gray(` From: ${result.source}`));
|
|
53
|
+
console.log(chalk.gray(` To: ${result.target}\n`));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error(chalk.red(`\nā Error copying configuration: ${error}\n`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const TOOL_CATALOG = [
|
|
60
|
+
{
|
|
61
|
+
name: 'TypeScript',
|
|
62
|
+
description: 'Base, React, Next.js, Node.js, Express, Bun tsconfig presets',
|
|
63
|
+
exports: [
|
|
64
|
+
'@rtorcato/repo-tooling/typescript/base',
|
|
65
|
+
'@rtorcato/repo-tooling/typescript/react',
|
|
66
|
+
'@rtorcato/repo-tooling/typescript/next',
|
|
67
|
+
'@rtorcato/repo-tooling/typescript/node',
|
|
68
|
+
'@rtorcato/repo-tooling/typescript/express',
|
|
69
|
+
'@rtorcato/repo-tooling/typescript/bun',
|
|
70
|
+
'@rtorcato/repo-tooling/typescript/test',
|
|
71
|
+
'@rtorcato/repo-tooling/typescript/reset',
|
|
72
|
+
],
|
|
73
|
+
fixTarget: 'tsconfig',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Bun',
|
|
77
|
+
description: 'Bun runtime/test-runner config (bunfig.toml + Bun-typed tsconfig)',
|
|
78
|
+
exports: ['@rtorcato/repo-tooling/typescript/bun'],
|
|
79
|
+
fixTarget: 'bun',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'ESLint',
|
|
83
|
+
description: 'Base and Next.js ESLint configurations',
|
|
84
|
+
exports: ['@rtorcato/repo-tooling/eslint/base', '@rtorcato/repo-tooling/eslint/nextjs'],
|
|
85
|
+
fixTarget: 'eslint',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Biome',
|
|
89
|
+
description: 'Fast formatter and linter configuration',
|
|
90
|
+
exports: ['@rtorcato/repo-tooling/biome'],
|
|
91
|
+
fixTarget: 'biome',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'Prettier',
|
|
95
|
+
description: 'Code formatter configuration',
|
|
96
|
+
exports: ['@rtorcato/repo-tooling/prettier'],
|
|
97
|
+
fixTarget: 'prettier',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'Vitest',
|
|
101
|
+
description: 'Testing framework configuration',
|
|
102
|
+
exports: [
|
|
103
|
+
'@rtorcato/repo-tooling/vitest/config',
|
|
104
|
+
'@rtorcato/repo-tooling/vitest/react',
|
|
105
|
+
'@rtorcato/repo-tooling/vitest/setup',
|
|
106
|
+
],
|
|
107
|
+
fixTarget: 'vitest',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'Jest',
|
|
111
|
+
description: 'Testing framework presets for browser and Node.js',
|
|
112
|
+
exports: [
|
|
113
|
+
'@rtorcato/repo-tooling/jest-presets/browser/jest-preset',
|
|
114
|
+
'@rtorcato/repo-tooling/jest-presets/node/jest-preset',
|
|
115
|
+
],
|
|
116
|
+
fixTarget: null,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Playwright',
|
|
120
|
+
description: 'End-to-end testing configuration',
|
|
121
|
+
exports: ['@rtorcato/repo-tooling/playwright'],
|
|
122
|
+
fixTarget: null,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'Cypress',
|
|
126
|
+
description: 'End-to-end testing configuration',
|
|
127
|
+
exports: ['@rtorcato/repo-tooling/cypress'],
|
|
128
|
+
fixTarget: 'cypress',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'Commitlint',
|
|
132
|
+
description: 'Conventional commit linting',
|
|
133
|
+
exports: ['@rtorcato/repo-tooling/commitlint/config'],
|
|
134
|
+
fixTarget: 'commitlint',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'Husky',
|
|
138
|
+
description: 'Git hooks for pre-commit validation',
|
|
139
|
+
exports: [],
|
|
140
|
+
fixTarget: 'husky',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'lint-staged',
|
|
144
|
+
description: 'Run linters on staged files (pairs with Husky)',
|
|
145
|
+
exports: [],
|
|
146
|
+
fixTarget: 'husky',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'Semantic Release',
|
|
150
|
+
description: 'Automated versioning and publishing',
|
|
151
|
+
exports: [
|
|
152
|
+
'@rtorcato/repo-tooling/semantic-release',
|
|
153
|
+
'@rtorcato/repo-tooling/semantic-release/github',
|
|
154
|
+
'@rtorcato/repo-tooling/semantic-release/docker',
|
|
155
|
+
],
|
|
156
|
+
fixTarget: 'semantic-release',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'Changesets',
|
|
160
|
+
description: 'Monorepo-friendly release tool (alternative to semantic-release)',
|
|
161
|
+
exports: ['@rtorcato/repo-tooling/changesets'],
|
|
162
|
+
fixTarget: 'changesets',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'Release Please',
|
|
166
|
+
description: 'Release-PR-driven release tool (alternative to semantic-release)',
|
|
167
|
+
exports: ['@rtorcato/repo-tooling/release-please'],
|
|
168
|
+
fixTarget: 'release-please',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'Oxlint',
|
|
172
|
+
description: 'Rust-based linter (additive to Biome/ESLint)',
|
|
173
|
+
exports: ['@rtorcato/repo-tooling/oxlint'],
|
|
174
|
+
fixTarget: 'oxlint',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'tsup',
|
|
178
|
+
description: 'TypeScript bundler configuration',
|
|
179
|
+
exports: ['@rtorcato/repo-tooling/tsup'],
|
|
180
|
+
fixTarget: null,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'TypeDoc',
|
|
184
|
+
description: 'API documentation generation for TypeScript library projects',
|
|
185
|
+
exports: ['@rtorcato/repo-tooling/typedoc'],
|
|
186
|
+
fixTarget: 'typedoc',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'Docs site',
|
|
190
|
+
description: 'Docusaurus docs site (apps/docs) with shared tokens + GitHub Pages deploy',
|
|
191
|
+
exports: [],
|
|
192
|
+
fixTarget: 'docs-site',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'esbuild',
|
|
196
|
+
description: 'Fast JavaScript bundler configuration',
|
|
197
|
+
exports: ['@rtorcato/repo-tooling/esbuild'],
|
|
198
|
+
fixTarget: null,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'Vite',
|
|
202
|
+
description: 'Modern web app build tool configuration',
|
|
203
|
+
exports: ['@rtorcato/repo-tooling/vite'],
|
|
204
|
+
fixTarget: null,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'EditorConfig',
|
|
208
|
+
description: 'Cross-editor formatting consistency (.editorconfig)',
|
|
209
|
+
exports: [],
|
|
210
|
+
fixTarget: 'editorconfig',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: '.nvmrc',
|
|
214
|
+
description: 'Pin Node version per repository',
|
|
215
|
+
exports: [],
|
|
216
|
+
fixTarget: 'nvmrc',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'knip',
|
|
220
|
+
description: 'Find unused files, exports, and dependencies',
|
|
221
|
+
exports: [],
|
|
222
|
+
fixTarget: 'knip',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'Dependabot',
|
|
226
|
+
description: 'Weekly automated dependency updates',
|
|
227
|
+
exports: [],
|
|
228
|
+
fixTarget: 'dependabot',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'Renovate',
|
|
232
|
+
description: 'Weekly automated dependency updates (alternative to Dependabot)',
|
|
233
|
+
exports: [],
|
|
234
|
+
fixTarget: 'renovate',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'CodeQL',
|
|
238
|
+
description: 'GitHub security scanning workflow',
|
|
239
|
+
exports: [],
|
|
240
|
+
fixTarget: 'codeql',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'SwiftLint',
|
|
244
|
+
description: 'Swift linter configuration (lint + --fix; pairs with `swiftlint --strict` in CI)',
|
|
245
|
+
exports: [],
|
|
246
|
+
fixTarget: 'swiftlint',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'Periphery',
|
|
250
|
+
description: 'Swift dead-code scanner configuration (retains public API for SwiftPM libraries)',
|
|
251
|
+
exports: [],
|
|
252
|
+
fixTarget: 'periphery',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'AI Agent Setup',
|
|
256
|
+
description: 'Scaffold AGENTS.md, CLAUDE.md, Cursor/Copilot rules, Claude skill, MCP example',
|
|
257
|
+
exports: [],
|
|
258
|
+
fixTarget: 'ai',
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
program
|
|
262
|
+
.command('list')
|
|
263
|
+
.alias('ls')
|
|
264
|
+
.description('š List all available tooling configurations')
|
|
265
|
+
.option('--json', 'Emit machine-readable JSON output')
|
|
266
|
+
.action((options) => {
|
|
267
|
+
if (options.json) {
|
|
268
|
+
console.log(JSON.stringify({ tools: TOOL_CATALOG }, null, 2));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
console.log(chalk.cyan('\nš ļø Available tooling configurations:\n'));
|
|
272
|
+
for (const { name, description } of TOOL_CATALOG) {
|
|
273
|
+
console.log(` ${chalk.green('ā')} ${chalk.bold(name)}: ${chalk.gray(description)}`);
|
|
274
|
+
}
|
|
275
|
+
console.log(chalk.dim('\nš” Run `repo-tooling setup` for a new project'));
|
|
276
|
+
console.log(chalk.dim(' or `repo-tooling fix` to apply missing pieces to an existing one\n'));
|
|
277
|
+
});
|
|
278
|
+
program
|
|
279
|
+
.command('doctor')
|
|
280
|
+
.description('𩺠Diagnose project alignment with @rtorcato/repo-tooling presets')
|
|
281
|
+
.option('-d, --directory <path>', 'Target directory to diagnose', process.cwd())
|
|
282
|
+
.option('--json', 'Emit machine-readable JSON output')
|
|
283
|
+
.action(doctorCommand);
|
|
284
|
+
program
|
|
285
|
+
.command('fix [target]')
|
|
286
|
+
.description('š§ Apply scaffolders for items doctor flagged')
|
|
287
|
+
.option('-d, --directory <path>', 'Target directory', process.cwd())
|
|
288
|
+
.option('--yes', 'Assume yes to all prompts (including drift overwrites)')
|
|
289
|
+
.option('--dry-run', 'Print what would change without writing files')
|
|
290
|
+
.option('--json', 'Emit machine-readable JSON output (implies --yes)')
|
|
291
|
+
.option('--list', 'List all registered fix targets and exit')
|
|
292
|
+
.option('--resync', 'Re-scaffold every file recorded in .repo-tooling.json')
|
|
293
|
+
.option('--diff', 'Show a unified diff of each change before confirming')
|
|
294
|
+
.action((target, options) => fixCommand(target, {
|
|
295
|
+
directory: options.directory,
|
|
296
|
+
yes: options.yes,
|
|
297
|
+
dryRun: options.dryRun,
|
|
298
|
+
json: options.json,
|
|
299
|
+
list: options.list,
|
|
300
|
+
resync: options.resync,
|
|
301
|
+
diff: options.diff,
|
|
302
|
+
}));
|
|
303
|
+
program.hook('preAction', async (_, actionCommand) => {
|
|
304
|
+
const name = actionCommand.name();
|
|
305
|
+
if (name === 'setup' || name === 'doctor' || name === 'fix') {
|
|
306
|
+
// `fix --list` is read-only and safe to run anywhere, including this repo.
|
|
307
|
+
if (name === 'fix' && actionCommand.opts().list)
|
|
308
|
+
return;
|
|
309
|
+
// Dogfood escape hatch (#273): allow read-only `doctor` against this repo
|
|
310
|
+
// so CI can audit our own config the same way it does consumers'. Scoped
|
|
311
|
+
// to doctor ā the mutating setup/fix stay blocked even with the flag set.
|
|
312
|
+
if (name === 'doctor' && process.env.REPO_TOOLING_ALLOW_SELF === '1')
|
|
313
|
+
return;
|
|
314
|
+
const dir = actionCommand.opts().directory ?? process.cwd();
|
|
315
|
+
if (await isSelfRepo(dir)) {
|
|
316
|
+
console.log(chalk.yellow('\nā ļø This command cannot be run inside the @rtorcato/repo-tooling repo itself.\n'));
|
|
317
|
+
console.log(chalk.gray(' setup and doctor are for consumer projects, not for the tooling repo.\n'));
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
// Handle unknown commands
|
|
323
|
+
program.on('command:*', () => {
|
|
324
|
+
console.error(chalk.red(`\nā Unknown command: ${program.args.join(' ')}`));
|
|
325
|
+
console.log(chalk.gray('See --help for a list of available commands.\n'));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
});
|
|
328
|
+
// Show help if no arguments
|
|
329
|
+
if (!process.argv.slice(2).length) {
|
|
330
|
+
program.outputHelp();
|
|
331
|
+
}
|
|
332
|
+
program.parse();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
export const PRESETS = {
|
|
4
|
+
biome: {
|
|
5
|
+
source: 'tooling/biome/biome.json',
|
|
6
|
+
target: 'biome.json',
|
|
7
|
+
desc: 'Biome formatter and linter configuration',
|
|
8
|
+
},
|
|
9
|
+
bun: {
|
|
10
|
+
source: 'tooling/bun/bunfig.toml',
|
|
11
|
+
target: 'bunfig.toml',
|
|
12
|
+
desc: 'Bun runtime/test-runner configuration',
|
|
13
|
+
},
|
|
14
|
+
nx: {
|
|
15
|
+
source: 'tooling/nx/nx.json',
|
|
16
|
+
target: 'nx.json',
|
|
17
|
+
desc: 'Nx monorepo task-orchestrator configuration',
|
|
18
|
+
},
|
|
19
|
+
changesets: {
|
|
20
|
+
source: 'tooling/changesets/config.json',
|
|
21
|
+
target: '.changeset/config.json',
|
|
22
|
+
desc: 'Changesets release-tool configuration',
|
|
23
|
+
},
|
|
24
|
+
'release-please': {
|
|
25
|
+
source: 'tooling/release-please/release-please-config.json',
|
|
26
|
+
target: 'release-please-config.json',
|
|
27
|
+
desc: 'Release Please release-tool configuration',
|
|
28
|
+
},
|
|
29
|
+
oxlint: {
|
|
30
|
+
source: 'tooling/oxlint/oxlintrc.json',
|
|
31
|
+
target: '.oxlintrc.json',
|
|
32
|
+
desc: 'Oxlint linter configuration (additive to Biome)',
|
|
33
|
+
},
|
|
34
|
+
tsconfig: {
|
|
35
|
+
source: 'tooling/typescript/tsconfig.base.json',
|
|
36
|
+
target: 'tsconfig.json',
|
|
37
|
+
desc: 'TypeScript base configuration',
|
|
38
|
+
},
|
|
39
|
+
swiftlint: {
|
|
40
|
+
source: 'tooling/swift/swiftlint.yml',
|
|
41
|
+
target: '.swiftlint.yml',
|
|
42
|
+
desc: 'SwiftLint configuration (lint + --fix; the standard swift-common runs)',
|
|
43
|
+
},
|
|
44
|
+
periphery: {
|
|
45
|
+
source: 'tooling/swift/periphery.yml',
|
|
46
|
+
target: '.periphery.yml',
|
|
47
|
+
desc: 'Periphery dead-code scan configuration (retains public API)',
|
|
48
|
+
},
|
|
49
|
+
'claude-skill': {
|
|
50
|
+
source: 'tooling/claude/repo-tooling.md',
|
|
51
|
+
target: '.claude/skills/repo-tooling.md',
|
|
52
|
+
desc: 'Claude Code skill for driving the repo-tooling CLI',
|
|
53
|
+
legacyTarget: '.claude/skills/js-tooling.md',
|
|
54
|
+
},
|
|
55
|
+
'mcp-example': {
|
|
56
|
+
source: 'tooling/mcp/mcp.json.example',
|
|
57
|
+
target: '.mcp.json.example',
|
|
58
|
+
desc: 'Commented MCP server template (copy to .mcp.json to activate)',
|
|
59
|
+
},
|
|
60
|
+
'docusaurus-sync-changelog': {
|
|
61
|
+
source: 'tooling/docusaurus/sync-changelog.mjs',
|
|
62
|
+
target: 'scripts/sync-changelog.mjs',
|
|
63
|
+
desc: 'Canonical CHANGELOG ā docs sync script for Docusaurus sites',
|
|
64
|
+
},
|
|
65
|
+
'docusaurus-theme-tokens': {
|
|
66
|
+
source: 'tooling/docusaurus/theme-tokens.css',
|
|
67
|
+
target: 'apps/docs/src/css/_jt-tokens.css',
|
|
68
|
+
desc: 'Shared Docusaurus design tokens (Geist + navy surfaces; accent per-project)',
|
|
69
|
+
},
|
|
70
|
+
'docusaurus-theme': {
|
|
71
|
+
source: 'tooling/docusaurus/theme.css',
|
|
72
|
+
target: 'apps/docs/src/css/theme.css',
|
|
73
|
+
desc: 'Shared Docusaurus component theme (navbar, cards, sidebar, footer, tables; accent-agnostic ā @import after docusaurus-theme-tokens)',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
export function getPackageRoot() {
|
|
77
|
+
const cliFile = new URL(import.meta.url).pathname;
|
|
78
|
+
return path.dirname(path.dirname(path.dirname(path.dirname(cliFile))));
|
|
79
|
+
}
|
|
80
|
+
export async function copyPreset(name, targetDir = process.cwd()) {
|
|
81
|
+
const preset = PRESETS[name];
|
|
82
|
+
const packageRoot = getPackageRoot();
|
|
83
|
+
const sourcePath = path.join(packageRoot, preset.source);
|
|
84
|
+
const targetPath = path.join(targetDir, preset.target);
|
|
85
|
+
await fs.copy(sourcePath, targetPath);
|
|
86
|
+
// Remove the pre-rename artifact so a re-run migrates it instead of leaving both.
|
|
87
|
+
if (preset.legacyTarget) {
|
|
88
|
+
const legacyPath = path.join(targetDir, preset.legacyTarget);
|
|
89
|
+
if (legacyPath !== targetPath)
|
|
90
|
+
await fs.remove(legacyPath);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
source: preset.source,
|
|
94
|
+
target: preset.target,
|
|
95
|
+
targetPath,
|
|
96
|
+
desc: preset.desc,
|
|
97
|
+
};
|
|
98
|
+
}
|