@bobfrankston/npmglobalize 1.0.152 → 1.0.154

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/README.md CHANGED
@@ -350,6 +350,11 @@ Workspace mode is auto-detected when run from a root with `"private": true` and
350
350
  -asis Skip ignore file checks (or set "asis": true in .globalize.json5)
351
351
  -fix-tags Automatically fix version/tag mismatches
352
352
  -rebase Automatically rebase if local is behind remote
353
+ -clean-nested-modules, -clean-nested
354
+ Before npm pack, wipe node_modules/ inside each file: dep
355
+ target. Fixes arborist "Cannot read properties of null"
356
+ crashes caused by sibling file: deps with nested
357
+ node_modules. Suggested automatically when the error hits.
353
358
  -show Show package.json dependency changes
354
359
  -package, -pkg Update package.json scripts to use npmglobalize (see below)
355
360
  -h, -help Show help
package/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * npmglobalize CLI - Transform file: dependencies to npm versions for publishing
4
4
  */
5
- import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, confirm, getBuildIssues, clearBuildIssues, recordBuildIssue, extractFirstTscError } from './lib.js';
5
+ import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, confirm, getBuildIssues, clearBuildIssues, recordBuildIssue, extractFirstTscError, ensureFileDepModules } from './lib.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { styleText } from 'util';
@@ -423,6 +423,7 @@ export async function main() {
423
423
  }
424
424
  }
