ghostimport 0.1.0 → 0.1.3
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 +32 -0
- package/dist/cli.js +149 -92
- package/dist/files.d.ts +1 -0
- package/dist/index.cjs +32 -1
- package/dist/index.js +32 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ The result: your build fails, or your CI breaks at 2am, or — in the worst case
|
|
|
40
40
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
|
+
Requires Node.js 22 or later.
|
|
44
|
+
|
|
43
45
|
```bash
|
|
44
46
|
# Run once (no install needed)
|
|
45
47
|
npx ghostimport
|
|
@@ -71,12 +73,27 @@ ghostimport --json
|
|
|
71
73
|
# Skip "missing from package.json" warnings
|
|
72
74
|
ghostimport --no-undeclared
|
|
73
75
|
|
|
76
|
+
# Watch mode (re-scans on file changes)
|
|
77
|
+
ghostimport --watch
|
|
78
|
+
|
|
74
79
|
# Help
|
|
75
80
|
ghostimport --help
|
|
76
81
|
```
|
|
77
82
|
|
|
78
83
|
### Add to CI (GitHub Actions)
|
|
79
84
|
|
|
85
|
+
Use the built-in action for the simplest setup:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
- name: Check for hallucinated packages
|
|
89
|
+
uses: FGuerreir0/ghostimport@main
|
|
90
|
+
with:
|
|
91
|
+
path: '.'
|
|
92
|
+
scary: 'false'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or run directly:
|
|
96
|
+
|
|
80
97
|
```yaml
|
|
81
98
|
- name: Check for hallucinated packages
|
|
82
99
|
run: npx ghostimport --quiet
|
|
@@ -84,6 +101,18 @@ ghostimport --help
|
|
|
84
101
|
|
|
85
102
|
This step will fail (exit code 1) if any hallucinated packages are found.
|
|
86
103
|
|
|
104
|
+
### Pre-commit
|
|
105
|
+
|
|
106
|
+
Add to `.pre-commit-config.yaml`:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
repos:
|
|
110
|
+
- repo: https://github.com/FGuerreir0/ghostimport
|
|
111
|
+
rev: main
|
|
112
|
+
hooks:
|
|
113
|
+
- id: ghostimport
|
|
114
|
+
```
|
|
115
|
+
|
|
87
116
|
---
|
|
88
117
|
|
|
89
118
|
## Programmatic API
|
|
@@ -163,6 +192,9 @@ interface ScanOptions {
|
|
|
163
192
|
**Automatically ignores:**
|
|
164
193
|
- Node.js built-ins (`fs`, `path`, `crypto`, `node:*`, ...)
|
|
165
194
|
- Relative imports (`./`, `../`)
|
|
195
|
+
- Path aliases (`@/`, `~/`, `$lib/`, and tsconfig `paths`)
|
|
196
|
+
- URL/protocol imports (`https:`, `data:`, `bun:`, ...)
|
|
197
|
+
- Virtual modules (`virtual:`, Vite/Rollup internals)
|
|
166
198
|
- `node_modules/`, `dist/`, `.git/`, `build/`
|
|
167
199
|
|
|
168
200
|
**Supported file types:** `.js` `.jsx` `.ts` `.tsx` `.mjs` `.cjs`
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import path5 from "path";
|
|
5
|
-
import { readFileSync } from "fs";
|
|
5
|
+
import { readFileSync, watch as fsWatch } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/scan.ts
|
|
@@ -79,6 +79,15 @@ function isBuiltin(name) {
|
|
|
79
79
|
return BUILTIN_MODULES.has(name);
|
|
80
80
|
}
|
|
81
81
|
function toPackageName(importPath) {
|
|
82
|
+
if (importPath.startsWith("@/") || importPath.startsWith("~/") || importPath.startsWith("#") || importPath.startsWith("$")) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(importPath)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (importPath.startsWith("\0") || importPath.startsWith("virtual:")) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
82
91
|
if (importPath.startsWith("@")) {
|
|
83
92
|
const parts = importPath.split("/");
|
|
84
93
|
if (parts.length < 2) return null;
|
|
@@ -259,7 +268,7 @@ import path3 from "path";
|
|
|
259
268
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"]);
|
|
260
269
|
var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
|
|
261
270
|
function walkFiles(dir) {
|
|
262
|
-
const
|
|
271
|
+
const results = [];
|
|
263
272
|
function walk(current) {
|
|
264
273
|
let entries;
|
|
265
274
|
try {
|
|
@@ -271,12 +280,12 @@ function walkFiles(dir) {
|
|
|
271
280
|
if (entry.isDirectory()) {
|
|
272
281
|
if (!SKIP_DIRS.has(entry.name)) walk(path3.join(current, entry.name));
|
|
273
282
|
} else if (entry.isFile()) {
|
|
274
|
-
if (CODE_EXTS.has(path3.extname(entry.name)))
|
|
283
|
+
if (CODE_EXTS.has(path3.extname(entry.name))) results.push(path3.join(current, entry.name));
|
|
275
284
|
}
|
|
276
285
|
}
|
|
277
286
|
}
|
|
278
287
|
walk(dir);
|
|
279
|
-
return
|
|
288
|
+
return results;
|
|
280
289
|
}
|
|
281
290
|
function readPackageJsonDeps(dir) {
|
|
282
291
|
const pkgPath = path3.join(dir, "package.json");
|
|
@@ -338,6 +347,27 @@ function readWorkspacePackages(dir) {
|
|
|
338
347
|
}
|
|
339
348
|
return names;
|
|
340
349
|
}
|
|
350
|
+
function readTsconfigPaths(dir) {
|
|
351
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
352
|
+
const candidates = ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"];
|
|
353
|
+
for (const name of candidates) {
|
|
354
|
+
const configPath = path3.join(dir, name);
|
|
355
|
+
if (!fs3.existsSync(configPath)) continue;
|
|
356
|
+
try {
|
|
357
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
358
|
+
const stripped = raw.replace(/\/\/.*$/gm, "");
|
|
359
|
+
const config2 = JSON.parse(stripped);
|
|
360
|
+
const paths = config2.compilerOptions?.paths;
|
|
361
|
+
if (!paths) continue;
|
|
362
|
+
for (const key of Object.keys(paths)) {
|
|
363
|
+
const prefix = key.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
364
|
+
if (prefix) aliases.add(prefix);
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return aliases;
|
|
370
|
+
}
|
|
341
371
|
|
|
342
372
|
// src/scan.ts
|
|
343
373
|
var CONCURRENCY = 10;
|
|
@@ -346,6 +376,7 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
|
|
|
346
376
|
const files = walkFiles(targetDir2);
|
|
347
377
|
const declaredDeps = readPackageJsonDeps(targetDir2);
|
|
348
378
|
const workspacePkgs = readWorkspacePackages(targetDir2);
|
|
379
|
+
const tsconfigAliases = readTsconfigPaths(targetDir2);
|
|
349
380
|
const cache = useCache ? loadCache() : {};
|
|
350
381
|
let cacheHits = 0;
|
|
351
382
|
const importMap = /* @__PURE__ */ new Map();
|
|
@@ -362,9 +393,9 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
|
|
|
362
393
|
}
|
|
363
394
|
}
|
|
364
395
|
const allPkgs = [...importMap.keys()].filter(
|
|
365
|
-
(pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg)
|
|
396
|
+
(pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg) && !tsconfigAliases.has(pkg)
|
|
366
397
|
);
|
|
367
|
-
const
|
|
398
|
+
const results = {
|
|
368
399
|
scanned: files.length,
|
|
369
400
|
packages: allPkgs.length,
|
|
370
401
|
hallucinated: [],
|
|
@@ -394,24 +425,24 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
|
|
|
394
425
|
}
|
|
395
426
|
onProgress?.({ pkg, exists, error, total: allPkgs.length, done: i + j + 1 });
|
|
396
427
|
if (exists === false) {
|
|
397
|
-
|
|
428
|
+
results.hallucinated.push({ pkg, files: matchedFiles });
|
|
398
429
|
} else if (exists === null && error) {
|
|
399
|
-
|
|
430
|
+
results.errors.push({ pkg, error, files: matchedFiles });
|
|
400
431
|
} else if (exists === true && !declaredDeps.has(pkg)) {
|
|
401
|
-
|
|
432
|
+
results.notInPackageJson.push({ pkg, files: matchedFiles });
|
|
402
433
|
}
|
|
403
434
|
}
|
|
404
435
|
}
|
|
405
436
|
if (useCache) saveCache(cache);
|
|
406
|
-
|
|
437
|
+
results.cacheHits = cacheHits;
|
|
407
438
|
if (scary) {
|
|
408
|
-
for (const { pkg, files: matchedFiles } of
|
|
409
|
-
|
|
439
|
+
for (const { pkg, files: matchedFiles } of results.hallucinated) {
|
|
440
|
+
results.scary.push({ pkg, files: matchedFiles, type: "available" });
|
|
410
441
|
}
|
|
411
|
-
for (const { pkg, files: matchedFiles } of
|
|
442
|
+
for (const { pkg, files: matchedFiles } of results.notInPackageJson) {
|
|
412
443
|
const info = await checkScary(pkg);
|
|
413
444
|
if (info.exists === true && info.risk !== "low") {
|
|
414
|
-
|
|
445
|
+
results.scary.push({
|
|
415
446
|
pkg,
|
|
416
447
|
files: matchedFiles,
|
|
417
448
|
type: "suspicious",
|
|
@@ -425,7 +456,7 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
|
|
|
425
456
|
}
|
|
426
457
|
}
|
|
427
458
|
}
|
|
428
|
-
return
|
|
459
|
+
return results;
|
|
429
460
|
}
|
|
430
461
|
|
|
431
462
|
// src/cli.ts
|
|
@@ -448,6 +479,7 @@ var targetDir = path5.resolve(args.find((a) => !a.startsWith("--") && !a.startsW
|
|
|
448
479
|
var flags = {
|
|
449
480
|
quiet: args.includes("--quiet") || args.includes("-q"),
|
|
450
481
|
json: args.includes("--json"),
|
|
482
|
+
watch: args.includes("--watch") || args.includes("-w"),
|
|
451
483
|
noUndeclared: args.includes("--no-undeclared"),
|
|
452
484
|
noCache: args.includes("--no-cache"),
|
|
453
485
|
scary: args.includes("--scary"),
|
|
@@ -468,6 +500,7 @@ ${c.bold("Usage:")}
|
|
|
468
500
|
${c.bold("Options:")}
|
|
469
501
|
--quiet, -q Only show problems (no progress)
|
|
470
502
|
--json Output results as JSON
|
|
503
|
+
--watch, -w Watch for file changes and re-scan
|
|
471
504
|
--no-undeclared Skip "imported but not in package.json" warnings
|
|
472
505
|
--scary Check for supply chain attack risk (squatting)
|
|
473
506
|
--no-cache Skip the local registry cache
|
|
@@ -491,97 +524,121 @@ ${c.bold("Examples:")}
|
|
|
491
524
|
process.exit(0);
|
|
492
525
|
}
|
|
493
526
|
var config = loadConfig(targetDir);
|
|
494
|
-
|
|
495
|
-
|
|
527
|
+
async function runScan() {
|
|
528
|
+
if (!flags.quiet && !flags.json) {
|
|
529
|
+
console.log(`
|
|
496
530
|
${c.bold("ghostimport")} ${c.gray(`v${version}`)}`);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
var lastProgress = "";
|
|
502
|
-
var results = await scan(targetDir, {
|
|
503
|
-
useCache: !flags.noCache,
|
|
504
|
-
scary: flags.scary,
|
|
505
|
-
config,
|
|
506
|
-
onProgress: flags.json || flags.quiet ? void 0 : ({ pkg, done, total }) => {
|
|
507
|
-
const pct = Math.round(done / total * 100);
|
|
508
|
-
const bar = "\u2588".repeat(Math.floor(pct / 5)) + "\u2591".repeat(20 - Math.floor(pct / 5));
|
|
509
|
-
const line = ` ${c.gray(bar)} ${pct}% ${c.dim(pkg.slice(0, 30))}`;
|
|
510
|
-
process.stdout.write("\r" + line + " ".repeat(Math.max(0, lastProgress.length - line.length)));
|
|
511
|
-
lastProgress = line;
|
|
531
|
+
console.log(c.gray(`Scanning ${targetDir}`));
|
|
532
|
+
if (flags.scary) console.log(c.magenta(" \u26A0 Scary mode: checking supply chain risk"));
|
|
533
|
+
console.log();
|
|
512
534
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
console.log(c.bold(c.red(` \u2717 ${results.hallucinated.length} hallucinated package${results.hallucinated.length > 1 ? "s" : ""} (do not exist on npm):
|
|
529
|
-
`)));
|
|
530
|
-
for (const { pkg, files } of results.hallucinated) {
|
|
531
|
-
console.log(` ${c.red("\u25CF")} ${c.bold(pkg)}`);
|
|
532
|
-
for (const f of files.slice(0, 3)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
|
|
533
|
-
if (files.length > 3) console.log(` ${c.gray(`\u21B3 ...and ${files.length - 3} more files`)}`);
|
|
535
|
+
let lastProgress = "";
|
|
536
|
+
const results = await scan(targetDir, {
|
|
537
|
+
useCache: !flags.noCache,
|
|
538
|
+
scary: flags.scary,
|
|
539
|
+
config,
|
|
540
|
+
onProgress: flags.json || flags.quiet ? void 0 : ({ pkg, done, total }) => {
|
|
541
|
+
const pct = Math.round(done / total * 100);
|
|
542
|
+
const bar = "\u2588".repeat(Math.floor(pct / 5)) + "\u2591".repeat(20 - Math.floor(pct / 5));
|
|
543
|
+
const line = ` ${c.gray(bar)} ${pct}% ${c.dim(pkg.slice(0, 30))}`;
|
|
544
|
+
process.stdout.write("\r" + line + " ".repeat(Math.max(0, lastProgress.length - line.length)));
|
|
545
|
+
lastProgress = line;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
if (!flags.json && !flags.quiet && lastProgress) {
|
|
549
|
+
process.stdout.write("\r" + " ".repeat(lastProgress.length + 5) + "\r");
|
|
534
550
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
551
|
+
if (flags.json) {
|
|
552
|
+
console.log(JSON.stringify(results, null, 2));
|
|
553
|
+
return results.hallucinated.length;
|
|
554
|
+
}
|
|
555
|
+
const totalIssues = results.hallucinated.length + (flags.noUndeclared ? 0 : results.notInPackageJson.length);
|
|
556
|
+
console.log(
|
|
557
|
+
` ${c.gray("Scanned")} ${c.cyan(results.scanned + " files")} \xB7 ${c.cyan(results.packages + " unique packages")} checked` + (results.cacheHits > 0 ? ` ${c.gray(`(${results.cacheHits} cached)`)}` : "") + "\n"
|
|
558
|
+
);
|
|
559
|
+
if (results.hallucinated.length === 0) {
|
|
560
|
+
console.log(c.green(" \u2713 No hallucinated packages found"));
|
|
561
|
+
} else {
|
|
562
|
+
console.log(c.bold(c.red(` \u2717 ${results.hallucinated.length} hallucinated package${results.hallucinated.length > 1 ? "s" : ""} (do not exist on npm):
|
|
542
563
|
`)));
|
|
543
|
-
for (const { pkg } of
|
|
544
|
-
console.log(` ${c.
|
|
545
|
-
console.log(` ${c.
|
|
546
|
-
console.log(` ${c.
|
|
564
|
+
for (const { pkg, files } of results.hallucinated) {
|
|
565
|
+
console.log(` ${c.red("\u25CF")} ${c.bold(pkg)}`);
|
|
566
|
+
for (const f of files.slice(0, 3)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
|
|
567
|
+
if (files.length > 3) console.log(` ${c.gray(`\u21B3 ...and ${files.length - 3} more files`)}`);
|
|
547
568
|
}
|
|
548
569
|
}
|
|
549
|
-
if (
|
|
570
|
+
if (flags.scary && results.scary.length > 0) {
|
|
550
571
|
console.log();
|
|
551
|
-
|
|
572
|
+
const available = results.scary.filter((s) => s.type === "available");
|
|
573
|
+
const suspicious = results.scary.filter((s) => s.type === "suspicious");
|
|
574
|
+
if (available.length > 0) {
|
|
575
|
+
console.log(c.bold(c.magenta(` \u{1F480} ${available.length} package name${available.length > 1 ? "s" : ""} available for malicious registration:
|
|
576
|
+
`)));
|
|
577
|
+
for (const { pkg } of available) {
|
|
578
|
+
console.log(` ${c.magenta("\u25CF")} ${c.bold(pkg)}`);
|
|
579
|
+
console.log(` ${c.red("\u21B3 Anyone can register this name with a malicious postinstall script")}`);
|
|
580
|
+
console.log(` ${c.red("\u21B3 If installed, it could exfiltrate .env, tokens, SSH keys")}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (suspicious.length > 0) {
|
|
584
|
+
console.log();
|
|
585
|
+
console.log(c.bold(c.magenta(` \u{1F575}\uFE0F ${suspicious.length} suspicious package${suspicious.length > 1 ? "s" : ""} (potential squats):
|
|
552
586
|
`)));
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
587
|
+
for (const entry of suspicious) {
|
|
588
|
+
if (entry.type !== "suspicious") continue;
|
|
589
|
+
console.log(` ${c.magenta("\u25CF")} ${c.bold(entry.pkg)} ${c.gray(`(created ${entry.created}, ${entry.downloads ?? "?"} downloads/week)`)}`);
|
|
590
|
+
for (const flag of entry.flags) console.log(` ${c.yellow("\u21B3")} ${flag}`);
|
|
591
|
+
}
|
|
557
592
|
}
|
|
558
593
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
console.log(c.bold(c.yellow(` \u26A0 ${results.notInPackageJson.length} package${results.notInPackageJson.length > 1 ? "s" : ""} imported but missing from package.json:
|
|
594
|
+
if (!flags.noUndeclared && results.notInPackageJson.length > 0) {
|
|
595
|
+
console.log();
|
|
596
|
+
console.log(c.bold(c.yellow(` \u26A0 ${results.notInPackageJson.length} package${results.notInPackageJson.length > 1 ? "s" : ""} imported but missing from package.json:
|
|
563
597
|
`)));
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
598
|
+
for (const { pkg, files } of results.notInPackageJson.slice(0, 10)) {
|
|
599
|
+
console.log(` ${c.yellow("\u25CF")} ${c.bold(pkg)}`);
|
|
600
|
+
for (const f of files.slice(0, 2)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
|
|
601
|
+
}
|
|
602
|
+
if (results.notInPackageJson.length > 10) {
|
|
603
|
+
console.log(` ${c.gray(` ...and ${results.notInPackageJson.length - 10} more`)}`);
|
|
604
|
+
}
|
|
567
605
|
}
|
|
568
|
-
if (results.
|
|
569
|
-
console.log(
|
|
606
|
+
if (results.errors.length > 0) {
|
|
607
|
+
console.log();
|
|
608
|
+
console.log(c.gray(` \u26A1 ${results.errors.length} package(s) could not be checked (network/timeout)`));
|
|
570
609
|
}
|
|
571
|
-
}
|
|
572
|
-
if (results.errors.length > 0) {
|
|
573
610
|
console.log();
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
} else if (flags.scary && results.scary.length > 0) {
|
|
580
|
-
const scaryCount = results.scary.filter((s) => s.type === "available").length;
|
|
581
|
-
console.log(c.red(c.bold(` Found ${totalIssues} issue${totalIssues > 1 ? "s" : ""} \xB7 ${scaryCount} supply chain risk${scaryCount > 1 ? "s" : ""}
|
|
611
|
+
if (totalIssues === 0 && results.scary.length === 0) {
|
|
612
|
+
console.log(c.green(c.bold(" All good! \u2713\n")));
|
|
613
|
+
} else if (flags.scary && results.scary.length > 0) {
|
|
614
|
+
const scaryCount = results.scary.filter((s) => s.type === "available").length;
|
|
615
|
+
console.log(c.red(c.bold(` Found ${totalIssues} issue${totalIssues > 1 ? "s" : ""} \xB7 ${scaryCount} supply chain risk${scaryCount > 1 ? "s" : ""}
|
|
582
616
|
`)));
|
|
583
|
-
} else {
|
|
584
|
-
|
|
617
|
+
} else {
|
|
618
|
+
console.log(c.red(c.bold(` Found ${totalIssues} issue${totalIssues > 1 ? "s" : ""}.
|
|
585
619
|
`)));
|
|
620
|
+
}
|
|
621
|
+
return results.hallucinated.length;
|
|
622
|
+
}
|
|
623
|
+
var issues = await runScan();
|
|
624
|
+
if (flags.watch) {
|
|
625
|
+
const CODE_EXTS2 = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
|
|
626
|
+
let debounce = null;
|
|
627
|
+
console.log(c.gray(` Watching for changes in ${targetDir}...
|
|
628
|
+
`));
|
|
629
|
+
fsWatch(targetDir, { recursive: true }, (_event, filename) => {
|
|
630
|
+
if (!filename) return;
|
|
631
|
+
const ext = path5.extname(filename);
|
|
632
|
+
if (!CODE_EXTS2.has(ext)) return;
|
|
633
|
+
if (filename.includes("node_modules") || filename.includes("dist")) return;
|
|
634
|
+
if (debounce) clearTimeout(debounce);
|
|
635
|
+
debounce = setTimeout(async () => {
|
|
636
|
+
console.clear();
|
|
637
|
+
await runScan();
|
|
638
|
+
console.log(c.gray(` Watching for changes...
|
|
639
|
+
`));
|
|
640
|
+
}, 300);
|
|
641
|
+
});
|
|
642
|
+
} else {
|
|
643
|
+
process.exit(issues > 0 ? 1 : 0);
|
|
586
644
|
}
|
|
587
|
-
process.exit(results.hallucinated.length > 0 ? 1 : 0);
|
package/dist/files.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function walkFiles(dir: string): string[];
|
|
2
2
|
export declare function readPackageJsonDeps(dir: string): Set<string>;
|
|
3
3
|
export declare function readWorkspacePackages(dir: string): Set<string>;
|
|
4
|
+
export declare function readTsconfigPaths(dir: string): Set<string>;
|
package/dist/index.cjs
CHANGED
|
@@ -112,6 +112,15 @@ function isBuiltin(name) {
|
|
|
112
112
|
return BUILTIN_MODULES.has(name);
|
|
113
113
|
}
|
|
114
114
|
function toPackageName(importPath) {
|
|
115
|
+
if (importPath.startsWith("@/") || importPath.startsWith("~/") || importPath.startsWith("#") || importPath.startsWith("$")) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(importPath)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (importPath.startsWith("\0") || importPath.startsWith("virtual:")) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
115
124
|
if (importPath.startsWith("@")) {
|
|
116
125
|
const parts = importPath.split("/");
|
|
117
126
|
if (parts.length < 2) return null;
|
|
@@ -371,6 +380,27 @@ function readWorkspacePackages(dir) {
|
|
|
371
380
|
}
|
|
372
381
|
return names;
|
|
373
382
|
}
|
|
383
|
+
function readTsconfigPaths(dir) {
|
|
384
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
385
|
+
const candidates = ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"];
|
|
386
|
+
for (const name of candidates) {
|
|
387
|
+
const configPath = import_path3.default.join(dir, name);
|
|
388
|
+
if (!import_fs3.default.existsSync(configPath)) continue;
|
|
389
|
+
try {
|
|
390
|
+
const raw = import_fs3.default.readFileSync(configPath, "utf8");
|
|
391
|
+
const stripped = raw.replace(/\/\/.*$/gm, "");
|
|
392
|
+
const config = JSON.parse(stripped);
|
|
393
|
+
const paths = config.compilerOptions?.paths;
|
|
394
|
+
if (!paths) continue;
|
|
395
|
+
for (const key of Object.keys(paths)) {
|
|
396
|
+
const prefix = key.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
397
|
+
if (prefix) aliases.add(prefix);
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return aliases;
|
|
403
|
+
}
|
|
374
404
|
|
|
375
405
|
// src/scan.ts
|
|
376
406
|
var import_fs4 = __toESM(require("fs"), 1);
|
|
@@ -381,6 +411,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
|
|
|
381
411
|
const files = walkFiles(targetDir);
|
|
382
412
|
const declaredDeps = readPackageJsonDeps(targetDir);
|
|
383
413
|
const workspacePkgs = readWorkspacePackages(targetDir);
|
|
414
|
+
const tsconfigAliases = readTsconfigPaths(targetDir);
|
|
384
415
|
const cache = useCache ? loadCache() : {};
|
|
385
416
|
let cacheHits = 0;
|
|
386
417
|
const importMap = /* @__PURE__ */ new Map();
|
|
@@ -397,7 +428,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
|
|
|
397
428
|
}
|
|
398
429
|
}
|
|
399
430
|
const allPkgs = [...importMap.keys()].filter(
|
|
400
|
-
(pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg)
|
|
431
|
+
(pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg) && !tsconfigAliases.has(pkg)
|
|
401
432
|
);
|
|
402
433
|
const results = {
|
|
403
434
|
scanned: files.length,
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,15 @@ function isBuiltin(name) {
|
|
|
68
68
|
return BUILTIN_MODULES.has(name);
|
|
69
69
|
}
|
|
70
70
|
function toPackageName(importPath) {
|
|
71
|
+
if (importPath.startsWith("@/") || importPath.startsWith("~/") || importPath.startsWith("#") || importPath.startsWith("$")) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(importPath)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (importPath.startsWith("\0") || importPath.startsWith("virtual:")) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
71
80
|
if (importPath.startsWith("@")) {
|
|
72
81
|
const parts = importPath.split("/");
|
|
73
82
|
if (parts.length < 2) return null;
|
|
@@ -327,6 +336,27 @@ function readWorkspacePackages(dir) {
|
|
|
327
336
|
}
|
|
328
337
|
return names;
|
|
329
338
|
}
|
|
339
|
+
function readTsconfigPaths(dir) {
|
|
340
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
341
|
+
const candidates = ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"];
|
|
342
|
+
for (const name of candidates) {
|
|
343
|
+
const configPath = path3.join(dir, name);
|
|
344
|
+
if (!fs3.existsSync(configPath)) continue;
|
|
345
|
+
try {
|
|
346
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
347
|
+
const stripped = raw.replace(/\/\/.*$/gm, "");
|
|
348
|
+
const config = JSON.parse(stripped);
|
|
349
|
+
const paths = config.compilerOptions?.paths;
|
|
350
|
+
if (!paths) continue;
|
|
351
|
+
for (const key of Object.keys(paths)) {
|
|
352
|
+
const prefix = key.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
353
|
+
if (prefix) aliases.add(prefix);
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return aliases;
|
|
359
|
+
}
|
|
330
360
|
|
|
331
361
|
// src/scan.ts
|
|
332
362
|
import fs4 from "fs";
|
|
@@ -337,6 +367,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
|
|
|
337
367
|
const files = walkFiles(targetDir);
|
|
338
368
|
const declaredDeps = readPackageJsonDeps(targetDir);
|
|
339
369
|
const workspacePkgs = readWorkspacePackages(targetDir);
|
|
370
|
+
const tsconfigAliases = readTsconfigPaths(targetDir);
|
|
340
371
|
const cache = useCache ? loadCache() : {};
|
|
341
372
|
let cacheHits = 0;
|
|
342
373
|
const importMap = /* @__PURE__ */ new Map();
|
|
@@ -353,7 +384,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
|
|
|
353
384
|
}
|
|
354
385
|
}
|
|
355
386
|
const allPkgs = [...importMap.keys()].filter(
|
|
356
|
-
(pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg)
|
|
387
|
+
(pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg) && !tsconfigAliases.has(pkg)
|
|
357
388
|
);
|
|
358
389
|
const results = {
|
|
359
390
|
scanned: files.length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghostimport",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Detects ghost imports in your code - imports that don't exist, hallucinated by AI coding tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"npm",
|
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
"imports",
|
|
13
13
|
"security",
|
|
14
14
|
"lint",
|
|
15
|
-
"ghost"
|
|
15
|
+
"ghost",
|
|
16
|
+
"import-validation",
|
|
17
|
+
"ai-safety",
|
|
18
|
+
"supply-chain",
|
|
19
|
+
"hallucination-detection",
|
|
20
|
+
"copilot",
|
|
21
|
+
"pre-commit"
|
|
16
22
|
],
|
|
17
23
|
"homepage": "https://github.com/FGuerreir0/ghostimport#readme",
|
|
18
24
|
"bugs": {
|
|
@@ -50,6 +56,7 @@
|
|
|
50
56
|
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
51
57
|
"test": "tsx tests/test.ts",
|
|
52
58
|
"dev": "tsx src/cli.ts",
|
|
59
|
+
"prepare": "git config core.hooksPath .githooks",
|
|
53
60
|
"prepublishOnly": "npm run build"
|
|
54
61
|
},
|
|
55
62
|
"devDependencies": {
|