@bobfrankston/npmglobalize 1.0.159 → 1.0.160

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.
Files changed (3) hide show
  1. package/lib.d.ts +17 -3
  2. package/lib.js +94 -14
  3. package/package.json +1 -1
package/lib.d.ts CHANGED
@@ -243,12 +243,26 @@ export declare function parseVersionTag(tag: string): number[] | null;
243
243
  export declare function compareVersions(a: number[], b: number[]): number;
244
244
  /** Fix version/tag mismatches */
245
245
  export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
246
+ /** Return declared deps (dependencies + devDependencies) that don't resolve from
247
+ * `pkgDir`. Skips `file:`/`workspace:`/`link:` specs — those are handled elsewhere.
248
+ * A declared dep that doesn't resolve means `package.json` and installed
249
+ * `node_modules/` are out of sync (e.g. dep added but `npm install` not re-run). */
250
+ export declare function missingDeps(pkgDir: string, pkg: any): string[];
246
251
  /** Walk `file:` deps transitively and run `npm install` in any target whose
247
- * package.json declares deps but has no `node_modules/`. Also covers `cwd`
248
- * itself on the first call, so a fresh clone with no `node_modules/` gets
249
- * installed before the upcoming build/pack tries to resolve imports.
252
+ * declared deps aren't all resolvable from its directory. Covers both
253
+ * "fresh clone, no `node_modules/`" and "dep added but `npm install` not
254
+ * re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
250
255
  * Cycle-safe via the shared `visited` set. */
251
256
  export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): void;
257
+ /** Ensure the workspace-root `node_modules/` is in sync with every member's
258
+ * declared deps. Workspaces hoist deps to the root, so a dep added to any
259
+ * member `package.json` without a follow-up `npm install` at the root leaves
260
+ * the root `node_modules/` stale — the case where `member/` dirs exist but
261
+ * the actual package (e.g. `marked`) is not hoisted. */
262
+ export declare function ensureWorkspaceDepModules(rootDir: string, members: Array<{
263
+ dir: string;
264
+ pkg: any;
265
+ }>, verbose?: boolean): void;
252
266
  /** Run a command and return success status */
253
267
  export declare function runCommand(cmd: string, args: string[], options?: {
254
268
  silent?: boolean;
package/lib.js CHANGED
@@ -1322,10 +1322,51 @@ function restoreNestedDepModules(stashed, verbose) {
1322
1322
  }
1323
1323
  }
1324
1324
  }
1325
+ /** Walk up the directory tree looking for `node_modules/<depName>/package.json`.
1326
+ * Matches Node's module resolution so hoisted workspace deps are found. */
1327
+ function depResolves(startDir, depName) {
1328
+ let dir = path.resolve(startDir);
1329
+ const segs = depName.split('/');
1330
+ while (true) {
1331
+ const candidate = path.join(dir, 'node_modules', ...segs, 'package.json');
1332
+ if (fs.existsSync(candidate))
1333
+ return true;
1334
+ const parent = path.dirname(dir);
1335
+ if (parent === dir)
1336
+ return false;
1337
+ dir = parent;
1338
+ }
1339
+ }
1340
+ /** Return declared deps (dependencies + devDependencies) that don't resolve from
1341
+ * `pkgDir`. Skips `file:`/`workspace:`/`link:` specs — those are handled elsewhere.
1342
+ * A declared dep that doesn't resolve means `package.json` and installed
1343
+ * `node_modules/` are out of sync (e.g. dep added but `npm install` not re-run). */
1344
+ export function missingDeps(pkgDir, pkg) {
1345
+ const missing = [];
1346
+ for (const key of ['dependencies', 'devDependencies']) {
1347
+ const deps = pkg?.[key];
1348
+ if (!deps || typeof deps !== 'object')
1349
+ continue;
1350
+ for (const [name, spec] of Object.entries(deps)) {
1351
+ if (typeof spec !== 'string')
1352
+ continue;
1353
+ if (spec.startsWith('file:') || spec.startsWith('workspace:') || spec.startsWith('link:'))
1354
+ continue;
1355
+ if (!depResolves(pkgDir, name))
1356
+ missing.push(name);
1357
+ }
1358
+ }
1359
+ return missing;
1360
+ }
1361
+ function formatMissingReason(missing) {
1362
+ if (missing.length === 1)
1363
+ return `missing dep: ${missing[0]}`;
1364
+ return `${missing.length} missing deps: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ', …' : ''}`;
1365
+ }
1325
1366
  /** Walk `file:` deps transitively and run `npm install` in any target whose
1326
- * package.json declares deps but has no `node_modules/`. Also covers `cwd`
1327
- * itself on the first call, so a fresh clone with no `node_modules/` gets
1328
- * installed before the upcoming build/pack tries to resolve imports.
1367
+ * declared deps aren't all resolvable from its directory. Covers both
1368
+ * "fresh clone, no `node_modules/`" and "dep added but `npm install` not
1369
+ * re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
1329
1370
  * Cycle-safe via the shared `visited` set. */
