@bobfrankston/npmglobalize 1.0.193 → 1.0.195
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 +13 -0
- package/cli.js +47 -3
- package/lib.d.ts +29 -1
- package/lib.js +225 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -454,6 +454,19 @@ Workspace mode is auto-detected when run from a root with `"private": true` and
|
|
|
454
454
|
target. Fixes arborist "Cannot read properties of null"
|
|
455
455
|
crashes caused by sibling file: deps with nested
|
|
456
456
|
node_modules. Suggested automatically when the error hits.
|
|
457
|
+
-ts7-report, -deprecation-report
|
|
458
|
+
Report-only: scan this package and its file: deps for
|
|
459
|
+
compilerOptions removed in TypeScript 7 and list a migration
|
|
460
|
+
to-do. Writes nothing.
|
|
461
|
+
-tsfix, -ts7-fix
|
|
462
|
+
One-off utility, NOT part of the main flow: apply the TS7
|
|
463
|
+
tsconfig migration to a package (and its file: deps) and exit —
|
|
464
|
+
no build, no commit, no push, no publish. Intended as a
|
|
465
|
+
temporary tool for fixing a local/subdirectory tsconfig in
|
|
466
|
+
place. The normal release flow already applies the same
|
|
467
|
+
migration automatically when a build hits a TS7 deprecation
|
|
468
|
+
error, so day-to-day you never need this flag.
|
|
469
|
+
npmglobalize <path> -tsfix
|
|
457
470
|
-show Show package.json dependency changes
|
|
458
471
|
-package, -pkg Update package.json scripts to use npmglobalize (see below)
|
|
459
472
|
-h, -help Show help
|
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';
|
|
@@ -56,6 +56,9 @@ Dependency Options:
|
|
|
56
56
|
public visibility to already-published deps in the cascade.
|
|
57
57
|
Use when a public consumer's deps weren't all marked public.
|
|
58
58
|
-no-prescan, -nps Skip upfront dep-graph prescan
|
|
59
|
+
-force-build, -fb Rebuild file: deps even when their output looks up
|
|
60
|
+
to date (freshness check compares .ts/tsconfig
|
|
61
|
+
mtimes against emitted .js/.d.ts)
|
|
59
62
|
-force-publish Republish dependencies even if version exists
|
|
60
63
|
-fix Run npm audit fix after transformation
|
|
61
64
|
-no-use-paths, -nup Declare this package standalone — file: deps shouldn't
|
|
@@ -121,6 +124,13 @@ Other Options:
|
|
|
121
124
|
compilerOptions removed in TypeScript 7 (deprecated
|
|
122
125
|
moduleResolution, target es3, deprecated flags). Lists a
|
|
123
126
|
migration to-do list and exits. Writes nothing.
|
|
127
|
+
-tsfix, -ts7-fix
|
|
128
|
+
One-off utility, not the main flow: apply the TS7 tsconfig
|
|
129
|
+
migration to this package and its file: deps, then exit.
|
|
130
|
+
Only tsconfig files change — no build, no commit, no push,
|
|
131
|
+
no publish. The release flow auto-applies the same migration
|
|
132
|
+
on a TS7 build error; use this only for a temporary in-place
|
|
133
|
+
fix of a local directory: npmglobalize <path> -tsfix
|
|
124
134
|
-show Show package.json dependency changes
|
|
125
135
|
-package, -pkg Update package.json scripts to use npmglobalize
|
|
126
136
|
-h, -help Show this help
|
|
@@ -161,6 +171,7 @@ function parseArgs(args) {
|
|
|
161
171
|
version: false,
|
|
162
172
|
error: '',
|
|
163
173
|
ts7Report: false,
|
|
174
|
+
ts7Fix: false,
|
|
164
175
|
explicitKeys: new Set()
|
|
165
176
|
};
|
|
166
177
|
const unrecognized = [];
|
|
@@ -182,6 +193,10 @@ function parseArgs(args) {
|
|
|
182
193
|
case '-deprecation-report':
|
|
183
194
|
options.ts7Report = true;
|
|
184
195
|
break;
|
|
196
|
+
case '-tsfix':
|
|
197
|
+
case '-ts7-fix':
|
|
198
|
+
options.ts7Fix = true;
|
|
199
|
+
break;
|
|
185
200
|
case '-patch':
|
|
186
201
|
options.bump = 'patch';
|
|
187
202
|
break;
|
|
@@ -233,6 +248,10 @@ function parseArgs(args) {
|
|
|
233
248
|
case '-force':
|
|
234
249
|
options.force = true;
|
|
235
250
|
break;
|
|
251
|
+
case '-force-build':
|
|
252
|
+
case '-fb':
|
|
253
|
+
options.forceBuild = true;
|
|
254
|
+
break;
|
|
236
255
|
case '-files':
|
|
237
256
|
options.files = true;
|
|
238
257
|
options.explicitKeys.add('files');
|
|
@@ -515,19 +534,44 @@ export async function main() {
|
|
|
515
534
|
printTs7Report(cwd);
|
|
516
535
|
process.exit(0);
|
|
517
536
|
}
|
|
537
|
+
// Fix-only TS7 tsconfig migration (no build / commit / push / publish).
|
|
538
|
+
if (cliOptions.ts7Fix) {
|
|
539
|
+
console.log('');
|
|
540
|
+
console.log(colors.accent('━━━ TypeScript 7 tsconfig fix ━━━'));
|
|
541
|
+
const { changedDirs, remaining } = fixTs7Deprecations(cwd);
|
|
542
|
+
if (changedDirs.length === 0) {
|
|
543
|
+
console.log(colors.success(' ✓ Nothing to fix — no removed-in-TS7 compilerOptions in writable tsconfigs.'));
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
console.log(colors.success(` ✓ Patched tsconfig(s) in ${changedDirs.length} package(s):`));
|
|
547
|
+
for (const dir of changedDirs)
|
|
548
|
+
console.log(` ${dir}`);
|
|
549
|
+
console.log(colors.muted(' Rebuild to validate — NodeNext resolution may surface real import errors the old resolver hid.'));
|
|
550
|
+
}
|
|
551
|
+
if (remaining.length > 0) {
|
|
552
|
+
console.log(colors.warn(` ${remaining.length} item(s) could not be auto-fixed:`));
|
|
553
|
+
for (const f of remaining) {
|
|
554
|
+
console.log(colors.warn(` • ${f.name}: ${f.option}=${JSON.stringify(f.value)} (in ${f.definedIn})`));
|
|
555
|
+
console.log(colors.muted(` → ${f.action}`));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
console.log('');
|
|
559
|
+
printBuildSummary();
|
|
560
|
+
process.exit(remaining.length > 0 ? 1 : 0);
|
|
561
|
+
}
|
|
518
562
|
// Build file: deps topologically, then the target itself.
|
|
519
563
|
// Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
|
|
520
564
|
// whose source has changed since their last build.
|
|
521
565
|
if (!cliOptions.cleanup) {
|
|
522
566
|
await ensureFileDepModules(cwd, !!cliOptions.verbose);
|
|
523
|
-
const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
|
|
567
|
+
const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force, forceBuild: !!cliOptions.forceBuild });
|
|
524
568
|
if (!depsOk && !cliOptions.force) {
|
|
525
569
|
printBuildSummary();
|
|
526
570
|
process.exit(1);
|
|
527
571
|
}
|
|
528
572
|
if (!depsOk)
|
|
529
573
|
console.log(colors.warn('Continuing with --force despite dep build failure...'));
|
|
530
|
-
const targetOk = await buildProject(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
|
|
574
|
+
const targetOk = await buildProject(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force, forceBuild: !!cliOptions.forceBuild });
|
|
531
575
|
if (!targetOk) {
|
|
532
576
|
if (!cliOptions.force) {
|
|
533
577
|
printBuildSummary();
|
package/lib.d.ts
CHANGED
|
@@ -108,6 +108,8 @@ export interface GlobalizeOptions {
|
|
|
108
108
|
usePaths?: boolean;
|
|
109
109
|
/** Local install only — skip transform/publish, just npm install -g . */
|
|
110
110
|
local?: boolean;
|
|
111
|
+
/** Build every file: dep even when outputs look up to date (skips the freshness check) */
|
|
112
|
+
forceBuild?: boolean;
|
|
111
113
|
/** Freeze node_modules: replace symlinks/junctions with real copies for network share use */
|
|
112
114
|
freeze?: boolean;
|
|
113
115
|
/** Before `npm pack`, delete `node_modules/` inside each `file:` dep target.
|
|
@@ -311,13 +313,26 @@ export declare function missingDeps(pkgDir: string, pkg: any): string[];
|
|
|
311
313
|
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
312
314
|
* Cycle-safe via the shared `visited` set. */
|
|
313
315
|
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): Promise<void>;
|
|
316
|
+
/** Cheap freshness check so the build cascade can skip packages whose output is
|
|
317
|
+
* already current. Returns true only when provably up to date:
|
|
318
|
+
* - outDir projects: newest source (.ts/.tsx/.mts/.cts + tsconfig*.json) must
|
|
319
|
+
* not be newer than the newest file under outDir.
|
|
320
|
+
* - side-by-side projects: every source must have an emitted sibling
|
|
321
|
+
* (.js/.jsx/.mjs/.cjs or declaration) at least as new.
|
|
322
|
+
* Conservative: project references, allowJs, missing outputs, or unreadable
|
|
323
|
+
* tsconfig all report stale (→ build). package.json mtime is deliberately
|
|
324
|
+
* ignored — npmglobalize itself rewrites it around every publish, which would
|
|
325
|
+
* otherwise force a rebuild on every run. */
|
|
326
|
+
export declare function isBuildUpToDate(cwd: string): boolean;
|
|
314
327
|
/** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
|
|
315
328
|
* TypeScript project lacks a build script, run `npm run build`, record
|
|
316
329
|
* failures. Returns true if build succeeded (or was skipped because no
|
|
317
|
-
* tsconfig / noEmit / no build script after declining the prompt
|
|
330
|
+
* tsconfig / noEmit / no build script after declining the prompt / output
|
|
331
|
+
* already up to date). Pass `forceBuild` to skip the freshness check. */
|
|
318
332
|
export declare function buildProject(cwd: string, opts?: {
|
|
319
333
|
verbose?: boolean;
|
|
320
334
|
force?: boolean;
|
|
335
|
+
forceBuild?: boolean;
|
|
321
336
|
}): Promise<boolean>;
|
|
322
337
|
/** Walk `file:` deps depth-first (deps before consumers) and build each one
|
|
323
338
|
* that has a tsconfig. Mirrors `ensureFileDepModules`'s traversal but invokes
|
|
@@ -327,6 +342,7 @@ export declare function buildProject(cwd: string, opts?: {
|
|
|
327
342
|
export declare function buildFileDepsTopologically(cwd: string, opts?: {
|
|
328
343
|
verbose?: boolean;
|
|
329
344
|
force?: boolean;
|
|
345
|
+
forceBuild?: boolean;
|
|
330
346
|
}, visited?: Set<string>): Promise<boolean>;
|
|
331
347
|
/** A single TS7-readiness problem found in a package's effective tsconfig. */
|
|
332
348
|
export interface Ts7Finding {
|
|
@@ -347,6 +363,18 @@ export interface Ts7Finding {
|
|
|
347
363
|
* The same migration `migrateTsconfigDeprecations` applies automatically on a build
|
|
348
364
|
* failure; this report is the read-only, whole-graph inventory. */
|
|
349
365
|
export declare function reportTs7Deprecations(cwd: string): Ts7Finding[];
|
|
366
|
+
/** Standalone fix mode (`-tsfix`): apply `migrateTsconfigDeprecations` to `cwd`
|
|
367
|
+
* and every package reachable through its `file:` dep graph — the write
|
|
368
|
+
* counterpart of `reportTs7Deprecations`. No git, no version bump, no publish;
|
|
369
|
+
* only tsconfig files change. After migrating, any `ignoreDeprecations` whose
|
|
370
|
+
* package has nothing deprecated left is deleted too (kept, it would silence
|
|
371
|
+
* the next real TS7-removal error). Returns the dirs whose tsconfigs changed
|
|
372
|
+
* plus the findings that remain (e.g. options inherited from read-only
|
|
373
|
+
* node_modules bases). */
|
|
374
|
+
export declare function fixTs7Deprecations(cwd: string): {
|
|
375
|
+
changedDirs: string[];
|
|
376
|
+
remaining: Ts7Finding[];
|
|
377
|
+
};
|
|
350
378
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
351
379
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
352
380
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
package/lib.js
CHANGED
|
@@ -1720,6 +1720,41 @@ export function printPnpmSuggestionSummary() {
|
|
|
1720
1720
|
console.error(l);
|
|
1721
1721
|
console.error('');
|
|
1722
1722
|
}
|
|
1723
|
+
/** Run `npm install` in `dir`; on failure, offer to delete `node_modules/`
|
|
1724
|
+
* and `package-lock.json` and retry once — the standard fix for arborist's
|
|
1725
|
+
* null-`package` crash on stale lockfile / junction state — before falling
|
|
1726
|
+
* back to the pnpm suggestion banner. Returns true if install succeeded.
|
|
1727
|
+
* `rmSync` unlinks junctions without descending, so file: sibling targets
|
|
1728
|
+
* are never touched. */
|
|
1729
|
+
async function npmInstallWithCleanRetry(dir, verbose, context) {
|
|
1730
|
+
let r = await runCommandAsync('npm', ['install'], { cwd: dir, silent: !verbose });
|
|
1731
|
+
if (r.success)
|
|
1732
|
+
return true;
|
|
1733
|
+
console.error(colors.red(` ✗ npm install failed in ${dir}`));
|
|
1734
|
+
if (r.stderr)
|
|
1735
|
+
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
1736
|
+
if (await confirm(` Delete node_modules/ and package-lock.json in ${dir} and retry npm install?`, true)) {
|
|
1737
|
+
try {
|
|
1738
|
+
fs.rmSync(path.join(dir, 'node_modules'), { recursive: true, force: true });
|
|
1739
|
+
}
|
|
1740
|
+
catch { /* best-effort */ }
|
|
1741
|
+
try {
|
|
1742
|
+
fs.rmSync(path.join(dir, 'package-lock.json'), { force: true });
|
|
1743
|
+
}
|
|
1744
|
+
catch { /* best-effort */ }
|
|
1745
|
+
console.log(colors.yellow(`↻ clean reinstall in ${dir}`));
|
|
1746
|
+
r = await runCommandAsync('npm', ['install'], { cwd: dir, silent: !verbose });
|
|
1747
|
+
if (r.success) {
|
|
1748
|
+
console.log(colors.green(` ✓ clean reinstall succeeded in ${dir}`));
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
console.error(colors.red(` ✗ npm install still failing in ${dir} after clean reinstall`));
|
|
1752
|
+
if (r.stderr)
|
|
1753
|
+
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
1754
|
+
}
|
|
1755
|
+
await suggestPnpmOnInstallFailure(context);
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1723
1758
|
/** Wait for a package version to appear on the npm registry.
|
|
1724
1759
|
* First-time publishes (brand-new package name) take much longer to
|
|
1725
1760
|
* propagate than version bumps — npm has no cached metadata to update,
|
|
@@ -1877,13 +1912,7 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
1877
1912
|
catch { /* best-effort */ }
|
|
1878
1913
|
}
|
|
1879
1914
|
console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs} (${formatMissingReason(cwdMissing)})`));
|
|
1880
|
-
|
|
1881
|
-
if (!r.success) {
|
|
1882
|
-
console.error(colors.red(` ✗ npm install failed in ${abs}`));
|
|
1883
|
-
if (r.stderr)
|
|
1884
|
-
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
1885
|
-
await suggestPnpmOnInstallFailure(`ensureFileDepModules (cwd): ${abs}`);
|
|
1886
|
-
}
|
|
1915
|
+
await npmInstallWithCleanRetry(abs, verbose, `ensureFileDepModules (cwd): ${abs}`);
|
|
1887
1916
|
}
|
|
1888
1917
|
for (const key of ['dependencies', 'devDependencies']) {
|
|
1889
1918
|
const deps = pkg?.[key];
|
|
@@ -1915,13 +1944,7 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
1915
1944
|
catch { /* best-effort */ }
|
|
1916
1945
|
}
|
|
1917
1946
|
console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target}) (${formatMissingReason(targetMissing)})`));
|
|
1918
|
-
|
|
1919
|
-
if (!r.success) {
|
|
1920
|
-
console.error(colors.red(` ✗ npm install failed in ${target}`));
|
|
1921
|
-
if (r.stderr)
|
|
1922
|
-
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
1923
|
-
await suggestPnpmOnInstallFailure(`ensureFileDepModules (file: dep ${name}): ${target}`);
|
|
1924
|
-
}
|
|
1947
|
+
await npmInstallWithCleanRetry(target, verbose, `ensureFileDepModules (file: dep ${name}): ${target}`);
|
|
1925
1948
|
}
|
|
1926
1949
|
await ensureFileDepModules(target, verbose, visited);
|
|
1927
1950
|
}
|
|
@@ -2244,10 +2267,144 @@ function migrateTsconfigDeprecations(cwd) {
|
|
|
2244
2267
|
}
|
|
2245
2268
|
return changed;
|
|
2246
2269
|
}
|
|
2270
|
+
/** Newest mtime of any regular file under `dir` (recursive). Symlinks/junctions
|
|
2271
|
+
* are not followed — the file: dep webs here link back into sibling checkouts. */
|
|
2272
|
+
function newestMtimeUnder(dir) {
|
|
2273
|
+
let newest = 0;
|
|
2274
|
+
let entries;
|
|
2275
|
+
try {
|
|
2276
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2277
|
+
}
|
|
2278
|
+
catch {
|
|
2279
|
+
return 0;
|
|
2280
|
+
}
|
|
2281
|
+
for (const e of entries) {
|
|
2282
|
+
if (e.isSymbolicLink())
|
|
2283
|
+
continue;
|
|
2284
|
+
const full = path.join(dir, e.name);
|
|
2285
|
+
if (e.isDirectory()) {
|
|
2286
|
+
newest = Math.max(newest, newestMtimeUnder(full));
|
|
2287
|
+
}
|
|
2288
|
+
else if (e.isFile()) {
|
|
2289
|
+
try {
|
|
2290
|
+
newest = Math.max(newest, fs.statSync(full).mtimeMs);
|
|
2291
|
+
}
|
|
2292
|
+
catch { /* ignore */ }
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return newest;
|
|
2296
|
+
}
|
|
2297
|
+
/** Cheap freshness check so the build cascade can skip packages whose output is
|
|
2298
|
+
* already current. Returns true only when provably up to date:
|
|
2299
|
+
* - outDir projects: newest source (.ts/.tsx/.mts/.cts + tsconfig*.json) must
|
|
2300
|
+
* not be newer than the newest file under outDir.
|
|
2301
|
+
* - side-by-side projects: every source must have an emitted sibling
|
|
2302
|
+
* (.js/.jsx/.mjs/.cjs or declaration) at least as new.
|
|
2303
|
+
* Conservative: project references, allowJs, missing outputs, or unreadable
|
|
2304
|
+
* tsconfig all report stale (→ build). package.json mtime is deliberately
|
|
2305
|
+
* ignored — npmglobalize itself rewrites it around every publish, which would
|
|
2306
|
+
* otherwise force a rebuild on every run. */
|
|
2307
|
+
export function isBuildUpToDate(cwd) {
|
|
2308
|
+
let tsconfig;
|
|
2309
|
+
try {
|
|
2310
|
+
tsconfig = JSON5.parse(fs.readFileSync(path.join(cwd, 'tsconfig.json'), 'utf-8'));
|
|
2311
|
+
}
|
|
2312
|
+
catch {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
const co = tsconfig.compilerOptions || {};
|
|
2316
|
+
if (tsconfig.references || co.allowJs || co.checkJs)
|
|
2317
|
+
return false;
|
|
2318
|
+
const outDir = typeof co.outDir === 'string' ? path.resolve(cwd, co.outDir) : null;
|
|
2319
|
+
// tsconfig "exclude" dirs aren't compiled, so their .ts files never get
|
|
2320
|
+
// outputs — don't count them as sources. Match on the literal path prefix
|
|
2321
|
+
// (glob tails like "tests/**" reduce to "tests").
|
|
2322
|
+
const excludes = (Array.isArray(tsconfig.exclude) ? tsconfig.exclude : [])
|
|
2323
|
+
.filter((x) => typeof x === 'string')
|
|
2324
|
+
.map((x) => x.replace(/^\.\//, '').replace(/[\\/]?\*.*$/, '').replace(/[\\/]+$/, '').toLowerCase())
|
|
2325
|
+
.filter((x) => x.length > 0 && !x.includes('*'));
|
|
2326
|
+
const sources = [];
|
|
2327
|
+
const collect = (dir, rel) => {
|
|
2328
|
+
let entries;
|
|
2329
|
+
try {
|
|
2330
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2331
|
+
}
|
|
2332
|
+
catch {
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
for (const e of entries) {
|
|
2336
|
+
if (e.isSymbolicLink())
|
|
2337
|
+
continue;
|
|
2338
|
+
const full = path.join(dir, e.name);
|
|
2339
|
+
const relPath = rel ? `${rel}/${e.name.toLowerCase()}` : e.name.toLowerCase();
|
|
2340
|
+
if (e.isDirectory()) {
|
|
2341
|
+
const lower = e.name.toLowerCase();
|
|
2342
|
+
if (lower === 'node_modules' || lower === 'prev' || e.name.startsWith('.'))
|
|
2343
|
+
continue;
|
|
2344
|
+
if (outDir && path.resolve(full) === outDir)
|
|
2345
|
+
continue;
|
|
2346
|
+
if (excludes.includes(relPath))
|
|
2347
|
+
continue;
|
|
2348
|
+
collect(full, relPath);
|
|
2349
|
+
}
|
|
2350
|
+
else if (/\.(ts|tsx|mts|cts)$/.test(e.name) && !/\.d\.(ts|mts|cts)$/.test(e.name)) {
|
|
2351
|
+
if (excludes.includes(relPath))
|
|
2352
|
+
continue;
|
|
2353
|
+
sources.push(full);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
collect(cwd, '');
|
|
2358
|
+
if (sources.length === 0)
|
|
2359
|
+
return true;
|
|
2360
|
+
const mtime = (p) => {
|
|
2361
|
+
try {
|
|
2362
|
+
return fs.statSync(p).mtimeMs;
|
|
2363
|
+
}
|
|
2364
|
+
catch {
|
|
2365
|
+
return -1;
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
// Any tsconfig*.json edit (own or extended base in the package) invalidates the build.
|
|
2369
|
+
let configTime = 0;
|
|
2370
|
+
try {
|
|
2371
|
+
for (const name of fs.readdirSync(cwd)) {
|
|
2372
|
+
if (/^tsconfig.*\.json$/.test(name))
|
|
2373
|
+
configTime = Math.max(configTime, mtime(path.join(cwd, name)));
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
catch { /* ignore */ }
|
|
2377
|
+
if (outDir) {
|
|
2378
|
+
const newestOut = newestMtimeUnder(outDir);
|
|
2379
|
+
if (newestOut === 0)
|
|
2380
|
+
return false;
|
|
2381
|
+
const newestSrc = Math.max(configTime, ...sources.map(mtime));
|
|
2382
|
+
return newestSrc <= newestOut;
|
|
2383
|
+
}
|
|
2384
|
+
// Side-by-side emit: each source needs an output sibling at least as new.
|
|
2385
|
+
let newestOut = 0;
|
|
2386
|
+
for (const src of sources) {
|
|
2387
|
+
const m = src.match(/\.(ts|tsx|mts|cts)$/);
|
|
2388
|
+
const base = src.slice(0, -m[0].length);
|
|
2389
|
+
const candidates = m[1] === 'mts' ? ['.mjs', '.d.mts']
|
|
2390
|
+
: m[1] === 'cts' ? ['.cjs', '.d.cts']
|
|
2391
|
+
: ['.js', '.jsx', '.d.ts'];
|
|
2392
|
+
let outTime = -1;
|
|
2393
|
+
for (const ext of candidates)
|
|
2394
|
+
outTime = Math.max(outTime, mtime(base + ext));
|
|
2395
|
+
if (outTime < 0)
|
|
2396
|
+
return false;
|
|
2397
|
+
if (mtime(src) > outTime)
|
|
2398
|
+
return false;
|
|
2399
|
+
newestOut = Math.max(newestOut, outTime);
|
|
2400
|
+
}
|
|
2401
|
+
return configTime <= newestOut;
|
|
2402
|
+
}
|
|
2247
2403
|
/** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
|
|
2248
2404
|
* TypeScript project lacks a build script, run `npm run build`, record
|
|
2249
2405
|
* failures. Returns true if build succeeded (or was skipped because no
|
|
2250
|
-
* tsconfig / noEmit / no build script after declining the prompt
|
|
2406
|
+
* tsconfig / noEmit / no build script after declining the prompt / output
|
|
2407
|
+
* already up to date). Pass `forceBuild` to skip the freshness check. */
|
|
2251
2408
|
export async function buildProject(cwd, opts = {}) {
|
|
2252
2409
|
let shouldBuild = false;
|
|
2253
2410
|
try {
|
|
@@ -2277,6 +2434,10 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
2277
2434
|
}
|
|
2278
2435
|
}
|
|
2279
2436
|
ensureTsconfigNodeTypes(cwd);
|
|
2437
|
+
if (!opts.forceBuild && isBuildUpToDate(cwd)) {
|
|
2438
|
+
console.log(colors.dim(`– Build up to date (${pkg.name || path.basename(cwd)})`));
|
|
2439
|
+
return true;
|
|
2440
|
+
}
|
|
2280
2441
|
console.log(`Building ${pkg.name || cwd}...`);
|
|
2281
2442
|
let buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
2282
2443
|
if (!buildResult.success) {
|
|
@@ -2477,6 +2638,53 @@ export function reportTs7Deprecations(cwd) {
|
|
|
2477
2638
|
}
|
|
2478
2639
|
return findings;
|
|
2479
2640
|
}
|
|
2641
|
+
/** Standalone fix mode (`-tsfix`): apply `migrateTsconfigDeprecations` to `cwd`
|
|
2642
|
+
* and every package reachable through its `file:` dep graph — the write
|
|
2643
|
+
* counterpart of `reportTs7Deprecations`. No git, no version bump, no publish;
|
|
2644
|
+
* only tsconfig files change. After migrating, any `ignoreDeprecations` whose
|
|
2645
|
+
* package has nothing deprecated left is deleted too (kept, it would silence
|
|
2646
|
+
* the next real TS7-removal error). Returns the dirs whose tsconfigs changed
|
|
2647
|
+
* plus the findings that remain (e.g. options inherited from read-only
|
|
2648
|
+
* node_modules bases). */
|
|
2649
|
+
export function fixTs7Deprecations(cwd) {
|
|
2650
|
+
const changedDirs = [];
|
|
2651
|
+
for (const dir of collectFileDepDirs(cwd)) {
|
|
2652
|
+
if (migrateTsconfigDeprecations(dir))
|
|
2653
|
+
changedDirs.push(dir);
|
|
2654
|
+
}
|
|
2655
|
+
// Drop ignoreDeprecations wherever nothing deprecated remains in that package.
|
|
2656
|
+
let remaining = reportTs7Deprecations(cwd);
|
|
2657
|
+
const igdDirs = new Set(remaining.filter(f => f.option === 'ignoreDeprecations').map(f => f.dir));
|
|
2658
|
+
let removedIgd = false;
|
|
2659
|
+
for (const dir of igdDirs) {
|
|
2660
|
+
if (remaining.some(f => f.dir === dir && f.option !== 'ignoreDeprecations'))
|
|
2661
|
+
continue;
|
|
2662
|
+
const { chain } = resolveTsconfigChain(path.join(dir, 'tsconfig.json'));
|
|
2663
|
+
const igd = effectiveCompilerOption(chain, 'ignoreDeprecations');
|
|
2664
|
+
if (!igd)
|
|
2665
|
+
continue;
|
|
2666
|
+
const def = chain.find(c => c.path === igd.definedIn);
|
|
2667
|
+
if (!def?.writable)
|
|
2668
|
+
continue;
|
|
2669
|
+
try {
|
|
2670
|
+
const text = fs.readFileSync(def.path, 'utf-8');
|
|
2671
|
+
const patched = removeCompilerOptionLine(text, 'ignoreDeprecations');
|
|
2672
|
+
if (patched != null && patched !== text) {
|
|
2673
|
+
fs.writeFileSync(def.path, patched);
|
|
2674
|
+
removedIgd = true;
|
|
2675
|
+
if (!changedDirs.includes(dir))
|
|
2676
|
+
changedDirs.push(dir);
|
|
2677
|
+
console.log(colors.cyan(` Removed "ignoreDeprecations" from ${path.relative(dir, def.path) || 'tsconfig.json'} (${path.basename(dir)})`));
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
catch (error) {
|
|
2681
|
+
console.error(colors.yellow(` Could not remove ignoreDeprecations (${def.path}): ${error.message}`));
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (removedIgd)
|
|
2685
|
+
remaining = reportTs7Deprecations(cwd);
|
|
2686
|
+
return { changedDirs, remaining };
|
|
2687
|
+
}
|
|
2480
2688
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
2481
2689
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
2482
2690
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
@@ -2502,13 +2710,7 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
|
|
|
2502
2710
|
return;
|
|
2503
2711
|
const list = [...allMissing];
|
|
2504
2712
|
console.log(colors.yellow(`↻ installing workspace node_modules in ${rootPkg?.name || path.basename(root)} (${formatMissingReason(list)})`));
|
|
2505
|
-
|
|
2506
|
-
if (!r.success) {
|
|
2507
|
-
console.error(colors.red(` ✗ npm install failed in ${root}`));
|
|
2508
|
-
if (r.stderr)
|
|
2509
|
-
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
2510
|
-
await suggestPnpmOnInstallFailure(`ensureWorkspaceDepModules: ${root}`);
|
|
2511
|
-
}
|
|
2713
|
+
await npmInstallWithCleanRetry(root, verbose, `ensureWorkspaceDepModules: ${root}`);
|
|
2512
2714
|
}
|
|
2513
2715
|
/** Extract the lines a human needs from a failed npm command's output: the
|
|
2514
2716
|
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
@@ -2704,7 +2906,7 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
|
|
|
2704
2906
|
const combined = `${result.output}\n${result.stderr}`;
|
|
2705
2907
|
fullLog += `===== attempt ${attempt + 1}: npm install -g ${pkgSpec} =====\n${combined}\n`;
|
|
2706
2908
|
if (result.success) {
|
|
2707
|
-
const added = combined.match(/^added \d+ packages?.*$/m);
|
|
2909
|
+
const added = combined.match(/^(?:added|changed|removed) \d+ packages?.*$/m);
|
|
2708
2910
|
if (added)
|
|
2709
2911
|
console.log(colors.dim(` ${added[0]}`));
|
|
2710
2912
|
warnSkippedScripts(combined);
|