@bobfrankston/npmglobalize 1.0.179 → 1.0.180
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 +7 -0
- package/cli.js +8 -0
- package/lib.d.ts +8 -0
- package/lib.js +161 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -427,6 +427,13 @@ Workspace mode is auto-detected when run from a root with `"private": true` and
|
|
|
427
427
|
history instead of creating a fresh repo.
|
|
428
428
|
-adopt Strict adopt: require a reachable git remote in
|
|
429
429
|
package.json.repository. Abort if probe fails. Skips prompt.
|
|
430
|
+
-no-import-check, -no-imports
|
|
431
|
+
Skip the undeclared-imports scan. By default, npmglobalize
|
|
432
|
+
scans .ts/.js source for imports of packages not declared in
|
|
433
|
+
any dependencies bucket and warns before publishing. The scan
|
|
434
|
+
catches silent runtime failures (ERR_MODULE_NOT_FOUND on a
|
|
435
|
+
clean install) where the import was resolving via a global or
|
|
436
|
+
parent node_modules at dev time but won't on the target host.
|
|
430
437
|
-force Continue despite git errors
|
|
431
438
|
-dry-run Preview what would happen
|
|
432
439
|
-quiet Suppress npm warnings (default)
|
package/cli.js
CHANGED
|
@@ -86,6 +86,10 @@ Other Options:
|
|
|
86
86
|
in package.json.repository if reachable, else fresh init)
|
|
87
87
|
-adopt Strict adopt: require a reachable git remote in
|
|
88
88
|
package.json.repository. Abort if probe fails. Skips prompt.
|
|
89
|
+
-no-import-check
|
|
90
|
+
Skip the undeclared-imports scan. Default: scan runs and
|
|
91
|
+
warns about packages imported in .ts/.js but missing from
|
|
92
|
+
any dependencies bucket (catches silent runtime failures).
|
|
89
93
|
-force Continue despite git errors
|
|
90
94
|
-dry-run Preview what would happen
|
|
91
95
|
-quiet Suppress npm warnings (default)
|
|
@@ -229,6 +233,10 @@ function parseArgs(args) {
|
|
|
229
233
|
case '-adopt':
|
|
230
234
|
options.adopt = true;
|
|
231
235
|
break;
|
|
236
|
+
case '-no-import-check':
|
|
237
|
+
case '-no-imports':
|
|
238
|
+
options.noImportCheck = true;
|
|
239
|
+
break;
|
|
232
240
|
case '-git':
|
|
233
241
|
i++;
|
|
234
242
|
if (args[i] === 'private' || args[i] === 'public') {
|
package/lib.d.ts
CHANGED
|
@@ -119,6 +119,8 @@ export interface GlobalizeOptions {
|
|
|
119
119
|
* package.json.repository. Aborts (rather than creating a fresh repo) if
|
|
120
120
|
* the probe fails. Skips the no-git prompt. */
|
|
121
121
|
adopt?: boolean;
|
|
122
|
+
/** Skip the undeclared-imports scan. Default false (scan runs). */
|
|
123
|
+
noImportCheck?: boolean;
|
|
122
124
|
/** Internal: signals this call is from workspace orchestrator */
|
|
123
125
|
/** Skip the upfront dep-graph prescan */
|
|
124
126
|
noPrescan?: boolean;
|
|
@@ -260,6 +262,12 @@ export interface PrescanIssue {
|
|
|
260
262
|
message: string;
|
|
261
263
|
suggestion?: string;
|
|
262
264
|
}
|
|
265
|
+
export interface UndeclaredImport {
|
|
266
|
+
name: string;
|
|
267
|
+
file: string;
|
|
268
|
+
line: number;
|
|
269
|
+
}
|
|
270
|
+
export declare function findUndeclaredImports(cwd: string, pkg: any): UndeclaredImport[];
|
|
263
271
|
/** Walk the full file: dep graph and collect issues up front — missing scopes,
|
|
264
272
|
* unresolvable paths, missing package.json, unpublished transitives. Lets the
|
|
265
273
|
* user resolve all problems before starting the publish cascade instead of
|
package/lib.js
CHANGED
|
@@ -13,6 +13,7 @@ import fs from 'fs';
|
|
|
13
13
|
import os from 'os';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { execSync, spawn, spawnSync } from 'child_process';
|
|
16
|
+
import { builtinModules } from 'module';
|
|
16
17
|
import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir } from '@bobfrankston/userconfig';
|
|
17
18
|
import { freezeDependencies } from '@bobfrankston/freezepak';
|
|
18
19
|
import { importgen as runImportgen } from '@bobfrankston/importgen';
|
|
@@ -1304,6 +1305,130 @@ function printDepTree(baseDir, indent = 0, visited = new Set()) {
|
|
|
1304
1305
|
}
|
|
1305
1306
|
}
|
|
1306
1307
|
}
|
|
1308
|
+
/** Source-import scan: finds package imports in .ts/.js that aren't declared
|
|
1309
|
+
* in any of the package.json dep buckets. Catches the failure mode where a
|
|
1310
|
+
* file imports `@scope/thing` but no one ever added it to dependencies — the
|
|
1311
|
+
* tarball publishes "fine" and crashes with ERR_MODULE_NOT_FOUND on install.
|
|
1312
|
+
* The previously-working case usually relies on a global/parent copy that
|
|
1313
|
+
* Node ESM resolution happened to find. */
|
|
1314
|
+
const SOURCE_SKIP_DIRS = new Set([
|
|
1315
|
+
'node_modules', 'prev', '.git', 'dist', 'build', 'out',
|
|
1316
|
+
'coverage', '.next', '.nuxt', '.turbo', '.cache'
|
|
1317
|
+
]);
|
|
1318
|
+
function collectSourceFiles(dir, acc = [], depth = 0) {
|
|
1319
|
+
if (depth > 12)
|
|
1320
|
+
return acc;
|
|
1321
|
+
let entries;
|
|
1322
|
+
try {
|
|
1323
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
return acc;
|
|
1327
|
+
}
|
|
1328
|
+
for (const ent of entries) {
|
|
1329
|
+
const name = ent.name;
|
|
1330
|
+
if (ent.isDirectory()) {
|
|
1331
|
+
if (SOURCE_SKIP_DIRS.has(name))
|
|
1332
|
+
continue;
|
|
1333
|
+
if (name.startsWith('.'))
|
|
1334
|
+
continue; // .git, .vscode, .idea, etc.
|
|
1335
|
+
collectSourceFiles(path.join(dir, name), acc, depth + 1);
|
|
1336
|
+
}
|
|
1337
|
+
else if (ent.isFile()) {
|
|
1338
|
+
if (name.endsWith('.d.ts') || name.endsWith('.map'))
|
|
1339
|
+
continue;
|
|
1340
|
+
if (name.endsWith('.ts') || name.endsWith('.tsx')
|
|
1341
|
+
|| name.endsWith('.js') || name.endsWith('.jsx')
|
|
1342
|
+
|| name.endsWith('.mjs') || name.endsWith('.cjs')) {
|
|
1343
|
+
acc.push(path.join(dir, name));
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return acc;
|
|
1348
|
+
}
|
|
1349
|
+
/** Extract the package name from an ESM/CJS module specifier.
|
|
1350
|
+
* Returns null for relative paths, absolute paths, URLs, node: builtins. */
|
|
1351
|
+
function specifierToPackageName(spec) {
|
|
1352
|
+
if (!spec)
|
|
1353
|
+
return null;
|
|
1354
|
+
if (spec.startsWith('.'))
|
|
1355
|
+
return null; // ./ or ../
|
|
1356
|
+
if (spec.startsWith('/'))
|
|
1357
|
+
return null; // /abs
|
|
1358
|
+
if (spec.startsWith('node:'))
|
|
1359
|
+
return null; // node:fs
|
|
1360
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(spec))
|
|
1361
|
+
return null; // url/data
|
|
1362
|
+
if (spec.startsWith('#'))
|
|
1363
|
+
return null; // subpath import
|
|
1364
|
+
if (spec.startsWith('@')) {
|
|
1365
|
+
const parts = spec.split('/');
|
|
1366
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
1367
|
+
}
|
|
1368
|
+
return spec.split('/')[0];
|
|
1369
|
+
}
|
|
1370
|
+
const NODE_BUILTIN_NAMES = new Set(builtinModules);
|
|
1371
|
+
export function findUndeclaredImports(cwd, pkg) {
|
|
1372
|
+
const declared = new Set();
|
|
1373
|
+
for (const k of DEP_KEYS) {
|
|
1374
|
+
const bucket = pkg[k] || pkg['.' + k]; // also consult the backup bucket if a transform is pending
|
|
1375
|
+
if (bucket)
|
|
1376
|
+
for (const n of Object.keys(bucket))
|
|
1377
|
+
declared.add(n);
|
|
1378
|
+
}
|
|
1379
|
+
const selfName = pkg.name;
|
|
1380
|
+
// One specifier per line is the common case; a few patterns cover the rest.
|
|
1381
|
+
// We process line-by-line so we can report file:line and avoid pathological
|
|
1382
|
+
// backtracking on huge files.
|
|
1383
|
+
const FROM_RE = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
1384
|
+
const BARE_IMPORT_RE = /(?:^|;|\s)import\s+['"]([^'"]+)['"]/g;
|
|
1385
|
+
const DYNAMIC_RE = /(?:^|[^.\w$])import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1386
|
+
const REQUIRE_RE = /(?:^|[^.\w$])require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1387
|
+
const PATTERNS = [FROM_RE, BARE_IMPORT_RE, DYNAMIC_RE, REQUIRE_RE];
|
|
1388
|
+
const findings = new Map();
|
|
1389
|
+
const files = collectSourceFiles(cwd);
|
|
1390
|
+
for (const f of files) {
|
|
1391
|
+
let content;
|
|
1392
|
+
try {
|
|
1393
|
+
content = fs.readFileSync(f, 'utf-8');
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
// Quick reject: no import/require keyword anywhere → skip
|
|
1399
|
+
if (!/\bimport\b|\brequire\s*\(/.test(content))
|
|
1400
|
+
continue;
|
|
1401
|
+
const lines = content.split('\n');
|
|
1402
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1403
|
+
const line = lines[i];
|
|
1404
|
+
for (const re of PATTERNS) {
|
|
1405
|
+
re.lastIndex = 0;
|
|
1406
|
+
let m;
|
|
1407
|
+
while ((m = re.exec(line))) {
|
|
1408
|
+
const spec = m[1];
|
|
1409
|
+
const pkgName = specifierToPackageName(spec);
|
|
1410
|
+
if (!pkgName)
|
|
1411
|
+
continue;
|
|
1412
|
+
if (NODE_BUILTIN_NAMES.has(pkgName))
|
|
1413
|
+
continue;
|
|
1414
|
+
if (pkgName === selfName)
|
|
1415
|
+
continue;
|
|
1416
|
+
if (declared.has(pkgName))
|
|
1417
|
+
continue;
|
|
1418
|
+
if (!findings.has(pkgName)) {
|
|
1419
|
+
findings.set(pkgName, {
|
|
1420
|
+
file: path.relative(cwd, f).replace(/\\/g, '/'),
|
|
1421
|
+
line: i + 1
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return [...findings.entries()]
|
|
1429
|
+
.map(([name, loc]) => ({ name, ...loc }))
|
|
1430
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1431
|
+
}
|
|
1307
1432
|
/** Walk the full file: dep graph and collect issues up front — missing scopes,
|
|
1308
1433
|
* unresolvable paths, missing package.json, unpublished transitives. Lets the
|
|
1309
1434
|
* user resolve all problems before starting the publish cascade instead of
|
|
@@ -3411,7 +3536,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3411
3536
|
const { bump = 'patch', noPublish = false, cleanup = false, install = false, link = false, wsl = false, force = false, files = true, dryRun = false, quiet = true, verbose = false, init = false, gitVisibility = 'private', npmVisibility = 'private', message, conform = false, asis = false, updateDeps = false, updateMajor = false, publishDeps = true, // Default to publishing deps for safety
|
|
3412
3537
|
publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
|
|
3413
3538
|
publicDeps = false, // -public-deps: cascade public visibility to all deps
|
|
3414
|
-
noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false } = options;
|
|
3539
|
+
noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false, noImportCheck = false } = options;
|
|
3415
3540
|
// Show tool version only for recursive dep calls (CLI already prints it at startup)
|
|
3416
3541
|
const toolVersion = getToolVersion();
|
|
3417
3542
|
if (!options._fromWorkspace && !options._fromCli) {
|
|
@@ -3554,6 +3679,41 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3554
3679
|
console.log(colors.dim('Prescan: no issues found in dep graph.'));
|
|
3555
3680
|
}
|
|
3556
3681
|
}
|
|
3682
|
+
// Source-import scan — catches imports of packages not declared in
|
|
3683
|
+
// dependencies/devDependencies/peer/optional. Runs by default; skip with
|
|
3684
|
+
// -no-import-check. Only at the top level (deps in the cascade will be
|
|
3685
|
+
// scanned when their own globalize() runs).
|
|
3686
|
+
if (!noImportCheck && !cleanup && !options._fromDep && !options._fromWorkspace) {
|
|
3687
|
+
const scanPkg = readPackageJson(cwd);
|
|
3688
|
+
const undeclared = findUndeclaredImports(cwd, scanPkg);
|
|
3689
|
+
if (undeclared.length > 0) {
|
|
3690
|
+
console.log('');
|
|
3691
|
+
console.log(colors.yellow(`Undeclared imports found (${undeclared.length}):`));
|
|
3692
|
+
for (const u of undeclared) {
|
|
3693
|
+
console.log(colors.yellow(` ${u.name}`)
|
|
3694
|
+
+ colors.dim(` (${u.file}:${u.line})`));
|
|
3695
|
+
}
|
|
3696
|
+
console.log(colors.dim(' These packages are imported but not in any package.json dep bucket.'));
|
|
3697
|
+
console.log(colors.dim(' After publish, `npm install` won\'t pull them and the package will'));
|
|
3698
|
+
console.log(colors.dim(' crash with ERR_MODULE_NOT_FOUND on a clean install.'));
|
|
3699
|
+
const interactive = !init && !options.autoInit && !publishDepsYes && !dryRun;
|
|
3700
|
+
if (interactive) {
|
|
3701
|
+
const ok = await confirm('Continue anyway?', false);
|
|
3702
|
+
if (!ok) {
|
|
3703
|
+
console.log('Aborted. Add the missing packages to dependencies and re-run.');
|
|
3704
|
+
console.log(colors.dim(' Tip: for siblings, use "file:../<dir>"; otherwise add the latest published version.'));
|
|
3705
|
+
return false;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
else {
|
|
3709
|
+
console.log(colors.yellow(' (non-interactive mode — continuing, but the install will likely fail at runtime)'));
|
|
3710
|
+
}
|
|
3711
|
+
console.log('');
|
|
3712
|
+
}
|
|
3713
|
+
else if (verbose) {
|
|
3714
|
+
console.log(colors.dim('Import scan: all imports are declared.'));
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3557
3717
|
// Check ignore files first (unless cleanup mode)
|
|
3558
3718
|
if (!cleanup && !asis) {
|
|
3559
3719
|
const checkResult = checkIgnoreFiles(cwd, { conform, asis, verbose, allowTs });
|