backend-diet 1.0.0
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.
Potentially problematic release.
This version of backend-diet might be problematic. Click here for more details.
- package/README.md +224 -0
- package/bin/pkg-diet.js +4 -0
- package/dist/cli.js +2021 -0
- package/dist/cli.js.map +1 -0
- package/package.json +71 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2021 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_ora = __toESM(require("ora"));
|
|
29
|
+
var import_path6 = __toESM(require("path"));
|
|
30
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
31
|
+
|
|
32
|
+
// src/scanner/walker.ts
|
|
33
|
+
var import_fast_glob = __toESM(require("fast-glob"));
|
|
34
|
+
var import_path = __toESM(require("path"));
|
|
35
|
+
var SOURCE_EXTENSIONS = ["js", "ts", "jsx", "tsx", "mjs", "cjs"];
|
|
36
|
+
var DEFAULT_IGNORE = [
|
|
37
|
+
"**/node_modules/**",
|
|
38
|
+
"**/dist/**",
|
|
39
|
+
"**/build/**",
|
|
40
|
+
"**/.next/**",
|
|
41
|
+
"**/.nuxt/**",
|
|
42
|
+
"**/coverage/**",
|
|
43
|
+
"**/*.min.js",
|
|
44
|
+
"**/*.bundle.js"
|
|
45
|
+
];
|
|
46
|
+
async function walkFiles(targetDir, extraIgnore = []) {
|
|
47
|
+
const pattern = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
48
|
+
const ignore = [...DEFAULT_IGNORE, ...extraIgnore];
|
|
49
|
+
const files = await (0, import_fast_glob.default)(pattern, {
|
|
50
|
+
cwd: import_path.default.resolve(targetDir),
|
|
51
|
+
absolute: true,
|
|
52
|
+
ignore,
|
|
53
|
+
followSymbolicLinks: false,
|
|
54
|
+
onlyFiles: true
|
|
55
|
+
});
|
|
56
|
+
return files.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/scanner/parser.ts
|
|
60
|
+
var babelParser = __toESM(require("@babel/parser"));
|
|
61
|
+
var import_traverse = __toESM(require("@babel/traverse"));
|
|
62
|
+
var t = __toESM(require("@babel/types"));
|
|
63
|
+
var import_fs = __toESM(require("fs"));
|
|
64
|
+
var import_path2 = __toESM(require("path"));
|
|
65
|
+
var BABEL_PLUGINS = [
|
|
66
|
+
"typescript",
|
|
67
|
+
"jsx",
|
|
68
|
+
["decorators", { decoratorsBeforeExport: true }],
|
|
69
|
+
"classProperties",
|
|
70
|
+
"classPrivateProperties",
|
|
71
|
+
"classPrivateMethods",
|
|
72
|
+
"exportDefaultFrom",
|
|
73
|
+
"exportNamespaceFrom",
|
|
74
|
+
"dynamicImport",
|
|
75
|
+
"nullishCoalescingOperator",
|
|
76
|
+
"optionalChaining",
|
|
77
|
+
"optionalCatchBinding",
|
|
78
|
+
"logicalAssignment",
|
|
79
|
+
"numericSeparator",
|
|
80
|
+
"bigInt",
|
|
81
|
+
"importMeta"
|
|
82
|
+
];
|
|
83
|
+
function safeParseCode(code) {
|
|
84
|
+
try {
|
|
85
|
+
return babelParser.parse(code, {
|
|
86
|
+
sourceType: "module",
|
|
87
|
+
allowImportExportEverywhere: true,
|
|
88
|
+
allowReturnOutsideFunction: true,
|
|
89
|
+
allowSuperOutsideMethod: true,
|
|
90
|
+
plugins: BABEL_PLUGINS,
|
|
91
|
+
errorRecovery: true
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
try {
|
|
95
|
+
return babelParser.parse(code, {
|
|
96
|
+
sourceType: "script",
|
|
97
|
+
allowImportExportEverywhere: true,
|
|
98
|
+
allowReturnOutsideFunction: true,
|
|
99
|
+
plugins: ["jsx", "classProperties"],
|
|
100
|
+
errorRecovery: true
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function normalizePackageName(source) {
|
|
108
|
+
if (source.startsWith("@")) {
|
|
109
|
+
const parts = source.split("/");
|
|
110
|
+
return parts.slice(0, 2).join("/");
|
|
111
|
+
}
|
|
112
|
+
return source.split("/")[0];
|
|
113
|
+
}
|
|
114
|
+
function getSubPath(source) {
|
|
115
|
+
const parts = source.split("/");
|
|
116
|
+
if (source.startsWith("@") && parts.length > 2) {
|
|
117
|
+
return parts.slice(2).join("/");
|
|
118
|
+
}
|
|
119
|
+
if (!source.startsWith("@") && parts.length > 1) {
|
|
120
|
+
return parts.slice(1).join("/");
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function parseFile(filePath) {
|
|
125
|
+
const records = [];
|
|
126
|
+
let code;
|
|
127
|
+
try {
|
|
128
|
+
code = import_fs.default.readFileSync(filePath, "utf-8");
|
|
129
|
+
} catch {
|
|
130
|
+
return records;
|
|
131
|
+
}
|
|
132
|
+
const ast = safeParseCode(code);
|
|
133
|
+
if (!ast) return records;
|
|
134
|
+
const relPath = import_path2.default.relative(process.cwd(), filePath);
|
|
135
|
+
(0, import_traverse.default)(ast, {
|
|
136
|
+
// Pattern 1: import { cloneDeep, merge } from 'lodash'
|
|
137
|
+
// Pattern 2: import cloneDeep from 'lodash/cloneDeep'
|
|
138
|
+
ImportDeclaration(nodePath) {
|
|
139
|
+
const source = nodePath.node.source.value;
|
|
140
|
+
if (!isThirdParty(source)) return;
|
|
141
|
+
const packageName = normalizePackageName(source);
|
|
142
|
+
const subPath = getSubPath(source);
|
|
143
|
+
const line = nodePath.node.loc?.start.line ?? 0;
|
|
144
|
+
const col = nodePath.node.loc?.start.column ?? 0;
|
|
145
|
+
const raw = code.split("\n")[line - 1]?.trim() ?? "";
|
|
146
|
+
for (const specifier of nodePath.node.specifiers) {
|
|
147
|
+
let functionName;
|
|
148
|
+
if (t.isImportSpecifier(specifier)) {
|
|
149
|
+
functionName = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
|
|
150
|
+
} else if (t.isImportDefaultSpecifier(specifier)) {
|
|
151
|
+
functionName = subPath ?? specifier.local.name;
|
|
152
|
+
} else if (t.isImportNamespaceSpecifier(specifier)) {
|
|
153
|
+
functionName = "*";
|
|
154
|
+
} else {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
records.push({ file: relPath, packageName, functionName, line, col, raw });
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
// Pattern 3: const { cloneDeep } = require('lodash')
|
|
161
|
+
VariableDeclarator(nodePath) {
|
|
162
|
+
const init = nodePath.node.init;
|
|
163
|
+
if (!t.isCallExpression(init)) return;
|
|
164
|
+
if (!t.isIdentifier(init.callee, { name: "require" })) return;
|
|
165
|
+
if (init.arguments.length === 0) return;
|
|
166
|
+
const arg = init.arguments[0];
|
|
167
|
+
if (!t.isStringLiteral(arg)) return;
|
|
168
|
+
if (!isThirdParty(arg.value)) return;
|
|
169
|
+
const source = arg.value;
|
|
170
|
+
const packageName = normalizePackageName(source);
|
|
171
|
+
const subPath = getSubPath(source);
|
|
172
|
+
const line = nodePath.node.loc?.start.line ?? 0;
|
|
173
|
+
const col = nodePath.node.loc?.start.column ?? 0;
|
|
174
|
+
const raw = code.split("\n")[line - 1]?.trim() ?? "";
|
|
175
|
+
if (t.isObjectPattern(nodePath.node.id)) {
|
|
176
|
+
for (const prop of nodePath.node.id.properties) {
|
|
177
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
178
|
+
records.push({
|
|
179
|
+
file: relPath,
|
|
180
|
+
packageName,
|
|
181
|
+
functionName: prop.key.name,
|
|
182
|
+
line,
|
|
183
|
+
col,
|
|
184
|
+
raw
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else if (t.isIdentifier(nodePath.node.id)) {
|
|
189
|
+
const functionName = subPath ?? "*";
|
|
190
|
+
records.push({ file: relPath, packageName, functionName, line, col, raw });
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
// Pattern 4: _.cloneDeep(obj) — namespace call detection
|
|
194
|
+
CallExpression(nodePath) {
|
|
195
|
+
const callee = nodePath.node.callee;
|
|
196
|
+
if (!t.isMemberExpression(callee)) return;
|
|
197
|
+
if (!t.isIdentifier(callee.object)) return;
|
|
198
|
+
if (!t.isIdentifier(callee.property)) return;
|
|
199
|
+
const nsName = callee.object.name;
|
|
200
|
+
const methodName = callee.property.name;
|
|
201
|
+
const line = nodePath.node.loc?.start.line ?? 0;
|
|
202
|
+
const col = nodePath.node.loc?.start.column ?? 0;
|
|
203
|
+
const raw = code.split("\n")[line - 1]?.trim() ?? "";
|
|
204
|
+
records.push({
|
|
205
|
+
file: relPath,
|
|
206
|
+
packageName: `__ns__${nsName}`,
|
|
207
|
+
functionName: methodName,
|
|
208
|
+
line,
|
|
209
|
+
col,
|
|
210
|
+
raw
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return records;
|
|
215
|
+
}
|
|
216
|
+
function isThirdParty(source) {
|
|
217
|
+
if (source.startsWith(".")) return false;
|
|
218
|
+
if (source.startsWith("/")) return false;
|
|
219
|
+
if (source.startsWith("node:")) return false;
|
|
220
|
+
const builtins = /* @__PURE__ */ new Set([
|
|
221
|
+
"fs",
|
|
222
|
+
"path",
|
|
223
|
+
"os",
|
|
224
|
+
"crypto",
|
|
225
|
+
"http",
|
|
226
|
+
"https",
|
|
227
|
+
"url",
|
|
228
|
+
"util",
|
|
229
|
+
"stream",
|
|
230
|
+
"child_process",
|
|
231
|
+
"cluster",
|
|
232
|
+
"events",
|
|
233
|
+
"net",
|
|
234
|
+
"tls",
|
|
235
|
+
"dns",
|
|
236
|
+
"readline",
|
|
237
|
+
"repl",
|
|
238
|
+
"vm",
|
|
239
|
+
"zlib",
|
|
240
|
+
"buffer",
|
|
241
|
+
"assert",
|
|
242
|
+
"perf_hooks",
|
|
243
|
+
"worker_threads",
|
|
244
|
+
"module",
|
|
245
|
+
"process",
|
|
246
|
+
"v8",
|
|
247
|
+
"inspector"
|
|
248
|
+
]);
|
|
249
|
+
return !builtins.has(source.split("/")[0]);
|
|
250
|
+
}
|
|
251
|
+
async function parseFiles(files) {
|
|
252
|
+
const allRecords = [];
|
|
253
|
+
for (const file of files) {
|
|
254
|
+
const records = parseFile(file);
|
|
255
|
+
allRecords.push(...records);
|
|
256
|
+
}
|
|
257
|
+
return allRecords;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/database/packs/lodash.ts
|
|
261
|
+
var lodash = {
|
|
262
|
+
packageName: "lodash",
|
|
263
|
+
description: "A modern JavaScript utility library delivering modularity, performance & extras.",
|
|
264
|
+
gzippedSize: 24e3,
|
|
265
|
+
canFullyReplace: true,
|
|
266
|
+
functions: [
|
|
267
|
+
{
|
|
268
|
+
name: "cloneDeep",
|
|
269
|
+
importPaths: ["lodash/cloneDeep"],
|
|
270
|
+
alternatives: [
|
|
271
|
+
{
|
|
272
|
+
api: "structuredClone",
|
|
273
|
+
minNodeVersion: "17.0.0",
|
|
274
|
+
bytesSaved: 3200,
|
|
275
|
+
difficulty: "trivial",
|
|
276
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/API/structuredClone",
|
|
277
|
+
snippet: {
|
|
278
|
+
before: `import { cloneDeep } from 'lodash';
|
|
279
|
+
const copy = cloneDeep(obj);`,
|
|
280
|
+
after: `const copy = structuredClone(obj);`
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "merge",
|
|
287
|
+
importPaths: ["lodash/merge"],
|
|
288
|
+
alternatives: [
|
|
289
|
+
{
|
|
290
|
+
api: "Object.assign / spread",
|
|
291
|
+
minNodeVersion: "4.0.0",
|
|
292
|
+
bytesSaved: 900,
|
|
293
|
+
difficulty: "easy",
|
|
294
|
+
snippet: {
|
|
295
|
+
before: `import { merge } from 'lodash';
|
|
296
|
+
const result = merge({}, defaults, overrides);`,
|
|
297
|
+
after: `const result = { ...defaults, ...overrides };`
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: "get",
|
|
304
|
+
importPaths: ["lodash/get"],
|
|
305
|
+
alternatives: [
|
|
306
|
+
{
|
|
307
|
+
api: "Optional Chaining (?.)",
|
|
308
|
+
minNodeVersion: "14.0.0",
|
|
309
|
+
bytesSaved: 700,
|
|
310
|
+
difficulty: "trivial",
|
|
311
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining",
|
|
312
|
+
snippet: {
|
|
313
|
+
before: `import { get } from 'lodash';
|
|
314
|
+
const val = get(obj, 'a.b.c', defaultVal);`,
|
|
315
|
+
after: `const val = obj?.a?.b?.c ?? defaultVal;`
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: "flatten",
|
|
322
|
+
importPaths: ["lodash/flatten"],
|
|
323
|
+
alternatives: [
|
|
324
|
+
{
|
|
325
|
+
api: "Array.prototype.flat",
|
|
326
|
+
minNodeVersion: "11.0.0",
|
|
327
|
+
bytesSaved: 400,
|
|
328
|
+
difficulty: "trivial",
|
|
329
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat",
|
|
330
|
+
snippet: {
|
|
331
|
+
before: `import { flatten } from 'lodash';
|
|
332
|
+
const flat = flatten(arr);`,
|
|
333
|
+
after: `const flat = arr.flat();`
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "flattenDeep",
|
|
340
|
+
importPaths: ["lodash/flattenDeep"],
|
|
341
|
+
alternatives: [
|
|
342
|
+
{
|
|
343
|
+
api: "Array.prototype.flat(Infinity)",
|
|
344
|
+
minNodeVersion: "11.0.0",
|
|
345
|
+
bytesSaved: 500,
|
|
346
|
+
difficulty: "trivial",
|
|
347
|
+
snippet: {
|
|
348
|
+
before: `import { flattenDeep } from 'lodash';
|
|
349
|
+
const flat = flattenDeep(arr);`,
|
|
350
|
+
after: `const flat = arr.flat(Infinity);`
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "uniq",
|
|
357
|
+
importPaths: ["lodash/uniq"],
|
|
358
|
+
alternatives: [
|
|
359
|
+
{
|
|
360
|
+
api: "Set",
|
|
361
|
+
minNodeVersion: "4.0.0",
|
|
362
|
+
bytesSaved: 350,
|
|
363
|
+
difficulty: "trivial",
|
|
364
|
+
snippet: {
|
|
365
|
+
before: `import { uniq } from 'lodash';
|
|
366
|
+
const unique = uniq(arr);`,
|
|
367
|
+
after: `const unique = [...new Set(arr)];`
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "uniqBy",
|
|
374
|
+
importPaths: ["lodash/uniqBy"],
|
|
375
|
+
alternatives: [
|
|
376
|
+
{
|
|
377
|
+
api: "Map + Array.from",
|
|
378
|
+
minNodeVersion: "4.0.0",
|
|
379
|
+
bytesSaved: 450,
|
|
380
|
+
difficulty: "easy",
|
|
381
|
+
snippet: {
|
|
382
|
+
before: `import { uniqBy } from 'lodash';
|
|
383
|
+
const unique = uniqBy(arr, (x) => x.id);`,
|
|
384
|
+
after: `const unique = Array.from(new Map(arr.map((x) => [x.id, x])).values());`
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "debounce",
|
|
391
|
+
importPaths: ["lodash/debounce"],
|
|
392
|
+
alternatives: [
|
|
393
|
+
{
|
|
394
|
+
api: "Custom closure",
|
|
395
|
+
minNodeVersion: "4.0.0",
|
|
396
|
+
bytesSaved: 600,
|
|
397
|
+
difficulty: "easy",
|
|
398
|
+
snippet: {
|
|
399
|
+
before: `import { debounce } from 'lodash';
|
|
400
|
+
const handler = debounce(fn, 300);`,
|
|
401
|
+
after: `function debounce(fn, ms) {
|
|
402
|
+
let timer;
|
|
403
|
+
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); };
|
|
404
|
+
}
|
|
405
|
+
const handler = debounce(fn, 300);`
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "throttle",
|
|
412
|
+
importPaths: ["lodash/throttle"],
|
|
413
|
+
alternatives: [
|
|
414
|
+
{
|
|
415
|
+
api: "Custom closure",
|
|
416
|
+
minNodeVersion: "4.0.0",
|
|
417
|
+
bytesSaved: 600,
|
|
418
|
+
difficulty: "easy",
|
|
419
|
+
snippet: {
|
|
420
|
+
before: `import { throttle } from 'lodash';
|
|
421
|
+
const handler = throttle(fn, 300);`,
|
|
422
|
+
after: `function throttle(fn, ms) {
|
|
423
|
+
let last = 0;
|
|
424
|
+
return (...args) => { const now = Date.now(); if (now - last >= ms) { last = now; fn(...args); } };
|
|
425
|
+
}
|
|
426
|
+
const handler = throttle(fn, 300);`
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "isEmpty",
|
|
433
|
+
importPaths: ["lodash/isEmpty"],
|
|
434
|
+
alternatives: [
|
|
435
|
+
{
|
|
436
|
+
api: "Native checks",
|
|
437
|
+
minNodeVersion: "4.0.0",
|
|
438
|
+
bytesSaved: 300,
|
|
439
|
+
difficulty: "easy",
|
|
440
|
+
snippet: {
|
|
441
|
+
before: `import { isEmpty } from 'lodash';
|
|
442
|
+
if (isEmpty(value)) { ... }`,
|
|
443
|
+
after: `// For arrays:
|
|
444
|
+
if (!arr.length) { ... }
|
|
445
|
+
// For objects:
|
|
446
|
+
if (!Object.keys(obj).length) { ... }
|
|
447
|
+
// For strings:
|
|
448
|
+
if (!str) { ... }`
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "isEqual",
|
|
455
|
+
importPaths: ["lodash/isEqual"],
|
|
456
|
+
alternatives: [
|
|
457
|
+
{
|
|
458
|
+
api: "JSON.stringify (shallow use case)",
|
|
459
|
+
minNodeVersion: "4.0.0",
|
|
460
|
+
bytesSaved: 800,
|
|
461
|
+
difficulty: "moderate",
|
|
462
|
+
snippet: {
|
|
463
|
+
before: `import { isEqual } from 'lodash';
|
|
464
|
+
if (isEqual(a, b)) { ... }`,
|
|
465
|
+
after: `// For simple serializable objects (no functions/undefined):
|
|
466
|
+
if (JSON.stringify(a) === JSON.stringify(b)) { ... }
|
|
467
|
+
// For deep equality with full support, keep lodash.isEqual.`
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "omit",
|
|
474
|
+
importPaths: ["lodash/omit"],
|
|
475
|
+
alternatives: [
|
|
476
|
+
{
|
|
477
|
+
api: "Destructuring",
|
|
478
|
+
minNodeVersion: "6.0.0",
|
|
479
|
+
bytesSaved: 350,
|
|
480
|
+
difficulty: "easy",
|
|
481
|
+
snippet: {
|
|
482
|
+
before: `import { omit } from 'lodash';
|
|
483
|
+
const clean = omit(obj, ['password', 'token']);`,
|
|
484
|
+
after: `const { password, token, ...clean } = obj;`
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "pick",
|
|
491
|
+
importPaths: ["lodash/pick"],
|
|
492
|
+
alternatives: [
|
|
493
|
+
{
|
|
494
|
+
api: "Object.fromEntries",
|
|
495
|
+
minNodeVersion: "12.0.0",
|
|
496
|
+
bytesSaved: 350,
|
|
497
|
+
difficulty: "easy",
|
|
498
|
+
snippet: {
|
|
499
|
+
before: `import { pick } from 'lodash';
|
|
500
|
+
const subset = pick(obj, ['name', 'age']);`,
|
|
501
|
+
after: `const keys = ['name', 'age'];
|
|
502
|
+
const subset = Object.fromEntries(keys.map((k) => [k, obj[k]]));`
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: "chunk",
|
|
509
|
+
importPaths: ["lodash/chunk"],
|
|
510
|
+
alternatives: [
|
|
511
|
+
{
|
|
512
|
+
api: "Array.from + slice",
|
|
513
|
+
minNodeVersion: "4.0.0",
|
|
514
|
+
bytesSaved: 400,
|
|
515
|
+
difficulty: "easy",
|
|
516
|
+
snippet: {
|
|
517
|
+
before: `import { chunk } from 'lodash';
|
|
518
|
+
const chunks = chunk(arr, 3);`,
|
|
519
|
+
after: `const chunk = (arr, size) =>
|
|
520
|
+
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
|
|
521
|
+
arr.slice(i * size, i * size + size)
|
|
522
|
+
);
|
|
523
|
+
const chunks = chunk(arr, 3);`
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "groupBy",
|
|
530
|
+
importPaths: ["lodash/groupBy"],
|
|
531
|
+
alternatives: [
|
|
532
|
+
{
|
|
533
|
+
api: "Array.prototype.reduce",
|
|
534
|
+
minNodeVersion: "4.0.0",
|
|
535
|
+
bytesSaved: 500,
|
|
536
|
+
difficulty: "easy",
|
|
537
|
+
snippet: {
|
|
538
|
+
before: `import { groupBy } from 'lodash';
|
|
539
|
+
const groups = groupBy(arr, (x) => x.category);`,
|
|
540
|
+
after: `const groups = arr.reduce((acc, x) => {
|
|
541
|
+
(acc[x.category] ??= []).push(x);
|
|
542
|
+
return acc;
|
|
543
|
+
}, {});`
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "sortBy",
|
|
550
|
+
importPaths: ["lodash/sortBy"],
|
|
551
|
+
alternatives: [
|
|
552
|
+
{
|
|
553
|
+
api: "Array.prototype.sort",
|
|
554
|
+
minNodeVersion: "4.0.0",
|
|
555
|
+
bytesSaved: 400,
|
|
556
|
+
difficulty: "easy",
|
|
557
|
+
snippet: {
|
|
558
|
+
before: `import { sortBy } from 'lodash';
|
|
559
|
+
const sorted = sortBy(arr, (x) => x.name);`,
|
|
560
|
+
after: `const sorted = [...arr].sort((a, b) => a.name.localeCompare(b.name));`
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
]
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "mapValues",
|
|
567
|
+
importPaths: ["lodash/mapValues"],
|
|
568
|
+
alternatives: [
|
|
569
|
+
{
|
|
570
|
+
api: "Object.fromEntries + Object.entries",
|
|
571
|
+
minNodeVersion: "12.0.0",
|
|
572
|
+
bytesSaved: 350,
|
|
573
|
+
difficulty: "easy",
|
|
574
|
+
snippet: {
|
|
575
|
+
before: `import { mapValues } from 'lodash';
|
|
576
|
+
const result = mapValues(obj, (v) => v * 2);`,
|
|
577
|
+
after: `const result = Object.fromEntries(
|
|
578
|
+
Object.entries(obj).map(([k, v]) => [k, v * 2])
|
|
579
|
+
);`
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "keyBy",
|
|
586
|
+
importPaths: ["lodash/keyBy"],
|
|
587
|
+
alternatives: [
|
|
588
|
+
{
|
|
589
|
+
api: "Array.prototype.reduce",
|
|
590
|
+
minNodeVersion: "4.0.0",
|
|
591
|
+
bytesSaved: 400,
|
|
592
|
+
difficulty: "easy",
|
|
593
|
+
snippet: {
|
|
594
|
+
before: `import { keyBy } from 'lodash';
|
|
595
|
+
const byId = keyBy(users, 'id');`,
|
|
596
|
+
after: `const byId = users.reduce((acc, u) => ({ ...acc, [u.id]: u }), {});`
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: "startCase",
|
|
603
|
+
importPaths: ["lodash/startCase"],
|
|
604
|
+
alternatives: [
|
|
605
|
+
{
|
|
606
|
+
api: "Regex + replace",
|
|
607
|
+
minNodeVersion: "4.0.0",
|
|
608
|
+
bytesSaved: 300,
|
|
609
|
+
difficulty: "moderate",
|
|
610
|
+
snippet: {
|
|
611
|
+
before: `import { startCase } from 'lodash';
|
|
612
|
+
const title = startCase('helloWorld');`,
|
|
613
|
+
after: `const startCase = (s) =>
|
|
614
|
+
s.replace(/([A-Z])/g, ' $1')
|
|
615
|
+
.replace(/[-_]/g, ' ')
|
|
616
|
+
.replace(/\\b\\w/g, (c) => c.toUpperCase())
|
|
617
|
+
.trim();
|
|
618
|
+
const title = startCase('helloWorld');`
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
]
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: "camelCase",
|
|
625
|
+
importPaths: ["lodash/camelCase"],
|
|
626
|
+
alternatives: [
|
|
627
|
+
{
|
|
628
|
+
api: "Regex + replace",
|
|
629
|
+
minNodeVersion: "4.0.0",
|
|
630
|
+
bytesSaved: 300,
|
|
631
|
+
difficulty: "moderate",
|
|
632
|
+
snippet: {
|
|
633
|
+
before: `import { camelCase } from 'lodash';
|
|
634
|
+
const name = camelCase('hello-world');`,
|
|
635
|
+
after: `const camelCase = (s) =>
|
|
636
|
+
s.toLowerCase().replace(/[-_\\s]+(\\w)/g, (_, c) => c.toUpperCase());
|
|
637
|
+
const name = camelCase('hello-world');`
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
]
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// src/database/packs/moment.ts
|
|
646
|
+
var moment = {
|
|
647
|
+
packageName: "moment",
|
|
648
|
+
description: "Parse, validate, manipulate, and display dates in JavaScript.",
|
|
649
|
+
gzippedSize: 72e3,
|
|
650
|
+
canFullyReplace: true,
|
|
651
|
+
functions: [
|
|
652
|
+
{
|
|
653
|
+
name: "default",
|
|
654
|
+
alternatives: [
|
|
655
|
+
{
|
|
656
|
+
api: "Intl.DateTimeFormat / Date",
|
|
657
|
+
minNodeVersion: "12.0.0",
|
|
658
|
+
bytesSaved: 72e3,
|
|
659
|
+
difficulty: "moderate",
|
|
660
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat",
|
|
661
|
+
snippet: {
|
|
662
|
+
before: `import moment from 'moment';
|
|
663
|
+
const formatted = moment().format('YYYY-MM-DD');`,
|
|
664
|
+
after: `const formatted = new Date().toISOString().slice(0, 10);
|
|
665
|
+
// Or with Intl:
|
|
666
|
+
const formatted = new Intl.DateTimeFormat('en-CA').format(new Date());`
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "format",
|
|
673
|
+
alternatives: [
|
|
674
|
+
{
|
|
675
|
+
api: "Intl.DateTimeFormat",
|
|
676
|
+
minNodeVersion: "12.0.0",
|
|
677
|
+
bytesSaved: 72e3,
|
|
678
|
+
difficulty: "moderate",
|
|
679
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat",
|
|
680
|
+
snippet: {
|
|
681
|
+
before: `moment(date).format('MMM D, YYYY')`,
|
|
682
|
+
after: `new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date)`
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
]
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: "diff",
|
|
689
|
+
alternatives: [
|
|
690
|
+
{
|
|
691
|
+
api: "Date arithmetic",
|
|
692
|
+
minNodeVersion: "4.0.0",
|
|
693
|
+
bytesSaved: 5e3,
|
|
694
|
+
difficulty: "easy",
|
|
695
|
+
snippet: {
|
|
696
|
+
before: `moment(end).diff(moment(start), 'days')`,
|
|
697
|
+
after: `Math.round((new Date(end) - new Date(start)) / (1000 * 60 * 60 * 24))`
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
]
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "add",
|
|
704
|
+
alternatives: [
|
|
705
|
+
{
|
|
706
|
+
api: "Date manipulation",
|
|
707
|
+
minNodeVersion: "4.0.0",
|
|
708
|
+
bytesSaved: 3e3,
|
|
709
|
+
difficulty: "easy",
|
|
710
|
+
snippet: {
|
|
711
|
+
before: `moment().add(7, 'days').toDate()`,
|
|
712
|
+
after: `const d = new Date();
|
|
713
|
+
d.setDate(d.getDate() + 7);`
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
]
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: "fromNow",
|
|
720
|
+
alternatives: [
|
|
721
|
+
{
|
|
722
|
+
api: "Intl.RelativeTimeFormat",
|
|
723
|
+
minNodeVersion: "12.0.0",
|
|
724
|
+
bytesSaved: 5e3,
|
|
725
|
+
difficulty: "moderate",
|
|
726
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat",
|
|
727
|
+
snippet: {
|
|
728
|
+
before: `moment(date).fromNow()`,
|
|
729
|
+
after: `const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
|
730
|
+
const diffMs = date - Date.now();
|
|
731
|
+
const diffDays = Math.round(diffMs / 86_400_000);
|
|
732
|
+
rtf.format(diffDays, 'day');`
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
name: "isBefore",
|
|
739
|
+
alternatives: [
|
|
740
|
+
{
|
|
741
|
+
api: "Date comparison",
|
|
742
|
+
minNodeVersion: "4.0.0",
|
|
743
|
+
bytesSaved: 1e3,
|
|
744
|
+
difficulty: "trivial",
|
|
745
|
+
snippet: {
|
|
746
|
+
before: `moment(a).isBefore(moment(b))`,
|
|
747
|
+
after: `new Date(a) < new Date(b)`
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
]
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "isAfter",
|
|
754
|
+
alternatives: [
|
|
755
|
+
{
|
|
756
|
+
api: "Date comparison",
|
|
757
|
+
minNodeVersion: "4.0.0",
|
|
758
|
+
bytesSaved: 1e3,
|
|
759
|
+
difficulty: "trivial",
|
|
760
|
+
snippet: {
|
|
761
|
+
before: `moment(a).isAfter(moment(b))`,
|
|
762
|
+
after: `new Date(a) > new Date(b)`
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "unix",
|
|
769
|
+
alternatives: [
|
|
770
|
+
{
|
|
771
|
+
api: "Date.now() / 1000",
|
|
772
|
+
minNodeVersion: "4.0.0",
|
|
773
|
+
bytesSaved: 500,
|
|
774
|
+
difficulty: "trivial",
|
|
775
|
+
snippet: {
|
|
776
|
+
before: `moment().unix()`,
|
|
777
|
+
after: `Math.floor(Date.now() / 1000)`
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
]
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
name: "utc",
|
|
784
|
+
alternatives: [
|
|
785
|
+
{
|
|
786
|
+
api: "toUTCString / toISOString",
|
|
787
|
+
minNodeVersion: "4.0.0",
|
|
788
|
+
bytesSaved: 1e3,
|
|
789
|
+
difficulty: "easy",
|
|
790
|
+
snippet: {
|
|
791
|
+
before: `moment.utc(date).format()`,
|
|
792
|
+
after: `new Date(date).toISOString()`
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
]
|
|
796
|
+
}
|
|
797
|
+
]
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// src/database/packs/axios.ts
|
|
801
|
+
var axios = {
|
|
802
|
+
packageName: "axios",
|
|
803
|
+
description: "Promise based HTTP client for the browser and Node.js.",
|
|
804
|
+
gzippedSize: 14e3,
|
|
805
|
+
canFullyReplace: true,
|
|
806
|
+
functions: [
|
|
807
|
+
{
|
|
808
|
+
name: "default",
|
|
809
|
+
alternatives: [
|
|
810
|
+
{
|
|
811
|
+
api: "fetch",
|
|
812
|
+
minNodeVersion: "18.0.0",
|
|
813
|
+
bytesSaved: 14e3,
|
|
814
|
+
difficulty: "easy",
|
|
815
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API",
|
|
816
|
+
snippet: {
|
|
817
|
+
before: `import axios from 'axios';
|
|
818
|
+
const { data } = await axios.get('/api/users');`,
|
|
819
|
+
after: `const data = await fetch('/api/users').then((r) => r.json());`
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: "get",
|
|
826
|
+
alternatives: [
|
|
827
|
+
{
|
|
828
|
+
api: "fetch",
|
|
829
|
+
minNodeVersion: "18.0.0",
|
|
830
|
+
bytesSaved: 14e3,
|
|
831
|
+
difficulty: "easy",
|
|
832
|
+
snippet: {
|
|
833
|
+
before: `import axios from 'axios';
|
|
834
|
+
const { data } = await axios.get(url, { params: { id: 1 } });`,
|
|
835
|
+
after: `const url = new URL('/api/users', baseUrl);
|
|
836
|
+
url.searchParams.set('id', '1');
|
|
837
|
+
const data = await fetch(url).then((r) => r.json());`
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
]
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: "post",
|
|
844
|
+
alternatives: [
|
|
845
|
+
{
|
|
846
|
+
api: "fetch (POST)",
|
|
847
|
+
minNodeVersion: "18.0.0",
|
|
848
|
+
bytesSaved: 14e3,
|
|
849
|
+
difficulty: "easy",
|
|
850
|
+
snippet: {
|
|
851
|
+
before: `import axios from 'axios';
|
|
852
|
+
await axios.post('/api/users', { name: 'Alice' });`,
|
|
853
|
+
after: `await fetch('/api/users', {
|
|
854
|
+
method: 'POST',
|
|
855
|
+
headers: { 'Content-Type': 'application/json' },
|
|
856
|
+
body: JSON.stringify({ name: 'Alice' }),
|
|
857
|
+
});`
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
]
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
name: "put",
|
|
864
|
+
alternatives: [
|
|
865
|
+
{
|
|
866
|
+
api: "fetch (PUT)",
|
|
867
|
+
minNodeVersion: "18.0.0",
|
|
868
|
+
bytesSaved: 14e3,
|
|
869
|
+
difficulty: "easy",
|
|
870
|
+
snippet: {
|
|
871
|
+
before: `await axios.put('/api/users/1', payload);`,
|
|
872
|
+
after: `await fetch('/api/users/1', {
|
|
873
|
+
method: 'PUT',
|
|
874
|
+
headers: { 'Content-Type': 'application/json' },
|
|
875
|
+
body: JSON.stringify(payload),
|
|
876
|
+
});`
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
]
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
name: "delete",
|
|
883
|
+
alternatives: [
|
|
884
|
+
{
|
|
885
|
+
api: "fetch (DELETE)",
|
|
886
|
+
minNodeVersion: "18.0.0",
|
|
887
|
+
bytesSaved: 14e3,
|
|
888
|
+
difficulty: "easy",
|
|
889
|
+
snippet: {
|
|
890
|
+
before: `await axios.delete('/api/users/1');`,
|
|
891
|
+
after: `await fetch('/api/users/1', { method: 'DELETE' });`
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
]
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
name: "create",
|
|
898
|
+
alternatives: [
|
|
899
|
+
{
|
|
900
|
+
api: "fetch wrapper",
|
|
901
|
+
minNodeVersion: "18.0.0",
|
|
902
|
+
bytesSaved: 14e3,
|
|
903
|
+
difficulty: "moderate",
|
|
904
|
+
snippet: {
|
|
905
|
+
before: `const api = axios.create({ baseURL: 'https://api.example.com', timeout: 5000 });`,
|
|
906
|
+
after: `const apiFetch = (path, init = {}) =>
|
|
907
|
+
fetch(\`https://api.example.com\${path}\`, {
|
|
908
|
+
signal: AbortSignal.timeout(5000),
|
|
909
|
+
...init,
|
|
910
|
+
}).then((r) => { if (!r.ok) throw new Error(r.statusText); return r.json(); });`
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
name: "all",
|
|
917
|
+
alternatives: [
|
|
918
|
+
{
|
|
919
|
+
api: "Promise.all",
|
|
920
|
+
minNodeVersion: "4.0.0",
|
|
921
|
+
bytesSaved: 500,
|
|
922
|
+
difficulty: "trivial",
|
|
923
|
+
snippet: {
|
|
924
|
+
before: `await axios.all([axios.get('/a'), axios.get('/b')]);`,
|
|
925
|
+
after: `await Promise.all([fetch('/a').then(r => r.json()), fetch('/b').then(r => r.json())]);`
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
]
|
|
929
|
+
}
|
|
930
|
+
]
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// src/database/packs/uuid.ts
|
|
934
|
+
var uuid = {
|
|
935
|
+
packageName: "uuid",
|
|
936
|
+
description: "RFC4122 UUIDs generator.",
|
|
937
|
+
gzippedSize: 3700,
|
|
938
|
+
canFullyReplace: true,
|
|
939
|
+
functions: [
|
|
940
|
+
{
|
|
941
|
+
name: "v4",
|
|
942
|
+
importPaths: ["uuid/v4"],
|
|
943
|
+
alternatives: [
|
|
944
|
+
{
|
|
945
|
+
api: "crypto.randomUUID",
|
|
946
|
+
minNodeVersion: "14.17.0",
|
|
947
|
+
bytesSaved: 3700,
|
|
948
|
+
difficulty: "trivial",
|
|
949
|
+
docsUrl: "https://nodejs.org/api/crypto.html#cryptorandomuuidoptions",
|
|
950
|
+
snippet: {
|
|
951
|
+
before: `import { v4 as uuidv4 } from 'uuid';
|
|
952
|
+
const id = uuidv4();`,
|
|
953
|
+
after: `const id = crypto.randomUUID();`
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
]
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
name: "v1",
|
|
960
|
+
importPaths: ["uuid/v1"],
|
|
961
|
+
alternatives: [
|
|
962
|
+
{
|
|
963
|
+
api: "crypto.randomUUID (v4 alternative)",
|
|
964
|
+
minNodeVersion: "14.17.0",
|
|
965
|
+
bytesSaved: 3700,
|
|
966
|
+
difficulty: "easy",
|
|
967
|
+
snippet: {
|
|
968
|
+
before: `import { v1 as uuidv1 } from 'uuid';
|
|
969
|
+
const id = uuidv1();`,
|
|
970
|
+
after: `// v1 (timestamp-based) has no direct native equivalent.
|
|
971
|
+
// If timestamp ordering isn't required, use:
|
|
972
|
+
const id = crypto.randomUUID();`
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
]
|
|
976
|
+
}
|
|
977
|
+
]
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
// src/database/packs/bluebird.ts
|
|
981
|
+
var bluebird = {
|
|
982
|
+
packageName: "bluebird",
|
|
983
|
+
description: "A full featured promise library with unmatched performance.",
|
|
984
|
+
gzippedSize: 17e3,
|
|
985
|
+
canFullyReplace: true,
|
|
986
|
+
functions: [
|
|
987
|
+
{
|
|
988
|
+
name: "default",
|
|
989
|
+
alternatives: [
|
|
990
|
+
{
|
|
991
|
+
api: "Native Promise",
|
|
992
|
+
minNodeVersion: "4.0.0",
|
|
993
|
+
bytesSaved: 17e3,
|
|
994
|
+
difficulty: "moderate",
|
|
995
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise",
|
|
996
|
+
snippet: {
|
|
997
|
+
before: `const Promise = require('bluebird');
|
|
998
|
+
const result = new Promise((resolve) => resolve(42));`,
|
|
999
|
+
after: `const result = Promise.resolve(42);`
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
name: "all",
|
|
1006
|
+
alternatives: [
|
|
1007
|
+
{
|
|
1008
|
+
api: "Promise.all",
|
|
1009
|
+
minNodeVersion: "4.0.0",
|
|
1010
|
+
bytesSaved: 2e3,
|
|
1011
|
+
difficulty: "trivial",
|
|
1012
|
+
snippet: {
|
|
1013
|
+
before: `Promise.all([p1, p2, p3])`,
|
|
1014
|
+
after: `Promise.all([p1, p2, p3]) // Native \u2014 same API!`
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
]
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "map",
|
|
1021
|
+
alternatives: [
|
|
1022
|
+
{
|
|
1023
|
+
api: "Promise.all + Array.map",
|
|
1024
|
+
minNodeVersion: "4.0.0",
|
|
1025
|
+
bytesSaved: 1500,
|
|
1026
|
+
difficulty: "easy",
|
|
1027
|
+
snippet: {
|
|
1028
|
+
before: `import Promise from 'bluebird';
|
|
1029
|
+
await Promise.map(ids, fetchUser, { concurrency: 3 });`,
|
|
1030
|
+
after: `// Simple version (no concurrency limit):
|
|
1031
|
+
await Promise.all(ids.map(fetchUser));
|
|
1032
|
+
|
|
1033
|
+
// With concurrency control (p-limit):
|
|
1034
|
+
import pLimit from 'p-limit';
|
|
1035
|
+
const limit = pLimit(3);
|
|
1036
|
+
await Promise.all(ids.map((id) => limit(() => fetchUser(id))));`
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
]
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
name: "mapSeries",
|
|
1043
|
+
alternatives: [
|
|
1044
|
+
{
|
|
1045
|
+
api: "for...of + await",
|
|
1046
|
+
minNodeVersion: "10.0.0",
|
|
1047
|
+
bytesSaved: 1200,
|
|
1048
|
+
difficulty: "easy",
|
|
1049
|
+
snippet: {
|
|
1050
|
+
before: `await Promise.mapSeries(items, processItem);`,
|
|
1051
|
+
after: `const results = [];
|
|
1052
|
+
for (const item of items) {
|
|
1053
|
+
results.push(await processItem(item));
|
|
1054
|
+
}`
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
]
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: "filter",
|
|
1061
|
+
alternatives: [
|
|
1062
|
+
{
|
|
1063
|
+
api: "Promise.all + Array.filter",
|
|
1064
|
+
minNodeVersion: "4.0.0",
|
|
1065
|
+
bytesSaved: 1e3,
|
|
1066
|
+
difficulty: "easy",
|
|
1067
|
+
snippet: {
|
|
1068
|
+
before: `await Promise.filter(items, asyncPredicate);`,
|
|
1069
|
+
after: `const flags = await Promise.all(items.map(asyncPredicate));
|
|
1070
|
+
const result = items.filter((_, i) => flags[i]);`
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
]
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
name: "props",
|
|
1077
|
+
alternatives: [
|
|
1078
|
+
{
|
|
1079
|
+
api: "Promise.all + Object.fromEntries",
|
|
1080
|
+
minNodeVersion: "12.0.0",
|
|
1081
|
+
bytesSaved: 1e3,
|
|
1082
|
+
difficulty: "easy",
|
|
1083
|
+
snippet: {
|
|
1084
|
+
before: `await Promise.props({ user: fetchUser(), posts: fetchPosts() });`,
|
|
1085
|
+
after: `const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
|
|
1086
|
+
const result = { user, posts };`
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
]
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
name: "each",
|
|
1093
|
+
alternatives: [
|
|
1094
|
+
{
|
|
1095
|
+
api: "for...of + await",
|
|
1096
|
+
minNodeVersion: "10.0.0",
|
|
1097
|
+
bytesSaved: 800,
|
|
1098
|
+
difficulty: "easy",
|
|
1099
|
+
snippet: {
|
|
1100
|
+
before: `await Promise.each(items, async (item) => { await process(item); });`,
|
|
1101
|
+
after: `for (const item of items) { await process(item); }`
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
]
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
name: "any",
|
|
1108
|
+
alternatives: [
|
|
1109
|
+
{
|
|
1110
|
+
api: "Promise.any",
|
|
1111
|
+
minNodeVersion: "15.0.0",
|
|
1112
|
+
bytesSaved: 500,
|
|
1113
|
+
difficulty: "trivial",
|
|
1114
|
+
snippet: {
|
|
1115
|
+
before: `await Promise.any([p1, p2, p3]);`,
|
|
1116
|
+
after: `await Promise.any([p1, p2, p3]); // Native \u2014 same API!`
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
]
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
name: "reflect",
|
|
1123
|
+
alternatives: [
|
|
1124
|
+
{
|
|
1125
|
+
api: "Promise.allSettled",
|
|
1126
|
+
minNodeVersion: "12.9.0",
|
|
1127
|
+
bytesSaved: 800,
|
|
1128
|
+
difficulty: "easy",
|
|
1129
|
+
snippet: {
|
|
1130
|
+
before: `const results = await Promise.all(promises.map((p) => p.reflect()));
|
|
1131
|
+
results.filter((r) => r.isFulfilled());`,
|
|
1132
|
+
after: `const results = await Promise.allSettled(promises);
|
|
1133
|
+
results.filter((r) => r.status === 'fulfilled');`
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
]
|
|
1137
|
+
}
|
|
1138
|
+
]
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
// src/database/packs/underscore.ts
|
|
1142
|
+
var underscore = {
|
|
1143
|
+
packageName: "underscore",
|
|
1144
|
+
description: "A JavaScript utility library providing lots of useful functional programming helpers.",
|
|
1145
|
+
gzippedSize: 6800,
|
|
1146
|
+
canFullyReplace: true,
|
|
1147
|
+
functions: [
|
|
1148
|
+
{
|
|
1149
|
+
name: "each",
|
|
1150
|
+
alternatives: [
|
|
1151
|
+
{
|
|
1152
|
+
api: "Array.prototype.forEach",
|
|
1153
|
+
minNodeVersion: "4.0.0",
|
|
1154
|
+
bytesSaved: 400,
|
|
1155
|
+
difficulty: "trivial",
|
|
1156
|
+
snippet: {
|
|
1157
|
+
before: `import _ from 'underscore';
|
|
1158
|
+
_.each(arr, (item) => console.log(item));`,
|
|
1159
|
+
after: `arr.forEach((item) => console.log(item));`
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
]
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
name: "map",
|
|
1166
|
+
alternatives: [
|
|
1167
|
+
{
|
|
1168
|
+
api: "Array.prototype.map",
|
|
1169
|
+
minNodeVersion: "4.0.0",
|
|
1170
|
+
bytesSaved: 400,
|
|
1171
|
+
difficulty: "trivial",
|
|
1172
|
+
snippet: {
|
|
1173
|
+
before: `_.map(arr, (x) => x * 2)`,
|
|
1174
|
+
after: `arr.map((x) => x * 2)`
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
]
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
name: "filter",
|
|
1181
|
+
alternatives: [
|
|
1182
|
+
{
|
|
1183
|
+
api: "Array.prototype.filter",
|
|
1184
|
+
minNodeVersion: "4.0.0",
|
|
1185
|
+
bytesSaved: 300,
|
|
1186
|
+
difficulty: "trivial",
|
|
1187
|
+
snippet: {
|
|
1188
|
+
before: `_.filter(arr, (x) => x > 0)`,
|
|
1189
|
+
after: `arr.filter((x) => x > 0)`
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
]
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
name: "reduce",
|
|
1196
|
+
alternatives: [
|
|
1197
|
+
{
|
|
1198
|
+
api: "Array.prototype.reduce",
|
|
1199
|
+
minNodeVersion: "4.0.0",
|
|
1200
|
+
bytesSaved: 300,
|
|
1201
|
+
difficulty: "trivial",
|
|
1202
|
+
snippet: {
|
|
1203
|
+
before: `_.reduce(arr, (acc, x) => acc + x, 0)`,
|
|
1204
|
+
after: `arr.reduce((acc, x) => acc + x, 0)`
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
]
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
name: "find",
|
|
1211
|
+
alternatives: [
|
|
1212
|
+
{
|
|
1213
|
+
api: "Array.prototype.find",
|
|
1214
|
+
minNodeVersion: "4.0.0",
|
|
1215
|
+
bytesSaved: 300,
|
|
1216
|
+
difficulty: "trivial",
|
|
1217
|
+
snippet: {
|
|
1218
|
+
before: `_.find(arr, (x) => x.id === id)`,
|
|
1219
|
+
after: `arr.find((x) => x.id === id)`
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
]
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
name: "contains",
|
|
1226
|
+
alternatives: [
|
|
1227
|
+
{
|
|
1228
|
+
api: "Array.prototype.includes",
|
|
1229
|
+
minNodeVersion: "6.0.0",
|
|
1230
|
+
bytesSaved: 200,
|
|
1231
|
+
difficulty: "trivial",
|
|
1232
|
+
snippet: {
|
|
1233
|
+
before: `_.contains(arr, value)`,
|
|
1234
|
+
after: `arr.includes(value)`
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
name: "keys",
|
|
1241
|
+
alternatives: [
|
|
1242
|
+
{
|
|
1243
|
+
api: "Object.keys",
|
|
1244
|
+
minNodeVersion: "4.0.0",
|
|
1245
|
+
bytesSaved: 200,
|
|
1246
|
+
difficulty: "trivial",
|
|
1247
|
+
snippet: {
|
|
1248
|
+
before: `_.keys(obj)`,
|
|
1249
|
+
after: `Object.keys(obj)`
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
]
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
name: "values",
|
|
1256
|
+
alternatives: [
|
|
1257
|
+
{
|
|
1258
|
+
api: "Object.values",
|
|
1259
|
+
minNodeVersion: "7.0.0",
|
|
1260
|
+
bytesSaved: 200,
|
|
1261
|
+
difficulty: "trivial",
|
|
1262
|
+
snippet: {
|
|
1263
|
+
before: `_.values(obj)`,
|
|
1264
|
+
after: `Object.values(obj)`
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
]
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: "extend",
|
|
1271
|
+
alternatives: [
|
|
1272
|
+
{
|
|
1273
|
+
api: "Object.assign",
|
|
1274
|
+
minNodeVersion: "4.0.0",
|
|
1275
|
+
bytesSaved: 200,
|
|
1276
|
+
difficulty: "trivial",
|
|
1277
|
+
snippet: {
|
|
1278
|
+
before: `_.extend(target, source)`,
|
|
1279
|
+
after: `Object.assign(target, source)`
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
]
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
name: "clone",
|
|
1286
|
+
alternatives: [
|
|
1287
|
+
{
|
|
1288
|
+
api: "structuredClone / spread",
|
|
1289
|
+
minNodeVersion: "17.0.0",
|
|
1290
|
+
bytesSaved: 300,
|
|
1291
|
+
difficulty: "trivial",
|
|
1292
|
+
snippet: {
|
|
1293
|
+
before: `_.clone(obj)`,
|
|
1294
|
+
after: `structuredClone(obj) // deep clone
|
|
1295
|
+
// or { ...obj } for shallow`
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
]
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
name: "flatten",
|
|
1302
|
+
alternatives: [
|
|
1303
|
+
{
|
|
1304
|
+
api: "Array.prototype.flat",
|
|
1305
|
+
minNodeVersion: "11.0.0",
|
|
1306
|
+
bytesSaved: 300,
|
|
1307
|
+
difficulty: "trivial",
|
|
1308
|
+
snippet: {
|
|
1309
|
+
before: `_.flatten(arr)`,
|
|
1310
|
+
after: `arr.flat()`
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
]
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
name: "uniq",
|
|
1317
|
+
alternatives: [
|
|
1318
|
+
{
|
|
1319
|
+
api: "Set",
|
|
1320
|
+
minNodeVersion: "4.0.0",
|
|
1321
|
+
bytesSaved: 250,
|
|
1322
|
+
difficulty: "trivial",
|
|
1323
|
+
snippet: {
|
|
1324
|
+
before: `_.uniq(arr)`,
|
|
1325
|
+
after: `[...new Set(arr)]`
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
]
|
|
1329
|
+
}
|
|
1330
|
+
]
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
// src/database/index.ts
|
|
1334
|
+
var ALL_PACKAGES = [lodash, moment, axios, uuid, bluebird, underscore];
|
|
1335
|
+
var packageRegistry = /* @__PURE__ */ new Map();
|
|
1336
|
+
var functionRegistry = /* @__PURE__ */ new Map();
|
|
1337
|
+
var subPathRegistry = /* @__PURE__ */ new Map();
|
|
1338
|
+
for (const pkg of ALL_PACKAGES) {
|
|
1339
|
+
packageRegistry.set(pkg.packageName, pkg);
|
|
1340
|
+
const fnMap = /* @__PURE__ */ new Map();
|
|
1341
|
+
for (const fn of pkg.functions) {
|
|
1342
|
+
fnMap.set(fn.name, fn);
|
|
1343
|
+
if (fn.importPaths) {
|
|
1344
|
+
for (const subPath of fn.importPaths) {
|
|
1345
|
+
subPathRegistry.set(subPath, { packageName: pkg.packageName, functionName: fn.name });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
functionRegistry.set(pkg.packageName, fnMap);
|
|
1350
|
+
}
|
|
1351
|
+
function lookupPackage(packageName) {
|
|
1352
|
+
return packageRegistry.get(packageName);
|
|
1353
|
+
}
|
|
1354
|
+
function lookupFunction(packageName, functionName) {
|
|
1355
|
+
return functionRegistry.get(packageName)?.get(functionName);
|
|
1356
|
+
}
|
|
1357
|
+
function lookupSubPath(subPath) {
|
|
1358
|
+
return subPathRegistry.get(subPath);
|
|
1359
|
+
}
|
|
1360
|
+
function getAllPackages() {
|
|
1361
|
+
return ALL_PACKAGES;
|
|
1362
|
+
}
|
|
1363
|
+
function matchImports(imports) {
|
|
1364
|
+
const matches = [];
|
|
1365
|
+
const namespaceToPackage = /* @__PURE__ */ new Map();
|
|
1366
|
+
for (const rec of imports) {
|
|
1367
|
+
if (rec.functionName === "*" && !rec.packageName.startsWith("__ns__")) {
|
|
1368
|
+
namespaceToPackage.set(rec.packageName, rec.packageName);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
const fileNsMap = /* @__PURE__ */ new Map();
|
|
1372
|
+
for (const rec of imports) {
|
|
1373
|
+
if (rec.functionName === "*" && !rec.packageName.startsWith("__ns__")) {
|
|
1374
|
+
const commonNs = rec.packageName === "lodash" || rec.packageName === "underscore" ? "_" : rec.packageName;
|
|
1375
|
+
if (!fileNsMap.has(rec.file)) fileNsMap.set(rec.file, /* @__PURE__ */ new Map());
|
|
1376
|
+
fileNsMap.get(rec.file).set(commonNs, rec.packageName);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
for (const rec of imports) {
|
|
1380
|
+
if (rec.functionName === "*") continue;
|
|
1381
|
+
let packageName = rec.packageName;
|
|
1382
|
+
let functionName = rec.functionName;
|
|
1383
|
+
if (packageName.startsWith("__ns__")) {
|
|
1384
|
+
const nsId = packageName.replace("__ns__", "");
|
|
1385
|
+
const filePkg = fileNsMap.get(rec.file)?.get(nsId);
|
|
1386
|
+
if (!filePkg) continue;
|
|
1387
|
+
packageName = filePkg;
|
|
1388
|
+
}
|
|
1389
|
+
let pkgEntry = lookupPackage(packageName);
|
|
1390
|
+
let fnEntry = pkgEntry ? lookupFunction(packageName, functionName) : void 0;
|
|
1391
|
+
if (!fnEntry) {
|
|
1392
|
+
const subPathKey = `${packageName}/${functionName}`;
|
|
1393
|
+
const resolved = lookupSubPath(subPathKey);
|
|
1394
|
+
if (resolved) {
|
|
1395
|
+
pkgEntry = lookupPackage(resolved.packageName);
|
|
1396
|
+
fnEntry = pkgEntry ? lookupFunction(resolved.packageName, resolved.functionName) : void 0;
|
|
1397
|
+
if (pkgEntry) packageName = resolved.packageName;
|
|
1398
|
+
if (fnEntry) functionName = resolved.functionName;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (!fnEntry && pkgEntry) {
|
|
1402
|
+
fnEntry = lookupFunction(packageName, "default");
|
|
1403
|
+
}
|
|
1404
|
+
if (!pkgEntry || !fnEntry) continue;
|
|
1405
|
+
matches.push({
|
|
1406
|
+
importRecord: { ...rec, packageName, functionName },
|
|
1407
|
+
packageEntry: pkgEntry,
|
|
1408
|
+
functionEntry: fnEntry
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1412
|
+
return matches.filter((m) => {
|
|
1413
|
+
const key = `${m.importRecord.file}::${m.importRecord.packageName}::${m.importRecord.functionName}`;
|
|
1414
|
+
if (seen.has(key)) return false;
|
|
1415
|
+
seen.add(key);
|
|
1416
|
+
return true;
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/scorer.ts
|
|
1421
|
+
var import_fs2 = __toESM(require("fs"));
|
|
1422
|
+
var import_path3 = __toESM(require("path"));
|
|
1423
|
+
function loadPackageJson(targetDir) {
|
|
1424
|
+
const pkgPath = import_path3.default.join(targetDir, "package.json");
|
|
1425
|
+
try {
|
|
1426
|
+
const raw = import_fs2.default.readFileSync(pkgPath, "utf-8");
|
|
1427
|
+
return JSON.parse(raw);
|
|
1428
|
+
} catch {
|
|
1429
|
+
return {};
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function getTotalDepsSize(targetDir) {
|
|
1433
|
+
const pkg = loadPackageJson(targetDir);
|
|
1434
|
+
const allDeps = {
|
|
1435
|
+
...pkg.dependencies,
|
|
1436
|
+
...pkg.devDependencies
|
|
1437
|
+
};
|
|
1438
|
+
const allPkgs = getAllPackages();
|
|
1439
|
+
const knownSizes = new Map(allPkgs.map((p) => [p.packageName, p.gzippedSize]));
|
|
1440
|
+
let total = 0;
|
|
1441
|
+
for (const depName of Object.keys(allDeps)) {
|
|
1442
|
+
const size = knownSizes.get(depName);
|
|
1443
|
+
if (size) total += size;
|
|
1444
|
+
}
|
|
1445
|
+
if (total === 0) {
|
|
1446
|
+
total = allPkgs.reduce((sum, p) => sum + p.gzippedSize, 0);
|
|
1447
|
+
}
|
|
1448
|
+
return total;
|
|
1449
|
+
}
|
|
1450
|
+
function getScoreGrade(score) {
|
|
1451
|
+
if (score >= 90) return "SVELTE";
|
|
1452
|
+
if (score >= 70) return "HEALTHY";
|
|
1453
|
+
if (score >= 40) return "NEEDS WORK";
|
|
1454
|
+
return "CRITICAL";
|
|
1455
|
+
}
|
|
1456
|
+
function computeScore(totalBytesSaved, totalDepsSize) {
|
|
1457
|
+
if (totalDepsSize === 0) return 100;
|
|
1458
|
+
const wasteRatio = Math.min(totalBytesSaved / totalDepsSize, 1);
|
|
1459
|
+
return Math.max(0, Math.round(100 - wasteRatio * 100));
|
|
1460
|
+
}
|
|
1461
|
+
function buildScanResult(targetDir, filesScanned, imports, matches, scanDurationMs) {
|
|
1462
|
+
const totalBytesSaved = matches.reduce((sum, m) => {
|
|
1463
|
+
const maxSaving = Math.max(...m.functionEntry.alternatives.map((a) => a.bytesSaved));
|
|
1464
|
+
return sum + maxSaving;
|
|
1465
|
+
}, 0);
|
|
1466
|
+
const totalDepsSize = getTotalDepsSize(targetDir);
|
|
1467
|
+
const refactorScore = computeScore(totalBytesSaved, totalDepsSize);
|
|
1468
|
+
const grade = getScoreGrade(refactorScore);
|
|
1469
|
+
return {
|
|
1470
|
+
targetDir,
|
|
1471
|
+
filesScanned,
|
|
1472
|
+
imports,
|
|
1473
|
+
matches,
|
|
1474
|
+
totalBytesSaved,
|
|
1475
|
+
totalDepsSize,
|
|
1476
|
+
refactorScore,
|
|
1477
|
+
grade,
|
|
1478
|
+
scanDurationMs
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function formatBytes(bytes) {
|
|
1482
|
+
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
1483
|
+
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(1)} KB`;
|
|
1484
|
+
return `${bytes} B`;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/reporter/terminal.ts
|
|
1488
|
+
var import_chalk = __toESM(require("chalk"));
|
|
1489
|
+
var import_boxen = __toESM(require("boxen"));
|
|
1490
|
+
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
1491
|
+
var import_gradient_string = __toESM(require("gradient-string"));
|
|
1492
|
+
var BRAND = (0, import_gradient_string.default)(["#FF6B6B", "#FFD93D", "#6BCB77", "#4D96FF"]);
|
|
1493
|
+
var gradeColor = {
|
|
1494
|
+
CRITICAL: import_chalk.default.red.bold,
|
|
1495
|
+
"NEEDS WORK": import_chalk.default.yellow.bold,
|
|
1496
|
+
HEALTHY: import_chalk.default.green.bold,
|
|
1497
|
+
SVELTE: import_chalk.default.cyan.bold
|
|
1498
|
+
};
|
|
1499
|
+
var gradeIcon = {
|
|
1500
|
+
CRITICAL: "\u{1F480}",
|
|
1501
|
+
"NEEDS WORK": "\u26A0\uFE0F ",
|
|
1502
|
+
HEALTHY: "\u2705",
|
|
1503
|
+
SVELTE: "\u{1F957}"
|
|
1504
|
+
};
|
|
1505
|
+
var difficultyColor = {
|
|
1506
|
+
trivial: import_chalk.default.green,
|
|
1507
|
+
easy: import_chalk.default.cyan,
|
|
1508
|
+
moderate: import_chalk.default.yellow,
|
|
1509
|
+
hard: import_chalk.default.red
|
|
1510
|
+
};
|
|
1511
|
+
var difficultyLabel = {
|
|
1512
|
+
trivial: "TRIVIAL",
|
|
1513
|
+
easy: "EASY",
|
|
1514
|
+
moderate: "MODERATE",
|
|
1515
|
+
hard: "HARD"
|
|
1516
|
+
};
|
|
1517
|
+
function scoreBar(score) {
|
|
1518
|
+
const filled = Math.round(score / 5);
|
|
1519
|
+
const empty = 20 - filled;
|
|
1520
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
1521
|
+
if (score >= 90) return import_chalk.default.cyan(bar);
|
|
1522
|
+
if (score >= 70) return import_chalk.default.green(bar);
|
|
1523
|
+
if (score >= 40) return import_chalk.default.yellow(bar);
|
|
1524
|
+
return import_chalk.default.red(bar);
|
|
1525
|
+
}
|
|
1526
|
+
function sparkline(values) {
|
|
1527
|
+
const BLOCKS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
1528
|
+
const min = Math.min(...values);
|
|
1529
|
+
const max = Math.max(...values);
|
|
1530
|
+
const range = max - min || 1;
|
|
1531
|
+
return values.map((v) => {
|
|
1532
|
+
const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
|
|
1533
|
+
return BLOCKS[idx];
|
|
1534
|
+
}).join("");
|
|
1535
|
+
}
|
|
1536
|
+
function formatDiff(before, after) {
|
|
1537
|
+
const beforeLines = before.split("\n").map((l) => import_chalk.default.red(`- ${l}`));
|
|
1538
|
+
const afterLines = after.split("\n").map((l) => import_chalk.default.green(`+ ${l}`));
|
|
1539
|
+
return [...beforeLines, ...afterLines].join("\n");
|
|
1540
|
+
}
|
|
1541
|
+
function wrapSnippet(alternative) {
|
|
1542
|
+
return [
|
|
1543
|
+
import_chalk.default.dim(" \u250C\u2500\u2500 Before \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
1544
|
+
...alternative.snippet.before.split("\n").map((l) => import_chalk.default.red(` \u2502 ${l}`)),
|
|
1545
|
+
import_chalk.default.dim(" \u251C\u2500\u2500 After \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
1546
|
+
...alternative.snippet.after.split("\n").map((l) => import_chalk.default.green(` \u2502 ${l}`)),
|
|
1547
|
+
import_chalk.default.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")
|
|
1548
|
+
].join("\n");
|
|
1549
|
+
}
|
|
1550
|
+
function printHeader() {
|
|
1551
|
+
const title = BRAND(
|
|
1552
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\n \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\n \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \n \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \n \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \n \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D "
|
|
1553
|
+
);
|
|
1554
|
+
console.log(
|
|
1555
|
+
(0, import_boxen.default)(title, {
|
|
1556
|
+
padding: 1,
|
|
1557
|
+
borderStyle: "round",
|
|
1558
|
+
borderColor: "magenta"
|
|
1559
|
+
})
|
|
1560
|
+
);
|
|
1561
|
+
console.log(
|
|
1562
|
+
import_chalk.default.dim(" Put your project on a diet. Find heavy packages, go native.\n")
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
function printScoreCard(result) {
|
|
1566
|
+
const { refactorScore, grade, totalBytesSaved, filesScanned, matches, scanDurationMs } = result;
|
|
1567
|
+
const uniquePackages = new Set(matches.map((m) => m.packageEntry.packageName));
|
|
1568
|
+
const colorFn = gradeColor[grade];
|
|
1569
|
+
const icon = gradeIcon[grade];
|
|
1570
|
+
const content = [
|
|
1571
|
+
"",
|
|
1572
|
+
` ${BRAND("REFACTOR SCORE")}`,
|
|
1573
|
+
"",
|
|
1574
|
+
` ${scoreBar(refactorScore)} ${colorFn(`${refactorScore}/100`)}`,
|
|
1575
|
+
"",
|
|
1576
|
+
` Grade: ${colorFn(`${icon} ${grade}`)}`,
|
|
1577
|
+
"",
|
|
1578
|
+
` ${import_chalk.default.dim("\u2500".repeat(44))}`,
|
|
1579
|
+
"",
|
|
1580
|
+
` ${import_chalk.default.white("Potential savings:")} ${import_chalk.default.yellow.bold(formatBytes(totalBytesSaved))}`,
|
|
1581
|
+
` ${import_chalk.default.white("Packages to replace:")} ${import_chalk.default.yellow.bold(String(uniquePackages.size))}`,
|
|
1582
|
+
` ${import_chalk.default.white("Usage sites found:")} ${import_chalk.default.yellow.bold(String(matches.length))}`,
|
|
1583
|
+
` ${import_chalk.default.white("Files scanned:")} ${import_chalk.default.dim(String(filesScanned))}`,
|
|
1584
|
+
` ${import_chalk.default.white("Scan duration:")} ${import_chalk.default.dim(`${scanDurationMs}ms`)}`,
|
|
1585
|
+
""
|
|
1586
|
+
].join("\n");
|
|
1587
|
+
console.log(
|
|
1588
|
+
(0, import_boxen.default)(content, {
|
|
1589
|
+
borderStyle: "double",
|
|
1590
|
+
borderColor: grade === "CRITICAL" ? "red" : grade === "SVELTE" ? "cyan" : "yellow",
|
|
1591
|
+
padding: 0
|
|
1592
|
+
})
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
function printMatchesTable(matches) {
|
|
1596
|
+
if (matches.length === 0) {
|
|
1597
|
+
console.log(import_chalk.default.green("\n \u2713 No heavy library usage found. Your project is already lean!\n"));
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
console.log(import_chalk.default.bold.white("\n DETECTED USAGE\n"));
|
|
1601
|
+
const table = new import_cli_table3.default({
|
|
1602
|
+
head: [
|
|
1603
|
+
import_chalk.default.bold.cyan("Package"),
|
|
1604
|
+
import_chalk.default.bold.cyan("Function"),
|
|
1605
|
+
import_chalk.default.bold.cyan("File"),
|
|
1606
|
+
import_chalk.default.bold.cyan("Line"),
|
|
1607
|
+
import_chalk.default.bold.cyan("Savings"),
|
|
1608
|
+
import_chalk.default.bold.cyan("Effort")
|
|
1609
|
+
],
|
|
1610
|
+
style: { head: [], border: ["dim"] },
|
|
1611
|
+
colWidths: [16, 16, 36, 6, 12, 12],
|
|
1612
|
+
wordWrap: true
|
|
1613
|
+
});
|
|
1614
|
+
for (const match of matches) {
|
|
1615
|
+
const best = match.functionEntry.alternatives[0];
|
|
1616
|
+
if (!best) continue;
|
|
1617
|
+
const diffColor = difficultyColor[best.difficulty];
|
|
1618
|
+
const shortFile = match.importRecord.file.length > 34 ? "\u2026" + match.importRecord.file.slice(-33) : match.importRecord.file;
|
|
1619
|
+
table.push([
|
|
1620
|
+
import_chalk.default.magenta(match.packageEntry.packageName),
|
|
1621
|
+
import_chalk.default.white(match.importRecord.functionName),
|
|
1622
|
+
import_chalk.default.dim(shortFile),
|
|
1623
|
+
import_chalk.default.dim(String(match.importRecord.line)),
|
|
1624
|
+
import_chalk.default.yellow(formatBytes(best.bytesSaved)),
|
|
1625
|
+
diffColor(difficultyLabel[best.difficulty])
|
|
1626
|
+
]);
|
|
1627
|
+
}
|
|
1628
|
+
console.log(table.toString());
|
|
1629
|
+
}
|
|
1630
|
+
function printSuggestions(matches) {
|
|
1631
|
+
if (matches.length === 0) return;
|
|
1632
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
1633
|
+
for (const m of matches) {
|
|
1634
|
+
const key = m.packageEntry.packageName;
|
|
1635
|
+
if (!byPackage.has(key)) byPackage.set(key, []);
|
|
1636
|
+
byPackage.get(key).push(m);
|
|
1637
|
+
}
|
|
1638
|
+
console.log(import_chalk.default.bold.white("\n NATIVE ALTERNATIVES\n"));
|
|
1639
|
+
for (const [packageName, pkgMatches] of byPackage) {
|
|
1640
|
+
const totalSavings = pkgMatches.reduce((sum, m) => {
|
|
1641
|
+
const best = m.functionEntry.alternatives[0];
|
|
1642
|
+
return sum + (best?.bytesSaved ?? 0);
|
|
1643
|
+
}, 0);
|
|
1644
|
+
console.log(
|
|
1645
|
+
(0, import_boxen.default)(
|
|
1646
|
+
` ${import_chalk.default.bold.magenta(packageName)} ${import_chalk.default.dim(`\u2192 could save ${formatBytes(totalSavings)}`)}`,
|
|
1647
|
+
{ padding: 0, borderStyle: "single", borderColor: "magenta" }
|
|
1648
|
+
)
|
|
1649
|
+
);
|
|
1650
|
+
console.log();
|
|
1651
|
+
const seenFunctions = /* @__PURE__ */ new Set();
|
|
1652
|
+
for (const match of pkgMatches) {
|
|
1653
|
+
const fnName = match.functionEntry.name;
|
|
1654
|
+
if (seenFunctions.has(fnName)) continue;
|
|
1655
|
+
seenFunctions.add(fnName);
|
|
1656
|
+
const alt = match.functionEntry.alternatives[0];
|
|
1657
|
+
if (!alt) continue;
|
|
1658
|
+
const diffColor = difficultyColor[alt.difficulty];
|
|
1659
|
+
console.log(
|
|
1660
|
+
` ${import_chalk.default.bold.white(fnName)} ${import_chalk.default.dim("\u2192")} ${import_chalk.default.cyan(alt.api)} ${import_chalk.default.dim("|")} Node \u2265 ${import_chalk.default.dim(alt.minNodeVersion)} ${import_chalk.default.dim("|")} ${diffColor(difficultyLabel[alt.difficulty])}` + (alt.docsUrl ? `
|
|
1661
|
+
${import_chalk.default.dim.underline(alt.docsUrl)}` : "")
|
|
1662
|
+
);
|
|
1663
|
+
console.log();
|
|
1664
|
+
console.log(wrapSnippet(alt));
|
|
1665
|
+
console.log();
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function printTimeMachine(history, currentScore) {
|
|
1670
|
+
if (history.length === 0) {
|
|
1671
|
+
console.log(import_chalk.default.dim("\n No history yet \u2014 run again to build a trend.\n"));
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const scores = [...history.map((h) => h.score), currentScore];
|
|
1675
|
+
const spark = sparkline(scores);
|
|
1676
|
+
const trend = scores[scores.length - 1] - scores[0];
|
|
1677
|
+
const trendStr = trend > 0 ? import_chalk.default.green(`\u25B2 +${trend} (leaner!)`) : trend < 0 ? import_chalk.default.red(`\u25BC ${trend} (getting fatter!)`) : import_chalk.default.dim("\u2501 no change");
|
|
1678
|
+
console.log(import_chalk.default.bold.white("\n TIME MACHINE\n"));
|
|
1679
|
+
console.log(` Score trend: ${import_chalk.default.cyan(spark)} ${trendStr}`);
|
|
1680
|
+
console.log();
|
|
1681
|
+
const table = new import_cli_table3.default({
|
|
1682
|
+
head: [import_chalk.default.bold.cyan("Date"), import_chalk.default.bold.cyan("Ref"), import_chalk.default.bold.cyan("Score"), import_chalk.default.bold.cyan("Grade")],
|
|
1683
|
+
style: { head: [], border: ["dim"] }
|
|
1684
|
+
});
|
|
1685
|
+
for (const entry of history.slice(-8)) {
|
|
1686
|
+
const color = gradeColor[entry.grade] ?? import_chalk.default.white;
|
|
1687
|
+
table.push([
|
|
1688
|
+
import_chalk.default.dim(new Date(entry.timestamp).toLocaleDateString()),
|
|
1689
|
+
import_chalk.default.dim(entry.gitRef ?? "\u2014"),
|
|
1690
|
+
color(String(entry.score)),
|
|
1691
|
+
color(entry.grade)
|
|
1692
|
+
]);
|
|
1693
|
+
}
|
|
1694
|
+
console.log(table.toString());
|
|
1695
|
+
console.log();
|
|
1696
|
+
}
|
|
1697
|
+
function printFooter(_result, fixMode) {
|
|
1698
|
+
const lines = [];
|
|
1699
|
+
if (fixMode) {
|
|
1700
|
+
lines.push(import_chalk.default.green(" \u2726 Run with --fix --yes to auto-apply trivial patches."));
|
|
1701
|
+
}
|
|
1702
|
+
lines.push(
|
|
1703
|
+
import_chalk.default.dim(" \u2726 Add to CI: ") + import_chalk.default.white("npx backend-diet scan --format json")
|
|
1704
|
+
);
|
|
1705
|
+
lines.push(
|
|
1706
|
+
import_chalk.default.dim(" \u2726 Get badge: ") + import_chalk.default.white("npx backend-diet scan --badge")
|
|
1707
|
+
);
|
|
1708
|
+
lines.push(
|
|
1709
|
+
import_chalk.default.dim(" \u2726 Track trend:") + import_chalk.default.white(" npx backend-diet scan --since HEAD~10")
|
|
1710
|
+
);
|
|
1711
|
+
lines.push("");
|
|
1712
|
+
lines.push(
|
|
1713
|
+
import_chalk.default.dim(" Built by ") + import_chalk.default.white("Marwan Said") + import_chalk.default.dim(" \xB7 github.com/1iPluto/backend-diet")
|
|
1714
|
+
);
|
|
1715
|
+
console.log(
|
|
1716
|
+
(0, import_boxen.default)(lines.join("\n"), {
|
|
1717
|
+
borderStyle: "round",
|
|
1718
|
+
borderColor: "dim",
|
|
1719
|
+
padding: 0,
|
|
1720
|
+
margin: { top: 1, bottom: 0, left: 0, right: 0 }
|
|
1721
|
+
})
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
function printError(msg) {
|
|
1725
|
+
console.error(import_chalk.default.red(`
|
|
1726
|
+
\u2717 ${msg}
|
|
1727
|
+
`));
|
|
1728
|
+
}
|
|
1729
|
+
function printInfo(msg) {
|
|
1730
|
+
console.log(import_chalk.default.dim(` \u2139 ${msg}`));
|
|
1731
|
+
}
|
|
1732
|
+
function printPatchPreview(file, before, after) {
|
|
1733
|
+
console.log(import_chalk.default.bold.white(`
|
|
1734
|
+
\u{1F4C4} ${file}
|
|
1735
|
+
`));
|
|
1736
|
+
console.log(formatDiff(before, after));
|
|
1737
|
+
console.log();
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/reporter/json.ts
|
|
1741
|
+
function formatBytes2(bytes) {
|
|
1742
|
+
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
1743
|
+
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(1)} KB`;
|
|
1744
|
+
return `${bytes} B`;
|
|
1745
|
+
}
|
|
1746
|
+
function buildJsonReport(result) {
|
|
1747
|
+
const uniquePackages = [...new Set(result.matches.map((m) => m.packageEntry.packageName))];
|
|
1748
|
+
return {
|
|
1749
|
+
version: "1.0.0",
|
|
1750
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1751
|
+
targetDir: result.targetDir,
|
|
1752
|
+
summary: {
|
|
1753
|
+
filesScanned: result.filesScanned,
|
|
1754
|
+
matchesFound: result.matches.length,
|
|
1755
|
+
uniquePackages,
|
|
1756
|
+
totalBytesSaved: result.totalBytesSaved,
|
|
1757
|
+
totalBytesSavedFormatted: formatBytes2(result.totalBytesSaved),
|
|
1758
|
+
refactorScore: result.refactorScore,
|
|
1759
|
+
grade: result.grade,
|
|
1760
|
+
scanDurationMs: result.scanDurationMs
|
|
1761
|
+
},
|
|
1762
|
+
matches: result.matches.map((m) => ({
|
|
1763
|
+
file: m.importRecord.file,
|
|
1764
|
+
line: m.importRecord.line,
|
|
1765
|
+
packageName: m.importRecord.packageName,
|
|
1766
|
+
functionName: m.importRecord.functionName,
|
|
1767
|
+
raw: m.importRecord.raw,
|
|
1768
|
+
alternatives: m.functionEntry.alternatives.map((a) => ({
|
|
1769
|
+
api: a.api,
|
|
1770
|
+
minNodeVersion: a.minNodeVersion,
|
|
1771
|
+
bytesSaved: a.bytesSaved,
|
|
1772
|
+
difficulty: a.difficulty,
|
|
1773
|
+
snippet: a.snippet,
|
|
1774
|
+
...a.docsUrl ? { docsUrl: a.docsUrl } : {}
|
|
1775
|
+
}))
|
|
1776
|
+
}))
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
function printJsonReport(result) {
|
|
1780
|
+
const report = buildJsonReport(result);
|
|
1781
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/patcher.ts
|
|
1785
|
+
var import_fs3 = __toESM(require("fs"));
|
|
1786
|
+
var import_path4 = __toESM(require("path"));
|
|
1787
|
+
function computePatches(matches, targetDir) {
|
|
1788
|
+
const results = [];
|
|
1789
|
+
const trivial = matches.filter(
|
|
1790
|
+
(m) => m.functionEntry.alternatives.some((a) => a.difficulty === "trivial")
|
|
1791
|
+
);
|
|
1792
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1793
|
+
for (const m of trivial) {
|
|
1794
|
+
const absFile = import_path4.default.resolve(targetDir, m.importRecord.file);
|
|
1795
|
+
if (!byFile.has(absFile)) byFile.set(absFile, []);
|
|
1796
|
+
byFile.get(absFile).push(m);
|
|
1797
|
+
}
|
|
1798
|
+
for (const [absFile, fileMatches] of byFile) {
|
|
1799
|
+
let source;
|
|
1800
|
+
try {
|
|
1801
|
+
source = import_fs3.default.readFileSync(absFile, "utf-8");
|
|
1802
|
+
} catch {
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
const lines = source.split("\n");
|
|
1806
|
+
for (const match of fileMatches) {
|
|
1807
|
+
const alt = match.functionEntry.alternatives.find((a) => a.difficulty === "trivial");
|
|
1808
|
+
if (!alt) continue;
|
|
1809
|
+
const lineIdx = match.importRecord.line - 1;
|
|
1810
|
+
const originalLine = lines[lineIdx];
|
|
1811
|
+
if (originalLine === void 0) continue;
|
|
1812
|
+
results.push({
|
|
1813
|
+
file: match.importRecord.file,
|
|
1814
|
+
originalLine: match.importRecord.line,
|
|
1815
|
+
before: originalLine,
|
|
1816
|
+
after: alt.snippet.after,
|
|
1817
|
+
applied: false
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return results;
|
|
1822
|
+
}
|
|
1823
|
+
function applyPatches(patches, targetDir) {
|
|
1824
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1825
|
+
for (const p of patches) {
|
|
1826
|
+
const absFile = import_path4.default.resolve(targetDir, p.file);
|
|
1827
|
+
if (!byFile.has(absFile)) byFile.set(absFile, []);
|
|
1828
|
+
byFile.get(absFile).push(p);
|
|
1829
|
+
}
|
|
1830
|
+
const applied = [];
|
|
1831
|
+
for (const [absFile, filePatches] of byFile) {
|
|
1832
|
+
let source;
|
|
1833
|
+
try {
|
|
1834
|
+
source = import_fs3.default.readFileSync(absFile, "utf-8");
|
|
1835
|
+
} catch {
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
let modified = source;
|
|
1839
|
+
for (const patch of filePatches) {
|
|
1840
|
+
if (modified.includes(patch.before)) {
|
|
1841
|
+
modified = modified.replace(patch.before, patch.after);
|
|
1842
|
+
applied.push({ ...patch, applied: true });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
try {
|
|
1846
|
+
import_fs3.default.writeFileSync(absFile, modified, "utf-8");
|
|
1847
|
+
} catch {
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return applied;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/badger.ts
|
|
1854
|
+
var BADGE_COLORS = {
|
|
1855
|
+
CRITICAL: "red",
|
|
1856
|
+
"NEEDS WORK": "yellow",
|
|
1857
|
+
HEALTHY: "green",
|
|
1858
|
+
SVELTE: "brightgreen"
|
|
1859
|
+
};
|
|
1860
|
+
function generateBadge(score, grade) {
|
|
1861
|
+
const color = BADGE_COLORS[grade];
|
|
1862
|
+
const label = encodeURIComponent("backend-diet");
|
|
1863
|
+
const message = encodeURIComponent(`${score}% lean`);
|
|
1864
|
+
const badgeUrl = `https://img.shields.io/badge/${label}-${message}-${color}?style=flat-square&logo=npm`;
|
|
1865
|
+
return `[](https://github.com/1iPluto/backend-diet)`;
|
|
1866
|
+
}
|
|
1867
|
+
function printBadge(score, grade) {
|
|
1868
|
+
const badge = generateBadge(score, grade);
|
|
1869
|
+
console.log("\n Paste this into your README.md:\n");
|
|
1870
|
+
console.log(` ${badge}`);
|
|
1871
|
+
console.log();
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// src/time-machine.ts
|
|
1875
|
+
var import_fs4 = __toESM(require("fs"));
|
|
1876
|
+
var import_path5 = __toESM(require("path"));
|
|
1877
|
+
var import_child_process = require("child_process");
|
|
1878
|
+
var HISTORY_FILE = ".backend-diet-history.json";
|
|
1879
|
+
function loadHistory(cwd) {
|
|
1880
|
+
const histPath = import_path5.default.join(cwd, HISTORY_FILE);
|
|
1881
|
+
try {
|
|
1882
|
+
const raw = import_fs4.default.readFileSync(histPath, "utf-8");
|
|
1883
|
+
return JSON.parse(raw);
|
|
1884
|
+
} catch {
|
|
1885
|
+
return [];
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function saveHistory(cwd, history) {
|
|
1889
|
+
const histPath = import_path5.default.join(cwd, HISTORY_FILE);
|
|
1890
|
+
try {
|
|
1891
|
+
import_fs4.default.writeFileSync(histPath, JSON.stringify(history, null, 2), "utf-8");
|
|
1892
|
+
} catch {
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
function getCurrentGitRef(cwd) {
|
|
1896
|
+
try {
|
|
1897
|
+
return (0, import_child_process.execSync)("git rev-parse --short HEAD", { cwd, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
1898
|
+
} catch {
|
|
1899
|
+
return void 0;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
function recordScanResult(result, cwd) {
|
|
1903
|
+
const history = loadHistory(cwd);
|
|
1904
|
+
const gitRef = getCurrentGitRef(cwd);
|
|
1905
|
+
const entry = {
|
|
1906
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1907
|
+
gitRef,
|
|
1908
|
+
score: result.refactorScore,
|
|
1909
|
+
grade: result.grade,
|
|
1910
|
+
totalBytesSaved: result.totalBytesSaved
|
|
1911
|
+
};
|
|
1912
|
+
history.push(entry);
|
|
1913
|
+
if (history.length > 50) history.splice(0, history.length - 50);
|
|
1914
|
+
saveHistory(cwd, history);
|
|
1915
|
+
}
|
|
1916
|
+
function getHistory(cwd) {
|
|
1917
|
+
return loadHistory(cwd);
|
|
1918
|
+
}
|
|
1919
|
+
function analyzeTimeMachineTrend(history, currentScore) {
|
|
1920
|
+
const allScores = [...history.map((h) => h.score), currentScore];
|
|
1921
|
+
const delta = allScores.length > 1 ? allScores[allScores.length - 1] - allScores[0] : 0;
|
|
1922
|
+
return {
|
|
1923
|
+
isImproving: delta >= 0,
|
|
1924
|
+
delta,
|
|
1925
|
+
sparkValues: allScores
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// src/cli.ts
|
|
1930
|
+
var VERSION = "1.0.0";
|
|
1931
|
+
async function runScan(directory, options) {
|
|
1932
|
+
const targetDir = import_path6.default.resolve(directory ?? process.cwd());
|
|
1933
|
+
const isJson = options.format === "json";
|
|
1934
|
+
if (!isJson && !options.noHeader) {
|
|
1935
|
+
printHeader();
|
|
1936
|
+
}
|
|
1937
|
+
const spinner = isJson ? null : (0, import_ora.default)({ text: "Scanning files\u2026", color: "cyan" }).start();
|
|
1938
|
+
try {
|
|
1939
|
+
const startMs = Date.now();
|
|
1940
|
+
const files = await walkFiles(targetDir, options.ignore ?? []);
|
|
1941
|
+
if (spinner) spinner.text = `Parsing ${files.length} files\u2026`;
|
|
1942
|
+
const imports = await parseFiles(files);
|
|
1943
|
+
if (spinner) spinner.text = "Matching against diet database\u2026";
|
|
1944
|
+
const matches = matchImports(imports);
|
|
1945
|
+
const scanDurationMs = Date.now() - startMs;
|
|
1946
|
+
const result = buildScanResult(targetDir, files.length, imports, matches, scanDurationMs);
|
|
1947
|
+
if (spinner) spinner.succeed(import_chalk2.default.green(`Scanned ${files.length} files in ${scanDurationMs}ms`));
|
|
1948
|
+
recordScanResult(result, targetDir);
|
|
1949
|
+
if (isJson) {
|
|
1950
|
+
printJsonReport(result);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
printScoreCard(result);
|
|
1954
|
+
printMatchesTable(matches);
|
|
1955
|
+
if (matches.length > 0) {
|
|
1956
|
+
printSuggestions(matches);
|
|
1957
|
+
}
|
|
1958
|
+
if (options.since) {
|
|
1959
|
+
printInfo("Time Machine: comparing score trend\u2026");
|
|
1960
|
+
const history = getHistory(targetDir);
|
|
1961
|
+
analyzeTimeMachineTrend(history, result.refactorScore);
|
|
1962
|
+
printTimeMachine(
|
|
1963
|
+
history.map((h) => ({
|
|
1964
|
+
timestamp: h.timestamp,
|
|
1965
|
+
score: h.score,
|
|
1966
|
+
gitRef: h.gitRef,
|
|
1967
|
+
grade: h.grade
|
|
1968
|
+
})),
|
|
1969
|
+
result.refactorScore
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
if (options.badge) {
|
|
1973
|
+
printBadge(result.refactorScore, result.grade);
|
|
1974
|
+
}
|
|
1975
|
+
if (options.fix) {
|
|
1976
|
+
const patches = computePatches(matches, targetDir);
|
|
1977
|
+
if (patches.length === 0) {
|
|
1978
|
+
printInfo("No trivial patches available for automatic rewriting.");
|
|
1979
|
+
} else {
|
|
1980
|
+
console.log(
|
|
1981
|
+
import_chalk2.default.bold.white(
|
|
1982
|
+
`
|
|
1983
|
+
AUTO-PATCH PREVIEW (${patches.length} trivial replacement${patches.length === 1 ? "" : "s"})
|
|
1984
|
+
`
|
|
1985
|
+
)
|
|
1986
|
+
);
|
|
1987
|
+
for (const patch of patches) {
|
|
1988
|
+
printPatchPreview(patch.file, patch.before, patch.after);
|
|
1989
|
+
}
|
|
1990
|
+
if (options.yes) {
|
|
1991
|
+
const applied = applyPatches(patches, targetDir);
|
|
1992
|
+
console.log(
|
|
1993
|
+
import_chalk2.default.green(`
|
|
1994
|
+
\u2713 Applied ${applied.length} patch${applied.length === 1 ? "" : "es"} successfully.
|
|
1995
|
+
`)
|
|
1996
|
+
);
|
|
1997
|
+
} else {
|
|
1998
|
+
console.log(import_chalk2.default.dim(" Add --yes to apply these patches in-place.\n"));
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
printFooter(result, !!options.fix && !options.yes);
|
|
2003
|
+
if (result.grade === "CRITICAL") {
|
|
2004
|
+
process.exit(1);
|
|
2005
|
+
}
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
if (spinner) spinner.fail("Scan failed");
|
|
2008
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2009
|
+
process.exit(1);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
var program = new import_commander.Command();
|
|
2013
|
+
program.name("backend-diet").version(VERSION, "-v, --version").description("Put your project on a diet. Find heavy packages, get native alternatives.").helpOption("-h, --help", "Display help");
|
|
2014
|
+
program.command("scan [directory]", { isDefault: true }).description("Scan source files for heavy library usage and suggest native alternatives").option("--fix", "Preview trivial auto-patches (dry run by default)").option("--yes", "Apply --fix patches in-place (requires --fix)").option("--badge", "Print a Shields.io badge for your README").option("--since <ref>", "Show score trend since a git ref (e.g. HEAD~10)").option("--format <fmt>", "Output format: terminal | json", "terminal").option("--ignore <patterns...>", 'Glob patterns to ignore (e.g. "vendor/**")').option("--no-header", "Suppress the ASCII art header").action(async (directory, options) => {
|
|
2015
|
+
await runScan(directory, {
|
|
2016
|
+
...options,
|
|
2017
|
+
noHeader: !options.header
|
|
2018
|
+
});
|
|
2019
|
+
});
|
|
2020
|
+
program.parse(process.argv);
|
|
2021
|
+
//# sourceMappingURL=cli.js.map
|