openvibers/openvibe.shared Commits
docs-status reads a workspace root's packages (npm/pnpm workspaces, else apps/* and packages/*) and every library a STATUS field names, so pin rounds keep Games, Tools and Examples current (#130)
Changes
@@@ -53,11 +53,58 @@ function pinsIn(pkg) {return out;}--/** The root package.json's pins, or {} when it is absent or unreadable. */++/**++ * The workspace packages' directories: the root package.json's `workspaces` (an array, or { packages }) and++ * pnpm-workspace.yaml's `packages`, with a trailing `/*` read one level deep. Negations and `**` are skipped.++ */++function workspaceDirs(dir) {++ const patterns = [];++ const root = readJson(path.join(dir, 'package.json'));++ const ws = root && (Array.isArray(root.workspaces) ? root.workspaces : root.workspaces && root.workspaces.packages);++ if (Array.isArray(ws)) patterns.push(...ws);++ const pnpm = path.join(dir, 'pnpm-workspace.yaml');++ if (fs.existsSync(pnpm)) {++ for (const m of fs.readFileSync(pnpm, 'utf8').matchAll(/^\s*-\s*['"]?([^'"\n#]+?)['"]?\s*$/gm)) patterns.push(m[1]);++ }++ // No declared workspaces (Tools keeps one package per app): the conventional folders.++ if (!patterns.length) patterns.push('apps/*', 'packages/*');++ const out = [];++ for (const pattern of patterns) {++ if (typeof pattern !== 'string' || pattern.startsWith('!') || pattern.includes('**')) continue;++ if (pattern.endsWith('/*')) {++ const base = path.join(dir, pattern.slice(0, -2));++ try {++ for (const e of fs.readdirSync(base, { withFileTypes: true })) if (e.isDirectory()) out.push(path.join(base, e.name));++ } catch { /* no such folder */ }++ } else out.push(path.join(dir, pattern));++ }++ return out;++}++++const newer = (a, b) => {++ const x = a.split('.').map(Number), y = b.split('.').map(Number);++ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];++ return false;++};++++/**++ * The repository's pins: its workspace packages' (the newest of each library, since a pin round moves them together)++ * with the root package.json's own over them. A workspace root that pins nothing itself (Games, Tools, Examples) used++ * to read as "pins nothing", so its STATUS.json kept naming old releases after every pin round.++ */function pinsFor(dir) {-- const file = path.join(dir, 'package.json');-- if (!fs.existsSync(file)) return {};-- try { return pinsIn(JSON.parse(fs.readFileSync(file, 'utf8'))); } catch { return {}; }++ const out = {};++ for (const d of workspaceDirs(dir)) {++ for (const [name, version] of Object.entries(pinsIn(readJson(path.join(d, 'package.json'))))) {++ if (!out[name] || newer(version, out[name])) out[name] = version;++ }++ }++ return Object.assign(out, pinsIn(readJson(path.join(dir, 'package.json'))));++}++++/** Every "openvibe-<lib> vX.Y.Z" a STATUS string names, moved to that library's pin (one field may name several). */++function withNamedPins(text, pins) {++ return String(text).replace(/\b(openvibe-[a-z]+) v(\d+\.\d+\.\d+)/g, (all, name) => (pins[name] ? `${name} v${pins[name]}` : all));}/** The status value for a pin, keeping prose after the version: "openvibe-contracts v0.61.0 (devDependency…)". */@@@ -67,6 +114,12 @@ function statusValue(existing, name, version) {return existing.replace(VERSION, version);}++/** Whether another tracked STATUS field names this library ("…, openvibe-sdk v0.65.0, …" in `contracts`). */++function namedElsewhere(status, lib) {++ const re = new RegExp(`\\b${escapeRe(lib.pkg)} v\\d+\\.\\d+\\.\\d+`);++ return LIBS.some((l) => l.field && l.field !== lib.field && typeof status[l.field] === 'string' && re.test(status[l.field]));++}++/** STATUS.json findings for every tracked pin. */function statusFindings(status, pins) {const out = [];@@@ -74,6 +127,7 @@ function statusFindings(status, pins) {if (!lib.field || !(lib.pkg in pins)) continue;const want = pins[lib.pkg];const existing = status[lib.field];++ if (existing === undefined && namedElsewhere(status, lib)) continue; // checked with the field that names itif (typeof existing !== 'string' || !VERSION.test(existing)) {out.push({ ok: false, file: 'STATUS.json', field: lib.field, reason: `pins ${lib.pkg} v${want} but STATUS.json has no ${lib.field} version` });continue;@@@ -82,6 +136,15 @@ function statusFindings(status, pins) {if (have !== want) out.push({ ok: false, file: 'STATUS.json', field: lib.field, reason: `pins ${lib.pkg} v${want} but STATUS.json ${lib.field} says v${have}` });else out.push({ ok: true, file: 'STATUS.json', field: lib.field, reason: `${lib.pkg} v${want}` });}++ // Other libraries a tracked field names ("openvibe-contracts v0.214.0, openvibe-sdk v0.65.0, …" in one field).++ for (const field of LIBS.filter((l) => l.field).map((l) => l.field)) {++ if (typeof status[field] !== 'string') continue;++ for (const [, name, have] of status[field].matchAll(/\b(openvibe-[a-z]+) v(\d+\.\d+\.\d+)/g)) {++ if (pins[name] && pins[name] !== have && !(byPackage.get(name) && byPackage.get(name).field === field)) {++ out.push({ ok: false, file: 'STATUS.json', field, reason: `pins ${name} v${pins[name]} but STATUS.json ${field} names v${have}` });++ }++ }++ }return out;}@@@ -151,8 +214,15 @@ function withStatusFields(status, pins, today) {const tracked = LIBS.filter((l) => l.field).map((l) => l.field);const updates = {};for (const lib of LIBS) {-- if (lib.field && pins[lib.pkg]) updates[lib.field] = statusValue(status[lib.field], lib.pkg, pins[lib.pkg]);++ if (lib.field && pins[lib.pkg] && !(status[lib.field] === undefined && namedElsewhere(status, lib))) {++ updates[lib.field] = statusValue(status[lib.field], lib.pkg, pins[lib.pkg]);++ }++ }++ for (const field of tracked) {++ const value = updates[field] !== undefined ? updates[field] : status[field];++ if (typeof value === 'string') updates[field] = withNamedPins(value, pins);}++ for (const field of tracked) if (status[field] === undefined && updates[field] === undefined) delete updates[field];const out = {};let placed = false;for (const [key, value] of Object.entries(status)) {@@@ -203,7 +273,7 @@ function main(dir = '.', { check = false } = {}) {return { ...inspect(dir), changed };}--module.exports = { LIBS, START, END, pinsIn, pinsFor, statusValue, statusFindings, versionsInner, versionsBlock, applyBlock, readmeFindings, inspect, withStatusFields, update, main };++module.exports = { LIBS, START, END, pinsIn, pinsFor, workspaceDirs, withNamedPins, statusValue, statusFindings, versionsInner, versionsBlock, applyBlock, readmeFindings, inspect, withStatusFields, update, main };if (require.main === module || !module.parent) {const args = process.argv.slice(2);@@@ -150,4 +150,49 @@ assert.strictEqual(applyBlock('a\n\n<!-- versions:start -->\n- openvibe-sdk: v0.assert.deepStrictEqual(update(dir, { today: '2026-10-10' }), [], 'a rerun writes nothing');fs.rmSync(dir, { recursive: true, force: true });}++++// A workspace root that pins nothing itself: its packages' pins are the repository's (Games' apps/server, Tools'++// apps/*, Examples' examples/*), and one STATUS field may name several libraries (2026-10-27: three repositories kept++// naming openvibe-contracts v0.214.0 after the v0.214.1 pin round because the root read as "pins nothing").++{++ const pkg = (deps) => JSON.stringify({ name: 'x', private: true, dependencies: deps });++ // npm workspaces (Examples): examples/* packages; the newest pin of a library wins.++ const npmRoot = tmp('ws-npm');++ write(npmRoot, 'package.json', JSON.stringify({ name: 'root', private: true, workspaces: ['examples/*'] }));++ for (const [name, v] of [['a', 'v0.214.1'], ['b', 'v0.214.0']]) {++ fs.mkdirSync(path.join(npmRoot, 'examples', name), { recursive: true });++ write(path.join(npmRoot, 'examples', name), 'package.json', pkg({ 'openvibe-contracts': url('OpenVibe.Contracts', v) }));++ }++ assert.deepStrictEqual(pinsFor(npmRoot), { 'openvibe-contracts': '0.214.1' });++ // pnpm (Games): pnpm-workspace.yaml; a composite `contracts` field names every library and the sdk/shared fields++ // are not demanded beside it; update moves each named version and keeps the prose.++ const pnpmRoot = tmp('ws-pnpm');++ write(pnpmRoot, 'package.json', JSON.stringify({ name: 'games', private: true }));++ write(pnpmRoot, 'pnpm-workspace.yaml', "packages:\n - 'apps/*'\n - 'packages/*'\n");++ fs.mkdirSync(path.join(pnpmRoot, 'apps', 'server'), { recursive: true });++ write(path.join(pnpmRoot, 'apps', 'server'), 'package.json', pkg({++ 'openvibe-contracts': url('OpenVibe.Contracts', 'v0.214.1'), 'openvibe-sdk': url('OpenVibe.SDK', 'v0.65.0'), 'openvibe-shared': url('OpenVibe.Shared', 'v4.15.2'),++ }));++ write(pnpmRoot, 'STATUS.json', JSON.stringify({ repository: 'OpenVibers/OpenVibe.Games',++ contracts: 'openvibe-contracts v0.214.0, openvibe-sdk v0.62.0, openvibe-shared v4.9.0 (apps/server/package.json)', updated: '2026-10-01' }, null, 2) + '\n');++ const before = inspect(pnpmRoot).findings.filter((f) => !f.ok).map((f) => f.reason);++ assert.ok(before.some((r) => /contracts says v0\.214\.0/.test(r)) && before.some((r) => /names v0\.62\.0/.test(r)) && before.some((r) => /names v4\.9\.0/.test(r)), before.join(' | '));++ assert.ok(!before.some((r) => /has no (sdk|shared) version/.test(r)), 'a library named in the contracts field is not missing its own field');++ update(pnpmRoot, { today: '2026-10-27' });++ const st = JSON.parse(read(pnpmRoot, 'STATUS.json'));++ assert.strictEqual(st.contracts, 'openvibe-contracts v0.214.1, openvibe-sdk v0.65.0, openvibe-shared v4.15.2 (apps/server/package.json)');++ assert.ok(!('sdk' in st) && !('shared' in st), 'no duplicate fields added');++ assert.strictEqual(st.updated, '2026-10-27');++ assert.deepStrictEqual(inspect(pnpmRoot).findings.filter((f) => !f.ok), []);++ // No declared workspaces (Tools): the conventional apps/* folders.++ const appsRoot = tmp('ws-apps');++ write(appsRoot, 'package.json', JSON.stringify({ name: 'tools', private: true }));++ fs.mkdirSync(path.join(appsRoot, 'apps', 'gateway'), { recursive: true });++ write(path.join(appsRoot, 'apps', 'gateway'), 'package.json', pkg({ 'openvibe-shared': url('OpenVibe.Shared', 'v4.15.2') }));++ assert.deepStrictEqual(pinsFor(appsRoot), { 'openvibe-shared': '4.15.2' });++ // The root's own pins still win over its packages'.++ write(appsRoot, 'package.json', JSON.stringify({ name: 'tools', private: true, dependencies: { 'openvibe-shared': url('OpenVibe.Shared', 'v4.15.1') } }));++ assert.deepStrictEqual(pinsFor(appsRoot), { 'openvibe-shared': '4.15.1' });++ for (const d of [npmRoot, pnpmRoot, appsRoot]) fs.rmSync(d, { recursive: true, force: true });++}console.log('docs-status: all checks passed');
@@ -53,11 +53,58 @@ function pinsIn(pkg) {
@@ -53,11 +53,58 @@ function pinsIn(pkg) {
return out;
return out;
}
}
-/** The root package.json's pins, or {} when it is absent or unreadable. */
+/**
+ * The workspace packages' directories: the root package.json's `workspaces` (an array, or { packages }) and
+ * pnpm-workspace.yaml's `packages`, with a trailing `/*` read one level deep. Negations and `**` are skipped.
+ */
+function workspaceDirs(dir) {
+ const patterns = [];
+ const root = readJson(path.join(dir, 'package.json'));
+ const ws = root && (Array.isArray(root.workspaces) ? root.workspaces : root.workspaces && root.workspaces.packages);
+ if (Array.isArray(ws)) patterns.push(...ws);
+ const pnpm = path.join(dir, 'pnpm-workspace.yaml');
+ if (fs.existsSync(pnpm)) {
+ for (const m of fs.readFileSync(pnpm, 'utf8').matchAll(/^\s*-\s*['"]?([^'"\n#]+?)['"]?\s*$/gm)) patterns.push(m[1]);
+ }
+ // No declared workspaces (Tools keeps one package per app): the conventional folders.
+ if (!patterns.length) patterns.push('apps/*', 'packages/*');
+ const out = [];
+ for (const pattern of patterns) {
+ if (typeof pattern !== 'string' || pattern.startsWith('!') || pattern.includes('**')) continue;
+ if (pattern.endsWith('/*')) {
+ const base = path.join(dir, pattern.slice(0, -2));
+ try {
+ for (const e of fs.readdirSync(base, { withFileTypes: true })) if (e.isDirectory()) out.push(path.join(base, e.name));
+ } catch { /* no such folder */ }
+ } else out.push(path.join(dir, pattern));
+ }
+ return out;
+}
+
+const newer = (a, b) => {
+ const x = a.split('.').map(Number), y = b.split('.').map(Number);
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
+ return false;
+};
+
+/**
+ * The repository's pins: its workspace packages' (the newest of each library, since a pin round moves them together)
+ * with the root package.json's own over them. A workspace root that pins nothing itself (Games, Tools, Examples) used
+ * to read as "pins nothing", so its STATUS.json kept naming old releases after every pin round.
+ */
function pinsFor(dir) {
function pinsFor(dir) {
- const file = path.join(dir, 'package.json');
- if (!fs.existsSync(file)) return {};
- try { return pinsIn(JSON.parse(fs.readFileSync(file, 'utf8'))); } catch { return {}; }
+ const out = {};
+ for (const d of workspaceDirs(dir)) {
+ for (const [name, version] of Object.entries(pinsIn(readJson(path.join(d, 'package.json'))))) {
+ if (!out[name] || newer(version, out[name])) out[name] = version;
+ }
+ }
+ return Object.assign(out, pinsIn(readJson(path.join(dir, 'package.json'))));
+}
+
+/** Every "openvibe-<lib> vX.Y.Z" a STATUS string names, moved to that library's pin (one field may name several). */
+function withNamedPins(text, pins) {
+ return String(text).replace(/\b(openvibe-[a-z]+) v(\d+\.\d+\.\d+)/g, (all, name) => (pins[name] ? `${name} v${pins[name]}` : all));
}
}
/** The status value for a pin, keeping prose after the version: "openvibe-contracts v0.61.0 (devDependency…)". */
/** The status value for a pin, keeping prose after the version: "openvibe-contracts v0.61.0 (devDependency…)". */
@@ -67,6 +114,12 @@ function statusValue(existing, name, version) {
@@ -67,6 +114,12 @@ function statusValue(existing, name, version) {
return existing.replace(VERSION, version);
return existing.replace(VERSION, version);
}
}
+/** Whether another tracked STATUS field names this library ("…, openvibe-sdk v0.65.0, …" in `contracts`). */
+function namedElsewhere(status, lib) {
+ const re = new RegExp(`\\b${escapeRe(lib.pkg)} v\\d+\\.\\d+\\.\\d+`);
+ return LIBS.some((l) => l.field && l.field !== lib.field && typeof status[l.field] === 'string' && re.test(status[l.field]));
+}
+
/** STATUS.json findings for every tracked pin. */
/** STATUS.json findings for every tracked pin. */
function statusFindings(status, pins) {
function statusFindings(status, pins) {
const out = [];
const out = [];
@@ -74,6 +127,7 @@ function statusFindings(status, pins) {
@@ -74,6 +127,7 @@ function statusFindings(status, pins) {
if (!lib.field || !(lib.pkg in pins)) continue;
if (!lib.field || !(lib.pkg in pins)) continue;
const want = pins[lib.pkg];
const want = pins[lib.pkg];
const existing = status[lib.field];
const existing = status[lib.field];
+ if (existing === undefined && namedElsewhere(status, lib)) continue; // checked with the field that names it
if (typeof existing !== 'string' || !VERSION.test(existing)) {
if (typeof existing !== 'string' || !VERSION.test(existing)) {
out.push({ ok: false, file: 'STATUS.json', field: lib.field, reason: `pins ${lib.pkg} v${want} but STATUS.json has no ${lib.field} version` });
out.push({ ok: false, file: 'STATUS.json', field: lib.field, reason: `pins ${lib.pkg} v${want} but STATUS.json has no ${lib.field} version` });
continue;
continue;
@@ -82,6 +136,15 @@ function statusFindings(status, pins) {
@@ -82,6 +136,15 @@ function statusFindings(status, pins) {
if (have !== want) out.push({ ok: false, file: 'STATUS.json', field: lib.field, reason: `pins ${lib.pkg} v${want} but STATUS.json ${lib.field} says v${have}` });
if (have !== want) out.push({ ok: false, file: 'STATUS.json', field: lib.field, reason: `pins ${lib.pkg} v${want} but STATUS.json ${lib.field} says v${have}` });
else out.push({ ok: true, file: 'STATUS.json', field: lib.field, reason: `${lib.pkg} v${want}` });
else out.push({ ok: true, file: 'STATUS.json', field: lib.field, reason: `${lib.pkg} v${want}` });
}
}
+ // Other libraries a tracked field names ("openvibe-contracts v0.214.0, openvibe-sdk v0.65.0, …" in one field).
+ for (const field of LIBS.filter((l) => l.field).map((l) => l.field)) {
+ if (typeof status[field] !== 'string') continue;
+ for (const [, name, have] of status[field].matchAll(/\b(openvibe-[a-z]+) v(\d+\.\d+\.\d+)/g)) {
+ if (pins[name] && pins[name] !== have && !(byPackage.get(name) && byPackage.get(name).field === field)) {
+ out.push({ ok: false, file: 'STATUS.json', field, reason: `pins ${name} v${pins[name]} but STATUS.json ${field} names v${have}` });
+ }
+ }
+ }
return out;
return out;
}
}
@@ -151,8 +214,15 @@ function withStatusFields(status, pins, today) {
@@ -151,8 +214,15 @@ function withStatusFields(status, pins, today) {
const tracked = LIBS.filter((l) => l.field).map((l) => l.field);
const tracked = LIBS.filter((l) => l.field).map((l) => l.field);
const updates = {};
const updates = {};
for (const lib of LIBS) {
for (const lib of LIBS) {
- if (lib.field && pins[lib.pkg]) updates[lib.field] = statusValue(status[lib.field], lib.pkg, pins[lib.pkg]);
+ if (lib.field && pins[lib.pkg] && !(status[lib.field] === undefined && namedElsewhere(status, lib))) {
+ updates[lib.field] = statusValue(status[lib.field], lib.pkg, pins[lib.pkg]);
+ }
+ }
+ for (const field of tracked) {
+ const value = updates[field] !== undefined ? updates[field] : status[field];
+ if (typeof value === 'string') updates[field] = withNamedPins(value, pins);
}
}
+ for (const field of tracked) if (status[field] === undefined && updates[field] === undefined) delete updates[field];
const out = {};
const out = {};
let placed = false;
let placed = false;
for (const [key, value] of Object.entries(status)) {
for (const [key, value] of Object.entries(status)) {
@@ -203,7 +273,7 @@ function main(dir = '.', { check = false } = {}) {
@@ -203,7 +273,7 @@ function main(dir = '.', { check = false } = {}) {
return { ...inspect(dir), changed };
return { ...inspect(dir), changed };
}
}
-module.exports = { LIBS, START, END, pinsIn, pinsFor, statusValue, statusFindings, versionsInner, versionsBlock, applyBlock, readmeFindings, inspect, withStatusFields, update, main };
+module.exports = { LIBS, START, END, pinsIn, pinsFor, workspaceDirs, withNamedPins, statusValue, statusFindings, versionsInner, versionsBlock, applyBlock, readmeFindings, inspect, withStatusFields, update, main };
if (require.main === module || !module.parent) {
if (require.main === module || !module.parent) {
const args = process.argv.slice(2);
const args = process.argv.slice(2);
@@ -150,4 +150,49 @@ assert.strictEqual(applyBlock('a\n\n<!-- versions:start -->\n- openvibe-sdk: v0.
@@ -150,4 +150,49 @@ assert.strictEqual(applyBlock('a\n\n<!-- versions:start -->\n- openvibe-sdk: v0.
assert.deepStrictEqual(update(dir, { today: '2026-10-10' }), [], 'a rerun writes nothing');
assert.deepStrictEqual(update(dir, { today: '2026-10-10' }), [], 'a rerun writes nothing');
fs.rmSync(dir, { recursive: true, force: true });
fs.rmSync(dir, { recursive: true, force: true });
}
}
+
+// A workspace root that pins nothing itself: its packages' pins are the repository's (Games' apps/server, Tools'
+// apps/*, Examples' examples/*), and one STATUS field may name several libraries (2026-10-27: three repositories kept
+// naming openvibe-contracts v0.214.0 after the v0.214.1 pin round because the root read as "pins nothing").
+{
+ const pkg = (deps) => JSON.stringify({ name: 'x', private: true, dependencies: deps });
+ // npm workspaces (Examples): examples/* packages; the newest pin of a library wins.
+ const npmRoot = tmp('ws-npm');
+ write(npmRoot, 'package.json', JSON.stringify({ name: 'root', private: true, workspaces: ['examples/*'] }));
+ for (const [name, v] of [['a', 'v0.214.1'], ['b', 'v0.214.0']]) {
+ fs.mkdirSync(path.join(npmRoot, 'examples', name), { recursive: true });
+ write(path.join(npmRoot, 'examples', name), 'package.json', pkg({ 'openvibe-contracts': url('OpenVibe.Contracts', v) }));
+ }
+ assert.deepStrictEqual(pinsFor(npmRoot), { 'openvibe-contracts': '0.214.1' });
+ // pnpm (Games): pnpm-workspace.yaml; a composite `contracts` field names every library and the sdk/shared fields
+ // are not demanded beside it; update moves each named version and keeps the prose.
+ const pnpmRoot = tmp('ws-pnpm');
+ write(pnpmRoot, 'package.json', JSON.stringify({ name: 'games', private: true }));
+ write(pnpmRoot, 'pnpm-workspace.yaml', "packages:\n - 'apps/*'\n - 'packages/*'\n");
+ fs.mkdirSync(path.join(pnpmRoot, 'apps', 'server'), { recursive: true });
+ write(path.join(pnpmRoot, 'apps', 'server'), 'package.json', pkg({
+ 'openvibe-contracts': url('OpenVibe.Contracts', 'v0.214.1'), 'openvibe-sdk': url('OpenVibe.SDK', 'v0.65.0'), 'openvibe-shared': url('OpenVibe.Shared', 'v4.15.2'),
+ }));
+ write(pnpmRoot, 'STATUS.json', JSON.stringify({ repository: 'OpenVibers/OpenVibe.Games',
+ contracts: 'openvibe-contracts v0.214.0, openvibe-sdk v0.62.0, openvibe-shared v4.9.0 (apps/server/package.json)', updated: '2026-10-01' }, null, 2) + '\n');
+ const before = inspect(pnpmRoot).findings.filter((f) => !f.ok).map((f) => f.reason);
+ assert.ok(before.some((r) => /contracts says v0\.214\.0/.test(r)) && before.some((r) => /names v0\.62\.0/.test(r)) && before.some((r) => /names v4\.9\.0/.test(r)), before.join(' | '));
+ assert.ok(!before.some((r) => /has no (sdk|shared) version/.test(r)), 'a library named in the contracts field is not missing its own field');
+ update(pnpmRoot, { today: '2026-10-27' });
+ const st = JSON.parse(read(pnpmRoot, 'STATUS.json'));
+ assert.strictEqual(st.contracts, 'openvibe-contracts v0.214.1, openvibe-sdk v0.65.0, openvibe-shared v4.15.2 (apps/server/package.json)');
+ assert.ok(!('sdk' in st) && !('shared' in st), 'no duplicate fields added');
+ assert.strictEqual(st.updated, '2026-10-27');
+ assert.deepStrictEqual(inspect(pnpmRoot).findings.filter((f) => !f.ok), []);
+ // No declared workspaces (Tools): the conventional apps/* folders.
+ const appsRoot = tmp('ws-apps');
+ write(appsRoot, 'package.json', JSON.stringify({ name: 'tools', private: true }));
+ fs.mkdirSync(path.join(appsRoot, 'apps', 'gateway'), { recursive: true });
+ write(path.join(appsRoot, 'apps', 'gateway'), 'package.json', pkg({ 'openvibe-shared': url('OpenVibe.Shared', 'v4.15.2') }));
+ assert.deepStrictEqual(pinsFor(appsRoot), { 'openvibe-shared': '4.15.2' });
+ // The root's own pins still win over its packages'.
+ write(appsRoot, 'package.json', JSON.stringify({ name: 'tools', private: true, dependencies: { 'openvibe-shared': url('OpenVibe.Shared', 'v4.15.1') } }));
+ assert.deepStrictEqual(pinsFor(appsRoot), { 'openvibe-shared': '4.15.1' });
+ for (const d of [npmRoot, pnpmRoot, appsRoot]) fs.rmSync(d, { recursive: true, force: true });
+}
console.log('docs-status: all checks passed');
console.log('docs-status: all checks passed');