@proteinjs/reflection-build 1.4.6 → 1.4.8
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/CHANGELOG.md +11 -0
- package/dist/src/runWatch.js +3 -30
- package/dist/src/runWatch.js.map +1 -1
- package/dist/src/watch.d.ts +1 -1
- package/dist/src/watch.js +39 -28
- package/dist/src/watch.js.map +1 -1
- package/package.json +2 -2
- package/src/runWatch.ts +3 -8
- package/src/watch.ts +44 -35
- package/test/examples/source-repository/a/node_modules/.package-lock.json +6 -6
- package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/CHANGELOG.md +11 -0
- package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.d.ts +44 -1
- package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js +207 -142
- package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js.map +1 -1
- package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/package.json +2 -2
- package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/src/PackageUtil.ts +176 -102
- package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/test/PackageUtil.symlinkDependencies.test.ts +76 -0
- package/test/examples/source-repository/a/package-lock.json +8 -8
- package/test/examples/source-repository/a/package.json +2 -2
- package/test/examples/source-repository/b/node_modules/.package-lock.json +4 -4
- package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/CHANGELOG.md +11 -0
- package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.d.ts +44 -1
- package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js +207 -142
- package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js.map +1 -1
- package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/package.json +2 -2
- package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/src/PackageUtil.ts +176 -102
- package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/test/PackageUtil.symlinkDependencies.test.ts +76 -0
- package/test/examples/source-repository/b/package-lock.json +6 -6
- package/test/examples/source-repository/b/package.json +2 -2
package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/src/PackageUtil.ts
CHANGED
|
@@ -325,9 +325,20 @@ export class PackageUtil {
|
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
327
|
* Symlink the dependencies of `localPackage` to other local packages in the workspace.
|
|
328
|
+
*
|
|
329
|
+
* This links the package's full TRANSITIVE closure of workspace dependencies,
|
|
330
|
+
* not just its directly-declared ones. A package's `package.json` only lists
|
|
331
|
+
* its direct deps, but those deps pull in workspace packages of their own
|
|
332
|
+
* (e.g. `flow-server` declares `@n3xah/space-server`, which itself depends on
|
|
333
|
+
* `@n3xah/space-common`). Node resolves a transitive dep like `space-common`
|
|
334
|
+
* out of the consumer's own `node_modules` first, so if we only symlinked
|
|
335
|
+
* direct deps, npm would satisfy `space-common` with a stale registry copy
|
|
336
|
+
* that lags the live workspace source — causing schema/version drift. By
|
|
337
|
+
* linking the whole closure, every workspace package a package can reach at
|
|
338
|
+
* runtime resolves to the live source tree.
|
|
339
|
+
*
|
|
328
340
|
* @param localPackage package to symlink the dependencies of
|
|
329
341
|
* @param localPackageMap `LocalPackageMap` of the workspace
|
|
330
|
-
* @param logger optionally provide a logger to capture this method's logging
|
|
331
342
|
*/
|
|
332
343
|
static async symlinkDependencies(localPackage: LocalPackage, localPackageMap: LocalPackageMap) {
|
|
333
344
|
const packageDir = path.dirname(localPackage.filePath);
|
|
@@ -336,118 +347,181 @@ export class PackageUtil {
|
|
|
336
347
|
await Fs.createFolder(nodeModulesPath);
|
|
337
348
|
}
|
|
338
349
|
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
350
|
+
const transitiveWorkspaceDependencies = await PackageUtil.getTransitiveWorkspaceDependencies(
|
|
351
|
+
localPackage,
|
|
352
|
+
localPackageMap
|
|
353
|
+
);
|
|
354
|
+
for (const dependencyPackageName of transitiveWorkspaceDependencies) {
|
|
355
|
+
await PackageUtil.symlinkPackage(dependencyPackageName, nodeModulesPath, packageDir, localPackageMap);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
343
358
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Compute the transitive closure of workspace dependencies for `localPackage`.
|
|
361
|
+
*
|
|
362
|
+
* Reuses `getPackageDependencyGraph`, which already crawls `dependencies` and
|
|
363
|
+
* `devDependencies` (transitively, across the whole workspace) and adds an
|
|
364
|
+
* edge `consumer -> dependency` for each. So the workspace deps reachable from
|
|
365
|
+
* a package are exactly the nodes reachable by following `successors` from its
|
|
366
|
+
* node. We do a cycle-safe traversal (a `visited` set), since the dependency
|
|
367
|
+
* graph can contain cycles. The package itself is excluded from the result.
|
|
368
|
+
*
|
|
369
|
+
* Only names present in `localPackageMap` are returned — i.e. packages that
|
|
370
|
+
* actually live in the workspace and can be symlinked (the graph already
|
|
371
|
+
* filters to file:/relative/workspace deps in `addDependencies`, but we filter
|
|
372
|
+
* again here so the result is exactly the set of linkable packages).
|
|
373
|
+
*
|
|
374
|
+
* @returns workspace package names this package transitively depends on
|
|
375
|
+
*/
|
|
376
|
+
private static async getTransitiveWorkspaceDependencies(
|
|
377
|
+
localPackage: LocalPackage,
|
|
378
|
+
localPackageMap: LocalPackageMap
|
|
379
|
+
): Promise<string[]> {
|
|
380
|
+
const graph = await PackageUtil.getPackageDependencyGraph(localPackageMap);
|
|
381
|
+
const rootPackageName = localPackage.packageJson['name'];
|
|
382
|
+
|
|
383
|
+
const transitiveDependencies = new Set<string>();
|
|
384
|
+
const visited = new Set<string>([rootPackageName]);
|
|
385
|
+
const stack: string[] = [rootPackageName];
|
|
386
|
+
while (stack.length > 0) {
|
|
387
|
+
const current = stack.pop()!;
|
|
388
|
+
const successors = (graph.successors(current) as string[] | void) || [];
|
|
389
|
+
for (const dependencyPackageName of successors) {
|
|
390
|
+
if (visited.has(dependencyPackageName)) {
|
|
349
391
|
continue;
|
|
350
392
|
}
|
|
351
393
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
|
|
394
|
+
visited.add(dependencyPackageName);
|
|
395
|
+
stack.push(dependencyPackageName);
|
|
396
|
+
if (localPackageMap[dependencyPackageName]) {
|
|
397
|
+
transitiveDependencies.add(dependencyPackageName);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return Array.from(transitiveDependencies);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Symlink a single workspace package into `nodeModulesPath`, and create
|
|
407
|
+
* `node_modules/.bin/<name>` shims for any `bin` it declares.
|
|
408
|
+
*
|
|
409
|
+
* This is the per-dependency linking logic used by `symlinkDependencies` for
|
|
410
|
+
* each package in the transitive closure. The caller is responsible for
|
|
411
|
+
* deciding WHICH packages to link; this method just links the one named.
|
|
412
|
+
*
|
|
413
|
+
* @param dependencyPackageName name of the workspace package to link
|
|
414
|
+
* @param nodeModulesPath absolute path to the consumer's `node_modules`
|
|
415
|
+
* @param packageDir absolute path to the consumer package's directory (cwd for `ln`)
|
|
416
|
+
* @param localPackageMap `LocalPackageMap` of the workspace
|
|
417
|
+
*/
|
|
418
|
+
private static async symlinkPackage(
|
|
419
|
+
dependencyPackageName: string,
|
|
420
|
+
nodeModulesPath: string,
|
|
421
|
+
packageDir: string,
|
|
422
|
+
localPackageMap: LocalPackageMap
|
|
423
|
+
) {
|
|
424
|
+
const dependencyPath = localPackageMap[dependencyPackageName]?.filePath
|
|
425
|
+
? path.dirname(localPackageMap[dependencyPackageName].filePath)
|
|
426
|
+
: null;
|
|
427
|
+
if (!dependencyPath) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const symlinkPath = path.join(nodeModulesPath, dependencyPackageName);
|
|
432
|
+
const symlinkParent = path.dirname(symlinkPath);
|
|
433
|
+
if (!(await Fs.exists(symlinkParent))) {
|
|
434
|
+
await Fs.createFolder(symlinkParent);
|
|
435
|
+
}
|
|
436
|
+
// Clear out any existing entry at symlinkPath before creating the
|
|
437
|
+
// new symlink. Use `fs.lstat` rather than `Fs.exists` because
|
|
438
|
+
// `Fs.exists` is `fs.stat`-backed — which FOLLOWS symlinks and
|
|
439
|
+
// throws on a broken target, making broken symlinks invisible
|
|
440
|
+
// here. That's a real failure mode: if a prior run produced a
|
|
441
|
+
// symlink to a path that no longer exists (e.g. after the tree
|
|
442
|
+
// was moved, mounted elsewhere, or retargeted by tooling), the
|
|
443
|
+
// broken link survives the `Fs.exists` check, the delete is
|
|
444
|
+
// skipped, and `ln -s` then fails with "File exists".
|
|
445
|
+
try {
|
|
446
|
+
await fs.lstat(symlinkPath);
|
|
447
|
+
// Existing entry (symlink, file, or directory) — remove it.
|
|
448
|
+
// `deleteFolder` (fs-extra `remove`) handles all three.
|
|
449
|
+
await Fs.deleteFolder(symlinkPath);
|
|
450
|
+
} catch (e: any) {
|
|
451
|
+
if (e.code !== 'ENOENT') {
|
|
452
|
+
throw e;
|
|
453
|
+
}
|
|
454
|
+
// Nothing there — nothing to clean up.
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Use a RELATIVE link target so the workspace is portable across
|
|
458
|
+
// mount points (a developer's laptop, CI containers, a remote
|
|
459
|
+
// sandbox, etc.) without having to re-run `symlink-workspace` just
|
|
460
|
+
// because the absolute root path changed. `ln -s TARGET LINK`
|
|
461
|
+
// resolves TARGET relative to the directory containing the link —
|
|
462
|
+
// i.e. `symlinkParent` — so we compute the relative path from there.
|
|
463
|
+
const relativeDependencyPath = path.relative(symlinkParent, dependencyPath);
|
|
464
|
+
await cmd('ln', ['-s', relativeDependencyPath, symlinkPath], { cwd: packageDir });
|
|
465
|
+
|
|
466
|
+
// Create `.bin/<name>` shims for every bin the dependency declares.
|
|
467
|
+
//
|
|
468
|
+
// This is normally npm's job: `npm install` creates shims at
|
|
469
|
+
// `node_modules/.bin/<name>` pointing into the installed package so
|
|
470
|
+
// lifecycle scripts like `npm run watch` (where npm prepends
|
|
471
|
+
// `./node_modules/.bin` to PATH) can find them. `symlink-workspace`
|
|
472
|
+
// bypasses `npm install`, so without this loop the shims only exist
|
|
473
|
+
// if the user happened to run `npm install` at some point and they
|
|
474
|
+
// survive. They don't survive a broken-symlink sweep, a fresh
|
|
475
|
+
// checkout, or a move to a new host — so we create them ourselves.
|
|
476
|
+
//
|
|
477
|
+
// Also chmod +x the bin target: tsc output doesn't preserve the
|
|
478
|
+
// execute bit that `npm install` sets from the published tarball.
|
|
479
|
+
const depPackageJson = JSON.parse(await Fs.readFile(localPackageMap[dependencyPackageName].filePath));
|
|
480
|
+
const bin = depPackageJson.bin;
|
|
481
|
+
const binEntries: Array<{ name: string; relPath: string }> = [];
|
|
482
|
+
if (bin && typeof bin === 'object') {
|
|
483
|
+
for (const binName in bin) {
|
|
484
|
+
binEntries.push({ name: binName, relPath: bin[binName] });
|
|
485
|
+
}
|
|
486
|
+
} else if (bin && typeof bin === 'string') {
|
|
487
|
+
// Shorthand: `"bin": "./path"` — the exposed name is the
|
|
488
|
+
// package's bare name (scope stripped). Matches npm behavior.
|
|
489
|
+
const bareName = dependencyPackageName.includes('/')
|
|
490
|
+
? dependencyPackageName.split('/').pop()!
|
|
491
|
+
: dependencyPackageName;
|
|
492
|
+
binEntries.push({ name: bareName, relPath: bin });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (binEntries.length > 0) {
|
|
496
|
+
const dotBinDir = path.join(nodeModulesPath, '.bin');
|
|
497
|
+
if (!(await Fs.exists(dotBinDir))) {
|
|
498
|
+
await Fs.createFolder(dotBinDir);
|
|
499
|
+
}
|
|
500
|
+
for (const { name, relPath } of binEntries) {
|
|
501
|
+
const binFilePath = path.resolve(dependencyPath, relPath);
|
|
502
|
+
if (await Fs.exists(binFilePath)) {
|
|
503
|
+
await fs.chmod(binFilePath, 0o755);
|
|
356
504
|
}
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// here. That's a real failure mode: if a prior run produced a
|
|
362
|
-
// symlink to a path that no longer exists (e.g. after the tree
|
|
363
|
-
// was moved, mounted elsewhere, or retargeted by tooling), the
|
|
364
|
-
// broken link survives the `Fs.exists` check, the delete is
|
|
365
|
-
// skipped, and `ln -s` then fails with "File exists".
|
|
505
|
+
const shimPath = path.join(dotBinDir, name);
|
|
506
|
+
// Same lstat-based cleanup as for the dep symlink — broken
|
|
507
|
+
// shims from a prior run in a different environment would
|
|
508
|
+
// otherwise make `ln -s` fail with "File exists".
|
|
366
509
|
try {
|
|
367
|
-
await fs.lstat(
|
|
368
|
-
|
|
369
|
-
// `deleteFolder` (fs-extra `remove`) handles all three.
|
|
370
|
-
await Fs.deleteFolder(symlinkPath);
|
|
510
|
+
await fs.lstat(shimPath);
|
|
511
|
+
await Fs.deleteFolder(shimPath);
|
|
371
512
|
} catch (e: any) {
|
|
372
513
|
if (e.code !== 'ENOENT') {
|
|
373
514
|
throw e;
|
|
374
515
|
}
|
|
375
|
-
// Nothing there — nothing to clean up.
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Use a RELATIVE link target so the workspace is portable across
|
|
379
|
-
// mount points (a developer's laptop, CI containers, a remote
|
|
380
|
-
// sandbox, etc.) without having to re-run `symlink-workspace` just
|
|
381
|
-
// because the absolute root path changed. `ln -s TARGET LINK`
|
|
382
|
-
// resolves TARGET relative to the directory containing the link —
|
|
383
|
-
// i.e. `symlinkParent` — so we compute the relative path from there.
|
|
384
|
-
const relativeDependencyPath = path.relative(symlinkParent, dependencyPath);
|
|
385
|
-
await cmd('ln', ['-s', relativeDependencyPath, symlinkPath], { cwd: packageDir });
|
|
386
|
-
|
|
387
|
-
// Create `.bin/<name>` shims for every bin the dependency declares.
|
|
388
|
-
//
|
|
389
|
-
// This is normally npm's job: `npm install` creates shims at
|
|
390
|
-
// `node_modules/.bin/<name>` pointing into the installed package so
|
|
391
|
-
// lifecycle scripts like `npm run watch` (where npm prepends
|
|
392
|
-
// `./node_modules/.bin` to PATH) can find them. `symlink-workspace`
|
|
393
|
-
// bypasses `npm install`, so without this loop the shims only exist
|
|
394
|
-
// if the user happened to run `npm install` at some point and they
|
|
395
|
-
// survive. They don't survive a broken-symlink sweep, a fresh
|
|
396
|
-
// checkout, or a move to a new host — so we create them ourselves.
|
|
397
|
-
//
|
|
398
|
-
// Also chmod +x the bin target: tsc output doesn't preserve the
|
|
399
|
-
// execute bit that `npm install` sets from the published tarball.
|
|
400
|
-
const depPackageJson = JSON.parse(await Fs.readFile(localPackageMap[dependencyPackageName].filePath));
|
|
401
|
-
const bin = depPackageJson.bin;
|
|
402
|
-
const binEntries: Array<{ name: string; relPath: string }> = [];
|
|
403
|
-
if (bin && typeof bin === 'object') {
|
|
404
|
-
for (const binName in bin) {
|
|
405
|
-
binEntries.push({ name: binName, relPath: bin[binName] });
|
|
406
|
-
}
|
|
407
|
-
} else if (bin && typeof bin === 'string') {
|
|
408
|
-
// Shorthand: `"bin": "./path"` — the exposed name is the
|
|
409
|
-
// package's bare name (scope stripped). Matches npm behavior.
|
|
410
|
-
const bareName = dependencyPackageName.includes('/')
|
|
411
|
-
? dependencyPackageName.split('/').pop()!
|
|
412
|
-
: dependencyPackageName;
|
|
413
|
-
binEntries.push({ name: bareName, relPath: bin });
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (binEntries.length > 0) {
|
|
417
|
-
const dotBinDir = path.join(nodeModulesPath, '.bin');
|
|
418
|
-
if (!(await Fs.exists(dotBinDir))) {
|
|
419
|
-
await Fs.createFolder(dotBinDir);
|
|
420
|
-
}
|
|
421
|
-
for (const { name, relPath } of binEntries) {
|
|
422
|
-
const binFilePath = path.resolve(dependencyPath, relPath);
|
|
423
|
-
if (await Fs.exists(binFilePath)) {
|
|
424
|
-
await fs.chmod(binFilePath, 0o755);
|
|
425
|
-
}
|
|
426
|
-
const shimPath = path.join(dotBinDir, name);
|
|
427
|
-
// Same lstat-based cleanup as for the dep symlink — broken
|
|
428
|
-
// shims from a prior run in a different environment would
|
|
429
|
-
// otherwise make `ln -s` fail with "File exists".
|
|
430
|
-
try {
|
|
431
|
-
await fs.lstat(shimPath);
|
|
432
|
-
await Fs.deleteFolder(shimPath);
|
|
433
|
-
} catch (e: any) {
|
|
434
|
-
if (e.code !== 'ENOENT') {
|
|
435
|
-
throw e;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// Relative target: from `node_modules/.bin` into the dep
|
|
439
|
-
// directory. Goes through the dep's symlinked node_modules
|
|
440
|
-
// entry (not into the source tree) so the shim continues to
|
|
441
|
-
// resolve correctly after the workspace is relocated.
|
|
442
|
-
const shimTargetAbsolute = path.join(nodeModulesPath, dependencyPackageName, relPath);
|
|
443
|
-
const shimRelative = path.relative(dotBinDir, shimTargetAbsolute);
|
|
444
|
-
await cmd('ln', ['-s', shimRelative, shimPath], { cwd: packageDir });
|
|
445
|
-
}
|
|
446
516
|
}
|
|
517
|
+
// Relative target: from `node_modules/.bin` into the dep
|
|
518
|
+
// directory. Goes through the dep's symlinked node_modules
|
|
519
|
+
// entry (not into the source tree) so the shim continues to
|
|
520
|
+
// resolve correctly after the workspace is relocated.
|
|
521
|
+
const shimTargetAbsolute = path.join(nodeModulesPath, dependencyPackageName, relPath);
|
|
522
|
+
const shimRelative = path.relative(dotBinDir, shimTargetAbsolute);
|
|
523
|
+
await cmd('ln', ['-s', shimRelative, shimPath], { cwd: packageDir });
|
|
447
524
|
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
await linkDependencies(localPackage.packageJson.dependencies);
|
|
451
|
-
await linkDependencies(localPackage.packageJson.devDependencies);
|
|
525
|
+
}
|
|
452
526
|
}
|
|
453
527
|
}
|
|
@@ -225,6 +225,82 @@ describe('PackageUtil.symlinkDependencies — relative link targets', () => {
|
|
|
225
225
|
expect(content).toContain('console.log("only")');
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
test('symlinks a TRANSITIVE workspace dependency the consumer reaches only through another dep', async () => {
|
|
229
|
+
// Regression test for the missing-transitive-link bug.
|
|
230
|
+
//
|
|
231
|
+
// A package.json only lists DIRECT deps. But Node resolves a
|
|
232
|
+
// transitive workspace dep (one pulled in THROUGH a direct dep) out of
|
|
233
|
+
// the consumer's own node_modules first — so if symlinkDependencies
|
|
234
|
+
// only linked direct deps, npm would satisfy the transitive one with a
|
|
235
|
+
// stale registry copy that lags the live workspace source, causing
|
|
236
|
+
// version/schema drift (this exact gap cost a content_reference
|
|
237
|
+
// last_activity_at schema mismatch between flow-server's transitive
|
|
238
|
+
// space-common and the live source).
|
|
239
|
+
//
|
|
240
|
+
// Topology: consumer -> mid -> leaf. consumer declares ONLY mid; its
|
|
241
|
+
// single path to leaf is transitive. leaf must still get symlinked into
|
|
242
|
+
// consumer's node_modules with a relative target.
|
|
243
|
+
const leafPkg = await writePackage('packages/leaf', '@scope/leaf');
|
|
244
|
+
const midPkg = await writePackage('packages/mid', '@scope/mid', {
|
|
245
|
+
'@scope/leaf': '^1.0.0',
|
|
246
|
+
});
|
|
247
|
+
const consumerPkg = await writePackage('packages/consumer', '@scope/consumer', {
|
|
248
|
+
'@scope/mid': '^1.0.0',
|
|
249
|
+
});
|
|
250
|
+
const packageMap: LocalPackageMap = {
|
|
251
|
+
'@scope/leaf': leafPkg,
|
|
252
|
+
'@scope/mid': midPkg,
|
|
253
|
+
'@scope/consumer': consumerPkg,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
|
|
257
|
+
|
|
258
|
+
// Direct dep is linked (baseline).
|
|
259
|
+
const midLink = await fs.readlink(path.join(workspaceRoot, 'packages/consumer/node_modules/@scope/mid'));
|
|
260
|
+
expect(path.isAbsolute(midLink)).toBe(false);
|
|
261
|
+
expect(midLink).toBe(path.join('..', '..', '..', 'mid'));
|
|
262
|
+
|
|
263
|
+
// Transitive dep — the whole point of this test — is ALSO linked, with
|
|
264
|
+
// a relative target, and resolves to the live leaf source.
|
|
265
|
+
const leafSymlinkPath = path.join(workspaceRoot, 'packages/consumer/node_modules/@scope/leaf');
|
|
266
|
+
expect((await fs.lstat(leafSymlinkPath)).isSymbolicLink()).toBe(true);
|
|
267
|
+
const leafLink = await fs.readlink(leafSymlinkPath);
|
|
268
|
+
expect(path.isAbsolute(leafLink)).toBe(false);
|
|
269
|
+
expect(leafLink).toBe(path.join('..', '..', '..', 'leaf'));
|
|
270
|
+
const resolvedLeaf = await fs.stat(leafSymlinkPath);
|
|
271
|
+
expect(resolvedLeaf.isDirectory()).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('is cycle-safe: links the full closure even when the dependency graph has a cycle', async () => {
|
|
275
|
+
// The dependency graph can contain cycles (a <-> b). The transitive
|
|
276
|
+
// closure traversal must terminate and still link every reachable
|
|
277
|
+
// workspace package.
|
|
278
|
+
//
|
|
279
|
+
// Topology: consumer -> a, a -> b, b -> a (cycle). consumer declares
|
|
280
|
+
// only a; both a and b must be linked.
|
|
281
|
+
const aPkg = await writePackage('packages/a', '@scope/a', {
|
|
282
|
+
'@scope/b': '^1.0.0',
|
|
283
|
+
});
|
|
284
|
+
const bPkg = await writePackage('packages/b', '@scope/b', {
|
|
285
|
+
'@scope/a': '^1.0.0',
|
|
286
|
+
});
|
|
287
|
+
const consumerPkg = await writePackage('packages/consumer', '@scope/consumer', {
|
|
288
|
+
'@scope/a': '^1.0.0',
|
|
289
|
+
});
|
|
290
|
+
const packageMap: LocalPackageMap = {
|
|
291
|
+
'@scope/a': aPkg,
|
|
292
|
+
'@scope/b': bPkg,
|
|
293
|
+
'@scope/consumer': consumerPkg,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
|
|
297
|
+
|
|
298
|
+
const aLink = await fs.readlink(path.join(workspaceRoot, 'packages/consumer/node_modules/@scope/a'));
|
|
299
|
+
const bLink = await fs.readlink(path.join(workspaceRoot, 'packages/consumer/node_modules/@scope/b'));
|
|
300
|
+
expect(aLink).toBe(path.join('..', '..', '..', 'a'));
|
|
301
|
+
expect(bLink).toBe(path.join('..', '..', '..', 'b'));
|
|
302
|
+
});
|
|
303
|
+
|
|
228
304
|
test('handles scoped and unscoped dependencies uniformly', async () => {
|
|
229
305
|
const scopedDep = await writePackage('packages/scoped', '@scope/foo');
|
|
230
306
|
const unscopedDep = await writePackage('packages/unscoped', 'bar');
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/reflection-build-test-a",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@proteinjs/reflection-build-test-a",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.21",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@dagrejs/graphlib": "2.1.4",
|
|
13
13
|
"@proteinjs/reflection": "^1.1.11",
|
|
14
14
|
"@proteinjs/reflection-build-test-b": "file:../b",
|
|
15
15
|
"@proteinjs/util": "^1.6.0",
|
|
16
|
-
"@proteinjs/util-node": "^1.
|
|
16
|
+
"@proteinjs/util-node": "^1.9.0",
|
|
17
17
|
"globby": "11.0.1",
|
|
18
18
|
"gulp": "4.0.2",
|
|
19
19
|
"gulp-shell": "0.8.0",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"../b": {
|
|
34
34
|
"name": "@proteinjs/reflection-build-test-b",
|
|
35
|
-
"version": "0.0.
|
|
35
|
+
"version": "0.0.21",
|
|
36
36
|
"license": "ISC",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@dagrejs/graphlib": "2.1.4",
|
|
39
39
|
"@proteinjs/reflection": "^1.1.11",
|
|
40
40
|
"@proteinjs/util": "^1.6.0",
|
|
41
|
-
"@proteinjs/util-node": "^1.
|
|
41
|
+
"@proteinjs/util-node": "^1.9.0",
|
|
42
42
|
"globby": "11.0.1",
|
|
43
43
|
"gulp": "4.0.2",
|
|
44
44
|
"gulp-shell": "0.8.0",
|
|
@@ -1129,9 +1129,9 @@
|
|
|
1129
1129
|
}
|
|
1130
1130
|
},
|
|
1131
1131
|
"node_modules/@proteinjs/util-node": {
|
|
1132
|
-
"version": "1.
|
|
1133
|
-
"resolved": "https://registry.npmjs.org/@proteinjs/util-node/-/util-node-1.
|
|
1134
|
-
"integrity": "sha512-
|
|
1132
|
+
"version": "1.9.0",
|
|
1133
|
+
"resolved": "https://registry.npmjs.org/@proteinjs/util-node/-/util-node-1.9.0.tgz",
|
|
1134
|
+
"integrity": "sha512-kn9zzmm7tUmlZiiJxGjkekhwsNprUzZM2r5rWZYgDD/BWdGCTA1QCfKEhmjCHLJutR1J+i4NmWcSfuYdyt+hjw==",
|
|
1135
1135
|
"license": "ISC",
|
|
1136
1136
|
"dependencies": {
|
|
1137
1137
|
"@dagrejs/graphlib": "2.1.4",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/reflection-build-test-a",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Test package a",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@proteinjs/reflection": "^1.1.11",
|
|
23
23
|
"@proteinjs/reflection-build-test-b": "file:../b",
|
|
24
24
|
"@proteinjs/util": "^1.6.0",
|
|
25
|
-
"@proteinjs/util-node": "^1.
|
|
25
|
+
"@proteinjs/util-node": "^1.9.0",
|
|
26
26
|
"globby": "11.0.1",
|
|
27
27
|
"gulp": "4.0.2",
|
|
28
28
|
"gulp-shell": "0.8.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/reflection-build-test-b",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
@@ -1074,9 +1074,9 @@
|
|
|
1074
1074
|
}
|
|
1075
1075
|
},
|
|
1076
1076
|
"node_modules/@proteinjs/util-node": {
|
|
1077
|
-
"version": "1.
|
|
1078
|
-
"resolved": "https://registry.npmjs.org/@proteinjs/util-node/-/util-node-1.
|
|
1079
|
-
"integrity": "sha512-
|
|
1077
|
+
"version": "1.9.0",
|
|
1078
|
+
"resolved": "https://registry.npmjs.org/@proteinjs/util-node/-/util-node-1.9.0.tgz",
|
|
1079
|
+
"integrity": "sha512-kn9zzmm7tUmlZiiJxGjkekhwsNprUzZM2r5rWZYgDD/BWdGCTA1QCfKEhmjCHLJutR1J+i4NmWcSfuYdyt+hjw==",
|
|
1080
1080
|
"license": "ISC",
|
|
1081
1081
|
"dependencies": {
|
|
1082
1082
|
"@dagrejs/graphlib": "2.1.4",
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.9.0](https://github.com/proteinjs/util/compare/@proteinjs/util-node@1.8.1...@proteinjs/util-node@1.9.0) (2026-06-24)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* symlink-workspace links each package's transitive workspace-dep closure ([55a7c25](https://github.com/proteinjs/util/commit/55a7c2541f500bec4e12bb05454acadd6b4c4b66))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## [1.8.1](https://github.com/proteinjs/util/compare/@proteinjs/util-node@1.8.0...@proteinjs/util-node@1.8.1) (2026-04-14)
|
|
7
18
|
|
|
8
19
|
|
|
@@ -101,9 +101,52 @@ export declare class PackageUtil {
|
|
|
101
101
|
static getWorkspaceMetadata(workspacePath: string): Promise<WorkspaceMetadata>;
|
|
102
102
|
/**
|
|
103
103
|
* Symlink the dependencies of `localPackage` to other local packages in the workspace.
|
|
104
|
+
*
|
|
105
|
+
* This links the package's full TRANSITIVE closure of workspace dependencies,
|
|
106
|
+
* not just its directly-declared ones. A package's `package.json` only lists
|
|
107
|
+
* its direct deps, but those deps pull in workspace packages of their own
|
|
108
|
+
* (e.g. `flow-server` declares `@n3xah/space-server`, which itself depends on
|
|
109
|
+
* `@n3xah/space-common`). Node resolves a transitive dep like `space-common`
|
|
110
|
+
* out of the consumer's own `node_modules` first, so if we only symlinked
|
|
111
|
+
* direct deps, npm would satisfy `space-common` with a stale registry copy
|
|
112
|
+
* that lags the live workspace source — causing schema/version drift. By
|
|
113
|
+
* linking the whole closure, every workspace package a package can reach at
|
|
114
|
+
* runtime resolves to the live source tree.
|
|
115
|
+
*
|
|
104
116
|
* @param localPackage package to symlink the dependencies of
|
|
105
117
|
* @param localPackageMap `LocalPackageMap` of the workspace
|
|
106
|
-
* @param logger optionally provide a logger to capture this method's logging
|
|
107
118
|
*/
|
|
108
119
|
static symlinkDependencies(localPackage: LocalPackage, localPackageMap: LocalPackageMap): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Compute the transitive closure of workspace dependencies for `localPackage`.
|
|
122
|
+
*
|
|
123
|
+
* Reuses `getPackageDependencyGraph`, which already crawls `dependencies` and
|
|
124
|
+
* `devDependencies` (transitively, across the whole workspace) and adds an
|
|
125
|
+
* edge `consumer -> dependency` for each. So the workspace deps reachable from
|
|
126
|
+
* a package are exactly the nodes reachable by following `successors` from its
|
|
127
|
+
* node. We do a cycle-safe traversal (a `visited` set), since the dependency
|
|
128
|
+
* graph can contain cycles. The package itself is excluded from the result.
|
|
129
|
+
*
|
|
130
|
+
* Only names present in `localPackageMap` are returned — i.e. packages that
|
|
131
|
+
* actually live in the workspace and can be symlinked (the graph already
|
|
132
|
+
* filters to file:/relative/workspace deps in `addDependencies`, but we filter
|
|
133
|
+
* again here so the result is exactly the set of linkable packages).
|
|
134
|
+
*
|
|
135
|
+
* @returns workspace package names this package transitively depends on
|
|
136
|
+
*/
|
|
137
|
+
private static getTransitiveWorkspaceDependencies;
|
|
138
|
+
/**
|
|
139
|
+
* Symlink a single workspace package into `nodeModulesPath`, and create
|
|
140
|
+
* `node_modules/.bin/<name>` shims for any `bin` it declares.
|
|
141
|
+
*
|
|
142
|
+
* This is the per-dependency linking logic used by `symlinkDependencies` for
|
|
143
|
+
* each package in the transitive closure. The caller is responsible for
|
|
144
|
+
* deciding WHICH packages to link; this method just links the one named.
|
|
145
|
+
*
|
|
146
|
+
* @param dependencyPackageName name of the workspace package to link
|
|
147
|
+
* @param nodeModulesPath absolute path to the consumer's `node_modules`
|
|
148
|
+
* @param packageDir absolute path to the consumer package's directory (cwd for `ln`)
|
|
149
|
+
* @param localPackageMap `LocalPackageMap` of the workspace
|
|
150
|
+
*/
|
|
151
|
+
private static symlinkPackage;
|
|
109
152
|
}
|