@bobfrankston/npmglobalize 1.0.148 → 1.0.150

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 CHANGED
@@ -94,6 +94,28 @@ Errors abort (unless `--force`); warnings prompt to continue.
94
94
 
95
95
  Skip the prescan with `-no-prescan` / `-nps`.
96
96
 
97
+ #### Workspace skip prescan
98
+
99
+ In workspace mode, before processing begins, each package is also checked to decide whether it actually needs work. A package is **skipped** (no rebuild, no version bump, no publish) only if **all** of these are true:
100
+
101
+ 1. **Working tree clean** — `git status --porcelain .` reports no uncommitted changes for that package's directory.
102
+ 2. **Version already on npm** — the version in its `package.json` is published to the registry, and no non-bookkeeping commit (anything outside "Pre-release commit" / "Restore file: dependencies" / "Pre-version cleanup" / "Untrack node_modules") is newer than that version's publish timestamp.
103
+ 3. **Build is fresh** — every `.ts` source file (excluding `.d.ts`) has a sibling `.js` whose mtime is ≥ the `.ts` mtime. A missing `.js` or an older `.js` counts as stale.
104
+ 4. **No sibling workspace `file:` dep flagged for work** — if another workspace package depends on this one via `file:`, and that dep is being updated in this run, this one is also processed (propagates through the graph in topological order).
105
+
106
+ If any condition fails, the package is processed. The prescan prints one line per package with `⟳` for "will process" (and the reason) or `✓` for "skip". Example:
107
+
108
+ ```
109
+ ⟳ mlproc — uncommitted changes
110
+ ⟳ pzip — stale build (index.ts newer than index.js)
111
+ ⟳ stage — dep pzip is being updated
112
+ ✓ puller — skip (clean, published, build fresh)
113
+ ```
114
+
115
+ The summary row for a skipped package shows `– name v1.0.0 (skipped — already up to date)`.
116
+
117
+ Use `--force` or `--force-publish` to bypass the skip prescan and process every package.
118
+
97
119
  **Force republish** all file: dependencies even if versions exist:
98
120
  ```bash
99
121
  npmglobalize --force-publish
@@ -59,7 +59,7 @@
59
59
  "package-lock.json",
60
60
  "*.ts",
61
61
  "!*.d.ts",
62
- "*.map",
62
+ // "*.map", // keep source maps in published packages for debugging
63
63
  "tsconfig.json",
64
64
  ".vscode/",
65
65
  ".globalize.json5"
package/lib.d.ts CHANGED
@@ -115,6 +115,7 @@ interface WorkspacePackageResult {
115
115
  success: boolean;
116
116
  version?: string;
117
117
  error?: string;
118
+ skipped?: boolean;
118
119
  }
119
120
  /** Aggregate result from workspace orchestration */
120
121
  export interface WorkspaceResult {
@@ -188,13 +189,15 @@ export declare function buildDependencyGraph(packages: Array<{
188
189
  /** Topological sort with cycle detection. Returns package names in dependency order. */
189
190
  export declare function topologicalSort(graph: Map<string, Set<string>>): string[];
190
191
  /** Transform file: dependencies to npm versions */
192
+ export type UnpublishedDep = {
193
+ name: string;
194
+ version: string;
195
+ path: string;
196
+ reason: 'new' | 'update';
197
+ };
191
198
  export declare function transformDeps(pkg: any, baseDir: string, verbose?: boolean, forcePublish?: boolean): {
192
199
  transformed: boolean;
193
- unpublished: Array<{
194
- name: string;
195
- version: string;
196
- path: string;
197
- }>;
200
+ unpublished: UnpublishedDep[];
198
201
  };
199
202
  /** A problem discovered by the prescan. */
200
203
  export interface PrescanIssue {
package/lib.js CHANGED
@@ -810,6 +810,49 @@ function hasUnpublishedTransitiveDeps(packageName, pkg, baseDir, verbose) {
810
810
  }
811
811
  return false;
812
812
  }
813
+ /** Walk a dir and return true if any `.ts` source has mtime newer than its sibling `.js`
814
+ * (or the `.js` is missing). Skips node_modules/prev/cruft/.git and declaration files. */
815
+ function hasStaleBuild(rootDir) {
816
+ const SKIP = new Set(['node_modules', 'prev', 'cruft', '.git']);
817
+ function walk(dir) {
818
+ let entries;
819
+ try {
820
+ entries = fs.readdirSync(dir, { withFileTypes: true });
821
+ }
822
+ catch {
823
+ return { stale: false };
824
+ }
825
+ for (const e of entries) {
826
+ const full = path.join(dir, e.name);
827
+ if (e.isDirectory()) {
828
+ if (SKIP.has(e.name.toLowerCase()))
829
+ continue;
830
+ const sub = walk(full);
831
+ if (sub.stale)
832
+ return sub;
833
+ }
834
+ else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
835
+ const jsPath = full.slice(0, -3) + '.js';
836
+ try {
837
+ const tsStat = fs.statSync(full);
838
+ let jsStat;
839
+ try {
840
+ jsStat = fs.statSync(jsPath);
841
+ }
842
+ catch {
843
+ return { stale: true, example: `${e.name} (no .js)` };
844
+ }
845
+ if (tsStat.mtimeMs > jsStat.mtimeMs) {
846
+ return { stale: true, example: `${e.name} newer than ${path.basename(jsPath)}` };
847
+ }
848
+ }
849
+ catch { /* unreadable — ignore */ }
850
+ }
851
+ }
852
+ return { stale: false };
853
+ }
854
+ return walk(rootDir);
855
+ }
813
856
  /** Check if local package directory has changes newer than the npm-published version.
814
857
  * Detects uncommitted changes or commits made after the version was published. */
815
858
  function hasLocalChanges(packageName, version, targetPath, verbose) {
@@ -863,7 +906,6 @@ function hasLocalChanges(packageName, version, targetPath, verbose) {
863
906
  return false;
864
907
  }
865
908
  }
866
- /** Transform file: dependencies to npm versions */
867
909
  export function transformDeps(pkg, baseDir, verbose = false, forcePublish = false) {
868
910
  let transformed = false;
869
911
  const unpublished = [];
@@ -894,7 +936,7 @@ export function transformDeps(pkg, baseDir, verbose = false, forcePublish = fals
894
936
  // Check if this version exists on npm (or if force publish)
895
937
  const versionExists = forcePublish ? false : checkVersionExists(name, targetVersion);
896
938
  if (!versionExists) {
897
- unpublished.push({ name, version: targetVersion, path: targetPath });
939
+ unpublished.push({ name, version: targetVersion, path: targetPath, reason: forcePublish ? 'update' : 'new' });
898
940
  if (forcePublish) {
899
941
  console.log(colors.yellow(` ⟳ ${name}@${targetVersion} will be republished (--force-publish)`));
900
942
  }
@@ -908,15 +950,15 @@ export function transformDeps(pkg, baseDir, verbose = false, forcePublish = fals
908
950
  }
909
951
  // Check transitive file: deps — if any are unpublished, this dep needs republishing
910
952
  if (hasUnpublishedTransitiveDeps(name, targetPkg, targetPath, verbose)) {
911
- unpublished.push({ name, version: targetVersion, path: targetPath });
953
+ unpublished.push({ name, version: targetVersion, path: targetPath, reason: 'update' });
912
954
  console.log(colors.yellow(` ⟳ ${name}@${targetVersion} has unpublished transitive deps — will republish`));
913
955
  }
914
956
  else if (hasLocalChanges(name, targetVersion, targetPath, verbose)) {
915
- unpublished.push({ name, version: targetVersion, path: targetPath });
957
+ unpublished.push({ name, version: targetVersion, path: targetPath, reason: 'update' });
916
958
  console.log(colors.yellow(` ⟳ ${name}@${targetVersion} has local changes not on npm — will republish`));
917
959
  }
918
960
  else if (checkIgnoreFiles(targetPath, { verbose }).securityChanges.length > 0) {
919
- unpublished.push({ name, version: targetVersion, path: targetPath });
961
+ unpublished.push({ name, version: targetVersion, path: targetPath, reason: 'update' });
920
962
  console.log(colors.yellow(` ⟳ ${name}@${targetVersion} has missing ignore patterns — will republish`));
921
963
  }
922
964
  }
@@ -2349,6 +2391,13 @@ export async function initGit(cwd, visibility, dryRun) {
2349
2391
  if (staged.status !== 0) {
2350
2392
  runCommandOrThrow('git', ['commit', '-m', 'Initial commit'], { cwd });
2351
2393
  }
2394
+ // Guarantee HEAD exists — `gh repo create --push` refuses an empty repo.
2395
+ // Can happen if the working tree had nothing to stage (everything ignored, etc.).
2396
+ const hasHead = spawnSafe('git', ['rev-parse', '--verify', 'HEAD'], { cwd, stdio: 'pipe' });
2397
+ if (hasHead.status !== 0) {
2398
+ console.log(colors.yellow(' No commits yet — creating an empty initial commit so --push can proceed.'));
2399
+ runCommandOrThrow('git', ['commit', '--allow-empty', '-m', 'Initial commit'], { cwd });
2400
+ }
2352
2401
  // Create GitHub repo (or link to existing one)
2353
2402
  const visFlag = visibility === 'private' ? '--private' : '--public';
2354
2403
  const createResult = runCommand('gh', ['repo', 'create', repoName, visFlag, '--source=.', '--push'], { cwd, silent: true });
@@ -3137,9 +3186,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3137
3186
  }
3138
3187
  // Don't set "private": true in package.json - that blocks all publishing
3139
3188
  console.log(`Package '${pkg.name}' will publish as PRIVATE (restricted access).`);
3140
- if (!currentAccess) {
3141
- console.log(colors.dim(` First publish - requires paid npm account`));
3142
- }
3143
3189
  }
3144
3190
  else if (effectiveNpmVisibility === 'public') {
3145
3191
  // User explicitly wants public (or confirmed via prompt)
@@ -3337,21 +3383,59 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3337
3383
  }
3338
3384
  console.log('');
3339
3385
  }
3340
- // Check if target packages need to be published
3386
+ // Publish/update dependencies.
3387
+ // - reason='update': already on npm but stale (local changes, broken transitive, missing ignore) — auto-publish always
3388
+ // - reason='new' : version not on npm yet — auto-publish if --publish-deps, else prompt
3341
3389
  if (transformResult.unpublished.length > 0 && !noPublish) {
3390
+ const updateDepsList = transformResult.unpublished.filter(d => d.reason === 'update');
3391
+ const newDepsList = transformResult.unpublished.filter(d => d.reason === 'new');
3342
3392
  console.log('');
3343
- console.log(colors.yellow('Dependencies to publish:'));
3344
- for (const { name, version, path } of transformResult.unpublished) {
3345
- console.log(colors.yellow(` ${name}@${version} (${path})`));
3393
+ if (updateDepsList.length > 0) {
3394
+ console.log(colors.yellow('Dependencies to update (already on npm, auto-republishing):'));
3395
+ for (const { name, version, path } of updateDepsList) {
3396
+ console.log(colors.yellow(` ${name}@${version} (${path})`));
3397
+ }
3398
+ }
3399
+ if (newDepsList.length > 0) {
3400
+ console.log(colors.yellow('New dependencies to publish:'));
3401
+ for (const { name, version, path } of newDepsList) {
3402
+ console.log(colors.yellow(` ${name}@${version} (${path})`));
3403
+ }
3346
3404
  }
3347
3405
  console.log('');
3348
3406
  console.log('Dependency tree (✓ = on npm, ✗ = needs publishing):');
3349
3407
  printDepTree(cwd);
3350
3408
  console.log('');
3351
- if (publishDeps) {
3409
+ // Decide which deps to publish now.
3410
+ // Updates always run. New deps run if --publish-deps; otherwise prompt.
3411
+ let depsToPublish = [...updateDepsList];
3412
+ let proceedWithNew = newDepsList.length === 0 || publishDeps;
3413
+ if (newDepsList.length > 0 && !publishDeps) {
3414
+ console.log(colors.yellow('Options for NEW dependencies:'));
3415
+ console.log(colors.yellow(' 1. Publish them manually first'));
3416
+ console.log(colors.yellow(' 2. Use --publish-deps (-pd) to publish them automatically'));
3417
+ console.log(colors.yellow(' 3. Use -npd (--no-publish-deps) to skip dependency publishing'));
3418
+ console.log(colors.dim(' (--force controls error-continuation only; it does not imply consent to publish new deps)'));
3419
+ console.log('');
3420
+ if (publishDepsYes) {
3421
+ proceedWithNew = true;
3422
+ }
3423
+ else {
3424
+ proceedWithNew = await confirm('Publish new dependencies now?', false);
3425
+ if (!proceedWithNew) {
3426
+ const newList = newDepsList.map(d => `${d.name}@${d.version}`).join(', ');
3427
+ recordBuildIssue(pkg.name || cwd, 'error', `Declined publishing new deps (${newList}). Rerun with -pd to auto-publish, or publish them manually first.`);
3428
+ return false;
3429
+ }
3430
+ }
3431
+ }
3432
+ if (proceedWithNew) {
3433
+ depsToPublish.push(...newDepsList);
3434
+ }
3435
+ if (depsToPublish.length > 0) {
3352
3436
  const action = forcePublish ? 'Publishing/updating' : 'Publishing';
3353
- console.log(`${action} file: dependencies first (--publish-deps)...`);
3354
- for (const { name, version, path } of transformResult.unpublished) {
3437
+ console.log(`${action} file: dependencies first...`);
3438
+ for (const { name, version, path } of depsToPublish) {
3355
3439
  console.log('');
3356
3440
  console.log(colors.yellow(`━━━ Publishing ${name}@${version} ━━━`));
3357
3441
  if (!dryRun) {
@@ -3393,20 +3477,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3393
3477
  console.log('');
3394
3478
  console.log(colors.green('✓ All dependencies published'));
3395
3479
  }
3396
- else {
3397
- console.log(colors.yellow('Options:'));
3398
- console.log(colors.yellow(' 1. Publish them manually first'));
3399
- console.log(colors.yellow(' 2. Use --publish-deps to publish them automatically'));
3400
- console.log(colors.yellow(' 3. Use --force to continue anyway (NOT RECOMMENDED)'));
3401
- console.log(colors.yellow(' 4. Use -npd (--no-publish-deps) to skip dependency publishing'));
3402
- console.log('');
3403
- if (!force && !publishDepsYes) {
3404
- const shouldContinue = await confirm('Continue with unpublished dependencies?', false);
3405
- if (!shouldContinue) {
3406
- return false;
3407
- }
3408
- }
3409
- }
3410
3480
  }
3411
3481
  if (!dryRun) {
3412
3482
  writePackageJson(cwd, pkg);
@@ -3878,7 +3948,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3878
3948
  }
3879
3949
  catch (error) {
3880
3950
  let autoFixed = false;
3881
- console.error(colors.red('ERROR: Version bump failed:'), error.message);
3951
+ console.error(colors.red(`ERROR: Version bump failed in ${cwd} (${pkg.name || '<unnamed>'}):`), error.message);
3882
3952
  // Show additional error details if available
3883
3953
  if (error.stderr || error.stdout || error.code) {
3884
3954
  if (error.stderr)
@@ -4035,8 +4105,39 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4035
4105
  return false;
4036
4106
  }
4037
4107
  }
4108
+ else if (/paths are ignored by one of your \.gitignore files/i.test(combinedOutput)) {
4109
+ // git add refused to stage files (usually package.json) because a
4110
+ // .gitignore rule matches. Ask git which exact line is to blame.
4111
+ const ignoredMatch = combinedOutput.match(/paths are ignored by one of your \.gitignore files:\s*\n([\s\S]*?)(?:hint:|$)/i);
4112
+ const ignoredFiles = (ignoredMatch ? ignoredMatch[1] : '')
4113
+ .split('\n').map(s => s.trim()).filter(Boolean);
4114
+ console.error(colors.red(`\n.gitignore in ${cwd} is blocking required file(s):`));
4115
+ for (const f of ignoredFiles) {
4116
+ const chk = spawnSafe('git', ['-C', cwd, 'check-ignore', '-v', f], { encoding: 'utf-8', stdio: 'pipe' });
4117
+ if (chk.status === 0 && chk.stdout?.trim()) {
4118
+ // Output format: "<source>:<line>:<pattern>\t<file>"
4119
+ const m = chk.stdout.trim().match(/^(.+?):(\d+):(.+?)\t(.+)$/);
4120
+ if (m) {
4121
+ const [, src, line, pattern, file] = m;
4122
+ console.error(colors.red(` ${file} — matched by ${src}:${line} pattern: ${pattern}`));
4123
+ if (pattern.trim() === '*') {
4124
+ console.error(colors.yellow(` → Line ${line} is a bare "*" which ignores EVERY file. Delete it.`));
4125
+ }
4126
+ }
4127
+ else {
4128
+ console.error(colors.red(` ${f}`));
4129
+ console.error(colors.dim(` ${chk.stdout.trim()}`));
4130
+ }
4131
+ }
4132
+ else {
4133
+ console.error(colors.red(` ${f}`));
4134
+ }
4135
+ }
4136
+ console.error(colors.yellow(`\nEdit ${path.join(cwd, '.gitignore')} to remove/narrow the blocking pattern,`));
4137
+ console.error(colors.yellow(`or exempt with an explicit negation, e.g.: !package.json`));
4138
+ }
4038
4139
  else if (error.message?.includes('unknown git error')) {
4039
- console.error(colors.yellow('Unknown git error — check git hooks, signing, or permissions'));
4140
+ console.error(colors.yellow(`Unknown git error in ${cwd} — check git hooks, signing, or permissions`));
4040
4141
  }
4041
4142
  else if (combinedOutput.includes('gh013') || combinedOutput.includes('push protection') || combinedOutput.includes('push declined due to repository rule')) {
4042
4143
  // GitHub push protection blocked a push (from postversion script)
@@ -4607,9 +4708,84 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
4607
4708
  }
4608
4709
  }
4609
4710
  }
4711
+ // Prescan: decide which packages actually need processing so we don't waste
4712
+ // time rebuilding+republishing ones with no relevant changes.
4713
+ // A package is SKIPPED only if ALL of:
4714
+ // - working tree clean (no uncommitted changes)
4715
+ // - current version is on npm AND no commits newer than its publish time
4716
+ // - build not stale (every .ts source has a .js no older than it)
4717
+ // - no sibling workspace file: dep was itself flagged for processing
4718
+ const flaggedForWork = new Set();
4719
+ const skipReasons = new Map();
4720
+ if (!options.force && !options.forcePublish) {
4721
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4722
+ console.log(` Prescan: checking ${filteredOrder.length} package(s)...`);
4723
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4724
+ const wsDeps = new Map(); // pkg → workspace-sibling file: deps
4725
+ for (const pkgInfo of packages) {
4726
+ const sibs = new Set();
4727
+ const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
4728
+ for (const [dn, dv] of Object.entries(deps)) {
4729
+ if (typeof dv === 'string' && dv.startsWith('file:') && wsNameSet.has(dn)) {
4730
+ sibs.add(dn);
4731
+ }
4732
+ }
4733
+ wsDeps.set(pkgInfo.name, sibs);
4734
+ }
4735
+ for (const pkgName of filteredOrder) {
4736
+ const pkgInfo = packages.find(p => p.name === pkgName);
4737
+ if (!pkgInfo)
4738
+ continue;
4739
+ let reason;
4740
+ const status = spawnSafe('git', ['-C', pkgInfo.dir, 'status', '--porcelain', '.'], { encoding: 'utf-8', stdio: 'pipe', shell: true });
4741
+ if (status.status === 0 && status.stdout.trim()) {
4742
+ reason = 'uncommitted changes';
4743
+ }
4744
+ else {
4745
+ const version = pkgInfo.pkg.version;
4746
+ if (!checkVersionExists(pkgInfo.name, version)) {
4747
+ reason = `v${version} not on npm`;
4748
+ }
4749
+ else if (hasLocalChanges(pkgInfo.name, version, pkgInfo.dir, false)) {
4750
+ reason = 'commits since last publish';
4751
+ }
4752
+ else {
4753
+ const stale = hasStaleBuild(pkgInfo.dir);
4754
+ if (stale.stale) {
4755
+ reason = `stale build (${stale.example})`;
4756
+ }
4757
+ else {
4758
+ const flaggedSib = [...(wsDeps.get(pkgName) || [])].find(s => flaggedForWork.has(s));
4759
+ if (flaggedSib)
4760
+ reason = `dep ${flaggedSib} is being updated`;
4761
+ }
4762
+ }
4763
+ }
4764
+ if (reason) {
4765
+ flaggedForWork.add(pkgName);
4766
+ console.log(` ${colors.yellow('⟳')} ${pkgName} — ${reason}`);
4767
+ }
4768
+ else {
4769
+ skipReasons.set(pkgName, 'clean, published, build fresh');
4770
+ console.log(` ${colors.green('✓')} ${pkgName} — skip (clean, published, build fresh)`);
4771
+ }
4772
+ }
4773
+ console.log('');
4774
+ if (flaggedForWork.size === 0) {
4775
+ console.log(colors.green('Nothing to do — all packages up to date.'));
4776
+ console.log('');
4777
+ return { success: true, packages: [], publishOrder };
4778
+ }
4779
+ console.log(colors.dim(`Prescan: ${flaggedForWork.size} to process, ${skipReasons.size} skip. (Use --force to process all.)`));
4780
+ console.log('');
4781
+ }
4610
4782
  // Process each package in dependency order
4611
4783
  const results = [];
4612
4784
  for (const pkgName of filteredOrder) {
4785
+ if (skipReasons.has(pkgName)) {
4786
+ results.push({ name: pkgName, dir: packages.find(p => p.name === pkgName).dir, success: true, version: packages.find(p => p.name === pkgName).pkg.version, skipped: true });
4787
+ continue;
4788
+ }
4613
4789
  const pkgInfo = packages.find(p => p.name === pkgName);
4614
4790
  if (!pkgInfo)
4615
4791
  continue;
@@ -4662,10 +4838,11 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
4662
4838
  console.log(allSuccess ? colors.green('✓ Workspace Summary') : colors.red('✗ Workspace Summary'));
4663
4839
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4664
4840
  for (const r of results) {
4665
- const status = r.success ? colors.green('✓') : colors.red('✗');
4841
+ const status = r.skipped ? colors.dim('–') : (r.success ? colors.green('✓') : colors.red('✗'));
4666
4842
  const ver = r.version ? ` v${r.version}` : '';
4667
4843
  const err = r.error ? colors.red(` (${r.error})`) : '';
4668
- console.log(` ${status} ${r.name}${ver}${err}`);
4844
+ const tag = r.skipped ? colors.dim(' (skipped — already up to date)') : '';
4845
+ console.log(` ${status} ${r.name}${ver}${err}${tag}`);
4669
4846
  }
4670
4847
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4671
4848
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.148",
3
+ "version": "1.0.150",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",