425
425
  if (pkg.scripts?.build) {
426
+ ensureFileDepModules(cwd, !!cliOptions.verbose);
426
427
  const { spawnSync } = await import('child_process');
427
428
  console.log(`Building ${cwd}...`);
428
429
  const buildResult = spawnSync('npm', ['run', 'build'], {
package/lib.d.ts CHANGED
@@ -234,6 +234,12 @@ export declare function parseVersionTag(tag: string): number[] | null;
234
234
  export declare function compareVersions(a: number[], b: number[]): number;
235
235
  /** Fix version/tag mismatches */
236
236
  export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
237
+ /** Walk `file:` deps transitively and run `npm install` in any target whose
238
+ * package.json declares deps but has no `node_modules/`. Recovers from prior
239
+ * aborted runs (or pre-1.0.153 `--clean-nested-modules` damage) so the
240
+ * upcoming build/pack can resolve transitive package imports.
241
+ * Cycle-safe via the shared `visited` set. */
242
+ export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): void;
237
243
  /** Run a command and return success status */
238
244
  export declare function runCommand(cmd: string, args: string[], options?: {
239
245
  silent?: boolean;
package/lib.js CHANGED
@@ -1262,12 +1262,10 @@ function waitForNpmVersion(pkgName, version, maxWaitMs = 90000) {
1262
1262
  process.stdout.write(' timed out\n');
1263
1263
  return false;
1264
1264
  }
1265
- /** Delete `node_modules/` inside each `file:` dep target.
1266
- * Works around arborist crashes during `npm pack` when sibling `file:` deps
1267
- * have their own populated `node_modules/`. Returns the names of cleaned deps. */
1268
1265
  function cleanNestedDepModules(pkg, cwd, verbose) {
1269
- const cleaned = [];
1266
+ const stashed = [];
1270
1267
  const seen = new Set();
1268
+ const suffix = `.npmglobalize-stash-${process.pid}`;
1271
1269
  for (const key of ['.dependencies', '.devDependencies', 'dependencies', 'devDependencies']) {
1272
1270
  const deps = pkg?.[key];
1273
1271
  if (!deps || typeof deps !== 'object')
@@ -1282,18 +1280,90 @@ function cleanNestedDepModules(pkg, cwd, verbose) {
1282
1280
  if (!fs.existsSync(nm))
1283
1281
  continue;
1284
1282
  seen.add(name);
1283
+ const backup = nm + suffix;
1285
1284
  try {
1286
- fs.rmSync(nm, { recursive: true, force: true });
1287
- cleaned.push(name);
1285
+ if (fs.existsSync(backup))
1286
+ fs.rmSync(backup, { recursive: true, force: true });
1287
+ fs.renameSync(nm, backup);
1288
+ stashed.push({ name, nm, backup });
1288
1289
  if (verbose)
1289
- console.log(colors.dim(` cleaned ${nm}`));
1290
+ console.log(colors.dim(` stashed ${nm} -> ${path.basename(backup)}`));
1290
1291
  }
1291
1292
  catch (e) {
1292
- console.error(colors.yellow(` warning: could not clean ${nm}: ${e.message}`));
1293
+ console.error(colors.yellow(` warning: could not stash ${nm}: ${e.message}`));
1293
1294
  }
1294
1295
  }
1295
1296
  }
1296
- return cleaned;
1297
+ return stashed;
1298
+ }
1299
+ /** Restore `node_modules/` previously moved aside by `cleanNestedDepModules`.
1300
+ * Safe to call multiple times; missing backups are skipped. */
1301
+ function restoreNestedDepModules(stashed, verbose) {
1302
+ for (const s of stashed) {
1303
+ try {
1304
+ if (!fs.existsSync(s.backup))
1305
+ continue;
1306
+ if (fs.existsSync(s.nm))
1307
+ fs.rmSync(s.nm, { recursive: true, force: true });
1308
+ fs.renameSync(s.backup, s.nm);
1309
+ if (verbose)
1310
+ console.log(colors.dim(` restored ${s.nm}`));
1311
+ }
1312
+ catch (e) {
1313
+ console.error(colors.yellow(` warning: could not restore ${s.nm}: ${e.message}`));
1314
+ console.error(colors.yellow(` backup remains at ${s.backup} — restore manually`));
1315
+ }
1316
+ }
1317
+ }
1318
+ /** Walk `file:` deps transitively and run `npm install` in any target whose
1319
+ * package.json declares deps but has no `node_modules/`. Recovers from prior
1320
+ * aborted runs (or pre-1.0.153 `--clean-nested-modules` damage) so the
1321
+ * upcoming build/pack can resolve transitive package imports.
1322
+ * Cycle-safe via the shared `visited` set. */
1323
+ export function ensureFileDepModules(cwd, verbose = false, visited = new Set()) {
1324
+ const abs = path.resolve(cwd);
1325
+ if (visited.has(abs))
1326
+ return;
1327
+ visited.add(abs);
1328
+ let pkg;
1329
+ try {
1330
+ pkg = readPackageJson(cwd);
1331
+ }
1332
+ catch {
1333
+ return;
1334
+ }
1335
+ for (const key of ['dependencies', 'devDependencies']) {
1336
+ const deps = pkg?.[key];
1337
+ if (!deps || typeof deps !== 'object')
1338
+ continue;
1339
+ for (const [name, spec] of Object.entries(deps)) {
1340
+ if (typeof spec !== 'string' || !spec.startsWith('file:'))
1341
+ continue;
1342
+ const target = path.resolve(cwd, spec.slice('file:'.length));
1343
+ if (!fs.existsSync(path.join(target, 'package.json')))
1344
+ continue;
1345
+ let targetPkg;
1346
+ try {
1347
+ targetPkg = readPackageJson(target);
1348
+ }
1349
+ catch {
1350
+ continue;
1351
+ }
1352
+ const hasDeps = (targetPkg?.dependencies && Object.keys(targetPkg.dependencies).length > 0) ||
1353
+ (targetPkg?.devDependencies && Object.keys(targetPkg.devDependencies).length > 0);
1354
+ const nm = path.join(target, 'node_modules');
1355
+ if (hasDeps && !fs.existsSync(nm)) {
1356
+ console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target})`));
1357
+ const r = runCommand('npm', ['install'], { cwd: target, silent: !verbose });
1358
+ if (!r.success) {
1359
+ console.error(colors.red(` ✗ npm install failed in ${target}`));
1360
+ if (r.stderr)
1361
+ console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1362
+ }
1363
+ }
1364
+ ensureFileDepModules(target, verbose, visited);
1365
+ }
1366
+ }
1297
1367
  }
1298
1368
  /** Run npm install -g with retries for registry propagation delay */
1299
1369
  function installGlobalWithRetry(pkgSpec, cwd, maxRetries = 3) {
@@ -3041,6 +3111,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3041
3111
  const pkg = readPackageJson(cwd);
3042
3112
  // Run build step if package.json has a build script (skip if CLI already built)
3043
3113
  if (pkg.scripts?.build && !options._fromCli) {
3114
+ if (!dryRun)
3115
+ ensureFileDepModules(cwd, verbose);
3044
3116
  console.log(`${timestamp()} Running build...`);
3045
3117
  if (!dryRun) {
3046
3118
  // Always capture output so we can extract tsc errors for the summary
@@ -4270,11 +4342,14 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4270
4342
  if (verbose) {
4271
4343
  console.log(colors.green(`✓ Authenticated as ${authStatus.username}`));
4272
4344
  }
4273
- // Optionally clean nested node_modules in file: dep targets before pack
4345
+ // Optionally stash nested node_modules in file: dep targets before pack
4346
+ // (arborist crashes when sibling file: deps have populated node_modules).
4347
+ // Restored immediately after pack so symlinked file: deps stay runnable.
4348
+ let stashedDepModules = [];
4274
4349
  if (options.cleanNestedModules) {
4275
- const cleaned = cleanNestedDepModules(pkg, cwd, verbose);
4276
- if (cleaned.length > 0) {
4277
- console.log(colors.yellow(` Cleaned node_modules in ${cleaned.length} file: dep target(s): ${cleaned.join(', ')}`));
4350
+ stashedDepModules = cleanNestedDepModules(pkg, cwd, verbose);
4351
+ if (stashedDepModules.length > 0) {
4352
+ console.log(colors.yellow(` Cleaned node_modules in ${stashedDepModules.length} file: dep target(s): ${stashedDepModules.map(s => s.name).join(', ')}`));
4278
4353
  }
4279
4354
  else if (verbose) {
4280
4355
  console.log(colors.dim(' --clean-nested-modules: nothing to clean'));
@@ -4282,6 +4357,10 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4282
4357
  }
4283
4358
  // Create tarball first
4284
4359
  const packResult = runCommand('npm', ['pack'], { cwd, silent: true });
4360
+ // Restore stashed node_modules now that pack is done — must happen
4361
+ // before any subsequent install -g symlinks to these dep targets.
4362
+ if (stashedDepModules.length > 0)
4363
+ restoreNestedDepModules(stashedDepModules, verbose);
4285
4364
  if (!packResult.success) {
4286
4365
  const d = diagnoseNpmPackFailure(cwd, packResult.output, packResult.stderr, pkg);
4287
4366
  console.error(colors.red(`ERROR: ${d.summary}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.152",
3
+ "version": "1.0.154",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -33,6 +33,7 @@
33
33
  "dependencies": {
34
34
  "@bobfrankston/freezepak": "^0.1.7",
35
35
  "@bobfrankston/importgen": "^0.1.34",
36
+ "@bobfrankston/npmglobalize": "^1.0.153",
36
37
  "@bobfrankston/themecolors": "^0.1.5",
37
38
  "@bobfrankston/userconfig": "^1.0.7",
38
39
  "@npmcli/package-json": "^7.0.4",
@@ -48,6 +49,7 @@
48
49
  ".dependencies": {
49
50
  "@bobfrankston/freezepak": "file:../freezepak",
50
51
  "@bobfrankston/importgen": "file:../importgen",
52
+ "@bobfrankston/npmglobalize": "^1.0.153",
51
53
  "@bobfrankston/themecolors": "file:../themecolors",
52
54
  "@bobfrankston/userconfig": "file:../userconfig",
53
55
  "@npmcli/package-json": "^7.0.4",
@@ -61,6 +63,7 @@
61
63
  "dependencies": {
62
64
  "@bobfrankston/freezepak": "^0.1.7",
63
65
  "@bobfrankston/importgen": "^0.1.34",
66
+ "@bobfrankston/npmglobalize": "^1.0.153",
64
67
  "@bobfrankston/themecolors": "^0.1.5",
65
68
  "@bobfrankston/userconfig": "^1.0.7",
66
69
  "@npmcli/package-json": "^7.0.4",