@bobfrankston/npmglobalize 1.0.192 → 1.0.194
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 +36 -1
- package/lib.d.ts +44 -0
- package/lib.js +282 -10
- package/package.json +1 -1
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, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations } from './lib.js';
|
|
5
|
+
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations } from './lib.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { colors } from './colors.js';
|
|
@@ -121,6 +121,11 @@ Other Options:
|
|
|
121
121
|
compilerOptions removed in TypeScript 7 (deprecated
|
|
122
122
|
moduleResolution, target es3, deprecated flags). Lists a
|
|
123
123
|
migration to-do list and exits. Writes nothing.
|
|
124
|
+
-tsfix, -ts7-fix
|
|
125
|
+
Fix-only: apply the TS7 tsconfig migration to this package
|
|
126
|
+
and its file: deps, then exit. Only tsconfig files change —
|
|
127
|
+
no build, no commit, no push, no publish. Use on any local
|
|
128
|
+
directory: npmglobalize <path> -tsfix
|
|
124
129
|
-show Show package.json dependency changes
|
|
125
130
|
-package, -pkg Update package.json scripts to use npmglobalize
|
|
126
131
|
-h, -help Show this help
|
|
@@ -161,6 +166,7 @@ function parseArgs(args) {
|
|
|
161
166
|
version: false,
|
|
162
167
|
error: '',
|
|
163
168
|
ts7Report: false,
|
|
169
|
+
ts7Fix: false,
|
|
164
170
|
explicitKeys: new Set()
|
|
165
171
|
};
|
|
166
172
|
const unrecognized = [];
|
|
@@ -182,6 +188,10 @@ function parseArgs(args) {
|
|
|
182
188
|
case '-deprecation-report':
|
|
183
189
|
options.ts7Report = true;
|
|
184
190
|
break;
|
|
191
|
+
case '-tsfix':
|
|
192
|
+
case '-ts7-fix':
|
|
193
|
+
options.ts7Fix = true;
|
|
194
|
+
break;
|
|
185
195
|
case '-patch':
|
|
186
196
|
options.bump = 'patch';
|
|
187
197
|
break;
|
|
@@ -515,6 +525,31 @@ export async function main() {
|
|
|
515
525
|
printTs7Report(cwd);
|
|
516
526
|
process.exit(0);
|
|
517
527
|
}
|
|
528
|
+
// Fix-only TS7 tsconfig migration (no build / commit / push / publish).
|
|
529
|
+
if (cliOptions.ts7Fix) {
|
|
530
|
+
console.log('');
|
|
531
|
+
console.log(colors.accent('━━━ TypeScript 7 tsconfig fix ━━━'));
|
|
532
|
+
const { changedDirs, remaining } = fixTs7Deprecations(cwd);
|
|
533
|
+
if (changedDirs.length === 0) {
|
|
534
|
+
console.log(colors.success(' ✓ Nothing to fix — no removed-in-TS7 compilerOptions in writable tsconfigs.'));
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log(colors.success(` ✓ Patched tsconfig(s) in ${changedDirs.length} package(s):`));
|
|
538
|
+
for (const dir of changedDirs)
|
|
539
|
+
console.log(` ${dir}`);
|
|
540
|
+
console.log(colors.muted(' Rebuild to validate — NodeNext resolution may surface real import errors the old resolver hid.'));
|
|
541
|
+
}
|
|
542
|
+
if (remaining.length > 0) {
|
|
543
|
+
console.log(colors.warn(` ${remaining.length} item(s) could not be auto-fixed:`));
|
|
544
|
+
for (const f of remaining) {
|
|
545
|
+
console.log(colors.warn(` • ${f.name}: ${f.option}=${JSON.stringify(f.value)} (in ${f.definedIn})`));
|
|
546
|
+
console.log(colors.muted(` → ${f.action}`));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
console.log('');
|
|
550
|
+
printBuildSummary();
|
|
551
|
+
process.exit(remaining.length > 0 ? 1 : 0);
|
|
552
|
+
}
|
|
518
553
|
// Build file: deps topologically, then the target itself.
|
|
519
554
|
// Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
|
|
520
555
|
// whose source has changed since their last build.
|
package/lib.d.ts
CHANGED
|
@@ -347,6 +347,18 @@ export interface Ts7Finding {
|
|
|
347
347
|
* The same migration `migrateTsconfigDeprecations` applies automatically on a build
|
|
348
348
|
* failure; this report is the read-only, whole-graph inventory. */
|
|
349
349
|
export declare function reportTs7Deprecations(cwd: string): Ts7Finding[];
|
|
350
|
+
/** Standalone fix mode (`-tsfix`): apply `migrateTsconfigDeprecations` to `cwd`
|
|
351
|
+
* and every package reachable through its `file:` dep graph — the write
|
|
352
|
+
* counterpart of `reportTs7Deprecations`. No git, no version bump, no publish;
|
|
353
|
+
* only tsconfig files change. After migrating, any `ignoreDeprecations` whose
|
|
354
|
+
* package has nothing deprecated left is deleted too (kept, it would silence
|
|
355
|
+
* the next real TS7-removal error). Returns the dirs whose tsconfigs changed
|
|
356
|
+
* plus the findings that remain (e.g. options inherited from read-only
|
|
357
|
+
* node_modules bases). */
|
|
358
|
+
export declare function fixTs7Deprecations(cwd: string): {
|
|
359
|
+
changedDirs: string[];
|
|
360
|
+
remaining: Ts7Finding[];
|
|
361
|
+
};
|
|
350
362
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
351
363
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
352
364
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
@@ -356,6 +368,38 @@ export declare function ensureWorkspaceDepModules(rootDir: string, members: Arra
|
|
|
356
368
|
dir: string;
|
|
357
369
|
pkg: any;
|
|
358
370
|
}>, verbose?: boolean): Promise<void>;
|
|
371
|
+
/** Extract the lines a human needs from a failed npm command's output: the
|
|
372
|
+
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
373
|
+
* deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
|
|
374
|
+
export declare function distillNpmErrors(combined: string): string[];
|
|
375
|
+
/** Scan a puppeteer browser cache (layout <root>/<browser>/<platform-version>)
|
|
376
|
+
* and repair every version folder that has no browser executable in it — the
|
|
377
|
+
* residue of a truncated extraction (see extractBrowserArchive for the Node
|
|
378
|
+
* 26 extract-zip bug that produces these). Per corrupt folder:
|
|
379
|
+
* 1. If the downloaded archive still sits next to it (the usual case right
|
|
380
|
+
* after a truncating run — the hung extraction means puppeteer's cleanup
|
|
381
|
+
* never deleted it), finish the extraction with the system unzipper. This
|
|
382
|
+
* is the only path that converges on Node 26, where re-extraction by
|
|
383
|
+
* puppeteer truncates again every time.
|
|
384
|
+
* 2. Otherwise delete the folder so the next attempt re-downloads.
|
|
385
|
+
*
|
|
386
|
+
* Timing matters: a truncating install EXITS 0, so the poisoned cache hides
|
|
387
|
+
* behind a successful install — and the NEXT install deletes the leftover
|
|
388
|
+
* archive before erroring. So this must run right after a SUCCESSFUL install
|
|
389
|
+
* (while the archive is still there), not only on failure. */
|
|
390
|
+
export declare function verifyPuppeteerCache(cacheRoot?: string): {
|
|
391
|
+
repaired: string[];
|
|
392
|
+
removed: string[];
|
|
393
|
+
};
|
|
394
|
+
/** Detect puppeteer's corrupted-browser-cache failure ("browser folder exists
|
|
395
|
+
* but the executable is missing") in failed install output and repair the
|
|
396
|
+
* whole cache it points into (verifyPuppeteerCache — the installer checks
|
|
397
|
+
* browsers in parallel and only reports the first corrupt one, so siblings
|
|
398
|
+
* are usually poisoned too). Returns the folders repaired or removed. */
|
|
399
|
+
export declare function fixPuppeteerCorruptedCache(combined: string): {
|
|
400
|
+
repaired: string[];
|
|
401
|
+
removed: string[];
|
|
402
|
+
};
|
|
359
403
|
/** Run a command and return success status */
|
|
360
404
|
export declare function runCommand(cmd: string, args: string[], options?: {
|
|
361
405
|
silent?: boolean;
|
package/lib.js
CHANGED
|
@@ -2477,6 +2477,53 @@ export function reportTs7Deprecations(cwd) {
|
|
|
2477
2477
|
}
|
|
2478
2478
|
return findings;
|
|
2479
2479
|
}
|
|
2480
|
+
/** Standalone fix mode (`-tsfix`): apply `migrateTsconfigDeprecations` to `cwd`
|
|
2481
|
+
* and every package reachable through its `file:` dep graph — the write
|
|
2482
|
+
* counterpart of `reportTs7Deprecations`. No git, no version bump, no publish;
|
|
2483
|
+
* only tsconfig files change. After migrating, any `ignoreDeprecations` whose
|
|
2484
|
+
* package has nothing deprecated left is deleted too (kept, it would silence
|
|
2485
|
+
* the next real TS7-removal error). Returns the dirs whose tsconfigs changed
|
|
2486
|
+
* plus the findings that remain (e.g. options inherited from read-only
|
|
2487
|
+
* node_modules bases). */
|
|
2488
|
+
export function fixTs7Deprecations(cwd) {
|
|
2489
|
+
const changedDirs = [];
|
|
2490
|
+
for (const dir of collectFileDepDirs(cwd)) {
|
|
2491
|
+
if (migrateTsconfigDeprecations(dir))
|
|
2492
|
+
changedDirs.push(dir);
|
|
2493
|
+
}
|
|
2494
|
+
// Drop ignoreDeprecations wherever nothing deprecated remains in that package.
|
|
2495
|
+
let remaining = reportTs7Deprecations(cwd);
|
|
2496
|
+
const igdDirs = new Set(remaining.filter(f => f.option === 'ignoreDeprecations').map(f => f.dir));
|
|
2497
|
+
let removedIgd = false;
|
|
2498
|
+
for (const dir of igdDirs) {
|
|
2499
|
+
if (remaining.some(f => f.dir === dir && f.option !== 'ignoreDeprecations'))
|
|
2500
|
+
continue;
|
|
2501
|
+
const { chain } = resolveTsconfigChain(path.join(dir, 'tsconfig.json'));
|
|
2502
|
+
const igd = effectiveCompilerOption(chain, 'ignoreDeprecations');
|
|
2503
|
+
if (!igd)
|
|
2504
|
+
continue;
|
|
2505
|
+
const def = chain.find(c => c.path === igd.definedIn);
|
|
2506
|
+
if (!def?.writable)
|
|
2507
|
+
continue;
|
|
2508
|
+
try {
|
|
2509
|
+
const text = fs.readFileSync(def.path, 'utf-8');
|
|
2510
|
+
const patched = removeCompilerOptionLine(text, 'ignoreDeprecations');
|
|
2511
|
+
if (patched != null && patched !== text) {
|
|
2512
|
+
fs.writeFileSync(def.path, patched);
|
|
2513
|
+
removedIgd = true;
|
|
2514
|
+
if (!changedDirs.includes(dir))
|
|
2515
|
+
changedDirs.push(dir);
|
|
2516
|
+
console.log(colors.cyan(` Removed "ignoreDeprecations" from ${path.relative(dir, def.path) || 'tsconfig.json'} (${path.basename(dir)})`));
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
catch (error) {
|
|
2520
|
+
console.error(colors.yellow(` Could not remove ignoreDeprecations (${def.path}): ${error.message}`));
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
if (removedIgd)
|
|
2524
|
+
remaining = reportTs7Deprecations(cwd);
|
|
2525
|
+
return { changedDirs, remaining };
|
|
2526
|
+
}
|
|
2480
2527
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
2481
2528
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
2482
2529
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
@@ -2510,19 +2557,244 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
|
|
|
2510
2557
|
await suggestPnpmOnInstallFailure(`ensureWorkspaceDepModules: ${root}`);
|
|
2511
2558
|
}
|
|
2512
2559
|
}
|
|
2560
|
+
/** Extract the lines a human needs from a failed npm command's output: the
|
|
2561
|
+
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
2562
|
+
* deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
|
|
2563
|
+
export function distillNpmErrors(combined) {
|
|
2564
|
+
const key = [];
|
|
2565
|
+
const seen = new Set();
|
|
2566
|
+
for (const raw of combined.split('\n')) {
|
|
2567
|
+
const m = raw.trimEnd().match(/^npm (?:error|ERR!)\s?(.*)$/);
|
|
2568
|
+
if (!m || !m[1])
|
|
2569
|
+
continue;
|
|
2570
|
+
const msg = m[1];
|
|
2571
|
+
if (/^\s*at /.test(msg))
|
|
2572
|
+
continue; // stack frames
|
|
2573
|
+
if (/^[\s{}\[\]),]*$/.test(msg))
|
|
2574
|
+
continue; // object-dump punctuation lines
|
|
2575
|
+
if (seen.has(msg))
|
|
2576
|
+
continue;
|
|
2577
|
+
seen.add(msg);
|
|
2578
|
+
key.push(msg);
|
|
2579
|
+
}
|
|
2580
|
+
return key.slice(0, 12);
|
|
2581
|
+
}
|
|
2582
|
+
/** Write the full output of a failed npm command to a log file under the temp
|
|
2583
|
+
* dir and return its path (null if the write itself fails). */
|
|
2584
|
+
function saveNpmLog(label, content) {
|
|
2585
|
+
try {
|
|
2586
|
+
const dir = path.join(os.tmpdir(), 'npmglobalize-logs');
|
|
2587
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2588
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2589
|
+
const file = path.join(dir, `${label.replace(/[^a-z0-9.@-]+/gi, '_')}-${stamp}.log`);
|
|
2590
|
+
fs.writeFileSync(file, content);
|
|
2591
|
+
return file;
|
|
2592
|
+
}
|
|
2593
|
+
catch {
|
|
2594
|
+
return null;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
/** True if `dir` contains a browser executable somewhere below it. A healthy
|
|
2598
|
+
* puppeteer browser folder always has one; a truncated extraction doesn't
|
|
2599
|
+
* (extraction writes the small metadata files first — manifest, ABOUT,
|
|
2600
|
+
* licenses — so an interrupted one holds only those). */
|
|
2601
|
+
function containsBrowserExecutable(dir) {
|
|
2602
|
+
try {
|
|
2603
|
+
for (const entry of fs.readdirSync(dir, { recursive: true })) {
|
|
2604
|
+
const name = String(entry);
|
|
2605
|
+
if (process.platform === 'win32' ? /\.exe$/i.test(name) : /(^|[\\/])(chrome|chrome-headless-shell|firefox)$/.test(name))
|
|
2606
|
+
return true;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
catch {
|
|
2610
|
+
// unreadable — treat as not-a-healthy-browser; caller only acts on puppeteer cache paths
|
|
2611
|
+
}
|
|
2612
|
+
return false;
|
|
2613
|
+
}
|
|
2614
|
+
/** Extract a puppeteer browser archive with the SYSTEM unzipper into a fresh
|
|
2615
|
+
* version folder. Workaround for a third-party bug: puppeteer's installer
|
|
2616
|
+
* unpacks zips with the `extract-zip` package, which on Node 26 silently
|
|
2617
|
+
* truncates — its promise never resolves, the event loop drains, and the
|
|
2618
|
+
* process exits 0 after writing only the first few small files. The download
|
|
2619
|
+
* itself is fine (and the archive is left behind, since the hung promise
|
|
2620
|
+
* means puppeteer's cleanup never runs), so completing the extraction
|
|
2621
|
+
* ourselves both repairs the folder and converges where delete-and-retry
|
|
2622
|
+
* cannot. TODO: drop this when puppeteer/extract-zip fix Node 26 extraction. */
|
|
2623
|
+
function extractBrowserArchive(archivePath, folder) {
|
|
2624
|
+
// Git-Bash puts GNU tar (no zip support) first on PATH — use bsdtar
|
|
2625
|
+
// explicitly on Windows; macOS tar is bsdtar; Linux needs unzip.
|
|
2626
|
+
const [cmd, args] = process.platform === 'win32'
|
|
2627
|
+
? [path.join(process.env.SystemRoot || 'C:\\WINDOWS', 'system32', 'tar.exe'), ['-xf', archivePath, '-C', folder]]
|
|
2628
|
+
: process.platform === 'darwin'
|
|
2629
|
+
? ['tar', ['-xf', archivePath, '-C', folder]]
|
|
2630
|
+
: ['unzip', ['-oq', archivePath, '-d', folder]];
|
|
2631
|
+
try {
|
|
2632
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
2633
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
2634
|
+
const r = spawnSafe(cmd, args, { stdio: 'pipe', encoding: 'utf-8' });
|
|
2635
|
+
return r.status === 0 && containsBrowserExecutable(folder);
|
|
2636
|
+
}
|
|
2637
|
+
catch {
|
|
2638
|
+
return false;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
/** Scan a puppeteer browser cache (layout <root>/<browser>/<platform-version>)
|
|
2642
|
+
* and repair every version folder that has no browser executable in it — the
|
|
2643
|
+
* residue of a truncated extraction (see extractBrowserArchive for the Node
|
|
2644
|
+
* 26 extract-zip bug that produces these). Per corrupt folder:
|
|
2645
|
+
* 1. If the downloaded archive still sits next to it (the usual case right
|
|
2646
|
+
* after a truncating run — the hung extraction means puppeteer's cleanup
|
|
2647
|
+
* never deleted it), finish the extraction with the system unzipper. This
|
|
2648
|
+
* is the only path that converges on Node 26, where re-extraction by
|
|
2649
|
+
* puppeteer truncates again every time.
|
|
2650
|
+
* 2. Otherwise delete the folder so the next attempt re-downloads.
|
|
2651
|
+
*
|
|
2652
|
+
* Timing matters: a truncating install EXITS 0, so the poisoned cache hides
|
|
2653
|
+
* behind a successful install — and the NEXT install deletes the leftover
|
|
2654
|
+
* archive before erroring. So this must run right after a SUCCESSFUL install
|
|
2655
|
+
* (while the archive is still there), not only on failure. */
|
|
2656
|
+
export function verifyPuppeteerCache(cacheRoot) {
|
|
2657
|
+
const root = cacheRoot || process.env.PUPPETEER_CACHE_DIR || path.join(os.homedir(), '.cache', 'puppeteer');
|
|
2658
|
+
const result = { repaired: [], removed: [] };
|
|
2659
|
+
let browserDirs;
|
|
2660
|
+
try {
|
|
2661
|
+
browserDirs = fs.readdirSync(root, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
2662
|
+
}
|
|
2663
|
+
catch {
|
|
2664
|
+
return result; // no puppeteer cache — nothing to verify
|
|
2665
|
+
}
|
|
2666
|
+
for (const browser of browserDirs) {
|
|
2667
|
+
const browserDir = path.join(root, browser.name);
|
|
2668
|
+
let entries;
|
|
2669
|
+
try {
|
|
2670
|
+
entries = fs.readdirSync(browserDir, { withFileTypes: true });
|
|
2671
|
+
}
|
|
2672
|
+
catch {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
for (const entry of entries) {
|
|
2676
|
+
// version folders look like win64-148.0.7778.97 / mac_arm-131.0.6778.204
|
|
2677
|
+
if (!entry.isDirectory() || !/^[a-z0-9_]+-[\d.]+$/i.test(entry.name))
|
|
2678
|
+
continue;
|
|
2679
|
+
const folder = path.join(browserDir, entry.name);
|
|
2680
|
+
if (containsBrowserExecutable(folder))
|
|
2681
|
+
continue;
|
|
2682
|
+
try {
|
|
2683
|
+
// archive sits next to the version folder, named e.g.
|
|
2684
|
+
// chrome/148.0.7778.97-chrome-win64.zip for chrome/win64-148.0.7778.97
|
|
2685
|
+
const version = entry.name.slice(entry.name.indexOf('-') + 1);
|
|
2686
|
+
const archive = entries.find(e => e.isFile() && e.name.startsWith(`${version}-`) && e.name.endsWith('.zip'))?.name;
|
|
2687
|
+
if (archive && extractBrowserArchive(path.join(browserDir, archive), folder)) {
|
|
2688
|
+
fs.rmSync(path.join(browserDir, archive), { force: true }); // puppeteer's own cleanup never ran
|
|
2689
|
+
result.repaired.push(folder);
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
2693
|
+
result.removed.push(folder);
|
|
2694
|
+
}
|
|
2695
|
+
catch {
|
|
2696
|
+
// couldn't repair or delete (locked?) — leave it; install will report it
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return result;
|
|
2701
|
+
}
|
|
2702
|
+
/** Detect puppeteer's corrupted-browser-cache failure ("browser folder exists
|
|
2703
|
+
* but the executable is missing") in failed install output and repair the
|
|
2704
|
+
* whole cache it points into (verifyPuppeteerCache — the installer checks
|
|
2705
|
+
* browsers in parallel and only reports the first corrupt one, so siblings
|
|
2706
|
+
* are usually poisoned too). Returns the folders repaired or removed. */
|
|
2707
|
+
export function fixPuppeteerCorruptedCache(combined) {
|
|
2708
|
+
for (const m of combined.matchAll(/The browser folder \((.+?)\) exists but the executable \((.+?)\) is missing/g)) {
|
|
2709
|
+
const folder = m[1];
|
|
2710
|
+
// Only ever touch a puppeteer cache — never an arbitrary path parsed from output
|
|
2711
|
+
if (!/[\\/]puppeteer[\\/]/i.test(folder))
|
|
2712
|
+
continue;
|
|
2713
|
+
// cache layout is <root>/<browser>/<platform-version>
|
|
2714
|
+
return verifyPuppeteerCache(path.dirname(path.dirname(folder)));
|
|
2715
|
+
}
|
|
2716
|
+
return { repaired: [], removed: [] };
|
|
2717
|
+
}
|
|
2718
|
+
/** npm 11.17+ gates lifecycle scripts behind `allowScripts` — non-covered
|
|
2719
|
+
* scripts are skipped and the install still exits 0. A skipped postinstall
|
|
2720
|
+
* can leave the package broken at runtime (puppeteer without its browser,
|
|
2721
|
+
* native modules unbuilt), so surface the warning instead of burying it in
|
|
2722
|
+
* the captured output. */
|
|
2723
|
+
function warnSkippedScripts(combined) {
|
|
2724
|
+
const pkgs = [...combined.matchAll(/^npm warn allow-scripts\s+(\S+@[^\s(]+) \(/gm)];
|
|
2725
|
+
if (!pkgs.length)
|
|
2726
|
+
return;
|
|
2727
|
+
const names = pkgs.map(m => m[1].replace(/@[^@]+$/, ''));
|
|
2728
|
+
console.log(colors.yellow(` ⚠ npm skipped install scripts (allowScripts): ${pkgs.map(m => m[1]).join(', ')}`));
|
|
2729
|
+
console.log(colors.yellow(` Package may be broken at runtime. Allow with: npm config set allow-scripts=${names.join(',')} --location=user`));
|
|
2730
|
+
}
|
|
2513
2731
|
/** Run npm install -g with retries for registry propagation delay.
|
|
2514
2732
|
* Brand-new packages (first-time publish) take much longer to become
|
|
2515
2733
|
* installable than version bumps, so we use longer waits and more
|
|
2516
|
-
* attempts when `isNewPackage` is true.
|
|
2734
|
+
* attempts when `isNewPackage` is true.
|
|
2735
|
+
*
|
|
2736
|
+
* Output is captured, not streamed: npm failures produce hundreds of lines
|
|
2737
|
+
* (cleanup warns, stack traces, object dumps). The terminal gets a distilled
|
|
2738
|
+
* error summary; the full text of all attempts goes to a log file.
|
|
2739
|
+
* Known-fixable failures (puppeteer's corrupted browser cache) are repaired
|
|
2740
|
+
* automatically and granted one immediate bonus retry. */
|
|
2517
2741
|
async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRetries) {
|
|
2518
2742
|
const retries = maxRetries ?? (isNewPackage ? 6 : 3);
|
|
2519
2743
|
const delaySec = isNewPackage ? 30 : 10;
|
|
2520
|
-
let
|
|
2521
|
-
|
|
2744
|
+
let fullLog = '';
|
|
2745
|
+
let result = { success: false, output: '', stderr: '' };
|
|
2746
|
+
let attempt = 0;
|
|
2747
|
+
let cacheFixRounds = 0;
|
|
2748
|
+
while (true) {
|
|
2749
|
+
console.log(colors.cyan(`> npm install -g ${pkgSpec}`));
|
|
2750
|
+
result = await runCommandAsync('npm', ['install', '-g', pkgSpec], { cwd, silent: true });
|
|
2751
|
+
const combined = `${result.output}\n${result.stderr}`;
|
|
2752
|
+
fullLog += `===== attempt ${attempt + 1}: npm install -g ${pkgSpec} =====\n${combined}\n`;
|
|
2753
|
+
if (result.success) {
|
|
2754
|
+
const added = combined.match(/^(?:added|changed|removed) \d+ packages?.*$/m);
|
|
2755
|
+
if (added)
|
|
2756
|
+
console.log(colors.dim(` ${added[0]}`));
|
|
2757
|
+
warnSkippedScripts(combined);
|
|
2758
|
+
// A truncated puppeteer browser extraction exits 0, so the install
|
|
2759
|
+
// LOOKS fine while the cache is poisoned for every future install.
|
|
2760
|
+
// Verify now, while the leftover archive still allows a repair.
|
|
2761
|
+
const verify = verifyPuppeteerCache();
|
|
2762
|
+
for (const folder of verify.repaired)
|
|
2763
|
+
console.log(colors.yellow(` ↻ Repaired truncated puppeteer browser extraction (from cached archive): ${folder}`));
|
|
2764
|
+
for (const folder of verify.removed)
|
|
2765
|
+
console.log(colors.yellow(` ↻ Removed corrupted puppeteer browser cache (no archive to repair from — will re-download when needed): ${folder}`));
|
|
2766
|
+
return result;
|
|
2767
|
+
}
|
|
2768
|
+
const cleanupFails = (combined.match(/npm warn cleanup\s+\[Error: EPERM/g) || []).length;
|
|
2769
|
+
if (cleanupFails)
|
|
2770
|
+
console.log(colors.dim(` (npm couldn't remove ${cleanupFails} leftover dir(s) — EPERM cleanup warnings, details in log)`));
|
|
2771
|
+
for (const line of distillNpmErrors(combined))
|
|
2772
|
+
console.error(colors.red(` ${line}`));
|
|
2773
|
+
// Known-fixable failure: repair and retry immediately — the cause is
|
|
2774
|
+
// local, so waiting for registry propagation is pointless. Puppeteer
|
|
2775
|
+
// reports one corrupted browser per run, so allow a few repair rounds
|
|
2776
|
+
// (each must actually repair or remove a folder to earn another retry).
|
|
2777
|
+
if (cacheFixRounds < 3) {
|
|
2778
|
+
const fix = fixPuppeteerCorruptedCache(combined);
|
|
2779
|
+
if (fix.repaired.length || fix.removed.length) {
|
|
2780
|
+
cacheFixRounds++;
|
|
2781
|
+
for (const folder of fix.repaired)
|
|
2782
|
+
console.log(colors.yellow(` ↻ Repaired corrupted puppeteer browser cache (extracted cached archive): ${folder}`));
|
|
2783
|
+
for (const folder of fix.removed)
|
|
2784
|
+
console.log(colors.yellow(` ↻ Removed corrupted puppeteer browser cache (will re-download): ${folder}`));
|
|
2785
|
+
console.log(colors.yellow(` (puppeteer's extract-zip truncates silently on Node 26, leaving folders without executables) — retrying now`));
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
attempt++;
|
|
2790
|
+
if (attempt >= retries)
|
|
2791
|
+
break;
|
|
2522
2792
|
console.log(colors.yellow(` Retrying install (attempt ${attempt + 1}/${retries}) in ${delaySec} seconds...`));
|
|
2523
2793
|
sleepSync(delaySec * 1000);
|
|
2524
|
-
result = await runCommandAsync('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
|
|
2525
2794
|
}
|
|
2795
|
+
const logFile = saveNpmLog(`install-${pkgSpec === '.' ? path.basename(cwd) : pkgSpec}`, fullLog);
|
|
2796
|
+
if (logFile)
|
|
2797
|
+
console.error(colors.dim(` Full npm output: ${logFile}`));
|
|
2526
2798
|
return result;
|
|
2527
2799
|
}
|
|
2528
2800
|
/** Run a command and return success status */
|
|
@@ -5523,7 +5795,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5523
5795
|
if (pkg.bin && (install || link || wsl)) {
|
|
5524
5796
|
if (link) {
|
|
5525
5797
|
console.log(`Installing ${pkgName} globally from local directory (link)...`);
|
|
5526
|
-
const localInstallResult = await
|
|
5798
|
+
const localInstallResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5527
5799
|
if (localInstallResult.success) {
|
|
5528
5800
|
console.log(colors.green(`✓ Linked globally: ${pkgName}@${pkgVersion}`));
|
|
5529
5801
|
}
|
|
@@ -5538,7 +5810,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5538
5810
|
// sibling imports. Local install handles workspace linking correctly.
|
|
5539
5811
|
if (pkg.workspaces) {
|
|
5540
5812
|
console.log(`Installing ${pkgName}@${pkgVersion} globally from local (workspace)...`);
|
|
5541
|
-
const localResult = await
|
|
5813
|
+
const localResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5542
5814
|
if (localResult.success) {
|
|
5543
5815
|
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
5544
5816
|
}
|
|
@@ -5577,7 +5849,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5577
5849
|
else {
|
|
5578
5850
|
console.log(colors.yellow(`${pkgName}@${pkgVersion} not found on npm — installing from local directory.`));
|
|
5579
5851
|
console.log(colors.dim(' Use -m "message" to publish this version to npm.'));
|
|
5580
|
-
const localResult = await
|
|
5852
|
+
const localResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5581
5853
|
if (localResult.success) {
|
|
5582
5854
|
console.log(colors.green(`✓ Installed globally from local: ${pkgName}@${pkgVersion}`));
|
|
5583
5855
|
}
|
|
@@ -6361,7 +6633,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6361
6633
|
if (link) {
|
|
6362
6634
|
console.log(`Linking globally: ${pkgName}@${pkgVersion}...`);
|
|
6363
6635
|
if (!dryRun) {
|
|
6364
|
-
const installResult = await
|
|
6636
|
+
const installResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
6365
6637
|
if (installResult.success) {
|
|
6366
6638
|
globalInstallOk = true;
|
|
6367
6639
|
console.log(colors.green(`✓ Linked globally: ${pkgName}@${pkgVersion}`));
|
|
@@ -6381,7 +6653,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6381
6653
|
// Workspace packages: install from local for proper workspace linking
|
|
6382
6654
|
console.log(`Installing ${pkgName}@${pkgVersion} globally from local (workspace)...`);
|
|
6383
6655
|
if (!dryRun) {
|
|
6384
|
-
const installResult = await
|
|
6656
|
+
const installResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
6385
6657
|
if (installResult.success) {
|
|
6386
6658
|
globalInstallOk = true;
|
|
6387
6659
|
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
@@ -6942,7 +7214,7 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
6942
7214
|
// Workspace root is private — always install from local directory
|
|
6943
7215
|
console.log(`Installing ${pkgName}@${pkgVersion} globally from local directory...`);
|
|
6944
7216
|
if (!dryRun) {
|
|
6945
|
-
const installResult = await
|
|
7217
|
+
const installResult = await installGlobalWithRetry('.', rootDir, false, 1);
|
|
6946
7218
|
if (installResult.success) {
|
|
6947
7219
|
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
6948
7220
|
}
|