@aigamo/catalog 0.0.1-alpha.72 → 0.0.1-alpha.74

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 (3) hide show
  1. package/README.md +30 -12
  2. package/package.json +31 -28
  3. package/sync-catalog.mjs +188 -0
package/README.md CHANGED
@@ -7,21 +7,39 @@ The shared [pnpm catalog](https://pnpm.io/catalogs) for `@aigamo` projects.
7
7
  is the single source of truth. This package just snapshots the catalog so other
8
8
  repositories can consume the same dependency versions.
9
9
 
10
- ## Shape
10
+ ## Consuming it
11
+
12
+ In any repo, run one command:
11
13
 
12
- ```jsonc
13
- {
14
- "catalog": { "react": "^18.3.1", "vite": "^8.3.0", /* ... */ },
15
- "catalogs": { /* named catalogs, if any */ }
16
- }
14
+ ```bash
15
+ pnpm dlx @aigamo/catalog && pnpm install
17
16
  ```
18
17
 
19
- ## Consuming it
18
+ That's it. It brings your `pnpm-workspace.yaml` `catalog:` block up to the
19
+ shared versions (creating the block if needed), then `pnpm install` applies
20
+ them. Run the same command again whenever you want to pick up newer versions.
21
+
22
+ It only touches deps your repo actually uses — a dep counts as used if it's
23
+ already in your catalog, or referenced via `catalog:` in any `package.json`:
24
+
25
+ - for each used dep, takes the shared version (adds if missing, updates if
26
+ changed), so a fresh repo just needs to reference `catalog:` and sync,
27
+ - never adds shared deps you don't use,
28
+ - keeps your own project-specific entries,
29
+ - removes nothing; leaves `minimumReleaseAge`, `allowBuilds`, `packages`,
30
+ ordering and comments untouched.
31
+
32
+ > Why a command and not automatic on install? pnpm doesn't run a dependency's
33
+ > install scripts unless you allow-list them, and a catalog change only takes
34
+ > effect on the next `pnpm install` anyway — so one explicit command is both
35
+ > simpler and less surprising.
36
+
37
+ ### CI check (optional)
20
38
 
21
- Add it as a dev dependency and read `@aigamo/catalog/catalog.json` in a sync
22
- script that writes the `catalog:` block of your own `pnpm-workspace.yaml`
23
- (merging any project-specific entries on top). See the consuming repo's
24
- `scripts/sync-catalog.mjs`.
39
+ `pnpm dlx @aigamo/catalog --check` writes nothing and exits non-zero when your
40
+ catalog is out of sync, so CI can fail if someone forgot to run the sync. The
41
+ raw data is also exported at `@aigamo/catalog/catalog.json` if you'd rather
42
+ script it yourself.
25
43
 
26
44
  ## Regenerating locally
27
45
 
@@ -29,4 +47,4 @@ script that writes the `catalog:` block of your own `pnpm-workspace.yaml`
29
47
  pnpm --filter @aigamo/catalog build
30
48
  ```
31
49
 
32
- Runs automatically on `prepack`, so a publish always ships the current catalog.
50
+ The publish workflow runs `pnpm build` before `nx release publish`, so a publish always ships the current catalog.
package/package.json CHANGED
@@ -1,29 +1,32 @@
1
1
  {
2
- "name": "@aigamo/catalog",
3
- "version": "0.0.1-alpha.72",
4
- "type": "module",
5
- "description": "Shared pnpm catalog for @aigamo projects, generated from hydrangean-diva's pnpm-workspace.yaml.",
6
- "scripts": {
7
- "build": "node build.mjs",
8
- "prepack": "node build.mjs"
9
- },
10
- "files": [
11
- "catalog.json"
12
- ],
13
- "exports": {
14
- ".": "./catalog.json",
15
- "./catalog.json": "./catalog.json"
16
- },
17
- "devDependencies": {
18
- "yaml": "^2.5.0"
19
- },
20
- "license": "MIT",
21
- "repository": {
22
- "type": "git",
23
- "url": "git+https://github.com/ycanardeau/hydrangean-diva.git"
24
- },
25
- "author": "Aigamo <51428094+ycanardeau@users.noreply.github.com>",
26
- "publishConfig": {
27
- "access": "public"
28
- }
29
- }
2
+ "name": "@aigamo/catalog",
3
+ "version": "0.0.1-alpha.74",
4
+ "type": "module",
5
+ "description": "Shared pnpm catalog for @aigamo projects, generated from hydrangean-diva's pnpm-workspace.yaml.",
6
+ "bin": {
7
+ "aigamo-sync-catalog": "sync-catalog.mjs"
8
+ },
9
+ "files": [
10
+ "catalog.json",
11
+ "sync-catalog.mjs"
12
+ ],
13
+ "exports": {
14
+ ".": "./catalog.json",
15
+ "./catalog.json": "./catalog.json"
16
+ },
17
+ "dependencies": {
18
+ "yaml": "^2.5.0"
19
+ },
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ycanardeau/hydrangean-diva.git"
24
+ },
25
+ "author": "Aigamo <51428094+ycanardeau@users.noreply.github.com>",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "node build.mjs"
31
+ }
32
+ }
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ // Shipped by @aigamo/catalog as the `aigamo-sync-catalog` bin.
3
+ //
4
+ // Brings the consuming repo's pnpm-workspace.yaml `catalog:` block up to the
5
+ // shared versions in this package's `catalog.json` snapshot, so every repo uses
6
+ // the same dependency versions.
7
+ //
8
+ // It only touches deps the repo actually uses — a dep is "used" if it is
9
+ // already listed in the consumer's catalog, or referenced via `catalog:` in any
10
+ // package.json. That way a repo never accumulates shared entries it doesn't use.
11
+ //
12
+ // Rules:
13
+ // - For every used dep the shared catalog knows about, take the shared version
14
+ // (added if missing, updated if it differs).
15
+ // - Shared deps the repo doesn't use are NOT added.
16
+ // - Project-specific entries (ones the shared catalog does not have) are kept.
17
+ // - Nothing is ever removed; minimumReleaseAge, allowBuilds, packages,
18
+ // ordering and comments are left untouched.
19
+ //
20
+ // Usage:
21
+ // pnpm dlx @aigamo/catalog sync this repo's catalog, then `pnpm install`
22
+ // aigamo-sync-catalog same, if installed as a dependency
23
+ // aigamo-sync-catalog --check report drift only, write nothing; exits
24
+ // non-zero when out of sync (for CI)
25
+
26
+ import { createRequire } from 'node:module';
27
+ import { dirname, resolve } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
30
+ import { parseDocument } from 'yaml';
31
+
32
+ const checkOnly = process.argv.slice(2).includes('--check');
33
+
34
+ function fail(message) {
35
+ console.error(`aigamo-sync-catalog: ${message}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ // This package's own catalog.json, co-located with this script.
40
+ const require = createRequire(import.meta.url);
41
+ const here = dirname(fileURLToPath(import.meta.url));
42
+ const sharedCatalog = require(resolve(here, 'catalog.json')).catalog ?? {};
43
+
44
+ // Prefer INIT_CWD (the dir the command was invoked from) so we find the
45
+ // consumer workspace even when run from the package store via `pnpm dlx`.
46
+ const start = process.env.INIT_CWD || process.cwd();
47
+ const wsPath = findWorkspaceFile(start);
48
+ if (!wsPath) {
49
+ fail(
50
+ `no pnpm-workspace.yaml found above ${start}\n` +
51
+ 'Create one with a `catalog:` line, then run this again.',
52
+ );
53
+ }
54
+
55
+ const doc = parseDocument(readFileSync(wsPath, 'utf8'));
56
+ // Create the catalog block if the consumer doesn't have one yet.
57
+ if (!doc.has('catalog')) doc.set('catalog', doc.createNode({}));
58
+ const catalog = doc.get('catalog');
59
+
60
+ // A dep is "used" if it is already in the catalog, or referenced via `catalog:`
61
+ // somewhere in the workspace.
62
+ const used = new Set(catalog.items.map((i) => String(i.key)));
63
+ for (const name of collectCatalogRefs(dirname(wsPath))) used.add(name);
64
+
65
+ const added = [];
66
+ const bumped = [];
67
+ const unresolved = [];
68
+ for (const name of used) {
69
+ if (!(name in sharedCatalog)) {
70
+ // Referenced via catalog: but neither in the catalog nor shared — the
71
+ // consumer has to supply it. Flag it; never touch it.
72
+ if (!catalog.has(name)) unresolved.push(name);
73
+ continue;
74
+ }
75
+ const next = sharedCatalog[name];
76
+ if (!catalog.has(name)) {
77
+ added.push(` ${name}: ${next}`);
78
+ if (!checkOnly) insertSorted(catalog, name, next);
79
+ } else if (String(catalog.get(name)) !== next) {
80
+ bumped.push(` ${name}: ${catalog.get(name)} -> ${next}`);
81
+ if (!checkOnly) catalog.set(name, next);
82
+ }
83
+ }
84
+
85
+ const changes = added.length + bumped.length;
86
+
87
+ function report() {
88
+ if (added.length) console.log(`added:\n${added.join('\n')}`);
89
+ if (bumped.length) console.log(`changed:\n${bumped.join('\n')}`);
90
+ if (unresolved.length) {
91
+ console.log(
92
+ `\nreferenced via \`catalog:\` but not in @aigamo/catalog — add a ` +
93
+ `version yourself:\n ${unresolved.join(', ')}`,
94
+ );
95
+ }
96
+ }
97
+
98
+ if (checkOnly) {
99
+ if (!changes) {
100
+ console.log('@aigamo/catalog: catalog is in sync.');
101
+ report();
102
+ process.exit(0);
103
+ }
104
+ console.log(`@aigamo/catalog: ${changes} entries out of sync:`);
105
+ report();
106
+ console.log('\nRun `pnpm dlx @aigamo/catalog && pnpm install` to apply.');
107
+ process.exit(1);
108
+ }
109
+
110
+ if (!changes) {
111
+ console.log('@aigamo/catalog: already in sync.');
112
+ report();
113
+ process.exit(0);
114
+ }
115
+
116
+ writeFileSync(wsPath, doc.toString({ singleQuote: true }));
117
+ console.log(`@aigamo/catalog: updated ${changes} entries in ${wsPath}`);
118
+ report();
119
+ console.log('\nRun `pnpm install` to apply.');
120
+
121
+ // Add a new catalog entry, keeping the block's existing ordering: the key is
122
+ // placed before the first existing key that sorts after it. On an already-sorted
123
+ // catalog this keeps it sorted; otherwise it just appends near the end.
124
+ function insertSorted(map, key, value) {
125
+ map.set(key, value); // appends the new pair
126
+ const pair = map.items.pop();
127
+ let i = map.items.findIndex((item) => String(item.key) > key);
128
+ if (i < 0) i = map.items.length;
129
+ map.items.splice(i, 0, pair);
130
+ }
131
+
132
+ // Walk up from a starting directory to find the consumer workspace file.
133
+ function findWorkspaceFile(startDir) {
134
+ let dir = startDir;
135
+ for (;;) {
136
+ const candidate = resolve(dir, 'pnpm-workspace.yaml');
137
+ if (existsSync(candidate)) return candidate;
138
+ const parent = dirname(dir);
139
+ if (parent === dir) return null;
140
+ dir = parent;
141
+ }
142
+ }
143
+
144
+ // Collect every dependency name referenced with the default `catalog:` protocol
145
+ // across the workspace's package.json files.
146
+ function collectCatalogRefs(rootDir) {
147
+ const names = new Set();
148
+ const fields = [
149
+ 'dependencies',
150
+ 'devDependencies',
151
+ 'peerDependencies',
152
+ 'optionalDependencies',
153
+ ];
154
+ (function walk(dir) {
155
+ let entries;
156
+ try {
157
+ entries = readdirSync(dir, { withFileTypes: true });
158
+ } catch {
159
+ return;
160
+ }
161
+ for (const entry of entries) {
162
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
163
+ const full = resolve(dir, entry.name);
164
+ if (entry.isDirectory()) {
165
+ walk(full);
166
+ } else if (entry.name === 'package.json') {
167
+ let pkg;
168
+ try {
169
+ pkg = JSON.parse(readFileSync(full, 'utf8'));
170
+ } catch {
171
+ continue;
172
+ }
173
+ for (const field of fields) {
174
+ const deps = pkg[field];
175
+ if (!deps) continue;
176
+ for (const [name, spec] of Object.entries(deps)) {
177
+ // `catalog:` and `catalog:default` are the default catalog;
178
+ // `catalog:<other>` points at a named catalog we don't manage.
179
+ if (spec === 'catalog:' || spec === 'catalog:default') {
180
+ names.add(name);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ })(rootDir);
187
+ return names;
188
+ }