@aigamo/catalog 0.0.1-alpha.72 → 0.0.1-alpha.75
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/README.md +30 -12
- package/package.json +31 -28
- 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
|
-
##
|
|
10
|
+
## Consuming it
|
|
11
|
+
|
|
12
|
+
In any repo, run one command:
|
|
11
13
|
|
|
12
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
2
|
+
"name": "@aigamo/catalog",
|
|
3
|
+
"version": "0.0.1-alpha.75",
|
|
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
|
+
}
|
package/sync-catalog.mjs
ADDED
|
@@ -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
|
+
}
|