@bobfrankston/npmglobalize 1.0.164 → 1.0.166

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.
Files changed (5) hide show
  1. package/README.md +25 -10
  2. package/cli.js +17 -46
  3. package/lib.d.ts +17 -0
  4. package/lib.js +92 -0
  5. 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. **Publishes file: dependencies** (if needed)
502
- 5. **Backs up** original file: references to `.dependencies`
503
- 6. **Converts** `file:` npm version references
504
- 7. **Commits** changes
505
- 8. **Bumps** version (using npm version) — skipped if recovering a failed publish
506
- 9. **Publishes** to npm
507
- 10. **Pushes** to git (with push-protection detection and auto-bypass)
508
- 11. **Installs** globally (if `--install`)
509
- 12. **Restores** file: references (if `--files`, default)
510
- 13. **Runs audit** (shows security status)
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, confirm, getBuildIssues, clearBuildIssues, recordBuildIssue, extractFirstTscError, ensureFileDepModules, runCommand } from './lib.js';
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 the target project if it has tsconfig (not noEmit)
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
- let shouldBuild = false;
435
- try {
436
- const tsconfigPath = path.join(cwd, 'tsconfig.json');
437
- const content = fs.readFileSync(tsconfigPath, 'utf-8');
438
- const tsconfig = JSON5.parse(content);
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 (shouldBuild) {
445
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
446
- if (!pkg.scripts?.build) {
447
- // TypeScript project without a build script — offer to add one
448
- console.log(styleText('yellow', `TypeScript project has no "build" script in ${cwd}`));
449
- const addIt = await confirm('Add "build": "tsc" to package.json?', true);
450
- if (addIt) {
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
@@ -1563,6 +1563,98 @@ export function ensureFileDepModules(cwd, verbose = false, visited = new Set())
1563
1563
  }
1564
1564
  }
1565
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
+ }
1566
1658
  /** Ensure the workspace-root `node_modules/` is in sync with every member's
1567
1659
  * declared deps. Workspaces hoist deps to the root, so a dep added to any
1568
1660
  * member `package.json` without a follow-up `npm install` at the root leaves
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.164",
3
+ "version": "1.0.166",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",