1330
1371
  export function ensureFileDepModules(cwd, verbose = false, visited = new Set()) {
1331
1372
  const abs = path.resolve(cwd);
@@ -1339,11 +1380,13 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
1339
1380
  catch {
1340
1381
  return;
1341
1382
  }
1342
- const cwdHasDeps = (pkg?.dependencies && Object.keys(pkg.dependencies).length > 0) ||
1343
- (pkg?.devDependencies && Object.keys(pkg.devDependencies).length > 0);
1344
- if (cwdHasDeps && !fs.existsSync(path.join(abs, 'node_modules'))) {
1383
+ const cwdMissing = missingDeps(abs, pkg);
1384
+ if (cwdMissing.length > 0) {
1385
+ // Only clear a stale lockfile if node_modules is entirely absent (fresh clone).
1386
+ // For partial-sync cases, keep the lockfile so pinned versions stay honored.
1387
+ const nm = path.join(abs, 'node_modules');
1345
1388
  const lock = path.join(abs, 'package-lock.json');
1346
- if (fs.existsSync(lock)) {
1389
+ if (!fs.existsSync(nm) && fs.existsSync(lock)) {
1347
1390
  if (verbose)
1348
1391
  console.log(colors.dim(` removing stale ${lock}`));
1349
1392
  try {
@@ -1351,7 +1394,7 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
1351
1394
  }
1352
1395
  catch { /* best-effort */ }
1353
1396
  }
1354
- console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs}`));
1397
+ console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs} (${formatMissingReason(cwdMissing)})`));
1355
1398
  const r = runCommand('npm', ['install'], { cwd: abs, silent: !verbose });
1356
1399
  if (!r.success) {
1357
1400
  console.error(colors.red(` ✗ npm install failed in ${abs}`));
@@ -1376,12 +1419,11 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
1376
1419
  catch {
1377
1420
  continue;
1378
1421
  }
1379
- const hasDeps = (targetPkg?.dependencies && Object.keys(targetPkg.dependencies).length > 0) ||
1380
- (targetPkg?.devDependencies && Object.keys(targetPkg.devDependencies).length > 0);
1381
- const nm = path.join(target, 'node_modules');
1382
- if (hasDeps && !fs.existsSync(nm)) {
1422
+ const targetMissing = missingDeps(target, targetPkg);
1423
+ if (targetMissing.length > 0) {
1424
+ const nm = path.join(target, 'node_modules');
1383
1425
  const lock = path.join(target, 'package-lock.json');
1384
- if (fs.existsSync(lock)) {
1426
+ if (!fs.existsSync(nm) && fs.existsSync(lock)) {
1385
1427
  if (verbose)
1386
1428
  console.log(colors.dim(` removing stale ${lock}`));
1387
1429
  try {
@@ -1389,7 +1431,7 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
1389
1431
  }
1390
1432
  catch { /* best-effort */ }
1391
1433
  }
1392
- console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target})`));
1434
+ console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target}) (${formatMissingReason(targetMissing)})`));
1393
1435
  const r = runCommand('npm', ['install'], { cwd: target, silent: !verbose });
1394
1436
  if (!r.success) {
1395
1437
  console.error(colors.red(` ✗ npm install failed in ${target}`));
@@ -1401,6 +1443,38 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
1401
1443
  }
1402
1444
  }
1403
1445
  }
1446
+ /** Ensure the workspace-root `node_modules/` is in sync with every member's
1447
+ * declared deps. Workspaces hoist deps to the root, so a dep added to any
1448
+ * member `package.json` without a follow-up `npm install` at the root leaves
1449
+ * the root `node_modules/` stale — the case where `member/` dirs exist but
1450
+ * the actual package (e.g. `marked`) is not hoisted. */
1451
+ export function ensureWorkspaceDepModules(rootDir, members, verbose = false) {
1452
+ const root = path.resolve(rootDir);
1453
+ let rootPkg;
1454
+ try {
1455
+ rootPkg = readPackageJson(root);
1456
+ }
1457
+ catch {
1458
+ return;
1459
+ }
1460
+ const allMissing = new Set();
1461
+ for (const name of missingDeps(root, rootPkg))
1462
+ allMissing.add(name);
1463
+ for (const m of members) {
1464
+ for (const name of missingDeps(m.dir, m.pkg))
1465
+ allMissing.add(name);
1466
+ }
1467
+ if (allMissing.size === 0)
1468
+ return;
1469
+ const list = [...allMissing];
1470
+ console.log(colors.yellow(`↻ installing workspace node_modules in ${rootPkg?.name || path.basename(root)} (${formatMissingReason(list)})`));
1471
+ const r = runCommand('npm', ['install'], { cwd: root, silent: !verbose });
1472
+ if (!r.success) {
1473
+ console.error(colors.red(` ✗ npm install failed in ${root}`));
1474
+ if (r.stderr)
1475
+ console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1476
+ }
1477
+ }
1404
1478
  /** Run npm install -g with retries for registry propagation delay */
1405
1479
  function installGlobalWithRetry(pkgSpec, cwd, maxRetries = 3) {
1406
1480
  let result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
@@ -4931,6 +5005,12 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
4931
5005
  }
4932
5006
  }
4933
5007
  }
5008
+ // Sync workspace-root node_modules with member package.json files. Catches
5009
+ // the case where a dep was added to a member package.json but `npm install`
5010
+ // wasn't re-run — the builds would otherwise fail resolving the new dep.
5011
+ if (!options.dryRun) {
5012
+ ensureWorkspaceDepModules(rootDir, packages.map(p => ({ dir: p.dir, pkg: p.pkg })), !!options.verbose);
5013
+ }
4934
5014
  // Prescan: decide which packages actually need processing so we don't waste
4935
5015
  // time rebuilding+republishing ones with no relevant changes.
4936
5016
  // A package is SKIPPED only if ALL of:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.159",
3
+ "version": "1.0.160",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",