@bobfrankston/npmglobalize 1.0.150 → 1.0.152

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/cli.js CHANGED
@@ -75,6 +75,10 @@ Other Options:
75
75
  (noEmit projects: removes TS ignores from .npmignore)
76
76
  -asis Skip ignore file checks (or set "asis": true in .globalize.json5)
77
77
  -rebase Automatically rebase if local is behind remote
78
+ -clean-nested-modules, -clean-nested
79
+ Before npm pack, wipe node_modules/ in each file: dep target.
80
+ Fixes arborist "Cannot read properties of null" crashes when
81
+ sibling file: deps have their own nested node_modules.
78
82
  -show Show package.json dependency changes
79
83
  -package, -pkg Update package.json scripts to use npmglobalize
80
84
  -h, -help Show this help
@@ -318,6 +322,11 @@ function parseArgs(args) {
318
322
  options.importgen = false;
319
323
  options.explicitKeys.add('importgen');
320
324
  break;
325
+ case '-clean-nested':
326
+ case '-clean-nested-modules':
327
+ options.cleanNestedModules = true;
328
+ options.explicitKeys.add('cleanNestedModules');
329
+ break;
321
330
  default:
322
331
  if (arg.startsWith('-')) {
323
332
  unrecognized.push(arg);
package/lib/config.js CHANGED
@@ -64,7 +64,7 @@ export function writeConfig(dir, config, explicitKeys) {
64
64
  const existing = readConfig(dir);
65
65
  // Filter out temporary flags and default values (unless explicitly set)
66
66
  const filtered = {};
67
- const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish', 'once']);
67
+ const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish', 'once', 'cleanNestedModules']);
68
68
  for (const [key, value] of Object.entries(config)) {
69
69
  if (omitKeys.has(key))
70
70
  continue;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Diagnose failures of npm commands run by npmglobalize.
3
+ *
4
+ * Pulls out common error shapes — especially ones whose root cause lives in
5
+ * a referenced (file:) module, not the current package — and turns them into
6
+ * a short summary + actionable hint instead of a raw arborist stack trace.
7
+ *
8
+ * Leaf module: no dependencies on lib.ts or other internal modules. Keeps
9
+ * the main publish flow in lib.ts readable.
10
+ */
11
+ export interface DiagnosedError {
12
+ /** Short one-liner suitable for the Issues Summary. */
13
+ summary: string;
14
+ /** Multi-line block to print to console.error (without color). */
15
+ details: string[];
16
+ /** If the root cause is a file: sibling, its package name. */
17
+ referencedModule?: string;
18
+ /** One concrete next step the user can take. */
19
+ hint?: string;
20
+ }
21
+ /** Diagnose an `npm pack` failure. */
22
+ export declare function diagnoseNpmPackFailure(cwd: string, output: string, stderr: string, pkg: any): DiagnosedError;
23
+ /** Given a nested-node_modules path like
24
+ * `node_modules/@scope/sibling/node_modules/transitive`
25
+ * or `../sibling/node_modules/transitive`,
26
+ * find the outermost dep name and resolve it to a file: sibling declared
27
+ * in pkg.dependencies / pkg['.dependencies'] / etc. */
28
+ declare function resolveSiblingFromNestedPath(nestedPath: string, cwd: string, pkg: any): {
29
+ name: string;
30
+ filePath: string;
31
+ } | undefined;
32
+ /** Scan pkg deps for a file: entry whose resolved absolute path equals absTarget. */
33
+ declare function findFileDepByPath(pkg: any, cwd: string, absTarget: string): {
34
+ name: string;
35
+ filePath: string;
36
+ } | undefined;
37
+ /** Exposed for ad-hoc testing — not used by the main flow. */
38
+ export declare const _test: {
39
+ resolveSiblingFromNestedPath: typeof resolveSiblingFromNestedPath;
40
+ findFileDepByPath: typeof findFileDepByPath;
41
+ };
42
+ export {};
43
+ //# sourceMappingURL=diagnose.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Diagnose failures of npm commands run by npmglobalize.
3
+ *
4
+ * Pulls out common error shapes — especially ones whose root cause lives in
5
+ * a referenced (file:) module, not the current package — and turns them into
6
+ * a short summary + actionable hint instead of a raw arborist stack trace.
7
+ *
8
+ * Leaf module: no dependencies on lib.ts or other internal modules. Keeps
9
+ * the main publish flow in lib.ts readable.
10
+ */
11
+ import path from 'path';
12
+ /** Diagnose an `npm pack` failure. */
13
+ export function diagnoseNpmPackFailure(cwd, output, stderr, pkg) {
14
+ const blob = `${output}\n${stderr}`;
15
+ const lower = blob.toLowerCase();
16
+ // Pattern 1: arborist "missing from lockfile: <path>" + null-`package` TypeError.
17
+ // Root cause: a file: sibling has its own populated node_modules/<transitive>
18
+ // that is not in the current package's lockfile. See notes.md §TODO:
19
+ // "Isolate npm pack from sibling file: dep junctions".
20
+ const missingMatch = blob.match(/missing from lockfile:\s*(\S+)/i);
21
+ const hasNullPackage = /cannot read propert(y|ies) of null \(reading 'package'\)/i.test(blob);
22
+ if (missingMatch && (hasNullPackage || /shrinkwrap failed to load/i.test(blob))) {
23
+ const nestedPath = missingMatch[1];
24
+ const sibling = resolveSiblingFromNestedPath(nestedPath, cwd, pkg);
25
+ const details = [
26
+ `npm arborist crashed walking a nested node_modules tree.`,
27
+ `Missing from lockfile: ${nestedPath}`,
28
+ ];
29
+ if (sibling) {
30
+ return {
31
+ summary: `npm pack failed — arborist crashed on sibling's node_modules`,
32
+ details,
33
+ referencedModule: sibling.name,
34
+ hint: `sibling ${sibling.name} has its own populated node_modules; see notes.md §TODO: Isolate npm pack from sibling file: dep junctions`,
35
+ };
36
+ }
37
+ return {
38
+ summary: `npm pack failed — arborist crashed on a nested node_modules`,
39
+ details,
40
+ hint: `likely a file: dep whose target has its own node_modules; see notes.md §TODO: Isolate npm pack from sibling file: dep junctions`,
41
+ };
42
+ }
43
+ // Pattern 2: arborist null-`package` without an obvious path — same class,
44
+ // no identifiable referenced module.
45
+ if (hasNullPackage) {
46
+ return {
47
+ summary: `npm pack failed — arborist internal error`,
48
+ details: [extractArboristFrame(blob) || 'Cannot read properties of null (reading \'package\')'],
49
+ hint: `likely a symlinked file: dep with its own node_modules; try renaming node_modules/ and running \`npm pack --dry-run\``,
50
+ };
51
+ }
52
+ // Pattern 3: shrinkwrap / lockfile parsing without the arborist crash.
53
+ if (/enolock|shrinkwrap failed to load|eresolve/i.test(lower)) {
54
+ return {
55
+ summary: `npm pack failed — lockfile problem`,
56
+ details: [extractFirstNpmError(blob) || blob.trim().slice(0, 400)],
57
+ hint: `run \`npm install\` in ${cwd}, then retry`,
58
+ };
59
+ }
60
+ // Pattern 4: file-locking on the tarball (AV, editor, explorer preview).
61
+ if (/\be(acces|busy|perm)\b/i.test(blob) && /\.tgz\b/i.test(blob)) {
62
+ return {
63
+ summary: `npm pack failed — tarball file locked`,
64
+ details: [extractFirstNpmError(blob) || blob.trim().slice(0, 400)],
65
+ hint: `close editors / antivirus holding the .tgz, then retry`,
66
+ };
67
+ }
68
+ // Fallthrough: raw output.
69
+ return {
70
+ summary: `npm pack failed`,
71
+ details: [
72
+ output.trim() ? `Output: ${output.trim()}` : '',
73
+ stderr.trim() ? `Error: ${stderr.trim()}` : '',
74
+ ].filter(Boolean),
75
+ };
76
+ }
77
+ /** Given a nested-node_modules path like
78
+ * `node_modules/@scope/sibling/node_modules/transitive`
79
+ * or `../sibling/node_modules/transitive`,
80
+ * find the outermost dep name and resolve it to a file: sibling declared
81
+ * in pkg.dependencies / pkg['.dependencies'] / etc. */
82
+ function resolveSiblingFromNestedPath(nestedPath, cwd, pkg) {
83
+ const normalized = nestedPath.replace(/\\/g, '/');
84
+ // Prefer the sibling-relative shape first: `../iflow-direct/node_modules/undici-types`.
85
+ // The *outermost* thing is the sibling dir, not the nested transitive.
86
+ const siblingMatch = normalized.match(/^(?:\.\.\/)+((?:@[^/]+\/)?[^/]+)\/node_modules\//);
87
+ if (siblingMatch) {
88
+ const absSibling = path.resolve(cwd, normalized.split('/node_modules/')[0]);
89
+ const resolved = findFileDepByPath(pkg, cwd, absSibling);
90
+ if (resolved)
91
+ return resolved;
92
+ return { name: siblingMatch[1], filePath: absSibling };
93
+ }
94
+ // Otherwise use the first `node_modules/<name>` segment.
95
+ const nmMatch = normalized.match(/(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)/);
96
+ const candidateName = nmMatch?.[1];
97
+ if (!candidateName)
98
+ return undefined;
99
+ // Try to confirm it's a file: dep by checking pkg.dependencies and .dependencies.
100
+ const allDeps = {};
101
+ for (const key of ['dependencies', '.dependencies', 'devDependencies', '.devDependencies']) {
102
+ if (pkg && pkg[key] && typeof pkg[key] === 'object')
103
+ Object.assign(allDeps, pkg[key]);
104
+ }
105
+ const spec = allDeps[candidateName];
106
+ if (spec && spec.startsWith('file:')) {
107
+ return { name: candidateName, filePath: path.resolve(cwd, spec.slice('file:'.length)) };
108
+ }
109
+ // Even if not explicitly file:, still useful to name it.
110
+ return { name: candidateName, filePath: path.resolve(cwd, 'node_modules', candidateName) };
111
+ }
112
+ /** Scan pkg deps for a file: entry whose resolved absolute path equals absTarget. */
113
+ function findFileDepByPath(pkg, cwd, absTarget) {
114
+ const target = path.resolve(absTarget).toLowerCase();
115
+ for (const key of ['dependencies', '.dependencies', 'devDependencies', '.devDependencies']) {
116
+ const deps = pkg && pkg[key];
117
+ if (!deps || typeof deps !== 'object')
118
+ continue;
119
+ for (const [name, spec] of Object.entries(deps)) {
120
+ if (typeof spec !== 'string' || !spec.startsWith('file:'))
121
+ continue;
122
+ const abs = path.resolve(cwd, spec.slice('file:'.length)).toLowerCase();
123
+ if (abs === target)
124
+ return { name, filePath: abs };
125
+ }
126
+ }
127
+ return undefined;
128
+ }
129
+ /** Pull out the first arborist stack frame for display. */
130
+ function extractArboristFrame(blob) {
131
+ const m = blob.match(/at [^\n]*arborist[^\n]*/i);
132
+ return m ? m[0].trim() : undefined;
133
+ }
134
+ /** Pull out the first `npm error <message>` line. */
135
+ function extractFirstNpmError(blob) {
136
+ const m = blob.match(/npm error [^\n]+/i);
137
+ return m ? m[0].trim() : undefined;
138
+ }
139
+ /** Exposed for ad-hoc testing — not used by the main flow. */
140
+ export const _test = { resolveSiblingFromNestedPath, findFileDepByPath };
141
+ //# sourceMappingURL=diagnose.js.map
package/lib/types.d.ts CHANGED
@@ -82,6 +82,9 @@ export interface GlobalizeOptions {
82
82
  local?: boolean;
83
83
  /** Freeze node_modules: replace symlinks/junctions with real copies for network share use */
84
84
  freeze?: boolean;
85
+ /** Before `npm pack`, delete `node_modules/` inside each `file:` dep target.
86
+ * Works around arborist crashes when siblings have nested node_modules. */
87
+ cleanNestedModules?: boolean;
85
88
  /** Internal: signals this call is from workspace orchestrator */
86
89
  _fromWorkspace?: boolean;
87
90
  /** Internal: signals this call is from CLI (version already printed) */
package/lib.d.ts CHANGED
@@ -95,6 +95,9 @@ export interface GlobalizeOptions {
95
95
  local?: boolean;
96
96
  /** Freeze node_modules: replace symlinks/junctions with real copies for network share use */
97
97
  freeze?: boolean;
98
+ /** Before `npm pack`, delete `node_modules/` inside each `file:` dep target.
99
+ * Works around arborist crashes when siblings have nested node_modules. */
100
+ cleanNestedModules?: boolean;
98
101
  /** Internal: auto-initialize git repos without prompting (user chose "all") */
99
102
  autoInit?: boolean;
100
103
  /** Internal: signals this call is from workspace orchestrator */
package/lib.js CHANGED
@@ -31,6 +31,7 @@ import libversion from 'libnpmversion';
31
31
  import JSON5 from 'json5';
32
32
  import { fileURLToPath } from 'url';
33
33
  import { themeColors } from '@bobfrankston/themecolors';
34
+ import { diagnoseNpmPackFailure } from './lib/diagnose.js';
34
35
  /** Semantic color functions — adapts to terminal light/dark theme */
35
36
  const colors = themeColors();
36
37
  const _buildIssues = [];
@@ -235,7 +236,7 @@ export function writeConfig(dir, config, explicitKeys) {
235
236
  const existing = readConfig(dir);
236
237
  // Filter out temporary flags and default values (unless explicitly set)
237
238
  const filtered = {};
238
- const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish', 'once']);
239
+ const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish', 'once', 'cleanNestedModules']);
239
240
  for (const [key, value] of Object.entries(config)) {
240
241
  if (omitKeys.has(key))
241
242
  continue;
@@ -1261,6 +1262,39 @@ function waitForNpmVersion(pkgName, version, maxWaitMs = 90000) {
1261
1262
  process.stdout.write(' timed out\n');
1262
1263
  return false;
1263
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
+ function cleanNestedDepModules(pkg, cwd, verbose) {
1269
+ const cleaned = [];
1270
+ const seen = new Set();
1271
+ for (const key of ['.dependencies', '.devDependencies', 'dependencies', 'devDependencies']) {
1272
+ const deps = pkg?.[key];
1273
+ if (!deps || typeof deps !== 'object')
1274
+ continue;
1275
+ for (const [name, spec] of Object.entries(deps)) {
1276
+ if (seen.has(name))
1277
+ continue;
1278
+ if (typeof spec !== 'string' || !spec.startsWith('file:'))
1279
+ continue;
1280
+ const target = path.resolve(cwd, spec.slice('file:'.length));
1281
+ const nm = path.join(target, 'node_modules');
1282
+ if (!fs.existsSync(nm))
1283
+ continue;
1284
+ seen.add(name);
1285
+ try {
1286
+ fs.rmSync(nm, { recursive: true, force: true });
1287
+ cleaned.push(name);
1288
+ if (verbose)
1289
+ console.log(colors.dim(` cleaned ${nm}`));
1290
+ }
1291
+ catch (e) {
1292
+ console.error(colors.yellow(` warning: could not clean ${nm}: ${e.message}`));
1293
+ }
1294
+ }
1295
+ }
1296
+ return cleaned;
1297
+ }
1264
1298
  /** Run npm install -g with retries for registry propagation delay */
1265
1299
  function installGlobalWithRetry(pkgSpec, cwd, maxRetries = 3) {
1266
1300
  let result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
@@ -2976,7 +3010,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2976
3010
  console.log(colors.yellow('Local branch is behind remote.'));
2977
3011
  if (rebase) {
2978
3012
  console.log('Rebasing local changes (--rebase)...');
2979
- const rebaseResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: false });
3013
+ const rebaseResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: false });
2980
3014
  if (!rebaseResult.success) {
2981
3015
  console.error(colors.red('ERROR: Rebase failed.'));
2982
3016
  console.error('You may need to resolve conflicts manually.');
@@ -3872,7 +3906,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3872
3906
  }
3873
3907
  // Pull latest from remote before version bump to avoid push rejection
3874
3908
  if (currentGitStatus.hasRemote && !dryRun) {
3875
- const pullResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: true });
3909
+ const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: true });
3876
3910
  if (!pullResult.success) {
3877
3911
  console.error(colors.yellow('Warning: git pull --rebase failed before version bump'));
3878
3912
  if (verbose) {
@@ -3973,7 +4007,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3973
4007
  // Version bump + tag succeeded locally; only the push failed.
3974
4008
  // Auto-pull --rebase and retry the push.
3975
4009
  console.log(colors.yellow('\nLocal branch is behind remote — pulling with rebase...'));
3976
- const pullResult = runCommand('git', ['pull', '--rebase'], { cwd });
4010
+ const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd });
3977
4011
  if (pullResult.success) {
3978
4012
  console.log(colors.green(' ✓ Rebased onto remote'));
3979
4013
  const pushResult = runCommand('git', ['push'], { cwd });
@@ -4236,13 +4270,31 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4236
4270
  if (verbose) {
4237
4271
  console.log(colors.green(`✓ Authenticated as ${authStatus.username}`));
4238
4272
  }
4273
+ // Optionally clean nested node_modules in file: dep targets before pack
4274
+ 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(', ')}`));
4278
+ }
4279
+ else if (verbose) {
4280
+ console.log(colors.dim(' --clean-nested-modules: nothing to clean'));
4281
+ }
4282
+ }
4239
4283
  // Create tarball first
4240
4284
  const packResult = runCommand('npm', ['pack'], { cwd, silent: true });
4241
4285
  if (!packResult.success) {
4242
- console.error(colors.red('ERROR: Failed to create package tarball'));
4243
- console.error(colors.yellow('Output:'), packResult.output);
4244
- console.error(colors.yellow('Error:'), packResult.stderr);
4245
- recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm pack failed');
4286
+ const d = diagnoseNpmPackFailure(cwd, packResult.output, packResult.stderr, pkg);
4287
+ console.error(colors.red(`ERROR: ${d.summary}`));
4288
+ for (const line of d.details)
4289
+ console.error(colors.yellow(' ' + line));
4290
+ if (d.referencedModule)
4291
+ console.error(colors.yellow(` Caused by referenced module: ${d.referencedModule}`));
4292
+ if (d.hint)
4293
+ console.error(colors.yellow(` Hint: ${d.hint}`));
4294
+ if (d.summary.includes('arborist') && !options.cleanNestedModules) {
4295
+ console.error(colors.yellow(` Retry with --clean-nested-modules to wipe node_modules/ inside each file: dep target`));
4296
+ }
4297
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', d.referencedModule ? `${d.summary} (via ${d.referencedModule})` : d.summary);
4246
4298
  return false;
4247
4299
  }
4248
4300
  // Get the tarball filename from npm pack output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.150",
3
+ "version": "1.0.152",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@bobfrankston/freezepak": "^0.1.7",
35
- "@bobfrankston/importgen": "^0.1.33",
35
+ "@bobfrankston/importgen": "^0.1.34",
36
36
  "@bobfrankston/themecolors": "^0.1.5",
37
37
  "@bobfrankston/userconfig": "^1.0.7",
38
38
  "@npmcli/package-json": "^7.0.4",
@@ -60,7 +60,7 @@
60
60
  ".transformedSnapshot": {
61
61
  "dependencies": {
62
62
  "@bobfrankston/freezepak": "^0.1.7",
63
- "@bobfrankston/importgen": "^0.1.33",
63
+ "@bobfrankston/importgen": "^0.1.34",
64
64
  "@bobfrankston/themecolors": "^0.1.5",
65
65
  "@bobfrankston/userconfig": "^1.0.7",
66
66
  "@npmcli/package-json": "^7.0.4",