@infograb/docker-slim-advisor 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/LICENSE +21 -0
- package/README.md +274 -0
- package/build/base-image-db-loader.d.ts +16 -0
- package/build/base-image-db-loader.js +72 -0
- package/build/base-image-db-loader.js.map +1 -0
- package/build/cli.d.ts +47 -0
- package/build/cli.js +142 -0
- package/build/cli.js.map +1 -0
- package/build/data/base-image-db-schema.d.ts +117 -0
- package/build/data/base-image-db-schema.js +108 -0
- package/build/data/base-image-db-schema.js.map +1 -0
- package/build/data/base-image-db.d.ts +21 -0
- package/build/data/base-image-db.js +190 -0
- package/build/data/base-image-db.js.map +1 -0
- package/build/exit-codes.d.ts +25 -0
- package/build/exit-codes.js +31 -0
- package/build/exit-codes.js.map +1 -0
- package/build/formatters/formatter-dispatcher.d.ts +15 -0
- package/build/formatters/formatter-dispatcher.js +27 -0
- package/build/formatters/formatter-dispatcher.js.map +1 -0
- package/build/formatters/json-formatter.d.ts +10 -0
- package/build/formatters/json-formatter.js +52 -0
- package/build/formatters/json-formatter.js.map +1 -0
- package/build/formatters/json-schema.d.ts +57 -0
- package/build/formatters/json-schema.js +13 -0
- package/build/formatters/json-schema.js.map +1 -0
- package/build/formatters/markdown-formatter.d.ts +12 -0
- package/build/formatters/markdown-formatter.js +97 -0
- package/build/formatters/markdown-formatter.js.map +1 -0
- package/build/formatters/terminal-formatter.d.ts +12 -0
- package/build/formatters/terminal-formatter.js +142 -0
- package/build/formatters/terminal-formatter.js.map +1 -0
- package/build/image-size-lookup.d.ts +35 -0
- package/build/image-size-lookup.js +187 -0
- package/build/image-size-lookup.js.map +1 -0
- package/build/multi-stage-detector.d.ts +29 -0
- package/build/multi-stage-detector.js +29 -0
- package/build/multi-stage-detector.js.map +1 -0
- package/build/output.d.ts +46 -0
- package/build/output.js +62 -0
- package/build/output.js.map +1 -0
- package/build/parser.d.ts +7 -0
- package/build/parser.js +123 -0
- package/build/parser.js.map +1 -0
- package/build/rules/alpine-swap.d.ts +10 -0
- package/build/rules/alpine-swap.js +81 -0
- package/build/rules/alpine-swap.js.map +1 -0
- package/build/rules/dockerignore-missing.d.ts +19 -0
- package/build/rules/dockerignore-missing.js +107 -0
- package/build/rules/dockerignore-missing.js.map +1 -0
- package/build/rules/index.d.ts +11 -0
- package/build/rules/index.js +22 -0
- package/build/rules/index.js.map +1 -0
- package/build/rules/package-cache-cleanup.d.ts +15 -0
- package/build/rules/package-cache-cleanup.js +89 -0
- package/build/rules/package-cache-cleanup.js.map +1 -0
- package/build/rules/run-merge.d.ts +12 -0
- package/build/rules/run-merge.js +67 -0
- package/build/rules/run-merge.js.map +1 -0
- package/build/rules/unnecessary-packages.d.ts +23 -0
- package/build/rules/unnecessary-packages.js +184 -0
- package/build/rules/unnecessary-packages.js.map +1 -0
- package/build/severity-filter.d.ts +22 -0
- package/build/severity-filter.js +31 -0
- package/build/severity-filter.js.map +1 -0
- package/build/size-estimator.d.ts +35 -0
- package/build/size-estimator.js +238 -0
- package/build/size-estimator.js.map +1 -0
- package/build/tty-detection.d.ts +20 -0
- package/build/tty-detection.js +33 -0
- package/build/tty-detection.js.map +1 -0
- package/build/types.d.ts +82 -0
- package/build/types.js +6 -0
- package/build/types.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 docker-slim-advisor contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# docker-slim-advisor
|
|
2
|
+
|
|
3
|
+
Static Dockerfile analyzer that recommends image size optimizations — no Docker build required.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @infograb/docker-slim-advisor analyze ./Dockerfile
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @infograb/docker-slim-advisor ./Dockerfile
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Example output** (against a typical Node.js app):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Docker Slim Advisor
|
|
21
|
+
--------------------------------------------------
|
|
22
|
+
|
|
23
|
+
File: Dockerfile
|
|
24
|
+
Base Image: node:18
|
|
25
|
+
|
|
26
|
+
Size Prediction
|
|
27
|
+
|
|
28
|
+
Before: 1.1GB
|
|
29
|
+
After: 226.6MB
|
|
30
|
+
|
|
31
|
+
[██████░░░░░░░░░░░░░░░░░░░░░░░░]
|
|
32
|
+
|
|
33
|
+
Estimated savings: 904.9MB (80% reduction)
|
|
34
|
+
|
|
35
|
+
Findings (2)
|
|
36
|
+
|
|
37
|
+
[HIGH] Use node:18-slim instead of node:18 (DSA001) Line 1
|
|
38
|
+
Switching from node:18 (1.0GB) to node:18-slim (200MB) saves ~800MB (80% reduction).
|
|
39
|
+
Fix: FROM node:18-slim
|
|
40
|
+
Saves ~800.0MB
|
|
41
|
+
|
|
42
|
+
[HIGH] Broad COPY/ADD without .dockerignore optimization (DSA004) Line 5
|
|
43
|
+
`COPY .` copies the entire build context. Without a comprehensive .dockerignore,
|
|
44
|
+
this includes node_modules, .git, build artifacts, and other unnecessary files.
|
|
45
|
+
Fix: Add node_modules, .git, dist, build, .env to .dockerignore
|
|
46
|
+
Saves ~104.9MB
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- **5 optimization rules** — alpine/slim base swap, RUN layer merge, apt/apk cache cleanup, `.dockerignore` detection, unnecessary package removal
|
|
54
|
+
- **3 output formats** — terminal (TTY-aware), JSON (versioned schema), Markdown
|
|
55
|
+
- **Size prediction** — before/after estimates with ±30% accuracy, no `docker build` needed
|
|
56
|
+
- **CI/CD ready** — structured exit codes, `NO_COLOR` support, stderr/stdout separation
|
|
57
|
+
- **Fast** — analysis completes in under 1 second for typical Dockerfiles
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
**Global install:**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install -g @infograb/docker-slim-advisor
|
|
67
|
+
docker-slim-advisor ./Dockerfile
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**One-off with npx (no install):**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx @infograb/docker-slim-advisor ./Dockerfile
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Requirements:** Node.js >= 18
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
docker-slim-advisor [dockerfile] [options]
|
|
84
|
+
|
|
85
|
+
Arguments:
|
|
86
|
+
dockerfile Path to Dockerfile (default: "Dockerfile")
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
-f, --format <format> Output format: terminal, json, markdown (default: "terminal")
|
|
90
|
+
-s, --severity <level> Minimum severity to report: LOW, MEDIUM, HIGH
|
|
91
|
+
-V, --version Print version
|
|
92
|
+
-h, --help Display help
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Examples:**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Analyze Dockerfile in current directory
|
|
99
|
+
docker-slim-advisor
|
|
100
|
+
|
|
101
|
+
# Analyze a specific file
|
|
102
|
+
docker-slim-advisor path/to/Dockerfile
|
|
103
|
+
|
|
104
|
+
# JSON output (machine-readable)
|
|
105
|
+
docker-slim-advisor --format json ./Dockerfile
|
|
106
|
+
|
|
107
|
+
# Only report HIGH severity findings
|
|
108
|
+
docker-slim-advisor --severity HIGH ./Dockerfile
|
|
109
|
+
|
|
110
|
+
# Markdown report for documentation
|
|
111
|
+
docker-slim-advisor --format markdown ./Dockerfile > report.md
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Exit Codes
|
|
115
|
+
|
|
116
|
+
| Code | Meaning |
|
|
117
|
+
|------|---------|
|
|
118
|
+
| `0` | No HIGH severity findings |
|
|
119
|
+
| `1` | One or more HIGH findings present |
|
|
120
|
+
| `2` | Error (file not found, parse failure, etc.) |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Output Formats
|
|
125
|
+
|
|
126
|
+
### Terminal (default)
|
|
127
|
+
|
|
128
|
+
Color and emoji output when connected to a TTY. Plain text in pipes. Respects `NO_COLOR` environment variable.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
docker-slim-advisor ./Dockerfile
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### JSON
|
|
135
|
+
|
|
136
|
+
Versioned schema suitable for downstream tooling.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
docker-slim-advisor --format json ./Dockerfile
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"schemaVersion": 1,
|
|
145
|
+
"dockerfilePath": "Dockerfile",
|
|
146
|
+
"isMultiStage": false,
|
|
147
|
+
"baseImage": "node:18",
|
|
148
|
+
"estimatedBeforeSize": {
|
|
149
|
+
"totalBytes": 1131500000,
|
|
150
|
+
"humanReadable": "1.1GB"
|
|
151
|
+
},
|
|
152
|
+
"estimatedAfterSize": {
|
|
153
|
+
"totalBytes": 226642400,
|
|
154
|
+
"humanReadable": "227MB"
|
|
155
|
+
},
|
|
156
|
+
"sizeReductionPercent": 80,
|
|
157
|
+
"totalFindings": 2,
|
|
158
|
+
"findings": [
|
|
159
|
+
{
|
|
160
|
+
"ruleId": "DSA001",
|
|
161
|
+
"severity": "HIGH",
|
|
162
|
+
"line": 1,
|
|
163
|
+
"title": "Use node:18-slim instead of node:18",
|
|
164
|
+
"description": "Switching from node:18 (1.0GB) to node:18-slim (200MB) saves ~800MB.",
|
|
165
|
+
"fix": "FROM node:18-slim",
|
|
166
|
+
"estimatedSavingsBytes": 800000000
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Markdown
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
docker-slim-advisor --format markdown ./Dockerfile
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Produces a GitHub-compatible report with a findings table and per-rule detail sections. Suitable for PR comments or documentation.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## CI/CD Integration
|
|
183
|
+
|
|
184
|
+
Use exit codes to fail pipelines on HIGH findings:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Fail CI if any HIGH findings are found
|
|
188
|
+
docker-slim-advisor ./Dockerfile
|
|
189
|
+
if [ $? -eq 1 ]; then
|
|
190
|
+
echo "Image optimization issues detected. Review the findings above."
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**GitHub Actions example:**
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
- name: Analyze Dockerfile
|
|
199
|
+
run: npx @infograb/docker-slim-advisor ./Dockerfile --severity HIGH
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Pipe-friendly (no color/emoji):**
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# NO_COLOR disables ANSI codes; plain text is always pipe-safe
|
|
206
|
+
NO_COLOR=1 docker-slim-advisor ./Dockerfile | tee advisor-report.txt
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**JSON in scripts:**
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
FINDINGS=$(docker-slim-advisor --format json ./Dockerfile | jq '.totalFindings')
|
|
213
|
+
echo "Found $FINDINGS optimization opportunities"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## How It Works
|
|
219
|
+
|
|
220
|
+
`docker-slim-advisor` performs **static analysis** — it reads and parses your Dockerfile as text, with no Docker daemon or image pull required.
|
|
221
|
+
|
|
222
|
+
1. **Parse** — Dockerfile is tokenized into a typed instruction AST (`FROM`, `RUN`, `COPY`, `ADD`, etc.). Multi-line `RUN` with backslash continuations are handled correctly.
|
|
223
|
+
2. **Detect** — Multi-stage builds (`multiple FROM` instructions) are detected and reported as already optimized.
|
|
224
|
+
3. **Evaluate rules** — Each of the 5 optimization rules inspects the AST and emits findings with line numbers, rule IDs, and estimated savings.
|
|
225
|
+
4. **Estimate sizes** — A bundled JSON database of 170+ image tags provides base image sizes. Layer size heuristics compute before/after predictions (±30% accuracy).
|
|
226
|
+
5. **Format output** — The result is rendered in the requested format with TTY detection and `NO_COLOR` support.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Optimization Rules
|
|
231
|
+
|
|
232
|
+
| Rule ID | Severity | Description |
|
|
233
|
+
|---------|----------|-------------|
|
|
234
|
+
| `DSA001` | HIGH | Switch to a slimmer base image (e.g. `node:18` → `node:18-slim`, `node:18-alpine`) |
|
|
235
|
+
| `DSA002` | MEDIUM | Merge multiple `RUN` instructions into one to reduce image layers |
|
|
236
|
+
| `DSA003` | MEDIUM | Clean apt/apk cache in the same `RUN` layer (`--no-install-recommends`, `rm -rf /var/lib/apt/lists/*`) |
|
|
237
|
+
| `DSA004` | HIGH | Add or improve `.dockerignore` to avoid copying unnecessary files via `COPY .` |
|
|
238
|
+
| `DSA005` | LOW | Remove unnecessary packages installed for build-time only (e.g. `curl`, `wget`, `git`) |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Supported Base Images
|
|
243
|
+
|
|
244
|
+
The bundled database covers **170+ image tags** across **53 base images**, including:
|
|
245
|
+
|
|
246
|
+
| Image | Image | Image |
|
|
247
|
+
|-------|-------|-------|
|
|
248
|
+
| `alpine` | `node` | `python` |
|
|
249
|
+
| `ubuntu` | `debian` | `golang` |
|
|
250
|
+
| `nginx` | `postgres` | `redis` |
|
|
251
|
+
| `rust` | `ruby` | `php` |
|
|
252
|
+
| `maven` | `gradle` | `eclipse-temurin` |
|
|
253
|
+
| `mongo` | `mysql` | `elasticsearch` |
|
|
254
|
+
| `gcr.io/distroless/*` | `mcr.microsoft.com/dotnet/*` | `pytorch/pytorch` |
|
|
255
|
+
|
|
256
|
+
Tag variants (e.g. `latest`, `slim`, `alpine`, `bookworm`, version-pinned) are included where available.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Contributing
|
|
261
|
+
|
|
262
|
+
1. Fork the repository and create a feature branch.
|
|
263
|
+
2. Install dependencies: `npm install`
|
|
264
|
+
3. Run tests: `npm test`
|
|
265
|
+
4. Check types: `npm run lint`
|
|
266
|
+
5. Submit a pull request with a clear description of the change.
|
|
267
|
+
|
|
268
|
+
To add a new optimization rule, create `src/rules/your-rule.ts` implementing the `Rule` interface, then register it in `src/rules/index.ts`.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base image database loader — reads static JSON bundle, provides
|
|
3
|
+
* size lookup and smaller-alternative search for the size estimator.
|
|
4
|
+
*/
|
|
5
|
+
/** A smaller alternative image found in the DB */
|
|
6
|
+
export interface AlternativeImage {
|
|
7
|
+
image: string;
|
|
8
|
+
tag: string;
|
|
9
|
+
sizeBytes: number;
|
|
10
|
+
}
|
|
11
|
+
/** Load the static image→size map from bundled JSON */
|
|
12
|
+
export declare function loadImageSizeMap(): Record<string, number>;
|
|
13
|
+
/** Lookup image size by name and tag. Returns undefined if not in DB. */
|
|
14
|
+
export declare function getImageSize(image: string, tag: string, images?: Record<string, number>): number | undefined;
|
|
15
|
+
/** Find a smaller alpine/slim alternative for the given image:tag */
|
|
16
|
+
export declare function findSmallerAlternative(image: string, tag: string, images?: Record<string, number>): AlternativeImage | undefined;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base image database loader — reads static JSON bundle, provides
|
|
3
|
+
* size lookup and smaller-alternative search for the size estimator.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
/** Suffixes to try when searching for slim alternatives */
|
|
11
|
+
const SLIM_SUFFIXES = ['-alpine', '-slim'];
|
|
12
|
+
let cachedImages = null;
|
|
13
|
+
/** Load the static image→size map from bundled JSON */
|
|
14
|
+
export function loadImageSizeMap() {
|
|
15
|
+
if (cachedImages)
|
|
16
|
+
return cachedImages;
|
|
17
|
+
const dbPath = join(__dirname, 'data', 'base-image-db.json');
|
|
18
|
+
const raw = JSON.parse(readFileSync(dbPath, 'utf-8'));
|
|
19
|
+
// Normalize: entries may be plain numbers or {compressedBytes} objects
|
|
20
|
+
const normalized = {};
|
|
21
|
+
for (const [key, value] of Object.entries(raw.images)) {
|
|
22
|
+
normalized[key] = typeof value === 'number' ? value : value.compressedBytes;
|
|
23
|
+
}
|
|
24
|
+
cachedImages = normalized;
|
|
25
|
+
return cachedImages;
|
|
26
|
+
}
|
|
27
|
+
/** Lookup image size by name and tag. Returns undefined if not in DB. */
|
|
28
|
+
export function getImageSize(image, tag, images) {
|
|
29
|
+
const db = images ?? loadImageSizeMap();
|
|
30
|
+
const key = `${image}:${tag}`;
|
|
31
|
+
if (db[key] !== undefined)
|
|
32
|
+
return db[key];
|
|
33
|
+
if (key === 'scratch:latest' || image === 'scratch')
|
|
34
|
+
return 0;
|
|
35
|
+
if (tag === 'latest' && db[image] !== undefined)
|
|
36
|
+
return db[image];
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Find a smaller alpine/slim alternative for the given image:tag */
|
|
40
|
+
export function findSmallerAlternative(image, tag, images) {
|
|
41
|
+
const db = images ?? loadImageSizeMap();
|
|
42
|
+
const currentSize = getImageSize(image, tag, db);
|
|
43
|
+
if (currentSize === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
// Already alpine/slim — no further suggestion
|
|
46
|
+
if (tag.includes('alpine') || tag.includes('slim'))
|
|
47
|
+
return undefined;
|
|
48
|
+
const versionPrefix = tag.match(/^[\d.]+/)?.[0] || '';
|
|
49
|
+
let best;
|
|
50
|
+
for (const suffix of SLIM_SUFFIXES) {
|
|
51
|
+
const candidates = [
|
|
52
|
+
`${image}:${tag}${suffix}`,
|
|
53
|
+
versionPrefix ? `${image}:${versionPrefix}${suffix}` : null,
|
|
54
|
+
`${image}:${suffix.slice(1)}`, // e.g. "alpine"
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
const size = db[candidate];
|
|
58
|
+
if (size !== undefined && size < currentSize) {
|
|
59
|
+
if (!best || size < best.sizeBytes) {
|
|
60
|
+
const colonIdx = candidate.indexOf(':');
|
|
61
|
+
best = {
|
|
62
|
+
image: candidate.slice(0, colonIdx),
|
|
63
|
+
tag: candidate.slice(colonIdx + 1),
|
|
64
|
+
sizeBytes: size,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=base-image-db-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-image-db-loader.js","sourceRoot":"","sources":["../src/base-image-db-loader.ts"],"names":[],"mappings":"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"}
|
package/build/cli.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for docker-slim-advisor.
|
|
4
|
+
*
|
|
5
|
+
* Parses command-line arguments using commander and routes
|
|
6
|
+
* to the analysis pipeline. Handles --format flag with
|
|
7
|
+
* 'terminal' (default), 'json', and 'markdown' values.
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import type { Severity, AnalysisResult } from './types.js';
|
|
11
|
+
/** Supported output format values */
|
|
12
|
+
export declare const OUTPUT_FORMATS: readonly ["terminal", "json", "markdown"];
|
|
13
|
+
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
14
|
+
/** Parsed CLI options after argument processing */
|
|
15
|
+
export interface CliOptions {
|
|
16
|
+
format: OutputFormat;
|
|
17
|
+
severity?: Severity;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate that a format string is one of the accepted values.
|
|
21
|
+
* Used as commander's argParser callback for --format flag.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseFormat(value: string): OutputFormat;
|
|
24
|
+
/**
|
|
25
|
+
* Validate that a severity string is one of the accepted values.
|
|
26
|
+
* Accepts case-insensitive input (e.g. "high", "HIGH", "High").
|
|
27
|
+
* Used as commander's argParser callback for --severity flag.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseSeverity(value: string): Severity;
|
|
30
|
+
/**
|
|
31
|
+
* Build and return the configured Commander program.
|
|
32
|
+
* Separated from execution for testability.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createProgram(): Command;
|
|
35
|
+
/**
|
|
36
|
+
* Parse argv and return structured CLI options + positional args.
|
|
37
|
+
* Exits with code 2 on invalid arguments (commander default behavior).
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseArgs(argv?: string[]): {
|
|
40
|
+
options: CliOptions;
|
|
41
|
+
dockerfilePath: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Run the full analysis pipeline: parse → detect → evaluate rules →
|
|
45
|
+
* estimate sizes → format output → determine exit code.
|
|
46
|
+
*/
|
|
47
|
+
export declare function analyze(dockerfilePath: string, options: CliOptions): AnalysisResult;
|
package/build/cli.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for docker-slim-advisor.
|
|
4
|
+
*
|
|
5
|
+
* Parses command-line arguments using commander and routes
|
|
6
|
+
* to the analysis pipeline. Handles --format flag with
|
|
7
|
+
* 'terminal' (default), 'json', and 'markdown' values.
|
|
8
|
+
*/
|
|
9
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { EXIT_CODE } from './exit-codes.js';
|
|
12
|
+
import { determineExitCode } from './exit-codes.js';
|
|
13
|
+
import { SEVERITY_LEVELS, isValidSeverity, filterBySeverity } from './severity-filter.js';
|
|
14
|
+
import { writeError, writeOutput } from './output.js';
|
|
15
|
+
import { parseDockerfile } from './parser.js';
|
|
16
|
+
import { detectMultiStage } from './multi-stage-detector.js';
|
|
17
|
+
import { rules } from './rules/index.js';
|
|
18
|
+
import { estimateBeforeSize, estimateAfterSize, calcSizeReductionPercent } from './size-estimator.js';
|
|
19
|
+
import { getImageSize } from './data/base-image-db.js';
|
|
20
|
+
import { getFormatter } from './formatters/formatter-dispatcher.js';
|
|
21
|
+
/** Supported output format values */
|
|
22
|
+
export const OUTPUT_FORMATS = ['terminal', 'json', 'markdown'];
|
|
23
|
+
/**
|
|
24
|
+
* Validate that a format string is one of the accepted values.
|
|
25
|
+
* Used as commander's argParser callback for --format flag.
|
|
26
|
+
*/
|
|
27
|
+
export function parseFormat(value) {
|
|
28
|
+
const lower = value.toLowerCase();
|
|
29
|
+
if (!OUTPUT_FORMATS.includes(lower)) {
|
|
30
|
+
throw new InvalidArgumentError(`Invalid format '${value}'. Must be one of: ${OUTPUT_FORMATS.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return lower;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate that a severity string is one of the accepted values.
|
|
36
|
+
* Accepts case-insensitive input (e.g. "high", "HIGH", "High").
|
|
37
|
+
* Used as commander's argParser callback for --severity flag.
|
|
38
|
+
*/
|
|
39
|
+
export function parseSeverity(value) {
|
|
40
|
+
if (!isValidSeverity(value)) {
|
|
41
|
+
throw new InvalidArgumentError(`Invalid severity '${value}'. Must be one of: ${SEVERITY_LEVELS.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
return value.toUpperCase();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build and return the configured Commander program.
|
|
47
|
+
* Separated from execution for testability.
|
|
48
|
+
*/
|
|
49
|
+
export function createProgram() {
|
|
50
|
+
const program = new Command();
|
|
51
|
+
program
|
|
52
|
+
.name('docker-slim-advisor')
|
|
53
|
+
.description('Statically analyze Dockerfiles to recommend image size optimizations')
|
|
54
|
+
.version('0.1.0')
|
|
55
|
+
.argument('[dockerfile]', 'Path to Dockerfile to analyze', 'Dockerfile')
|
|
56
|
+
.option('-f, --format <format>', `Output format: ${OUTPUT_FORMATS.join(', ')}`, parseFormat, 'terminal')
|
|
57
|
+
.option('-s, --severity <level>', `Minimum severity to report: ${SEVERITY_LEVELS.join(', ')}`, parseSeverity);
|
|
58
|
+
return program;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse argv and return structured CLI options + positional args.
|
|
62
|
+
* Exits with code 2 on invalid arguments (commander default behavior).
|
|
63
|
+
*/
|
|
64
|
+
export function parseArgs(argv) {
|
|
65
|
+
const program = createProgram();
|
|
66
|
+
// commander.parse() exits on --help/--version;
|
|
67
|
+
// commander.parseAsync() not needed since we have no async opts
|
|
68
|
+
program.parse(argv);
|
|
69
|
+
const opts = program.opts();
|
|
70
|
+
const dockerfilePath = program.args[0] ?? 'Dockerfile';
|
|
71
|
+
return { options: opts, dockerfilePath };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run the full analysis pipeline: parse → detect → evaluate rules →
|
|
75
|
+
* estimate sizes → format output → determine exit code.
|
|
76
|
+
*/
|
|
77
|
+
export function analyze(dockerfilePath, options) {
|
|
78
|
+
const content = readFileSync(dockerfilePath, 'utf-8');
|
|
79
|
+
const instructions = parseDockerfile(content);
|
|
80
|
+
const { isMultiStage } = detectMultiStage(instructions);
|
|
81
|
+
// Determine base image from the first FROM instruction
|
|
82
|
+
const fromInstr = instructions.find((i) => i.type === 'FROM');
|
|
83
|
+
const baseImage = fromInstr
|
|
84
|
+
? `${fromInstr.image}:${fromInstr.tag}`
|
|
85
|
+
: 'unknown';
|
|
86
|
+
// Build rule context with image size lookup
|
|
87
|
+
const ruleContext = { getImageSize };
|
|
88
|
+
// Evaluate all rules against parsed instructions
|
|
89
|
+
const recommendations = isMultiStage
|
|
90
|
+
? []
|
|
91
|
+
: rules.flatMap((rule) => rule.evaluate(instructions, ruleContext));
|
|
92
|
+
// Size estimation
|
|
93
|
+
const beforeSize = estimateBeforeSize(instructions, getImageSize);
|
|
94
|
+
// Calculate new base image size if alpine-swap is recommended
|
|
95
|
+
const alpineSwapRec = recommendations.find((r) => r.ruleId === 'DSA001');
|
|
96
|
+
const newBaseImageBytes = alpineSwapRec
|
|
97
|
+
? beforeSize.baseImageBytes - alpineSwapRec.estimatedSavingsBytes
|
|
98
|
+
: undefined;
|
|
99
|
+
const afterSize = estimateAfterSize(beforeSize, recommendations, newBaseImageBytes);
|
|
100
|
+
const sizeReductionPercent = calcSizeReductionPercent(beforeSize.totalBytes, afterSize.totalBytes);
|
|
101
|
+
return {
|
|
102
|
+
dockerfilePath,
|
|
103
|
+
isMultiStage,
|
|
104
|
+
baseImage,
|
|
105
|
+
instructions,
|
|
106
|
+
recommendations,
|
|
107
|
+
estimatedBeforeSizeBytes: beforeSize.totalBytes,
|
|
108
|
+
estimatedAfterSizeBytes: afterSize.totalBytes,
|
|
109
|
+
sizeReductionPercent,
|
|
110
|
+
schemaVersion: 1,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/* Main execution - only runs when invoked directly, not when imported */
|
|
114
|
+
async function main() {
|
|
115
|
+
try {
|
|
116
|
+
const { options, dockerfilePath } = parseArgs();
|
|
117
|
+
// Run analysis pipeline
|
|
118
|
+
const result = analyze(dockerfilePath, options);
|
|
119
|
+
// Filter recommendations by severity threshold
|
|
120
|
+
const filtered = options.severity
|
|
121
|
+
? filterBySeverity(result.recommendations, options.severity)
|
|
122
|
+
: result.recommendations;
|
|
123
|
+
// Dispatch to the correct formatter based on --format value
|
|
124
|
+
const formatter = getFormatter(options.format);
|
|
125
|
+
const output = formatter(result, filtered);
|
|
126
|
+
writeOutput(output);
|
|
127
|
+
// Set exit code based on findings
|
|
128
|
+
process.exitCode = determineExitCode(result.recommendations, options.severity);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
132
|
+
process.exitCode = EXIT_CODE.ERROR;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ESM doesn't have require.main === module; detect via argv
|
|
136
|
+
const isDirectRun = process.argv[1] &&
|
|
137
|
+
(process.argv[1].endsWith('/cli.js') ||
|
|
138
|
+
process.argv[1].endsWith('/cli.ts'));
|
|
139
|
+
if (isDirectRun) {
|
|
140
|
+
main();
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=cli.js.map
|
package/build/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|