Setting the file. One moment. Undeclared Dep · Vercel Optimize · vercel-labs/agent-skills · Skills Docslib/sanitizers/undeclared-dep.mjs
lib/sanitizers/undeclared-dep.mjs
JavaScript·78 lines·3 KB
require
\s
*
\(
\s
*
["']
(
[
^
"']
+
)
["']\s
*
\)
/
g
;
6// Captures package root from `pkg/sub` and `@scope/pkg/sub`.
7const PKG_ROOT_RE = /^(@[^/]+\/[^/]+|[^/]+)/;
8const NODE_BUILTINS = new Set([
9 'fs', 'fs/promises', 'path', 'os', 'crypto', 'http', 'https', 'http2', 'net',
10 'dns', 'tls', 'util', 'url', 'stream', 'buffer', 'events', 'process', 'child_process',
11 'cluster', 'worker_threads', 'inspector', 'perf_hooks', 'assert', 'console',
12 'querystring', 'string_decoder', 'tty', 'vm', 'zlib', 'readline', 'punycode',
13 'module', 'timers', 'async_hooks', 'v8', 'test', 'diagnostics_channel',
14]);
15
16export const metadata = {
17 id: 'undeclared-dep',
18 description: 'Prepend `npm i <pkg>` when fix imports a package not in package.json.',
19};
20
21export function apply(rec, ctx = {}) {
22 const pkg = ctx?.package ?? ctx?.signals?.package ?? null;
23 if (!pkg) return {};
24
25 const known = new Set([
26 ...Object.keys(pkg.dependencies ?? {}),
27 ...Object.keys(pkg.devDependencies ?? {}),
28 ...Object.keys(pkg.peerDependencies ?? {}),
29 ...Object.keys(pkg.optionalDependencies ?? {}),
30 ]);
31
32 const text = [rec.fix, rec.currentBehavior, rec.desiredBehavior]
33 .filter((s) => typeof s === 'string')
34 .join('\n');
35 const codeBlocks = extractCodeBlocks(text);
36 const importedRoots = new Set();
37 for (const block of codeBlocks) {
38 for (const m of block.matchAll(IMPORT_RE)) {
39 const root = pkgRoot(m[1]);
40 if (root) importedRoots.add(root);
41 }
42 for (const m of block.matchAll(REQUIRE_RE)) {
43 const root = pkgRoot(m[1]);
44 if (root) importedRoots.add(root);
45 }
46 }
47
48 const undeclared = [...importedRoots]
49 .filter((r) => !r.startsWith('.'))
50 .filter((r) => !NODE_BUILTINS.has(r))
51 .filter((r) => !r.startsWith('node:'))
52 .filter((r) => !known.has(r));
53
54 if (undeclared.length === 0) return {};
55
56 const installLines = undeclared.map((p) => `\`npm i ${p}\``).join(', ');
57 const prepend = `**Add dependency first**: ${installLines}\n\n`;
58 if (typeof rec.fix === 'string') rec.fix = prepend + rec.fix;
59 else rec.fix = prepend.trim();
60 return { tags: undeclared.map((p) => `undeclared-dep:${p}`), needsReview: true };
61}
62
63function pkgRoot(specifier) {
64 if (!specifier) return null;
65 if (specifier.startsWith('.')) return specifier;
66 const m = specifier.match(PKG_ROOT_RE);
67 return m ? m[1] : null;
68}
69
70function extractCodeBlocks(text) {
71 const out = [];
72 const re = /```[\w-]*\n?([\s\S]*?)```/g;
73 let m;
74 while ((m = re.exec(text)) !== null) out.push(m[1]);
75 // Also scan raw text for rare inline imports outside code blocks.
76 out.push(text);
77 return out;
78}