@bobfrankston/npmglobalize 1.0.149 → 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/README.md +22 -0
- package/ignorepatterns.json5 +1 -1
- package/lib/diagnose.d.ts +43 -0
- package/lib/diagnose.js +141 -0
- package/lib.d.ts +1 -0
- package/lib.js +167 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,28 @@ Errors abort (unless `--force`); warnings prompt to continue.
|
|
|
94
94
|
|
|
95
95
|
Skip the prescan with `-no-prescan` / `-nps`.
|
|
96
96
|
|
|
97
|
+
#### Workspace skip prescan
|
|
98
|
+
|
|
99
|
+
In workspace mode, before processing begins, each package is also checked to decide whether it actually needs work. A package is **skipped** (no rebuild, no version bump, no publish) only if **all** of these are true:
|
|
100
|
+
|
|
101
|
+
1. **Working tree clean** — `git status --porcelain .` reports no uncommitted changes for that package's directory.
|
|
102
|
+
2. **Version already on npm** — the version in its `package.json` is published to the registry, and no non-bookkeeping commit (anything outside "Pre-release commit" / "Restore file: dependencies" / "Pre-version cleanup" / "Untrack node_modules") is newer than that version's publish timestamp.
|
|
103
|
+
3. **Build is fresh** — every `.ts` source file (excluding `.d.ts`) has a sibling `.js` whose mtime is ≥ the `.ts` mtime. A missing `.js` or an older `.js` counts as stale.
|
|
104
|
+
4. **No sibling workspace `file:` dep flagged for work** — if another workspace package depends on this one via `file:`, and that dep is being updated in this run, this one is also processed (propagates through the graph in topological order).
|
|
105
|
+
|
|
106
|
+
If any condition fails, the package is processed. The prescan prints one line per package with `⟳` for "will process" (and the reason) or `✓` for "skip". Example:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
⟳ mlproc — uncommitted changes
|
|
110
|
+
⟳ pzip — stale build (index.ts newer than index.js)
|
|
111
|
+
⟳ stage — dep pzip is being updated
|
|
112
|
+
✓ puller — skip (clean, published, build fresh)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The summary row for a skipped package shows `– name v1.0.0 (skipped — already up to date)`.
|
|
116
|
+
|
|
117
|
+
Use `--force` or `--force-publish` to bypass the skip prescan and process every package.
|
|
118
|
+
|
|
97
119
|
**Force republish** all file: dependencies even if versions exist:
|
|
98
120
|
```bash
|
|
99
121
|
npmglobalize --force-publish
|
package/ignorepatterns.json5
CHANGED
|
@@ -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.d.ts
CHANGED
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 = [];
|
|
@@ -810,6 +811,49 @@ function hasUnpublishedTransitiveDeps(packageName, pkg, baseDir, verbose) {
|
|
|
810
811
|
}
|
|
811
812
|
return false;
|
|
812
813
|
}
|
|
814
|
+
/** Walk a dir and return true if any `.ts` source has mtime newer than its sibling `.js`
|
|
815
|
+
* (or the `.js` is missing). Skips node_modules/prev/cruft/.git and declaration files. */
|
|
816
|
+
function hasStaleBuild(rootDir) {
|
|
817
|
+
const SKIP = new Set(['node_modules', 'prev', 'cruft', '.git']);
|
|
818
|
+
function walk(dir) {
|
|
819
|
+
let entries;
|
|
820
|
+
try {
|
|
821
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
return { stale: false };
|
|
825
|
+
}
|
|
826
|
+
for (const e of entries) {
|
|
827
|
+
const full = path.join(dir, e.name);
|
|
828
|
+
if (e.isDirectory()) {
|
|
829
|
+
if (SKIP.has(e.name.toLowerCase()))
|
|
830
|
+
continue;
|
|
831
|
+
const sub = walk(full);
|
|
832
|
+
if (sub.stale)
|
|
833
|
+
return sub;
|
|
834
|
+
}
|
|
835
|
+
else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
|
|
836
|
+
const jsPath = full.slice(0, -3) + '.js';
|
|
837
|
+
try {
|
|
838
|
+
const tsStat = fs.statSync(full);
|
|
839
|
+
let jsStat;
|
|
840
|
+
try {
|
|
841
|
+
jsStat = fs.statSync(jsPath);
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
return { stale: true, example: `${e.name} (no .js)` };
|
|
845
|
+
}
|
|
846
|
+
if (tsStat.mtimeMs > jsStat.mtimeMs) {
|
|
847
|
+
return { stale: true, example: `${e.name} newer than ${path.basename(jsPath)}` };
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
catch { /* unreadable — ignore */ }
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return { stale: false };
|
|
854
|
+
}
|
|
855
|
+
return walk(rootDir);
|
|
856
|
+
}
|
|
813
857
|
/** Check if local package directory has changes newer than the npm-published version.
|
|
814
858
|
* Detects uncommitted changes or commits made after the version was published. */
|
|
815
859
|
function hasLocalChanges(packageName, version, targetPath, verbose) {
|
|
@@ -2933,7 +2977,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2933
2977
|
console.log(colors.yellow('Local branch is behind remote.'));
|
|
2934
2978
|
if (rebase) {
|
|
2935
2979
|
console.log('Rebasing local changes (--rebase)...');
|
|
2936
|
-
const rebaseResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: false });
|
|
2980
|
+
const rebaseResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: false });
|
|
2937
2981
|
if (!rebaseResult.success) {
|
|
2938
2982
|
console.error(colors.red('ERROR: Rebase failed.'));
|
|
2939
2983
|
console.error('You may need to resolve conflicts manually.');
|
|
@@ -3143,9 +3187,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3143
3187
|
}
|
|
3144
3188
|
// Don't set "private": true in package.json - that blocks all publishing
|
|
3145
3189
|
console.log(`Package '${pkg.name}' will publish as PRIVATE (restricted access).`);
|
|
3146
|
-
if (!currentAccess) {
|
|
3147
|
-
console.log(colors.dim(` First publish - requires paid npm account`));
|
|
3148
|
-
}
|
|
3149
3190
|
}
|
|
3150
3191
|
else if (effectiveNpmVisibility === 'public') {
|
|
3151
3192
|
// User explicitly wants public (or confirmed via prompt)
|
|
@@ -3832,7 +3873,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3832
3873
|
}
|
|
3833
3874
|
// Pull latest from remote before version bump to avoid push rejection
|
|
3834
3875
|
if (currentGitStatus.hasRemote && !dryRun) {
|
|
3835
|
-
const pullResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: true });
|
|
3876
|
+
const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: true });
|
|
3836
3877
|
if (!pullResult.success) {
|
|
3837
3878
|
console.error(colors.yellow('Warning: git pull --rebase failed before version bump'));
|
|
3838
3879
|
if (verbose) {
|
|
@@ -3908,7 +3949,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3908
3949
|
}
|
|
3909
3950
|
catch (error) {
|
|
3910
3951
|
let autoFixed = false;
|
|
3911
|
-
console.error(colors.red(
|
|
3952
|
+
console.error(colors.red(`ERROR: Version bump failed in ${cwd} (${pkg.name || '<unnamed>'}):`), error.message);
|
|
3912
3953
|
// Show additional error details if available
|
|
3913
3954
|
if (error.stderr || error.stdout || error.code) {
|
|
3914
3955
|
if (error.stderr)
|
|
@@ -3933,7 +3974,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3933
3974
|
// Version bump + tag succeeded locally; only the push failed.
|
|
3934
3975
|
// Auto-pull --rebase and retry the push.
|
|
3935
3976
|
console.log(colors.yellow('\nLocal branch is behind remote — pulling with rebase...'));
|
|
3936
|
-
const pullResult = runCommand('git', ['pull', '--rebase'], { cwd });
|
|
3977
|
+
const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd });
|
|
3937
3978
|
if (pullResult.success) {
|
|
3938
3979
|
console.log(colors.green(' ✓ Rebased onto remote'));
|
|
3939
3980
|
const pushResult = runCommand('git', ['push'], { cwd });
|
|
@@ -4065,8 +4106,39 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4065
4106
|
return false;
|
|
4066
4107
|
}
|
|
4067
4108
|
}
|
|
4109
|
+
else if (/paths are ignored by one of your \.gitignore files/i.test(combinedOutput)) {
|
|
4110
|
+
// git add refused to stage files (usually package.json) because a
|
|
4111
|
+
// .gitignore rule matches. Ask git which exact line is to blame.
|
|
4112
|
+
const ignoredMatch = combinedOutput.match(/paths are ignored by one of your \.gitignore files:\s*\n([\s\S]*?)(?:hint:|$)/i);
|
|
4113
|
+
const ignoredFiles = (ignoredMatch ? ignoredMatch[1] : '')
|
|
4114
|
+
.split('\n').map(s => s.trim()).filter(Boolean);
|
|
4115
|
+
console.error(colors.red(`\n.gitignore in ${cwd} is blocking required file(s):`));
|
|
4116
|
+
for (const f of ignoredFiles) {
|
|
4117
|
+
const chk = spawnSafe('git', ['-C', cwd, 'check-ignore', '-v', f], { encoding: 'utf-8', stdio: 'pipe' });
|
|
4118
|
+
if (chk.status === 0 && chk.stdout?.trim()) {
|
|
4119
|
+
// Output format: "<source>:<line>:<pattern>\t<file>"
|
|
4120
|
+
const m = chk.stdout.trim().match(/^(.+?):(\d+):(.+?)\t(.+)$/);
|
|
4121
|
+
if (m) {
|
|
4122
|
+
const [, src, line, pattern, file] = m;
|
|
4123
|
+
console.error(colors.red(` ${file} — matched by ${src}:${line} pattern: ${pattern}`));
|
|
4124
|
+
if (pattern.trim() === '*') {
|
|
4125
|
+
console.error(colors.yellow(` → Line ${line} is a bare "*" which ignores EVERY file. Delete it.`));
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
else {
|
|
4129
|
+
console.error(colors.red(` ${f}`));
|
|
4130
|
+
console.error(colors.dim(` ${chk.stdout.trim()}`));
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
else {
|
|
4134
|
+
console.error(colors.red(` ${f}`));
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
console.error(colors.yellow(`\nEdit ${path.join(cwd, '.gitignore')} to remove/narrow the blocking pattern,`));
|
|
4138
|
+
console.error(colors.yellow(`or exempt with an explicit negation, e.g.: !package.json`));
|
|
4139
|
+
}
|
|
4068
4140
|
else if (error.message?.includes('unknown git error')) {
|
|
4069
|
-
console.error(colors.yellow(
|
|
4141
|
+
console.error(colors.yellow(`Unknown git error in ${cwd} — check git hooks, signing, or permissions`));
|
|
4070
4142
|
}
|
|
4071
4143
|
else if (combinedOutput.includes('gh013') || combinedOutput.includes('push protection') || combinedOutput.includes('push declined due to repository rule')) {
|
|
4072
4144
|
// GitHub push protection blocked a push (from postversion script)
|
|
@@ -4168,10 +4240,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4168
4240
|
// Create tarball first
|
|
4169
4241
|
const packResult = runCommand('npm', ['pack'], { cwd, silent: true });
|
|
4170
4242
|
if (!packResult.success) {
|
|
4171
|
-
|
|
4172
|
-
console.error(colors.
|
|
4173
|
-
|
|
4174
|
-
|
|
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);
|
|
4175
4252
|
return false;
|
|
4176
4253
|
}
|
|
4177
4254
|
// Get the tarball filename from npm pack output
|
|
@@ -4637,9 +4714,84 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
4637
4714
|
}
|
|
4638
4715
|
}
|
|
4639
4716
|
}
|
|
4717
|
+
// Prescan: decide which packages actually need processing so we don't waste
|
|
4718
|
+
// time rebuilding+republishing ones with no relevant changes.
|
|
4719
|
+
// A package is SKIPPED only if ALL of:
|
|
4720
|
+
// - working tree clean (no uncommitted changes)
|
|
4721
|
+
// - current version is on npm AND no commits newer than its publish time
|
|
4722
|
+
// - build not stale (every .ts source has a .js no older than it)
|
|
4723
|
+
// - no sibling workspace file: dep was itself flagged for processing
|
|
4724
|
+
const flaggedForWork = new Set();
|
|
4725
|
+
const skipReasons = new Map();
|
|
4726
|
+
if (!options.force && !options.forcePublish) {
|
|
4727
|
+
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4728
|
+
console.log(` Prescan: checking ${filteredOrder.length} package(s)...`);
|
|
4729
|
+
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4730
|
+
const wsDeps = new Map(); // pkg → workspace-sibling file: deps
|
|
4731
|
+
for (const pkgInfo of packages) {
|
|
4732
|
+
const sibs = new Set();
|
|
4733
|
+
const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
|
|
4734
|
+
for (const [dn, dv] of Object.entries(deps)) {
|
|
4735
|
+
if (typeof dv === 'string' && dv.startsWith('file:') && wsNameSet.has(dn)) {
|
|
4736
|
+
sibs.add(dn);
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
wsDeps.set(pkgInfo.name, sibs);
|
|
4740
|
+
}
|
|
4741
|
+
for (const pkgName of filteredOrder) {
|
|
4742
|
+
const pkgInfo = packages.find(p => p.name === pkgName);
|
|
4743
|
+
if (!pkgInfo)
|
|
4744
|
+
continue;
|
|
4745
|
+
let reason;
|
|
4746
|
+
const status = spawnSafe('git', ['-C', pkgInfo.dir, 'status', '--porcelain', '.'], { encoding: 'utf-8', stdio: 'pipe', shell: true });
|
|
4747
|
+
if (status.status === 0 && status.stdout.trim()) {
|
|
4748
|
+
reason = 'uncommitted changes';
|
|
4749
|
+
}
|
|
4750
|
+
else {
|
|
4751
|
+
const version = pkgInfo.pkg.version;
|
|
4752
|
+
if (!checkVersionExists(pkgInfo.name, version)) {
|
|
4753
|
+
reason = `v${version} not on npm`;
|
|
4754
|
+
}
|
|
4755
|
+
else if (hasLocalChanges(pkgInfo.name, version, pkgInfo.dir, false)) {
|
|
4756
|
+
reason = 'commits since last publish';
|
|
4757
|
+
}
|
|
4758
|
+
else {
|
|
4759
|
+
const stale = hasStaleBuild(pkgInfo.dir);
|
|
4760
|
+
if (stale.stale) {
|
|
4761
|
+
reason = `stale build (${stale.example})`;
|
|
4762
|
+
}
|
|
4763
|
+
else {
|
|
4764
|
+
const flaggedSib = [...(wsDeps.get(pkgName) || [])].find(s => flaggedForWork.has(s));
|
|
4765
|
+
if (flaggedSib)
|
|
4766
|
+
reason = `dep ${flaggedSib} is being updated`;
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
if (reason) {
|
|
4771
|
+
flaggedForWork.add(pkgName);
|
|
4772
|
+
console.log(` ${colors.yellow('⟳')} ${pkgName} — ${reason}`);
|
|
4773
|
+
}
|
|
4774
|
+
else {
|
|
4775
|
+
skipReasons.set(pkgName, 'clean, published, build fresh');
|
|
4776
|
+
console.log(` ${colors.green('✓')} ${pkgName} — skip (clean, published, build fresh)`);
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
console.log('');
|
|
4780
|
+
if (flaggedForWork.size === 0) {
|
|
4781
|
+
console.log(colors.green('Nothing to do — all packages up to date.'));
|
|
4782
|
+
console.log('');
|
|
4783
|
+
return { success: true, packages: [], publishOrder };
|
|
4784
|
+
}
|
|
4785
|
+
console.log(colors.dim(`Prescan: ${flaggedForWork.size} to process, ${skipReasons.size} skip. (Use --force to process all.)`));
|
|
4786
|
+
console.log('');
|
|
4787
|
+
}
|
|
4640
4788
|
// Process each package in dependency order
|
|
4641
4789
|
const results = [];
|
|
4642
4790
|
for (const pkgName of filteredOrder) {
|
|
4791
|
+
if (skipReasons.has(pkgName)) {
|
|
4792
|
+
results.push({ name: pkgName, dir: packages.find(p => p.name === pkgName).dir, success: true, version: packages.find(p => p.name === pkgName).pkg.version, skipped: true });
|
|
4793
|
+
continue;
|
|
4794
|
+
}
|
|
4643
4795
|
const pkgInfo = packages.find(p => p.name === pkgName);
|
|
4644
4796
|
if (!pkgInfo)
|
|
4645
4797
|
continue;
|
|
@@ -4692,10 +4844,11 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
4692
4844
|
console.log(allSuccess ? colors.green('✓ Workspace Summary') : colors.red('✗ Workspace Summary'));
|
|
4693
4845
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4694
4846
|
for (const r of results) {
|
|
4695
|
-
const status = r.success ? colors.green('✓') : colors.red('✗');
|
|
4847
|
+
const status = r.skipped ? colors.dim('–') : (r.success ? colors.green('✓') : colors.red('✗'));
|
|
4696
4848
|
const ver = r.version ? ` v${r.version}` : '';
|
|
4697
4849
|
const err = r.error ? colors.red(` (${r.error})`) : '';
|
|
4698
|
-
|
|
4850
|
+
const tag = r.skipped ? colors.dim(' (skipped — already up to date)') : '';
|
|
4851
|
+
console.log(` ${status} ${r.name}${ver}${err}${tag}`);
|
|
4699
4852
|
}
|
|
4700
4853
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
4701
4854
|
console.log('');
|