@rtorcato/repo-tooling 2.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AGENTS.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +201 -0
  4. package/dist/cli/commands/doctor.js +336 -0
  5. package/dist/cli/commands/fix-targets.js +192 -0
  6. package/dist/cli/commands/fix.js +440 -0
  7. package/dist/cli/commands/setup-presets.js +281 -0
  8. package/dist/cli/commands/setup.js +501 -0
  9. package/dist/cli/generators/agent-rules.js +103 -0
  10. package/dist/cli/generators/badges.js +88 -0
  11. package/dist/cli/generators/build.js +216 -0
  12. package/dist/cli/generators/bun.js +25 -0
  13. package/dist/cli/generators/community-health.js +145 -0
  14. package/dist/cli/generators/docs-site.js +436 -0
  15. package/dist/cli/generators/git.js +164 -0
  16. package/dist/cli/generators/github-actions.js +35 -0
  17. package/dist/cli/generators/github-workflows.js +24 -0
  18. package/dist/cli/generators/gitlab-ci.js +10 -0
  19. package/dist/cli/generators/index.js +123 -0
  20. package/dist/cli/generators/linting.js +54 -0
  21. package/dist/cli/generators/misc.js +298 -0
  22. package/dist/cli/generators/nx.js +16 -0
  23. package/dist/cli/generators/package-json.js +317 -0
  24. package/dist/cli/generators/pnpm-workspace.js +126 -0
  25. package/dist/cli/generators/postcss.js +24 -0
  26. package/dist/cli/generators/readme.js +268 -0
  27. package/dist/cli/generators/security.js +156 -0
  28. package/dist/cli/generators/skills-install.js +70 -0
  29. package/dist/cli/generators/tailwind.js +34 -0
  30. package/dist/cli/generators/testing.js +88 -0
  31. package/dist/cli/generators/treeshake.js +148 -0
  32. package/dist/cli/generators/tsconfig.js +27 -0
  33. package/dist/cli/generators/turborepo.js +35 -0
  34. package/dist/cli/generators/typedoc.js +40 -0
  35. package/dist/cli/index.js +332 -0
  36. package/dist/cli/utils/copy-preset.js +98 -0
  37. package/dist/cli/utils/detect-language.js +22 -0
  38. package/dist/cli/utils/format.js +32 -0
  39. package/dist/cli/utils/install.js +28 -0
  40. package/dist/cli/utils/lockfile.js +84 -0
  41. package/dist/languages/js/checks.js +949 -0
  42. package/dist/languages/js/ci.js +251 -0
  43. package/dist/languages/js/fixers.js +735 -0
  44. package/dist/languages/registry.js +37 -0
  45. package/dist/languages/swift/checks.js +127 -0
  46. package/dist/languages/swift/ci.js +165 -0
  47. package/dist/languages/swift/fixers.js +102 -0
  48. package/dist/languages/swift/git-hooks.js +63 -0
  49. package/dist/languages/swift/gitignore.js +43 -0
  50. package/dist/languages/swift/scaffold.js +244 -0
  51. package/package.json +461 -0
  52. package/tooling/biome/README.md +90 -0
  53. package/tooling/biome/biome.json +63 -0
  54. package/tooling/bun/bunfig.toml +14 -0
  55. package/tooling/changesets/README.md +35 -0
  56. package/tooling/changesets/config.json +11 -0
  57. package/tooling/claude/repo-tooling.md +87 -0
  58. package/tooling/commitlint/commitlint.d.mts +4 -0
  59. package/tooling/commitlint/commitlint.mjs +40 -0
  60. package/tooling/cypress/cypress.config.d.mts +4 -0
  61. package/tooling/cypress/cypress.config.mjs +11 -0
  62. package/tooling/docusaurus/index.d.mts +17 -0
  63. package/tooling/docusaurus/index.mjs +38 -0
  64. package/tooling/docusaurus/sync-changelog.mjs +42 -0
  65. package/tooling/docusaurus/theme-tokens.css +79 -0
  66. package/tooling/docusaurus/theme.css +378 -0
  67. package/tooling/esbuild/index.d.mts +6 -0
  68. package/tooling/esbuild/index.mjs +102 -0
  69. package/tooling/eslint/base.d.mts +6 -0
  70. package/tooling/eslint/base.mjs +122 -0
  71. package/tooling/eslint/nextjs.d.mts +4 -0
  72. package/tooling/eslint/nextjs.mjs +22 -0
  73. package/tooling/eslint/types.d.ts +58 -0
  74. package/tooling/github-actions/workflows/cloudflare-pages.yml +42 -0
  75. package/tooling/github-actions/workflows/docker-publish.yml +44 -0
  76. package/tooling/github-actions/workflows/preview-deployments.yml +54 -0
  77. package/tooling/github-actions/workflows/vercel-deploy.yml +40 -0
  78. package/tooling/jest-presets/browser/jest-preset.d.mts +4 -0
  79. package/tooling/jest-presets/browser/jest-preset.mjs +14 -0
  80. package/tooling/jest-presets/node/jest-preset.d.mts +4 -0
  81. package/tooling/jest-presets/node/jest-preset.mjs +13 -0
  82. package/tooling/mcp/mcp.json.example +27 -0
  83. package/tooling/nx/nx.json +24 -0
  84. package/tooling/oxlint/README.md +25 -0
  85. package/tooling/oxlint/oxlintrc.json +28 -0
  86. package/tooling/playwright/playwright.config.d.mts +4 -0
  87. package/tooling/playwright/playwright.config.mjs +19 -0
  88. package/tooling/prettier/index.d.mts +4 -0
  89. package/tooling/prettier/index.mjs +36 -0
  90. package/tooling/release-please/.release-please-manifest.json +3 -0
  91. package/tooling/release-please/release-please-config.json +9 -0
  92. package/tooling/rolldown/rolldown.config.d.mts +18 -0
  93. package/tooling/rolldown/rolldown.config.mjs +59 -0
  94. package/tooling/rollup/rollup.config.d.mts +20 -0
  95. package/tooling/rollup/rollup.config.mjs +70 -0
  96. package/tooling/semantic-release/docker.d.mts +4 -0
  97. package/tooling/semantic-release/docker.mjs +59 -0
  98. package/tooling/semantic-release/github.d.mts +4 -0
  99. package/tooling/semantic-release/github.mjs +79 -0
  100. package/tooling/semantic-release/index.d.mts +4 -0
  101. package/tooling/semantic-release/index.mjs +80 -0
  102. package/tooling/swift/periphery.yml +4 -0
  103. package/tooling/swift/swiftlint.yml +23 -0
  104. package/tooling/tests/exports-resolution.d.mts +14 -0
  105. package/tooling/tests/exports-resolution.mjs +68 -0
  106. package/tooling/tests/ssr-safety.d.mts +14 -0
  107. package/tooling/tests/ssr-safety.mjs +53 -0
  108. package/tooling/tsup/index.d.mts +8 -0
  109. package/tooling/tsup/index.mjs +33 -0
  110. package/tooling/typedoc/typedoc.json +7 -0
  111. package/tooling/typescript/README.md +49 -0
  112. package/tooling/typescript/reset.d.ts +9 -0
  113. package/tooling/typescript/tsconfig.base.json +84 -0
  114. package/tooling/typescript/tsconfig.build.json +11 -0
  115. package/tooling/typescript/tsconfig.bun.json +9 -0
  116. package/tooling/typescript/tsconfig.express.json +9 -0
  117. package/tooling/typescript/tsconfig.next.json +20 -0
  118. package/tooling/typescript/tsconfig.node.json +9 -0
  119. package/tooling/typescript/tsconfig.react.json +15 -0
  120. package/tooling/typescript/tsconfig.test.json +8 -0
  121. package/tooling/typescript/v1/tsconfig.base.json +81 -0
  122. package/tooling/typescript/v1/tsconfig.express.json +9 -0
  123. package/tooling/typescript/v1/tsconfig.next.json +19 -0
  124. package/tooling/typescript/v1/tsconfig.node.json +9 -0
  125. package/tooling/typescript/v1/tsconfig.react.json +14 -0
  126. package/tooling/typescript/v1/tsconfig.test.json +8 -0
  127. package/tooling/vite/vite.config.d.mts +4 -0
  128. package/tooling/vite/vite.config.mjs +18 -0
  129. package/tooling/vitest/jsdom-shims.d.mts +1 -0
  130. package/tooling/vitest/jsdom-shims.mjs +58 -0
  131. package/tooling/vitest/vitest.config.d.mts +4 -0
  132. package/tooling/vitest/vitest.config.mjs +25 -0
  133. package/tooling/vitest/vitest.config.react.d.mts +4 -0
  134. package/tooling/vitest/vitest.config.react.mjs +27 -0
  135. package/tooling/vitest/vitest.setup.d.mts +1 -0
  136. package/tooling/vitest/vitest.setup.mjs +3 -0
