@bobfrankston/npmglobalize 1.0.142 → 1.0.144

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/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * npmglobalize CLI - Transform file: dependencies to npm versions for publishing
4
4
  */
5
- import { globalize, globalizeWorkspace, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, confirm, getBuildIssues, clearBuildIssues } from './lib.js';
5
+ import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, confirm, getBuildIssues, clearBuildIssues, recordBuildIssue, extractFirstTscError } from './lib.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { styleText } from 'util';
@@ -343,6 +343,7 @@ function printBuildSummary() {
343
343
  console.log('');
344
344
  }
345
345
  export async function main() {
346
+ installCleanupHandlers();
346
347
  // Show version at the very start
347
348
  const ownPkgPath = path.join(__dirname, 'package.json');
348
349
  const ownPkg = JSON.parse(fs.readFileSync(ownPkgPath, 'utf-8'));
@@ -402,15 +403,23 @@ export async function main() {
402
403
  }
403
404
  }
404
405
  if (pkg.scripts?.build) {
405
- const { execSync } = await import('child_process');
406
- try {
407
- console.log(`Building ${cwd}...`);
408
- execSync('npm run build', { cwd, stdio: 'inherit' });
406
+ const { spawnSync } = await import('child_process');
407
+ console.log(`Building ${cwd}...`);
408
+ const buildResult = spawnSync('npm', ['run', 'build'], {
409
+ cwd, encoding: 'utf-8', stdio: 'pipe', shell: true
410
+ });
411
+ if (buildResult.status === 0) {
409
412
  console.log(styleText('green', '✓ Build succeeded'));
410
413
  }
411
- catch (error) {
414
+ else {
415
+ const buildOutput = (buildResult.stderr || '') + (buildResult.stdout || '');
416
+ if (buildOutput)
417
+ console.error(buildOutput);
412
418
  console.error(styleText('red', `Build failed in ${cwd}`));
419
+ const firstErr = extractFirstTscError(buildOutput);
420
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', firstErr || 'Build failed');
413
421
  if (!cliOptions.force) {
422
+ printBuildSummary();
414
423
  process.exit(1);
415
424
  }
416
425
  console.log(styleText('yellow', 'Continuing with --force...'));
@@ -15,7 +15,8 @@
15
15
  "*.pfx",
16
16
  "token",
17
17
  "tokens",
18
- "*.token"
18
+ "*.token",
19
+ "*.tgz"
19
20
  ],
20
21
  // Prompted or auto-added with --conform
21
22
  recommended: [
package/lib.d.ts CHANGED
@@ -22,6 +22,9 @@ export declare function recordBuildIssue(module: string, severity: 'error' | 'wa
22
22
  export declare function getBuildIssues(): readonly BuildIssue[];
23
23
  /** Clear accumulated issues (call at start of run) */
24
24
  export declare function clearBuildIssues(): void;
25
+ /** Extract the first TypeScript error line from build output for the summary.
26
+ * Returns a short string like "file.ts(42,5): error TS2339: Property 'foo' ..." */
27
+ export declare function extractFirstTscError(output: string): string | null;
25
28
  /** Options for the globalize operation */
26
29
  export interface GlobalizeOptions {
27
30
  /** Bump type: patch (default), minor, major */
@@ -129,6 +132,8 @@ export declare function readConfig(dir: string): Partial<GlobalizeOptions>;
129
132
  export declare function writeConfig(dir: string, config: Partial<GlobalizeOptions>, explicitKeys?: Set<string>): void;
130
133
  /** Write package.json to a directory */
131
134
  export declare function writePackageJson(dir: string, pkg: any): void;
135
+ /** Install signal/exit handlers that restore `.dependencies` backups on abnormal exit. */
136
+ export declare function installCleanupHandlers(): void;
132
137
  /** Resolve a file: path to absolute path */
133
138
  export declare function resolveFilePath(fileRef: string, baseDir: string): string;
134
139
  /** Check if a dependency value is a file: reference */
@@ -185,7 +190,8 @@ export declare function transformDeps(pkg: any, baseDir: string, verbose?: boole
185
190
  path: string;
186
191
  }>;
187
192
  };
188
- /** Restore file: dependencies from .dependencies */
193
+ /** Restore file: dependencies from .dependencies using a three-way merge that
194
+ * preserves any external modifications made since the last transform. */
189
195
  export declare function restoreDeps(pkg: any, verbose?: boolean): boolean;
190
196
  /** Check if .dependencies exist (already transformed) */
191
197
  export declare function hasBackup(pkg: any): boolean;
@@ -265,6 +271,7 @@ declare const _default: {
265
271
  validatePackageJson: typeof validatePackageJson;
266
272
  confirm: typeof confirm;
267
273
  initGit: typeof initGit;
274
+ installCleanupHandlers: typeof installCleanupHandlers;
268
275
  };
269
276
  export default _default;
270
277
  //# sourceMappingURL=lib.d.ts.map
package/lib.js CHANGED
@@ -46,6 +46,25 @@ export function getBuildIssues() {
46
46
  export function clearBuildIssues() {
47
47
  _buildIssues.length = 0;
48
48
  }
49
+ /** Extract the first TypeScript error line from build output for the summary.
50
+ * Returns a short string like "file.ts(42,5): error TS2339: Property 'foo' ..." */
51
+ export function extractFirstTscError(output) {
52
+ if (!output)
53
+ return null;
54
+ // tsc errors: src/file.ts(line,col): error TS1234: message
55
+ const tscMatch = output.match(/^(.+?\(\d+,\d+\): error TS\d+: .+)$/m);
56
+ if (tscMatch) {
57
+ const line = tscMatch[1];
58
+ return line.length > 120 ? line.slice(0, 117) + '...' : line;
59
+ }
60
+ // Generic "error" line
61
+ const errMatch = output.match(/^(error .+)$/m);
62
+ if (errMatch) {
63
+ const line = errMatch[1];
64
+ return line.length > 120 ? line.slice(0, 117) + '...' : line;
65
+ }
66
+ return null;
67
+ }
49
68
  /**
50
69
  * Remove 'nul' files from a directory tree (Windows reserved name issue).
51
70
  * These files break git and npm on Windows. Uses \\?\ prefix to bypass name validation.
@@ -294,10 +313,111 @@ export function writeConfig(dir, config, explicitKeys) {
294
313
  lines.push('}');
295
314
  fs.writeFileSync(configPath, lines.join('\n') + '\n');
296
315
  }
316
+ /** Package.json paths with a live `.dependencies` backup on disk.
317
+ * Tracked so the exit handler can restore them if the process dies mid-run. */
318
+ const dirtyPackageJsons = new Set();
297
319
  /** Write package.json to a directory */
298
320
  export function writePackageJson(dir, pkg) {
299
321
  const pkgPath = path.join(dir, 'package.json');
300
322
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
323
+ if (hasBackup(pkg)) {
324
+ dirtyPackageJsons.add(pkgPath);
325
+ }
326
+ else {
327
+ dirtyPackageJsons.delete(pkgPath);
328
+ }
329
+ }
330
+ /** Three-way merge that restores `.dependencies` backup into `dependencies` while
331
+ * preserving external modifications made since the last transform.
332
+ *
333
+ * Uses `.transformedSnapshot` (written by transformDeps) to distinguish
334
+ * "untouched transform output" from "externally modified":
335
+ * - current[name] === snapshot[name] → untouched, restore to backup value
336
+ * - current[name] !== snapshot[name] → modified externally, keep current
337
+ * - name in backup but missing from current → removed externally, leave removed
338
+ * - name in current but not in backup → added externally, keep
339
+ *
340
+ * If no snapshot exists (legacy backup from before this mechanism), falls back to
341
+ * blind restore plus new-dep merge, matching the old behavior. */
342
+ function mergeRestore(pkg, verbose = false) {
343
+ const snapshot = pkg['.transformedSnapshot'];
344
+ let restored = false;
345
+ for (const key of DEP_KEYS) {
346
+ const dotKey = '.' + key;
347
+ if (!pkg[dotKey])
348
+ continue;
349
+ const backup = pkg[dotKey];
350
+ const current = pkg[key] || {};
351
+ const snap = snapshot?.[key];
352
+ const merged = { ...current };
353
+ for (const [name, backupValue] of Object.entries(backup)) {
354
+ if (!(name in current))
355
+ continue; // removed externally — leave removed
356
+ if (snap && name in snap) {
357
+ if (current[name] === snap[name]) {
358
+ merged[name] = backupValue; // untouched — restore
359
+ }
360
+ else if (verbose) {
361
+ console.log(colors.yellow(` Preserving externally-modified ${key}.${name}: ${current[name]} (transform wrote ${snap[name]})`));
362
+ }
363
+ }
364
+ else {
365
+ merged[name] = backupValue; // legacy: no snapshot, blind restore
366
+ }
367
+ }
368
+ pkg[key] = merged;
369
+ delete pkg[dotKey];
370
+ restored = true;
371
+ }
372
+ if (restored && snapshot) {
373
+ delete pkg['.transformedSnapshot'];
374
+ }
375
+ return restored;
376
+ }
377
+ /** Synchronous emergency restore — called from signal/exit handlers.
378
+ * Reads each tracked package.json, runs the three-way merge, writes it back. */
379
+ function emergencyRestoreDeps() {
380
+ if (dirtyPackageJsons.size === 0)
381
+ return;
382
+ console.error(colors.yellow(`\nRestoring file: dependencies in ${dirtyPackageJsons.size} package.json file(s)...`));
383
+ for (const pkgPath of dirtyPackageJsons) {
384
+ try {
385
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
386
+ if (mergeRestore(pkg)) {
387
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
388
+ console.error(colors.green(` ✓ ${pkgPath}`));
389
+ }
390
+ }
391
+ catch (err) {
392
+ console.error(colors.red(` ✗ ${pkgPath}: ${err.message}`));
393
+ }
394
+ }
395
+ dirtyPackageJsons.clear();
396
+ }
397
+ let cleanupHandlersInstalled = false;
398
+ /** Install signal/exit handlers that restore `.dependencies` backups on abnormal exit. */
399
+ export function installCleanupHandlers() {
400
+ if (cleanupHandlersInstalled)
401
+ return;
402
+ cleanupHandlersInstalled = true;
403
+ process.on('exit', emergencyRestoreDeps);
404
+ const signalHandler = (signal) => {
405
+ emergencyRestoreDeps();
406
+ process.exit(128 + (signal === 'SIGINT' ? 2 : signal === 'SIGTERM' ? 15 : 1));
407
+ };
408
+ process.on('SIGINT', signalHandler);
409
+ process.on('SIGTERM', signalHandler);
410
+ process.on('SIGHUP', signalHandler);
411
+ process.on('uncaughtException', (err) => {
412
+ console.error(colors.red('\nUncaught exception:'), err);
413
+ emergencyRestoreDeps();
414
+ process.exit(1);
415
+ });
416
+ process.on('unhandledRejection', (reason) => {
417
+ console.error(colors.red('\nUnhandled rejection:'), reason);
418
+ emergencyRestoreDeps();
419
+ process.exit(1);
420
+ });
301
421
  }
302
422
  /** Resolve a file: path to absolute path */
303
423
  export function resolveFilePath(fileRef, baseDir) {
@@ -747,36 +867,21 @@ function hasLocalChanges(packageName, version, targetPath, verbose) {
747
867
  export function transformDeps(pkg, baseDir, verbose = false, forcePublish = false) {
748
868
  let transformed = false;
749
869
  const unpublished = [];
870
+ // If re-running on a previously-transformed pkg, merge-restore first.
871
+ // This preserves any external modifications (AI tools, npm install, manual edits)
872
+ // made since the last transform, then proceeds to re-transform from a clean slate.
873
+ if (hasBackup(pkg)) {
874
+ mergeRestore(pkg, verbose);
875
+ }
750
876
  for (const key of DEP_KEYS) {
751
877
  if (!pkg[key])
752
878
  continue;
753
879
  const dotKey = '.' + key;
754
- // If .dependencies already exists, restore it and merge any new dependencies
755
- if (pkg[dotKey]) {
756
- // Save any new dependencies that aren't in .dependencies
757
- const currentDeps = { ...pkg[key] };
758
- // Restore .dependencies back to dependencies
759
- pkg[key] = { ...pkg[dotKey] };
760
- // Merge in any NEW dependencies that were added since transformation
761
- for (const [name, value] of Object.entries(currentDeps)) {
762
- if (!(name in pkg[dotKey])) {
763
- // This is a new dependency, add it
764
- pkg[key][name] = value;
765
- // Also add to .dependencies backup
766
- pkg[dotKey][name] = value;
767
- if (verbose) {
768
- console.log(` Merged new dependency: ${name}`);
769
- }
770
- }
771
- }
772
- }
773
880
  const hasFileRefs = Object.values(pkg[key]).some(v => isFileRef(v));
774
881
  if (!hasFileRefs)
775
882
  continue;
776
- // Backup original (or update existing backup with merged deps)
777
- if (!pkg[dotKey]) {
778
- pkg[dotKey] = { ...pkg[key] };
779
- }
883
+ // Backup original
884
+ pkg[dotKey] = { ...pkg[key] };
780
885
  // Transform file: refs to npm versions
781
886
  for (const [name, value] of Object.entries(pkg[key])) {
782
887
  if (isFileRef(value)) {
@@ -827,6 +932,17 @@ export function transformDeps(pkg, baseDir, verbose = false, forcePublish = fals
827
932
  }
828
933
  }
829
934
  }
935
+ // Record snapshot of transform output so restore can distinguish untouched
936
+ // entries from those modified externally since the transform ran.
937
+ if (transformed) {
938
+ const snapshot = {};
939
+ for (const key of DEP_KEYS) {
940
+ if (pkg['.' + key]) {
941
+ snapshot[key] = { ...pkg[key] };
942
+ }
943
+ }
944
+ pkg['.transformedSnapshot'] = snapshot;
945
+ }
830
946
  return { transformed, unpublished };
831
947
  }
832
948
  /** Build and print a dependency tree of file: references.
@@ -870,21 +986,10 @@ function printDepTree(baseDir, indent = 0, visited = new Set()) {
870
986
  }
871
987
  }
872
988
  }
873
- /** Restore file: dependencies from .dependencies */
989
+ /** Restore file: dependencies from .dependencies using a three-way merge that
990
+ * preserves any external modifications made since the last transform. */
874
991
  export function restoreDeps(pkg, verbose = false) {
875
- let restored = false;
876
- for (const key of DEP_KEYS) {
877
- const dotKey = '.' + key;
878
- if (pkg[dotKey]) {
879
- pkg[key] = pkg[dotKey];
880
- delete pkg[dotKey];
881
- restored = true;
882
- if (verbose) {
883
- console.log(` Restored ${key} from ${dotKey}`);
884
- }
885
- }
886
- }
887
- return restored;
992
+ return mergeRestore(pkg, verbose);
888
993
  }
889
994
  /** Check if .dependencies exist (already transformed) */
890
995
  export function hasBackup(pkg) {
@@ -2746,17 +2851,24 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2746
2851
  if (pkg.scripts?.build && !options._fromCli) {
2747
2852
  console.log(`${timestamp()} Running build...`);
2748
2853
  if (!dryRun) {
2749
- const buildResult = runCommand('npm', ['run', 'build'], { cwd, silent: !verbose });
2854
+ // Always capture output so we can extract tsc errors for the summary
2855
+ const buildResult = runCommand('npm', ['run', 'build'], { cwd, silent: true });
2750
2856
  if (!buildResult.success) {
2751
- console.error(colors.red('ERROR: Build failed:'), buildResult.stderr || buildResult.output);
2752
- diagnoseBuildFailure(buildResult.stderr || buildResult.output || '', cwd);
2753
- recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'Build failed');
2857
+ const buildOutput = buildResult.stderr || buildResult.output;
2858
+ if (buildOutput)
2859
+ console.error(buildOutput);
2860
+ console.error(colors.red('ERROR: Build failed'));
2861
+ diagnoseBuildFailure(buildOutput || '', cwd);
2862
+ const firstErr = extractFirstTscError(buildOutput || '');
2863
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', firstErr || 'Build failed');
2754
2864
  if (!force) {
2755
2865
  return false;
2756
2866
  }
2757
2867
  console.log(colors.yellow('Continuing with --force despite build failure...'));
2758
2868
  }
2759
2869
  else {
2870
+ if (verbose && buildResult.output)
2871
+ process.stdout.write(buildResult.output);
2760
2872
  console.log(`${timestamp()} ${colors.green('✓ Build succeeded')}`);
2761
2873
  }
2762
2874
  }
@@ -4000,25 +4112,20 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4000
4112
  }
4001
4113
  }
4002
4114
  catch (e) {
4003
- // Ignore cleanup errors
4115
+ console.error(colors.yellow(` Warning: could not delete tarball ${tarballName} — remove manually`));
4004
4116
  }
4005
4117
  if (!publishResult.success) {
4006
- console.error(colors.red('\nERROR: npm publish failed\n'));
4007
- recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm publish failed');
4008
- // Check for specific error types
4118
+ // Check for specific error types before recording
4009
4119
  const output = (publishResult.output + '\n' + publishResult.stderr).toLowerCase();
4010
- if (output.includes('err_string_too_long') || output.includes('string longer than')) {
4011
- console.error(colors.red('Tarball too large check .npmignore. Run: npm pack --dry-run'));
4012
- }
4013
- else if (output.includes('e409') || output.includes('409 conflict')) {
4014
- console.error(colors.yellow('npm still processing previous version — wait and retry'));
4015
- }
4016
- else if (output.includes('cannot publish over') || output.includes('previously published')) {
4120
+ // "Already published" is benign — don't record as error
4121
+ if (output.includes('cannot publish over') || output.includes('previously published')) {
4017
4122
  const currentPkgVersion = readPackageJson(cwd).version;
4018
4123
  if (output.includes(currentPkgVersion)) {
4019
4124
  console.log(colors.green('✓ Already published — continuing'));
4020
4125
  }
4021
4126
  else {
4127
+ console.error(colors.red('\nERROR: npm publish failed\n'));
4128
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm publish failed');
4022
4129
  console.error(colors.yellow('Version conflict — run npmglobalize again'));
4023
4130
  if (transformResult.transformed) {
4024
4131
  const failPkg = readPackageJson(cwd);
@@ -4030,7 +4137,19 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4030
4137
  return false;
4031
4138
  }
4032
4139
  }
4140
+ else if (output.includes('err_string_too_long') || output.includes('string longer than')) {
4141
+ console.error(colors.red('\nERROR: npm publish failed\n'));
4142
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm publish failed');
4143
+ console.error(colors.red('Tarball too large — check .npmignore. Run: npm pack --dry-run'));
4144
+ }
4145
+ else if (output.includes('e409') || output.includes('409 conflict')) {
4146
+ console.error(colors.red('\nERROR: npm publish failed\n'));
4147
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm publish failed');
4148
+ console.error(colors.yellow('npm still processing previous version — wait and retry'));
4149
+ }
4033
4150
  else if (output.includes('403') || output.includes('forbidden')) {
4151
+ console.error(colors.red('\nERROR: npm publish failed\n'));
4152
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm publish failed');
4034
4153
  console.error(colors.yellow('Publish forbidden — check npm login and package access'));
4035
4154
  if (transformResult.transformed) {
4036
4155
  const failPkg = readPackageJson(cwd);
@@ -4042,9 +4161,13 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4042
4161
  return false;
4043
4162
  }
4044
4163
  else if (output.includes('402') || output.includes('payment required')) {
4164
+ console.error(colors.red('\nERROR: npm publish failed\n'));
4165
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm publish failed');
4045
4166
  console.error(colors.yellow('Private packages need paid npm account — use --npm public'));
4046
4167
  }
4047
4168
  else {
4169
+ console.error(colors.red('\nERROR: npm publish failed\n'));
4170
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm publish failed');
4048
4171
  console.error(colors.yellow('Publish failed — run npm login or check npm whoami'));
4049
4172
  }
4050
4173
  if (transformResult.transformed) {
@@ -4526,6 +4649,7 @@ export default {
4526
4649
  getGitStatus,
4527
4650
  validatePackageJson,
4528
4651
  confirm,
4529
- initGit
4652
+ initGit,
4653
+ installCleanupHandlers
4530
4654
  };
4531
4655
  //# sourceMappingURL=lib.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.142",
3
+ "version": "1.0.144",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -56,5 +56,19 @@
56
56
  "npm-registry-fetch": "^19.1.1",
57
57
  "pacote": "^21.0.4",
58
58
  "simple-git": "^3.30.0"
59
+ },
60
+ ".transformedSnapshot": {
61
+ "dependencies": {
62
+ "@bobfrankston/freezepak": "^0.1.6",
63
+ "@bobfrankston/importgen": "^0.1.32",
64
+ "@bobfrankston/themecolors": "^0.1.4",
65
+ "@bobfrankston/userconfig": "^1.0.6",
66
+ "@npmcli/package-json": "^7.0.4",
67
+ "json5": "^2.2.3",
68
+ "libnpmversion": "^8.0.3",
69
+ "npm-registry-fetch": "^19.1.1",
70
+ "pacote": "^21.0.4",
71
+ "simple-git": "^3.30.0"
72
+ }
59
73
  }
60
74
  }