@bobfrankston/npmglobalize 1.0.163 → 1.0.165
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 +25 -10
- package/cli.js +17 -46
- package/lib.d.ts +17 -0
- package/lib.js +134 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -498,19 +498,34 @@ npmglobalize --dry-run # See what would happen
|
|
|
498
498
|
1. **Validates** package.json and git status
|
|
499
499
|
2. **Checks** if current version is on npm (recovers from failed publishes)
|
|
500
500
|
3. **Updates dependencies** (if `--update-deps`)
|
|
501
|
-
4. **
|
|
502
|
-
5. **
|
|
503
|
-
6. **
|
|
504
|
-
7. **
|
|
505
|
-
8. **
|
|
506
|
-
9. **
|
|
507
|
-
10. **
|
|
508
|
-
11. **
|
|
509
|
-
12. **
|
|
510
|
-
13. **
|
|
501
|
+
4. **Builds `file:` deps in topological order**, then the target itself, so consumers' `tsc` reads up-to-date `.d.ts` from sibling checkouts whose source has changed (see [Build Cascade](#build-cascade))
|
|
502
|
+
5. **Publishes file: dependencies** (if needed)
|
|
503
|
+
6. **Backs up** original file: references to `.dependencies`
|
|
504
|
+
7. **Converts** `file:` → npm version references
|
|
505
|
+
8. **Commits** changes
|
|
506
|
+
9. **Bumps** version (using npm version) — skipped if recovering a failed publish
|
|
507
|
+
10. **Publishes** to npm
|
|
508
|
+
11. **Pushes** to git (with push-protection detection and auto-bypass)
|
|
509
|
+
12. **Installs** globally (if `--install`)
|
|
510
|
+
13. **Restores** file: references (if `--files`, default)
|
|
511
|
+
14. **Runs audit** (shows security status)
|
|
511
512
|
|
|
512
513
|
## Operational Details
|
|
513
514
|
|
|
515
|
+
### Build Cascade
|
|
516
|
+
|
|
517
|
+
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.
|
|
518
|
+
|
|
519
|
+
For each project visited (the target and every transitive `file:` dep):
|
|
520
|
+
|
|
521
|
+
- If `tsconfig.json` is missing or has `"noEmit": true` → **skip** (not a TypeScript build).
|
|
522
|
+
- If `tsconfig.json` exists but `package.json` has no `"build"` script → **prompt** to add `"build": "tsc"`. Decline and that project is skipped.
|
|
523
|
+
- Otherwise → run `npm run build`. A failure halts the cascade unless `-force` is passed.
|
|
524
|
+
|
|
525
|
+
Cycle-safe via a shared visited set; each project is built at most once per run.
|
|
526
|
+
|
|
527
|
+
This complements the existing publish cascade (which ensures version refs are correct) by closing the build-freshness gap that `npm install` alone left open.
|
|
528
|
+
|
|
514
529
|
### The `.dependencies` Backup (Internal/Transient)
|
|
515
530
|
|
|
516
531
|
**You should never see `.dependencies` in your `package.json` under normal operation.** It is a temporary internal backup that exists only during the brief publish cycle and is removed automatically when the cycle completes.
|
package/cli.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* npmglobalize CLI - Transform file: dependencies to npm versions for publishing
|
|
4
4
|
*/
|
|
5
|
-
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson,
|
|
5
|
+
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically } from './lib.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { styleText } from 'util';
|
|
9
|
-
import JSON5 from 'json5';
|
|
10
9
|
// npmglobalize install directory (for --version)
|
|
11
10
|
const __dirname = import.meta.dirname;
|
|
12
11
|
function printHelp() {
|
|
@@ -429,53 +428,25 @@ export async function main() {
|
|
|
429
428
|
process.exit(1);
|
|
430
429
|
}
|
|
431
430
|
}
|
|
432
|
-
// Build
|
|
431
|
+
// Build file: deps topologically, then the target itself.
|
|
432
|
+
// Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
|
|
433
|
+
// whose source has changed since their last build.
|
|
433
434
|
if (!cliOptions.cleanup) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
shouldBuild = tsconfig.compilerOptions?.noEmit !== true;
|
|
440
|
-
}
|
|
441
|
-
catch (error) {
|
|
442
|
-
// No tsconfig — skip build
|
|
435
|
+
ensureFileDepModules(cwd, !!cliOptions.verbose);
|
|
436
|
+
const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
|
|
437
|
+
if (!depsOk && !cliOptions.force) {
|
|
438
|
+
printBuildSummary();
|
|
439
|
+
process.exit(1);
|
|
443
440
|
}
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (!pkg.scripts)
|
|
452
|
-
pkg.scripts = {};
|
|
453
|
-
pkg.scripts.build = 'tsc';
|
|
454
|
-
writePackageJson(cwd, pkg);
|
|
455
|
-
console.log(styleText('green', '✓ Added "build": "tsc" to package.json'));
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (pkg.scripts?.build) {
|
|
459
|
-
ensureFileDepModules(cwd, !!cliOptions.verbose);
|
|
460
|
-
console.log(`Building ${cwd}...`);
|
|
461
|
-
const buildResult = runCommand('npm', ['run', 'build'], { cwd, silent: true });
|
|
462
|
-
if (buildResult.success) {
|
|
463
|
-
console.log(styleText('green', '✓ Build succeeded'));
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
const buildOutput = (buildResult.stderr || '') + (buildResult.output || '');
|
|
467
|
-
if (buildOutput)
|
|
468
|
-
console.error(buildOutput);
|
|
469
|
-
console.error(styleText('red', `Build failed in ${cwd}`));
|
|
470
|
-
const firstErr = extractFirstTscError(buildOutput);
|
|
471
|
-
recordBuildIssue(pkg.name || path.basename(cwd), 'error', firstErr || 'Build failed');
|
|
472
|
-
if (!cliOptions.force) {
|
|
473
|
-
printBuildSummary();
|
|
474
|
-
process.exit(1);
|
|
475
|
-
}
|
|
476
|
-
console.log(styleText('yellow', 'Continuing with --force...'));
|
|
477
|
-
}
|
|
441
|
+
if (!depsOk)
|
|
442
|
+
console.log(styleText('yellow', 'Continuing with --force despite dep build failure...'));
|
|
443
|
+
const targetOk = await buildProject(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
|
|
444
|
+
if (!targetOk) {
|
|
445
|
+
if (!cliOptions.force) {
|
|
446
|
+
printBuildSummary();
|
|
447
|
+
process.exit(1);
|
|
478
448
|
}
|
|
449
|
+
console.log(styleText('yellow', 'Continuing with --force...'));
|
|
479
450
|
}
|
|
480
451
|
}
|
|
481
452
|
// Handle --package: update scripts in target package.json
|
package/lib.d.ts
CHANGED
|
@@ -270,6 +270,23 @@ export declare function missingDeps(pkgDir: string, pkg: any): string[];
|
|
|
270
270
|
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
271
271
|
* Cycle-safe via the shared `visited` set. */
|
|
272
272
|
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): void;
|
|
273
|
+
/** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
|
|
274
|
+
* TypeScript project lacks a build script, run `npm run build`, record
|
|
275
|
+
* failures. Returns true if build succeeded (or was skipped because no
|
|
276
|
+
* tsconfig / noEmit / no build script after declining the prompt). */
|
|
277
|
+
export declare function buildProject(cwd: string, opts?: {
|
|
278
|
+
verbose?: boolean;
|
|
279
|
+
force?: boolean;
|
|
280
|
+
}): Promise<boolean>;
|
|
281
|
+
/** Walk `file:` deps depth-first (deps before consumers) and build each one
|
|
282
|
+
* that has a tsconfig. Mirrors `ensureFileDepModules`'s traversal but invokes
|
|
283
|
+
* `buildProject` instead of `npm install`. Cycle-safe via shared `visited` set.
|
|
284
|
+
* Does NOT build `cwd` itself — caller does that after this returns. Returns
|
|
285
|
+
* true if every visited dep built successfully (or was skipped). */
|
|
286
|
+
export declare function buildFileDepsTopologically(cwd: string, opts?: {
|
|
287
|
+
verbose?: boolean;
|
|
288
|
+
force?: boolean;
|
|
289
|
+
}, visited?: Set<string>): Promise<boolean>;
|
|
273
290
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
274
291
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
275
292
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
package/lib.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Consider library-based approach if async operations or cross-platform issues arise.
|
|
11
11
|
*/
|
|
12
12
|
import fs from 'fs';
|
|
13
|
+
import os from 'os';
|
|
13
14
|
import path from 'path';
|
|
14
15
|
import { execSync, spawnSync } from 'child_process';
|
|
15
16
|
import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir } from '@bobfrankston/userconfig';
|
|
@@ -1562,6 +1563,98 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
|
|
|
1562
1563
|
}
|
|
1563
1564
|
}
|
|
1564
1565
|
}
|
|
1566
|
+
/** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
|
|
1567
|
+
* TypeScript project lacks a build script, run `npm run build`, record
|
|
1568
|
+
* failures. Returns true if build succeeded (or was skipped because no
|
|
1569
|
+
* tsconfig / noEmit / no build script after declining the prompt). */
|
|
1570
|
+
export async function buildProject(cwd, opts = {}) {
|
|
1571
|
+
let shouldBuild = false;
|
|
1572
|
+
try {
|
|
1573
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
1574
|
+
const content = fs.readFileSync(tsconfigPath, 'utf-8');
|
|
1575
|
+
const tsconfig = JSON5.parse(content);
|
|
1576
|
+
shouldBuild = tsconfig.compilerOptions?.noEmit !== true;
|
|
1577
|
+
}
|
|
1578
|
+
catch {
|
|
1579
|
+
// No tsconfig — skip
|
|
1580
|
+
}
|
|
1581
|
+
if (!shouldBuild)
|
|
1582
|
+
return true;
|
|
1583
|
+
const pkg = readPackageJson(cwd);
|
|
1584
|
+
if (!pkg.scripts?.build) {
|
|
1585
|
+
console.log(colors.yellow(`TypeScript project has no "build" script in ${pkg.name || cwd}`));
|
|
1586
|
+
const addIt = await confirm(`Add "build": "tsc" to ${pkg.name || path.basename(cwd)}'s package.json?`, true);
|
|
1587
|
+
if (addIt) {
|
|
1588
|
+
if (!pkg.scripts)
|
|
1589
|
+
pkg.scripts = {};
|
|
1590
|
+
pkg.scripts.build = 'tsc';
|
|
1591
|
+
writePackageJson(cwd, pkg);
|
|
1592
|
+
console.log(colors.green(`✓ Added "build": "tsc" to ${pkg.name || path.basename(cwd)}`));
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
return true;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
console.log(`Building ${pkg.name || cwd}...`);
|
|
1599
|
+
const buildResult = runCommand('npm', ['run', 'build'], { cwd, silent: true });
|
|
1600
|
+
if (buildResult.success) {
|
|
1601
|
+
console.log(colors.green(`✓ Build succeeded (${pkg.name || path.basename(cwd)})`));
|
|
1602
|
+
return true;
|
|
1603
|
+
}
|
|
1604
|
+
const buildOutput = (buildResult.stderr || '') + (buildResult.output || '');
|
|
1605
|
+
if (buildOutput)
|
|
1606
|
+
console.error(buildOutput);
|
|
1607
|
+
console.error(colors.red(`Build failed in ${pkg.name || cwd}`));
|
|
1608
|
+
const firstErr = extractFirstTscError(buildOutput);
|
|
1609
|
+
recordBuildIssue(pkg.name || path.basename(cwd), 'error', firstErr || 'Build failed');
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
/** Walk `file:` deps depth-first (deps before consumers) and build each one
|
|
1613
|
+
* that has a tsconfig. Mirrors `ensureFileDepModules`'s traversal but invokes
|
|
1614
|
+
* `buildProject` instead of `npm install`. Cycle-safe via shared `visited` set.
|
|
1615
|
+
* Does NOT build `cwd` itself — caller does that after this returns. Returns
|
|
1616
|
+
* true if every visited dep built successfully (or was skipped). */
|
|
1617
|
+
export async function buildFileDepsTopologically(cwd, opts = {}, visited = new Set()) {
|
|
1618
|
+
const abs = path.resolve(cwd);
|
|
1619
|
+
if (visited.has(abs))
|
|
1620
|
+
return true;
|
|
1621
|
+
visited.add(abs);
|
|
1622
|
+
let pkg;
|
|
1623
|
+
try {
|
|
1624
|
+
pkg = readPackageJson(cwd);
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
return true;
|
|
1628
|
+
}
|
|
1629
|
+
let allOk = true;
|
|
1630
|
+
for (const key of ['dependencies', 'devDependencies']) {
|
|
1631
|
+
const deps = pkg?.[key];
|
|
1632
|
+
if (!deps || typeof deps !== 'object')
|
|
1633
|
+
continue;
|
|
1634
|
+
for (const [, spec] of Object.entries(deps)) {
|
|
1635
|
+
if (typeof spec !== 'string' || !spec.startsWith('file:'))
|
|
1636
|
+
continue;
|
|
1637
|
+
const target = path.resolve(cwd, spec.slice('file:'.length));
|
|
1638
|
+
if (!fs.existsSync(path.join(target, 'package.json')))
|
|
1639
|
+
continue;
|
|
1640
|
+
const targetAbs = path.resolve(target);
|
|
1641
|
+
if (visited.has(targetAbs))
|
|
1642
|
+
continue;
|
|
1643
|
+
// Recurse first (deps before consumer)
|
|
1644
|
+
const childOk = await buildFileDepsTopologically(target, opts, visited);
|
|
1645
|
+
if (!childOk)
|
|
1646
|
+
allOk = false;
|
|
1647
|
+
// Build this dep itself
|
|
1648
|
+
const ok = await buildProject(target, opts);
|
|
1649
|
+
if (!ok) {
|
|
1650
|
+
allOk = false;
|
|
1651
|
+
if (!opts.force)
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return allOk;
|
|
1657
|
+
}
|
|
1565
1658
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
1566
1659
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
1567
1660
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
|
@@ -1642,6 +1735,45 @@ export function runCommand(cmd, args, options = {}) {
|
|
|
1642
1735
|
return { success: false, output: '', stderr: error.message };
|
|
1643
1736
|
}
|
|
1644
1737
|
}
|
|
1738
|
+
/** Dump forensic info to a temp file when `npm pack` is killed by a spurious
|
|
1739
|
+
* Ctrl+C. Goal: correlate the failure with whatever else was attached to the
|
|
1740
|
+
* console (Claude Code wrapper, VS Code task, AV, etc.). Returns the log path. */
|
|
1741
|
+
function dumpPackCtrlcDiagnostics(pkg, cwd, packOutput, packStderr) {
|
|
1742
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1743
|
+
const logPath = path.join(os.tmpdir(), `npmglobalize-pack-ctrlc-${ts}.log`);
|
|
1744
|
+
const lines = [];
|
|
1745
|
+
lines.push(`# npm pack Ctrl+C diagnostic — ${new Date().toISOString()}`);
|
|
1746
|
+
lines.push(`package: ${pkg?.name}@${pkg?.version}`);
|
|
1747
|
+
lines.push(`cwd: ${cwd}`);
|
|
1748
|
+
lines.push(`our pid: ${process.pid} ppid: ${process.ppid}`);
|
|
1749
|
+
lines.push(`platform: ${process.platform} node: ${process.version}`);
|
|
1750
|
+
for (const k of ['TERM_PROGRAM', 'WT_SESSION', 'CLAUDE_SESSION_ID', 'CLAUDECODE', 'VSCODE_PID', 'VSCODE_INJECTION', 'ComSpec']) {
|
|
1751
|
+
if (process.env[k])
|
|
1752
|
+
lines.push(`env ${k}: ${process.env[k]}`);
|
|
1753
|
+
}
|
|
1754
|
+
lines.push('');
|
|
1755
|
+
lines.push('## pack stdout (first 2KB)');
|
|
1756
|
+
lines.push(packOutput.slice(0, 2048));
|
|
1757
|
+
lines.push('');
|
|
1758
|
+
lines.push('## pack stderr (first 2KB)');
|
|
1759
|
+
lines.push(packStderr.slice(0, 2048));
|
|
1760
|
+
lines.push('');
|
|
1761
|
+
if (process.platform === 'win32') {
|
|
1762
|
+
try {
|
|
1763
|
+
const ps = spawnSafe('wmic', ['process', 'get', 'Name,ProcessId,ParentProcessId,SessionId,CommandLine', '/format:csv'], { encoding: 'utf-8', timeout: 5000 });
|
|
1764
|
+
lines.push('## process snapshot (wmic)');
|
|
1765
|
+
lines.push((ps.stdout || ps.stderr || '(no output)').slice(0, 64 * 1024));
|
|
1766
|
+
}
|
|
1767
|
+
catch (e) {
|
|
1768
|
+
lines.push(`## wmic failed: ${e?.message}`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
try {
|
|
1772
|
+
fs.writeFileSync(logPath, lines.join('\n'));
|
|
1773
|
+
}
|
|
1774
|
+
catch { /* ignore */ }
|
|
1775
|
+
return logPath;
|
|
1776
|
+
}
|
|
1645
1777
|
/** Install a package globally in WSL, auto-fixing the root-owned /usr/local/lib/node_modules
|
|
1646
1778
|
* EACCES case by switching npm to a user prefix (~/.npm-global) and retrying once.
|
|
1647
1779
|
* Output is captured (so we can scan for the error) and then mirrored to the terminal. */
|
|
@@ -4673,7 +4805,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4673
4805
|
// npm.cmd. Our process survives, so a retry usually succeeds.
|
|
4674
4806
|
let packResult = runCommand('npm', ['pack'], { cwd, silent: true });
|
|
4675
4807
|
if (!packResult.success && /Terminate batch job|\^C/.test(packResult.output + packResult.stderr)) {
|
|
4676
|
-
|
|
4808
|
+
const logPath = dumpPackCtrlcDiagnostics(pkg, cwd, packResult.output, packResult.stderr);
|
|
4809
|
+
console.error(colors.yellow(` npm pack interrupted by spurious Ctrl+C — retrying once... (diag: ${logPath})`));
|
|
4677
4810
|
packResult = runCommand('npm', ['pack'], { cwd, silent: true });
|
|
4678
4811
|
}
|
|
4679
4812
|
// Restore stashed node_modules now that pack is done — must happen
|