ruvector 0.2.19 → 0.2.20
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/bin/cli.js +139 -0
- package/bin/mcp-server.js +269 -0
- package/package.json +14 -6
- package/src/decompiler/index.js +407 -0
- package/src/decompiler/metrics.js +86 -0
- package/src/decompiler/module-splitter.js +498 -0
- package/src/decompiler/module-tree.js +142 -0
- package/src/decompiler/name-predictor.js +400 -0
- package/src/decompiler/npm-fetch.js +176 -0
- package/src/decompiler/reconstructor.js +499 -0
- package/src/decompiler/reference-tracker.js +285 -0
- package/src/decompiler/statement-parser.js +285 -0
- package/src/decompiler/style-improver.js +438 -0
- package/src/decompiler/subcategories.js +339 -0
- package/src/decompiler/validator.js +379 -0
- package/src/decompiler/witness.js +140 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* name-predictor.js - Pattern-based name prediction for minified identifiers.
|
|
3
|
+
*
|
|
4
|
+
* Uses the 210+ training patterns from claude-code-patterns.json to infer
|
|
5
|
+
* meaningful names based on context strings, property accesses, and
|
|
6
|
+
* structural patterns found near each identifier.
|
|
7
|
+
*
|
|
8
|
+
* Falls back to structural heuristics when no pattern matches.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/** Cached patterns array — loaded once. */
|
|
17
|
+
let _cachedPatterns = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load patterns from the JSON training data.
|
|
21
|
+
* @param {string} [patternPath] - override path to patterns JSON
|
|
22
|
+
* @returns {Array<{context_strings: string[], property_names: string[], inferred_name: string, module_hint: string, confidence: number}>}
|
|
23
|
+
*/
|
|
24
|
+
function loadPatterns(patternPath) {
|
|
25
|
+
if (_cachedPatterns && !patternPath) return _cachedPatterns;
|
|
26
|
+
|
|
27
|
+
const defaultPath = path.resolve(
|
|
28
|
+
__dirname,
|
|
29
|
+
'../../../../../crates/ruvector-decompiler/data/claude-code-patterns.json',
|
|
30
|
+
);
|
|
31
|
+
const resolved = patternPath || defaultPath;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
35
|
+
_cachedPatterns = JSON.parse(raw);
|
|
36
|
+
return _cachedPatterns;
|
|
37
|
+
} catch {
|
|
38
|
+
// Pattern file not found — return empty, rely on structural rules
|
|
39
|
+
_cachedPatterns = [];
|
|
40
|
+
return _cachedPatterns;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structural rename rules — deterministic, no model needed.
|
|
46
|
+
* Ordered by specificity (most specific first).
|
|
47
|
+
*/
|
|
48
|
+
const STRUCTURAL_RULES = [
|
|
49
|
+
// Async generators
|
|
50
|
+
{
|
|
51
|
+
test: (decl) => /async\s+function\s*\*/.test(decl),
|
|
52
|
+
nameFrom: (decl, ctx) => {
|
|
53
|
+
if (ctx.some((s) => s.includes('agent')) && ctx.some((s) => s.includes('loop'))) return 'agentLoop';
|
|
54
|
+
if (ctx.some((s) => s.includes('stream'))) return 'streamGenerator';
|
|
55
|
+
return 'asyncGenerator';
|
|
56
|
+
},
|
|
57
|
+
confidence: 0.7,
|
|
58
|
+
},
|
|
59
|
+
// Error subclasses
|
|
60
|
+
{
|
|
61
|
+
test: (decl) => /class\s+\w+\s+extends\s+Error/.test(decl),
|
|
62
|
+
nameFrom: (decl) => {
|
|
63
|
+
const match = decl.match(/class\s+(\w+)\s+extends\s+(\w*Error)/);
|
|
64
|
+
if (match && !isMinifiedLike(match[1])) return match[1];
|
|
65
|
+
return 'CustomError';
|
|
66
|
+
},
|
|
67
|
+
confidence: 0.85,
|
|
68
|
+
},
|
|
69
|
+
// Class extending another class
|
|
70
|
+
{
|
|
71
|
+
test: (decl) => /class\s+\w+\s+extends\s+\w+/.test(decl),
|
|
72
|
+
nameFrom: (decl) => {
|
|
73
|
+
const match = decl.match(/class\s+(\w+)\s+extends\s+(\w+)/);
|
|
74
|
+
if (match && !isMinifiedLike(match[1])) return match[1];
|
|
75
|
+
if (match) return `${match[2]}Subclass`;
|
|
76
|
+
return null;
|
|
77
|
+
},
|
|
78
|
+
confidence: 0.75,
|
|
79
|
+
},
|
|
80
|
+
// Regular class
|
|
81
|
+
{
|
|
82
|
+
test: (decl) => /class\s+\w+/.test(decl),
|
|
83
|
+
nameFrom: (decl) => {
|
|
84
|
+
const match = decl.match(/class\s+(\w+)/);
|
|
85
|
+
if (match && !isMinifiedLike(match[1])) return match[1];
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
confidence: 0.7,
|
|
89
|
+
},
|
|
90
|
+
// Export default function
|
|
91
|
+
{
|
|
92
|
+
test: (decl) => /export\s+default\s+function/.test(decl),
|
|
93
|
+
nameFrom: () => 'defaultExport',
|
|
94
|
+
confidence: 0.5,
|
|
95
|
+
},
|
|
96
|
+
// Named function
|
|
97
|
+
{
|
|
98
|
+
test: (decl) => /function\s+([a-zA-Z_$]\w+)/.test(decl),
|
|
99
|
+
nameFrom: (decl) => {
|
|
100
|
+
const match = decl.match(/function\s+([a-zA-Z_$]\w+)/);
|
|
101
|
+
if (match && !isMinifiedLike(match[1])) return match[1];
|
|
102
|
+
return null;
|
|
103
|
+
},
|
|
104
|
+
confidence: 0.8,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parameter naming rules based on usage context.
|
|
110
|
+
*/
|
|
111
|
+
const PARAM_RULES = [
|
|
112
|
+
{ context: ['.messages', 'systemPrompt', 'canUseTool'], name: 'params', type: 'AgentLoopParams' },
|
|
113
|
+
{ context: ['.method', '.url', '.headers'], name: 'request', type: 'Request' },
|
|
114
|
+
{ context: ['.status', '.json', '.send'], name: 'response', type: 'Response' },
|
|
115
|
+
{ context: ['.next', '.done', '.value'], name: 'iterator', type: 'Iterator' },
|
|
116
|
+
{ context: ['.emit', '.on', '.removeListener'], name: 'emitter', type: 'EventEmitter' },
|
|
117
|
+
{ context: ['.pipe', '.write', '.end'], name: 'stream', type: 'Stream' },
|
|
118
|
+
{ context: ['.query', '.params', '.body'], name: 'req', type: 'Request' },
|
|
119
|
+
{ context: ['.resolve', '.reject'], name: 'promise', type: 'Promise' },
|
|
120
|
+
{ context: ['.name', '.version', '.description'], name: 'packageInfo', type: 'PackageInfo' },
|
|
121
|
+
{ context: ['.key', '.value', '.ttl'], name: 'cacheEntry', type: 'CacheEntry' },
|
|
122
|
+
{ context: ['.token', '.user', '.role'], name: 'session', type: 'Session' },
|
|
123
|
+
{ context: ['.width', '.height', '.x', '.y'], name: 'rect', type: 'Rect' },
|
|
124
|
+
{ context: ['.type', '.data', '.target'], name: 'event', type: 'Event' },
|
|
125
|
+
{ context: ['.path', '.content', '.encoding'], name: 'file', type: 'FileInfo' },
|
|
126
|
+
{ context: ['.host', '.port', '.protocol'], name: 'connectionInfo', type: 'ConnectionInfo' },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a name looks minified (short, no semantic meaning).
|
|
131
|
+
* @param {string} name
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
function isMinifiedLike(name) {
|
|
135
|
+
if (!name) return true;
|
|
136
|
+
return /^[a-zA-Z][a-zA-Z0-9$]{0,2}$/.test(name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Score how well a pattern matches the given context.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} pattern - from claude-code-patterns.json
|
|
143
|
+
* @param {string[]} contextStrings - strings found near the identifier
|
|
144
|
+
* @param {string[]} propertyNames - property accesses found near the identifier
|
|
145
|
+
* @returns {number} 0-1 match score
|
|
146
|
+
*/
|
|
147
|
+
function scorePatternMatch(pattern, contextStrings, propertyNames) {
|
|
148
|
+
let score = 0;
|
|
149
|
+
let totalChecks = 0;
|
|
150
|
+
|
|
151
|
+
// Check context string matches
|
|
152
|
+
if (pattern.context_strings && pattern.context_strings.length > 0) {
|
|
153
|
+
for (const ctx of pattern.context_strings) {
|
|
154
|
+
totalChecks++;
|
|
155
|
+
if (contextStrings.some((s) => s.includes(ctx) || ctx.includes(s))) {
|
|
156
|
+
score++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check property name matches
|
|
162
|
+
if (pattern.property_names && pattern.property_names.length > 0) {
|
|
163
|
+
for (const prop of pattern.property_names) {
|
|
164
|
+
totalChecks++;
|
|
165
|
+
const propAccess = `.${prop}`;
|
|
166
|
+
if (propertyNames.some((p) => p === propAccess || p === prop)) {
|
|
167
|
+
score++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (totalChecks === 0) return 0;
|
|
173
|
+
return score / totalChecks;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Predict a meaningful name for a minified identifier.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} minifiedName - the original short name (e.g. "s$")
|
|
180
|
+
* @param {string[]} contextStrings - strings/properties near the identifier
|
|
181
|
+
* @param {object} [options]
|
|
182
|
+
* @param {string} [options.declaration] - the declaration statement
|
|
183
|
+
* @param {string} [options.patternPath] - path to patterns JSON
|
|
184
|
+
* @param {number} [options.minConfidence=0.3] - minimum confidence to accept
|
|
185
|
+
* @returns {{name: string, confidence: number, source: string}|null}
|
|
186
|
+
*/
|
|
187
|
+
function predictName(minifiedName, contextStrings, options = {}) {
|
|
188
|
+
const { declaration = '', patternPath, minConfidence = 0.3 } = options;
|
|
189
|
+
|
|
190
|
+
// Separate context strings from property accesses
|
|
191
|
+
const props = contextStrings.filter((s) => s.startsWith('.'));
|
|
192
|
+
const strings = contextStrings.filter((s) => !s.startsWith('.'));
|
|
193
|
+
|
|
194
|
+
// 1. Try direct-assignment analysis (highest precision)
|
|
195
|
+
// If X = Y.propertyName, then X should be named "propertyName"
|
|
196
|
+
const directName = inferFromDirectAssignment(minifiedName, declaration);
|
|
197
|
+
if (directName) {
|
|
198
|
+
return { name: directName.name, confidence: directName.confidence, source: 'direct-assign' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 2. Try structural rules (high specificity)
|
|
202
|
+
for (const rule of STRUCTURAL_RULES) {
|
|
203
|
+
if (rule.test(declaration)) {
|
|
204
|
+
const name = rule.nameFrom(declaration, contextStrings);
|
|
205
|
+
if (name && rule.confidence >= minConfidence) {
|
|
206
|
+
return { name, confidence: rule.confidence, source: 'structural' };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 3. Try pattern matching against training data
|
|
212
|
+
const patterns = loadPatterns(patternPath);
|
|
213
|
+
let bestMatch = null;
|
|
214
|
+
let bestScore = 0;
|
|
215
|
+
|
|
216
|
+
for (const pattern of patterns) {
|
|
217
|
+
const matchScore = scorePatternMatch(pattern, strings, props);
|
|
218
|
+
const adjustedScore = matchScore * pattern.confidence;
|
|
219
|
+
|
|
220
|
+
if (adjustedScore > bestScore && adjustedScore >= minConfidence) {
|
|
221
|
+
bestScore = adjustedScore;
|
|
222
|
+
bestMatch = pattern;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (bestMatch) {
|
|
227
|
+
return {
|
|
228
|
+
name: toCamelCase(bestMatch.inferred_name),
|
|
229
|
+
confidence: bestScore,
|
|
230
|
+
source: 'pattern',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 3. Try parameter naming rules
|
|
235
|
+
for (const rule of PARAM_RULES) {
|
|
236
|
+
const matches = rule.context.filter((c) =>
|
|
237
|
+
contextStrings.some((s) => s.includes(c)),
|
|
238
|
+
);
|
|
239
|
+
if (matches.length >= 2) {
|
|
240
|
+
return {
|
|
241
|
+
name: rule.name,
|
|
242
|
+
confidence: 0.6,
|
|
243
|
+
source: 'param-rule',
|
|
244
|
+
type: rule.type,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 4. Heuristic fallbacks based on usage patterns
|
|
250
|
+
const heuristic = heuristicName(minifiedName, contextStrings, declaration);
|
|
251
|
+
if (heuristic && heuristic.confidence >= minConfidence) {
|
|
252
|
+
return heuristic;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Infer a name from direct assignment patterns.
|
|
260
|
+
* let B = A.messages -> B should be "messages"
|
|
261
|
+
* let Z = await Y2(B, G, ...) -> Z might be "result" or inferred from Y2
|
|
262
|
+
* var s$ = async function*(...) -> handled by structural rules
|
|
263
|
+
*
|
|
264
|
+
* @param {string} minifiedName
|
|
265
|
+
* @param {string} declaration
|
|
266
|
+
* @returns {{name: string, confidence: number}|null}
|
|
267
|
+
*/
|
|
268
|
+
function inferFromDirectAssignment(minifiedName, declaration) {
|
|
269
|
+
if (!declaration) return null;
|
|
270
|
+
const escaped = minifiedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
271
|
+
|
|
272
|
+
// Pattern: X = Y.propertyName (property extraction)
|
|
273
|
+
const propMatch = declaration.match(
|
|
274
|
+
new RegExp(`${escaped}\\s*=\\s*\\w+\\.(\\w{2,30})`)
|
|
275
|
+
);
|
|
276
|
+
if (propMatch && propMatch[1].length > 2) {
|
|
277
|
+
return { name: propMatch[1], confidence: 0.85 };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Pattern: X = await Y(...) (function call result)
|
|
281
|
+
const awaitCallMatch = declaration.match(
|
|
282
|
+
new RegExp(`${escaped}\\s*=\\s*await\\s+(\\w+)\\(`)
|
|
283
|
+
);
|
|
284
|
+
if (awaitCallMatch) {
|
|
285
|
+
const fnName = awaitCallMatch[1];
|
|
286
|
+
if (!isMinifiedLike(fnName)) {
|
|
287
|
+
// Infer from function name: createApiRequest -> apiRequest
|
|
288
|
+
const resultName = fnName.replace(/^(create|get|fetch|make|build|load)/, '').replace(/^./, (c) => c.toLowerCase());
|
|
289
|
+
return { name: resultName || 'result', confidence: 0.7 };
|
|
290
|
+
}
|
|
291
|
+
return { name: 'result', confidence: 0.5 };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Pattern: for await (let J of Z) -> J is an item from the iterable
|
|
295
|
+
const forOfMatch = declaration.match(
|
|
296
|
+
new RegExp(`for\\s+await\\s*\\(\\s*(?:let|const|var)\\s+${escaped}\\s+of\\s+(\\w+)`)
|
|
297
|
+
);
|
|
298
|
+
if (forOfMatch) {
|
|
299
|
+
return { name: 'item', confidence: 0.5 };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Pattern: function parameter (first param of function*)
|
|
303
|
+
const funcParamMatch = declaration.match(
|
|
304
|
+
new RegExp(`function\\s*\\*?\\s*\\w*\\s*\\(\\s*${escaped}(?:\\s*,|\\s*\\))`)
|
|
305
|
+
);
|
|
306
|
+
if (funcParamMatch) {
|
|
307
|
+
// First parameter of a function -> "params" or "options"
|
|
308
|
+
return { name: 'params', confidence: 0.5 };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Heuristic name inference from usage patterns.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} minifiedName
|
|
318
|
+
* @param {string[]} contextStrings
|
|
319
|
+
* @param {string} declaration
|
|
320
|
+
* @returns {{name: string, confidence: number, source: string}|null}
|
|
321
|
+
*/
|
|
322
|
+
function heuristicName(minifiedName, contextStrings, declaration) {
|
|
323
|
+
const ctx = contextStrings.join(' ').toLowerCase();
|
|
324
|
+
|
|
325
|
+
// Callback / handler patterns
|
|
326
|
+
if (ctx.includes('callback') || ctx.includes('handler') || ctx.includes('listener')) {
|
|
327
|
+
return { name: 'handler', confidence: 0.4, source: 'heuristic' };
|
|
328
|
+
}
|
|
329
|
+
// Error variable
|
|
330
|
+
if (/catch\s*\(\s*$/.test(declaration) || ctx.includes('error') && ctx.includes('catch')) {
|
|
331
|
+
return { name: 'error', confidence: 0.5, source: 'heuristic' };
|
|
332
|
+
}
|
|
333
|
+
// Iterator / loop variable used with .length or [i]
|
|
334
|
+
if (ctx.includes('.length') && ctx.includes('for')) {
|
|
335
|
+
return { name: 'items', confidence: 0.4, source: 'heuristic' };
|
|
336
|
+
}
|
|
337
|
+
// Result of await
|
|
338
|
+
if (declaration.includes('await')) {
|
|
339
|
+
if (ctx.includes('fetch') || ctx.includes('request')) {
|
|
340
|
+
return { name: 'response', confidence: 0.5, source: 'heuristic' };
|
|
341
|
+
}
|
|
342
|
+
return { name: 'result', confidence: 0.35, source: 'heuristic' };
|
|
343
|
+
}
|
|
344
|
+
// Boolean-looking (used in conditions)
|
|
345
|
+
if (ctx.includes('if') && (ctx.includes('===true') || ctx.includes('===false') || ctx.includes('!!'))) {
|
|
346
|
+
return { name: 'isEnabled', confidence: 0.35, source: 'heuristic' };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Convert a PascalCase or kebab-case name to camelCase.
|
|
354
|
+
* @param {string} name
|
|
355
|
+
* @returns {string}
|
|
356
|
+
*/
|
|
357
|
+
function toCamelCase(name) {
|
|
358
|
+
if (!name) return name;
|
|
359
|
+
// If already camelCase, return as-is
|
|
360
|
+
if (/^[a-z]/.test(name) && !name.includes('-') && !name.includes('_')) return name;
|
|
361
|
+
// PascalCase -> camelCase
|
|
362
|
+
if (/^[A-Z]/.test(name) && !name.includes('-') && !name.includes('_')) {
|
|
363
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
364
|
+
}
|
|
365
|
+
// kebab-case or snake_case -> camelCase
|
|
366
|
+
return name.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toLowerCase());
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Infer a parameter name from its position and usage.
|
|
371
|
+
* @param {number} index - parameter position (0-based)
|
|
372
|
+
* @param {string[]} contextStrings
|
|
373
|
+
* @returns {string}
|
|
374
|
+
*/
|
|
375
|
+
function inferParamName(index, contextStrings) {
|
|
376
|
+
// Try param rules
|
|
377
|
+
for (const rule of PARAM_RULES) {
|
|
378
|
+
const matches = rule.context.filter((c) =>
|
|
379
|
+
contextStrings.some((s) => s.includes(c)),
|
|
380
|
+
);
|
|
381
|
+
if (matches.length >= 1) {
|
|
382
|
+
return rule.name;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Generic fallback
|
|
387
|
+
const fallbacks = ['param', 'value', 'data', 'input', 'arg', 'item'];
|
|
388
|
+
return fallbacks[index] || `param${index}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
predictName,
|
|
393
|
+
loadPatterns,
|
|
394
|
+
scorePatternMatch,
|
|
395
|
+
inferParamName,
|
|
396
|
+
toCamelCase,
|
|
397
|
+
isMinifiedLike,
|
|
398
|
+
STRUCTURAL_RULES,
|
|
399
|
+
PARAM_RULES,
|
|
400
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm-fetch.js - Fetch package info and files from the npm registry.
|
|
3
|
+
*
|
|
4
|
+
* Uses the built-in Node 18+ fetch API. Retrieves package metadata from
|
|
5
|
+
* registry.npmjs.org and file contents from unpkg.com / jsdelivr.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const REGISTRY_BASE = 'https://registry.npmjs.org';
|
|
11
|
+
const JSDELIVR_BASE = 'https://data.jsdelivr.com/v1/package/npm';
|
|
12
|
+
const UNPKG_BASE = 'https://unpkg.com';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetch package metadata from the npm registry.
|
|
16
|
+
* @param {string} packageName - npm package name (e.g. 'express', '@anthropic-ai/claude-code')
|
|
17
|
+
* @returns {Promise<{name: string, description: string, versions: string[], latest: string, distTags: object, packageJson: object}>}
|
|
18
|
+
*/
|
|
19
|
+
async function fetchPackageInfo(packageName) {
|
|
20
|
+
const url = `${REGISTRY_BASE}/${encodeURIComponent(packageName).replace('%40', '@')}`;
|
|
21
|
+
const resp = await fetch(url, {
|
|
22
|
+
headers: { Accept: 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!resp.ok) {
|
|
26
|
+
throw new Error(`Package "${packageName}" not found (HTTP ${resp.status})`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = await resp.json();
|
|
30
|
+
const versions = Object.keys(data.versions || {}).reverse();
|
|
31
|
+
const distTags = data['dist-tags'] || {};
|
|
32
|
+
const latest = distTags.latest || versions[0] || '';
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
name: data.name,
|
|
36
|
+
description: data.description || '',
|
|
37
|
+
versions,
|
|
38
|
+
latest,
|
|
39
|
+
distTags,
|
|
40
|
+
packageJson: data.versions?.[latest] || {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch the file list for a specific package version via jsDelivr.
|
|
46
|
+
* @param {string} packageName
|
|
47
|
+
* @param {string} version
|
|
48
|
+
* @returns {Promise<Array<{name: string, hash: string, size: number}>>}
|
|
49
|
+
*/
|
|
50
|
+
async function fetchPackageFileList(packageName, version) {
|
|
51
|
+
const encodedName = encodeURIComponent(packageName).replace('%40', '@');
|
|
52
|
+
const url = `${JSDELIVR_BASE}/${encodedName}@${version}/flat`;
|
|
53
|
+
const resp = await fetch(url);
|
|
54
|
+
if (!resp.ok) {
|
|
55
|
+
throw new Error(`Could not list files for ${packageName}@${version}`);
|
|
56
|
+
}
|
|
57
|
+
const data = await resp.json();
|
|
58
|
+
return (data.files || []);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fetch the content of a single file from unpkg.
|
|
63
|
+
* @param {string} packageName
|
|
64
|
+
* @param {string} version
|
|
65
|
+
* @param {string} filePath - e.g. '/dist/index.js'
|
|
66
|
+
* @returns {Promise<string>}
|
|
67
|
+
*/
|
|
68
|
+
async function fetchFileContent(packageName, version, filePath) {
|
|
69
|
+
const cleanPath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
70
|
+
const url = `${UNPKG_BASE}/${packageName}@${version}/${cleanPath}`;
|
|
71
|
+
const resp = await fetch(url, { redirect: 'follow' });
|
|
72
|
+
if (!resp.ok) {
|
|
73
|
+
throw new Error(`Could not fetch ${cleanPath} from ${packageName}@${version} (HTTP ${resp.status})`);
|
|
74
|
+
}
|
|
75
|
+
return resp.text();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find the main JS bundle file from a file list.
|
|
80
|
+
* Checks common locations and falls back to the largest .js file.
|
|
81
|
+
* @param {Array<{name: string, size: number}>} files
|
|
82
|
+
* @param {object} [packageJson] - optional package.json to check main/module/browser fields
|
|
83
|
+
* @returns {string|null}
|
|
84
|
+
*/
|
|
85
|
+
function findMainBundle(files, packageJson) {
|
|
86
|
+
// Check package.json fields first
|
|
87
|
+
if (packageJson) {
|
|
88
|
+
const fields = ['browser', 'module', 'main'];
|
|
89
|
+
for (const field of fields) {
|
|
90
|
+
if (typeof packageJson[field] === 'string') {
|
|
91
|
+
const normalized = packageJson[field].startsWith('/')
|
|
92
|
+
? packageJson[field]
|
|
93
|
+
: '/' + packageJson[field];
|
|
94
|
+
if (files.some((f) => f.name === normalized)) {
|
|
95
|
+
return normalized;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Well-known candidate paths
|
|
102
|
+
const candidates = [
|
|
103
|
+
'/dist/cli.js',
|
|
104
|
+
'/dist/index.js',
|
|
105
|
+
'/dist/main.js',
|
|
106
|
+
'/dist/bundle.js',
|
|
107
|
+
'/lib/index.js',
|
|
108
|
+
'/lib/cli.js',
|
|
109
|
+
'/index.js',
|
|
110
|
+
'/cli.js',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
if (files.some((f) => f.name === candidate)) {
|
|
115
|
+
return candidate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fallback: largest JS file (prefer non-minified)
|
|
120
|
+
const jsFiles = files
|
|
121
|
+
.filter((f) => f.name.endsWith('.js') && !f.name.endsWith('.min.js'))
|
|
122
|
+
.sort((a, b) => b.size - a.size);
|
|
123
|
+
|
|
124
|
+
return jsFiles.length > 0 ? jsFiles[0].name : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse a target string into its components.
|
|
129
|
+
* Handles: 'express', 'express@4.18.2', '@scope/pkg@1.0.0',
|
|
130
|
+
* './local.js', 'https://...'.
|
|
131
|
+
* @param {string} target
|
|
132
|
+
* @returns {{type: 'npm'|'file'|'url', name?: string, version?: string, path?: string, url?: string}}
|
|
133
|
+
*/
|
|
134
|
+
function parseTarget(target) {
|
|
135
|
+
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
136
|
+
return { type: 'url', url: target };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (target.startsWith('.') || target.startsWith('/')) {
|
|
140
|
+
return { type: 'file', path: target };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// npm package: @scope/name@version or name@version
|
|
144
|
+
let name = target;
|
|
145
|
+
let version = undefined;
|
|
146
|
+
if (target.startsWith('@')) {
|
|
147
|
+
// scoped: @scope/name@version
|
|
148
|
+
const afterScope = target.indexOf('/', 1);
|
|
149
|
+
if (afterScope > 0) {
|
|
150
|
+
const atIdx = target.indexOf('@', afterScope + 1);
|
|
151
|
+
if (atIdx > 0) {
|
|
152
|
+
name = target.slice(0, atIdx);
|
|
153
|
+
version = target.slice(atIdx + 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
const atIdx = target.indexOf('@');
|
|
158
|
+
if (atIdx > 0) {
|
|
159
|
+
name = target.slice(0, atIdx);
|
|
160
|
+
version = target.slice(atIdx + 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { type: 'npm', name, version };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
fetchPackageInfo,
|
|
169
|
+
fetchPackageFileList,
|
|
170
|
+
fetchFileContent,
|
|
171
|
+
findMainBundle,
|
|
172
|
+
parseTarget,
|
|
173
|
+
REGISTRY_BASE,
|
|
174
|
+
UNPKG_BASE,
|
|
175
|
+
JSDELIVR_BASE,
|
|
176
|
+
};
|