@bobfrankston/npmglobalize 1.0.160 → 1.0.162

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
@@ -189,6 +189,31 @@ npmglobalize -np # --nopublish (formerly --apply)
189
189
  npmglobalize --cleanup # Restore original file: references
190
190
  ```
191
191
 
192
+ ### 📝 Release Notes via `.commitmsg`
193
+
194
+ For multi-line or reusable release notes, write them to a `.commitmsg` file in the package root instead of passing them on the command line:
195
+
196
+ ```bash
197
+ cat > .commitmsg <<'EOF'
198
+ Added foo feature
199
+ Fixed bar regression
200
+ EOF
201
+ npmglobalize
202
+ ```
203
+
204
+ Behavior:
205
+ - If `-m` / `-message` is **not** given and `.commitmsg` exists, its contents are used as the commit message (and force a release even if the working tree is otherwise clean).
206
+ - After a successful `npm publish`, npmglobalize:
207
+ 1. Appends the contents to `npmchanges.md` under a `## v<version> — <YYYY-MM-DD>` header (creating the file if needed)
208
+ 2. Deletes `.commitmsg`
209
+ 3. Commits both changes as `Log v<version> to npmchanges.md` and pushes
210
+
211
+ Notes:
212
+ - **Git/GitHub only.** npm publish does not consume git commit messages; `npmchanges.md` lives in the git repo and on GitHub but is excluded from the published npm tarball (the standard `*.md` rule keeps only `README.md`).
213
+ - If both `-m` and `.commitmsg` are present, `-m` wins and `.commitmsg` is left alone (not consumed).
214
+ - If publish fails, `.commitmsg` is preserved for the next attempt.
215
+ - `.commitmsg` is auto-added to `.npmignore` (security pattern) so it never leaks into the tarball.
216
+
192
217
  ### 🔧 Git Integration & Error Recovery
193
218
 
194
219
  **Automatic tag conflict resolution**:
@@ -288,6 +313,8 @@ Publishing requires being on a branch so commits and tags can be properly tracke
288
313
  -nopublish, -np Just transform, don't publish (persisted to config)
289
314
  -cleanup Restore file: dependencies from .dependencies backup
290
315
  -m, -message <msg> Custom commit message (forces release even without changes)
316
+ If -m not given, a `.commitmsg` file (if present) is used instead.
317
+ See "Release Notes via .commitmsg" below.
291
318
  ```
292
319
 
293
320
  ### Dependency Options
package/cli.js CHANGED
@@ -27,6 +27,9 @@ Release Options:
27
27
  -nopublish, -np Just transform, don't publish (persisted to config)
28
28
  -cleanup Restore from .dependencies
29
29
  -m, -message <msg> Custom commit message (forces release even without changes)
30
+ If -m not given and a .commitmsg file exists in cwd, its
31
+ contents are used. After a successful publish it is appended
32
+ to npmchanges.md (with version header) and deleted.
30
33
 
31
34
  Dependency Options:
32
35
  -update-deps, -ud Update package.json to latest (minor/patch only, safe)
@@ -41,7 +41,8 @@
41
41
  ".env*",
42
42
  "token*",
43
43
  "certs/",
44
- "*cert*/"
44
+ "*cert*/",
45
+ ".commitmsg"
45
46
  ],
46
47
  // Prompted or auto-added with --conform
47
48
  recommended: [
package/lib.d.ts CHANGED
@@ -186,12 +186,25 @@ export declare function getFileRefs(pkg: any): Map<string, {
186
186
  name: string;
187
187
  value: string;
188
188
  }>;
189
+ /** Expand workspace entries (which may include globs like 'packages/*') to relative
190
+ * dir paths from rootDir, normalized to forward slashes. Only `<base>/*` is expanded;
191
+ * more exotic globs (`**`, `?`, character classes) are passed through unchanged. */
192
+ export declare function expandWorkspaceEntries(rootDir: string, entries: string[]): string[];
189
193
  /** Resolve workspace entries to package info. Skips dirs without package.json or with private:true. */
190
194
  export declare function resolveWorkspacePackages(rootDir: string): Array<{
191
195
  name: string;
192
196
  dir: string;
193
197
  pkg: any;
194
198
  }>;
