@rtorcato/repo-tooling 3.2.0 → 3.2.1

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.
@@ -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-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`.
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
- const FAMILY_GLOB = '@rtorcato/*';
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
- const SETTINGS = [
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 SETTINGS.filter((s) => s.applies(needsEsbuild) && !s.satisfied(yaml)).map((s) => s.label);
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 SETTINGS) {
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.0",
3
+ "version": "3.2.1",
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": [