jpm-pkg 1.0.3

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.
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ // Full SemVer implementation — no external dep
4
+ // Supports: X.Y.Z, X.Y.Z-pre+build, ranges: ^, ~, >, >=, <, <=, =, *, x, ||, ranges
5
+
6
+ const RE_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-.]+))?$/;
7
+ const RE_RANGE_PART = /^\s*([\^~>=<!*]*)([0-9x*]+(?:\.[0-9x*]+(?:\.[0-9x*]+)?)?(?:-[0-9A-Za-z-.]+)?)\s*$/;
8
+
9
+ /**
10
+ * Represents a parsed Semantic Version (SemVer).
11
+ */
12
+ class Version {
13
+ /**
14
+ * @param {string} str - The version string to parse
15
+ */
16
+ constructor(str) {
17
+ const m = RE_VERSION.exec(str.trim().replace(/^v/, ''));
18
+ if (!m) throw new Error(`Invalid version: ${str}`);
19
+ this.major = parseInt(m[1], 10);
20
+ this.minor = parseInt(m[2], 10);
21
+ this.patch = parseInt(m[3], 10);
22
+ this.pre = m[4] ? m[4].split('.') : [];
23
+ this.build = m[5] ? m[5].split('.') : [];
24
+ this.raw = str;
25
+ }
26
+
27
+ /**
28
+ * Reconstructs the canonical version string.
29
+ * @returns {string}
30
+ */
31
+ toString() {
32
+ let s = `${this.major}.${this.minor}.${this.patch}`;
33
+ if (this.pre.length) s += `-${this.pre.join('.')}`;
34
+ if (this.build.length) s += `+${this.build.join('.')}`;
35
+ return s;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Compares two pre-release identifier arrays according to SemVer rules.
41
+ *
42
+ * @param {string[]} a - First pre-release array
43
+ * @param {string[]} b - Second pre-release array
44
+ * @returns {number} -1 if a < b, 1 if a > b, 0 if equal
45
+ * @private
46
+ */
47
+ function comparePre(a, b) {
48
+ if (!a.length && !b.length) return 0;
49
+ if (!a.length) return 1;
50
+ if (!b.length) return -1;
51
+ const len = Math.max(a.length, b.length);
52
+ for (let i = 0; i < len; i++) {
53
+ if (a[i] === b[i]) continue;
54
+ if (a[i] === undefined) return -1;
55
+ if (b[i] === undefined) return 1;
56
+ const na = parseInt(a[i], 10), nb = parseInt(b[i], 10);
57
+ if (!isNaN(na) && !isNaN(nb)) return na < nb ? -1 : 1;
58
+ if (!isNaN(na)) return -1;
59
+ if (!isNaN(nb)) return 1;
60
+ return a[i] < b[i] ? -1 : 1;
61
+ }
62
+ return 0;
63
+ }
64
+
65
+ /**
66
+ * Compares two versions.
67
+ *
68
+ * @param {string|Version} a
69
+ * @param {string|Version} b
70
+ * @returns {number} -1 if a < b, 1 if a > b, 0 if equal
71
+ */
72
+ function compare(a, b) {
73
+ const va = a instanceof Version ? a : new Version(a);
74
+ const vb = b instanceof Version ? b : new Version(b);
75
+ for (const k of ['major', 'minor', 'patch']) {
76
+ if (va[k] !== vb[k]) return va[k] < vb[k] ? -1 : 1;
77
+ }
78
+ return comparePre(va.pre, vb.pre);
79
+ }
80
+
81
+ /** @returns {boolean} True if a is greater than b */
82
+ function gt(a, b) { return compare(a, b) > 0; }
83
+ /** @returns {boolean} True if a is less than b */
84
+ function lt(a, b) { return compare(a, b) < 0; }
85
+ /** @returns {boolean} True if a is greater than or equal to b */
86
+ function gte(a, b) { return compare(a, b) >= 0; }
87
+ /** @returns {boolean} True if a is less than or equal to b */
88
+ function lte(a, b) { return compare(a, b) <= 0; }
89
+ /** @returns {boolean} True if versions are logically equal */
90
+ function eq(a, b) { return compare(a, b) === 0; }
91
+
92
+ /**
93
+ * Parses a single semver range token (e.g., "^1.2.3") into a predicate.
94
+ *
95
+ * @param {string} token
96
+ * @returns {function(string|Version): boolean}
97
+ * @private
98
+ */
99
+ function parseSimpleRange(token) {
100
+ token = token.trim();
101
+ if (!token || token === '*' || token === 'latest') return () => true;
102
+
103
+ const hyphen = token.match(/^(.+?)\s+-\s+(.+)$/);
104
+ if (hyphen) {
105
+ const lo = hyphen[1].trim(), hi = hyphen[2].trim();
106
+ return (v) => gte(v, lo) && lte(v, hi);
107
+ }
108
+
109
+ const m = RE_RANGE_PART.exec(token);
110
+ if (!m) return () => false;
111
+
112
+ const [, op, verStr] = m;
113
+ const hasPre = verStr.includes('-');
114
+
115
+ const baseVer = verStr.split(/[-+]/)[0];
116
+ const parts = baseVer.split('.').map(p => (p === 'x' || p === '*' ? null : parseInt(p, 10)));
117
+ const [maj, min, pat] = parts;
118
+
119
+ let lo, hi;
120
+
121
+ if (op === '^') {
122
+ lo = verStr;
123
+ if (maj !== null && maj > 0) hi = `${maj + 1}.0.0-0`;
124
+ else if (min !== null && min > 0) hi = `0.${min + 1}.0-0`;
125
+ else hi = `0.0.${(pat ?? 0) + 1}-0`;
126
+ } else if (op === '~') {
127
+ lo = verStr;
128
+ if (min !== null) hi = `${maj}.${min + 1}.0-0`;
129
+ else hi = `${maj + 1}.0.0-0`;
130
+ } else if (op === '>') {
131
+ return (v) => gt(v, verStr);
132
+ } else if (op === '<') {
133
+ return (v) => lt(v, verStr);
134
+ } else if (op === '>=') {
135
+ return (v) => gte(v, verStr);
136
+ } else if (op === '<=') {
137
+ return (v) => lte(v, verStr);
138
+ } else if (op === '=') {
139
+ return (v) => eq(v, verStr);
140
+ } else {
141
+ if (maj === null || isNaN(maj)) return () => true;
142
+ if (min === null || isNaN(min)) {
143
+ lo = `${maj}.0.0`;
144
+ hi = `${maj + 1}.0.0-0`;
145
+ } else if (pat === null || isNaN(pat)) {
146
+ lo = `${maj}.${min}.0`;
147
+ hi = `${maj}.${min + 1}.0-0`;
148
+ } else {
149
+ return (v) => eq(v, verStr);
150
+ }
151
+ }
152
+
153
+ return (v) => {
154
+ const ver = v instanceof Version ? v : new Version(v);
155
+ if (!gte(ver, lo)) return false;
156
+ if (hi && !lt(ver, hi)) return false;
157
+
158
+ if (ver.pre.length > 0) {
159
+ if (hasPre) {
160
+ const rangeV = new Version(verStr);
161
+ return ver.major === rangeV.major && ver.minor === rangeV.minor && ver.patch === rangeV.patch;
162
+ }
163
+ return false;
164
+ }
165
+ return true;
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Checks if a version satisfies a given semver range.
171
+ *
172
+ * @param {string} version
173
+ * @param {string} range
174
+ * @returns {boolean}
175
+ */
176
+ function satisfies(version, range) {
177
+ if (!range || range === '*' || range === 'latest') return true;
178
+ try {
179
+ const orGroups = range.split('||');
180
+ return orGroups.some(group => {
181
+ const parts = group.trim().split(/\s+(?=[\^~><=!])/);
182
+ return parts.every(part => parseSimpleRange(part)(version));
183
+ });
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ /** @returns {boolean} True if the range string is valid */
190
+ function validRange(range) {
191
+ try { parseSimpleRange(range); return true; } catch { return false; }
192
+ }
193
+
194
+ /** @returns {Version|null} Parsed version or null if invalid */
195
+ function parse(str) {
196
+ try { return new Version(str); } catch { return null; }
197
+ }
198
+
199
+ /** @returns {string|null} Validated version string or null */
200
+ function valid(str) {
201
+ return parse(str) ? str.trim().replace(/^v/, '') : null;
202
+ }
203
+
204
+ /**
205
+ * Coerces a dirty string into a valid major.minor.patch version string.
206
+ * @param {string} str
207
+ * @returns {string|null}
208
+ */
209
+ function coerce(str) {
210
+ const m = str.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
211
+ if (!m) return null;
212
+ return `${m[1] ?? 0}.${m[2] ?? 0}.${m[3] ?? 0}`;
213
+ }
214
+
215
+ /**
216
+ * Finds the highest version in an array that satisfies a range.
217
+ *
218
+ * @param {string[]} versions
219
+ * @param {string} range
220
+ * @returns {string|null}
221
+ */
222
+ function maxSatisfying(versions, range) {
223
+ return versions
224
+ .filter(v => { try { return satisfies(v, range); } catch { return false; } })
225
+ .sort((a, b) => compare(b, a))[0] ?? null;
226
+ }
227
+
228
+ /**
229
+ * Finds the lowest version in an array that satisfies a range.
230
+ *
231
+ * @param {string[]} versions
232
+ * @param {string} range
233
+ * @returns {string|null}
234
+ */
235
+ function minSatisfying(versions, range) {
236
+ return versions
237
+ .filter(v => { try { return satisfies(v, range); } catch { return false; } })
238
+ .sort((a, b) => compare(a, b))[0] ?? null;
239
+ }
240
+
241
+ /**
242
+ * Increments a version according to release type.
243
+ *
244
+ * @param {string} version
245
+ * @param {'major'|'minor'|'patch'|'prerelease'} release
246
+ * @returns {string|null}
247
+ */
248
+ function inc(version, release) {
249
+ const v = new Version(version);
250
+ if (release === 'major') return `${v.major + 1}.0.0`;
251
+ if (release === 'minor') return `${v.major}.${v.minor + 1}.0`;
252
+ if (release === 'patch') return `${v.major}.${v.minor}.${v.patch + 1}`;
253
+ if (release === 'prerelease') {
254
+ if (v.pre.length) {
255
+ const last = parseInt(v.pre[v.pre.length - 1], 10);
256
+ if (!isNaN(last)) {
257
+ return `${v.major}.${v.minor}.${v.patch}-${v.pre.slice(0, -1).concat(last + 1).join('.')}`;
258
+ }
259
+ }
260
+ return `${v.major}.${v.minor}.${v.patch + 1}-0`;
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /** Sorts an array of version strings */
266
+ function sort(versions) {
267
+ return [...versions].sort((a, b) => compare(a, b));
268
+ }
269
+
270
+ /** Sorts an array of version strings in reverse */
271
+ function rsort(versions) {
272
+ return [...versions].sort((a, b) => compare(b, a));
273
+ }
274
+
275
+ module.exports = {
276
+ Version, compare, gt, lt, gte, lte, eq,
277
+ satisfies, validRange, parse, valid, coerce,
278
+ maxSatisfying, minSatisfying, inc, sort, rsort,
279
+ };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * System capabilities and environment detection.
5
+ */
6
+ const system = {
7
+ platform: process.platform, // 'win32', 'linux', 'darwin', etc.
8
+ arch: process.arch, // 'x64', 'arm64', etc.
9
+
10
+ /**
11
+ * Checks if a package's OS/CPU requirements match the current system.
12
+ *
13
+ * @param {Object} meta - Package metadata (must contain 'os' and/or 'cpu' arrays)
14
+ * @returns {boolean} True if the package is compatible or no requirements defined
15
+ */
16
+ isCompatible(meta) {
17
+ // OS check
18
+ if (meta.os && Array.isArray(meta.os)) {
19
+ const isMatch = meta.os.some(o => {
20
+ if (o.startsWith('!')) return system.platform !== o.slice(1);
21
+ return system.platform === o;
22
+ });
23
+ if (!isMatch) return false;
24
+ }
25
+
26
+ // CPU check
27
+ if (meta.cpu && Array.isArray(meta.cpu)) {
28
+ const isMatch = meta.cpu.some(c => {
29
+ if (c.startsWith('!')) return system.arch !== c.slice(1);
30
+ return system.arch === c;
31
+ });
32
+ if (!isMatch) return false;
33
+ }
34
+
35
+ return true;
36
+ }
37
+ };
38
+
39
+ module.exports = system;
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const PackageJSON = require('../core/package-json');
6
+ const { mkdirp, symlink } = require('../utils/fs');
7
+ const logger = require('../utils/logger');
8
+
9
+ /**
10
+ * Discovers and manages workspaces in a monorepo.
11
+ * Supports glob-like patterns: "packages/*", "apps/*"
12
+ */
13
+ class Workspace {
14
+ constructor(rootDir) {
15
+ this.rootDir = rootDir;
16
+ this.rootPkg = PackageJSON.fromDir(rootDir);
17
+ }
18
+
19
+ /** Returns array of { name, version, dir, pkg } for all workspace packages */
20
+ getPackages() {
21
+ const patterns = this.rootPkg.workspaces;
22
+ if (!patterns || !patterns.length) return [];
23
+
24
+ const packages = [];
25
+ for (const pattern of patterns) {
26
+ const matchedDirs = this._glob(pattern);
27
+ for (const dir of matchedDirs) {
28
+ const pkgFile = path.join(dir, 'package.json');
29
+ if (!fs.existsSync(pkgFile)) continue;
30
+ const pkg = PackageJSON.fromDir(dir);
31
+ packages.push({
32
+ name: pkg.name,
33
+ version: pkg.version,
34
+ dir,
35
+ pkg,
36
+ });
37
+ }
38
+ }
39
+
40
+ return packages;
41
+ }
42
+
43
+ /**
44
+ * Link workspace packages into each other's node_modules
45
+ * so inter-workspace imports work without publishing.
46
+ */
47
+ async link() {
48
+ const packages = this.getPackages();
49
+ const byName = new Map(packages.map(p => [p.name, p]));
50
+
51
+ for (const ws of packages) {
52
+ const allDeps = {
53
+ ...ws.pkg.dependencies,
54
+ ...ws.pkg.devDependencies,
55
+ };
56
+
57
+ for (const depName of Object.keys(allDeps)) {
58
+ if (!byName.has(depName)) continue;
59
+ const depWs = byName.get(depName);
60
+
61
+ // Create symlink: ws/node_modules/depName → depWs.dir
62
+ const linkPath = path.join(ws.dir, 'node_modules', depName);
63
+ mkdirp(path.dirname(linkPath));
64
+ try {
65
+ symlink(depWs.dir, linkPath);
66
+ logger.verbose(`linked ${depName} → ${depWs.dir} in ${ws.name}`);
67
+ } catch (err) {
68
+ logger.warn(`Could not link ${depName} in ${ws.name}: ${err.message}`);
69
+ }
70
+ }
71
+ }
72
+
73
+ logger.success(`Linked ${packages.length} workspace packages`);
74
+ }
75
+
76
+ /** Run a script across all (or filtered) workspaces */
77
+ async runScript(scriptName, { filter } = {}) {
78
+ const { spawnSync } = require('node:child_process');
79
+ const packages = this.getPackages().filter(ws =>
80
+ !filter || ws.name.includes(filter)
81
+ );
82
+
83
+ for (const ws of packages) {
84
+ if (!ws.pkg.scripts[scriptName]) {
85
+ logger.verbose(`[${ws.name}] no script "${scriptName}" — skipping`);
86
+ continue;
87
+ }
88
+ logger.section(`▶ ${ws.name} — ${scriptName}`);
89
+ const result = spawnSync(ws.pkg.scripts[scriptName], {
90
+ cwd: ws.dir,
91
+ shell: true,
92
+ stdio: 'inherit',
93
+ });
94
+ if (result.status !== 0) {
95
+ logger.error(`[${ws.name}] script "${scriptName}" failed with exit code ${result.status}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Simple glob: supports "packages/*" — one level wildcard only */
101
+ _glob(pattern) {
102
+ const parts = pattern.split('/');
103
+ let dirs = [this.rootDir];
104
+
105
+ for (const part of parts) {
106
+ const next = [];
107
+ for (const base of dirs) {
108
+ if (part === '*' || part === '**') {
109
+ try {
110
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
111
+ if (entry.isDirectory()) next.push(path.join(base, entry.name));
112
+ }
113
+ } catch { }
114
+ } else {
115
+ const candidate = path.join(base, part);
116
+ if (fs.existsSync(candidate)) next.push(candidate);
117
+ }
118
+ }
119
+ dirs = next;
120
+ }
121
+
122
+ return dirs;
123
+ }
124
+ }
125
+
126
+ module.exports = Workspace;