@bobfrankston/npmglobalize 1.0.179 → 1.0.181
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 +12 -0
- package/cli.js +10 -0
- package/lib.d.ts +12 -0
- package/lib.js +182 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -427,6 +427,18 @@ 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
|
+
-strict-imports, -import-check
|
|
431
|
+
Opt in to scanning .ts/.js source for imports of packages not
|
|
432
|
+
declared in any dependencies bucket. Off by default — the
|
|
433
|
+
always-declare style this enforces doesn't hold in monorepos
|
|
434
|
+
that rely on workspace cross-refs or the -public-deps cascade
|
|
435
|
+
(those produce noisy prompts that get dismissed reflexively,
|
|
436
|
+
which defeats the safety purpose). Use only on packages where
|
|
437
|
+
every import is meant to be a direct package.json declaration.
|
|
438
|
+
When it does fire, it catches silent runtime failures —
|
|
439
|
+
ERR_MODULE_NOT_FOUND on a clean install of a published tarball
|
|
440
|
+
that was resolving via an ambient parent/global node_modules
|
|
441
|
+
at dev time.
|
|
430
442
|
-force Continue despite git errors
|
|
431
443
|
-dry-run Preview what would happen
|
|
432
444
|
-quiet Suppress npm warnings (default)
|
package/cli.js
CHANGED
|
@@ -86,6 +86,12 @@ 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
|
+
-strict-imports, -import-check
|
|
90
|
+
Scan .ts/.js for imports not declared in any deps bucket and
|
|
91
|
+
warn before publish. Off by default — the always-declare
|
|
92
|
+
style this enforces doesn't hold for monorepos that rely on
|
|
93
|
+
workspace cross-refs / -public-deps cascade. Opt in only
|
|
94
|
+
when the assumption holds in your codebase.
|
|
89
95
|
-force Continue despite git errors
|
|
90
96
|
-dry-run Preview what would happen
|
|
91
97
|
-quiet Suppress npm warnings (default)
|
|
@@ -229,6 +235,10 @@ function parseArgs(args) {
|
|
|
229
235
|
case '-adopt':
|
|
230
236
|
options.adopt = true;
|
|
231
237
|
break;
|
|
238
|
+
case '-strict-imports':
|
|
239
|
+
case '-import-check':
|
|
240
|
+
options.importCheck = true;
|
|
241
|
+
break;
|
|
232
242
|
case '-git':
|
|
233
243
|
i++;
|
|
234
244
|
if (args[i] === 'private' || args[i] === 'public') {
|
package/lib.d.ts
CHANGED
|
@@ -119,6 +119,12 @@ 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
|
+
/** Run the undeclared-imports scan (`-strict-imports`). Default false —
|
|
123
|
+
* the scan assumes a style where every import is declared in the
|
|
124
|
+
* consuming package's package.json, which doesn't hold for monorepos
|
|
125
|
+
* that rely on workspace cross-refs / -public-deps cascade. Opt in when
|
|
126
|
+
* the assumption holds. */
|
|
127
|
+
importCheck?: boolean;
|
|
122
128
|
/** Internal: signals this call is from workspace orchestrator */
|
|
123
129
|
/** Skip the upfront dep-graph prescan */
|
|
124
130
|
noPrescan?: boolean;
|
|
@@ -260,6 +266,12 @@ export interface PrescanIssue {
|
|
|
260
266
|
message: string;
|
|
261
267
|
suggestion?: string;
|
|
262
268
|
}
|
|
269
|
+
export interface UndeclaredImport {
|
|
270
|
+
name: string;
|
|
271
|
+
file: string;
|
|
272
|
+
line: number;
|
|
273
|
+
}
|
|
274
|
+
export declare function findUndeclaredImports(cwd: string, pkg: any): UndeclaredImport[];
|
|
263
275
|
/** Walk the full file: dep graph and collect issues up front — missing scopes,
|
|
264
276
|
* unresolvable paths, missing package.json, unpublished transitives. Lets the
|
|
265
277
|
* 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,149 @@ 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
|
+
/** npm package name rules (simplified): lowercase letters, digits, `-`, `_`, `.`,
|
|
1350
|
+
* optionally with an `@scope/` prefix. No spaces, no `<>`, no uppercase. */
|
|
1351
|
+
const NPM_NAME_RE = /^(@[a-z0-9_.~-]+\/)?[a-z0-9_.~-]+$/;
|
|
1352
|
+
/** Extract the package name from an ESM/CJS module specifier.
|
|
1353
|
+
* Returns null for relative paths, absolute paths, URLs, node: builtins, and
|
|
1354
|
+
* anything that doesn't look like a valid npm package name (filters out
|
|
1355
|
+
* matches that landed in comments or string literals). */
|
|
1356
|
+
function specifierToPackageName(spec) {
|
|
1357
|
+
if (!spec)
|
|
1358
|
+
return null;
|
|
1359
|
+
if (spec.startsWith('.'))
|
|
1360
|
+
return null; // ./ or ../
|
|
1361
|
+
if (spec.startsWith('/'))
|
|
1362
|
+
return null; // /abs
|
|
1363
|
+
if (spec.startsWith('node:'))
|
|
1364
|
+
return null; // node:fs
|
|
1365
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(spec))
|
|
1366
|
+
return null; // url/data
|
|
1367
|
+
if (spec.startsWith('#'))
|
|
1368
|
+
return null; // subpath import
|
|
1369
|
+
let candidate;
|
|
1370
|
+
if (spec.startsWith('@')) {
|
|
1371
|
+
const parts = spec.split('/');
|
|
1372
|
+
if (parts.length < 2)
|
|
1373
|
+
return null;
|
|
1374
|
+
candidate = `${parts[0]}/${parts[1]}`;
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
candidate = spec.split('/')[0];
|
|
1378
|
+
}
|
|
1379
|
+
// Reject anything that isn't a plausible npm name — this filters out
|
|
1380
|
+
// matches inside string literals / comments like "Bob <bob@gmail.com>"
|
|
1381
|
+
// or "All Inboxes" that slipped through the regex.
|
|
1382
|
+
if (!NPM_NAME_RE.test(candidate))
|
|
1383
|
+
return null;
|
|
1384
|
+
return candidate;
|
|
1385
|
+
}
|
|
1386
|
+
const NODE_BUILTIN_NAMES = new Set(builtinModules);
|
|
1387
|
+
export function findUndeclaredImports(cwd, pkg) {
|
|
1388
|
+
const declared = new Set();
|
|
1389
|
+
for (const k of DEP_KEYS) {
|
|
1390
|
+
const bucket = pkg[k] || pkg['.' + k]; // also consult the backup bucket if a transform is pending
|
|
1391
|
+
if (bucket)
|
|
1392
|
+
for (const n of Object.keys(bucket))
|
|
1393
|
+
declared.add(n);
|
|
1394
|
+
}
|
|
1395
|
+
const selfName = pkg.name;
|
|
1396
|
+
// Anchored to line start (with optional leading whitespace) so that a
|
|
1397
|
+
// comment like `// imported from "Bob <bob@gmail.com>"` or an inline
|
|
1398
|
+
// string literal `"received from 'x'"` can't trigger the regex. This is
|
|
1399
|
+
// intentionally stricter than full JS parsing — minified one-line imports
|
|
1400
|
+
// are missed, but those are vendor bundles where false negatives are far
|
|
1401
|
+
// better than the false-positive noise.
|
|
1402
|
+
const IMPORT_FROM_RE = /^\s*(?:import|export)\s(?:[^'"`;]+\s)?from\s+['"]([^'"]+)['"]/g;
|
|
1403
|
+
const IMPORT_BARE_RE = /^\s*import\s+['"]([^'"]+)['"]/g;
|
|
1404
|
+
const DYNAMIC_RE = /(?:^|[^.\w$])import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1405
|
+
const REQUIRE_RE = /(?:^|[^.\w$])require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1406
|
+
const PATTERNS = [IMPORT_FROM_RE, IMPORT_BARE_RE, DYNAMIC_RE, REQUIRE_RE];
|
|
1407
|
+
const findings = new Map();
|
|
1408
|
+
const files = collectSourceFiles(cwd);
|
|
1409
|
+
for (const f of files) {
|
|
1410
|
+
let content;
|
|
1411
|
+
try {
|
|
1412
|
+
content = fs.readFileSync(f, 'utf-8');
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
// Quick reject: no import/require keyword anywhere → skip
|
|
1418
|
+
if (!/\bimport\b|\brequire\s*\(/.test(content))
|
|
1419
|
+
continue;
|
|
1420
|
+
const lines = content.split('\n');
|
|
1421
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1422
|
+
const line = lines[i];
|
|
1423
|
+
for (const re of PATTERNS) {
|
|
1424
|
+
re.lastIndex = 0;
|
|
1425
|
+
let m;
|
|
1426
|
+
while ((m = re.exec(line))) {
|
|
1427
|
+
const spec = m[1];
|
|
1428
|
+
const pkgName = specifierToPackageName(spec);
|
|
1429
|
+
if (!pkgName)
|
|
1430
|
+
continue;
|
|
1431
|
+
if (NODE_BUILTIN_NAMES.has(pkgName))
|
|
1432
|
+
continue;
|
|
1433
|
+
if (pkgName === selfName)
|
|
1434
|
+
continue;
|
|
1435
|
+
if (declared.has(pkgName))
|
|
1436
|
+
continue;
|
|
1437
|
+
if (!findings.has(pkgName)) {
|
|
1438
|
+
findings.set(pkgName, {
|
|
1439
|
+
file: path.relative(cwd, f).replace(/\\/g, '/'),
|
|
1440
|
+
line: i + 1
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return [...findings.entries()]
|
|
1448
|
+
.map(([name, loc]) => ({ name, ...loc }))
|
|
1449
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1450
|
+
}
|
|
1307
1451
|
/** Walk the full file: dep graph and collect issues up front — missing scopes,
|
|
1308
1452
|
* unresolvable paths, missing package.json, unpublished transitives. Lets the
|
|
1309
1453
|
* user resolve all problems before starting the publish cascade instead of
|
|
@@ -3411,7 +3555,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3411
3555
|
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
3556
|
publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
|
|
3413
3557
|
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;
|
|
3558
|
+
noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false, importCheck = false } = options;
|
|
3415
3559
|
// Show tool version only for recursive dep calls (CLI already prints it at startup)
|
|
3416
3560
|
const toolVersion = getToolVersion();
|
|
3417
3561
|
if (!options._fromWorkspace && !options._fromCli) {
|
|
@@ -3554,6 +3698,43 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3554
3698
|
console.log(colors.dim('Prescan: no issues found in dep graph.'));
|
|
3555
3699
|
}
|
|
3556
3700
|
}
|
|
3701
|
+
// Source-import scan — catches imports of packages not declared in
|
|
3702
|
+
// dependencies/devDependencies/peer/optional. Opt-in via -strict-imports
|
|
3703
|
+
// because the always-declare style this check enforces doesn't hold in
|
|
3704
|
+
// monorepos that rely on workspace cross-refs / -public-deps. Only at the
|
|
3705
|
+
// top level (deps in the cascade will be scanned when their own
|
|
3706
|
+
// globalize() runs).
|
|
3707
|
+
if (importCheck && !cleanup && !options._fromDep && !options._fromWorkspace) {
|
|
3708
|
+
const scanPkg = readPackageJson(cwd);
|
|
3709
|
+
const undeclared = findUndeclaredImports(cwd, scanPkg);
|
|
3710
|
+
if (undeclared.length > 0) {
|
|
3711
|
+
console.log('');
|
|
3712
|
+
console.log(colors.yellow(`Undeclared imports found (${undeclared.length}):`));
|
|
3713
|
+
for (const u of undeclared) {
|
|
3714
|
+
console.log(colors.yellow(` ${u.name}`)
|
|
3715
|
+
+ colors.dim(` (${u.file}:${u.line})`));
|
|
3716
|
+
}
|
|
3717
|
+
console.log(colors.dim(' These packages are imported but not in any package.json dep bucket.'));
|
|
3718
|
+
console.log(colors.dim(' After publish, `npm install` won\'t pull them and the package will'));
|
|
3719
|
+
console.log(colors.dim(' crash with ERR_MODULE_NOT_FOUND on a clean install.'));
|
|
3720
|
+
const interactive = !init && !options.autoInit && !publishDepsYes && !dryRun;
|
|
3721
|
+
if (interactive) {
|
|
3722
|
+
const ok = await confirm('Continue anyway?', false);
|
|
3723
|
+
if (!ok) {
|
|
3724
|
+
console.log('Aborted. Add the missing packages to dependencies and re-run.');
|
|
3725
|
+
console.log(colors.dim(' Tip: for siblings, use "file:../<dir>"; otherwise add the latest published version.'));
|
|
3726
|
+
return false;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
else {
|
|
3730
|
+
console.log(colors.yellow(' (non-interactive mode — continuing, but the install will likely fail at runtime)'));
|
|
3731
|
+
}
|
|
3732
|
+
console.log('');
|
|
3733
|
+
}
|
|
3734
|
+
else if (verbose) {
|
|
3735
|
+
console.log(colors.dim('Import scan: all imports are declared.'));
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3557
3738
|
// Check ignore files first (unless cleanup mode)
|
|
3558
3739
|
if (!cleanup && !asis) {
|
|
3559
3740
|
const checkResult = checkIgnoreFiles(cwd, { conform, asis, verbose, allowTs });
|