199
+ /** Like resolveWorkspacePackages but INCLUDES private packages and exposes the
200
+ * workspaces[] entry (relative path) for each. Use for build ordering, where
201
+ * a private workspace package is still part of the dep graph. */
202
+ export declare function resolveAllWorkspacePackages(rootDir: string): Array<{
203
+ name: string;
204
+ dir: string;
205
+ pkg: any;
206
+ entry: string;
207
+ }>;
195
208
  /** Build a dependency graph among workspace packages. Returns Map<name, Set<depName>>. */
196
209
  export declare function buildDependencyGraph(packages: Array<{
197
210
  name: string;
@@ -244,7 +257,10 @@ export declare function compareVersions(a: number[], b: number[]): number;
244
257
  /** Fix version/tag mismatches */
245
258
  export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
246
259
  /** Return declared deps (dependencies + devDependencies) that don't resolve from
247
- * `pkgDir`. Skips `file:`/`workspace:`/`link:` specs those are handled elsewhere.
260
+ * `pkgDir`. Skips `workspace:`/`link:` specs (handled by workspace tooling /
261
+ * rarely used). `file:` deps are checked: npm installs them as junctions
262
+ * (Windows) or symlinks, and `fs.existsSync` traverses both, so a missing
263
+ * file: dep is a real out-of-sync case worth flagging.
248
264
  * A declared dep that doesn't resolve means `package.json` and installed
249
265
  * `node_modules/` are out of sync (e.g. dep added but `npm install` not re-run). */
250
266
  export declare function missingDeps(pkgDir: string, pkg: any): string[];
package/lib.js CHANGED
@@ -685,6 +685,41 @@ export function getFileRefs(pkg) {
685
685
  return refs;
686
686
  }
687
687
  // ─── Workspace helpers ───────────────────────────────────────────────
688
+ /** Expand workspace entries (which may include globs like 'packages/*') to relative
689
+ * dir paths from rootDir, normalized to forward slashes. Only `<base>/*` is expanded;
690
+ * more exotic globs (`**`, `?`, character classes) are passed through unchanged. */
691
+ export function expandWorkspaceEntries(rootDir, entries) {
692
+ const expanded = [];
693
+ const seen = new Set();
694
+ const push = (rel) => { if (!seen.has(rel)) {
695
+ seen.add(rel);
696
+ expanded.push(rel);
697
+ } };
698
+ for (const entry of entries) {
699
+ const norm = entry.replace(/\\/g, '/');
700
+ const m = norm.match(/^(.+?)\/\*$/);
701
+ if (m && !m[1].includes('*')) {
702
+ const baseDir = path.resolve(rootDir, m[1]);
703
+ if (!fs.existsSync(baseDir) || !fs.statSync(baseDir).isDirectory())
704
+ continue;
705
+ const subs = fs.readdirSync(baseDir).sort();
706
+ for (const sub of subs) {
707
+ if (sub.startsWith('.'))
708
+ continue;
709
+ const subPath = path.join(baseDir, sub);
710
+ if (!fs.statSync(subPath).isDirectory())
711
+ continue;
712
+ if (!fs.existsSync(path.join(subPath, 'package.json')))
713
+ continue;
714
+ push(`${m[1]}/${sub}`);
715
+ }
716
+ }
717
+ else {
718
+ push(norm);
719
+ }
720
+ }
721
+ return expanded;
722
+ }
688
723
  /** Resolve workspace entries to package info. Skips dirs without package.json or with private:true. */
689
724
  export function resolveWorkspacePackages(rootDir) {
690
725
  const rootPkg = readPackageJson(rootDir);
@@ -692,7 +727,7 @@ export function resolveWorkspacePackages(rootDir) {
692
727
  if (!Array.isArray(workspaces))
693
728
  return [];
694
729
  const results = [];
695
- for (const entry of workspaces) {
730
+ for (const entry of expandWorkspaceEntries(rootDir, workspaces)) {
696
731
  const pkgDir = path.resolve(rootDir, entry);
697
732
  const pkgJsonPath = path.join(pkgDir, 'package.json');
698
733
  if (!fs.existsSync(pkgJsonPath))
@@ -704,6 +739,87 @@ export function resolveWorkspacePackages(rootDir) {
704
739
  }
705
740
  return results;
706
741
  }
742
+ /** Like resolveWorkspacePackages but INCLUDES private packages and exposes the
743
+ * workspaces[] entry (relative path) for each. Use for build ordering, where
744
+ * a private workspace package is still part of the dep graph. */
745
+ export function resolveAllWorkspacePackages(rootDir) {
746
+ const rootPkg = readPackageJson(rootDir);
747
+ const workspaces = rootPkg.workspaces;
748
+ if (!Array.isArray(workspaces))
749
+ return [];
750
+ const results = [];
751
+ for (const entry of expandWorkspaceEntries(rootDir, workspaces)) {
752
+ const pkgDir = path.resolve(rootDir, entry);
753
+ const pkgJsonPath = path.join(pkgDir, 'package.json');
754
+ if (!fs.existsSync(pkgJsonPath))
755
+ continue;
756
+ const pkg = readPackageJson(pkgDir);
757
+ results.push({ name: pkg.name || entry, dir: pkgDir, pkg, entry });
758
+ }
759
+ return results;
760
+ }
761
+ /** Workspace-root build with `npm run ... --workspaces` runs sub-packages in
762
+ * workspaces[] array order. If that order doesn't match topological dep order,
763
+ * a consumer can be compiled against a sibling whose .d.ts/.js is still stale.
764
+ * Temporarily rewrites workspaces[] to topological order for the build, then
765
+ * restores it. Returns undefined if no rewrite is needed (or no build script
766
+ * uses --workspaces). */
767
+ function reorderWorkspacesForBuild(cwd, pkg, verbose) {
768
+ if (!Array.isArray(pkg.workspaces) || pkg.workspaces.length < 2)
769
+ return undefined;
770
+ const buildScript = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '';
771
+ if (!buildScript.includes('--workspaces') && !buildScript.includes('-ws'))
772
+ return undefined;
773
+ const all = resolveAllWorkspacePackages(cwd);
774
+ if (all.length < 2)
775
+ return undefined;
776
+ const graph = buildDependencyGraph(all);
777
+ let order;
778
+ try {
779
+ order = topologicalSort(graph);
780
+ }
781
+ catch (e) {
782
+ console.error(colors.yellow(` Skipping workspace reorder: ${e.message}`));
783
+ return undefined;
784
+ }
785
+ const nameToEntry = new Map();
786
+ for (const p of all)
787
+ nameToEntry.set(p.name, p.entry);
788
+ const newEntries = order
789
+ .map(n => nameToEntry.get(n))
790
+ .filter((e) => !!e);
791
+ // If the expanded current order already matches topological order AND has no
792
+ // globs, nothing to do. (When globs are present, npm expands them in fs order
793
+ // — we always need to write an explicit list.)
794
+ const original = pkg.workspaces.slice();
795
+ const hasGlob = original.some(e => e.includes('*'));
796
+ if (!hasGlob) {
797
+ const cur = original.map(e => e.replace(/\\/g, '/'));
798
+ if (cur.length === newEntries.length && cur.every((v, i) => v === newEntries[i])) {
799
+ return undefined;
800
+ }
801
+ }
802
+ pkg.workspaces = newEntries;
803
+ writePackageJson(cwd, pkg);
804
+ if (verbose) {
805
+ console.log(colors.dim(` Reordered workspaces[] for topological build: ${newEntries.join(' → ')}`));
806
+ }
807
+ let restored = false;
808
+ return () => {
809
+ if (restored)
810
+ return;
811
+ restored = true;
812
+ try {
813
+ pkg.workspaces = original;
814
+ const onDisk = readPackageJson(cwd);
815
+ onDisk.workspaces = original;
816
+ writePackageJson(cwd, onDisk);
817
+ }
818
+ catch (e) {
819
+ console.error(colors.yellow(` Warning: failed to restore workspaces[]: ${e.message}`));
820
+ }
821
+ };
822
+ }
707
823
  /** Build a dependency graph among workspace packages. Returns Map<name, Set<depName>>. */
708
824
  export function buildDependencyGraph(packages) {
709
825
  const nameSet = new Set(packages.map(p => p.name));
@@ -1338,7 +1454,10 @@ function depResolves(startDir, depName) {
1338
1454
  }
1339
1455
  }
1340
1456
  /** Return declared deps (dependencies + devDependencies) that don't resolve from
1341
- * `pkgDir`. Skips `file:`/`workspace:`/`link:` specs those are handled elsewhere.
1457
+ * `pkgDir`. Skips `workspace:`/`link:` specs (handled by workspace tooling /
1458
+ * rarely used). `file:` deps are checked: npm installs them as junctions
1459
+ * (Windows) or symlinks, and `fs.existsSync` traverses both, so a missing
1460
+ * file: dep is a real out-of-sync case worth flagging.
1342
1461
  * A declared dep that doesn't resolve means `package.json` and installed
1343
1462
  * `node_modules/` are out of sync (e.g. dep added but `npm install` not re-run). */
1344
1463
  export function missingDeps(pkgDir, pkg) {
@@ -1350,7 +1469,7 @@ export function missingDeps(pkgDir, pkg) {
1350
1469
  for (const [name, spec] of Object.entries(deps)) {
1351
1470
  if (typeof spec !== 'string')
1352
1471
  continue;
1353
- if (spec.startsWith('file:') || spec.startsWith('workspace:') || spec.startsWith('link:'))
1472
+ if (spec.startsWith('workspace:') || spec.startsWith('link:'))
1354
1473
  continue;
1355
1474
  if (!depResolves(pkgDir, name))
1356
1475
  missing.push(name);
@@ -3281,25 +3400,34 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3281
3400
  ensureFileDepModules(cwd, verbose);
3282
3401
  console.log(`${timestamp()} Running build...`);
3283
3402
  if (!dryRun) {
3284
- // Always capture output so we can extract tsc errors for the summary
3285
- const buildResult = runCommand('npm', ['run', 'build'], { cwd, silent: true });
3286
- if (!buildResult.success) {
3287
- const buildOutput = buildResult.stderr || buildResult.output;
3288
- if (buildOutput)
3289
- console.error(buildOutput);
3290
- console.error(colors.red('ERROR: Build failed'));
3291
- diagnoseBuildFailure(buildOutput || '', cwd);
3292
- const firstErr = extractFirstTscError(buildOutput || '');
3293
- recordBuildIssue(pkg.name || path.basename(cwd), 'error', firstErr || 'Build failed');
3294
- if (!force) {
3295
- return false;
3403
+ // For workspace roots whose build invokes `--workspaces`, npm uses
3404
+ // workspaces[] array order; rewrite to topological order so deps build
3405
+ // before consumers (avoids stale-.d.ts errors in cross-package imports).
3406
+ const restoreWorkspaces = reorderWorkspacesForBuild(cwd, pkg, verbose);
3407
+ try {
3408
+ // Always capture output so we can extract tsc errors for the summary
3409
+ const buildResult = runCommand('npm', ['run', 'build'], { cwd, silent: true });
3410
+ if (!buildResult.success) {
3411
+ const buildOutput = buildResult.stderr || buildResult.output;
3412
+ if (buildOutput)
3413
+ console.error(buildOutput);
3414
+ console.error(colors.red('ERROR: Build failed'));
3415
+ diagnoseBuildFailure(buildOutput || '', cwd);
3416
+ const firstErr = extractFirstTscError(buildOutput || '');
3417
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', firstErr || 'Build failed');
3418
+ if (!force) {
3419
+ return false;
3420
+ }
3421
+ console.log(colors.yellow('Continuing with --force despite build failure...'));
3422
+ }
3423
+ else {
3424
+ if (verbose && buildResult.output)
3425
+ process.stdout.write(buildResult.output);
3426
+ console.log(`${timestamp()} ${colors.green('✓ Build succeeded')}`);
3296
3427
  }
3297
- console.log(colors.yellow('Continuing with --force despite build failure...'));
3298
3428
  }
3299
- else {
3300
- if (verbose && buildResult.output)
3301
- process.stdout.write(buildResult.output);
3302
- console.log(`${timestamp()} ${colors.green('✓ Build succeeded')}`);
3429
+ finally {
3430
+ restoreWorkspaces?.();
3303
3431
  }
3304
3432
  }
3305
3433
  else {
@@ -3919,11 +4047,29 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3919
4047
  }
3920
4048
  // Re-check git status after all transformations and potential commits
3921
4049
  currentGitStatus = getGitStatus(cwd);
4050
+ // If no -m given, fall back to .commitmsg file contents (one-shot changelog entry).
4051
+ // Consumed (appended to npmchanges.md + deleted) only after a successful publish.
4052
+ const commitMsgPath = path.join(cwd, '.commitmsg');
4053
+ let commitMsgFromFile = null;
4054
+ let effectiveMessage = message;
4055
+ if (!effectiveMessage && fs.existsSync(commitMsgPath)) {
4056
+ try {
4057
+ const content = fs.readFileSync(commitMsgPath, 'utf-8').trim();
4058
+ if (content) {
4059
+ commitMsgFromFile = content;
4060
+ effectiveMessage = content;
4061
+ console.log(colors.cyan(` Using .commitmsg for commit message (${content.split('\n')[0].slice(0, 60)}${content.length > 60 ? '…' : ''})`));
4062
+ }
4063
+ }
4064
+ catch (err) {
4065
+ console.error(colors.yellow(` Warning: could not read .commitmsg: ${err.message}`));
4066
+ }
4067
+ }
3922
4068
  // Check if there are changes to commit or a custom message
3923
4069
  // Skip this check for first publish (currentAccess null) or just-initialized repos
3924
4070
  const isFirstPublish = !currentAccess;
3925
4071
  let skipVersionBump = false;
3926
- if (!currentGitStatus.hasUncommitted && !message && !justInitialized && !isFirstPublish) {
4072
+ if (!currentGitStatus.hasUncommitted && !effectiveMessage && !justInitialized && !isFirstPublish) {
3927
4073
  // Check if the current version is actually on npm (it might have failed to publish previously)
3928
4074
  const versionCheck = spawnSafe('npm', ['view', `${pkg.name}@${pkg.version}`, 'version'], {
3929
4075
  shell: process.platform === 'win32',
@@ -4074,7 +4220,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4074
4220
  }
4075
4221
  // Git operations
4076
4222
  if (currentGitStatus.hasUncommitted) {
4077
- const commitMsg = message || 'Pre-release commit';
4223
+ const commitMsg = effectiveMessage || 'Pre-release commit';
4078
4224
  console.log(`${timestamp()} Committing changes: ${commitMsg}`);
4079
4225
  if (!dryRun) {
4080
4226
  // Remove 'nul' files that break git on Windows
@@ -4701,6 +4847,36 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4701
4847
  const finalAccess = effectiveNpmVisibility || currentAccess || (isScoped ? 'restricted' : 'public');
4702
4848
  const accessLabel = (finalAccess === 'restricted' || finalAccess === 'private') ? 'PRIVATE' : 'PUBLIC';
4703
4849
  console.log(`${timestamp()} ${colors.green(`✓ Published to npm as ${accessLabel}`)}`);
4850
+ // Consume .commitmsg: append to npmchanges.md, delete the file, commit+push.
4851
+ // Only runs if .commitmsg was actually used as the commit message.
4852
+ if (commitMsgFromFile) {
4853
+ try {
4854
+ const publishedVersion = readPackageJson(cwd).version;
4855
+ const npmChangesPath = path.join(cwd, 'npmchanges.md');
4856
+ const date = new Date().toISOString().slice(0, 10);
4857
+ const header = `## v${publishedVersion} — ${date}\n\n`;
4858
+ const entry = header + commitMsgFromFile.trim() + '\n\n';
4859
+ const existing = fs.existsSync(npmChangesPath)
4860
+ ? fs.readFileSync(npmChangesPath, 'utf-8')
4861
+ : '# npm Publish Changes\n\n';
4862
+ const sep = existing.endsWith('\n') ? '' : '\n';
4863
+ fs.writeFileSync(npmChangesPath, existing + sep + entry);
4864
+ try {
4865
+ fs.unlinkSync(commitMsgPath);
4866
+ }
4867
+ catch { /* ignore */ }
4868
+ runCommand('git', ['add', 'npmchanges.md', '.commitmsg'], { cwd, silent: true });
4869
+ const logResult = gitCommit(`Log v${publishedVersion} to npmchanges.md`, cwd);
4870
+ if (logResult.success) {
4871
+ console.log(colors.green(` ✓ Appended to npmchanges.md and removed .commitmsg`));
4872
+ if (currentGitStatus.hasRemote)
4873
+ pushWithProtection(cwd, verbose);
4874
+ }
4875
+ }
4876
+ catch (err) {
4877
+ console.error(colors.yellow(` Warning: could not update npmchanges.md: ${err.message}`));
4878
+ }
4879
+ }
4704
4880
  }
4705
4881
  else {
4706
4882
  console.log(` [dry-run] Would run: npm publish ${quiet ? '--quiet' : ''}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.160",
3
+ "version": "1.0.162",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",