@bobfrankston/npmglobalize 1.0.191 → 1.0.193
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/_synctoken.cjs +28 -0
- package/_synctoken.d.cts +2 -0
- package/cli.js +55 -9
- package/colors.js +5 -1
- package/lib.d.ts +51 -3
- package/lib.js +621 -79
- package/package.json +1 -1
package/_synctoken.cjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// One-shot: copy the current Windows npm authToken into WSL's ~/.npmrc.
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const winNpmrc = path.join(os.homedir(), '.npmrc');
|
|
7
|
+
const wslNpmrc = '\\\\wsl.localhost\\Ubuntu\\home\\bob\\.npmrc';
|
|
8
|
+
const KEY = '//registry.npmjs.org/:_authToken=';
|
|
9
|
+
|
|
10
|
+
const winText = fs.readFileSync(winNpmrc, 'utf8');
|
|
11
|
+
const m = winText.match(/\/\/registry\.npmjs\.org\/:_authToken=(\S+)/);
|
|
12
|
+
if (!m) { console.error('No Windows token found in', winNpmrc); process.exit(1); }
|
|
13
|
+
const token = m[1];
|
|
14
|
+
|
|
15
|
+
let wslText = '';
|
|
16
|
+
try { wslText = fs.readFileSync(wslNpmrc, 'utf8'); }
|
|
17
|
+
catch (e) { console.error('Cannot read WSL .npmrc:', e.message); process.exit(1); }
|
|
18
|
+
|
|
19
|
+
const line = KEY + token;
|
|
20
|
+
const lines = wslText.split(/\r?\n/);
|
|
21
|
+
let replaced = false;
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
if (lines[i].startsWith(KEY)) { lines[i] = line; replaced = true; }
|
|
24
|
+
}
|
|
25
|
+
if (!replaced) lines.push(line);
|
|
26
|
+
const out = lines.filter((l, i) => !(l === '' && i === lines.length - 1)).join('\n') + '\n';
|
|
27
|
+
fs.writeFileSync(wslNpmrc, out);
|
|
28
|
+
console.log(replaced ? 'Replaced stale WSL token' : 'Added token to WSL .npmrc', '(tail ...' + token.slice(-6) + ')');
|
package/_synctoken.d.cts
ADDED
package/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
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 } from './lib.js';
|
|
5
|
+
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations } from './lib.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import {
|
|
8
|
+
import { colors } from './colors.js';
|
|
9
9
|
// npmglobalize install directory (for --version)
|
|
10
10
|
const __dirname = import.meta.dirname;
|
|
11
11
|
/** Own version, read once. Stamped into the startup banner AND the Issues Summary
|
|
@@ -116,6 +116,11 @@ Other Options:
|
|
|
116
116
|
Before npm pack, wipe node_modules/ in each file: dep target.
|
|
117
117
|
Fixes arborist "Cannot read properties of null" crashes when
|
|
118
118
|
sibling file: deps have their own nested node_modules.
|
|
119
|
+
-ts7-report, -deprecation-report
|
|
120
|
+
Report-only: scan this package and its file: deps for
|
|
121
|
+
compilerOptions removed in TypeScript 7 (deprecated
|
|
122
|
+
moduleResolution, target es3, deprecated flags). Lists a
|
|
123
|
+
migration to-do list and exits. Writes nothing.
|
|
119
124
|
-show Show package.json dependency changes
|
|
120
125
|
-package, -pkg Update package.json scripts to use npmglobalize
|
|
121
126
|
-h, -help Show this help
|
|
@@ -155,6 +160,7 @@ function parseArgs(args) {
|
|
|
155
160
|
help: false,
|
|
156
161
|
version: false,
|
|
157
162
|
error: '',
|
|
163
|
+
ts7Report: false,
|
|
158
164
|
explicitKeys: new Set()
|
|
159
165
|
};
|
|
160
166
|
const unrecognized = [];
|
|
@@ -172,6 +178,10 @@ function parseArgs(args) {
|
|
|
172
178
|
case '-v':
|
|
173
179
|
options.version = true;
|
|
174
180
|
break;
|
|
181
|
+
case '-ts7-report':
|
|
182
|
+
case '-deprecation-report':
|
|
183
|
+
options.ts7Report = true;
|
|
184
|
+
break;
|
|
175
185
|
case '-patch':
|
|
176
186
|
options.bump = 'patch';
|
|
177
187
|
break;
|
|
@@ -423,24 +433,55 @@ function parseArgs(args) {
|
|
|
423
433
|
}
|
|
424
434
|
return options;
|
|
425
435
|
}
|
|
426
|
-
/** Print accumulated build issues
|
|
436
|
+
/** Print accumulated build issues, colored per severity via the theme module. */
|
|
427
437
|
function printBuildSummary() {
|
|
428
438
|
const issues = getBuildIssues();
|
|
429
439
|
if (issues.length === 0)
|
|
430
440
|
return;
|
|
431
441
|
console.log('');
|
|
432
|
-
console.log(
|
|
442
|
+
console.log(colors.warn(`━━━ Issues Summary (npmglobalize v${NPMGLOBALIZE_VERSION}) ━━━`));
|
|
433
443
|
for (const issue of issues) {
|
|
444
|
+
// Errors in red (readable on any background); warnings in the theme warn color.
|
|
445
|
+
const paint = issue.severity === 'error' ? colors.error : colors.warn;
|
|
434
446
|
const icon = issue.severity === 'error' ? '✗' : '⚠';
|
|
435
|
-
console.log(
|
|
447
|
+
console.log(paint(` ${icon} ${issue.module}: ${issue.message}`));
|
|
436
448
|
}
|
|
437
|
-
console.log(
|
|
449
|
+
console.log(colors.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
450
|
+
console.log('');
|
|
451
|
+
}
|
|
452
|
+
/** Print the report-only TS7-readiness scan over cwd + its file: dep graph. */
|
|
453
|
+
function printTs7Report(cwd) {
|
|
454
|
+
const findings = reportTs7Deprecations(cwd);
|
|
455
|
+
console.log('');
|
|
456
|
+
console.log(colors.accent('━━━ TypeScript 7 readiness ━━━'));
|
|
457
|
+
if (findings.length === 0) {
|
|
458
|
+
console.log(colors.success(' ✓ No removed-in-TS7 compilerOptions found in this package or its file: deps.'));
|
|
459
|
+
console.log('');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// Group by package for a readable migration to-do list.
|
|
463
|
+
const byPkg = new Map();
|
|
464
|
+
for (const f of findings) {
|
|
465
|
+
if (!byPkg.has(f.name))
|
|
466
|
+
byPkg.set(f.name, []);
|
|
467
|
+
byPkg.get(f.name).push(f);
|
|
468
|
+
}
|
|
469
|
+
console.log(colors.warn(` ${findings.length} item(s) across ${byPkg.size} package(s) need migration before TypeScript 7:`));
|
|
470
|
+
for (const [name, items] of byPkg) {
|
|
471
|
+
console.log(colors.accent(`\n ${name}`));
|
|
472
|
+
for (const f of items) {
|
|
473
|
+
console.log(colors.warn(` • ${f.option}: ${JSON.stringify(f.value)} (in ${f.definedIn})`));
|
|
474
|
+
console.log(colors.muted(` → ${f.action}`));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
console.log(colors.muted('\n Note: npmglobalize auto-applies these migrations at build time when a deprecation error fires —'));
|
|
478
|
+
console.log(colors.muted(' it does NOT silence them with "ignoreDeprecations". Any ignoreDeprecations above is itself a flagged bug.'));
|
|
438
479
|
console.log('');
|
|
439
480
|
}
|
|
440
481
|
export async function main() {
|
|
441
482
|
installCleanupHandlers();
|
|
442
483
|
// Show version at the very start
|
|
443
|
-
console.log(
|
|
484
|
+
console.log(colors.accent(`npmglobalize v${NPMGLOBALIZE_VERSION}`));
|
|
444
485
|
const args = process.argv.slice(2);
|
|
445
486
|
const cliOptions = parseArgs(args);
|
|
446
487
|
if (cliOptions.help) {
|
|
@@ -469,6 +510,11 @@ export async function main() {
|
|
|
469
510
|
process.exit(1);
|
|
470
511
|
}
|
|
471
512
|
}
|
|
513
|
+
// Report-only TS7-readiness scan (no transform / build / publish).
|
|
514
|
+
if (cliOptions.ts7Report) {
|
|
515
|
+
printTs7Report(cwd);
|
|
516
|
+
process.exit(0);
|
|
517
|
+
}
|
|
472
518
|
// Build file: deps topologically, then the target itself.
|
|
473
519
|
// Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
|
|
474
520
|
// whose source has changed since their last build.
|
|
@@ -480,14 +526,14 @@ export async function main() {
|
|
|
480
526
|
process.exit(1);
|
|
481
527
|
}
|
|
482
528
|
if (!depsOk)
|
|
483
|
-
console.log(
|
|
529
|
+
console.log(colors.warn('Continuing with --force despite dep build failure...'));
|
|
484
530
|
const targetOk = await buildProject(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
|
|
485
531
|
if (!targetOk) {
|
|
486
532
|
if (!cliOptions.force) {
|
|
487
533
|
printBuildSummary();
|
|
488
534
|
process.exit(1);
|
|
489
535
|
}
|
|
490
|
-
console.log(
|
|
536
|
+
console.log(colors.warn('Continuing with --force...'));
|
|
491
537
|
}
|
|
492
538
|
}
|
|
493
539
|
// Handle --package: update scripts in target package.json
|
package/colors.js
CHANGED
|
@@ -18,7 +18,11 @@ function detectTheme() {
|
|
|
18
18
|
return 'light';
|
|
19
19
|
if (process.env.NPMG_THEME === 'dark')
|
|
20
20
|
return 'dark';
|
|
21
|
-
|
|
21
|
+
// Unknown background: default to the 'light' palette. Its colors (magenta warn,
|
|
22
|
+
// red, green, blue, cyan) are all readable on BOTH light and dark terminals,
|
|
23
|
+
// whereas the 'dark' palette's yellow warn is invisible on a light background.
|
|
24
|
+
// Per global pref: never default to yellow on an unknown background.
|
|
25
|
+
return 'light';
|
|
22
26
|
}
|
|
23
27
|
const theme = detectTheme();
|
|
24
28
|
// Raw ANSI colors per theme — semantic → [dark, light]
|
package/lib.d.ts
CHANGED
|
@@ -328,6 +328,25 @@ export declare function buildFileDepsTopologically(cwd: string, opts?: {
|
|
|
328
328
|
verbose?: boolean;
|
|
329
329
|
force?: boolean;
|
|
330
330
|
}, visited?: Set<string>): Promise<boolean>;
|
|
331
|
+
/** A single TS7-readiness problem found in a package's effective tsconfig. */
|
|
332
|
+
export interface Ts7Finding {
|
|
333
|
+
name: string;
|
|
334
|
+
dir: string;
|
|
335
|
+
option: string;
|
|
336
|
+
value: string;
|
|
337
|
+
/** tsconfig (relative to the package dir) where the value is actually set —
|
|
338
|
+
* may be the package's own file or a base it extends. */
|
|
339
|
+
definedIn: string;
|
|
340
|
+
action: string;
|
|
341
|
+
}
|
|
342
|
+
/** Report-only TS7-readiness scan over `cwd` and its `file:` dep graph. For each
|
|
343
|
+
* package with a tsconfig it resolves the *effective* compilerOptions through the
|
|
344
|
+
* `extends` chain and flags removed-in-TS7 settings (deprecated `moduleResolution`,
|
|
345
|
+
* `target: es3`, and the deprecated boolean flags) plus any `ignoreDeprecations`
|
|
346
|
+
* (itself a flagged bug — it silences a real TS7 error). Reads only — never writes.
|
|
347
|
+
* The same migration `migrateTsconfigDeprecations` applies automatically on a build
|
|
348
|
+
* failure; this report is the read-only, whole-graph inventory. */
|
|
349
|
+
export declare function reportTs7Deprecations(cwd: string): Ts7Finding[];
|
|
331
350
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
332
351
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
333
352
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
@@ -337,6 +356,38 @@ export declare function ensureWorkspaceDepModules(rootDir: string, members: Arra
|
|
|
337
356
|
dir: string;
|
|
338
357
|
pkg: any;
|
|
339
358
|
}>, verbose?: boolean): Promise<void>;
|
|
359
|
+
/** Extract the lines a human needs from a failed npm command's output: the
|
|
360
|
+
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
361
|
+
* deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
|
|
362
|
+
export declare function distillNpmErrors(combined: string): string[];
|
|
363
|
+
/** Scan a puppeteer browser cache (layout <root>/<browser>/<platform-version>)
|
|
364
|
+
* and repair every version folder that has no browser executable in it — the
|
|
365
|
+
* residue of a truncated extraction (see extractBrowserArchive for the Node
|
|
366
|
+
* 26 extract-zip bug that produces these). Per corrupt folder:
|
|
367
|
+
* 1. If the downloaded archive still sits next to it (the usual case right
|
|
368
|
+
* after a truncating run — the hung extraction means puppeteer's cleanup
|
|
369
|
+
* never deleted it), finish the extraction with the system unzipper. This
|
|
370
|
+
* is the only path that converges on Node 26, where re-extraction by
|
|
371
|
+
* puppeteer truncates again every time.
|
|
372
|
+
* 2. Otherwise delete the folder so the next attempt re-downloads.
|
|
373
|
+
*
|
|
374
|
+
* Timing matters: a truncating install EXITS 0, so the poisoned cache hides
|
|
375
|
+
* behind a successful install — and the NEXT install deletes the leftover
|
|
376
|
+
* archive before erroring. So this must run right after a SUCCESSFUL install
|
|
377
|
+
* (while the archive is still there), not only on failure. */
|
|
378
|
+
export declare function verifyPuppeteerCache(cacheRoot?: string): {
|
|
379
|
+
repaired: string[];
|
|
380
|
+
removed: string[];
|
|
381
|
+
};
|
|
382
|
+
/** Detect puppeteer's corrupted-browser-cache failure ("browser folder exists
|
|
383
|
+
* but the executable is missing") in failed install output and repair the
|
|
384
|
+
* whole cache it points into (verifyPuppeteerCache — the installer checks
|
|
385
|
+
* browsers in parallel and only reports the first corrupt one, so siblings
|
|
386
|
+
* are usually poisoned too). Returns the folders repaired or removed. */
|
|
387
|
+
export declare function fixPuppeteerCorruptedCache(combined: string): {
|
|
388
|
+
repaired: string[];
|
|
389
|
+
removed: string[];
|
|
390
|
+
};
|
|
340
391
|
/** Run a command and return success status */
|
|
341
392
|
export declare function runCommand(cmd: string, args: string[], options?: {
|
|
342
393
|
silent?: boolean;
|
|
@@ -364,9 +415,6 @@ export declare function runCommandAsync(cmd: string, args: string[], options?: {
|
|
|
364
415
|
stderr: string;
|
|
365
416
|
signal: NodeJS.Signals | null;
|
|
366
417
|
}>;
|
|
367
|
-
/** Install a package globally in WSL, auto-fixing the root-owned /usr/local/lib/node_modules
|
|
368
|
-
* EACCES case by switching npm to a user prefix (~/.npm-global) and retrying once.
|
|
369
|
-
* Output is captured (so we can scan for the error) and then mirrored to the terminal. */
|
|
370
418
|
export declare function installInWsl(wslArgs: string[], opts?: {
|
|
371
419
|
cwd?: string;
|
|
372
420
|
}): Promise<{
|
package/lib.js
CHANGED
|
@@ -1975,6 +1975,39 @@ function insertCompilerOption(text, key, valueLiteral) {
|
|
|
1975
1975
|
// Inline single-line object: `{ "target": ... }`
|
|
1976
1976
|
return text.slice(0, braceEnd) + ` ${prop},` + text.slice(braceEnd);
|
|
1977
1977
|
}
|
|
1978
|
+
/** Replace the value of an existing top-level `compilerOptions.<key>` in `text`,
|
|
1979
|
+
* preserving the file's formatting. The key's value may be a quoted string, a
|
|
1980
|
+
* boolean/number, or any run up to the next comma/brace/newline. Returns the
|
|
1981
|
+
* patched text, or null if the key isn't present as a simple property. */
|
|
1982
|
+
function replaceCompilerOptionValue(text, key, newValueLiteral) {
|
|
1983
|
+
const re = new RegExp(`("${key}"\\s*:\\s*)("(?:[^"\\\\]|\\\\.)*"|[^,}\\n]+)`);
|
|
1984
|
+
const m = text.match(re);
|
|
1985
|
+
if (!m)
|
|
1986
|
+
return null;
|
|
1987
|
+
return text.slice(0, m.index) + m[1] + newValueLiteral + text.slice(m.index + m[0].length);
|
|
1988
|
+
}
|
|
1989
|
+
/** Set `compilerOptions.<key>` to `valueLiteral`: replace it in place if already
|
|
1990
|
+
* present, otherwise insert it. Returns patched text, or null if neither could be
|
|
1991
|
+
* done safely (no locatable `compilerOptions` object). */
|
|
1992
|
+
function upsertCompilerOption(text, key, valueLiteral) {
|
|
1993
|
+
const replaced = replaceCompilerOptionValue(text, key, valueLiteral);
|
|
1994
|
+
if (replaced !== null)
|
|
1995
|
+
return replaced;
|
|
1996
|
+
return insertCompilerOption(text, key, valueLiteral);
|
|
1997
|
+
}
|
|
1998
|
+
/** Remove the whole `compilerOptions.<key>` property line. Works for the usual
|
|
1999
|
+
* one-option-per-line tsconfig formatting; returns null if `<key>` isn't on its
|
|
2000
|
+
* own line (caller then reports it as a manual fix). A trailing comma left on the
|
|
2001
|
+
* preceding line is fine — tsconfig is parsed as JSONC (trailing commas allowed). */
|
|
2002
|
+
function removeCompilerOptionLine(text, key) {
|
|
2003
|
+
const lines = text.split('\n');
|
|
2004
|
+
const re = new RegExp(`^\\s*"${key}"\\s*:`);
|
|
2005
|
+
const idx = lines.findIndex(l => re.test(l));
|
|
2006
|
+
if (idx === -1)
|
|
2007
|
+
return null;
|
|
2008
|
+
lines.splice(idx, 1);
|
|
2009
|
+
return lines.join('\n');
|
|
2010
|
+
}
|
|
1978
2011
|
/** Insert a `"types": [...]` line as the first property of `compilerOptions`.
|
|
1979
2012
|
* Only ever called when `compilerOptions.types` is absent. */
|
|
1980
2013
|
function insertTypesIntoTsconfig(text, typesArr) {
|
|
@@ -2107,54 +2140,109 @@ function ensureTsconfigNodeTypes(cwd) {
|
|
|
2107
2140
|
}
|
|
2108
2141
|
recordBuildIssue(chain[0].parsed?.name || path.basename(cwd), 'warning', `TS6+ build may fail: no writable compilerOptions block in the tsconfig chain of ${cwd} to add "types"`);
|
|
2109
2142
|
}
|
|
2110
|
-
/** TS6 turned several long-deprecated compiler options (
|
|
2111
|
-
*
|
|
2112
|
-
* errors (`TS5107`/`TS5101`) that
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
|
|
2143
|
+
/** TS6 turned several long-deprecated compiler options (`moduleResolution:
|
|
2144
|
+
* "node10"`/`"node"`/`"classic"`, `target: "es3"`, the dead boolean flags) into
|
|
2145
|
+
* hard errors (`TS5107`/`TS5101`) that TS7 *removes*. We deliberately do NOT
|
|
2146
|
+
* silence them with `"ignoreDeprecations"` — that just defers a real error to the
|
|
2147
|
+
* day TS7 lands. Instead we perform the actual migration in place:
|
|
2148
|
+
* • `moduleResolution` → `"nodenext"` (and align `module` to `"nodenext"`, since
|
|
2149
|
+
* `nodenext` resolution requires a `nodenext`/`node16`/`preserve` module),
|
|
2150
|
+
* • `target: "es3"` → `"es2022"`,
|
|
2151
|
+
* • delete each dead flag (`importsNotUsedAsValues`, `out`, …).
|
|
2152
|
+
* Each option is edited in the writable file where it's defined; when it's
|
|
2153
|
+
* inherited from a `node_modules` base we instead write an override into the
|
|
2154
|
+
* most-leaf writable config so it wins. This is a *semantic* change: the rebuild
|
|
2155
|
+
* may now surface genuine resolution errors — that is the point. Those are real
|
|
2156
|
+
* bugs the `node10` resolver was hiding, not deprecations. Returns true if it
|
|
2157
|
+
* changed any file. */
|
|
2158
|
+
function migrateTsconfigDeprecations(cwd) {
|
|
2122
2159
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
2123
2160
|
if (!fs.existsSync(tsconfigPath))
|
|
2124
2161
|
return false;
|
|
2125
|
-
const { chain
|
|
2162
|
+
const { chain } = resolveTsconfigChain(tsconfigPath);
|
|
2126
2163
|
if (!chain.length)
|
|
2127
2164
|
return false;
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
return
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2165
|
+
const name = (() => { try {
|
|
2166
|
+
return readPackageJson(cwd).name;
|
|
2167
|
+
}
|
|
2168
|
+
catch {
|
|
2169
|
+
return null;
|
|
2170
|
+
} })()
|
|
2171
|
+
|| chain[0].parsed?.name || path.basename(cwd);
|
|
2172
|
+
const mostLeafWritable = chain.find(c => c.writable); // chain is leaf-first
|
|
2173
|
+
/** File to edit for an option defined in `definedIn`: that file if writable,
|
|
2174
|
+
* else the most-leaf writable config (an override there beats a node_modules base). */
|
|
2175
|
+
const targetFor = (definedIn) => {
|
|
2176
|
+
const def = chain.find(c => c.path === definedIn);
|
|
2177
|
+
return def?.writable ? def : mostLeafWritable;
|
|
2178
|
+
};
|
|
2179
|
+
const edit = (filePath, fn) => {
|
|
2136
2180
|
let text;
|
|
2137
2181
|
try {
|
|
2138
|
-
text = fs.readFileSync(
|
|
2182
|
+
text = fs.readFileSync(filePath, 'utf-8');
|
|
2139
2183
|
}
|
|
2140
2184
|
catch {
|
|
2141
|
-
|
|
2185
|
+
return false;
|
|
2142
2186
|
}
|
|
2143
|
-
const patched =
|
|
2144
|
-
if (
|
|
2145
|
-
|
|
2187
|
+
const patched = fn(text);
|
|
2188
|
+
if (patched == null || patched === text)
|
|
2189
|
+
return false;
|
|
2146
2190
|
try {
|
|
2147
|
-
fs.writeFileSync(
|
|
2148
|
-
const where = path.relative(cwd, cand.path) || 'tsconfig.json';
|
|
2149
|
-
console.log(colors.cyan(` Patched ${where}: added "ignoreDeprecations": ${valueLiteral} (silences TS6 removed-in-TS7 deprecation errors)`));
|
|
2150
|
-
recordBuildIssue(chain[0].parsed?.name || path.basename(cwd), 'warning', `Deprecated compilerOptions silenced via ignoreDeprecations=${version}. Migrate moduleResolution to "node16"/"nodenext"/"bundler" before TypeScript 7.`);
|
|
2191
|
+
fs.writeFileSync(filePath, patched);
|
|
2151
2192
|
return true;
|
|
2152
2193
|
}
|
|
2153
2194
|
catch (error) {
|
|
2154
|
-
console.error(colors.yellow(` Could not write tsconfig patch (${
|
|
2195
|
+
console.error(colors.yellow(` Could not write tsconfig patch (${filePath}): ${error.message}`));
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
const note = (msg) => console.log(colors.cyan(` ${msg}`));
|
|
2200
|
+
let changed = false;
|
|
2201
|
+
// moduleResolution: node/node10/classic → nodenext (+ align module).
|
|
2202
|
+
const mr = effectiveCompilerOption(chain, 'moduleResolution');
|
|
2203
|
+
if (mr && typeof mr.value === 'string' && DEPRECATED_MODULE_RESOLUTION.has(mr.value.toLowerCase())) {
|
|
2204
|
+
const tgt = targetFor(mr.definedIn);
|
|
2205
|
+
if (tgt && edit(tgt.path, t => upsertCompilerOption(t, 'moduleResolution', '"nodenext"'))) {
|
|
2206
|
+
changed = true;
|
|
2207
|
+
note(`Migrated moduleResolution "${mr.value}" → "nodenext" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'}`);
|
|
2208
|
+
// nodenext resolution requires a matching module setting.
|
|
2209
|
+
const mod = effectiveCompilerOption(chain, 'module');
|
|
2210
|
+
const modOk = mod && typeof mod.value === 'string' && /^(node16|nodenext|preserve)$/i.test(mod.value);
|
|
2211
|
+
if (!modOk) {
|
|
2212
|
+
const modTgt = mod ? targetFor(mod.definedIn) : mostLeafWritable;
|
|
2213
|
+
if (modTgt && edit(modTgt.path, t => upsertCompilerOption(t, 'module', '"nodenext"'))) {
|
|
2214
|
+
note(`Aligned module → "nodenext" (required by moduleResolution "nodenext")`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
recordBuildIssue(name, 'warning', `Migrated moduleResolution "${mr.value}" → "nodenext". Verify relative imports resolve (NodeNext requires explicit file extensions on relative specifiers).`);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
// target: es3 → es2022 (es3 is removed in TS7).
|
|
2221
|
+
const tg = effectiveCompilerOption(chain, 'target');
|
|
2222
|
+
if (tg && typeof tg.value === 'string' && tg.value.toLowerCase() === 'es3') {
|
|
2223
|
+
const tgt = targetFor(tg.definedIn);
|
|
2224
|
+
if (tgt && edit(tgt.path, t => upsertCompilerOption(t, 'target', '"es2022"'))) {
|
|
2225
|
+
changed = true;
|
|
2226
|
+
note(`Raised target "es3" → "es2022" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// Dead boolean flags: delete where writable; report when inherited from a base.
|
|
2230
|
+
for (const flag of DEPRECATED_TS7_FLAGS) {
|
|
2231
|
+
const f = effectiveCompilerOption(chain, flag);
|
|
2232
|
+
if (!f)
|
|
2233
|
+
continue;
|
|
2234
|
+
const def = chain.find(c => c.path === f.definedIn);
|
|
2235
|
+
if (def?.writable) {
|
|
2236
|
+
if (edit(def.path, t => removeCompilerOptionLine(t, flag))) {
|
|
2237
|
+
changed = true;
|
|
2238
|
+
note(`Removed deprecated flag "${flag}" from ${path.relative(cwd, def.path) || 'tsconfig.json'}`);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
else {
|
|
2242
|
+
recordBuildIssue(name, 'warning', `Deprecated flag "${flag}" is inherited from ${path.relative(cwd, f.definedIn)} (a node_modules base) — remove it there before TypeScript 7.`);
|
|
2155
2243
|
}
|
|
2156
2244
|
}
|
|
2157
|
-
return
|
|
2245
|
+
return changed;
|
|
2158
2246
|
}
|
|
2159
2247
|
/** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
|
|
2160
2248
|
* TypeScript project lacks a build script, run `npm run build`, record
|
|
@@ -2192,12 +2280,12 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
2192
2280
|
console.log(`Building ${pkg.name || cwd}...`);
|
|
2193
2281
|
let buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
2194
2282
|
if (!buildResult.success) {
|
|
2195
|
-
// TS6 deprecation error (removed-in-TS7):
|
|
2196
|
-
//
|
|
2283
|
+
// TS6 deprecation error (removed-in-TS7): perform the real migration
|
|
2284
|
+
// (moduleResolution→nodenext, raise target, drop dead flags) and retry once.
|
|
2285
|
+
// We don't silence with ignoreDeprecations — the retry may now surface real
|
|
2286
|
+
// resolution errors, which are genuine bugs to fix, not deprecations.
|
|
2197
2287
|
const out = (buildResult.stderr || '') + (buildResult.output || '');
|
|
2198
|
-
|
|
2199
|
-
out.match(/['"]ignoreDeprecations['"]\s*:\s*['"]([\d.]+)['"]/);
|
|
2200
|
-
if (dep && ensureTsconfigIgnoreDeprecations(cwd, dep[1])) {
|
|
2288
|
+
if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd)) {
|
|
2201
2289
|
buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
2202
2290
|
}
|
|
2203
2291
|
}
|
|
@@ -2259,6 +2347,136 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
|
|
|
2259
2347
|
}
|
|
2260
2348
|
return allOk;
|
|
2261
2349
|
}
|
|
2350
|
+
/** `moduleResolution` values TypeScript 6 deprecated and TypeScript 7 removes. */
|
|
2351
|
+
const DEPRECATED_MODULE_RESOLUTION = new Set(['node', 'node10', 'classic']);
|
|
2352
|
+
/** Compiler flags TypeScript deprecated (TS5101) and will remove in TS7. Their
|
|
2353
|
+
* mere presence — at any value — is a migration item. */
|
|
2354
|
+
const DEPRECATED_TS7_FLAGS = [
|
|
2355
|
+
'charset', 'importsNotUsedAsValues', 'keyofStringsOnly', 'noImplicitUseStrict',
|
|
2356
|
+
'noStrictGenericChecks', 'out', 'preserveValueImports',
|
|
2357
|
+
'suppressExcessPropertyErrors', 'suppressImplicitAnyIndexErrors',
|
|
2358
|
+
];
|
|
2359
|
+
/** First defined value of `key` across the resolved chain (leaf overrides base),
|
|
2360
|
+
* plus which file in the chain defined it. */
|
|
2361
|
+
function effectiveCompilerOption(chain, key) {
|
|
2362
|
+
for (const c of chain) {
|
|
2363
|
+
const v = c.parsed?.compilerOptions?.[key];
|
|
2364
|
+
if (v !== undefined)
|
|
2365
|
+
return { value: v, definedIn: c.path };
|
|
2366
|
+
}
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2369
|
+
/** Expand one npm `workspaces` pattern (relative to `baseDir`) to package dirs.
|
|
2370
|
+
* Handles an explicit path and a single-level `<prefix>/*` glob — the common
|
|
2371
|
+
* cases; deep `**` globs aren't expanded. */
|
|
2372
|
+
function expandWorkspacePattern(baseDir, pattern) {
|
|
2373
|
+
if (!pattern.includes('*')) {
|
|
2374
|
+
const d = path.resolve(baseDir, pattern);
|
|
2375
|
+
return fs.existsSync(path.join(d, 'package.json')) ? [d] : [];
|
|
2376
|
+
}
|
|
2377
|
+
const prefix = pattern.slice(0, pattern.indexOf('*')).replace(/[\\/]$/, '');
|
|
2378
|
+
const parent = path.resolve(baseDir, prefix);
|
|
2379
|
+
try {
|
|
2380
|
+
return fs.readdirSync(parent, { withFileTypes: true })
|
|
2381
|
+
.filter(e => e.isDirectory())
|
|
2382
|
+
.map(e => path.join(parent, e.name))
|
|
2383
|
+
.filter(d => fs.existsSync(path.join(d, 'package.json')));
|
|
2384
|
+
}
|
|
2385
|
+
catch {
|
|
2386
|
+
return [];
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
/** Collect this package and every package reachable through `file:` deps AND
|
|
2390
|
+
* npm `workspaces` members (the same graph the build cascade walks). Returns a
|
|
2391
|
+
* set of absolute dirs. */
|
|
2392
|
+
function collectFileDepDirs(cwd, visited = new Set()) {
|
|
2393
|
+
const abs = path.resolve(cwd);
|
|
2394
|
+
if (visited.has(abs))
|
|
2395
|
+
return visited;
|
|
2396
|
+
visited.add(abs);
|
|
2397
|
+
let pkg;
|
|
2398
|
+
try {
|
|
2399
|
+
pkg = readPackageJson(cwd);
|
|
2400
|
+
}
|
|
2401
|
+
catch {
|
|
2402
|
+
return visited;
|
|
2403
|
+
}
|
|
2404
|
+
for (const key of ['dependencies', 'devDependencies']) {
|
|
2405
|
+
const deps = pkg?.[key];
|
|
2406
|
+
if (!deps || typeof deps !== 'object')
|
|
2407
|
+
continue;
|
|
2408
|
+
for (const [, spec] of Object.entries(deps)) {
|
|
2409
|
+
if (typeof spec !== 'string' || !spec.startsWith('file:'))
|
|
2410
|
+
continue;
|
|
2411
|
+
const target = path.resolve(cwd, spec.slice('file:'.length));
|
|
2412
|
+
if (!fs.existsSync(path.join(target, 'package.json')))
|
|
2413
|
+
continue;
|
|
2414
|
+
collectFileDepDirs(target, visited);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
// Workspace members (referenced via the `workspaces` field, not file: deps).
|
|
2418
|
+
const ws = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
|
|
2419
|
+
if (Array.isArray(ws)) {
|
|
2420
|
+
for (const pattern of ws) {
|
|
2421
|
+
if (typeof pattern !== 'string')
|
|
2422
|
+
continue;
|
|
2423
|
+
for (const memberDir of expandWorkspacePattern(cwd, pattern))
|
|
2424
|
+
collectFileDepDirs(memberDir, visited);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
return visited;
|
|
2428
|
+
}
|
|
2429
|
+
/** Report-only TS7-readiness scan over `cwd` and its `file:` dep graph. For each
|
|
2430
|
+
* package with a tsconfig it resolves the *effective* compilerOptions through the
|
|
2431
|
+
* `extends` chain and flags removed-in-TS7 settings (deprecated `moduleResolution`,
|
|
2432
|
+
* `target: es3`, and the deprecated boolean flags) plus any `ignoreDeprecations`
|
|
2433
|
+
* (itself a flagged bug — it silences a real TS7 error). Reads only — never writes.
|
|
2434
|
+
* The same migration `migrateTsconfigDeprecations` applies automatically on a build
|
|
2435
|
+
* failure; this report is the read-only, whole-graph inventory. */
|
|
2436
|
+
export function reportTs7Deprecations(cwd) {
|
|
2437
|
+
const dirs = collectFileDepDirs(cwd);
|
|
2438
|
+
const findings = [];
|
|
2439
|
+
for (const dir of dirs) {
|
|
2440
|
+
const tsconfigPath = path.join(dir, 'tsconfig.json');
|
|
2441
|
+
if (!fs.existsSync(tsconfigPath))
|
|
2442
|
+
continue;
|
|
2443
|
+
const { chain } = resolveTsconfigChain(tsconfigPath);
|
|
2444
|
+
if (!chain.length)
|
|
2445
|
+
continue;
|
|
2446
|
+
let name;
|
|
2447
|
+
try {
|
|
2448
|
+
name = readPackageJson(dir).name || path.basename(dir);
|
|
2449
|
+
}
|
|
2450
|
+
catch {
|
|
2451
|
+
name = path.basename(dir);
|
|
2452
|
+
}
|
|
2453
|
+
const rel = (p) => path.relative(dir, p) || 'tsconfig.json';
|
|
2454
|
+
// Any ignoreDeprecations is itself a latent bug: it silences a real,
|
|
2455
|
+
// removed-in-TS7 error rather than fixing it. Flag it, loudly.
|
|
2456
|
+
const igd = effectiveCompilerOption(chain, 'ignoreDeprecations');
|
|
2457
|
+
if (igd !== null) {
|
|
2458
|
+
findings.push({ name, dir, option: 'ignoreDeprecations', value: String(igd.value), definedIn: rel(igd.definedIn),
|
|
2459
|
+
action: 'REMOVE — silences a real TS7-removal error instead of fixing it; a bug waiting to bite at TS7. Fix the underlying option(s) below, then delete this.' });
|
|
2460
|
+
}
|
|
2461
|
+
const mr = effectiveCompilerOption(chain, 'moduleResolution');
|
|
2462
|
+
if (mr && typeof mr.value === 'string' && DEPRECATED_MODULE_RESOLUTION.has(mr.value.toLowerCase())) {
|
|
2463
|
+
findings.push({ name, dir, option: 'moduleResolution', value: mr.value, definedIn: rel(mr.definedIn),
|
|
2464
|
+
action: 'migrate to "node16" / "nodenext" / "bundler" (validate import resolution)' });
|
|
2465
|
+
}
|
|
2466
|
+
const tgt = effectiveCompilerOption(chain, 'target');
|
|
2467
|
+
if (tgt && typeof tgt.value === 'string' && tgt.value.toLowerCase() === 'es3') {
|
|
2468
|
+
findings.push({ name, dir, option: 'target', value: tgt.value, definedIn: rel(tgt.definedIn),
|
|
2469
|
+
action: 'raise target — "es3" is removed in TS7' });
|
|
2470
|
+
}
|
|
2471
|
+
for (const flag of DEPRECATED_TS7_FLAGS) {
|
|
2472
|
+
const f = effectiveCompilerOption(chain, flag);
|
|
2473
|
+
if (f)
|
|
2474
|
+
findings.push({ name, dir, option: flag, value: String(f.value), definedIn: rel(f.definedIn),
|
|
2475
|
+
action: 'remove — deprecated flag, removed in TS7' });
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return findings;
|
|
2479
|
+
}
|
|
2262
2480
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
2263
2481
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
2264
2482
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
@@ -2292,19 +2510,244 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
|
|
|
2292
2510
|
await suggestPnpmOnInstallFailure(`ensureWorkspaceDepModules: ${root}`);
|
|
2293
2511
|
}
|
|
2294
2512
|
}
|
|
2513
|
+
/** Extract the lines a human needs from a failed npm command's output: the
|
|
2514
|
+
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
2515
|
+
* deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
|
|
2516
|
+
export function distillNpmErrors(combined) {
|
|
2517
|
+
const key = [];
|
|
2518
|
+
const seen = new Set();
|
|
2519
|
+
for (const raw of combined.split('\n')) {
|
|
2520
|
+
const m = raw.trimEnd().match(/^npm (?:error|ERR!)\s?(.*)$/);
|
|
2521
|
+
if (!m || !m[1])
|
|
2522
|
+
continue;
|
|
2523
|
+
const msg = m[1];
|
|
2524
|
+
if (/^\s*at /.test(msg))
|
|
2525
|
+
continue; // stack frames
|
|
2526
|
+
if (/^[\s{}\[\]),]*$/.test(msg))
|
|
2527
|
+
continue; // object-dump punctuation lines
|
|
2528
|
+
if (seen.has(msg))
|
|
2529
|
+
continue;
|
|
2530
|
+
seen.add(msg);
|
|
2531
|
+
key.push(msg);
|
|
2532
|
+
}
|
|
2533
|
+
return key.slice(0, 12);
|
|
2534
|
+
}
|
|
2535
|
+
/** Write the full output of a failed npm command to a log file under the temp
|
|
2536
|
+
* dir and return its path (null if the write itself fails). */
|
|
2537
|
+
function saveNpmLog(label, content) {
|
|
2538
|
+
try {
|
|
2539
|
+
const dir = path.join(os.tmpdir(), 'npmglobalize-logs');
|
|
2540
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2541
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2542
|
+
const file = path.join(dir, `${label.replace(/[^a-z0-9.@-]+/gi, '_')}-${stamp}.log`);
|
|
2543
|
+
fs.writeFileSync(file, content);
|
|
2544
|
+
return file;
|
|
2545
|
+
}
|
|
2546
|
+
catch {
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/** True if `dir` contains a browser executable somewhere below it. A healthy
|
|
2551
|
+
* puppeteer browser folder always has one; a truncated extraction doesn't
|
|
2552
|
+
* (extraction writes the small metadata files first — manifest, ABOUT,
|
|
2553
|
+
* licenses — so an interrupted one holds only those). */
|
|
2554
|
+
function containsBrowserExecutable(dir) {
|
|
2555
|
+
try {
|
|
2556
|
+
for (const entry of fs.readdirSync(dir, { recursive: true })) {
|
|
2557
|
+
const name = String(entry);
|
|
2558
|
+
if (process.platform === 'win32' ? /\.exe$/i.test(name) : /(^|[\\/])(chrome|chrome-headless-shell|firefox)$/.test(name))
|
|
2559
|
+
return true;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
catch {
|
|
2563
|
+
// unreadable — treat as not-a-healthy-browser; caller only acts on puppeteer cache paths
|
|
2564
|
+
}
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
/** Extract a puppeteer browser archive with the SYSTEM unzipper into a fresh
|
|
2568
|
+
* version folder. Workaround for a third-party bug: puppeteer's installer
|
|
2569
|
+
* unpacks zips with the `extract-zip` package, which on Node 26 silently
|
|
2570
|
+
* truncates — its promise never resolves, the event loop drains, and the
|
|
2571
|
+
* process exits 0 after writing only the first few small files. The download
|
|
2572
|
+
* itself is fine (and the archive is left behind, since the hung promise
|
|
2573
|
+
* means puppeteer's cleanup never runs), so completing the extraction
|
|
2574
|
+
* ourselves both repairs the folder and converges where delete-and-retry
|
|
2575
|
+
* cannot. TODO: drop this when puppeteer/extract-zip fix Node 26 extraction. */
|
|
2576
|
+
function extractBrowserArchive(archivePath, folder) {
|
|
2577
|
+
// Git-Bash puts GNU tar (no zip support) first on PATH — use bsdtar
|
|
2578
|
+
// explicitly on Windows; macOS tar is bsdtar; Linux needs unzip.
|
|
2579
|
+
const [cmd, args] = process.platform === 'win32'
|
|
2580
|
+
? [path.join(process.env.SystemRoot || 'C:\\WINDOWS', 'system32', 'tar.exe'), ['-xf', archivePath, '-C', folder]]
|
|
2581
|
+
: process.platform === 'darwin'
|
|
2582
|
+
? ['tar', ['-xf', archivePath, '-C', folder]]
|
|
2583
|
+
: ['unzip', ['-oq', archivePath, '-d', folder]];
|
|
2584
|
+
try {
|
|
2585
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
2586
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
2587
|
+
const r = spawnSafe(cmd, args, { stdio: 'pipe', encoding: 'utf-8' });
|
|
2588
|
+
return r.status === 0 && containsBrowserExecutable(folder);
|
|
2589
|
+
}
|
|
2590
|
+
catch {
|
|
2591
|
+
return false;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
/** Scan a puppeteer browser cache (layout <root>/<browser>/<platform-version>)
|
|
2595
|
+
* and repair every version folder that has no browser executable in it — the
|
|
2596
|
+
* residue of a truncated extraction (see extractBrowserArchive for the Node
|
|
2597
|
+
* 26 extract-zip bug that produces these). Per corrupt folder:
|
|
2598
|
+
* 1. If the downloaded archive still sits next to it (the usual case right
|
|
2599
|
+
* after a truncating run — the hung extraction means puppeteer's cleanup
|
|
2600
|
+
* never deleted it), finish the extraction with the system unzipper. This
|
|
2601
|
+
* is the only path that converges on Node 26, where re-extraction by
|
|
2602
|
+
* puppeteer truncates again every time.
|
|
2603
|
+
* 2. Otherwise delete the folder so the next attempt re-downloads.
|
|
2604
|
+
*
|
|
2605
|
+
* Timing matters: a truncating install EXITS 0, so the poisoned cache hides
|
|
2606
|
+
* behind a successful install — and the NEXT install deletes the leftover
|
|
2607
|
+
* archive before erroring. So this must run right after a SUCCESSFUL install
|
|
2608
|
+
* (while the archive is still there), not only on failure. */
|
|
2609
|
+
export function verifyPuppeteerCache(cacheRoot) {
|
|
2610
|
+
const root = cacheRoot || process.env.PUPPETEER_CACHE_DIR || path.join(os.homedir(), '.cache', 'puppeteer');
|
|
2611
|
+
const result = { repaired: [], removed: [] };
|
|
2612
|
+
let browserDirs;
|
|
2613
|
+
try {
|
|
2614
|
+
browserDirs = fs.readdirSync(root, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
2615
|
+
}
|
|
2616
|
+
catch {
|
|
2617
|
+
return result; // no puppeteer cache — nothing to verify
|
|
2618
|
+
}
|
|
2619
|
+
for (const browser of browserDirs) {
|
|
2620
|
+
const browserDir = path.join(root, browser.name);
|
|
2621
|
+
let entries;
|
|
2622
|
+
try {
|
|
2623
|
+
entries = fs.readdirSync(browserDir, { withFileTypes: true });
|
|
2624
|
+
}
|
|
2625
|
+
catch {
|
|
2626
|
+
continue;
|
|
2627
|
+
}
|
|
2628
|
+
for (const entry of entries) {
|
|
2629
|
+
// version folders look like win64-148.0.7778.97 / mac_arm-131.0.6778.204
|
|
2630
|
+
if (!entry.isDirectory() || !/^[a-z0-9_]+-[\d.]+$/i.test(entry.name))
|
|
2631
|
+
continue;
|
|
2632
|
+
const folder = path.join(browserDir, entry.name);
|
|
2633
|
+
if (containsBrowserExecutable(folder))
|
|
2634
|
+
continue;
|
|
2635
|
+
try {
|
|
2636
|
+
// archive sits next to the version folder, named e.g.
|
|
2637
|
+
// chrome/148.0.7778.97-chrome-win64.zip for chrome/win64-148.0.7778.97
|
|
2638
|
+
const version = entry.name.slice(entry.name.indexOf('-') + 1);
|
|
2639
|
+
const archive = entries.find(e => e.isFile() && e.name.startsWith(`${version}-`) && e.name.endsWith('.zip'))?.name;
|
|
2640
|
+
if (archive && extractBrowserArchive(path.join(browserDir, archive), folder)) {
|
|
2641
|
+
fs.rmSync(path.join(browserDir, archive), { force: true }); // puppeteer's own cleanup never ran
|
|
2642
|
+
result.repaired.push(folder);
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
2646
|
+
result.removed.push(folder);
|
|
2647
|
+
}
|
|
2648
|
+
catch {
|
|
2649
|
+
// couldn't repair or delete (locked?) — leave it; install will report it
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
return result;
|
|
2654
|
+
}
|
|
2655
|
+
/** Detect puppeteer's corrupted-browser-cache failure ("browser folder exists
|
|
2656
|
+
* but the executable is missing") in failed install output and repair the
|
|
2657
|
+
* whole cache it points into (verifyPuppeteerCache — the installer checks
|
|
2658
|
+
* browsers in parallel and only reports the first corrupt one, so siblings
|
|
2659
|
+
* are usually poisoned too). Returns the folders repaired or removed. */
|
|
2660
|
+
export function fixPuppeteerCorruptedCache(combined) {
|
|
2661
|
+
for (const m of combined.matchAll(/The browser folder \((.+?)\) exists but the executable \((.+?)\) is missing/g)) {
|
|
2662
|
+
const folder = m[1];
|
|
2663
|
+
// Only ever touch a puppeteer cache — never an arbitrary path parsed from output
|
|
2664
|
+
if (!/[\\/]puppeteer[\\/]/i.test(folder))
|
|
2665
|
+
continue;
|
|
2666
|
+
// cache layout is <root>/<browser>/<platform-version>
|
|
2667
|
+
return verifyPuppeteerCache(path.dirname(path.dirname(folder)));
|
|
2668
|
+
}
|
|
2669
|
+
return { repaired: [], removed: [] };
|
|
2670
|
+
}
|
|
2671
|
+
/** npm 11.17+ gates lifecycle scripts behind `allowScripts` — non-covered
|
|
2672
|
+
* scripts are skipped and the install still exits 0. A skipped postinstall
|
|
2673
|
+
* can leave the package broken at runtime (puppeteer without its browser,
|
|
2674
|
+
* native modules unbuilt), so surface the warning instead of burying it in
|
|
2675
|
+
* the captured output. */
|
|
2676
|
+
function warnSkippedScripts(combined) {
|
|
2677
|
+
const pkgs = [...combined.matchAll(/^npm warn allow-scripts\s+(\S+@[^\s(]+) \(/gm)];
|
|
2678
|
+
if (!pkgs.length)
|
|
2679
|
+
return;
|
|
2680
|
+
const names = pkgs.map(m => m[1].replace(/@[^@]+$/, ''));
|
|
2681
|
+
console.log(colors.yellow(` ⚠ npm skipped install scripts (allowScripts): ${pkgs.map(m => m[1]).join(', ')}`));
|
|
2682
|
+
console.log(colors.yellow(` Package may be broken at runtime. Allow with: npm config set allow-scripts=${names.join(',')} --location=user`));
|
|
2683
|
+
}
|
|
2295
2684
|
/** Run npm install -g with retries for registry propagation delay.
|
|
2296
2685
|
* Brand-new packages (first-time publish) take much longer to become
|
|
2297
2686
|
* installable than version bumps, so we use longer waits and more
|
|
2298
|
-
* attempts when `isNewPackage` is true.
|
|
2687
|
+
* attempts when `isNewPackage` is true.
|
|
2688
|
+
*
|
|
2689
|
+
* Output is captured, not streamed: npm failures produce hundreds of lines
|
|
2690
|
+
* (cleanup warns, stack traces, object dumps). The terminal gets a distilled
|
|
2691
|
+
* error summary; the full text of all attempts goes to a log file.
|
|
2692
|
+
* Known-fixable failures (puppeteer's corrupted browser cache) are repaired
|
|
2693
|
+
* automatically and granted one immediate bonus retry. */
|
|
2299
2694
|
async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRetries) {
|
|
2300
2695
|
const retries = maxRetries ?? (isNewPackage ? 6 : 3);
|
|
2301
2696
|
const delaySec = isNewPackage ? 30 : 10;
|
|
2302
|
-
let
|
|
2303
|
-
|
|
2697
|
+
let fullLog = '';
|
|
2698
|
+
let result = { success: false, output: '', stderr: '' };
|
|
2699
|
+
let attempt = 0;
|
|
2700
|
+
let cacheFixRounds = 0;
|
|
2701
|
+
while (true) {
|
|
2702
|
+
console.log(colors.cyan(`> npm install -g ${pkgSpec}`));
|
|
2703
|
+
result = await runCommandAsync('npm', ['install', '-g', pkgSpec], { cwd, silent: true });
|
|
2704
|
+
const combined = `${result.output}\n${result.stderr}`;
|
|
2705
|
+
fullLog += `===== attempt ${attempt + 1}: npm install -g ${pkgSpec} =====\n${combined}\n`;
|
|
2706
|
+
if (result.success) {
|
|
2707
|
+
const added = combined.match(/^added \d+ packages?.*$/m);
|
|
2708
|
+
if (added)
|
|
2709
|
+
console.log(colors.dim(` ${added[0]}`));
|
|
2710
|
+
warnSkippedScripts(combined);
|
|
2711
|
+
// A truncated puppeteer browser extraction exits 0, so the install
|
|
2712
|
+
// LOOKS fine while the cache is poisoned for every future install.
|
|
2713
|
+
// Verify now, while the leftover archive still allows a repair.
|
|
2714
|
+
const verify = verifyPuppeteerCache();
|
|
2715
|
+
for (const folder of verify.repaired)
|
|
2716
|
+
console.log(colors.yellow(` ↻ Repaired truncated puppeteer browser extraction (from cached archive): ${folder}`));
|
|
2717
|
+
for (const folder of verify.removed)
|
|
2718
|
+
console.log(colors.yellow(` ↻ Removed corrupted puppeteer browser cache (no archive to repair from — will re-download when needed): ${folder}`));
|
|
2719
|
+
return result;
|
|
2720
|
+
}
|
|
2721
|
+
const cleanupFails = (combined.match(/npm warn cleanup\s+\[Error: EPERM/g) || []).length;
|
|
2722
|
+
if (cleanupFails)
|
|
2723
|
+
console.log(colors.dim(` (npm couldn't remove ${cleanupFails} leftover dir(s) — EPERM cleanup warnings, details in log)`));
|
|
2724
|
+
for (const line of distillNpmErrors(combined))
|
|
2725
|
+
console.error(colors.red(` ${line}`));
|
|
2726
|
+
// Known-fixable failure: repair and retry immediately — the cause is
|
|
2727
|
+
// local, so waiting for registry propagation is pointless. Puppeteer
|
|
2728
|
+
// reports one corrupted browser per run, so allow a few repair rounds
|
|
2729
|
+
// (each must actually repair or remove a folder to earn another retry).
|
|
2730
|
+
if (cacheFixRounds < 3) {
|
|
2731
|
+
const fix = fixPuppeteerCorruptedCache(combined);
|
|
2732
|
+
if (fix.repaired.length || fix.removed.length) {
|
|
2733
|
+
cacheFixRounds++;
|
|
2734
|
+
for (const folder of fix.repaired)
|
|
2735
|
+
console.log(colors.yellow(` ↻ Repaired corrupted puppeteer browser cache (extracted cached archive): ${folder}`));
|
|
2736
|
+
for (const folder of fix.removed)
|
|
2737
|
+
console.log(colors.yellow(` ↻ Removed corrupted puppeteer browser cache (will re-download): ${folder}`));
|
|
2738
|
+
console.log(colors.yellow(` (puppeteer's extract-zip truncates silently on Node 26, leaving folders without executables) — retrying now`));
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
attempt++;
|
|
2743
|
+
if (attempt >= retries)
|
|
2744
|
+
break;
|
|
2304
2745
|
console.log(colors.yellow(` Retrying install (attempt ${attempt + 1}/${retries}) in ${delaySec} seconds...`));
|
|
2305
2746
|
sleepSync(delaySec * 1000);
|
|
2306
|
-
result = await runCommandAsync('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
|
|
2307
2747
|
}
|
|
2748
|
+
const logFile = saveNpmLog(`install-${pkgSpec === '.' ? path.basename(cwd) : pkgSpec}`, fullLog);
|
|
2749
|
+
if (logFile)
|
|
2750
|
+
console.error(colors.dim(` Full npm output: ${logFile}`));
|
|
2308
2751
|
return result;
|
|
2309
2752
|
}
|
|
2310
2753
|
/** Run a command and return success status */
|
|
@@ -2439,36 +2882,133 @@ function dumpPackCtrlcDiagnostics(pkg, cwd, packOutput, packStderr) {
|
|
|
2439
2882
|
/** Install a package globally in WSL, auto-fixing the root-owned /usr/local/lib/node_modules
|
|
2440
2883
|
* EACCES case by switching npm to a user prefix (~/.npm-global) and retrying once.
|
|
2441
2884
|
* Output is captured (so we can scan for the error) and then mirrored to the terminal. */
|
|
2885
|
+
/** Pull the install spec (e.g. `@scope/name@1.2.3`) out of WSL install args.
|
|
2886
|
+
* Returns null for a local-dir install (`.`) or when no spec is present —
|
|
2887
|
+
* those can't 404 on registry propagation. */
|
|
2888
|
+
function extractWslInstallSpec(wslArgs) {
|
|
2889
|
+
const idx = wslArgs.findIndex(a => a === 'install' || a === 'i');
|
|
2890
|
+
if (idx === -1)
|
|
2891
|
+
return null;
|
|
2892
|
+
for (const a of wslArgs.slice(idx + 1)) {
|
|
2893
|
+
if (a.startsWith('-'))
|
|
2894
|
+
continue; // flag
|
|
2895
|
+
if (a === '.' || a === './')
|
|
2896
|
+
return null; // local dir install
|
|
2897
|
+
return a; // first positional = the spec
|
|
2898
|
+
}
|
|
2899
|
+
return null;
|
|
2900
|
+
}
|
|
2901
|
+
/** Is WSL's npm authenticated to the registry? (npm masks a private-package 401
|
|
2902
|
+
* as a 404, so a WSL install can fail with E404 purely because WSL's token is
|
|
2903
|
+
* stale/missing while Windows' token is fine.) */
|
|
2904
|
+
async function isWslNpmAuthed() {
|
|
2905
|
+
const r = await runCommandAsync('wsl', ['npm', 'whoami'], { silent: true });
|
|
2906
|
+
return r.success && (r.output || '').trim().length > 0;
|
|
2907
|
+
}
|
|
2908
|
+
/** Copy the current Windows npm authToken into WSL's npm config. WSL keeps its
|
|
2909
|
+
* own `~/.npmrc` (separate `/home/<user>`), so a token rotated on Windows leaves
|
|
2910
|
+
* WSL stale → private installs 404. Uses `wsl npm config set` (argv passed
|
|
2911
|
+
* directly via spawn — no shell-quoting hazards). Returns true if a token was
|
|
2912
|
+
* found on Windows and the set command succeeded. */
|
|
2913
|
+
async function syncNpmTokenToWsl() {
|
|
2914
|
+
let token;
|
|
2915
|
+
try {
|
|
2916
|
+
const winNpmrc = path.join(os.homedir(), '.npmrc');
|
|
2917
|
+
token = fs.readFileSync(winNpmrc, 'utf8').match(/\/\/registry\.npmjs\.org\/:_authToken=(\S+)/)?.[1];
|
|
2918
|
+
}
|
|
2919
|
+
catch {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
if (!token)
|
|
2923
|
+
return false;
|
|
2924
|
+
const r = await runCommandAsync('wsl', ['npm', 'config', 'set', `//registry.npmjs.org/:_authToken=${token}`], { silent: true });
|
|
2925
|
+
return r.success;
|
|
2926
|
+
}
|
|
2927
|
+
/** Poll the npm registry *from inside WSL* until `spec` resolves there. The
|
|
2928
|
+
* Windows-side `waitForNpmVersion` can report "ready" while WSL's npm still
|
|
2929
|
+
* hits a CDN edge that 404s — so for a WSL install we must confirm visibility
|
|
2930
|
+
* in WSL's own view before retrying. */
|
|
2931
|
+
async function waitForNpmVersionInWsl(spec, maxWaitMs = 180000) {
|
|
2932
|
+
const interval = 10000;
|
|
2933
|
+
const maxAttempts = Math.ceil(maxWaitMs / interval);
|
|
2934
|
+
process.stdout.write(`${timestamp()} Waiting for ${spec} on npm registry (WSL view)`);
|
|
2935
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
2936
|
+
const r = await runCommandAsync('wsl', ['npm', 'view', spec, 'version'], { silent: true });
|
|
2937
|
+
if (r.success && (r.output || '').trim()) {
|
|
2938
|
+
process.stdout.write(' ready\n');
|
|
2939
|
+
return true;
|
|
2940
|
+
}
|
|
2941
|
+
process.stdout.write('.');
|
|
2942
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
2943
|
+
}
|
|
2944
|
+
process.stdout.write(' timed out\n');
|
|
2945
|
+
return false;
|
|
2946
|
+
}
|
|
2442
2947
|
export async function installInWsl(wslArgs, opts = {}) {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2948
|
+
const runOnce = async () => {
|
|
2949
|
+
console.log(colors.cyan(`> wsl ${wslArgs.join(' ')}`));
|
|
2950
|
+
const r = await runCommandAsync('wsl', wslArgs, { cwd: opts.cwd, silent: true });
|
|
2951
|
+
if (r.output)
|
|
2952
|
+
process.stdout.write(r.output);
|
|
2953
|
+
if (r.stderr)
|
|
2954
|
+
process.stderr.write(r.stderr);
|
|
2955
|
+
return r;
|
|
2956
|
+
};
|
|
2957
|
+
let result = await runOnce();
|
|
2449
2958
|
if (result.success)
|
|
2450
2959
|
return { success: true, fixed: false };
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
if (
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2960
|
+
let combined = (result.output || '') + '\n' + (result.stderr || '');
|
|
2961
|
+
// EACCES on a root-owned npm prefix → switch to a user prefix and retry.
|
|
2962
|
+
if (/EACCES/.test(combined) && /\/usr\/(?:local\/)?lib\/node_modules/.test(combined)) {
|
|
2963
|
+
console.log(colors.yellow('Detected root-owned npm prefix in WSL — switching to ~/.npm-global and retrying...'));
|
|
2964
|
+
const fix = `set -e; npm config set prefix "$HOME/.npm-global"; mkdir -p "$HOME/.npm-global/bin"; if ! grep -q '\\.npm-global/bin' "$HOME/.bashrc" 2>/dev/null; then printf '\\n# npm user-prefix bin (added by npmglobalize)\\nexport PATH="$HOME/.npm-global/bin:$PATH"\\n' >> "$HOME/.bashrc"; fi`;
|
|
2965
|
+
const fixResult = await runCommandAsync('wsl', ['bash', '-lc', fix], { silent: true });
|
|
2966
|
+
if (!fixResult.success) {
|
|
2967
|
+
console.error(colors.red('Failed to apply WSL npm prefix fix:'));
|
|
2968
|
+
if (fixResult.stderr)
|
|
2969
|
+
process.stderr.write(fixResult.stderr);
|
|
2970
|
+
return { success: false, fixed: false };
|
|
2971
|
+
}
|
|
2972
|
+
console.log(colors.green('✓ WSL npm prefix set to ~/.npm-global; PATH appended to ~/.bashrc'));
|
|
2973
|
+
result = await runOnce();
|
|
2974
|
+
if (result.success)
|
|
2975
|
+
return { success: true, fixed: true };
|
|
2976
|
+
combined = (result.output || '') + '\n' + (result.stderr || '');
|
|
2977
|
+
}
|
|
2978
|
+
// E404 on a scoped package has TWO causes, and npm gives the same error for
|
|
2979
|
+
// both (it masks a private-package 401 as 404):
|
|
2980
|
+
// (a) WSL's npm token is stale/missing (Windows is authed, WSL isn't), or
|
|
2981
|
+
// (b) genuine registry propagation lag for a just-published version.
|
|
2982
|
+
// Distinguish via `wsl npm whoami` and apply the matching fix.
|
|
2983
|
+
if (/E404|404 Not Found/.test(combined)) {
|
|
2984
|
+
const spec = extractWslInstallSpec(wslArgs);
|
|
2985
|
+
if (!(await isWslNpmAuthed())) {
|
|
2986
|
+
// (a) Auth: sync the Windows token into WSL, then retry.
|
|
2987
|
+
console.log(colors.yellow("WSL's npm isn't authenticated (stale/missing token) — syncing your Windows npm token to WSL..."));
|
|
2988
|
+
if (await syncNpmTokenToWsl() && await isWslNpmAuthed()) {
|
|
2989
|
+
console.log(colors.green('✓ Synced npm token to WSL'));
|
|
2990
|
+
result = await runOnce();
|
|
2991
|
+
if (result.success)
|
|
2992
|
+
return { success: true, fixed: true };
|
|
2993
|
+
}
|
|
2994
|
+
else {
|
|
2995
|
+
console.error(colors.yellow(' Could not authenticate WSL npm. Run `wsl npm login` (or sync your ~/.npmrc token) and retry.'));
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
else if (spec) {
|
|
2999
|
+
// (b) Propagation: WSL is authed but the version isn't visible there yet.
|
|
3000
|
+
console.log(colors.yellow(`WSL npm can't see ${spec} yet — registry propagation lag (Windows already saw it). Waiting in WSL...`));
|
|
3001
|
+
if (await waitForNpmVersionInWsl(spec)) {
|
|
3002
|
+
result = await runOnce();
|
|
3003
|
+
if (result.success)
|
|
3004
|
+
return { success: true, fixed: true };
|
|
3005
|
+
}
|
|
3006
|
+
else {
|
|
3007
|
+
console.error(colors.yellow(` ${spec} still not visible to WSL's npm after waiting — try the WSL install again shortly.`));
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return { success: false, fixed: false };
|
|
2472
3012
|
}
|
|
2473
3013
|
/** Diagnose common build/version failure patterns and print actionable hints.
|
|
2474
3014
|
* Returns true if a diagnosis was printed. */
|
|
@@ -2562,12 +3102,14 @@ function diagnoseBuildFailure(errorText, cwd) {
|
|
|
2562
3102
|
diagnosed = true;
|
|
2563
3103
|
}
|
|
2564
3104
|
// Pattern: TS6 made deprecated options (moduleResolution=node10, ...) hard errors,
|
|
2565
|
-
// removed in TS7. (Safety net for when the auto
|
|
3105
|
+
// removed in TS7. (Safety net for when the auto-migration patch+retry didn't fire —
|
|
3106
|
+
// e.g. an unresolved chain or a non-writable base.)
|
|
2566
3107
|
if (!diagnosed && /error TS510[17]\b/.test(errorText)) {
|
|
2567
|
-
|
|
2568
|
-
console.error(colors.yellow('
|
|
2569
|
-
console.error(colors.yellow(
|
|
2570
|
-
console.error(colors.yellow('
|
|
3108
|
+
console.error(colors.yellow('\n Hint: TypeScript 6 flags long-deprecated options that TS7 removes.'));
|
|
3109
|
+
console.error(colors.yellow(' Fix (do NOT use "ignoreDeprecations" — that only hides the error until TS7):'));
|
|
3110
|
+
console.error(colors.yellow(' • moduleResolution "node10"/"node"/"classic" → "nodenext" (set module to "nodenext" too).'));
|
|
3111
|
+
console.error(colors.yellow(' • target "es3" → "es2022"; remove dead flags (importsNotUsedAsValues, out, …).'));
|
|
3112
|
+
console.error(colors.yellow(' Run npmglobalize -ts7-report for the full per-package list.'));
|
|
2571
3113
|
diagnosed = true;
|
|
2572
3114
|
}
|
|
2573
3115
|
// Pattern: TypeScript compilation error
|
|
@@ -5206,7 +5748,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5206
5748
|
if (pkg.bin && (install || link || wsl)) {
|
|
5207
5749
|
if (link) {
|
|
5208
5750
|
console.log(`Installing ${pkgName} globally from local directory (link)...`);
|
|
5209
|
-
const localInstallResult = await
|
|
5751
|
+
const localInstallResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5210
5752
|
if (localInstallResult.success) {
|
|
5211
5753
|
console.log(colors.green(`✓ Linked globally: ${pkgName}@${pkgVersion}`));
|
|
5212
5754
|
}
|
|
@@ -5221,7 +5763,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5221
5763
|
// sibling imports. Local install handles workspace linking correctly.
|
|
5222
5764
|
if (pkg.workspaces) {
|
|
5223
5765
|
console.log(`Installing ${pkgName}@${pkgVersion} globally from local (workspace)...`);
|
|
5224
|
-
const localResult = await
|
|
5766
|
+
const localResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5225
5767
|
if (localResult.success) {
|
|
5226
5768
|
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
5227
5769
|
}
|
|
@@ -5260,7 +5802,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5260
5802
|
else {
|
|
5261
5803
|
console.log(colors.yellow(`${pkgName}@${pkgVersion} not found on npm — installing from local directory.`));
|
|
5262
5804
|
console.log(colors.dim(' Use -m "message" to publish this version to npm.'));
|
|
5263
|
-
const localResult = await
|
|
5805
|
+
const localResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5264
5806
|
if (localResult.success) {
|
|
5265
5807
|
console.log(colors.green(`✓ Installed globally from local: ${pkgName}@${pkgVersion}`));
|
|
5266
5808
|
}
|
|
@@ -6044,7 +6586,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6044
6586
|
if (link) {
|
|
6045
6587
|
console.log(`Linking globally: ${pkgName}@${pkgVersion}...`);
|
|
6046
6588
|
if (!dryRun) {
|
|
6047
|
-
const installResult = await
|
|
6589
|
+
const installResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
6048
6590
|
if (installResult.success) {
|
|
6049
6591
|
globalInstallOk = true;
|
|
6050
6592
|
console.log(colors.green(`✓ Linked globally: ${pkgName}@${pkgVersion}`));
|
|
@@ -6064,7 +6606,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6064
6606
|
// Workspace packages: install from local for proper workspace linking
|
|
6065
6607
|
console.log(`Installing ${pkgName}@${pkgVersion} globally from local (workspace)...`);
|
|
6066
6608
|
if (!dryRun) {
|
|
6067
|
-
const installResult = await
|
|
6609
|
+
const installResult = await installGlobalWithRetry('.', cwd, false, 1);
|
|
6068
6610
|
if (installResult.success) {
|
|
6069
6611
|
globalInstallOk = true;
|
|
6070
6612
|
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
@@ -6625,7 +7167,7 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
6625
7167
|
// Workspace root is private — always install from local directory
|
|
6626
7168
|
console.log(`Installing ${pkgName}@${pkgVersion} globally from local directory...`);
|
|
6627
7169
|
if (!dryRun) {
|
|
6628
|
-
const installResult = await
|
|
7170
|
+
const installResult = await installGlobalWithRetry('.', rootDir, false, 1);
|
|
6629
7171
|
if (installResult.success) {
|
|
6630
7172
|
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
6631
7173
|
}
|