@bobfrankston/npmglobalize 1.0.149 → 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 {
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) {
@@ -3143,9 +3186,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3143
3186
  }
3144
3187
  // Don't set "private": true in package.json - that blocks all publishing
3145
3188
  console.log(`Package '${pkg.name}' will publish as PRIVATE (restricted access).`);
3146
- if (!currentAccess) {
3147
- console.log(colors.dim(` First publish - requires paid npm account`));
3148
- }
3149
3189
  }
3150
3190
  else if (effectiveNpmVisibility === 'public') {
3151
3191
  // User explicitly wants public (or confirmed via prompt)
@@ -3908,7 +3948,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3908
3948
  }
3909
3949
  catch (error) {
3910
3950
  let autoFixed = false;
3911
- 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);
3912
3952
  // Show additional error details if available
3913
3953
  if (error.stderr || error.stdout || error.code) {
3914
3954
  if (error.stderr)
@@ -4065,8 +4105,39 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4065
4105
  return false;
4066
4106
  }
4067
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
+ }
4068
4139
  else if (error.message?.includes('unknown git error')) {
4069
- 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`));
4070
4141
  }
4071
4142
  else if (combinedOutput.includes('gh013') || combinedOutput.includes('push protection') || combinedOutput.includes('push declined due to repository rule')) {
4072
4143
  // GitHub push protection blocked a push (from postversion script)
@@ -4637,9 +4708,84 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
4637
4708
  }
4638
4709
  }
4639
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
+ }
4640
4782
  // Process each package in dependency order
4641
4783
  const results = [];
4642
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
+ }
4643
4789
  const pkgInfo = packages.find(p => p.name === pkgName);
4644
4790
  if (!pkgInfo)
4645
4791
  continue;
@@ -4692,10 +4838,11 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
4692
4838
  console.log(allSuccess ? colors.green('✓ Workspace Summary') : colors.red('✗ Workspace Summary'));
4693
4839
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4694
4840
  for (const r of results) {
4695
- const status = r.success ? colors.green('✓') : colors.red('✗');
4841
+ const status = r.skipped ? colors.dim('–') : (r.success ? colors.green('✓') : colors.red('✗'));
4696
4842
  const ver = r.version ? ` v${r.version}` : '';
4697
4843
  const err = r.error ? colors.red(` (${r.error})`) : '';
4698
- 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}`);
4699
4846
  }
4700
4847
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4701
4848
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.149",
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",