@bobfrankston/npmglobalize 1.0.115 → 1.0.117
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 +19 -12
- package/cli.js +10 -0
- package/lib.d.ts +2 -0
- package/lib.js +102 -16
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -127,6 +127,16 @@ npmglobalize --fix # Runs npm audit fix
|
|
|
127
127
|
npmglobalize --no-fix
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### 🔑 OAuth Credentials Handling
|
|
131
|
+
|
|
132
|
+
`npmglobalize` automatically detects `credentials.json` files and handles them based on OAuth app type:
|
|
133
|
+
|
|
134
|
+
- **Desktop/installed apps** (`"installed"` key in JSON): The `client_secret` is just a public app registration ID, not a real secret. Google's own docs state: *"the client_secret is obviously not treated as a secret."* These files are kept in the repo — `!credentials.json` is added to `.gitignore`/`.npmignore` to override any broader ignore patterns.
|
|
135
|
+
|
|
136
|
+
- **Web apps** (`"web"` key in JSON): The `client_secret` is a real secret. These files are automatically added to `.gitignore`/`.npmignore` to prevent accidental exposure.
|
|
137
|
+
|
|
138
|
+
This distinction also drives the **push protection auto-bypass**: when GitHub blocks a push because it detects an OAuth client secret, `npmglobalize` checks the credential file type. For installed apps, it auto-bypasses (marking as `false_positive`) since the credential is public by design.
|
|
139
|
+
|
|
130
140
|
### 🔄 File Reference Management
|
|
131
141
|
|
|
132
142
|
**Default behavior** (restore file: references after publish):
|
|
@@ -409,22 +419,19 @@ npmglobalize --dry-run # See what would happen
|
|
|
409
419
|
|
|
410
420
|
## Operational Details
|
|
411
421
|
|
|
412
|
-
### The `.dependencies` Backup
|
|
422
|
+
### The `.dependencies` Backup (Internal/Transient)
|
|
413
423
|
|
|
414
|
-
|
|
424
|
+
**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.
|
|
415
425
|
|
|
416
|
-
|
|
417
|
-
- Before transforming, the original `file:` entries are copied to `.dependencies`
|
|
418
|
-
- After a successful publish, the originals are restored from `.dependencies` and the backup is removed
|
|
419
|
-
- If the process crashes, is interrupted, or exits early (e.g., private package, `--nopublish`), `.dependencies` remains in `package.json`
|
|
420
|
-
- On the **next run**, `transformDeps` detects `.dependencies`, restores the originals first, then re-transforms — so data is never lost
|
|
426
|
+
During publishing, `npmglobalize` temporarily replaces `file:` references with npm version strings. The original `file:` entries are stashed in `.dependencies` (and `.devDependencies`, etc.) so they can be restored afterward. Once the publish succeeds and `file:` paths are restored, `.dependencies` is deleted. A normal run leaves no trace of it.
|
|
421
427
|
|
|
422
|
-
**
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
428
|
+
**If you see `.dependencies` in your `package.json`, something went wrong** — the tool crashed, was killed, or the publish failed partway through. It is not a feature to rely on or edit manually.
|
|
429
|
+
|
|
430
|
+
**Recovery:**
|
|
431
|
+
- **Re-run `npmglobalize`**: It detects leftover `.dependencies`, restores the originals, and continues normally. Self-healing is automatic.
|
|
432
|
+
- **Manual restore**: `npmglobalize -cleanup` restores file: deps and removes the `.dependencies` backup.
|
|
426
433
|
|
|
427
|
-
**Why
|
|
434
|
+
**Why a persistent backup?** If the tool crashes hard (killed process, power failure, npm timeout), there's no cleanup code to run. The backup in `package.json` survives because it was written before the risky operations began. The next run self-heals.
|
|
428
435
|
|
|
429
436
|
### Flag Conventions
|
|
430
437
|
|
package/cli.js
CHANGED
|
@@ -42,6 +42,8 @@ Install Options:
|
|
|
42
42
|
-wsl Also install globally in WSL
|
|
43
43
|
-freeze Freeze node_modules (replace symlinks with real copies for network shares)
|
|
44
44
|
-nofreeze Disable freeze
|
|
45
|
+
-importgen Run importgen to update import maps before publishing
|
|
46
|
+
-noimportgen Disable importgen
|
|
45
47
|
-once Don't persist flags to .globalize.json5
|
|
46
48
|
|
|
47
49
|
Mode Options:
|
|
@@ -297,6 +299,14 @@ function parseArgs(args) {
|
|
|
297
299
|
case '-once':
|
|
298
300
|
options.once = true;
|
|
299
301
|
break;
|
|
302
|
+
case '-importgen':
|
|
303
|
+
options.importgen = true;
|
|
304
|
+
options.explicitKeys.add('importgen');
|
|
305
|
+
break;
|
|
306
|
+
case '-noimportgen':
|
|
307
|
+
options.importgen = false;
|
|
308
|
+
options.explicitKeys.add('importgen');
|
|
309
|
+
break;
|
|
300
310
|
default:
|
|
301
311
|
if (arg.startsWith('-')) {
|
|
302
312
|
unrecognized.push(arg);
|
package/lib.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface GlobalizeOptions {
|
|
|
72
72
|
package?: boolean;
|
|
73
73
|
/** Don't persist CLI flags to .globalize.json5 */
|
|
74
74
|
once?: boolean;
|
|
75
|
+
/** Run importgen to update import maps before publishing */
|
|
76
|
+
importgen?: boolean;
|
|
75
77
|
/** Local install only — skip transform/publish, just npm install -g . */
|
|
76
78
|
local?: boolean;
|
|
77
79
|
/** Freeze node_modules: replace symlinks/junctions with real copies for network share use */
|
package/lib.js
CHANGED
|
@@ -14,6 +14,7 @@ import path from 'path';
|
|
|
14
14
|
import { execSync, spawnSync } from 'child_process';
|
|
15
15
|
import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir } from '@bobfrankston/userconfig';
|
|
16
16
|
import { freezeDependencies } from '@bobfrankston/freezepak';
|
|
17
|
+
import { importgen as runImportgen } from '@bobfrankston/importgen';
|
|
17
18
|
/** Wrapper for spawnSync that avoids DEP0190 (args + shell: true).
|
|
18
19
|
* When shell is true, joins cmd+args into a single command string. */
|
|
19
20
|
function spawnSafe(cmd, args, options = {}) {
|
|
@@ -1440,30 +1441,51 @@ function parsePushProtection(errorOutput, cwd) {
|
|
|
1440
1441
|
return result;
|
|
1441
1442
|
}
|
|
1442
1443
|
result.detected = true;
|
|
1443
|
-
//
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1444
|
+
// Match secret blocks using a single regex across any dash characters
|
|
1445
|
+
// Pattern: "-- Type Name ---..." then "path: file:line" then "unblock-secret/id" URL
|
|
1446
|
+
// Handles em-dash (—), en-dash (–), and regular dash (-)
|
|
1447
|
+
const secretRegex = /[-\u2014\u2013]{2,}\s+(.+?)\s+[-\u2014\u2013]{2,}[\s\S]*?path:\s+(\S+?)(?::\d+)?\s[\s\S]*?(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/g;
|
|
1448
|
+
let match;
|
|
1449
|
+
while ((match = secretRegex.exec(cleaned)) !== null) {
|
|
1450
|
+
result.secrets.push({
|
|
1451
|
+
type: match[1].trim(),
|
|
1452
|
+
file: match[2].trim(),
|
|
1453
|
+
unblockUrl: match[3].trim()
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
// If regex didn't match (encoding issues), fall back to finding unblock URLs + paths
|
|
1457
|
+
if (result.secrets.length === 0) {
|
|
1458
|
+
const urlRegex = /(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/g;
|
|
1459
|
+
const pathRegex = /path:\s+(\S+?)(?::\d+)?\s/g;
|
|
1460
|
+
const urls = [];
|
|
1461
|
+
const files = [];
|
|
1462
|
+
let m;
|
|
1463
|
+
while ((m = urlRegex.exec(cleaned)) !== null)
|
|
1464
|
+
urls.push(m[1].trim());
|
|
1465
|
+
while ((m = pathRegex.exec(cleaned)) !== null)
|
|
1466
|
+
files.push(m[1].trim());
|
|
1467
|
+
// Try to detect secret type from text
|
|
1468
|
+
const hasOAuth = cleaned.toLowerCase().includes('oauth');
|
|
1469
|
+
const secretType = hasOAuth ? 'Google OAuth Credential' : 'Secret';
|
|
1470
|
+
for (let i = 0; i < Math.max(urls.length, files.length); i++) {
|
|
1471
|
+
result.secrets.push({
|
|
1472
|
+
type: secretType,
|
|
1473
|
+
file: files[i] || '',
|
|
1474
|
+
unblockUrl: urls[i] || ''
|
|
1475
|
+
});
|
|
1456
1476
|
}
|
|
1457
1477
|
}
|
|
1458
1478
|
// Check if all detected secrets are from Google OAuth installed apps (safe to include)
|
|
1459
1479
|
if (result.secrets.length > 0) {
|
|
1460
1480
|
result.allInstalledOAuth = result.secrets.every(s => {
|
|
1461
|
-
if (!s.type.toLowerCase().includes('oauth'))
|
|
1462
|
-
return false;
|
|
1463
1481
|
if (!s.file)
|
|
1464
1482
|
return false;
|
|
1483
|
+
// Check by type name OR by inspecting the actual credential file
|
|
1465
1484
|
const credType = detectCredentialsTypeFromFile(path.join(cwd, s.file));
|
|
1466
|
-
|
|
1485
|
+
if (credType === 'installed')
|
|
1486
|
+
return true;
|
|
1487
|
+
// Also match if the type name mentions OAuth (even if file check failed)
|
|
1488
|
+
return s.type.toLowerCase().includes('oauth') && credType !== 'web';
|
|
1467
1489
|
});
|
|
1468
1490
|
}
|
|
1469
1491
|
return result;
|
|
@@ -2534,6 +2556,27 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2534
2556
|
console.log(' [dry-run] Would run: npm run build');
|
|
2535
2557
|
}
|
|
2536
2558
|
}
|
|
2559
|
+
// Run importgen if enabled (update import maps in HTML files)
|
|
2560
|
+
if (options.importgen) {
|
|
2561
|
+
try {
|
|
2562
|
+
if (!dryRun) {
|
|
2563
|
+
const igResult = runImportgen(cwd);
|
|
2564
|
+
console.log(colors.green('✓ importgen updated import maps'));
|
|
2565
|
+
if (verbose && igResult.depDirs.length > 0) {
|
|
2566
|
+
console.log(` ${igResult.depDirs.length} dependency dirs resolved`);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
else {
|
|
2570
|
+
console.log(' [dry-run] Would run: importgen');
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
catch (error) {
|
|
2574
|
+
// importgen throws if no HTML file found — that's fine, just skip
|
|
2575
|
+
if (verbose) {
|
|
2576
|
+
console.log(colors.dim(` importgen skipped: ${error.message}`));
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2537
2580
|
// Pre-flight check: fix version/tag mismatches silently
|
|
2538
2581
|
if (!dryRun) {
|
|
2539
2582
|
fixVersionTagMismatch(cwd, pkg, verbose);
|
|
@@ -3823,6 +3866,49 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
3823
3866
|
console.log(`Packages (${packages.length}): ${packages.map(p => p.name).join(', ')}`);
|
|
3824
3867
|
console.log(`Publish order: ${publishOrder.join(' → ')}`);
|
|
3825
3868
|
console.log('');
|
|
3869
|
+
// Check that workspace packages' deps are hoisted to root (needed for global install)
|
|
3870
|
+
const rootPkg = readPackageJson(rootDir);
|
|
3871
|
+
if (rootPkg.bin) {
|
|
3872
|
+
const wsNames = new Set(packages.map(p => p.name));
|
|
3873
|
+
const rootDeps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
|
|
3874
|
+
const missing = [];
|
|
3875
|
+
for (const pkgInfo of packages) {
|
|
3876
|
+
const deps = pkgInfo.pkg.dependencies || {};
|
|
3877
|
+
for (const [dep, ver] of Object.entries(deps)) {
|
|
3878
|
+
if (wsNames.has(dep))
|
|
3879
|
+
continue; // sibling workspace package — bundled
|
|
3880
|
+
if (dep in rootDeps)
|
|
3881
|
+
continue; // already in root
|
|
3882
|
+
if (missing.some(m => m.dep === dep))
|
|
3883
|
+
continue; // already flagged
|
|
3884
|
+
missing.push({ dep, version: ver, from: pkgInfo.name });
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
if (missing.length > 0) {
|
|
3888
|
+
console.log(colors.yellow('⚠ Workspace packages have dependencies not in root package.json:'));
|
|
3889
|
+
for (const m of missing) {
|
|
3890
|
+
console.log(colors.yellow(` ${m.dep} (${m.version}) — from ${m.from}`));
|
|
3891
|
+
}
|
|
3892
|
+
console.log(colors.dim(' Global install (npm install -g) only installs root deps.'));
|
|
3893
|
+
if (!options.dryRun) {
|
|
3894
|
+
const hoist = await confirm('Add missing deps to root package.json?', true);
|
|
3895
|
+
if (hoist) {
|
|
3896
|
+
if (!rootPkg.dependencies)
|
|
3897
|
+
rootPkg.dependencies = {};
|
|
3898
|
+
for (const m of missing) {
|
|
3899
|
+
rootPkg.dependencies[m.dep] = m.version;
|
|
3900
|
+
console.log(colors.green(` + ${m.dep}: ${m.version}`));
|
|
3901
|
+
}
|
|
3902
|
+
writePackageJson(rootDir, rootPkg);
|
|
3903
|
+
console.log(colors.green('✓ Hoisted workspace deps to root package.json'));
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
else {
|
|
3907
|
+
console.log(colors.dim(' [dry-run] Would offer to hoist missing deps'));
|
|
3908
|
+
}
|
|
3909
|
+
console.log('');
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3826
3912
|
// Handle --cleanup: restore deps for all packages and return
|
|
3827
3913
|
if (options.cleanup) {
|
|
3828
3914
|
console.log('Restoring workspace dependencies...');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/npmglobalize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.117",
|
|
4
4
|
"description": "Transform file: dependencies to npm versions for publishing",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@bobfrankston/freezepak": "^0.1.4",
|
|
35
|
+
"@bobfrankston/importgen": "^0.1.31",
|
|
35
36
|
"@bobfrankston/userconfig": "^1.0.4",
|
|
36
37
|
"@npmcli/package-json": "^7.0.4",
|
|
37
38
|
"json5": "^2.2.3",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
},
|
|
46
47
|
".dependencies": {
|
|
47
48
|
"@bobfrankston/freezepak": "file:../freezepak",
|
|
49
|
+
"@bobfrankston/importgen": "file:../importgen",
|
|
48
50
|
"@bobfrankston/userconfig": "file:../userconfig",
|
|
49
51
|
"@npmcli/package-json": "^7.0.4",
|
|
50
52
|
"json5": "^2.2.3",
|