lambda-doctor 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,737 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/cli.ts
4
+ import { Command } from "commander";
5
+ import ora from "ora";
6
+ import fs6 from "fs/promises";
7
+ import path7 from "path";
8
+
9
+ // src/index.ts
10
+ import path6 from "path";
11
+
12
+ // src/analyzers/bundle-size.ts
13
+ import fg from "fast-glob";
14
+ import fs from "fs/promises";
15
+ import path from "path";
16
+ function getPackageName(filePath) {
17
+ const parts = filePath.split("/");
18
+ if (parts[0].startsWith("@")) {
19
+ return `${parts[0]}/${parts[1]}`;
20
+ }
21
+ return parts[0];
22
+ }
23
+ var bundleSizeAnalyzer = {
24
+ name: "bundle-size",
25
+ description: "Analyzes total node_modules size and identifies the largest dependencies",
26
+ async analyze(targetPath) {
27
+ const start = performance.now();
28
+ const diagnostics = [];
29
+ const nodeModulesPath = path.join(targetPath, "node_modules");
30
+ try {
31
+ await fs.access(nodeModulesPath);
32
+ } catch {
33
+ return {
34
+ analyzer: "bundle-size",
35
+ durationMs: performance.now() - start,
36
+ diagnostics: [],
37
+ metadata: { totalSizeBytes: 0, topDependencies: [] }
38
+ };
39
+ }
40
+ try {
41
+ const files = await fg("**/*", {
42
+ cwd: nodeModulesPath,
43
+ stats: true,
44
+ onlyFiles: true,
45
+ dot: true
46
+ });
47
+ const packageSizes = /* @__PURE__ */ new Map();
48
+ for (const entry of files) {
49
+ const pkgName = getPackageName(entry.path);
50
+ const size = entry.stats?.size ?? 0;
51
+ packageSizes.set(pkgName, (packageSizes.get(pkgName) ?? 0) + size);
52
+ }
53
+ const totalSizeBytes = [...packageSizes.values()].reduce((a, b) => a + b, 0);
54
+ const sorted = [...packageSizes.entries()].sort((a, b) => b[1] - a[1]);
55
+ const top10 = sorted.slice(0, 10);
56
+ const topDependencies = top10.map(([name, sizeBytes]) => ({
57
+ name,
58
+ version: "",
59
+ sizeBytes,
60
+ isHeavy: false
61
+ }));
62
+ const totalMB = totalSizeBytes / (1024 * 1024);
63
+ if (totalMB > 50) {
64
+ diagnostics.push({
65
+ analyzer: "bundle-size",
66
+ severity: "critical",
67
+ title: "Bundle size is critically large",
68
+ description: `Total node_modules size is ${totalMB.toFixed(1)}MB. AWS Lambda has a 250MB unzipped limit and large bundles severely impact cold start times.`,
69
+ recommendation: "Use a bundler (esbuild/webpack) to tree-shake and bundle only what you need. Remove unused dependencies.",
70
+ estimatedImpactMs: Math.min(totalMB * 10, 2e3)
71
+ });
72
+ } else if (totalMB > 10) {
73
+ diagnostics.push({
74
+ analyzer: "bundle-size",
75
+ severity: "warning",
76
+ title: "Bundle size is large",
77
+ description: `Total node_modules size is ${totalMB.toFixed(1)}MB. This adds unnecessary cold start latency.`,
78
+ recommendation: "Consider using a bundler to reduce the deployment package size.",
79
+ estimatedImpactMs: Math.min(totalMB * 5, 500)
80
+ });
81
+ }
82
+ for (const [name, sizeBytes] of sorted) {
83
+ const depMB = sizeBytes / (1024 * 1024);
84
+ if (depMB > 5) {
85
+ diagnostics.push({
86
+ analyzer: "bundle-size",
87
+ severity: "critical",
88
+ title: `Dependency "${name}" is very large (${depMB.toFixed(1)}MB)`,
89
+ description: `The package "${name}" takes up ${depMB.toFixed(1)}MB on disk.`,
90
+ recommendation: `Look for a lighter alternative to "${name}" or ensure it's being tree-shaken.`,
91
+ estimatedImpactMs: Math.min(depMB * 8, 500)
92
+ });
93
+ } else if (depMB > 1) {
94
+ diagnostics.push({
95
+ analyzer: "bundle-size",
96
+ severity: "warning",
97
+ title: `Dependency "${name}" is large (${depMB.toFixed(1)}MB)`,
98
+ description: `The package "${name}" takes up ${depMB.toFixed(1)}MB on disk.`,
99
+ recommendation: `Consider replacing "${name}" with a lighter alternative.`,
100
+ estimatedImpactMs: Math.min(depMB * 5, 200)
101
+ });
102
+ }
103
+ }
104
+ return {
105
+ analyzer: "bundle-size",
106
+ durationMs: performance.now() - start,
107
+ diagnostics,
108
+ metadata: { totalSizeBytes, topDependencies }
109
+ };
110
+ } catch (error) {
111
+ console.warn("bundle-size analyzer warning:", error);
112
+ return {
113
+ analyzer: "bundle-size",
114
+ durationMs: performance.now() - start,
115
+ diagnostics: [],
116
+ metadata: { totalSizeBytes: 0, topDependencies: [] }
117
+ };
118
+ }
119
+ }
120
+ };
121
+
122
+ // src/analyzers/heavy-dependencies.ts
123
+ import fs2 from "fs/promises";
124
+ import path2 from "path";
125
+
126
+ // known-heavy-packages.ts
127
+ var HEAVY_PACKAGES = [
128
+ {
129
+ name: "aws-sdk",
130
+ typicalSizeBytes: 65e6,
131
+ reason: "AWS SDK v2 is 65MB+ and loads all service clients. Lambda runtime includes it, but it's slow.",
132
+ alternative: "Use @aws-sdk/client-* (v3) with selective imports. Only import clients you need.",
133
+ estimatedSavingsMs: 400
134
+ },
135
+ {
136
+ name: "moment",
137
+ typicalSizeBytes: 48e5,
138
+ reason: "Moment.js is 4.8MB with locales. Not tree-shakeable.",
139
+ alternative: "Use dayjs (2KB) or date-fns with tree-shaking.",
140
+ estimatedSavingsMs: 50
141
+ },
142
+ {
143
+ name: "moment-timezone",
144
+ typicalSizeBytes: 82e5,
145
+ reason: "Moment-timezone adds 8MB+ of timezone data on top of Moment.",
146
+ alternative: "Use dayjs/plugin/timezone or Intl.DateTimeFormat (built-in).",
147
+ estimatedSavingsMs: 80
148
+ },
149
+ {
150
+ name: "lodash",
151
+ typicalSizeBytes: 14e5,
152
+ reason: "Full lodash is 1.4MB. Not tree-shakeable with CommonJS.",
153
+ alternative: "Use lodash-es (tree-shakeable) or individual packages like lodash.get.",
154
+ estimatedSavingsMs: 30
155
+ },
156
+ {
157
+ name: "axios",
158
+ typicalSizeBytes: 45e4,
159
+ reason: "Axios is 450KB. Overkill for Lambda where you can use native fetch (Node 18+).",
160
+ alternative: "Use native fetch (Node 18+) or undici.",
161
+ estimatedSavingsMs: 15
162
+ },
163
+ {
164
+ name: "express",
165
+ typicalSizeBytes: 55e4,
166
+ reason: "Express has 30+ dependencies. Heavy for a single Lambda function.",
167
+ alternative: "Use lambda-api (zero deps) or direct API Gateway event parsing.",
168
+ estimatedSavingsMs: 20
169
+ },
170
+ {
171
+ name: "bluebird",
172
+ typicalSizeBytes: 35e4,
173
+ reason: "Bluebird is unnecessary in Node 18+ which has native Promise with good performance.",
174
+ alternative: "Use native Promise (built-in).",
175
+ estimatedSavingsMs: 10
176
+ },
177
+ {
178
+ name: "uuid",
179
+ typicalSizeBytes: 15e4,
180
+ reason: "UUID package is 150KB. Node 18+ has crypto.randomUUID() built-in.",
181
+ alternative: "Use crypto.randomUUID() (built-in Node 18+).",
182
+ estimatedSavingsMs: 5
183
+ },
184
+ {
185
+ name: "winston",
186
+ typicalSizeBytes: 25e5,
187
+ reason: "Winston is 2.5MB with many transports. Too heavy for Lambda.",
188
+ alternative: "Use @aws-lambda-powertools/logger or pino.",
189
+ estimatedSavingsMs: 40
190
+ },
191
+ {
192
+ name: "joi",
193
+ typicalSizeBytes: 95e4,
194
+ reason: "Joi is ~1MB. Heavy for Lambda validation.",
195
+ alternative: "Use zod (lighter, TypeScript-native) or ajv.",
196
+ estimatedSavingsMs: 20
197
+ },
198
+ {
199
+ name: "mongoose",
200
+ typicalSizeBytes: 38e5,
201
+ reason: "Mongoose is 3.8MB. Extremely heavy for Lambda.",
202
+ alternative: "Use native MongoDB driver or Dynamoose for DynamoDB.",
203
+ estimatedSavingsMs: 100
204
+ },
205
+ {
206
+ name: "typescript",
207
+ typicalSizeBytes: 65e6,
208
+ reason: "TypeScript compiler in production bundle is 65MB. Should only be a devDependency.",
209
+ alternative: "Move to devDependencies. Deploy compiled JavaScript only.",
210
+ estimatedSavingsMs: 500
211
+ },
212
+ {
213
+ name: "ts-node",
214
+ typicalSizeBytes: 35e5,
215
+ reason: "ts-node adds 200-500ms cold start. Should not be in production.",
216
+ alternative: "Transpile to JavaScript before deployment.",
217
+ estimatedSavingsMs: 350
218
+ },
219
+ {
220
+ name: "@nestjs/core",
221
+ typicalSizeBytes: 52e5,
222
+ reason: "NestJS is a heavy framework (5MB+). Decorator metadata and DI container add cold start overhead.",
223
+ alternative: "Use lighter patterns for Lambda: plain handlers or lambda-api.",
224
+ estimatedSavingsMs: 150
225
+ },
226
+ {
227
+ name: "puppeteer",
228
+ typicalSizeBytes: 3e8,
229
+ reason: "Puppeteer bundles Chromium (300MB+). Exceeds Lambda package limits.",
230
+ alternative: "Use @sparticuz/chromium with puppeteer-core for Lambda.",
231
+ estimatedSavingsMs: 2e3
232
+ }
233
+ ];
234
+ function findHeavyPackage(name) {
235
+ return HEAVY_PACKAGES.find((pkg) => pkg.name === name);
236
+ }
237
+ function isHeavyPackage(name) {
238
+ return HEAVY_PACKAGES.some((pkg) => pkg.name === name);
239
+ }
240
+
241
+ // src/analyzers/heavy-dependencies.ts
242
+ var DEV_ONLY_PACKAGES = ["typescript", "ts-node", "ts-jest", "jest", "mocha", "eslint", "prettier", "tsup", "webpack", "rollup", "esbuild"];
243
+ var heavyDependenciesAnalyzer = {
244
+ name: "heavy-dependencies",
245
+ description: "Detects known heavy dependencies and dev tools in production",
246
+ async analyze(targetPath) {
247
+ const start = performance.now();
248
+ const diagnostics = [];
249
+ try {
250
+ const pkgJsonPath = path2.join(targetPath, "package.json");
251
+ const pkgJson = JSON.parse(await fs2.readFile(pkgJsonPath, "utf-8"));
252
+ const deps = pkgJson.dependencies ?? {};
253
+ const devDeps = pkgJson.devDependencies ?? {};
254
+ let heavyCount = 0;
255
+ for (const depName of Object.keys(deps)) {
256
+ const heavy = findHeavyPackage(depName);
257
+ if (heavy) {
258
+ heavyCount++;
259
+ diagnostics.push({
260
+ analyzer: "heavy-dependencies",
261
+ severity: "warning",
262
+ title: `Heavy dependency: ${depName}`,
263
+ description: heavy.reason,
264
+ recommendation: heavy.alternative,
265
+ estimatedImpactMs: heavy.estimatedSavingsMs,
266
+ filePath: "package.json"
267
+ });
268
+ }
269
+ }
270
+ for (const depName of Object.keys(deps)) {
271
+ if (DEV_ONLY_PACKAGES.includes(depName)) {
272
+ diagnostics.push({
273
+ analyzer: "heavy-dependencies",
274
+ severity: "critical",
275
+ title: `Dev tool "${depName}" in production dependencies`,
276
+ description: `"${depName}" is a development tool that should not be in "dependencies". It adds unnecessary size and cold start time.`,
277
+ recommendation: `Move "${depName}" from "dependencies" to "devDependencies".`,
278
+ estimatedImpactMs: findHeavyPackage(depName)?.estimatedSavingsMs ?? 100,
279
+ filePath: "package.json"
280
+ });
281
+ }
282
+ }
283
+ return {
284
+ analyzer: "heavy-dependencies",
285
+ durationMs: performance.now() - start,
286
+ diagnostics,
287
+ metadata: { heavyCount, totalDependencies: Object.keys(deps).length }
288
+ };
289
+ } catch (error) {
290
+ console.warn("heavy-dependencies analyzer warning:", error);
291
+ return {
292
+ analyzer: "heavy-dependencies",
293
+ durationMs: performance.now() - start,
294
+ diagnostics: []
295
+ };
296
+ }
297
+ }
298
+ };
299
+
300
+ // src/analyzers/import-analysis.ts
301
+ import fg2 from "fast-glob";
302
+ import fs3 from "fs/promises";
303
+ import path3 from "path";
304
+ var ESM_IMPORT = /^import\s+.*\s+from\s+['"](.+)['"]/gm;
305
+ var CJS_REQUIRE = /^(?:const|let|var)\s+.*=\s*require\(['"](.+)['"]\)/gm;
306
+ var WILDCARD_IMPORT = /^import\s+\*\s+as\s+\w+\s+from\s+['"](.+)['"]/gm;
307
+ function getPackageName2(specifier) {
308
+ if (specifier.startsWith(".") || specifier.startsWith("/")) return null;
309
+ const parts = specifier.split("/");
310
+ if (parts[0].startsWith("@")) {
311
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
312
+ }
313
+ return parts[0];
314
+ }
315
+ function isTopLevel(line) {
316
+ const indent = line.length - line.trimStart().length;
317
+ return indent <= 1;
318
+ }
319
+ var importAnalysisAnalyzer = {
320
+ name: "import-analysis",
321
+ description: "Analyzes import patterns for tree-shaking issues and heavy top-level imports",
322
+ async analyze(targetPath) {
323
+ const start = performance.now();
324
+ const diagnostics = [];
325
+ try {
326
+ const files = await fg2(["**/*.ts", "**/*.js", "**/*.mjs"], {
327
+ cwd: targetPath,
328
+ ignore: ["node_modules/**", "dist/**", "**/*.d.ts"],
329
+ absolute: false
330
+ });
331
+ for (const file of files) {
332
+ const filePath = path3.join(targetPath, file);
333
+ const content = await fs3.readFile(filePath, "utf-8");
334
+ const lines = content.split("\n");
335
+ for (let i = 0; i < lines.length; i++) {
336
+ const line = lines[i];
337
+ let match;
338
+ WILDCARD_IMPORT.lastIndex = 0;
339
+ while ((match = WILDCARD_IMPORT.exec(line)) !== null) {
340
+ const pkg = getPackageName2(match[1]);
341
+ if (pkg) {
342
+ diagnostics.push({
343
+ analyzer: "import-analysis",
344
+ severity: "warning",
345
+ title: `Wildcard import of "${pkg}" prevents tree-shaking`,
346
+ description: `"import * as ..." from "${match[1]}" imports the entire module, preventing bundlers from removing unused code.`,
347
+ recommendation: `Use named imports: import { specificFunction } from "${match[1]}"`,
348
+ estimatedImpactMs: 20,
349
+ filePath: file,
350
+ line: i + 1
351
+ });
352
+ }
353
+ }
354
+ if (isTopLevel(line)) {
355
+ for (const regex of [ESM_IMPORT, CJS_REQUIRE]) {
356
+ regex.lastIndex = 0;
357
+ while ((match = regex.exec(line)) !== null) {
358
+ const pkg = getPackageName2(match[1]);
359
+ if (pkg && isHeavyPackage(pkg)) {
360
+ const heavy = findHeavyPackage(pkg);
361
+ diagnostics.push({
362
+ analyzer: "import-analysis",
363
+ severity: "warning",
364
+ title: `Top-level import of heavy package "${pkg}"`,
365
+ description: `"${pkg}" is imported at the top level in ${file}. This forces it to load during cold start even if not needed for every invocation.`,
366
+ recommendation: `Consider lazy-loading: move the import inside the function that uses it. ${heavy.alternative}`,
367
+ estimatedImpactMs: heavy.estimatedSavingsMs / 2,
368
+ filePath: file,
369
+ line: i + 1
370
+ });
371
+ }
372
+ }
373
+ }
374
+ }
375
+ }
376
+ }
377
+ return {
378
+ analyzer: "import-analysis",
379
+ durationMs: performance.now() - start,
380
+ diagnostics,
381
+ metadata: { filesScanned: files.length }
382
+ };
383
+ } catch (error) {
384
+ console.warn("import-analysis analyzer warning:", error);
385
+ return {
386
+ analyzer: "import-analysis",
387
+ durationMs: performance.now() - start,
388
+ diagnostics: []
389
+ };
390
+ }
391
+ }
392
+ };
393
+
394
+ // src/analyzers/aws-sdk.ts
395
+ import fs4 from "fs/promises";
396
+ import fg3 from "fast-glob";
397
+ import path4 from "path";
398
+ var awsSdkAnalyzer = {
399
+ name: "aws-sdk",
400
+ description: "Checks AWS SDK version usage and migration status",
401
+ async analyze(targetPath) {
402
+ const start = performance.now();
403
+ const diagnostics = [];
404
+ try {
405
+ const pkgJsonPath = path4.join(targetPath, "package.json");
406
+ const pkgJson = JSON.parse(await fs4.readFile(pkgJsonPath, "utf-8"));
407
+ const allDeps = { ...pkgJson.dependencies ?? {}, ...pkgJson.devDependencies ?? {} };
408
+ const hasV2 = "aws-sdk" in allDeps;
409
+ const v3Clients = Object.keys(allDeps).filter((d) => d.startsWith("@aws-sdk/client-"));
410
+ const hasV3 = v3Clients.length > 0;
411
+ if (hasV2 && !hasV3) {
412
+ diagnostics.push({
413
+ analyzer: "aws-sdk",
414
+ severity: "critical",
415
+ title: "Using AWS SDK v2 (aws-sdk)",
416
+ description: "AWS SDK v2 is 65MB+ and loads all service clients. It adds 400ms+ to cold starts.",
417
+ recommendation: "Migrate to AWS SDK v3 (@aws-sdk/client-*). Only import the clients you need.",
418
+ estimatedImpactMs: 400,
419
+ filePath: "package.json"
420
+ });
421
+ }
422
+ if (hasV2 && hasV3) {
423
+ diagnostics.push({
424
+ analyzer: "aws-sdk",
425
+ severity: "warning",
426
+ title: "Incomplete AWS SDK v2 to v3 migration",
427
+ description: "Both aws-sdk (v2) and @aws-sdk/client-* (v3) are present. This means v2 is still bundled alongside v3.",
428
+ recommendation: 'Complete the migration to SDK v3 and remove the "aws-sdk" dependency.',
429
+ estimatedImpactMs: 400,
430
+ filePath: "package.json"
431
+ });
432
+ }
433
+ if (v3Clients.length > 5) {
434
+ diagnostics.push({
435
+ analyzer: "aws-sdk",
436
+ severity: "info",
437
+ title: `${v3Clients.length} AWS SDK v3 clients detected`,
438
+ description: `This Lambda uses ${v3Clients.length} AWS SDK clients: ${v3Clients.join(", ")}. Each client adds to bundle size.`,
439
+ recommendation: "Verify all clients are necessary. Consider splitting into multiple Lambdas if responsibilities are too broad.",
440
+ estimatedImpactMs: v3Clients.length * 5
441
+ });
442
+ }
443
+ const sourceFiles = await fg3(["**/*.ts", "**/*.js", "**/*.mjs"], {
444
+ cwd: targetPath,
445
+ ignore: ["node_modules/**", "dist/**", "**/*.d.ts"],
446
+ absolute: false
447
+ });
448
+ for (const file of sourceFiles) {
449
+ const content = await fs4.readFile(path4.join(targetPath, file), "utf-8");
450
+ if (content.includes("@aws-sdk/client-sso")) {
451
+ diagnostics.push({
452
+ analyzer: "aws-sdk",
453
+ severity: "warning",
454
+ title: "Unnecessary SSO client in Lambda",
455
+ description: "@aws-sdk/client-sso is imported but Lambda functions use IAM roles, not SSO authentication.",
456
+ recommendation: "Remove the @aws-sdk/client-sso import. Lambda uses IAM execution roles for authentication.",
457
+ estimatedImpactMs: 15,
458
+ filePath: file
459
+ });
460
+ break;
461
+ }
462
+ }
463
+ return {
464
+ analyzer: "aws-sdk",
465
+ durationMs: performance.now() - start,
466
+ diagnostics,
467
+ metadata: { hasV2, hasV3, v3ClientCount: v3Clients.length }
468
+ };
469
+ } catch (error) {
470
+ console.warn("aws-sdk analyzer warning:", error);
471
+ return {
472
+ analyzer: "aws-sdk",
473
+ durationMs: performance.now() - start,
474
+ diagnostics: []
475
+ };
476
+ }
477
+ }
478
+ };
479
+
480
+ // src/analyzers/bundler-detection.ts
481
+ import fs5 from "fs/promises";
482
+ import fg4 from "fast-glob";
483
+ import path5 from "path";
484
+ var BUNDLER_PACKAGES = ["esbuild", "webpack", "rollup", "tsup", "parcel"];
485
+ var BUNDLER_CONFIGS = [
486
+ "webpack.config.*",
487
+ "rollup.config.*",
488
+ "tsup.config.*",
489
+ "esbuild.config.*",
490
+ ".parcelrc"
491
+ ];
492
+ var BUNDLER_KEYWORDS = ["esbuild", "webpack", "rollup", "tsup", "parcel", "bundle"];
493
+ var bundlerDetectionAnalyzer = {
494
+ name: "bundler-detection",
495
+ description: "Detects whether a bundler is configured and checks ESM/bundler setup",
496
+ async analyze(targetPath) {
497
+ const start = performance.now();
498
+ const diagnostics = [];
499
+ try {
500
+ const pkgJsonPath = path5.join(targetPath, "package.json");
501
+ const pkgJson = JSON.parse(await fs5.readFile(pkgJsonPath, "utf-8"));
502
+ const devDeps = pkgJson.devDependencies ?? {};
503
+ const deps = pkgJson.dependencies ?? {};
504
+ const allDeps = { ...deps, ...devDeps };
505
+ const scripts = pkgJson.scripts ?? {};
506
+ const detectedBundlers = [];
507
+ for (const bundler of BUNDLER_PACKAGES) {
508
+ if (bundler in allDeps) {
509
+ detectedBundlers.push(bundler);
510
+ }
511
+ }
512
+ const configFiles = await fg4(BUNDLER_CONFIGS, {
513
+ cwd: targetPath,
514
+ dot: true
515
+ });
516
+ const bundlerScripts = [];
517
+ for (const [name, script] of Object.entries(scripts)) {
518
+ if (typeof script === "string" && BUNDLER_KEYWORDS.some((kw) => script.includes(kw))) {
519
+ bundlerScripts.push(name);
520
+ }
521
+ }
522
+ let serverlessPlugin = null;
523
+ try {
524
+ const slsPath = path5.join(targetPath, "serverless.yml");
525
+ const slsContent = await fs5.readFile(slsPath, "utf-8");
526
+ if (slsContent.includes("serverless-esbuild")) serverlessPlugin = "serverless-esbuild";
527
+ else if (slsContent.includes("serverless-webpack")) serverlessPlugin = "serverless-webpack";
528
+ } catch {
529
+ }
530
+ const isESM = pkgJson.type === "module";
531
+ const hasBundler = detectedBundlers.length > 0 || configFiles.length > 0 || serverlessPlugin !== null;
532
+ if (!hasBundler) {
533
+ diagnostics.push({
534
+ analyzer: "bundler-detection",
535
+ severity: "critical",
536
+ title: "No bundler detected",
537
+ description: "No bundler (esbuild, webpack, rollup, etc.) was found in this project. Without a bundler, the entire node_modules directory is deployed, causing large bundle sizes and slow cold starts.",
538
+ recommendation: "Add esbuild (fastest) or webpack to bundle your Lambda function. This is typically the single biggest cold start improvement you can make.",
539
+ estimatedImpactMs: 500
540
+ });
541
+ } else {
542
+ diagnostics.push({
543
+ analyzer: "bundler-detection",
544
+ severity: "info",
545
+ title: `Bundler detected: ${detectedBundlers.join(", ") || serverlessPlugin || "config found"}`,
546
+ description: `Found bundler setup: packages=[${detectedBundlers.join(", ")}], configs=[${configFiles.join(", ")}]${serverlessPlugin ? `, serverless plugin=${serverlessPlugin}` : ""}.`,
547
+ recommendation: "Ensure your bundler is configured for tree-shaking and minification.",
548
+ estimatedImpactMs: 0
549
+ });
550
+ }
551
+ if (!isESM) {
552
+ diagnostics.push({
553
+ analyzer: "bundler-detection",
554
+ severity: "info",
555
+ title: "Project is not using ESM",
556
+ description: '"type": "module" is not set in package.json. ESM enables better tree-shaking with bundlers.',
557
+ recommendation: 'Consider setting "type": "module" in package.json for better tree-shaking support.',
558
+ estimatedImpactMs: 20
559
+ });
560
+ }
561
+ return {
562
+ analyzer: "bundler-detection",
563
+ durationMs: performance.now() - start,
564
+ diagnostics,
565
+ metadata: {
566
+ detectedBundlers,
567
+ configFiles,
568
+ bundlerScripts,
569
+ serverlessPlugin,
570
+ isESM,
571
+ hasBundler
572
+ }
573
+ };
574
+ } catch (error) {
575
+ console.warn("bundler-detection analyzer warning:", error);
576
+ return {
577
+ analyzer: "bundler-detection",
578
+ durationMs: performance.now() - start,
579
+ diagnostics: []
580
+ };
581
+ }
582
+ }
583
+ };
584
+
585
+ // src/reporters/console.ts
586
+ import chalk from "chalk";
587
+ var SEVERITY_ICON = {
588
+ critical: "\u{1F534}",
589
+ warning: "\u26A0\uFE0F",
590
+ info: "\u{1F4A1}"
591
+ };
592
+ var SEVERITY_COLOR = {
593
+ critical: chalk.red,
594
+ warning: chalk.yellow,
595
+ info: chalk.blue
596
+ };
597
+ function formatBytes(bytes) {
598
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
599
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB`;
600
+ return `${bytes}B`;
601
+ }
602
+ function printReport(report) {
603
+ console.log();
604
+ console.log(chalk.bold.cyan("\u{1FA7A} Lambda Doctor \u2014 Diagnosis Report"));
605
+ console.log(chalk.gray(` Target: ${report.targetPath}`));
606
+ console.log(chalk.gray(` Date: ${report.timestamp}`));
607
+ console.log();
608
+ const bundleSizeResult = report.results.find((r) => r.analyzer === "bundle-size");
609
+ if (bundleSizeResult?.metadata) {
610
+ const { totalSizeBytes, topDependencies } = bundleSizeResult.metadata;
611
+ if (totalSizeBytes > 0) {
612
+ console.log(chalk.bold("\u{1F4E6} Bundle Size Breakdown"));
613
+ console.log(chalk.gray(` Total: ${formatBytes(totalSizeBytes)}`));
614
+ console.log();
615
+ if (topDependencies && topDependencies.length > 0) {
616
+ console.log(chalk.gray(" Top Dependencies:"));
617
+ for (const dep of topDependencies) {
618
+ const bar = "\u2588".repeat(Math.max(1, Math.round(dep.sizeBytes / totalSizeBytes * 30)));
619
+ console.log(` ${chalk.white(dep.name.padEnd(35))} ${formatBytes(dep.sizeBytes).padStart(10)} ${chalk.green(bar)}`);
620
+ }
621
+ console.log();
622
+ }
623
+ }
624
+ }
625
+ const allDiagnostics = report.results.flatMap((r) => r.diagnostics);
626
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
627
+ const sorted = [...allDiagnostics].sort(
628
+ (a, b) => severityOrder[a.severity] - severityOrder[b.severity]
629
+ );
630
+ if (sorted.length === 0) {
631
+ console.log(chalk.green.bold("\u2705 No issues found! Your Lambda looks healthy."));
632
+ console.log();
633
+ return;
634
+ }
635
+ console.log(chalk.bold("\u{1F50D} Diagnostics"));
636
+ console.log();
637
+ for (const diag of sorted) {
638
+ const icon = SEVERITY_ICON[diag.severity];
639
+ const colorFn = SEVERITY_COLOR[diag.severity];
640
+ const location = diag.filePath ? chalk.gray(` (${diag.filePath}${diag.line ? `:${diag.line}` : ""})`) : "";
641
+ console.log(`${icon} ${colorFn(diag.severity.toUpperCase())} ${chalk.bold(diag.title)}${location}`);
642
+ console.log(chalk.gray(` ${diag.description}`));
643
+ console.log(chalk.green(` \u2192 ${diag.recommendation}`));
644
+ if (diag.estimatedImpactMs > 0) {
645
+ console.log(chalk.cyan(` \u23F1 Est. improvement: ~${diag.estimatedImpactMs}ms`));
646
+ }
647
+ console.log();
648
+ }
649
+ const { summary } = report;
650
+ console.log(chalk.bold("\u2500".repeat(60)));
651
+ console.log(
652
+ chalk.bold("Summary: ") + chalk.red(`${summary.critical} critical`) + chalk.gray(" | ") + chalk.yellow(`${summary.warnings} warnings`) + chalk.gray(" | ") + chalk.blue(`${summary.info} info`)
653
+ );
654
+ if (summary.estimatedTotalImpactMs > 0) {
655
+ console.log(
656
+ chalk.bold.green(`
657
+ \u{1F680} Total estimated cold start improvement: ~${summary.estimatedTotalImpactMs}ms`)
658
+ );
659
+ }
660
+ console.log();
661
+ }
662
+
663
+ // src/index.ts
664
+ var ALL_ANALYZERS = [
665
+ bundleSizeAnalyzer,
666
+ heavyDependenciesAnalyzer,
667
+ importAnalysisAnalyzer,
668
+ awsSdkAnalyzer,
669
+ bundlerDetectionAnalyzer
670
+ ];
671
+ async function analyze(config) {
672
+ const targetPath = path6.resolve(config.targetPath);
673
+ const start = performance.now();
674
+ const analyzersToRun = config.analyzers ? ALL_ANALYZERS.filter((a) => config.analyzers.includes(a.name)) : ALL_ANALYZERS;
675
+ const results = await Promise.all(
676
+ analyzersToRun.map((a) => a.analyze(targetPath))
677
+ );
678
+ const allDiagnostics = results.flatMap((r) => r.diagnostics);
679
+ const summary = {
680
+ totalIssues: allDiagnostics.length,
681
+ critical: allDiagnostics.filter((d) => d.severity === "critical").length,
682
+ warnings: allDiagnostics.filter((d) => d.severity === "warning").length,
683
+ info: allDiagnostics.filter((d) => d.severity === "info").length,
684
+ estimatedTotalImpactMs: allDiagnostics.reduce((sum, d) => sum + d.estimatedImpactMs, 0),
685
+ heavyDependencies: results.find((r) => r.analyzer === "heavy-dependencies")?.metadata?.heavyCount ?? 0,
686
+ bundleSizeBytes: results.find((r) => r.analyzer === "bundle-size")?.metadata?.totalSizeBytes ?? void 0
687
+ };
688
+ return {
689
+ targetPath,
690
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
691
+ totalDurationMs: performance.now() - start,
692
+ results,
693
+ summary
694
+ };
695
+ }
696
+
697
+ // src/bin/cli.ts
698
+ var program = new Command();
699
+ program.name("lambda-doctor").description("Diagnose and fix AWS Lambda cold start performance issues").version("0.1.0");
700
+ program.command("analyze").description("Analyze a Lambda project directory").argument("<path>", "Path to the Lambda project directory").option("--format <format>", "Output format: console or json", "console").option("--verbose", "Show verbose output", false).action(async (targetPath, options) => {
701
+ const resolvedPath = path7.resolve(targetPath);
702
+ try {
703
+ await fs6.access(resolvedPath);
704
+ } catch {
705
+ console.error(`Error: Path does not exist: ${resolvedPath}`);
706
+ process.exit(1);
707
+ }
708
+ try {
709
+ await fs6.access(path7.join(resolvedPath, "package.json"));
710
+ } catch {
711
+ console.error(`Error: No package.json found in ${resolvedPath}`);
712
+ process.exit(1);
713
+ }
714
+ const spinner = ora("Analyzing Lambda project...").start();
715
+ try {
716
+ const report = await analyze({
717
+ targetPath: resolvedPath,
718
+ format: options.format,
719
+ verbose: options.verbose
720
+ });
721
+ spinner.stop();
722
+ if (options.format === "json") {
723
+ console.log(JSON.stringify(report, null, 2));
724
+ } else {
725
+ printReport(report);
726
+ }
727
+ if (report.summary.critical > 0) {
728
+ process.exit(1);
729
+ }
730
+ } catch (error) {
731
+ spinner.fail("Analysis failed");
732
+ console.error(error);
733
+ process.exit(1);
734
+ }
735
+ });
736
+ program.parse();
737
+ //# sourceMappingURL=cli.js.map