@rtorcato/repo-tooling 3.2.0 → 3.2.2
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.
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
+
import selfPackageJson from '../../../package.json' with { type: 'json' };
|
|
3
4
|
import { copyPreset, PRESETS } from '../utils/copy-preset.js';
|
|
4
5
|
import { buildBadgeRow, parseRepository } from './badges.js';
|
|
5
6
|
import { inferSubpathsFromExports } from './treeshake.js';
|
|
7
|
+
/**
|
|
8
|
+
* What a scaffolded docs site should depend on for *this* CLI — read from the
|
|
9
|
+
* running version rather than written down, because a literal here goes stale
|
|
10
|
+
* silently. It had drifted to `^2.47.0`, two majors behind, so every site
|
|
11
|
+
* scaffolded since was handed a pre-rename version predating the peer-dependency
|
|
12
|
+
* split.
|
|
13
|
+
*
|
|
14
|
+
* The floor is the exact running version, not `^<major>.0.0`: the config being
|
|
15
|
+
* generated is the one this version emits, and claiming compatibility back to
|
|
16
|
+
* the start of the major would be the same over-wide-range problem doctor's
|
|
17
|
+
* `Config schema versions` check exists to catch (#330).
|
|
18
|
+
*/
|
|
19
|
+
const SELF_RANGE = `^${selfPackageJson.version}`;
|
|
6
20
|
/**
|
|
7
21
|
* Docs-site (Docusaurus) generator — the Phase 2 counterpart to the shared
|
|
8
22
|
* assets shipped in #54. Scaffolds a working Docusaurus site under `apps/docs`,
|
|
@@ -179,8 +193,12 @@ ${typedocPlugins} [
|
|
|
179
193
|
],
|
|
180
194
|
copyright: \`Copyright © \${new Date().getFullYear()} ${meta.title}. Built with Docusaurus.\`,
|
|
181
195
|
},
|
|
196
|
+
// \`theme\` is the LIGHT-mode Prism theme and \`darkTheme\` the dark one. Both
|
|
197
|
+
// were vsDark here, which is why the shared stylesheet had to pin fenced
|
|
198
|
+
// blocks dark in light mode too (#324). Keep this pairing and the CSS in
|
|
199
|
+
// step — vsDark tokens on a light surface are unreadable.
|
|
182
200
|
prism: {
|
|
183
|
-
theme: prismThemes.
|
|
201
|
+
theme: prismThemes.vsLight,
|
|
184
202
|
darkTheme: prismThemes.vsDark,
|
|
185
203
|
additionalLanguages: ['bash', 'json', 'typescript'],
|
|
186
204
|
},
|
|
@@ -222,7 +240,7 @@ const TSCONFIG = `// Improves IDE type-checking; not used by \`docusaurus start/
|
|
|
222
240
|
`;
|
|
223
241
|
function customCss(accent) {
|
|
224
242
|
// Import the shared tokens, then override only the accent (per #54's model).
|
|
225
|
-
return `/* Site theme: shared
|
|
243
|
+
return `/* Site theme: the shared design tokens + this project's accent. */
|
|
226
244
|
@import "./_jt-tokens.css";
|
|
227
245
|
|
|
228
246
|
:root {
|
|
@@ -239,7 +257,7 @@ function customCss(accent) {
|
|
|
239
257
|
function docsPackageJson(meta, typedoc) {
|
|
240
258
|
const typedocDevDeps = typedoc
|
|
241
259
|
? {
|
|
242
|
-
'@rtorcato/repo-tooling':
|
|
260
|
+
'@rtorcato/repo-tooling': SELF_RANGE,
|
|
243
261
|
'docusaurus-plugin-typedoc': '^1.4.0',
|
|
244
262
|
typedoc: '^0.28.0',
|
|
245
263
|
'typedoc-plugin-markdown': '^4.9.0',
|
|
@@ -278,7 +296,7 @@ function docsPackageJson(meta, typedoc) {
|
|
|
278
296
|
'@docusaurus/tsconfig': '^3.8.1',
|
|
279
297
|
'@docusaurus/types': '^3.10.2',
|
|
280
298
|
'@playwright/test': '^1.49.0',
|
|
281
|
-
'@rtorcato/repo-tooling':
|
|
299
|
+
'@rtorcato/repo-tooling': SELF_RANGE,
|
|
282
300
|
'@types/react': '^19.0.0',
|
|
283
301
|
typescript: '~5.6.3',
|
|
284
302
|
...typedocDevDeps,
|
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { generateSwiftProject } from '../../languages/swift/scaffold.js';
|
|
5
5
|
import { installAiSetup } from './agent-rules.js';
|
|
6
6
|
import { bundlerNeedsEsbuild, ensureBuildApprovals, generateBuildConfigs } from './build.js';
|
|
7
|
-
import { ensurePnpmSettings } from './pnpm-workspace.js';
|
|
7
|
+
import { ensurePnpmSettings, familyGlob } from './pnpm-workspace.js';
|
|
8
8
|
import { generateGitConfigs } from './git.js';
|
|
9
9
|
import { generateGitHubActions } from './github-actions.js';
|
|
10
10
|
import { generateLintingConfigs } from './linting.js';
|
|
@@ -84,7 +84,7 @@ export async function generateConfigs(config, targetDir) {
|
|
|
84
84
|
await ensureBuildApprovals(config, targetDir);
|
|
85
85
|
// Family-wide pnpm settings (#314). Runs last of the workspace writers so it
|
|
86
86
|
// merges into whatever they wrote rather than racing them for the file.
|
|
87
|
-
await ensurePnpmSettings(targetDir, bundlerNeedsEsbuild(config));
|
|
87
|
+
await ensurePnpmSettings(targetDir, bundlerNeedsEsbuild(config), familyGlob(config.projectName));
|
|
88
88
|
// Turborepo task pipeline (pnpm-workspace monorepos, when opted-in)
|
|
89
89
|
if (config.turborepo) {
|
|
90
90
|
await generateTurborepo(targetDir);
|
|
@@ -12,12 +12,24 @@ import fs from 'fs-extra';
|
|
|
12
12
|
export const WORKSPACE_FILE = 'pnpm-workspace.yaml';
|
|
13
13
|
/**
|
|
14
14
|
* pnpm's `minimumReleaseAge` cutoff holds back freshly published versions —
|
|
15
|
-
* good against a typosquat
|
|
16
|
-
* same-day
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* good against a typosquat or a hijacked account, but it also stalls every
|
|
16
|
+
* consumer of a same-day fix in a sibling package for 24h. Exempting the
|
|
17
|
+
* repo's *own* scope trades that off only for packages it already publishes.
|
|
18
|
+
*
|
|
19
|
+
* Derived from the consuming package's name rather than hardcoded: this is a
|
|
20
|
+
* public CLI, and writing one organisation's scope into a stranger's config
|
|
21
|
+
* would loosen a supply-chain guard for packages they neither use nor chose.
|
|
22
|
+
* An unscoped package gets no such setting at all — there is no "family" to
|
|
23
|
+
* infer, and guessing one would be worse than leaving it alone.
|
|
24
|
+
*
|
|
25
|
+
* One glob covers a whole scope: pnpm matches these entries with
|
|
26
|
+
* `@pnpm/config.matcher`, so there's no package list to keep in sync.
|
|
19
27
|
*/
|
|
20
|
-
|
|
28
|
+
export function familyGlob(packageName) {
|
|
29
|
+
const name = typeof packageName === 'string' ? packageName : '';
|
|
30
|
+
const scope = /^(@[^/]+)\//.exec(name)?.[1];
|
|
31
|
+
return scope ? `${scope}/*` : null;
|
|
32
|
+
}
|
|
21
33
|
/** Bundlers that pull in esbuild, whose install script pnpm 11 refuses to run unapproved. */
|
|
22
34
|
const ESBUILD_BUNDLERS = ['esbuild', 'tsup', 'vite'];
|
|
23
35
|
/** True when the repo depends on a bundler that drags esbuild in. */
|
|
@@ -43,7 +55,29 @@ function section(yaml, key) {
|
|
|
43
55
|
}
|
|
44
56
|
return body;
|
|
45
57
|
}
|
|
46
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The managed settings for one repo. A function rather than a constant because
|
|
60
|
+
* the release-age exemption is scope-derived, and a repo with no scope to
|
|
61
|
+
* derive doesn't get that setting at all.
|
|
62
|
+
*/
|
|
63
|
+
function settingsFor(glob) {
|
|
64
|
+
return glob ? [...BASE_SETTINGS, releaseAgeSetting(glob)] : BASE_SETTINGS;
|
|
65
|
+
}
|
|
66
|
+
function releaseAgeSetting(glob) {
|
|
67
|
+
return {
|
|
68
|
+
label: `minimumReleaseAgeExclude: ${glob}`,
|
|
69
|
+
applies: () => true,
|
|
70
|
+
satisfied: (yaml) => (section(yaml, 'minimumReleaseAgeExclude') ?? []).some((l) => l.includes(glob)),
|
|
71
|
+
key: 'minimumReleaseAgeExclude',
|
|
72
|
+
block: `# Exempt this package's own scope from pnpm's minimumReleaseAge cutoff, so a
|
|
73
|
+
# same-day fix in a sibling package is installable today rather than tomorrow.
|
|
74
|
+
minimumReleaseAgeExclude:
|
|
75
|
+
- '${glob}'
|
|
76
|
+
`,
|
|
77
|
+
item: ` - '${glob}'`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const BASE_SETTINGS = [
|
|
47
81
|
{
|
|
48
82
|
label: 'verifyDepsBeforeRun: false',
|
|
49
83
|
applies: () => true,
|
|
@@ -57,18 +91,6 @@ verifyDepsBeforeRun: false
|
|
|
57
91
|
`,
|
|
58
92
|
item: '',
|
|
59
93
|
},
|
|
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
94
|
{
|
|
73
95
|
label: 'allowBuilds: esbuild',
|
|
74
96
|
applies: (needsEsbuild) => needsEsbuild,
|
|
@@ -82,12 +104,11 @@ allowBuilds:
|
|
|
82
104
|
item: ' esbuild: true',
|
|
83
105
|
},
|
|
84
106
|
];
|
|
85
|
-
function hasFamilyGlob(line) {
|
|
86
|
-
return line.includes(FAMILY_GLOB);
|
|
87
|
-
}
|
|
88
107
|
/** Managed settings absent from `yaml`, named as doctor reports them. */
|
|
89
|
-
export function missingPnpmSettings(yaml, needsEsbuild) {
|
|
90
|
-
return
|
|
108
|
+
export function missingPnpmSettings(yaml, needsEsbuild, glob) {
|
|
109
|
+
return settingsFor(glob)
|
|
110
|
+
.filter((s) => s.applies(needsEsbuild) && !s.satisfied(yaml))
|
|
111
|
+
.map((s) => s.label);
|
|
91
112
|
}
|
|
92
113
|
/** Insert `item` directly under an existing `key:` line, keeping the rest untouched. */
|
|
93
114
|
function insertUnder(yaml, key, item) {
|
|
@@ -97,9 +118,9 @@ function insertUnder(yaml, key, item) {
|
|
|
97
118
|
return lines.join('\n');
|
|
98
119
|
}
|
|
99
120
|
/** Merge every missing managed setting into `yaml` and return the new contents. */
|
|
100
|
-
export function upsertPnpmSettings(yaml, needsEsbuild) {
|
|
121
|
+
export function upsertPnpmSettings(yaml, needsEsbuild, glob) {
|
|
101
122
|
let next = yaml;
|
|
102
|
-
for (const setting of
|
|
123
|
+
for (const setting of settingsFor(glob)) {
|
|
103
124
|
if (!setting.applies(needsEsbuild) || setting.satisfied(next))
|
|
104
125
|
continue;
|
|
105
126
|
if (setting.item && section(next, setting.key)) {
|
|
@@ -115,10 +136,10 @@ export function upsertPnpmSettings(yaml, needsEsbuild) {
|
|
|
115
136
|
* Merge the managed pnpm settings into `pnpm-workspace.yaml`, creating it when
|
|
116
137
|
* absent. Returns the relative path if anything changed, else null.
|
|
117
138
|
*/
|
|
118
|
-
export async function ensurePnpmSettings(targetDir, needsEsbuild) {
|
|
139
|
+
export async function ensurePnpmSettings(targetDir, needsEsbuild, glob) {
|
|
119
140
|
const file = path.join(targetDir, WORKSPACE_FILE);
|
|
120
141
|
const current = (await fs.pathExists(file)) ? await fs.readFile(file, 'utf-8') : '';
|
|
121
|
-
const next = upsertPnpmSettings(current, needsEsbuild);
|
|
142
|
+
const next = upsertPnpmSettings(current, needsEsbuild, glob);
|
|
122
143
|
if (next === current)
|
|
123
144
|
return null;
|
|
124
145
|
await fs.writeFile(file, next.replace(/^\n+/, ''));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { hookHasUncommented } from '../../base/checks.js';
|
|
4
|
-
import { WORKSPACE_FILE, dependsOnEsbuild, missingPnpmSettings, } from '../../cli/generators/pnpm-workspace.js';
|
|
4
|
+
import { WORKSPACE_FILE, dependsOnEsbuild, familyGlob, missingPnpmSettings, } from '../../cli/generators/pnpm-workspace.js';
|
|
5
5
|
const PACKAGE = '@rtorcato/repo-tooling';
|
|
6
6
|
const NODE_MIN_MAJOR = 22;
|
|
7
7
|
const NODE_LTS_REQUIREMENTS = {
|
|
@@ -849,7 +849,7 @@ export async function checkPnpmWorkspace(dir, pkg) {
|
|
|
849
849
|
return { check, status: 'ok', detail: 'not a pnpm repo' };
|
|
850
850
|
}
|
|
851
851
|
const yaml = exists ? await fs.readFile(file, 'utf-8') : '';
|
|
852
|
-
const missing = missingPnpmSettings(yaml, dependsOnEsbuild(allDeps(pkg)));
|
|
852
|
+
const missing = missingPnpmSettings(yaml, dependsOnEsbuild(allDeps(pkg)), familyGlob(pkg?.name));
|
|
853
853
|
if (missing.length === 0) {
|
|
854
854
|
return { check, status: 'ok', detail: `${WORKSPACE_FILE} carries the managed settings` };
|
|
855
855
|
}
|
|
@@ -10,7 +10,7 @@ import { GH_WORKFLOWS, generateGhWorkflow } from '../../cli/generators/github-wo
|
|
|
10
10
|
import { generateESLintConfig, generatePrettierConfig } from '../../cli/generators/linting.js';
|
|
11
11
|
import { alignNodeVersion, ensureEnginesNode, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, generateVscodeExtensions, } from '../../cli/generators/misc.js';
|
|
12
12
|
import { composeVerifyScriptFromPkg } from '../../cli/generators/package-json.js';
|
|
13
|
-
import { WORKSPACE_FILE, dependsOnEsbuild, ensurePnpmSettings, } from '../../cli/generators/pnpm-workspace.js';
|
|
13
|
+
import { WORKSPACE_FILE, dependsOnEsbuild, ensurePnpmSettings, familyGlob, } from '../../cli/generators/pnpm-workspace.js';
|
|
14
14
|
import { generatePostcss } from '../../cli/generators/postcss.js';
|
|
15
15
|
import { generateCypressConfig, generateVitestConfig } from '../../cli/generators/testing.js';
|
|
16
16
|
import { generateTreeshakeCheck, inferSubpathsFromExports } from '../../cli/generators/treeshake.js';
|
|
@@ -351,7 +351,7 @@ export const FIXERS = [
|
|
|
351
351
|
...(pkg?.dependencies ?? {}),
|
|
352
352
|
...(pkg?.devDependencies ?? {}),
|
|
353
353
|
};
|
|
354
|
-
const written = await ensurePnpmSettings(targetDir, dependsOnEsbuild(deps));
|
|
354
|
+
const written = await ensurePnpmSettings(targetDir, dependsOnEsbuild(deps), familyGlob(pkg?.name));
|
|
355
355
|
return { filesWritten: written ? [written] : [] };
|
|
356
356
|
},
|
|
357
357
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtorcato/repo-tooling",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
"vitest": "^4.1.10"
|
|
278
278
|
},
|
|
279
279
|
"peerDependencies": {
|
|
280
|
-
"@biomejs/biome": "^2.
|
|
280
|
+
"@biomejs/biome": "^2.5.0",
|
|
281
281
|
"@commitlint/cli": "^20.0.0 || ^21.0.0",
|
|
282
282
|
"@commitlint/config-conventional": "^21.2.0",
|
|
283
283
|
"@commitlint/types": "^21.2.0",
|
|
@@ -185,10 +185,15 @@ body,
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/* ---------- Code ----------
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
Every surface and text colour here comes from the theme tokens, which already
|
|
189
|
+
flip between light and dark — the values that used to be hardcoded (#0c111b,
|
|
190
|
+
#dfe5ee, #94a1b4) were simply the dark side of tokens that existed all along.
|
|
191
|
+
|
|
192
|
+
This pairs with `prism.theme: vsLight` / `prism.darkTheme: vsDark` in
|
|
193
|
+
docusaurus.config.ts. The two must move together (#324): vsDark tokens on a
|
|
194
|
+
light surface are pale and unreadable, which is why the background was pinned
|
|
195
|
+
dark in both modes in the first place. If a fenced block still renders dark in
|
|
196
|
+
light mode, that config is the thing to check. */
|
|
192
197
|
code {
|
|
193
198
|
background: var(--jt-code-bg);
|
|
194
199
|
border: 1px solid var(--jt-border);
|
|
@@ -202,23 +207,30 @@ div[class*="codeBlockContainer"],
|
|
|
202
207
|
pre[class*="language-"] {
|
|
203
208
|
border: 1px solid var(--jt-border);
|
|
204
209
|
border-radius: 12px;
|
|
205
|
-
background
|
|
210
|
+
/* !important beats the inline background the Prism theme sets on the <pre>. */
|
|
211
|
+
background: var(--jt-code-bg) !important;
|
|
206
212
|
}
|
|
207
213
|
.theme-code-block code,
|
|
208
214
|
pre[class*="language-"] code {
|
|
209
215
|
background: transparent !important;
|
|
210
216
|
border: none !important;
|
|
211
|
-
|
|
217
|
+
/* Base colour for anything Prism does not tokenise. */
|
|
218
|
+
color: var(--jt-text);
|
|
212
219
|
}
|
|
213
220
|
div[class*="codeBlockTitle"] {
|
|
214
|
-
background:
|
|
215
|
-
color:
|
|
221
|
+
background: var(--jt-surface2) !important;
|
|
222
|
+
color: var(--jt-muted) !important;
|
|
216
223
|
border-bottom: 1px solid var(--jt-border);
|
|
217
224
|
}
|
|
225
|
+
/* In dark the title sits a shade *below* the block; in light there is no token
|
|
226
|
+
darker than --jt-code-bg, so the bottom border does the separating instead. */
|
|
227
|
+
[data-theme="dark"] div[class*="codeBlockTitle"] {
|
|
228
|
+
background: var(--jt-bg) !important;
|
|
229
|
+
}
|
|
218
230
|
[data-theme="light"] .theme-code-block,
|
|
219
231
|
[data-theme="light"] div[class*="codeBlockContainer"],
|
|
220
232
|
[data-theme="light"] pre[class*="language-"] {
|
|
221
|
-
border-color:
|
|
233
|
+
border-color: var(--jt-border2);
|
|
222
234
|
box-shadow: 0 8px 24px -18px rgba(12, 18, 28, 0.5);
|
|
223
235
|
}
|
|
224
236
|
|