@xemahq/repo-build-tooling 0.2.3 → 0.3.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/repo-build-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM with zero dependencies so the published artifact is the reviewed source.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
|
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
},
|
|
18
18
|
"type": "module",
|
|
19
19
|
"files": [
|
|
20
|
-
"README.md"
|
|
20
|
+
"README.md",
|
|
21
|
+
"src"
|
|
21
22
|
],
|
|
22
23
|
"bin": {
|
|
23
24
|
"xema-scrub-swagger-paths": "src/scrub-swagger-plugin-paths.mjs",
|
|
24
|
-
"xema-check-workspace-ranges": "src/check-workspace-range-matches-local.mjs"
|
|
25
|
+
"xema-check-workspace-ranges": "src/check-workspace-range-matches-local.mjs",
|
|
26
|
+
"xema-check-no-vendored-deps": "src/check-no-vendored-deps.mjs"
|
|
25
27
|
},
|
|
26
28
|
"exports": {
|
|
27
|
-
"./
|
|
29
|
+
"./check-no-vendored-deps": "./src/check-no-vendored-deps.mjs",
|
|
30
|
+
"./check-workspace-range-matches-local": "./src/check-workspace-range-matches-local.mjs",
|
|
28
31
|
"./package.json": "./package.json",
|
|
29
|
-
"./
|
|
32
|
+
"./scrub-swagger-plugin-paths": "./src/scrub-swagger-plugin-paths.mjs"
|
|
30
33
|
},
|
|
31
34
|
"scripts": {
|
|
32
|
-
"test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs"
|
|
35
|
+
"test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs"
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
+
// A `workspace:` range for a package this repo does NOT own is a VENDORED
|
|
4
|
+
// UPSTREAM DEP: the name resolves to a local directory instead of the registry
|
|
5
|
+
// version the rest of the fleet installs. Two copies of one class, one of them
|
|
6
|
+
// invisible to every version gate, because a lockfile that never resolved the
|
|
7
|
+
// name has nothing to compare.
|
|
8
|
+
//
|
|
9
|
+
// ── Why this is one shared bin and not four copies ──
|
|
10
|
+
//
|
|
11
|
+
// It was four: xema-kernel-sdk, xema-kernel-contracts, xema-biome-sdk and
|
|
12
|
+
// xema-runtime-core each carried `tooling/boundaries/check-no-vendored-deps.mjs`.
|
|
13
|
+
// All four asserted the IDENTICAL predicate — byte-equivalent modulo quote
|
|
14
|
+
// style — and differed only in how they walked `packages/`. That is not
|
|
15
|
+
// per-repo configuration: all four `pnpm-workspace.yaml` files declare the same
|
|
16
|
+
// two globs, so there was nothing to configure and three of the four traversals
|
|
17
|
+
// were elaborations nobody needed.
|
|
18
|
+
//
|
|
19
|
+
// They had also drifted in a way that MATTERS. xema-kernel-contracts' copy
|
|
20
|
+
// recursed without bound and without stopping at a package leaf, so applied to
|
|
21
|
+
// xema-biome-sdk's tree it ingested two TEST FIXTURES as locally-owned packages
|
|
22
|
+
// — widening the allowed set, latent rather than live only because both
|
|
23
|
+
// fixtures happen to declare registry ranges today. Nothing bound the four
|
|
24
|
+
// copies to each other; `check-carved-lib-parity` covers `lib/` helpers, not
|
|
25
|
+
// gates.
|
|
26
|
+
//
|
|
27
|
+
// ── The vacuous-green hole, fixed here ──
|
|
28
|
+
//
|
|
29
|
+
// All four copies guarded that `packages/` EXISTS and never that any manifest
|
|
30
|
+
// was FOUND. A directory rename below it, or a layout change, and every one of
|
|
31
|
+
// them printed `OK: … (0 local packages scanned)` and exited 0 — green while
|
|
32
|
+
// executing nothing, this fleet's most-repeated defect. The two shallow
|
|
33
|
+
// traversals were precisely the ones a layout change would silently empty.
|
|
34
|
+
// `assertNonEmptyCorpus` below refuses a zero-manifest scan.
|
|
35
|
+
//
|
|
36
|
+
// Usage (from the repository root):
|
|
37
|
+
// xema-check-no-vendored-deps
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
import { execFileSync } from 'node:child_process';
|
|
40
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
41
|
+
import { join, relative } from 'node:path';
|
|
42
|
+
import { pathToFileURL } from 'node:url';
|
|
43
|
+
|
|
44
|
+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The DEPTHS at which a workspace manifest may sit, derived from
|
|
48
|
+
* `pnpm-workspace.yaml` rather than assumed.
|
|
49
|
+
*
|
|
50
|
+
* Deriving is what stops the kernel-contracts defect recurring: an unbounded
|
|
51
|
+
* walk ingests a `test/fixtures/**\/package.json` as a locally-owned package,
|
|
52
|
+
* which WIDENS the allowed set — the one direction a gate must never drift in.
|
|
53
|
+
* A glob of `packages/*` admits `packages/<a>/package.json` (2 segments below
|
|
54
|
+
* the root); `packages/*\/*` admits 3.
|
|
55
|
+
*
|
|
56
|
+
* Returns null when the file declares no `packages/`-rooted glob at all, which
|
|
57
|
+
* the caller treats as "this repo is not shaped for this check" rather than as
|
|
58
|
+
* an empty result.
|
|
59
|
+
*
|
|
60
|
+
* The depth is `find`'s, i.e. levels BELOW the `packages` start path, and it
|
|
61
|
+
* equals the glob's segment count: `packages/*` admits `packages/a/package.json`
|
|
62
|
+
* at depth 2, `packages/*\/*` admits `packages/a/b/package.json` at depth 3.
|
|
63
|
+
*
|
|
64
|
+
* Written as `length + 1` first, which passed every repo — because BOTH entries
|
|
65
|
+
* were off by one and the two ranges overlapped on a tree whose packages all
|
|
66
|
+
* sit at one level. The counts matched the four incumbent gates exactly, which
|
|
67
|
+
* is precisely what made it look right. A repo with a manifest directly at
|
|
68
|
+
* `packages/<name>/package.json` would have been silently skipped, and a
|
|
69
|
+
* narrowed scan is the one failure this check exists to prevent.
|
|
70
|
+
*/
|
|
71
|
+
export function manifestDepthsFor(workspaceYaml) {
|
|
72
|
+
const depths = new Set();
|
|
73
|
+
for (const line of workspaceYaml.split('\n')) {
|
|
74
|
+
const m = line.match(/^\s*-\s*['"]?(packages\/[^'"#\s]*)['"]?\s*$/);
|
|
75
|
+
if (!m) continue;
|
|
76
|
+
depths.add(m[1].split('/').length);
|
|
77
|
+
}
|
|
78
|
+
return depths.size > 0 ? [...depths].sort((a, b) => a - b) : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every manifest at exactly the declared depths. `find` rather than a hand
|
|
83
|
+
* walk, for the same reason the sibling check uses it: no dependency, and the
|
|
84
|
+
* depth bounds are expressed once instead of reimplemented per level.
|
|
85
|
+
*/
|
|
86
|
+
function listManifests(repoRoot, depths) {
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const depth of depths) {
|
|
89
|
+
let stdout;
|
|
90
|
+
try {
|
|
91
|
+
stdout = execFileSync(
|
|
92
|
+
'find',
|
|
93
|
+
[
|
|
94
|
+
'packages',
|
|
95
|
+
'-mindepth', String(depth),
|
|
96
|
+
'-maxdepth', String(depth),
|
|
97
|
+
'-name', 'package.json',
|
|
98
|
+
'-not', '-path', '*/node_modules/*',
|
|
99
|
+
'-not', '-path', '*/dist/*',
|
|
100
|
+
],
|
|
101
|
+
{ cwd: repoRoot, encoding: 'utf8' },
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const line of stdout.split('\n')) if (line.trim()) out.push(line.trim());
|
|
107
|
+
}
|
|
108
|
+
return [...new Set(out)].sort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The finding set. Pure over its inputs so it is testable without a tree.
|
|
113
|
+
*
|
|
114
|
+
* @param {Array<{path:string, manifest:object}>} manifests
|
|
115
|
+
* @returns {Array<{path:string, field:string, depName:string, depRange:string}>}
|
|
116
|
+
*/
|
|
117
|
+
export function findVendoredDeps(manifests) {
|
|
118
|
+
const localPackageNames = new Set(
|
|
119
|
+
manifests.map(({ manifest }) => manifest.name).filter(Boolean),
|
|
120
|
+
);
|
|
121
|
+
const offenders = [];
|
|
122
|
+
for (const { path, manifest } of manifests) {
|
|
123
|
+
for (const field of DEP_FIELDS) {
|
|
124
|
+
const deps = manifest[field];
|
|
125
|
+
if (!deps) continue;
|
|
126
|
+
for (const [depName, depRange] of Object.entries(deps)) {
|
|
127
|
+
if (typeof depRange !== 'string') continue;
|
|
128
|
+
if (!depRange.startsWith('workspace:')) continue;
|
|
129
|
+
if (localPackageNames.has(depName)) continue;
|
|
130
|
+
offenders.push({ path, field, depName, depRange });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return offenders;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A corpus of zero manifests is a FAILURE, never a pass.
|
|
139
|
+
*
|
|
140
|
+
* This is the assertion all four predecessors lacked. `packages/` existing says
|
|
141
|
+
* nothing about whether anything was read, and a check that reports OK over an
|
|
142
|
+
* empty scan is worse than no check because it reads as protection in review.
|
|
143
|
+
*/
|
|
144
|
+
function assertNonEmptyCorpus(count, repoRoot, depths) {
|
|
145
|
+
if (count > 0) return null;
|
|
146
|
+
return (
|
|
147
|
+
`found ZERO package manifests under ${join(repoRoot, 'packages')} at the depth(s) ` +
|
|
148
|
+
`${depths.join(', ')} declared by pnpm-workspace.yaml.\n` +
|
|
149
|
+
'An empty scan must never read as a passing one. Either the workspace globs and the ' +
|
|
150
|
+
'tree have diverged, or this check is pointed at the wrong root.'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function main() {
|
|
155
|
+
const repoRoot = process.cwd();
|
|
156
|
+
if (!existsSync(join(repoRoot, 'package.json'))) {
|
|
157
|
+
console.error(
|
|
158
|
+
`::error::${repoRoot} has no package.json — run this from the repository root ` +
|
|
159
|
+
'so it scans the repository, not whatever directory it was invoked from.',
|
|
160
|
+
);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const wsPath = join(repoRoot, 'pnpm-workspace.yaml');
|
|
165
|
+
if (!existsSync(wsPath)) {
|
|
166
|
+
console.error(`::error::no pnpm-workspace.yaml at ${repoRoot}; cannot derive the scan depth.`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
const depths = manifestDepthsFor(readFileSync(wsPath, 'utf8'));
|
|
170
|
+
if (depths === null) {
|
|
171
|
+
console.error(
|
|
172
|
+
`::error::pnpm-workspace.yaml at ${repoRoot} declares no \`packages/\` glob. ` +
|
|
173
|
+
'This check asserts a property of that tree; it must not silently pass over its absence.',
|
|
174
|
+
);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const manifests = [];
|
|
179
|
+
for (const rel of listManifests(repoRoot, depths)) {
|
|
180
|
+
try {
|
|
181
|
+
manifests.push({ path: rel, manifest: JSON.parse(readFileSync(join(repoRoot, rel), 'utf8')) });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(`::error::${rel} is not readable JSON: ${error.message}`);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const empty = assertNonEmptyCorpus(manifests.length, repoRoot, depths);
|
|
189
|
+
if (empty) {
|
|
190
|
+
console.error(`::error::${empty}`);
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const offenders = findVendoredDeps(manifests);
|
|
195
|
+
if (offenders.length > 0) {
|
|
196
|
+
console.error('Vendored upstream deps detected (must use the registry version):');
|
|
197
|
+
for (const o of offenders) {
|
|
198
|
+
console.error(` ${o.path} ${o.field}.${o.depName} = ${o.depRange}`);
|
|
199
|
+
}
|
|
200
|
+
console.error(
|
|
201
|
+
'\nA `workspace:` range for a package this repo does not own resolves to a local ' +
|
|
202
|
+
'directory instead of the registry version the rest of the fleet installs.',
|
|
203
|
+
);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(
|
|
208
|
+
`OK: no vendored upstream workspace deps (${manifests.length} local packages scanned ` +
|
|
209
|
+
`at depth ${depths.join(', ')})`,
|
|
210
|
+
);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The sibling `xema-check-workspace-ranges` documents why this guard is written
|
|
215
|
+
// with `realpathSync`: invoked through a pnpm bin shim, `process.argv[1]` is the
|
|
216
|
+
// SYMLINK, so a naive `import.meta.url === pathToFileURL(argv[1]).href` is false
|
|
217
|
+
// and the bin prints nothing and exits 0. That hole shipped, and it was
|
|
218
|
+
// invisible in carved CI — which installs the published tarball, where argv[1]
|
|
219
|
+
// is already a real path. Same shape, same treatment.
|
|
220
|
+
const invokedPath = process.argv[1];
|
|
221
|
+
let invokedRealPath = null;
|
|
222
|
+
if (typeof invokedPath === 'string' && invokedPath.length > 0) {
|
|
223
|
+
try {
|
|
224
|
+
invokedRealPath = realpathSync(invokedPath);
|
|
225
|
+
} catch {
|
|
226
|
+
invokedRealPath = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (invokedRealPath !== null && import.meta.url === pathToFileURL(invokedRealPath).href) {
|
|
230
|
+
process.exit(main());
|
|
231
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { deepStrictEqual, strictEqual } from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { findVendoredDeps, manifestDepthsFor } from './check-no-vendored-deps.mjs';
|
|
5
|
+
|
|
6
|
+
// ── manifestDepthsFor ─────────────────────────────────────────────────────
|
|
7
|
+
// The depth is `find`'s: levels BELOW the `packages` start path. It equals the
|
|
8
|
+
// glob's segment count. Both cases below are pinned because the first draft
|
|
9
|
+
// used `length + 1` and PASSED on all four real repositories — the two entries
|
|
10
|
+
// were each off by one and their ranges overlapped on a tree whose packages all
|
|
11
|
+
// sit at one level. Matching counts is exactly what made the bug invisible.
|
|
12
|
+
|
|
13
|
+
test('a one-level glob admits packages/<name>/package.json at depth 2', () => {
|
|
14
|
+
deepStrictEqual(manifestDepthsFor("packages:\n - 'packages/*'\n"), [2]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('a two-level glob admits packages/<a>/<b>/package.json at depth 3', () => {
|
|
18
|
+
deepStrictEqual(manifestDepthsFor("packages:\n - 'packages/*/*'\n"), [3]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('both globs together admit both depths, sorted', () => {
|
|
22
|
+
deepStrictEqual(
|
|
23
|
+
manifestDepthsFor("packages:\n - 'packages/*'\n - 'packages/*/*'\n"),
|
|
24
|
+
[2, 3],
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('a repo declaring no packages/ glob returns null, never an empty scan', () => {
|
|
29
|
+
// null is "not shaped for this check", which the caller REFUSES. Returning []
|
|
30
|
+
// would let it scan nothing and report OK — the defect this file replaces.
|
|
31
|
+
strictEqual(manifestDepthsFor("packages:\n - 'apps/*'\n"), null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('unquoted and double-quoted globs parse identically', () => {
|
|
35
|
+
deepStrictEqual(manifestDepthsFor('packages:\n - packages/*\n'), [2]);
|
|
36
|
+
deepStrictEqual(manifestDepthsFor('packages:\n - "packages/*/*"\n'), [3]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ── findVendoredDeps ──────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const local = (name, deps) => ({ path: `packages/${name}/package.json`, manifest: { name, ...deps } });
|
|
42
|
+
|
|
43
|
+
test('a workspace: range for a package this repo OWNS is fine', () => {
|
|
44
|
+
const found = findVendoredDeps([
|
|
45
|
+
local('@x/a', { dependencies: { '@x/b': 'workspace:*' } }),
|
|
46
|
+
local('@x/b', {}),
|
|
47
|
+
]);
|
|
48
|
+
deepStrictEqual(found, []);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a workspace: range for a package this repo does NOT own is a finding', () => {
|
|
52
|
+
const found = findVendoredDeps([
|
|
53
|
+
local('@x/a', { dependencies: { '@upstream/c': 'workspace:*' } }),
|
|
54
|
+
]);
|
|
55
|
+
strictEqual(found.length, 1);
|
|
56
|
+
strictEqual(found[0].depName, '@upstream/c');
|
|
57
|
+
strictEqual(found[0].field, 'dependencies');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('all three dependency fields are examined', () => {
|
|
61
|
+
const found = findVendoredDeps([
|
|
62
|
+
local('@x/a', {
|
|
63
|
+
dependencies: { '@u/one': 'workspace:*' },
|
|
64
|
+
devDependencies: { '@u/two': 'workspace:^1.0.0' },
|
|
65
|
+
peerDependencies: { '@u/three': 'workspace:~2.0.0' },
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
deepStrictEqual(found.map((f) => f.field).sort(), [
|
|
69
|
+
'dependencies', 'devDependencies', 'peerDependencies',
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a registry range is never a finding, whatever it names', () => {
|
|
74
|
+
deepStrictEqual(
|
|
75
|
+
findVendoredDeps([local('@x/a', { dependencies: { '@upstream/c': '^1.2.3' } })]),
|
|
76
|
+
[],
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a non-string range is skipped rather than crashing the run', () => {
|
|
81
|
+
deepStrictEqual(
|
|
82
|
+
findVendoredDeps([local('@x/a', { dependencies: { '@u/c': null } })]),
|
|
83
|
+
[],
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a fixture ingested as a local package WIDENS the allowed set', () => {
|
|
88
|
+
// The measured xema-kernel-contracts defect, pinned as behaviour rather than
|
|
89
|
+
// as prose: its unbounded walk ingested two test fixtures under
|
|
90
|
+
// packages/*/*/test/fixtures/, and a fixture whose name matches a declared
|
|
91
|
+
// dependency makes a real finding disappear. The depth bound is what stops
|
|
92
|
+
// it; this asserts the CONSEQUENCE so the bound cannot be relaxed silently.
|
|
93
|
+
const withoutFixture = findVendoredDeps([
|
|
94
|
+
local('@x/a', { dependencies: { '@u/fixture': 'workspace:*' } }),
|
|
95
|
+
]);
|
|
96
|
+
strictEqual(withoutFixture.length, 1, 'the finding exists when the fixture is out of scope');
|
|
97
|
+
|
|
98
|
+
const withFixture = findVendoredDeps([
|
|
99
|
+
local('@x/a', { dependencies: { '@u/fixture': 'workspace:*' } }),
|
|
100
|
+
{ path: 'packages/a/test/fixtures/f/package.json', manifest: { name: '@u/fixture' } },
|
|
101
|
+
]);
|
|
102
|
+
strictEqual(withFixture.length, 0, 'ingesting the fixture SILENCES it — the widening');
|
|
103
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole check rests on ONE predicate — `satisfies` — so that is what these
|
|
3
|
+
* tests attack.
|
|
4
|
+
*
|
|
5
|
+
* The failure this guard exists to prevent is subtle in exactly one place: npm's
|
|
6
|
+
* `^0.y.z` rule pins the MINOR, not the major. A `satisfies` that treats `^` as
|
|
7
|
+
* "same major" uniformly would call `0.3.0` a match for `^0.2.0` and report a
|
|
8
|
+
* clean scan over all 58 real violations — a check that reports green while
|
|
9
|
+
* examining everything and concluding nothing. Every 0.x case below is aimed at
|
|
10
|
+
* that specific wrong implementation.
|
|
11
|
+
*/
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import test from 'node:test';
|
|
14
|
+
|
|
15
|
+
import { satisfies } from './check-workspace-range-matches-local.mjs';
|
|
16
|
+
|
|
17
|
+
test('^0.y.z pins the MINOR — the rule the whole check turns on', () => {
|
|
18
|
+
// The real defect: a client moved 0.2.0 -> 0.3.0 and fell out of every
|
|
19
|
+
// consumer's range, silently dropping the build edge.
|
|
20
|
+
assert.equal(satisfies('0.3.0', '^0.2.0'), false);
|
|
21
|
+
assert.equal(satisfies('0.2.9', '^0.2.0'), true);
|
|
22
|
+
assert.equal(satisfies('0.2.0', '^0.2.1'), false); // below the floor
|
|
23
|
+
assert.equal(satisfies('1.0.0', '^0.2.0'), false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('^x.y.z (x > 0) pins the MAJOR', () => {
|
|
27
|
+
assert.equal(satisfies('1.9.3', '^1.2.0'), true);
|
|
28
|
+
assert.equal(satisfies('2.0.0', '^1.2.0'), false);
|
|
29
|
+
assert.equal(satisfies('1.1.0', '^1.2.0'), false); // below the floor
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('~ pins the minor at every major', () => {
|
|
33
|
+
assert.equal(satisfies('1.2.9', '~1.2.0'), true);
|
|
34
|
+
assert.equal(satisfies('1.3.0', '~1.2.0'), false);
|
|
35
|
+
assert.equal(satisfies('0.2.9', '~0.2.0'), true);
|
|
36
|
+
assert.equal(satisfies('0.3.0', '~0.2.0'), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('>= is a floor with no ceiling', () => {
|
|
40
|
+
assert.equal(satisfies('7.5.0', '>=0.14.0'), true);
|
|
41
|
+
assert.equal(satisfies('0.13.0', '>=0.14.0'), false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('an exact range means exactly that version', () => {
|
|
45
|
+
assert.equal(satisfies('1.2.3', '1.2.3'), true);
|
|
46
|
+
assert.equal(satisfies('1.2.4', '1.2.3'), false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a prerelease is compared on its release part, never string-wise', () => {
|
|
50
|
+
// `0.3.0-rc.1` must not read as "less than 0.3.0 therefore outside ^0.2.0
|
|
51
|
+
// is fine" — the range check has to reach the same verdict as `0.3.0`.
|
|
52
|
+
assert.equal(satisfies('0.3.0-rc.1', '^0.2.0'), false);
|
|
53
|
+
assert.equal(satisfies('0.2.5-rc.1', '^0.2.0'), true);
|
|
54
|
+
});
|