@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,123 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { generateSwiftProject } from '../../languages/swift/scaffold.js';
|
|
5
|
+
import { installAiSetup } from './agent-rules.js';
|
|
6
|
+
import { bundlerNeedsEsbuild, ensureBuildApprovals, generateBuildConfigs } from './build.js';
|
|
7
|
+
import { ensurePnpmSettings } from './pnpm-workspace.js';
|
|
8
|
+
import { generateGitConfigs } from './git.js';
|
|
9
|
+
import { generateGitHubActions } from './github-actions.js';
|
|
10
|
+
import { generateLintingConfigs } from './linting.js';
|
|
11
|
+
import { generateKnipConfig, generateMiscBaseline, generateVscodeExtensions } from './misc.js';
|
|
12
|
+
import { generatePackageJson } from './package-json.js';
|
|
13
|
+
import { generateReadme } from './readme.js';
|
|
14
|
+
import { generateSecurityConfigs } from './security.js';
|
|
15
|
+
import { generateTestingConfigs } from './testing.js';
|
|
16
|
+
import { generateTreeshakeCheck, inferSubpathsFromExports } from './treeshake.js';
|
|
17
|
+
import { generateTSConfig } from './tsconfig.js';
|
|
18
|
+
import { generateTailwind } from './tailwind.js';
|
|
19
|
+
import { generateTurborepo } from './turborepo.js';
|
|
20
|
+
import { generateNx } from './nx.js';
|
|
21
|
+
import { generateBun } from './bun.js';
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
export async function generateConfigs(config, targetDir) {
|
|
25
|
+
// Swift takes its own path (#288) rather than opting out of each step below.
|
|
26
|
+
// Everything from here down is rooted in package.json — the file a Swift repo
|
|
27
|
+
// is defined by not having.
|
|
28
|
+
if (config.language === 'swift') {
|
|
29
|
+
return generateSwiftProject(config, targetDir);
|
|
30
|
+
}
|
|
31
|
+
// Generate package.json (must run before generateMiscBaseline,
|
|
32
|
+
// which sets engines.node on the resulting file)
|
|
33
|
+
await generatePackageJson(config, targetDir);
|
|
34
|
+
// Universal baseline: .editorconfig, .nvmrc, engines.node, knip.json
|
|
35
|
+
await generateMiscBaseline(targetDir);
|
|
36
|
+
// Recommend the editor extensions matching the enabled tools
|
|
37
|
+
await generateVscodeExtensions(config, targetDir);
|
|
38
|
+
// Generate TypeScript configuration
|
|
39
|
+
if (config.typescript.enabled) {
|
|
40
|
+
await generateTSConfig(config, targetDir);
|
|
41
|
+
}
|
|
42
|
+
// Generate linting configurations
|
|
43
|
+
if (config.linting.tool !== 'none') {
|
|
44
|
+
await generateLintingConfigs(config, targetDir);
|
|
45
|
+
}
|
|
46
|
+
// Generate testing configurations
|
|
47
|
+
if (config.testing.framework !== 'none') {
|
|
48
|
+
await generateTestingConfigs(config, targetDir);
|
|
49
|
+
}
|
|
50
|
+
// Generate git configurations
|
|
51
|
+
if (config.gitHooks || config.commitLint) {
|
|
52
|
+
await generateGitConfigs(config, targetDir);
|
|
53
|
+
}
|
|
54
|
+
// Generate GitHub Actions workflow
|
|
55
|
+
await generateGitHubActions(config, targetDir);
|
|
56
|
+
// Generate security automation (Dependabot + CodeQL)
|
|
57
|
+
if (config.securityAutomation) {
|
|
58
|
+
await generateSecurityConfigs(targetDir);
|
|
59
|
+
}
|
|
60
|
+
// Generate build configurations
|
|
61
|
+
if (config.bundler !== 'none') {
|
|
62
|
+
await generateBuildConfigs(config, targetDir);
|
|
63
|
+
}
|
|
64
|
+
// Tree-shake verification check (libraries only, when opted-in)
|
|
65
|
+
if (config.treeshakeCheck && config.projectType === 'library') {
|
|
66
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
67
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
68
|
+
const { allCandidates, defaultAllowed } = inferSubpathsFromExports(pkg);
|
|
69
|
+
if (defaultAllowed && allCandidates.length >= 2) {
|
|
70
|
+
await generateTreeshakeCheck(targetDir, {
|
|
71
|
+
workspaceName: config.projectName,
|
|
72
|
+
allowedSubpath: defaultAllowed,
|
|
73
|
+
forbiddenSubpaths: allCandidates.filter((s) => s !== defaultAllowed),
|
|
74
|
+
});
|
|
75
|
+
// treeshake-check writes pnpm-workspace.yaml; regenerate knip so it
|
|
76
|
+
// picks up the workspaces form (the baseline pass above ran before the
|
|
77
|
+
// workspace file existed and emitted the flat single-package form).
|
|
78
|
+
await generateKnipConfig(targetDir);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Approve esbuild's build script for pnpm 11 via pnpm-workspace.yaml. Runs
|
|
82
|
+
// after the treeshake-check path so its richer workspace file (if written)
|
|
83
|
+
// is never clobbered.
|
|
84
|
+
await ensureBuildApprovals(config, targetDir);
|
|
85
|
+
// Family-wide pnpm settings (#314). Runs last of the workspace writers so it
|
|
86
|
+
// merges into whatever they wrote rather than racing them for the file.
|
|
87
|
+
await ensurePnpmSettings(targetDir, bundlerNeedsEsbuild(config));
|
|
88
|
+
// Turborepo task pipeline (pnpm-workspace monorepos, when opted-in)
|
|
89
|
+
if (config.turborepo) {
|
|
90
|
+
await generateTurborepo(targetDir);
|
|
91
|
+
}
|
|
92
|
+
// Nx task orchestrator (alternative to Turborepo, when opted-in)
|
|
93
|
+
if (config.nx) {
|
|
94
|
+
await generateNx(targetDir);
|
|
95
|
+
}
|
|
96
|
+
// Tailwind CSS v4 (frontend projects, when opted-in)
|
|
97
|
+
if (config.tailwind) {
|
|
98
|
+
await generateTailwind(targetDir);
|
|
99
|
+
}
|
|
100
|
+
// Bun runtime (#225): bunfig.toml (the Bun-typed tsconfig is already emitted
|
|
101
|
+
// by generateTSConfig above when config.bun, so generateBun only adds bunfig).
|
|
102
|
+
if (config.bun) {
|
|
103
|
+
await generateBun(targetDir);
|
|
104
|
+
}
|
|
105
|
+
// AI agent files (AGENTS.md, CLAUDE.md, Cursor/Copilot, Claude skill, MCP example)
|
|
106
|
+
if (config.aiSetup) {
|
|
107
|
+
await installAiSetup(targetDir);
|
|
108
|
+
}
|
|
109
|
+
// Generate README
|
|
110
|
+
await generateReadme(config, targetDir);
|
|
111
|
+
// Copy ts-reset if TypeScript is enabled
|
|
112
|
+
if (config.typescript.enabled) {
|
|
113
|
+
await copyTSReset(targetDir);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function copyTSReset(targetDir) {
|
|
117
|
+
const toolingRoot = path.resolve(__dirname, '../../../tooling');
|
|
118
|
+
const resetPath = path.join(toolingRoot, 'typescript', 'reset.d.ts');
|
|
119
|
+
const targetPath = path.join(targetDir, 'reset.d.ts');
|
|
120
|
+
if (await fs.pathExists(resetPath)) {
|
|
121
|
+
await fs.copy(resetPath, targetPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function generateLintingConfigs(config, targetDir) {
|
|
4
|
+
// Generate Biome config
|
|
5
|
+
if (config.linting.tool === 'biome' || config.linting.tool === 'both') {
|
|
6
|
+
await generateBiomeConfig(targetDir);
|
|
7
|
+
}
|
|
8
|
+
// Generate ESLint config
|
|
9
|
+
if (config.linting.tool === 'eslint' || config.linting.tool === 'both') {
|
|
10
|
+
await generateESLintConfig(config, targetDir);
|
|
11
|
+
}
|
|
12
|
+
// Generate Prettier config if using ESLint
|
|
13
|
+
if (config.linting.tool === 'eslint') {
|
|
14
|
+
await generatePrettierConfig(targetDir);
|
|
15
|
+
}
|
|
16
|
+
// Generate Oxlint config (additive — runs alongside Biome/ESLint)
|
|
17
|
+
if (config.oxlint) {
|
|
18
|
+
await generateOxlintConfig(targetDir);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function generateOxlintConfig(targetDir) {
|
|
22
|
+
// Oxlint's `extends` resolution from npm packages isn't reliably supported,
|
|
23
|
+
// so we copy the full preset rather than write a thin pointer file (same
|
|
24
|
+
// pattern as biome.jsonc — the user owns the file once it's in their repo).
|
|
25
|
+
const { copyPreset } = await import('../utils/copy-preset.js');
|
|
26
|
+
await copyPreset('oxlint', targetDir);
|
|
27
|
+
}
|
|
28
|
+
export async function generateBiomeConfig(targetDir) {
|
|
29
|
+
const biomeConfigPath = path.join(targetDir, 'biome.jsonc');
|
|
30
|
+
// Biome 2.x schema + shape. The base preset (extends) already defines the
|
|
31
|
+
// file globs via `files.includes`; emitting the old 1.x `include`/`ignore`
|
|
32
|
+
// keys here forced consumers to run `biome migrate` before `biome check`
|
|
33
|
+
// would run at all.
|
|
34
|
+
const biomeConfig = {
|
|
35
|
+
$schema: 'https://biomejs.dev/schemas/2.5.0/schema.json',
|
|
36
|
+
extends: ['@rtorcato/repo-tooling/biome'],
|
|
37
|
+
};
|
|
38
|
+
await fs.writeJson(biomeConfigPath, biomeConfig, { spaces: 2 });
|
|
39
|
+
}
|
|
40
|
+
export async function generateESLintConfig(config, targetDir) {
|
|
41
|
+
const eslintConfigPath = path.join(targetDir, 'eslint.config.mjs');
|
|
42
|
+
const configType = config.linting.eslintConfig || 'base';
|
|
43
|
+
const eslintConfig = `import { default as config } from '@rtorcato/repo-tooling/eslint/${configType}'
|
|
44
|
+
|
|
45
|
+
export default config
|
|
46
|
+
`;
|
|
47
|
+
await fs.writeFile(eslintConfigPath, eslintConfig);
|
|
48
|
+
}
|
|
49
|
+
export async function generatePrettierConfig(targetDir) {
|
|
50
|
+
const prettierConfigPath = path.join(targetDir, 'prettier.config.mjs');
|
|
51
|
+
const prettierConfig = `export { default } from '@rtorcato/repo-tooling/prettier'
|
|
52
|
+
`;
|
|
53
|
+
await fs.writeFile(prettierConfigPath, prettierConfig);
|
|
54
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
const EDITORCONFIG_CONTENT = `root = true
|
|
4
|
+
|
|
5
|
+
[*]
|
|
6
|
+
charset = utf-8
|
|
7
|
+
end_of_line = lf
|
|
8
|
+
indent_style = tab
|
|
9
|
+
indent_size = 2
|
|
10
|
+
insert_final_newline = true
|
|
11
|
+
trim_trailing_whitespace = true
|
|
12
|
+
|
|
13
|
+
[*.md]
|
|
14
|
+
trim_trailing_whitespace = false
|
|
15
|
+
|
|
16
|
+
[*.{json,yml,yaml}]
|
|
17
|
+
indent_style = space
|
|
18
|
+
indent_size = 2
|
|
19
|
+
`;
|
|
20
|
+
const NVMRC_CONTENT = '22\n';
|
|
21
|
+
const SIZE_LIMIT_CONFIG = [
|
|
22
|
+
{
|
|
23
|
+
name: 'package (default entry)',
|
|
24
|
+
path: 'dist/index.js',
|
|
25
|
+
limit: '10 kB',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
// Exports-driven size-limit config for multi-subpath libraries: one budget per
|
|
29
|
+
// exported subpath, computed from package.json at run time so newly added
|
|
30
|
+
// modules are covered automatically. The static-array form silently drifts as
|
|
31
|
+
// modules ship without a budget (#165). The root '.' barrel is skipped — it
|
|
32
|
+
// re-exports everything and grows legitimately. Tighten individual modules via
|
|
33
|
+
// OVERRIDES.
|
|
34
|
+
const SIZE_LIMIT_CJS = `const pkg = require('./package.json')
|
|
35
|
+
|
|
36
|
+
// Per-subpath budget overrides, e.g. { './clipboard': '500 B' }.
|
|
37
|
+
const OVERRIDES = {}
|
|
38
|
+
const DEFAULT_LIMIT = '10 kB'
|
|
39
|
+
|
|
40
|
+
// Resolve the ESM entry file for an exports condition (string, or an object
|
|
41
|
+
// with a string \`import\`, or a nested \`import.default\`).
|
|
42
|
+
function importPath(cond) {
|
|
43
|
+
if (typeof cond === 'string') return cond
|
|
44
|
+
if (cond && typeof cond === 'object') {
|
|
45
|
+
if (typeof cond.import === 'string') return cond.import
|
|
46
|
+
if (cond.import && typeof cond.import === 'object') return cond.import.default
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = Object.entries(pkg.exports || {})
|
|
52
|
+
.filter(([sub]) => sub !== '.' && sub !== './package.json')
|
|
53
|
+
.map(([sub, cond]) => [sub, importPath(cond)])
|
|
54
|
+
.filter(([, file]) => typeof file === 'string')
|
|
55
|
+
.map(([sub, file]) => ({
|
|
56
|
+
name: \`\${pkg.name}\${sub.slice(1)}\`,
|
|
57
|
+
path: file.replace(/^\\.\\//, ''),
|
|
58
|
+
limit: OVERRIDES[sub] || DEFAULT_LIMIT,
|
|
59
|
+
}))
|
|
60
|
+
`;
|
|
61
|
+
const CODEOWNERS_CONTENT = `# .github/CODEOWNERS
|
|
62
|
+
# Each line is a file pattern followed by one or more owners.
|
|
63
|
+
# Owners can be GitHub usernames (@user) or team names (@org/team).
|
|
64
|
+
# Order matters — the last matching pattern wins.
|
|
65
|
+
# See https://docs.github.com/articles/about-code-owners/
|
|
66
|
+
#
|
|
67
|
+
# Examples:
|
|
68
|
+
# * @your-username
|
|
69
|
+
# /src/api/ @backend-team
|
|
70
|
+
# /docs/ @docs-team @your-username
|
|
71
|
+
# *.md @docs-team
|
|
72
|
+
|
|
73
|
+
* @TODO-owner
|
|
74
|
+
`;
|
|
75
|
+
export async function generateEditorConfig(targetDir) {
|
|
76
|
+
await fs.writeFile(path.join(targetDir, '.editorconfig'), EDITORCONFIG_CONTENT);
|
|
77
|
+
}
|
|
78
|
+
export async function generateNvmrc(targetDir) {
|
|
79
|
+
await fs.writeFile(path.join(targetDir, '.nvmrc'), NVMRC_CONTENT);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The VS Code extension IDs recommended for the tools this config enables.
|
|
83
|
+
* EditorConfig is always included — the baseline always writes .editorconfig.
|
|
84
|
+
*/
|
|
85
|
+
export function recommendedExtensions(config) {
|
|
86
|
+
const ids = new Set(['EditorConfig.EditorConfig']);
|
|
87
|
+
const lint = config.linting.tool;
|
|
88
|
+
if (lint === 'biome' || lint === 'both' || config.formatting.tool === 'biome') {
|
|
89
|
+
ids.add('biomejs.biome');
|
|
90
|
+
}
|
|
91
|
+
if (lint === 'eslint' || lint === 'both')
|
|
92
|
+
ids.add('dbaeumer.vscode-eslint');
|
|
93
|
+
if (config.formatting.tool === 'prettier')
|
|
94
|
+
ids.add('esbenp.prettier-vscode');
|
|
95
|
+
if (config.oxlint)
|
|
96
|
+
ids.add('oxc.oxc-vscode');
|
|
97
|
+
if (config.testing.framework === 'vitest')
|
|
98
|
+
ids.add('vitest.explorer');
|
|
99
|
+
if (config.testing.framework === 'playwright')
|
|
100
|
+
ids.add('ms-playwright.playwright');
|
|
101
|
+
return [...ids];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Emit .vscode/extensions.json recommending the editor extensions that match the
|
|
105
|
+
* enabled tools. Merge-friendly: preserves any recommendations already present
|
|
106
|
+
* and never clobbers a hand-authored file it can't parse as JSON (e.g. JSONC
|
|
107
|
+
* with comments) — returns null in that case. Returns the written path otherwise.
|
|
108
|
+
*/
|
|
109
|
+
export async function generateVscodeExtensions(config, targetDir) {
|
|
110
|
+
const filepath = path.join(targetDir, '.vscode', 'extensions.json');
|
|
111
|
+
let existing = [];
|
|
112
|
+
if (await fs.pathExists(filepath)) {
|
|
113
|
+
try {
|
|
114
|
+
const current = (await fs.readJson(filepath));
|
|
115
|
+
if (Array.isArray(current.recommendations)) {
|
|
116
|
+
existing = current.recommendations.filter((r) => typeof r === 'string');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const merged = [...new Set([...existing, ...recommendedExtensions(config)])];
|
|
124
|
+
await fs.ensureDir(path.dirname(filepath));
|
|
125
|
+
await fs.writeJson(filepath, { recommendations: merged }, { spaces: 2 });
|
|
126
|
+
return '.vscode/extensions.json';
|
|
127
|
+
}
|
|
128
|
+
const DEFAULT_NODE_MAJOR = 22;
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the authoritative Node major: an existing .nvmrc/.node-version wins,
|
|
131
|
+
* else the `engines.node` floor, else the default. Used to seed .nvmrc when a
|
|
132
|
+
* repo has no pin yet before pointing workflows at it.
|
|
133
|
+
*/
|
|
134
|
+
async function resolveNodeMajor(targetDir) {
|
|
135
|
+
for (const candidate of ['.nvmrc', '.node-version']) {
|
|
136
|
+
const p = path.join(targetDir, candidate);
|
|
137
|
+
if (await fs.pathExists(p)) {
|
|
138
|
+
const m = (await fs.readFile(p, 'utf-8')).trim().match(/\d+/);
|
|
139
|
+
if (m)
|
|
140
|
+
return Number.parseInt(m[0], 10);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
144
|
+
if (await fs.pathExists(pkgPath)) {
|
|
145
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
146
|
+
const node = pkg.engines?.node;
|
|
147
|
+
const m = node?.match(/\d+/);
|
|
148
|
+
if (m)
|
|
149
|
+
return Number.parseInt(m[0], 10);
|
|
150
|
+
}
|
|
151
|
+
return DEFAULT_NODE_MAJOR;
|
|
152
|
+
}
|
|
153
|
+
// A hardcoded scalar `node-version: <major>` line (leading digit after an
|
|
154
|
+
// optional quote). Deliberately does NOT match matrix arrays (`[22, 24]`),
|
|
155
|
+
// `${{ }}` expressions, or bare `node-version:` input keys — those are left as-is.
|
|
156
|
+
const NODE_VERSION_LINE = /^([ \t]*)node-version:[ \t]*['"]?\d[\w.-]*['"]?[ \t]*$/gm;
|
|
157
|
+
/**
|
|
158
|
+
* Make .nvmrc the single Node source of truth: ensure it exists (pinned to the
|
|
159
|
+
* resolved major) and rewrite hardcoded scalar `node-version:` lines in CI
|
|
160
|
+
* workflows to `node-version-file: .nvmrc`. Returns the files changed.
|
|
161
|
+
*/
|
|
162
|
+
export async function alignNodeVersion(targetDir) {
|
|
163
|
+
const written = [];
|
|
164
|
+
const nvmrcPath = path.join(targetDir, '.nvmrc');
|
|
165
|
+
if (!(await fs.pathExists(nvmrcPath))) {
|
|
166
|
+
const major = await resolveNodeMajor(targetDir);
|
|
167
|
+
await fs.writeFile(nvmrcPath, `${major}\n`);
|
|
168
|
+
written.push('.nvmrc');
|
|
169
|
+
}
|
|
170
|
+
const workflowsDir = path.join(targetDir, '.github', 'workflows');
|
|
171
|
+
if (await fs.pathExists(workflowsDir)) {
|
|
172
|
+
const files = (await fs.readdir(workflowsDir)).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
const p = path.join(workflowsDir, file);
|
|
175
|
+
const contents = await fs.readFile(p, 'utf-8');
|
|
176
|
+
const next = contents.replace(NODE_VERSION_LINE, '$1node-version-file: .nvmrc');
|
|
177
|
+
if (next !== contents) {
|
|
178
|
+
await fs.writeFile(p, next);
|
|
179
|
+
written.push(`.github/workflows/${file}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return written;
|
|
184
|
+
}
|
|
185
|
+
export async function ensureEnginesNode(targetDir, version = '>=22') {
|
|
186
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
187
|
+
if (!(await fs.pathExists(pkgPath)))
|
|
188
|
+
return 'no-package-json';
|
|
189
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
190
|
+
const engines = pkg.engines ?? {};
|
|
191
|
+
if (engines.node)
|
|
192
|
+
return 'already-set';
|
|
193
|
+
pkg.engines = { ...engines, node: version };
|
|
194
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
195
|
+
return 'added';
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build a knip config whose `entry` matches the project's build model, read
|
|
199
|
+
* from package.json `exports`:
|
|
200
|
+
* - >1 public subpath export → per-file/multi-entry lib (e.g. react-common,
|
|
201
|
+
* whose build makes every src module its own entry). Every file is a public
|
|
202
|
+
* entry, so `entry` covers all of src — flagging none as "unused files"
|
|
203
|
+
* while still catching unused exports and dependencies.
|
|
204
|
+
* - single root barrel / no exports → narrow `src/index` entry so knip can
|
|
205
|
+
* trace and flag genuinely unreachable modules.
|
|
206
|
+
* `.tsx` is included so React libraries aren't silently skipped.
|
|
207
|
+
*/
|
|
208
|
+
/**
|
|
209
|
+
* Extract package globs from a pnpm-workspace.yaml `packages:` list without a
|
|
210
|
+
* YAML dependency. Matches `- 'apps/*'` / `- "apps/*"` / `- apps/*` entries
|
|
211
|
+
* under the `packages:` key, stopping at the next top-level key.
|
|
212
|
+
*/
|
|
213
|
+
function parseWorkspacePackages(yaml) {
|
|
214
|
+
const globs = [];
|
|
215
|
+
let inPackages = false;
|
|
216
|
+
for (const line of yaml.split('\n')) {
|
|
217
|
+
if (/^packages:\s*$/.test(line)) {
|
|
218
|
+
inPackages = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!inPackages)
|
|
222
|
+
continue;
|
|
223
|
+
const item = line.match(/^\s*-\s*['"]?([^'"#]+?)['"]?\s*$/);
|
|
224
|
+
if (item?.[1]) {
|
|
225
|
+
globs.push(item[1].trim());
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (/^\S/.test(line))
|
|
229
|
+
break; // a new top-level key ends the list
|
|
230
|
+
}
|
|
231
|
+
return globs;
|
|
232
|
+
}
|
|
233
|
+
export async function buildKnipConfig(targetDir) {
|
|
234
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
235
|
+
let multiEntry = false;
|
|
236
|
+
if (await fs.pathExists(pkgPath)) {
|
|
237
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
238
|
+
if (pkg.exports && typeof pkg.exports === 'object') {
|
|
239
|
+
const subpaths = Object.keys(pkg.exports).filter((k) => k.startsWith('.') && k !== './package.json');
|
|
240
|
+
multiEntry = subpaths.length > 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const $schema = 'https://unpkg.com/knip@6/schema.json';
|
|
244
|
+
const entry = multiEntry ? ['src/**/*.{ts,tsx}'] : ['src/index.{ts,tsx}'];
|
|
245
|
+
const project = ['src/**/*.{ts,tsx}'];
|
|
246
|
+
// In a pnpm workspace, the flat entry/project form makes `knip` mis-analyze
|
|
247
|
+
// the sub-packages (it treats the whole repo as one package). Emit the
|
|
248
|
+
// `workspaces` form instead: the root keeps the build-model globs, and each
|
|
249
|
+
// declared package glob gets `{}` so knip auto-detects its entries.
|
|
250
|
+
const wsPath = path.join(targetDir, 'pnpm-workspace.yaml');
|
|
251
|
+
if (await fs.pathExists(wsPath)) {
|
|
252
|
+
const workspaces = { '.': { entry, project } };
|
|
253
|
+
for (const glob of parseWorkspacePackages(await fs.readFile(wsPath, 'utf-8'))) {
|
|
254
|
+
workspaces[glob] = {};
|
|
255
|
+
}
|
|
256
|
+
return { $schema, workspaces };
|
|
257
|
+
}
|
|
258
|
+
return { $schema, entry, project };
|
|
259
|
+
}
|
|
260
|
+
export async function generateKnipConfig(targetDir) {
|
|
261
|
+
const config = await buildKnipConfig(targetDir);
|
|
262
|
+
await fs.writeJson(path.join(targetDir, 'knip.json'), config, { spaces: 2 });
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Scaffold a size-limit budget. Multi-subpath libraries (≥1 subpath export
|
|
266
|
+
* beyond the root barrel) get an exports-driven `.size-limit.cjs` that emits one
|
|
267
|
+
* budget per subpath from package.json at run time — so new modules are covered
|
|
268
|
+
* automatically. Everything else gets a static `.size-limit.json` for the root
|
|
269
|
+
* bundle. Returns the written filename.
|
|
270
|
+
*/
|
|
271
|
+
export async function generateSizeLimitConfig(targetDir) {
|
|
272
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
273
|
+
let subpathCount = 0;
|
|
274
|
+
if (await fs.pathExists(pkgPath)) {
|
|
275
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
276
|
+
if (pkg.exports && typeof pkg.exports === 'object') {
|
|
277
|
+
subpathCount = Object.keys(pkg.exports).filter((k) => k.startsWith('./') && k !== './package.json').length;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (subpathCount >= 1) {
|
|
281
|
+
await fs.writeFile(path.join(targetDir, '.size-limit.cjs'), SIZE_LIMIT_CJS);
|
|
282
|
+
return '.size-limit.cjs';
|
|
283
|
+
}
|
|
284
|
+
await fs.writeJson(path.join(targetDir, '.size-limit.json'), SIZE_LIMIT_CONFIG, { spaces: 2 });
|
|
285
|
+
return '.size-limit.json';
|
|
286
|
+
}
|
|
287
|
+
export async function generateCodeowners(targetDir) {
|
|
288
|
+
const filepath = path.join(targetDir, '.github', 'CODEOWNERS');
|
|
289
|
+
await fs.ensureDir(path.dirname(filepath));
|
|
290
|
+
await fs.writeFile(filepath, CODEOWNERS_CONTENT);
|
|
291
|
+
return '.github/CODEOWNERS';
|
|
292
|
+
}
|
|
293
|
+
export async function generateMiscBaseline(targetDir) {
|
|
294
|
+
await generateEditorConfig(targetDir);
|
|
295
|
+
await generateNvmrc(targetDir);
|
|
296
|
+
await ensureEnginesNode(targetDir);
|
|
297
|
+
await generateKnipConfig(targetDir);
|
|
298
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
/**
|
|
4
|
+
* Scaffold a starter nx.json for a pnpm-workspace monorepo (the Turborepo
|
|
5
|
+
* alternative). Copies the shipped preset — a minimal `targetDefaults` pipeline;
|
|
6
|
+
* plugin generators are deferred. Never clobbers a hand-tuned nx.json (there is
|
|
7
|
+
* no doctor "ok" short-circuit for the targeted `fix nx` path). Returns the
|
|
8
|
+
* relative path written, or [] when nx.json already exists.
|
|
9
|
+
*/
|
|
10
|
+
export async function generateNx(targetDir) {
|
|
11
|
+
if (await fs.pathExists(path.join(targetDir, 'nx.json')))
|
|
12
|
+
return [];
|
|
13
|
+
const { copyPreset } = await import('../utils/copy-preset.js');
|
|
14
|
+
const result = await copyPreset('nx', targetDir);
|
|
15
|
+
return [result.target];
|
|
16
|
+
}
|