@@ -0,0 +1,317 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ // Exact pnpm version for the `packageManager` field — corepack requires a
4
+ // pinned `pnpm@x.y.z` (not a range), and it's the single source of truth for
5
+ // pnpm/action-setup, so the generated workflows drop their `version:` inputs.
6
+ const PNPM_VERSION = '11.1.3';
7
+ export async function generatePackageJson(config, targetDir) {
8
+ const packageJsonPath = path.join(targetDir, 'package.json');
9
+ let existingPackageJson = {};
10
+ if (await fs.pathExists(packageJsonPath)) {
11
+ existingPackageJson = await fs.readJson(packageJsonPath);
12
+ }
13
+ const includeTreeshake = Boolean(config.treeshakeCheck && config.projectType === 'library');
14
+ const packageJson = {
15
+ name: config.projectName,
16
+ version: '0.1.0',
17
+ description: '',
18
+ type: 'module',
19
+ packageManager: `pnpm@${PNPM_VERSION}`,
20
+ ...existingPackageJson,
21
+ scripts: {
22
+ ...getScripts(config, { includeTreeshake }),
23
+ ...existingPackageJson?.scripts,
24
+ },
25
+ dependencies: {
26
+ ...existingPackageJson?.dependencies,
27
+ },
28
+ devDependencies: {
29
+ '@rtorcato/repo-tooling': 'latest',
30
+ ...getDependencies(config),
31
+ ...existingPackageJson?.devDependencies,
32
+ },
33
+ };
34
+ // Add additional package.json fields based on project type.
35
+ // Exports must match tsup's output for a "type": "module" package with
36
+ // format: ['cjs','esm']: ESM → index.js, CJS → index.cjs, types →
37
+ // index.d.ts (ESM) / index.d.cts (CJS).
38
+ if (config.projectType === 'library') {
39
+ packageJson.main = './dist/index.cjs';
40
+ packageJson.module = './dist/index.js';
41
+ packageJson.types = './dist/index.d.ts';
42
+ packageJson.exports = {
43
+ '.': {
44
+ types: {
45
+ import: './dist/index.d.ts',
46
+ require: './dist/index.d.cts',
47
+ },
48
+ import: './dist/index.js',
49
+ require: './dist/index.cjs',
50
+ },
51
+ };
52
+ packageJson.files = ['dist'];
53
+ packageJson.publishConfig = {
54
+ access: 'public',
55
+ };
56
+ }
57
+ // Commitizen adapter path — makes `pnpm commit` launch the conventional
58
+ // changelog prompt. Merged so an existing `config` block is preserved.
59
+ if (config.commitLint) {
60
+ packageJson.config = {
61
+ ...(packageJson.config ?? {}),
62
+ commitizen: { path: './node_modules/cz-conventional-changelog' },
63
+ };
64
+ }
65
+ // Build-script approvals (esbuild etc.) are NOT written here: pnpm 11 ignores
66
+ // package.json's `pnpm` field and reads them from pnpm-workspace.yaml instead
67
+ // (see ensureBuildApprovals in build.ts).
68
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
69
+ }
70
+ function getScripts(config, opts = {}) {
71
+ const scripts = {};
72
+ // TypeScript scripts
73
+ if (config.typescript.enabled) {
74
+ scripts['typecheck'] = 'tsc --noEmit';
75
+ }
76
+ // Linting scripts
77
+ if (config.linting.tool === 'biome' || config.linting.tool === 'both') {
78
+ scripts['lint'] = 'biome lint .';
79
+ scripts['format'] = 'biome format .';
80
+ scripts['check'] = 'biome check .';
81
+ scripts['check:fix'] = 'biome check --fix .';
82
+ }
83
+ else if (config.linting.tool === 'eslint') {
84
+ scripts['lint'] = 'eslint .';
85
+ scripts['lint:fix'] = 'eslint . --fix';
86
+ scripts['format'] = 'prettier --write .';
87
+ }
88
+ // Testing scripts
89
+ if (config.testing.framework === 'vitest') {
90
+ scripts['test'] = 'vitest';
91
+ scripts['test:watch'] = 'vitest --watch';
92
+ scripts['test:ui'] = 'vitest --ui';
93
+ scripts['coverage'] = 'vitest run --coverage';
94
+ }
95
+ else if (config.testing.framework === 'jest') {
96
+ scripts['test'] = 'jest';
97
+ scripts['test:watch'] = 'jest --watch';
98
+ scripts['coverage'] = 'jest --coverage';
99
+ }
100
+ else if (config.testing.framework === 'playwright') {
101
+ scripts['test:e2e'] = 'playwright test';
102
+ scripts['test:e2e:ui'] = 'playwright test --ui';
103
+ }
104
+ else if (config.testing.framework === 'cypress') {
105
+ scripts['test:e2e'] = 'cypress run';
106
+ scripts['test:e2e:ui'] = 'cypress open';
107
+ }
108
+ // Build scripts
109
+ if (config.bundler === 'tsup') {
110
+ scripts['build'] = 'tsup';
111
+ scripts['build:watch'] = 'tsup --watch';
112
+ }
113
+ else if (config.bundler === 'esbuild') {
114
+ scripts['build'] = 'node build.mjs';
115
+ }
116
+ else if (config.bundler === 'rollup') {
117
+ scripts['build'] = 'rollup -c';
118
+ scripts['build:watch'] = 'rollup -c --watch';
119
+ }
120
+ else if (config.bundler === 'rolldown') {
121
+ scripts['build'] = 'rolldown -c';
122
+ scripts['build:watch'] = 'rolldown -c --watch';
123
+ }
124
+ else if (config.bundler === 'vite') {
125
+ scripts['build'] = 'vite build';
126
+ scripts['dev'] = 'vite';
127
+ scripts['preview'] = 'vite preview';
128
+ }
129
+ // Commitizen assistant — `pnpm commit` guides a conventional commit that
130
+ // then passes the commitlint enforcement layer.
131
+ if (config.commitLint) {
132
+ scripts['commit'] = 'cz';
133
+ }
134
+ // knip is part of the universal baseline (knip.json is always generated).
135
+ scripts['knip'] = 'knip';
136
+ // publint validates the published package (exports, types, main) against dist.
137
+ if (config.publint) {
138
+ scripts['publint'] = 'publint --strict';
139
+ }
140
+ // are-the-types-wrong validates that a consumer's `import` actually resolves
141
+ // the shipped `.d.ts`. Only meaningful for publishable libraries. The default
142
+ // profile fits the dual (cjs+esm) exports the library scaffold emits.
143
+ if (config.projectType === 'library') {
144
+ scripts['attw'] = 'attw --pack';
145
+ }
146
+ // Git hooks
147
+ if (config.gitHooks) {
148
+ scripts['prepare'] = 'husky';
149
+ }
150
+ // Semantic release
151
+ if (config.semanticRelease) {
152
+ scripts['release'] = 'semantic-release';
153
+ }
154
+ if (opts.includeTreeshake) {
155
+ scripts['pretreeshake'] = scripts['build'] ? 'pnpm build' : 'echo "no build step"';
156
+ scripts['treeshake'] = 'pnpm --filter=*treeshake-check run check';
157
+ }
158
+ const verify = composeVerifyScript(config, opts);
159
+ if (verify) {
160
+ scripts['verify'] = verify;
161
+ }
162
+ return scripts;
163
+ }
164
+ export function composeVerifyScript(config, opts = {}) {
165
+ const cmds = [];
166
+ if (config.typescript.enabled)
167
+ cmds.push('pnpm typecheck');
168
+ if (config.linting.tool === 'biome' || config.linting.tool === 'both') {
169
+ cmds.push('pnpm check');
170
+ }
171
+ else if (config.linting.tool === 'eslint') {
172
+ cmds.push('pnpm lint');
173
+ }
174
+ if (config.testing.framework === 'vitest') {
175
+ cmds.push('pnpm exec vitest run');
176
+ }
177
+ else if (config.testing.framework === 'jest') {
178
+ cmds.push('pnpm test --ci');
179
+ }
180
+ else if (config.testing.framework === 'playwright' || config.testing.framework === 'cypress') {
181
+ cmds.push('pnpm test:e2e');
182
+ }
183
+ if (opts.includeTreeshake)
184
+ cmds.push('pnpm treeshake');
185
+ if (config.publint)
186
+ cmds.push('pnpm publint');
187
+ // attw is a publish-safety rider, not a core check — it never justifies a
188
+ // verify chain on its own, so append it only once we've decided to emit one.
189
+ if (cmds.length < 2)
190
+ return null;
191
+ if (config.projectType === 'library')
192
+ cmds.push('pnpm attw');
193
+ return cmds.join(' && ');
194
+ }
195
+ /**
196
+ * Derive the verify chain from a real package.json's scripts + deps.
197
+ * Used by `fix verify`, where we shouldn't assume tools beyond what the
198
+ * project actually has.
199
+ */
200
+ export function composeVerifyScriptFromPkg(pkg, opts = {}) {
201
+ const scripts = pkg.scripts ?? {};
202
+ const deps = {
203
+ ...(pkg.dependencies ?? {}),
204
+ ...(pkg.devDependencies ?? {}),
205
+ };
206
+ const cmds = [];
207
+ if (scripts.typecheck || deps.typescript)
208
+ cmds.push('pnpm typecheck');
209
+ if (scripts.check)
210
+ cmds.push('pnpm check');
211
+ else if (scripts.lint && !scripts.check)
212
+ cmds.push('pnpm lint');
213
+ if (deps.vitest)
214
+ cmds.push('pnpm exec vitest run');
215
+ else if (deps.jest)
216
+ cmds.push('pnpm test --ci');
217
+ else if (deps['@playwright/test'] || deps.cypress)
218
+ cmds.push('pnpm test:e2e');
219
+ if (opts.includeTreeshake)
220
+ cmds.push('pnpm treeshake');
221
+ if (deps.publint || scripts.publint)
222
+ cmds.push('pnpm publint');
223
+ return cmds.length >= 2 ? cmds.join(' && ') : null;
224
+ }
225
+ function getDependencies(config) {
226
+ // knip is part of the universal baseline (knip.json + `knip` script).
227
+ const deps = { knip: '^6.0.0' };
228
+ // TypeScript
229
+ if (config.typescript.enabled) {
230
+ deps['typescript'] = '^5.9.3';
231
+ deps['@types/node'] = '^26.0.1';
232
+ }
233
+ // Linting tools
234
+ if (config.linting.tool === 'biome' || config.linting.tool === 'both') {
235
+ deps['@biomejs/biome'] = '^2.5.1';
236
+ }
237
+ if (config.linting.tool === 'eslint' || config.linting.tool === 'both') {
238
+ deps['eslint'] = '^9.0.0';
239
+ deps['prettier'] = '^3.0.0';
240
+ }
241
+ // Testing frameworks
242
+ if (config.testing.framework === 'vitest') {
243
+ deps['vitest'] = '^4.1.9';
244
+ // Coverage provider so `pnpm coverage` (and the CI Codecov upload) works.
245
+ deps['@vitest/coverage-v8'] = '^4.1.9';
246
+ if (config.testing.environment === 'browser' || config.testing.environment === 'both') {
247
+ deps['@vitest/ui'] = '^4.1.9';
248
+ deps['jsdom'] = '^25.0.0';
249
+ }
250
+ }
251
+ else if (config.testing.framework === 'jest') {
252
+ deps['jest'] = '^29.0.0';
253
+ if (config.typescript.enabled) {
254
+ deps['ts-jest'] = '^29.0.0';
255
+ }
256
+ }
257
+ else if (config.testing.framework === 'playwright') {
258
+ deps['@playwright/test'] = '^1.60.0';
259
+ }
260
+ else if (config.testing.framework === 'cypress') {
261
+ deps['cypress'] = '^13.0.0';
262
+ }
263
+ // Build tools
264
+ if (config.bundler === 'tsup') {
265
+ deps['tsup'] = '^8.0.0';
266
+ }
267
+ else if (config.bundler === 'esbuild') {
268
+ deps['esbuild'] = '^0.25.0';
269
+ }
270
+ else if (config.bundler === 'rollup') {
271
+ deps['rollup'] = '^4.0.0';
272
+ deps['@rollup/plugin-typescript'] = '^12.0.0';
273
+ deps['tslib'] = '^2.0.0';
274
+ }
275
+ else if (config.bundler === 'rolldown') {
276
+ // Rolldown transpiles TypeScript natively — no plugin-typescript/tslib.
277
+ deps['rolldown'] = '^1.0.0';
278
+ }
279
+ else if (config.bundler === 'vite') {
280
+ deps['vite'] = '^6.0.0';
281
+ }
282
+ // Tailwind CSS v4 — CSS-first, wired via the PostCSS plugin.
283
+ if (config.tailwind) {
284
+ deps['tailwindcss'] = '^4.0.0';
285
+ deps['@tailwindcss/postcss'] = '^4.0.0';
286
+ }
287
+ // Publishing hygiene
288
+ if (config.publint) {
289
+ deps['publint'] = '^0.3.0';
290
+ }
291
+ // Git hooks
292
+ if (config.gitHooks) {
293
+ deps['husky'] = '^9.0.0';
294
+ deps['lint-staged'] = '^16.0.0';
295
+ }
296
+ // are-the-types-wrong — validate published type resolution for libraries.
297
+ if (config.projectType === 'library') {
298
+ deps['@arethetypeswrong/cli'] = '^0.18.2';
299
+ }
300
+ // Commit linting (enforcement) + commitizen (assistance)
301
+ if (config.commitLint) {
302
+ deps['@commitlint/cli'] = '^20.0.0';
303
+ deps['@commitlint/config-conventional'] = '^20.0.0';
304
+ deps['commitizen'] = '^4.3.1';
305
+ deps['cz-conventional-changelog'] = '^3.3.0';
306
+ }
307
+ // Semantic release. The shipped github preset activates the changelog and
308
+ // git plugins (and @semantic-release/github), so they must be installed too
309
+ // or `semantic-release` crashes with "Cannot find module".
310
+ if (config.semanticRelease) {
311
+ deps['semantic-release'] = '^25.0.0';
312
+ deps['@semantic-release/github'] = '^12.0.0';
313
+ deps['@semantic-release/changelog'] = '^6.0.0';
314
+ deps['@semantic-release/git'] = '^10.0.0';
315
+ }
316
+ return deps;
317
+ }
@@ -0,0 +1,126 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ /**
4
+ * Since pnpm 10, repo-wide settings live in `pnpm-workspace.yaml` rather than
5
+ * `.npmrc` — for single-package repos too, not just workspaces. These are the
6
+ * family-wide settings that otherwise get hand-copied and drift (#314).
7
+ *
8
+ * Every write here is a merge: `pnpm-workspace.yaml` also carries the repo's own
9
+ * `packages:` globs and hand-vetted `allowBuilds` entries, so nothing is
10
+ * rewritten and no existing value is overridden.
11
+ */
12
+ export const WORKSPACE_FILE = 'pnpm-workspace.yaml';
13
+ /**
14
+ * pnpm's `minimumReleaseAge` cutoff holds back freshly published versions —
15
+ * good against a typosquat-hijack, but it also stalls every consumer of a
16
+ * same-day `@rtorcato/*` fix for 24h. One glob covers the whole family: pnpm
17
+ * matches these entries with `@pnpm/config.matcher`, so there's no package list
18
+ * to keep in sync with `@rtorcato/shared-docs`'s `FAMILY`.
19
+ */
20
+ const FAMILY_GLOB = '@rtorcato/*';
21
+ /** Bundlers that pull in esbuild, whose install script pnpm 11 refuses to run unapproved. */
22
+ const ESBUILD_BUNDLERS = ['esbuild', 'tsup', 'vite'];
23
+ /** True when the repo depends on a bundler that drags esbuild in. */
24
+ export function dependsOnEsbuild(deps) {
25
+ return ESBUILD_BUNDLERS.some((name) => name in deps);
26
+ }
27
+ /**
28
+ * The lines of a top-level YAML block, e.g. everything indented under
29
+ * `allowBuilds:`. Returns null when the key isn't present at all. Hand-rolled
30
+ * because adding a YAML parser to a 5-dependency CLI to read three keys isn't
31
+ * worth it — and the same shape is already parsed this way in `misc.ts`.
32
+ */
33
+ function section(yaml, key) {
34
+ const lines = yaml.split('\n');
35
+ const start = lines.findIndex((line) => line.startsWith(`${key}:`));
36
+ if (start === -1)
37
+ return null;
38
+ const body = [];
39
+ for (const line of lines.slice(start + 1)) {
40
+ if (/^\S/.test(line))
41
+ break; // a new top-level key ends the block
42
+ body.push(line);
43
+ }
44
+ return body;
45
+ }
46
+ const SETTINGS = [
47
+ {
48
+ label: 'verifyDepsBeforeRun: false',
49
+ applies: () => true,
50
+ // Any explicit value counts — a repo that deliberately opted into
51
+ // verification shouldn't be nagged back to the family default.
52
+ satisfied: (yaml) => /^verifyDepsBeforeRun:/m.test(yaml),
53
+ key: 'verifyDepsBeforeRun',
54
+ block: `# Don't re-verify node_modules on every script run — the check costs a
55
+ # second per invocation and CI installs with --frozen-lockfile anyway.
56
+ verifyDepsBeforeRun: false
57
+ `,
58
+ item: '',
59
+ },
60
+ {
61
+ label: `minimumReleaseAgeExclude: ${FAMILY_GLOB}`,
62
+ applies: () => true,
63
+ satisfied: (yaml) => (section(yaml, 'minimumReleaseAgeExclude') ?? []).some(hasFamilyGlob),
64
+ key: 'minimumReleaseAgeExclude',
65
+ block: `# Exempt the @rtorcato family from pnpm's minimumReleaseAge cutoff, so a
66
+ # same-day fix in a sibling package is installable today rather than tomorrow.
67
+ minimumReleaseAgeExclude:
68
+ - '${FAMILY_GLOB}'
69
+ `,
70
+ item: ` - '${FAMILY_GLOB}'`,
71
+ },
72
+ {
73
+ label: 'allowBuilds: esbuild',
74
+ applies: (needsEsbuild) => needsEsbuild,
75
+ satisfied: (yaml) => (section(yaml, 'allowBuilds') ?? []).some((l) => /^\s*esbuild:/.test(l)),
76
+ key: 'allowBuilds',
77
+ block: `# pnpm 11 reads build-script approvals from this map, not the older
78
+ # onlyBuiltDependencies list, and fails the install outright without them.
79
+ allowBuilds:
80
+ esbuild: true
81
+ `,
82
+ item: ' esbuild: true',
83
+ },
84
+ ];
85
+ function hasFamilyGlob(line) {
86
+ return line.includes(FAMILY_GLOB);
87
+ }
88
+ /** Managed settings absent from `yaml`, named as doctor reports them. */
89
+ export function missingPnpmSettings(yaml, needsEsbuild) {
90
+ return SETTINGS.filter((s) => s.applies(needsEsbuild) && !s.satisfied(yaml)).map((s) => s.label);
91
+ }
92
+ /** Insert `item` directly under an existing `key:` line, keeping the rest untouched. */
93
+ function insertUnder(yaml, key, item) {
94
+ const lines = yaml.split('\n');
95
+ const at = lines.findIndex((line) => line.startsWith(`${key}:`));
96
+ lines.splice(at + 1, 0, item);
97
+ return lines.join('\n');
98
+ }
99
+ /** Merge every missing managed setting into `yaml` and return the new contents. */
100
+ export function upsertPnpmSettings(yaml, needsEsbuild) {
101
+ let next = yaml;
102
+ for (const setting of SETTINGS) {
103
+ if (!setting.applies(needsEsbuild) || setting.satisfied(next))
104
+ continue;
105
+ if (setting.item && section(next, setting.key)) {
106
+ next = insertUnder(next, setting.key, setting.item);
107
+ }
108
+ else {
109
+ next = `${next.replace(/\n*$/, '\n')}\n${setting.block}`;
110
+ }
111
+ }
112
+ return next;
113
+ }
114
+ /**
115
+ * Merge the managed pnpm settings into `pnpm-workspace.yaml`, creating it when
116
+ * absent. Returns the relative path if anything changed, else null.
117
+ */
118
+ export async function ensurePnpmSettings(targetDir, needsEsbuild) {
119
+ const file = path.join(targetDir, WORKSPACE_FILE);
120
+ const current = (await fs.pathExists(file)) ? await fs.readFile(file, 'utf-8') : '';
121
+ const next = upsertPnpmSettings(current, needsEsbuild);
122
+ if (next === current)
123
+ return null;
124
+ await fs.writeFile(file, next.replace(/^\n+/, ''));
125
+ return WORKSPACE_FILE;
126
+ }
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ /**
4
+ * Scaffold a minimal PostCSS config for non-Tailwind CSS pipelines (autoprefixer
5
+ * over your browserslist). Tailwind v4 users get their PostCSS wiring from the
6
+ * Tailwind preset instead (`@tailwindcss/postcss`, which autoprefixes on its
7
+ * own), so this stays out of their way — safe-add never clobbers an existing
8
+ * postcss.config.mjs.
9
+ */
10
+ export async function generatePostcss(targetDir) {
11
+ const dest = path.join(targetDir, 'postcss.config.mjs');
12
+ if (await fs.pathExists(dest))
13
+ return [];
14
+ await fs.writeFile(dest, POSTCSS_CONFIG);
15
+ return ['postcss.config.mjs'];
16
+ }
17
+ const POSTCSS_CONFIG = `export default {
18
+ plugins: {
19
+ // Adds vendor prefixes based on your browserslist (package.json or
20
+ // .browserslistrc). Add more PostCSS plugins here as needed.
21
+ autoprefixer: {},
22
+ },
23
+ }
24
+ `;