@bobfrankston/npmglobalize 1.0.216 → 1.0.218
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 +34 -20
- package/cli.js +14 -2
- package/lib.d.ts +71 -24
- package/lib.js +343 -176
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,19 +142,24 @@ npmglobalize --update-major
|
|
|
142
142
|
|
|
143
143
|
### 🔒 Security Auditing
|
|
144
144
|
|
|
145
|
-
**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
Every run ends with an `npm audit` report. The **Release Summary** (or Transform Summary
|
|
146
|
+
with `-nopublish`) carries an `Audit:` row — `✓ no vulnerabilities`, the severity counts,
|
|
147
|
+
or `not run` with npm's reason (typically `ENOLOCK` when there is no lockfile). Critical
|
|
148
|
+
and high findings are listed one per package in the **Issues Summary** at the very end,
|
|
149
|
+
each with the advisory title and URL, how the package got in (`through …` / `pulled in
|
|
150
|
+
by …` / `direct dependency`), and what would clear it:
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
npmglobalize
|
|
153
|
-
|
|
152
|
+
- `fix: npm audit fix` — a compatible update exists; the next run's fix step takes it
|
|
153
|
+
- `fix: npm audit fix --force → node-gyp@13.0.2 (breaking — not run automatically)` — only a
|
|
154
|
+
semver-major bump clears it; npmglobalize never passes `--force`, that is your call
|
|
155
|
+
- `fix: none published yet` — wait for upstream
|
|
156
|
+
|
|
157
|
+
**Auto-fix** (`-fix`, **on by default**, remembered as `"fix": true` in `.globalize.json5`):
|
|
158
|
+
`npm audit fix` runs before the pre-release commit, so a changed lockfile is committed with
|
|
159
|
+
the release. It only ever installs semver-compatible updates.
|
|
154
160
|
|
|
155
|
-
**Disable audit**:
|
|
156
161
|
```bash
|
|
157
|
-
npmglobalize
|
|
162
|
+
npmglobalize -no-fix # report only, no fix attempt
|
|
158
163
|
```
|
|
159
164
|
|
|
160
165
|
### 🧩 Install Scripts (npm `allowScripts`)
|
|
@@ -409,8 +414,9 @@ not what you meant to publish, so npmglobalize lists the conflicted files and st
|
|
|
409
414
|
-no-publish-deps, -npd Skip auto-publishing file: dependencies
|
|
410
415
|
-no-prescan, -nps Skip upfront dep-graph prescan
|
|
411
416
|
-force-publish Republish dependencies even if version exists
|
|
412
|
-
-fix
|
|
413
|
-
|
|
417
|
+
-fix, -no-fix Run npm audit fix before the pre-release commit (default on,
|
|
418
|
+
never --force). The audit REPORT always runs; critical/high
|
|
419
|
+
findings are listed in the Issues Summary with a fix hint.
|
|
414
420
|
-no-use-paths, -nup Declare package standalone; do not resolve file: deps
|
|
415
421
|
from sibling checkouts (see Configuration File)
|
|
416
422
|
```
|
|
@@ -703,12 +709,15 @@ npmglobalize --dry-run # See what would happen
|
|
|
703
709
|
11. **Pushes** to git (with push-protection detection and auto-bypass)
|
|
704
710
|
12. **Installs** globally (if `--install`)
|
|
705
711
|
13. **Restores** file: references (if `--files`, default)
|
|
706
|
-
14. **Runs audit**
|
|
712
|
+
14. **Runs audit** — `Audit:` row in the Release Summary; critical/high findings explained in the Issues Summary
|
|
707
713
|
|
|
708
714
|
## Operational Details
|
|
709
715
|
|
|
710
716
|
### Build Cascade
|
|
711
717
|
|
|
718
|
+
Before anything is built, every project in the `file:` tree with missing deps gets an `npm install`. If one fails, the run **stops** there — the compiler/linker/npm lines that name the failure are printed (the full output goes to a log under `%TEMP%
|
|
719
|
+
pmglobalize-logs`), the failure is repeated in the Issues Summary, and nothing is built, published, or installed on top of a broken tree. `-force` continues anyway. A stale-lockfile / junction failure offers a clean reinstall (delete `node_modules/` + `package-lock.json`, retry once); a compiler or linker error does not, since a reinstall would reach the same line.
|
|
720
|
+
|
|
712
721
|
Before transforming or publishing anything, `npmglobalize` builds `file:` dependencies in topological order — deps before consumers — and then builds the target itself. This guarantees the target's `tsc` reads up-to-date `.d.ts` and `.js` from sibling checkouts even when a dep's source has changed since its last build.
|
|
713
722
|
|
|
714
723
|
For each project visited (the target and every transitive `file:` dep):
|
|
@@ -727,7 +736,7 @@ Browser projects that use [`importgen`](https://www.npmjs.com/package/@bobfranks
|
|
|
727
736
|
|
|
728
737
|
Before building each project, it checks whether the project is an importgen project, in this order:
|
|
729
738
|
|
|
730
|
-
1. `.vscode/tasks.json` has a task whose `command` is `importgen` (the HTML file, if the task names one, is reused).
|
|
739
|
+
1. `.vscode/tasks.json` has a task whose `command` is `importgen` (the HTML file, if the task names one, is reused). The task's `"options": { "cwd": ... }` is honored too: a plain sub-directory (`www/`) yields `importgen www/index.html`, while a sub-directory that is a package of its own (`client/package.json`) is not a root importgen project at all — its own build script owns the import map and the root build delegates to it (see Sub-projects below).
|
|
731
740
|
2. A root-level `.htm`/`.html` file already contains a `<script type="importmap">` block (`index.html`, `default.html`, `default.htm` are checked first, so a stray `temp.htm` doesn't win).
|
|
732
741
|
3. `importgen` is in `dependencies`/`devDependencies` **and** the project has one of those HTML files — the HTML requirement keeps packages that merely *use* importgen as a library from matching.
|
|
733
742
|
|
|
@@ -735,20 +744,25 @@ If a signal matches and the `build` script doesn't already run `importgen`, you'
|
|
|
735
744
|
|
|
736
745
|
Once wired, the freshness check gains a second condition: a project whose build runs importgen is only considered up to date if the generated HTML is at least as new as `package.json`. Adding a dependency changes the import map without touching any `.ts` file, which the source-vs-output comparison alone would miss.
|
|
737
746
|
|
|
738
|
-
#### Sub-projects (a
|
|
747
|
+
#### Sub-projects (a sub-directory with its own build script or `tsconfig.json`)
|
|
748
|
+
|
|
749
|
+
A package can hold more than one project. Two shapes, treated differently:
|
|
750
|
+
|
|
751
|
+
- **A sub-package** — `client/` with its own `package.json` whose `build` script (`importgen index.html && tsc`) already says how the client builds. That script is the single source of truth, so the root build **delegates** to it: `npm run build --prefix client`. The root never re-implements the client's recipe (no `importgen client/index.html && tsc -p client` in the root script), and the sub-package's own build script gets the same importgen check the root does.
|
|
752
|
+
- **A plain tsconfig sub-directory** — a service worker in `Sw/` with its own `tsconfig.json` (`lib: ["WebWorker"]`, its own `outDir`) that the root `tsconfig.json` lists under `exclude`, so a bare `"build": "tsc"` compiles everything *except* the service worker and the stale `sw2.js` ships. It has no build of its own, so the root runs `tsc -p Sw`.
|
|
739
753
|
|
|
740
|
-
|
|
754
|
+
As with import maps, these have historically been built only by a second `.vscode/tasks.json` watcher, which runs on folder open and nowhere else.
|
|
741
755
|
|
|
742
756
|
Before building, `npmglobalize` looks for sub-projects, in this order:
|
|
743
757
|
|
|
744
|
-
1. `.vscode/tasks.json` has a task that runs `tsc` with `"options": { "cwd": "${workspaceFolder}/
|
|
745
|
-
2. An immediate sub-directory containing its own `tsconfig.json`. Build output and vendored trees (`node_modules`, `prev`, `dist`, `built`, `out`, `wwwroot`, `coverage`, `temp`, `preflight`, dot-directories) are never scanned.
|
|
758
|
+
1. `.vscode/tasks.json` has a task that runs `tsc` or `importgen` with `"options": { "cwd": "${workspaceFolder}/client" }`, or runs `tsc` with `-p`/`--project` naming a sub-directory (authoritative — it's how the project is actually built today; the task's `label` is quoted back in the prompt).
|
|
759
|
+
2. An immediate sub-directory containing its own `package.json` build script or `tsconfig.json`. Build output and vendored trees (`node_modules`, `prev`, `dist`, `built`, `out`, `wwwroot`, `coverage`, `temp`, `preflight`, dot-directories) are never scanned.
|
|
746
760
|
|
|
747
761
|
Nothing is detected when the root `tsconfig.json` uses project `references` — that build graph belongs to `tsc -b` and isn't second-guessed.
|
|
748
762
|
|
|
749
|
-
When the `build` script is `tsc`-driven and doesn't already
|
|
763
|
+
When the `build` script is `tsc`-driven and doesn't already build a detected sub-project, you're prompted to append it — `"build": "tsc"` → `"build": "tsc && npm run build --prefix client"` or `"build": "tsc && tsc -p Sw"`, or for an importgen project `"build": "importgen default.htm && tsc && tsc -p Sw"`. Already-wired scripts are recognized in any of their spellings (`tsc -p Sw`, `tsc --project ./Sw/tsconfig.json`, `cd Sw && tsc`, `npm run build --prefix client`, `npm -C client run build`), and the directory is emitted with its real on-disk casing so the script still works on WSL and CI. Decline and `"subProjects": false` is written to `.globalize.json5`, suppressing the prompt for good; a `"subProjects": ["Sw"]` array pins the list instead of detecting it.
|
|
750
764
|
|
|
751
|
-
Sub-projects the build actually
|
|
765
|
+
Sub-projects the build actually runs are also folded into the freshness check — the package rebuilds when a sub-project's sources are newer than its output (for a sub-package, also when its import map is older than its `package.json`). A sub-project that emits *outside* its own directory (`"outDir": ".."`, the usual service-worker case) always reports stale: comparing its sources against the whole package would prove nothing.
|
|
752
766
|
|
|
753
767
|
#### TypeScript 6 `types` auto-fix
|
|
754
768
|
|
package/cli.js
CHANGED
|
@@ -60,7 +60,9 @@ Dependency Options:
|
|
|
60
60
|
to date (freshness check compares .ts/tsconfig
|
|
61
61
|
mtimes against emitted .js/.d.ts)
|
|
62
62
|
-force-publish Republish dependencies even if version exists
|
|
63
|
-
-fix
|
|
63
|
+
-fix, -no-fix Run npm audit fix before the pre-release commit (default on,
|
|
64
|
+
never --force). The audit REPORT always runs; critical/high
|
|
65
|
+
findings are listed in the Issues Summary with a fix hint.
|
|
64
66
|
-no-use-paths, -nup Declare this package standalone — file: deps shouldn't
|
|
65
67
|
be resolved from sibling checkouts (persisted as
|
|
66
68
|
usePaths:false in .globalize.json5; FYI for now)
|
|
@@ -596,7 +598,17 @@ export async function main() {
|
|
|
596
598
|
// Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
|
|
597
599
|
// whose source has changed since their last build.
|
|
598
600
|
if (!cliOptions.cleanup) {
|
|
599
|
-
|
|
601
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: a failed npm install used
|
|
602
|
+
// to be reported and then walked past — the run went on to "Build up to date" and a
|
|
603
|
+
// green "Installed locally" for a package with no node_modules. Stop here instead.
|
|
604
|
+
const modulesOk = await ensureFileDepModules(cwd, !!cliOptions.verbose);
|
|
605
|
+
if (!modulesOk && !cliOptions.force) {
|
|
606
|
+
console.error(colors.error('npm install failed — stopping before build/publish (-force continues anyway).'));
|
|
607
|
+
printBuildSummary();
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
if (!modulesOk)
|
|
611
|
+
console.log(colors.warn('Continuing with -force despite npm install failure...'));
|
|
600
612
|
const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force, forceBuild: !!cliOptions.forceBuild });
|
|
601
613
|
if (!depsOk && !cliOptions.force) {
|
|
602
614
|
printBuildSummary();
|
package/lib.d.ts
CHANGED
|
@@ -101,7 +101,8 @@ export interface GlobalizeOptions {
|
|
|
101
101
|
publicDeps?: boolean;
|
|
102
102
|
/** Force republish dependencies even if version exists on npm */
|
|
103
103
|
forcePublish?: boolean;
|
|
104
|
-
/** Run npm audit
|
|
104
|
+
/** Run `npm audit fix` before the pre-release commit (default true; never `--force`). The audit
|
|
105
|
+
* REPORT always runs regardless — this only controls the fix attempt. */
|
|
105
106
|
fix?: boolean;
|
|
106
107
|
/** Automatically fix version/tag mismatches */
|
|
107
108
|
fixTags?: boolean;
|
|
@@ -378,7 +379,10 @@ export declare function parseVersionTag(tag: string): number[] | null;
|
|
|
378
379
|
export declare function compareVersions(a: number[], b: number[]): number;
|
|
379
380
|
/** Fix version/tag mismatches */
|
|
380
381
|
export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
|
|
381
|
-
|
|
382
|
+
/** Lines from a failed install that name the actual failure: linker / compiler / msbuild
|
|
383
|
+
* errors from a native addon build, and node-gyp's own verdict. npm's "npm error " prefix is
|
|
384
|
+
* stripped so a line is recognised whether npm relayed it or node-gyp printed it directly. */
|
|
385
|
+
export declare function distillBuildErrors(combined: string): string[];
|
|
382
386
|
/** Return declared deps (dependencies + devDependencies) that don't resolve from
|
|
383
387
|
* `pkgDir`. Skips `workspace:`/`link:` specs (handled by workspace tooling /
|
|
384
388
|
* rarely used). `file:` deps are checked: npm installs them as junctions
|
|
@@ -392,7 +396,7 @@ export declare function missingDeps(pkgDir: string, pkg: any): string[];
|
|
|
392
396
|
* "fresh clone, no `node_modules/`" and "dep added but `npm install` not
|
|
393
397
|
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
394
398
|
* Cycle-safe via the shared `visited` set. */
|
|
395
|
-
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): Promise<
|
|
399
|
+
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): Promise<boolean>;
|
|
396
400
|
/** Cheap freshness check so the build cascade can skip packages whose output is
|
|
397
401
|
* already current. Returns true only when provably up to date:
|
|
398
402
|
* - outDir projects: newest source (.ts/.tsx/.mts/.cts + tsconfig*.json) must
|
|
@@ -423,22 +427,30 @@ export declare function detectImportgen(cwd: string): {
|
|
|
423
427
|
* start its watcher on folder open. Declining records `importgen: false` in
|
|
424
428
|
* .globalize.json5 so it stops asking. */
|
|
425
429
|
export declare function ensureImportgenInBuild(cwd: string): Promise<void>;
|
|
426
|
-
/**
|
|
427
|
-
|
|
428
|
-
|
|
430
|
+
/** A sub-directory the root build must build, and how. */
|
|
431
|
+
interface SubProject {
|
|
432
|
+
dir: string; /** Relative directory with its on-disk casing */
|
|
433
|
+
kind: 'package' | 'tsconfig'; /** package: delegate to its build script; tsconfig: `tsc -p` */
|
|
434
|
+
reason: string; /** The signal that found it, quoted in prompts */
|
|
435
|
+
}
|
|
436
|
+
/** Sub-directories the root build must build itself. A package can hold more
|
|
437
|
+
* than one project — a service worker compiled with `lib: WebWorker` that the
|
|
438
|
+
* root tsconfig `exclude`s, or a browser client that is a package of its own
|
|
439
|
+
* with its own build script — and the root `tsc` silently skips them.
|
|
429
440
|
* Signals, in order:
|
|
430
|
-
* 1. `.vscode/tasks.json` runs tsc with `options.cwd` or `-p`
|
|
431
|
-
* sub-dir (authoritative: it's how the developer actually
|
|
432
|
-
*
|
|
441
|
+
* 1. `.vscode/tasks.json` runs tsc or importgen with `options.cwd` or `-p`
|
|
442
|
+
* pointing at a sub-dir (authoritative: it's how the developer actually
|
|
443
|
+
* builds today),
|
|
444
|
+
* 2. an immediate sub-directory holding its own package.json build script
|
|
445
|
+
* or tsconfig.json.
|
|
433
446
|
* Returns nothing when the root tsconfig uses project `references` — `tsc -b`
|
|
434
447
|
* owns that build graph and shouldn't be second-guessed. */
|
|
435
|
-
export declare function detectSubProjects(cwd: string):
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
*
|
|
441
|
-
* what to compile. Declining records `subProjects: false` in .globalize.json5. */
|
|
448
|
+
export declare function detectSubProjects(cwd: string): SubProject[];
|
|
449
|
+
/** Append a build step for every sub-project the existing build script misses:
|
|
450
|
+
* `npm run build --prefix <dir>` for a sub-package, `tsc -p <dir>` for a plain
|
|
451
|
+
* tsconfig. Only touches tsc-driven builds — a bundler-driven `build` has its
|
|
452
|
+
* own idea of what to compile. Declining records `subProjects: false` in
|
|
453
|
+
* .globalize.json5. */
|
|
442
454
|
export declare function ensureSubProjectsInBuild(cwd: string): Promise<void>;
|
|
443
455
|
/** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
|
|
444
456
|
* TypeScript project lacks a build script, run `npm run build`, record
|
|
@@ -499,7 +511,7 @@ export declare function fixTs7Deprecations(cwd: string): {
|
|
|
499
511
|
export declare function ensureWorkspaceDepModules(rootDir: string, members: Array<{
|
|
500
512
|
dir: string;
|
|
501
513
|
pkg: any;
|
|
502
|
-
}>, verbose?: boolean): Promise<
|
|
514
|
+
}>, verbose?: boolean): Promise<boolean>;
|
|
503
515
|
/** Extract the lines a human needs from a failed npm command's output: the
|
|
504
516
|
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
505
517
|
* deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
|
|
@@ -622,13 +634,48 @@ export declare function promptChoice(message: string, choices: string[]): Promis
|
|
|
622
634
|
export declare function adoptExistingRemote(cwd: string, repoUrl: string, defaultBranch: string, dryRun: boolean): Promise<boolean>;
|
|
623
635
|
/** Initialize git repository */
|
|
624
636
|
export declare function initGit(cwd: string, visibility: 'private' | 'public', dryRun: boolean, allowTs?: boolean): Promise<boolean>;
|
|
625
|
-
/**
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
637
|
+
/** Vulnerability severity as reported by `npm audit --json` */
|
|
638
|
+
export type AuditSeverity = 'critical' | 'high' | 'moderate' | 'low' | 'info';
|
|
639
|
+
/** One vulnerable package from `npm audit --json` */
|
|
640
|
+
export interface AuditFinding {
|
|
641
|
+
/** Package name */
|
|
642
|
+
name: string;
|
|
643
|
+
severity: AuditSeverity;
|
|
644
|
+
/** Vulnerable version range, e.g. "<7.5.4" */
|
|
645
|
+
range: string;
|
|
646
|
+
/** Declared directly in this package.json (vs. pulled in transitively) */
|
|
647
|
+
isDirect: boolean;
|
|
648
|
+
/** Advisory titles + URLs. Empty when the package is vulnerable only because of a dep it uses. */
|
|
649
|
+
advisories: {
|
|
650
|
+
title: string;
|
|
651
|
+
url: string;
|
|
652
|
+
}[];
|
|
653
|
+
/** Vulnerable deps this package is affected THROUGH (npm's string `via` entries) */
|
|
654
|
+
via: string[];
|
|
655
|
+
/** Packages that are affected BECAUSE of this one (npm's `effects`) */
|
|
656
|
+
effects: string[];
|
|
657
|
+
/** 'plain' = `npm audit fix` clears it; 'major' = needs `--force` (breaking); 'none' = no fix published */
|
|
658
|
+
fix: 'plain' | 'major' | 'none';
|
|
659
|
+
/** The top-level package@version the fix would install (npm names it when the fix is an object) */
|
|
660
|
+
fixTarget: string;
|
|
661
|
+
}
|
|
662
|
+
/** Outcome of an `npm audit --json` run */
|
|
663
|
+
export interface AuditResult {
|
|
664
|
+
/** false when npm audit itself could not run (no lockfile, registry down...) — see error */
|
|
665
|
+
ran: boolean;
|
|
666
|
+
error: string;
|
|
667
|
+
counts: Record<AuditSeverity, number> & {
|
|
668
|
+
total: number;
|
|
669
|
+
};
|
|
670
|
+
findings: AuditFinding[];
|
|
671
|
+
}
|
|
672
|
+
/** Run `npm audit fix` (never `--force`: that installs semver-major bumps and can break the
|
|
673
|
+
* build; the summary tells the user when that is what it would take). Silent on purpose —
|
|
674
|
+
* the report that follows shows what is left. */
|
|
675
|
+
export declare function runNpmAuditFix(cwd: string): Promise<void>;
|
|
676
|
+
/** Run `npm audit --json` (read-only) and parse it into an AuditResult. Never throws; a run
|
|
677
|
+
* that could not happen comes back with ran=false and the reason in error. */
|
|
678
|
+
export declare function runNpmAudit(cwd: string): AuditResult;
|
|
632
679
|
/** Get the version of npmglobalize itself */
|
|
633
680
|
export declare function getToolVersion(): string;
|
|
634
681
|
export declare function globalize(cwd: string, options?: GlobalizeOptions, configOptions?: Partial<GlobalizeOptions>): Promise<boolean>;
|
package/lib.js
CHANGED
|
@@ -29,7 +29,6 @@ function spawnSafe(cmd, args, options = {}) {
|
|
|
29
29
|
return spawnSync(cmd, args, opts);
|
|
30
30
|
}
|
|
31
31
|
import readline from 'readline';
|
|
32
|
-
import { styleText } from 'util';
|
|
33
32
|
import libversion from 'libnpmversion';
|
|
34
33
|
import JSON5 from 'json5';
|
|
35
34
|
import { fileURLToPath } from 'url';
|
|
@@ -332,7 +331,7 @@ const BOOL_COMMENTS = {
|
|
|
332
331
|
force: { on: 'Continue despite git errors', off: 'Stop on git errors' },
|
|
333
332
|
quiet: { on: 'Suppress npm warnings', off: 'Show npm warnings' },
|
|
334
333
|
verbose: { on: 'Show detailed output', off: 'Normal output detail' },
|
|
335
|
-
fix: { on: 'Auto-run npm audit fix', off: 'Do NOT run npm audit fix' },
|
|
334
|
+
fix: { on: 'Auto-run npm audit fix (default)', off: 'Do NOT run npm audit fix (audit report still runs)' },
|
|
336
335
|
local: { on: 'Local install only (skip transform/publish)', off: 'Normal transform/publish flow' },
|
|
337
336
|
noPublish: { on: 'Transform but don\'t publish', off: 'Publish normally' },
|
|
338
337
|
freeze: { on: 'Freeze node_modules (replace symlinks with real copies)', off: 'Leave node_modules symlinks as-is' },
|
|
@@ -345,7 +344,7 @@ const VALUE_COMMENTS = {
|
|
|
345
344
|
gitVisibility: 'private (default) or public',
|
|
346
345
|
npmVisibility: 'private (default) or public',
|
|
347
346
|
bump: 'patch (default), minor, or major',
|
|
348
|
-
subProjects: 'Sub-dirs with their own tsconfig that build must
|
|
347
|
+
subProjects: 'Sub-dirs with their own build script or tsconfig that build must run (false = never ask)'
|
|
349
348
|
};
|
|
350
349
|
/** Write .globalize.json5 config file */
|
|
351
350
|
export function writeConfig(dir, config, explicitKeys) {
|
|
@@ -444,14 +443,14 @@ export function writeConfig(dir, config, explicitKeys) {
|
|
|
444
443
|
['"verbose": false', 'true = show detailed output'],
|
|
445
444
|
['"gitVisibility": "private"', 'Git repo: private or public'],
|
|
446
445
|
['"npmVisibility": "private"', 'npm package: private or public'],
|
|
447
|
-
['"fix":
|
|
446
|
+
['"fix": true', 'true = auto-run npm audit fix (never --force); false = report only'],
|
|
448
447
|
['"local": false', 'true = local install only (skip transform/publish); clear with -global'],
|
|
449
448
|
['"noPublish": false', 'true = transform but don\'t publish'],
|
|
450
449
|
['"freeze": false', 'true = freeze node_modules (replace symlinks with real copies)'],
|
|
451
450
|
['"usePaths": true', 'true = resolve file: deps from siblings; false = use latest npm version (standalone)'],
|
|
452
451
|
['"allowTs": false', 'true = include .ts source in npm tarball (auto-true for noEmit projects)'],
|
|
453
452
|
['"importgen": auto', 'Detected from .vscode/tasks.json / import map in HTML; false = never ask'],
|
|
454
|
-
['"subProjects": auto', 'Sub-dirs with their own tsconfig (e.g. ["Sw"]); detected from .vscode/tasks.json; false = never ask']
|
|
453
|
+
['"subProjects": auto', 'Sub-dirs with their own build script or tsconfig (e.g. ["client", "Sw"]); detected from .vscode/tasks.json; false = never ask']
|
|
455
454
|
];
|
|
456
455
|
const refWidth = Math.max(...reference.map(([decl]) => decl.length));
|
|
457
456
|
for (const [decl, note] of reference) {
|
|
@@ -2069,73 +2068,67 @@ export function fixVersionTagMismatch(cwd, pkg, verbose = false) {
|
|
|
2069
2068
|
function sleepSync(ms) {
|
|
2070
2069
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2071
2070
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
const
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
}
|
|
2125
|
-
/** Run `npm install` in `dir`; on failure, offer to delete `node_modules/`
|
|
2126
|
-
* and `package-lock.json` and retry once — the standard fix for arborist's
|
|
2127
|
-
* null-`package` crash on stale lockfile / junction state — before falling
|
|
2128
|
-
* back to the pnpm suggestion banner. Returns true if install succeeded.
|
|
2129
|
-
* `rmSync` unlinks junctions without descending, so file: sibling targets
|
|
2130
|
-
* are never touched. */
|
|
2071
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: the pnpm-eval banner that used
|
|
2072
|
+
// to live here ("npm install FAILED -- consider trying pnpm", plus a 60-second pause) is
|
|
2073
|
+
// gone. pnpm was evaluated and judged not feasible, so the suggestion was noise that
|
|
2074
|
+
// delayed the run and hid the real error. A failed install is now reported by
|
|
2075
|
+
// reportInstallFailure (the actual compiler/linker/npm lines, plus a log file) and stops
|
|
2076
|
+
// the run — see ensureFileDepModules and its callers.
|
|
2077
|
+
/** Lines from a failed install that name the actual failure: linker / compiler / msbuild
|
|
2078
|
+
* errors from a native addon build, and node-gyp's own verdict. npm's "npm error " prefix is
|
|
2079
|
+
* stripped so a line is recognised whether npm relayed it or node-gyp printed it directly. */
|
|
2080
|
+
export function distillBuildErrors(combined) {
|
|
2081
|
+
const out = [];
|
|
2082
|
+
const seen = new Set();
|
|
2083
|
+
for (const raw of combined.split('\n')) {
|
|
2084
|
+
const line = raw.replace(/^npm (?:error|ERR!)\s?/, '').trimEnd();
|
|
2085
|
+
if (!/fatal error|\berror (?:LNK|C|MSB|TS)\d+|gyp ERR! (?:build|configure|rebuild) error/.test(line))
|
|
2086
|
+
continue;
|
|
2087
|
+
if (seen.has(line))
|
|
2088
|
+
continue;
|
|
2089
|
+
seen.add(line);
|
|
2090
|
+
out.push(line);
|
|
2091
|
+
}
|
|
2092
|
+
return out.slice(0, 12);
|
|
2093
|
+
}
|
|
2094
|
+
/** Print why an `npm install` failed and keep the whole output in a log file. Native-build
|
|
2095
|
+
* errors (LNK1117 and friends) come first — they are the cause; npm's own "command failed"
|
|
2096
|
+
* lines only say that something did. Was: the first 5 lines of stderr, which for a node-gyp
|
|
2097
|
+
* failure are "npm error code 1 / path / command failed / command ..." and never the error. */
|
|
2098
|
+
function reportInstallFailure(dir, r, attempt) {
|
|
2099
|
+
const combined = [r.output, r.stderr].filter(Boolean).join('\n');
|
|
2100
|
+
const logFile = saveNpmLog(`install-${path.basename(dir)}`, combined);
|
|
2101
|
+
const buildErrors = distillBuildErrors(combined);
|
|
2102
|
+
const npmErrors = distillNpmErrors(combined).filter(l => !buildErrors.includes(l));
|
|
2103
|
+
console.error(colors.red(` ✗ npm install failed in ${dir}${attempt}`));
|
|
2104
|
+
for (const l of buildErrors)
|
|
2105
|
+
console.error(colors.red(` ${l}`));
|
|
2106
|
+
for (const l of npmErrors.slice(0, buildErrors.length > 0 ? 4 : 8))
|
|
2107
|
+
console.error(colors.dim(` ${l}`));
|
|
2108
|
+
if (logFile)
|
|
2109
|
+
console.error(colors.dim(` full output: ${logFile}`));
|
|
2110
|
+
const headline = buildErrors.find(l => /fatal error|\berror /.test(l))
|
|
2111
|
+
|| buildErrors[0]
|
|
2112
|
+
|| npmErrors.find(l => !/^(code \d+|path |command )/.test(l))
|
|
2113
|
+
|| 'see log';
|
|
2114
|
+
return { isBuildError: buildErrors.length > 0, headline, logFile };
|
|
2115
|
+
}
|
|
2116
|
+
/** Run `npm install` in `dir`. On failure, offer to delete `node_modules/` and
|
|
2117
|
+
* `package-lock.json` and retry once — the standard fix for arborist's null-`package`
|
|
2118
|
+
* crash on stale lockfile / junction state. Not offered when the output holds a
|
|
2119
|
+
* compiler / linker error: that is a broken native build, not a broken tree, and the
|
|
2120
|
+
* reinstall would spend a minute reaching the same line. Returns true if install
|
|
2121
|
+
* succeeded; a failure is recorded for the Issues Summary. `rmSync` unlinks junctions
|
|
2122
|
+
* without descending, so file: sibling targets are never touched. */
|
|
2131
2123
|
async function npmInstallWithCleanRetry(dir, verbose, context) {
|
|
2132
2124
|
let r = await runCommandAsync('npm', ['install'], { cwd: dir, silent: !verbose });
|
|
2133
2125
|
if (r.success)
|
|
2134
2126
|
return true;
|
|
2135
|
-
|
|
2136
|
-
if (
|
|
2137
|
-
console.error(colors.dim(
|
|
2138
|
-
|
|
2127
|
+
let failure = reportInstallFailure(dir, r, '');
|
|
2128
|
+
if (failure.isBuildError) {
|
|
2129
|
+
console.error(colors.dim(' (compiler/linker error — a clean reinstall would hit the same line, so not offering one)'));
|
|
2130
|
+
}
|
|
2131
|
+
else if (await confirm(` Delete node_modules/ and package-lock.json in ${dir} and retry npm install?`, true)) {
|
|
2139
2132
|
try {
|
|
2140
2133
|
fs.rmSync(path.join(dir, 'node_modules'), { recursive: true, force: true });
|
|
2141
2134
|
}
|
|
@@ -2150,11 +2143,9 @@ async function npmInstallWithCleanRetry(dir, verbose, context) {
|
|
|
2150
2143
|
console.log(colors.green(` ✓ clean reinstall succeeded in ${dir}`));
|
|
2151
2144
|
return true;
|
|
2152
2145
|
}
|
|
2153
|
-
|
|
2154
|
-
if (r.stderr)
|
|
2155
|
-
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
2146
|
+
failure = reportInstallFailure(dir, r, ' after clean reinstall');
|
|
2156
2147
|
}
|
|
2157
|
-
|
|
2148
|
+
recordBuildIssue(path.basename(dir), 'error', `npm install failed (${context}): ${failure.headline}${failure.logFile ? ` — log: ${failure.logFile}` : ''}`);
|
|
2158
2149
|
return false;
|
|
2159
2150
|
}
|
|
2160
2151
|
/** Wait for a package version to appear on the npm registry.
|
|
@@ -2308,17 +2299,22 @@ function formatMissingReason(missing) {
|
|
|
2308
2299
|
* "fresh clone, no `node_modules/`" and "dep added but `npm install` not
|
|
2309
2300
|
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
2310
2301
|
* Cycle-safe via the shared `visited` set. */
|
|
2302
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction ("if the install failed why
|
|
2303
|
+
// continue"): returns false when any npm install in the tree failed. Every dep is still
|
|
2304
|
+
// attempted so one run reports all failures; the CALLER stops before building/publishing
|
|
2305
|
+
// on false (unless -force).
|
|
2311
2306
|
export async function ensureFileDepModules(cwd, verbose = false, visited = new Set()) {
|
|
2312
2307
|
const abs = path.resolve(cwd);
|
|
2313
2308
|
if (visited.has(abs))
|
|
2314
|
-
return;
|
|
2309
|
+
return true;
|
|
2315
2310
|
visited.add(abs);
|
|
2311
|
+
let ok = true;
|
|
2316
2312
|
let pkg;
|
|
2317
2313
|
try {
|
|
2318
2314
|
pkg = readPackageJson(cwd);
|
|
2319
2315
|
}
|
|
2320
2316
|
catch {
|
|
2321
|
-
return;
|
|
2317
|
+
return true; // no package.json here: nothing to install, nothing failed
|
|
2322
2318
|
}
|
|
2323
2319
|
const cwdMissing = missingDeps(abs, pkg);
|
|
2324
2320
|
if (cwdMissing.length > 0) {
|
|
@@ -2335,7 +2331,8 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
2335
2331
|
catch { /* best-effort */ }
|
|
2336
2332
|
}
|
|
2337
2333
|
console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs} (${formatMissingReason(cwdMissing)})`));
|
|
2338
|
-
await npmInstallWithCleanRetry(abs, verbose, `ensureFileDepModules (cwd): ${abs}`)
|
|
2334
|
+
if (!await npmInstallWithCleanRetry(abs, verbose, `ensureFileDepModules (cwd): ${abs}`))
|
|
2335
|
+
ok = false;
|
|
2339
2336
|
}
|
|
2340
2337
|
for (const key of ['dependencies', 'devDependencies']) {
|
|
2341
2338
|
const deps = pkg?.[key];
|
|
@@ -2376,11 +2373,14 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
2376
2373
|
catch { /* best-effort */ }
|
|
2377
2374
|
}
|
|
2378
2375
|
console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target}) (${formatMissingReason(targetMissing)})`));
|
|
2379
|
-
await npmInstallWithCleanRetry(target, verbose, `ensureFileDepModules (file: dep ${name}): ${target}`)
|
|
2376
|
+
if (!await npmInstallWithCleanRetry(target, verbose, `ensureFileDepModules (file: dep ${name}): ${target}`))
|
|
2377
|
+
ok = false;
|
|
2380
2378
|
}
|
|
2381
|
-
await ensureFileDepModules(target, verbose, visited)
|
|
2379
|
+
if (!await ensureFileDepModules(target, verbose, visited))
|
|
2380
|
+
ok = false;
|
|
2382
2381
|
}
|
|
2383
2382
|
}
|
|
2383
|
+
return ok;
|
|
2384
2384
|
}
|
|
2385
2385
|
/** Cached major version of the global `tsc`. null = not yet probed,
|
|
2386
2386
|
* 0 = probe failed / tsc not found. TypeScript 6 stopped auto-including
|
|
@@ -3019,7 +3019,27 @@ export function detectImportgen(cwd) {
|
|
|
3019
3019
|
if (!scriptRunsImportgen(command))
|
|
3020
3020
|
continue;
|
|
3021
3021
|
const named = args.find(a => /\.html?$/i.test(a));
|
|
3022
|
-
|
|
3022
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction.
|
|
3023
|
+
// Honor the task's `options.cwd`. itemw runs importgen with cwd
|
|
3024
|
+
// `${workspaceFolder}/client` because its HTML is client/index.html;
|
|
3025
|
+
// ignoring that made us search the root, find nothing, and wire a bare
|
|
3026
|
+
// `importgen` that failed with "No HTML file found".
|
|
3027
|
+
// - cwd is a sub-PACKAGE (has its own package.json): its own build
|
|
3028
|
+
// script owns the import map, and the root build delegates to it
|
|
3029
|
+
// (see detectSubProjects). Not a root importgen project.
|
|
3030
|
+
// - cwd is a plain sub-directory (www/ with just the HTML): run
|
|
3031
|
+
// `importgen www/index.html` from the root — importgen resolves
|
|
3032
|
+
// positional paths against cwd and finds package.json by walking up.
|
|
3033
|
+
const taskCwd = typeof task?.options?.cwd === 'string' ? task.options.cwd : '';
|
|
3034
|
+
let subdir = normalizeProjectPath(taskCwd);
|
|
3035
|
+
if (path.isAbsolute(subdir))
|
|
3036
|
+
subdir = ''; // some other machine's absolute path — nothing we can build from
|
|
3037
|
+
if (subdir && fs.existsSync(path.join(cwd, subdir, 'package.json')))
|
|
3038
|
+
return null;
|
|
3039
|
+
const searchDir = subdir ? path.join(cwd, subdir) : cwd;
|
|
3040
|
+
const found = findImportgenHtml(searchDir, named);
|
|
3041
|
+
const htmlFile = found && subdir ? `${subdir}/${found}` : found;
|
|
3042
|
+
return { htmlFile, reason: '.vscode/tasks.json runs importgen' };
|
|
3023
3043
|
}
|
|
3024
3044
|
}
|
|
3025
3045
|
catch {
|
|
@@ -3178,13 +3198,34 @@ function resolveOnDiskCase(base, rel) {
|
|
|
3178
3198
|
}
|
|
3179
3199
|
return out.length ? out.join('/') : null;
|
|
3180
3200
|
}
|
|
3181
|
-
/**
|
|
3182
|
-
*
|
|
3201
|
+
/** Classify `rel` under `cwd`: a package with its own build script, a plain
|
|
3202
|
+
* tsconfig sub-project, or null when it's neither. */
|
|
3203
|
+
function subProjectKind(cwd, rel) {
|
|
3204
|
+
try {
|
|
3205
|
+
const pkg = readPackageJson(path.join(cwd, rel));
|
|
3206
|
+
if (typeof pkg?.scripts?.build === 'string' && pkg.scripts.build.trim())
|
|
3207
|
+
return 'package';
|
|
3208
|
+
}
|
|
3209
|
+
catch {
|
|
3210
|
+
// No package.json there — fall through to the tsconfig test
|
|
3211
|
+
}
|
|
3212
|
+
return fs.existsSync(path.join(cwd, rel, 'tsconfig.json')) ? 'tsconfig' : null;
|
|
3213
|
+
}
|
|
3214
|
+
/** The command the root build script runs for a sub-project. */
|
|
3215
|
+
function subProjectBuildCommand(s) {
|
|
3216
|
+
return s.kind === 'package'
|
|
3217
|
+
? `npm run build --prefix ${quoteScriptPath(s.dir)}`
|
|
3218
|
+
: `tsc -p ${quoteScriptPath(s.dir)}`;
|
|
3219
|
+
}
|
|
3220
|
+
/** True when `script` already builds `dir` — `tsc -p Sw`,
|
|
3221
|
+
* `tsc --project ./Sw/tsconfig.json`, `cd Sw && tsc`, or for a package
|
|
3222
|
+
* `npm run build --prefix client` / `npm -C client run build`. */
|
|
3183
3223
|
function scriptBuildsSubProject(script, dir) {
|
|
3184
3224
|
const target = normalizeProjectPath(dir).toLowerCase();
|
|
3185
3225
|
if (!target)
|
|
3186
3226
|
return false;
|
|
3187
3227
|
for (const re of [/(?:^|\s)(?:-p|--project)[\s=]+("[^"]+"|'[^']+'|\S+)/g,
|
|
3228
|
+
/(?:^|\s)(?:-C|--prefix)[\s=]+("[^"]+"|'[^']+'|\S+)/g,
|
|
3188
3229
|
/(?:^|[\s&|(])cd\s+("[^"]+"|'[^']+'|\S+)/g]) {
|
|
3189
3230
|
for (const m of script.matchAll(re)) {
|
|
3190
3231
|
if (normalizeProjectPath(m[1]).toLowerCase() === target)
|
|
@@ -3193,13 +3234,16 @@ function scriptBuildsSubProject(script, dir) {
|
|
|
3193
3234
|
}
|
|
3194
3235
|
return false;
|
|
3195
3236
|
}
|
|
3196
|
-
/** Sub-directories
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3237
|
+
/** Sub-directories the root build must build itself. A package can hold more
|
|
3238
|
+
* than one project — a service worker compiled with `lib: WebWorker` that the
|
|
3239
|
+
* root tsconfig `exclude`s, or a browser client that is a package of its own
|
|
3240
|
+
* with its own build script — and the root `tsc` silently skips them.
|
|
3199
3241
|
* Signals, in order:
|
|
3200
|
-
* 1. `.vscode/tasks.json` runs tsc with `options.cwd` or `-p`
|
|
3201
|
-
* sub-dir (authoritative: it's how the developer actually
|
|
3202
|
-
*
|
|
3242
|
+
* 1. `.vscode/tasks.json` runs tsc or importgen with `options.cwd` or `-p`
|
|
3243
|
+
* pointing at a sub-dir (authoritative: it's how the developer actually
|
|
3244
|
+
* builds today),
|
|
3245
|
+
* 2. an immediate sub-directory holding its own package.json build script
|
|
3246
|
+
* or tsconfig.json.
|
|
3203
3247
|
* Returns nothing when the root tsconfig uses project `references` — `tsc -b`
|
|
3204
3248
|
* owns that build graph and shouldn't be second-guessed. */
|
|
3205
3249
|
export function detectSubProjects(cwd) {
|
|
@@ -3211,7 +3255,7 @@ export function detectSubProjects(cwd) {
|
|
|
3211
3255
|
catch {
|
|
3212
3256
|
// No root tsconfig (or unparsable) — nested ones still count
|
|
3213
3257
|
}
|
|
3214
|
-
const found = new Map(); // on-disk relative dir →
|
|
3258
|
+
const found = new Map(); // on-disk relative dir → entry
|
|
3215
3259
|
const add = (raw, reason) => {
|
|
3216
3260
|
const rel = normalizeProjectPath(raw);
|
|
3217
3261
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
|
|
@@ -3221,10 +3265,11 @@ export function detectSubProjects(cwd) {
|
|
|
3221
3265
|
const onDisk = resolveOnDiskCase(cwd, rel);
|
|
3222
3266
|
if (!onDisk)
|
|
3223
3267
|
return;
|
|
3224
|
-
|
|
3268
|
+
const kind = subProjectKind(cwd, onDisk);
|
|
3269
|
+
if (!kind)
|
|
3225
3270
|
return;
|
|
3226
3271
|
if (!found.has(onDisk))
|
|
3227
|
-
found.set(onDisk, reason);
|
|
3272
|
+
found.set(onDisk, { dir: onDisk, kind, reason });
|
|
3228
3273
|
};
|
|
3229
3274
|
// 1. .vscode/tasks.json — JSON5 because VS Code allows comments/trailing commas
|
|
3230
3275
|
try {
|
|
@@ -3232,37 +3277,39 @@ export function detectSubProjects(cwd) {
|
|
|
3232
3277
|
for (const task of Array.isArray(tasks?.tasks) ? tasks.tasks : []) {
|
|
3233
3278
|
const command = typeof task?.command === 'string' ? task.command : '';
|
|
3234
3279
|
const args = Array.isArray(task?.args) ? task.args.filter((a) => typeof a === 'string') : [];
|
|
3235
|
-
|
|
3280
|
+
const line = [command, ...args].join(' ');
|
|
3281
|
+
const tool = scriptRunsTsc(line) ? 'tsc' : scriptRunsImportgen(line) ? 'importgen' : null;
|
|
3282
|
+
if (!tool)
|
|
3236
3283
|
continue;
|
|
3237
|
-
const label = typeof task?.label === 'string' ? task.label :
|
|
3284
|
+
const label = typeof task?.label === 'string' ? task.label : `a ${tool} task`;
|
|
3238
3285
|
const projIdx = args.findIndex(a => a === '-p' || a === '--project');
|
|
3239
|
-
if (projIdx !== -1 && args[projIdx + 1]) {
|
|
3286
|
+
if (tool === 'tsc' && projIdx !== -1 && args[projIdx + 1]) {
|
|
3240
3287
|
add(args[projIdx + 1], `.vscode/tasks.json "${label}" builds it`);
|
|
3241
3288
|
}
|
|
3242
3289
|
else if (typeof task?.options?.cwd === 'string') {
|
|
3243
|
-
add(task.options.cwd, `.vscode/tasks.json "${label}" runs
|
|
3290
|
+
add(task.options.cwd, `.vscode/tasks.json "${label}" runs ${tool} there`);
|
|
3244
3291
|
}
|
|
3245
3292
|
}
|
|
3246
3293
|
}
|
|
3247
3294
|
catch {
|
|
3248
3295
|
// No tasks.json, or unparsable — fall through to the filesystem scan
|
|
3249
3296
|
}
|
|
3250
|
-
// 2. Immediate sub-directories with their own tsconfig.json
|
|
3297
|
+
// 2. Immediate sub-directories with their own build script or tsconfig.json
|
|
3251
3298
|
try {
|
|
3252
3299
|
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
3253
3300
|
if (!e.isDirectory() || e.isSymbolicLink())
|
|
3254
3301
|
continue;
|
|
3255
3302
|
if (e.name.startsWith('.') || SUBPROJECT_SKIP_DIRS.has(e.name.toLowerCase()))
|
|
3256
3303
|
continue;
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3304
|
+
const kind = subProjectKind(cwd, e.name);
|
|
3305
|
+
if (kind)
|
|
3306
|
+
add(e.name, kind === 'package' ? `${e.name}/package.json has a build script` : `${e.name}/tsconfig.json`);
|
|
3260
3307
|
}
|
|
3261
3308
|
}
|
|
3262
3309
|
catch {
|
|
3263
3310
|
// Unreadable directory — nothing more to detect
|
|
3264
3311
|
}
|
|
3265
|
-
return [...found
|
|
3312
|
+
return [...found.values()];
|
|
3266
3313
|
}
|
|
3267
3314
|
/** The sub-projects to wire, honoring `.globalize.json5`: `false` disables the
|
|
3268
3315
|
* whole check, an array pins the list instead of detecting it. */
|
|
@@ -3270,9 +3317,14 @@ function configuredSubProjects(cwd, config) {
|
|
|
3270
3317
|
if (config.subProjects === false)
|
|
3271
3318
|
return [];
|
|
3272
3319
|
if (Array.isArray(config.subProjects)) {
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3320
|
+
const listed = [];
|
|
3321
|
+
for (const d of config.subProjects) {
|
|
3322
|
+
const dir = resolveOnDiskCase(cwd, normalizeProjectPath(d)) || d;
|
|
3323
|
+
const kind = subProjectKind(cwd, dir);
|
|
3324
|
+
if (kind)
|
|
3325
|
+
listed.push({ dir, kind, reason: 'listed in .globalize.json5' });
|
|
3326
|
+
}
|
|
3327
|
+
return listed;
|
|
3276
3328
|
}
|
|
3277
3329
|
return detectSubProjects(cwd);
|
|
3278
3330
|
}
|
|
@@ -3280,9 +3332,11 @@ function configuredSubProjects(cwd, config) {
|
|
|
3280
3332
|
function quoteScriptPath(p) {
|
|
3281
3333
|
return /[\s&|<>]/.test(p) ? `"${p}"` : p;
|
|
3282
3334
|
}
|
|
3283
|
-
/** Append
|
|
3284
|
-
*
|
|
3285
|
-
*
|
|
3335
|
+
/** Append a build step for every sub-project the existing build script misses:
|
|
3336
|
+
* `npm run build --prefix <dir>` for a sub-package, `tsc -p <dir>` for a plain
|
|
3337
|
+
* tsconfig. Only touches tsc-driven builds — a bundler-driven `build` has its
|
|
3338
|
+
* own idea of what to compile. Declining records `subProjects: false` in
|
|
3339
|
+
* .globalize.json5. */
|
|
3286
3340
|
export async function ensureSubProjectsInBuild(cwd) {
|
|
3287
3341
|
const config = readConfig(cwd);
|
|
3288
3342
|
if (config.subProjects === false)
|
|
@@ -3297,12 +3351,18 @@ export async function ensureSubProjectsInBuild(cwd) {
|
|
|
3297
3351
|
const buildScript = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build.trim() : '';
|
|
3298
3352
|
if (!buildScript || !scriptRunsTsc(buildScript))
|
|
3299
3353
|
return;
|
|
3300
|
-
const
|
|
3301
|
-
|
|
3354
|
+
const subs = configuredSubProjects(cwd, config);
|
|
3355
|
+
// A sub-package owns its own import map: give its build script the same
|
|
3356
|
+
// importgen check the root gets, so delegating to it is enough.
|
|
3357
|
+
for (const s of subs) {
|
|
3358
|
+
if (s.kind === 'package')
|
|
3359
|
+
await ensureImportgenInBuild(path.join(cwd, s.dir));
|
|
3360
|
+
}
|
|
3361
|
+
const missing = subs.filter(s => !scriptBuildsSubProject(buildScript, s.dir));
|
|
3302
3362
|
if (missing.length === 0)
|
|
3303
3363
|
return;
|
|
3304
3364
|
const name = pkg.name || path.basename(cwd);
|
|
3305
|
-
const newScript = [buildScript, ...missing.map(
|
|
3365
|
+
const newScript = [buildScript, ...missing.map(subProjectBuildCommand)].join(' && ');
|
|
3306
3366
|
// Default to yes only when tasks.json says the developer already builds these
|
|
3307
3367
|
// — a bare nested tsconfig (tests, examples) is a weaker signal, and this
|
|
3308
3368
|
// prompt rewrites a build script that currently works.
|
|
@@ -3318,7 +3378,7 @@ export async function ensureSubProjectsInBuild(cwd) {
|
|
|
3318
3378
|
}
|
|
3319
3379
|
pkg.scripts.build = newScript;
|
|
3320
3380
|
writePackageJson(cwd, pkg);
|
|
3321
|
-
console.log(colors.green(`✓ Added ${missing.map(
|
|
3381
|
+
console.log(colors.green(`✓ Added ${missing.map(subProjectBuildCommand).join(' && ')} to ${name}'s build script`));
|
|
3322
3382
|
}
|
|
3323
3383
|
/** Freshness for the sub-projects the build script actually compiles. A
|
|
3324
3384
|
* sub-project that emits outside its own directory (a service worker with
|
|
@@ -3327,6 +3387,22 @@ export async function ensureSubProjectsInBuild(cwd) {
|
|
|
3327
3387
|
function areSubProjectsUpToDate(cwd, subs) {
|
|
3328
3388
|
for (const s of subs) {
|
|
3329
3389
|
const dir = path.join(cwd, s.dir);
|
|
3390
|
+
if (s.kind === 'package') {
|
|
3391
|
+
// Its own build script decides what it emits; judge it the way the
|
|
3392
|
+
// root is judged — sources vs output, plus the import map if it has one.
|
|
3393
|
+
if (!isBuildUpToDate(dir))
|
|
3394
|
+
return false;
|
|
3395
|
+
let subBuild = '';
|
|
3396
|
+
try {
|
|
3397
|
+
subBuild = readPackageJson(dir).scripts?.build ?? '';
|
|
3398
|
+
}
|
|
3399
|
+
catch {
|
|
3400
|
+
return false;
|
|
3401
|
+
}
|
|
3402
|
+
if (scriptRunsImportgen(subBuild) && !isImportMapUpToDate(dir))
|
|
3403
|
+
return false;
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3330
3406
|
let co;
|
|
3331
3407
|
try {
|
|
3332
3408
|
co = JSON5.parse(fs.readFileSync(path.join(dir, 'tsconfig.json'), 'utf-8')).compilerOptions || {};
|
|
@@ -3395,7 +3471,7 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3395
3471
|
// Compile the sub-projects (service worker, …) in the same breath: the
|
|
3396
3472
|
// root tsc excludes them, so a bare "tsc" would leave them unbuilt.
|
|
3397
3473
|
const subs = configuredSubProjects(cwd, readConfig(cwd));
|
|
3398
|
-
const tscParts = ['tsc', ...subs.map(
|
|
3474
|
+
const tscParts = ['tsc', ...subs.map(subProjectBuildCommand)];
|
|
3399
3475
|
const newScript = [...(existingBuild ? [existingBuild] : []), ...tscParts].join(' && ');
|
|
3400
3476
|
console.log(colors.yellow(`TypeScript project has no "build" script in ${pkg.name || cwd}`));
|
|
3401
3477
|
for (const s of subs)
|
|
@@ -3770,7 +3846,7 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
|
|
|
3770
3846
|
rootPkg = readPackageJson(root);
|
|
3771
3847
|
}
|
|
3772
3848
|
catch {
|
|
3773
|
-
return;
|
|
3849
|
+
return true;
|
|
3774
3850
|
}
|
|
3775
3851
|
const allMissing = new Set();
|
|
3776
3852
|
for (const name of missingDeps(root, rootPkg))
|
|
@@ -3780,10 +3856,10 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
|
|
|
3780
3856
|
allMissing.add(name);
|
|
3781
3857
|
}
|
|
3782
3858
|
if (allMissing.size === 0)
|
|
3783
|
-
return;
|
|
3859
|
+
return true;
|
|
3784
3860
|
const list = [...allMissing];
|
|
3785
3861
|
console.log(colors.yellow(`↻ installing workspace node_modules in ${rootPkg?.name || path.basename(root)} (${formatMissingReason(list)})`));
|
|
3786
|
-
|
|
3862
|
+
return npmInstallWithCleanRetry(root, verbose, `ensureWorkspaceDepModules: ${root}`);
|
|
3787
3863
|
}
|
|
3788
3864
|
/** Extract the lines a human needs from a failed npm command's output: the
|
|
3789
3865
|
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
@@ -6019,54 +6095,140 @@ export async function initGit(cwd, visibility, dryRun, allowTs) {
|
|
|
6019
6095
|
}
|
|
6020
6096
|
return true;
|
|
6021
6097
|
}
|
|
6022
|
-
/**
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6098
|
+
/** Run `npm audit fix` (never `--force`: that installs semver-major bumps and can break the
|
|
6099
|
+
* build; the summary tells the user when that is what it would take). Silent on purpose —
|
|
6100
|
+
* the report that follows shows what is left. */
|
|
6101
|
+
export async function runNpmAuditFix(cwd) {
|
|
6102
|
+
await runCommandAsync('npm', ['audit', 'fix'], { cwd, silent: true });
|
|
6103
|
+
}
|
|
6104
|
+
/** Classify npm's `fixAvailable` — true | false | { name, version, isSemVerMajor } */
|
|
6105
|
+
function classifyFix(fixAvailable) {
|
|
6106
|
+
if (fixAvailable === true)
|
|
6107
|
+
return { fix: 'plain', fixTarget: '' };
|
|
6108
|
+
if (fixAvailable && typeof fixAvailable === 'object') {
|
|
6109
|
+
const f = fixAvailable;
|
|
6110
|
+
const fixTarget = f.name ? `${f.name}@${f.version ?? '?'}` : '';
|
|
6111
|
+
return { fix: f.isSemVerMajor ? 'major' : 'plain', fixTarget };
|
|
6112
|
+
}
|
|
6113
|
+
return { fix: 'none', fixTarget: '' };
|
|
6114
|
+
}
|
|
6115
|
+
/** Run `npm audit --json` (read-only) and parse it into an AuditResult. Never throws; a run
|
|
6116
|
+
* that could not happen comes back with ran=false and the reason in error. */
|
|
6117
|
+
export function runNpmAudit(cwd) {
|
|
6118
|
+
const counts = { critical: 0, high: 0, moderate: 0, low: 0, info: 0, total: 0 };
|
|
6119
|
+
const result = { ran: false, error: '', counts, findings: [] };
|
|
6120
|
+
const proc = spawnSafe('npm', ['audit', '--json'], { cwd, encoding: 'utf-8', stdio: 'pipe', shell: true });
|
|
6121
|
+
// The JSON shape is npm's, not ours — narrowed field by field below, hence the any.
|
|
6122
|
+
let data;
|
|
6034
6123
|
try {
|
|
6035
|
-
|
|
6036
|
-
const m = auditData.metadata?.vulnerabilities || {};
|
|
6037
|
-
const critical = m.critical || 0;
|
|
6038
|
-
const high = m.high || 0;
|
|
6039
|
-
const moderate = m.moderate || 0;
|
|
6040
|
-
const low = m.low || 0;
|
|
6041
|
-
const total = critical + high + moderate + low + (m.info || 0);
|
|
6042
|
-
if (total > 0) {
|
|
6043
|
-
hasVulnerabilities = true;
|
|
6044
|
-
const parts = [];
|
|
6045
|
-
if (critical > 0)
|
|
6046
|
-
parts.push(colors.red(`${critical} critical`));
|
|
6047
|
-
if (high > 0)
|
|
6048
|
-
parts.push(colors.red(`${high} high`));
|
|
6049
|
-
if (moderate > 0)
|
|
6050
|
-
parts.push(colors.yellow(`${moderate} moderate`));
|
|
6051
|
-
if (low > 0)
|
|
6052
|
-
parts.push(`${low} low`);
|
|
6053
|
-
report = `${total} vulnerabilities`;
|
|
6054
|
-
// Only show if high/critical remain after fix
|
|
6055
|
-
if (critical > 0 || high > 0) {
|
|
6056
|
-
console.log(colors.red(` Audit: ${parts.join(', ')}`));
|
|
6057
|
-
}
|
|
6058
|
-
else if (verbose) {
|
|
6059
|
-
console.log(colors.dim(` Audit: ${parts.join(', ')}`));
|
|
6060
|
-
}
|
|
6061
|
-
}
|
|
6062
|
-
else {
|
|
6063
|
-
report = 'No vulnerabilities';
|
|
6064
|
-
}
|
|
6124
|
+
data = JSON.parse(proc.stdout || '');
|
|
6065
6125
|
}
|
|
6066
|
-
catch {
|
|
6067
|
-
|
|
6126
|
+
catch (error) {
|
|
6127
|
+
// Not a swallow: the failure becomes result.error, which the summary prints as "Audit: not run".
|
|
6128
|
+
const stderr = (proc.stderr || '').trim().split('\n')[0];
|
|
6129
|
+
result.error = stderr || `npm audit produced no JSON (${error.message})`;
|
|
6130
|
+
return result;
|
|
6131
|
+
}
|
|
6132
|
+
if (data?.error) {
|
|
6133
|
+
// npm reports its own failures (ENOLOCK, network...) as JSON on stdout
|
|
6134
|
+
result.error = [data.error.code, data.error.summary].filter(Boolean).join(': ');
|
|
6135
|
+
return result;
|
|
6136
|
+
}
|
|
6137
|
+
result.ran = true;
|
|
6138
|
+
const m = data.metadata?.vulnerabilities || {};
|
|
6139
|
+
for (const sev of ['critical', 'high', 'moderate', 'low', 'info']) {
|
|
6140
|
+
counts[sev] = m[sev] || 0;
|
|
6141
|
+
counts.total += counts[sev];
|
|
6142
|
+
}
|
|
6143
|
+
const vulns = data.vulnerabilities || {};
|
|
6144
|
+
for (const name of Object.keys(vulns)) {
|
|
6145
|
+
const v = vulns[name];
|
|
6146
|
+
const viaList = Array.isArray(v.via) ? v.via : [];
|
|
6147
|
+
const advisories = viaList
|
|
6148
|
+
.filter((x) => !!x && typeof x === 'object')
|
|
6149
|
+
.map(x => ({ title: x.title || '', url: x.url || '' }));
|
|
6150
|
+
const via = viaList.filter((x) => typeof x === 'string');
|
|
6151
|
+
result.findings.push({
|
|
6152
|
+
name,
|
|
6153
|
+
severity: v.severity,
|
|
6154
|
+
range: v.range || '',
|
|
6155
|
+
isDirect: !!v.isDirect,
|
|
6156
|
+
advisories,
|
|
6157
|
+
via,
|
|
6158
|
+
effects: Array.isArray(v.effects) ? v.effects : [],
|
|
6159
|
+
...classifyFix(v.fixAvailable)
|
|
6160
|
+
});
|
|
6068
6161
|
}
|
|
6069
|
-
|
|
6162
|
+
const order = ['critical', 'high', 'moderate', 'low', 'info'];
|
|
6163
|
+
result.findings.sort((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity) || a.name.localeCompare(b.name));
|
|
6164
|
+
return result;
|
|
6165
|
+
}
|
|
6166
|
+
/** One-line explanation of a finding for the Issues Summary: what, why it is here, how to clear it. */
|
|
6167
|
+
function describeAuditFinding(f) {
|
|
6168
|
+
const parts = [`audit ${f.severity.toUpperCase()}: ${f.name} ${f.range}`.trim()];
|
|
6169
|
+
if (f.advisories.length > 0) {
|
|
6170
|
+
// One advisory named in full; the rest counted. tar alone carries 12 and a line per
|
|
6171
|
+
// title would bury the Issues Summary. The URL leads to the complete list.
|
|
6172
|
+
const a = f.advisories[0];
|
|
6173
|
+
const more = f.advisories.length - 1;
|
|
6174
|
+
parts.push(`${a.title}${a.url ? ` (${a.url})` : ''}${more > 0 ? ` +${more} more` : ''}`);
|
|
6175
|
+
}
|
|
6176
|
+
if (f.via.length > 0)
|
|
6177
|
+
parts.push(`through ${f.via.join(', ')}`);
|
|
6178
|
+
if (f.isDirect)
|
|
6179
|
+
parts.push('direct dependency');
|
|
6180
|
+
else if (f.effects.length > 0)
|
|
6181
|
+
parts.push(`pulled in by ${f.effects.join(', ')}`);
|
|
6182
|
+
switch (f.fix) {
|
|
6183
|
+
case 'plain':
|
|
6184
|
+
parts.push('fix: npm audit fix');
|
|
6185
|
+
break;
|
|
6186
|
+
case 'major':
|
|
6187
|
+
parts.push(`fix: npm audit fix --force → ${f.fixTarget || 'a semver-major bump'} (breaking — not run automatically)`);
|
|
6188
|
+
break;
|
|
6189
|
+
case 'none':
|
|
6190
|
+
parts.push('fix: none published yet');
|
|
6191
|
+
break;
|
|
6192
|
+
}
|
|
6193
|
+
return parts.join(' — ');
|
|
6194
|
+
}
|
|
6195
|
+
/** Run the audit report and put critical/high findings into the Issues Summary. Call once,
|
|
6196
|
+
* right before the end-of-run summary. */
|
|
6197
|
+
function auditForSummary(cwd, moduleName) {
|
|
6198
|
+
const audit = runNpmAudit(cwd);
|
|
6199
|
+
for (const f of audit.findings) {
|
|
6200
|
+
if (f.severity === 'critical' || f.severity === 'high') {
|
|
6201
|
+
recordBuildIssue(moduleName, 'warning', describeAuditFinding(f));
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
return audit;
|
|
6205
|
+
}
|
|
6206
|
+
/** The "Audit:" row for the Release / Transform Summary. */
|
|
6207
|
+
function auditSummaryRow(audit, dryRun) {
|
|
6208
|
+
if (dryRun)
|
|
6209
|
+
return ` Audit: ${colors.dim('skipped (dry-run)')}`;
|
|
6210
|
+
if (!audit)
|
|
6211
|
+
return ` Audit: ${colors.dim('not run')}`;
|
|
6212
|
+
if (!audit.ran)
|
|
6213
|
+
return ` Audit: ${colors.yellow('not run')} ${colors.dim(audit.error)}`;
|
|
6214
|
+
if (audit.counts.total === 0)
|
|
6215
|
+
return ` Audit: ${colors.green('✓')} no vulnerabilities`;
|
|
6216
|
+
const c = audit.counts;
|
|
6217
|
+
const parts = [];
|
|
6218
|
+
if (c.critical > 0)
|
|
6219
|
+
parts.push(colors.red(`${c.critical} critical`));
|
|
6220
|
+
if (c.high > 0)
|
|
6221
|
+
parts.push(colors.red(`${c.high} high`));
|
|
6222
|
+
if (c.moderate > 0)
|
|
6223
|
+
parts.push(colors.yellow(`${c.moderate} moderate`));
|
|
6224
|
+
if (c.low > 0)
|
|
6225
|
+
parts.push(`${c.low} low`);
|
|
6226
|
+
if (c.info > 0)
|
|
6227
|
+
parts.push(`${c.info} info`);
|
|
6228
|
+
const serious = c.critical > 0 || c.high > 0;
|
|
6229
|
+
const mark = serious ? colors.red('✗') : colors.yellow('!');
|
|
6230
|
+
const hint = serious ? colors.dim(' (details in Issues Summary)') : '';
|
|
6231
|
+
return ` Audit: ${mark} ${parts.join(', ')}${hint}`;
|
|
6070
6232
|
}
|
|
6071
6233
|
/** Get the version of npmglobalize itself */
|
|
6072
6234
|
export function getToolVersion() {
|
|
@@ -6687,8 +6849,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6687
6849
|
// before doing anything else. Independent of build script presence — a dep
|
|
6688
6850
|
// with new declared deps still needs its own `npm install` even if it has
|
|
6689
6851
|
// no build step. CLI entrypoint already ran this, so skip when _fromCli.
|
|
6690
|
-
if (!options._fromCli && !dryRun)
|
|
6691
|
-
await ensureFileDepModules(cwd, verbose);
|
|
6852
|
+
if (!options._fromCli && !dryRun) {
|
|
6853
|
+
const modulesOk = await ensureFileDepModules(cwd, verbose);
|
|
6854
|
+
if (!modulesOk && !force) {
|
|
6855
|
+
console.error(colors.red(`npm install failed in ${pkg.name || cwd} — stopping before build/publish (-force continues anyway).`));
|
|
6856
|
+
return false;
|
|
6857
|
+
}
|
|
6858
|
+
if (!modulesOk)
|
|
6859
|
+
console.log(colors.yellow('Continuing with -force despite npm install failure...'));
|
|
6860
|
+
}
|
|
6692
6861
|
// Run build step if package.json has a build script (skip if CLI already built)
|
|
6693
6862
|
if (pkg.scripts?.build && !options._fromCli) {
|
|
6694
6863
|
console.log(`${timestamp()} Running build...`);
|
|
@@ -7326,19 +7495,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7326
7495
|
}
|
|
7327
7496
|
}
|
|
7328
7497
|
}
|
|
7329
|
-
//
|
|
7330
|
-
|
|
7498
|
+
// 2026-09-06 — Claude Code (Fable 5.1): fix step only here (before the pre-release commit,
|
|
7499
|
+
// so a changed lockfile is committed). The report moved next to the summaries so its
|
|
7500
|
+
// result is shown where the user is looking, not scrolled off under the git output.
|
|
7501
|
+
if (fix) {
|
|
7331
7502
|
if (!dryRun) {
|
|
7332
|
-
await
|
|
7503
|
+
await runNpmAuditFix(cwd);
|
|
7333
7504
|
}
|
|
7334
7505
|
else {
|
|
7335
|
-
console.log(' [dry-run] Would run npm audit');
|
|
7506
|
+
console.log(' [dry-run] Would run npm audit fix');
|
|
7336
7507
|
}
|
|
7337
7508
|
}
|
|
7338
|
-
else if (fix && !dryRun) {
|
|
7339
|
-
// Run fix even if no deps changed
|
|
7340
|
-
await runNpmAudit(cwd, fix, verbose);
|
|
7341
|
-
}
|
|
7342
7509
|
if (noPublish) {
|
|
7343
7510
|
console.log('Transform complete (--nopublish mode).');
|
|
7344
7511
|
if (freeze && !dryRun) {
|
|
@@ -7390,11 +7557,11 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7390
7557
|
console.log(` Package: ${colors.green(finalPkg2.name)}`);
|
|
7391
7558
|
console.log(` Version: ${colors.green('v' + finalPkg2.version)}`);
|
|
7392
7559
|
console.log(` Published: ${colors.red('✗')}`);
|
|
7560
|
+
console.log(auditSummaryRow(dryRun ? null : auditForSummary(cwd, finalPkg2.name), dryRun));
|
|
7393
7561
|
for (const s of surprises)
|
|
7394
7562
|
console.log(` ${colors.yellow('!')} ${s}`);
|
|
7395
7563
|
console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
7396
7564
|
console.log('');
|
|
7397
|
-
printPnpmSuggestionSummary();
|
|
7398
7565
|
return true;
|
|
7399
7566
|
}
|
|
7400
7567
|
// Skip if private
|
|
@@ -8472,11 +8639,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
8472
8639
|
// publish so the version recorded is the one that actually shipped.
|
|
8473
8640
|
await recordUpstream(cwd, dryRun, verbose);
|
|
8474
8641
|
console.log(`${timestamp()} Done!`);
|
|
8475
|
-
// Run final audit report if not already run
|
|
8476
|
-
const auditAlreadyRun = (fix || updateDeps) && (transformResult.transformed || alreadyTransformed || updateDeps);
|
|
8477
|
-
if (!auditAlreadyRun && (fix || updateDeps || transformResult.transformed) && !dryRun) {
|
|
8478
|
-
await runNpmAudit(cwd, false, verbose); // Just report, don't fix again
|
|
8479
|
-
}
|
|
8480
8642
|
// Print summary
|
|
8481
8643
|
console.log('');
|
|
8482
8644
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
@@ -8489,6 +8651,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
8489
8651
|
console.log(` Version: ${colors.green('v' + finalPkg.version)}`);
|
|
8490
8652
|
console.log(` Published: ${colors.green('✓')} (${accessLabel})`);
|
|
8491
8653
|
console.log(` Git pushed: ${colors.green('✓')}`);
|
|
8654
|
+
console.log(auditSummaryRow(dryRun ? null : auditForSummary(cwd, finalPkg.name), dryRun));
|
|
8492
8655
|
if (explicitKeys?.has('gitVisibility')) {
|
|
8493
8656
|
console.log(` Git visibility: ${colors.green(gitVisibility.toUpperCase())}`);
|
|
8494
8657
|
}
|
|
@@ -8537,7 +8700,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
8537
8700
|
}
|
|
8538
8701
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
8539
8702
|
console.log('');
|
|
8540
|
-
printPnpmSuggestionSummary();
|
|
8541
8703
|
// Only show "To use run" message if package provides commands (has bin field)
|
|
8542
8704
|
if (finalPkg.bin) {
|
|
8543
8705
|
const commandName = finalPkg.name.includes('/') ? finalPkg.name.split('/')[1] : finalPkg.name;
|
|
@@ -8750,7 +8912,13 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
8750
8912
|
// the case where a dep was added to a member package.json but `npm install`
|
|
8751
8913
|
// wasn't re-run — the builds would otherwise fail resolving the new dep.
|
|
8752
8914
|
if (!options.dryRun) {
|
|
8753
|
-
await ensureWorkspaceDepModules(rootDir, packages.map(p => ({ dir: p.dir, pkg: p.pkg })), !!options.verbose);
|
|
8915
|
+
const modulesOk = await ensureWorkspaceDepModules(rootDir, packages.map(p => ({ dir: p.dir, pkg: p.pkg })), !!options.verbose);
|
|
8916
|
+
if (!modulesOk && !options.force) {
|
|
8917
|
+
console.error(colors.red('npm install failed — stopping before build/publish (-force continues anyway).'));
|
|
8918
|
+
return { success: false, packages: [], publishOrder: [] };
|
|
8919
|
+
}
|
|
8920
|
+
if (!modulesOk)
|
|
8921
|
+
console.log(colors.yellow('Continuing with -force despite npm install failure...'));
|
|
8754
8922
|
}
|
|
8755
8923
|
// Prescan: decide which packages actually need processing so we don't waste
|
|
8756
8924
|
// time rebuilding+republishing ones with no relevant changes.
|
|
@@ -8891,7 +9059,6 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
8891
9059
|
}
|
|
8892
9060
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
8893
9061
|
console.log('');
|
|
8894
|
-
printPnpmSuggestionSummary();
|
|
8895
9062
|
// Global install of the workspace root (monorepo CLI)
|
|
8896
9063
|
const { install = false, link = false, wsl = false, dryRun = false, verbose = false } = options;
|
|
8897
9064
|
const rootPkgFinal = readPackageJson(rootDir);
|