@bobfrankston/npmglobalize 1.0.150 → 1.0.151
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/lib/diagnose.d.ts +43 -0
- package/lib/diagnose.js +141 -0
- package/lib.js +13 -7
- package/package.json +1 -1
|
@@ -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
|
package/lib/diagnose.js
ADDED
|
@@ -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.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 = [];
|
|
@@ -2976,7 +2977,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2976
2977
|
console.log(colors.yellow('Local branch is behind remote.'));
|
|
2977
2978
|
if (rebase) {
|
|
2978
2979
|
console.log('Rebasing local changes (--rebase)...');
|
|
2979
|
-
const rebaseResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: false });
|
|
2980
|
+
const rebaseResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: false });
|
|
2980
2981
|
if (!rebaseResult.success) {
|
|
2981
2982
|
console.error(colors.red('ERROR: Rebase failed.'));
|
|
2982
2983
|
console.error('You may need to resolve conflicts manually.');
|
|
@@ -3872,7 +3873,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3872
3873
|
}
|
|
3873
3874
|
// Pull latest from remote before version bump to avoid push rejection
|
|
3874
3875
|
if (currentGitStatus.hasRemote && !dryRun) {
|
|
3875
|
-
const pullResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: true });
|
|
3876
|
+
const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: true });
|
|
3876
3877
|
if (!pullResult.success) {
|
|
3877
3878
|
console.error(colors.yellow('Warning: git pull --rebase failed before version bump'));
|
|
3878
3879
|
if (verbose) {
|
|
@@ -3973,7 +3974,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3973
3974
|
// Version bump + tag succeeded locally; only the push failed.
|
|
3974
3975
|
// Auto-pull --rebase and retry the push.
|
|
3975
3976
|
console.log(colors.yellow('\nLocal branch is behind remote — pulling with rebase...'));
|
|
3976
|
-
const pullResult = runCommand('git', ['pull', '--rebase'], { cwd });
|
|
3977
|
+
const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd });
|
|
3977
3978
|
if (pullResult.success) {
|
|
3978
3979
|
console.log(colors.green(' ✓ Rebased onto remote'));
|
|
3979
3980
|
const pushResult = runCommand('git', ['push'], { cwd });
|
|
@@ -4239,10 +4240,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4239
4240
|
// Create tarball first
|
|
4240
4241
|
const packResult = runCommand('npm', ['pack'], { cwd, silent: true });
|
|
4241
4242
|
if (!packResult.success) {
|
|
4242
|
-
|
|
4243
|
-
console.error(colors.
|
|
4244
|
-
|
|
4245
|
-
|
|
4243
|
+
const d = diagnoseNpmPackFailure(cwd, packResult.output, packResult.stderr, pkg);
|
|
4244
|
+
console.error(colors.red(`ERROR: ${d.summary}`));
|
|
4245
|
+
for (const line of d.details)
|
|
4246
|
+
console.error(colors.yellow(' ' + line));
|
|
4247
|
+
if (d.referencedModule)
|
|
4248
|
+
console.error(colors.yellow(` Caused by referenced module: ${d.referencedModule}`));
|
|
4249
|
+
if (d.hint)
|
|
4250
|
+
console.error(colors.yellow(` Hint: ${d.hint}`));
|
|
4251
|
+
recordBuildIssue(pkg.name || path.basename(cwd), 'error', d.referencedModule ? `${d.summary} (via ${d.referencedModule})` : d.summary);
|
|
4246
4252
|
return false;
|
|
4247
4253
|
}
|
|
4248
4254
|
// Get the tarball filename from npm pack output
|