Skip to content

Commit 2942d7b

Browse files
authored
Merge pull request #2717 from srod/develop
feat: Add image compression support and parallel file processing
2 parents 8422994 + 7bf2442 commit 2942d7b

116 files changed

Lines changed: 4919 additions & 421 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.Jules/palette.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 2025-12-30 - Sidebar Toggle Accessibility
2+
**Learning:** `aria-pressed` is for toggle buttons (like Bold/Italic), while `aria-expanded` is for disclosing content (like Menus/Sidebars). Screen readers treat them differently.
3+
**Action:** Always check if a toggle button controls a region (`aria-expanded`) or changes a state (`aria-pressed`).

.changeset/feat-image-support.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@node-minify/sharp": minor
3+
"@node-minify/svgo": minor
4+
"@node-minify/imagemin": minor
5+
"@node-minify/utils": patch
6+
"@node-minify/types": patch
7+
"@node-minify/core": patch
8+
---
9+
10+
feat: Add image compression support
11+
12+
New packages:
13+
- `@node-minify/sharp`: Convert and compress images to WebP, AVIF, PNG, JPEG using sharp
14+
- `@node-minify/svgo`: Optimize SVG files using SVGO
15+
- `@node-minify/imagemin`: Compress PNG, JPEG, GIF images using imagemin
16+
17+
Core changes:
18+
- Support for binary (Buffer) content in compressors
19+
- Multi-format output support (e.g., convert PNG to both WebP and AVIF)
20+
- New `buffer` and `outputs` fields in CompressorResult type

.github/workflows/codeql.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ jobs:
2828

2929
- name: Setup Bun
3030
uses: oven-sh/setup-bun@v2
31+
with:
32+
bun-version: "1.3.5"
3133

3234
- name: Setup Node.js
33-
uses: actions/setup-node@v4
35+
uses: actions/setup-node@v6
3436
with:
35-
node-version: '22'
37+
node-version: "22"
3638

3739
- name: Install dependencies
3840
run: bun install --frozen-lockfile

.github/workflows/publish.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,31 @@ jobs:
4747
with:
4848
publish: bun run changeset:release
4949
version: bun run changeset:version
50-
createGithubReleases: true
50+
createGithubReleases: false
5151
env:
5252
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5353
NPM_CONFIG_PROVENANCE: true
54+
55+
- name: 📦 Create Aggregated GitHub Release
56+
if: steps.changesets.outputs.published == 'true'
57+
run: |
58+
VERSION=$(jq -r '.version' packages/core/package.json)
59+
PACKAGES='${{ steps.changesets.outputs.publishedPackages }}'
60+
61+
# Format package list as markdown
62+
PACKAGE_LIST=$(echo "$PACKAGES" | jq -r '.[] | "- `\(.name)@\(.version)`"')
63+
64+
# Create release notes
65+
cat > /tmp/release-notes.md << 'EOF'
66+
## Published Packages
67+
68+
EOF
69+
echo "$PACKAGE_LIST" >> /tmp/release-notes.md
70+
echo "" >> /tmp/release-notes.md
71+
echo "See individual package CHANGELOGs for details." >> /tmp/release-notes.md
72+
73+
gh release create "v${VERSION}" \
74+
--title "v${VERSION}" \
75+
--notes-file /tmp/release-notes.md
76+
env:
77+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/test.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
name: Test
22

3-
on: [push]
3+
permissions:
4+
contents: read
5+
6+
on:
7+
push:
8+
branches: [main, develop]
9+
pull_request:
10+
11+
concurrency:
12+
group: test-${{ github.ref }}
13+
cancel-in-progress: true
414

515
jobs:
616
test:
@@ -15,20 +25,22 @@ jobs:
1525

1626
- name: Setup Bun
1727
uses: oven-sh/setup-bun@v2
28+
with:
29+
bun-version: "1.3.5"
1830

1931
- name: Set up JDK 11
2032
uses: actions/setup-java@v5
2133
with:
2234
java-version: '11'
23-
distribution: 'adopt'
35+
distribution: 'temurin'
2436

2537
- name: Use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
2638
uses: actions/setup-node@v6
2739
with:
2840
node-version: ${{ matrix.node-version }}
2941

3042
- name: Install dependencies
31-
run: bun install
43+
run: bun install --frozen-lockfile
3244

3345
- name: Run tests
3446
run: bun run ci

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ tests/tmp
1414
*.tsbuildinfo
1515
examples/public/**/*-dist
1616
examples/public/**/*.min.html
17+
packages/utils/__tests__/temp_perf/

.jules/bolt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Bolt's Journal

AGENTS.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to AI coding assistants when working with code in this repository.
4+
5+
## Requirements
6+
- **Node.js**: >=20.0.0
7+
- **Bun**: 1.3.5+
8+
9+
## Package Manager
10+
11+
This branch (`main`) uses **Bun** as the package manager and runtime.
12+
13+
## Commands
14+
- **Install**: `bun install`
15+
- **Build**: `bun run build`
16+
- **Lint**: `bun run lint` | **Format**: `bun run format`
17+
- **Test all**: `bun run test`
18+
- **Test single package**: `bun run test packages/<name>` (e.g., `bun run test packages/core`)
19+
- **Test single file**: `bun run test packages/core/__tests__/core.test.ts`
20+
- **Typecheck**: `bun run typecheck`
21+
- **CI (full)**: `bun run ci`
22+
23+
## Code Style (Biome)
24+
- **Indent**: 4 spaces | **Quotes**: double | **Semicolons**: always | **Trailing commas**: ES5
25+
- **Imports**: Use `type` keyword for type-only imports. Use `node:` prefix for Node.js built-ins (e.g., `node:child_process`)
26+
- **File extensions**: Include `.ts` in local imports (e.g., `./setup.ts`)
27+
- **Naming**: camelCase for functions/variables, PascalCase for types/interfaces
28+
- **Tests**: Vitest with `describe`/`test`/`expect`. Files in `packages/*/__tests__/*.test.ts`
29+
- **Error handling**: Use `try/catch` with `if (err instanceof Error)` checks
30+
- **Source files**: Include copyright header: `/*! node-minify ... MIT Licensed */`
31+
32+
### Linter Rules (from biome.json)
33+
- `noForEach`: off (forEach allowed)
34+
- `noParameterAssign`: off (parameter reassignment allowed)
35+
- `noExplicitAny`: off (but prefer proper types when possible)
36+
- Organize imports automatically on save
37+
38+
## Architecture
39+
40+
This is a Bun monorepo for compressing JavaScript, CSS, and HTML files using various backends.
41+
42+
### Package Structure
43+
44+
**Core packages** (in `/packages`):
45+
- `core` - Main `minify()` function, orchestrates compression
46+
- `utils` - Shared utilities (file operations, gzip sizing)
47+
- `run` - Command execution wrapper for external tools
48+
- `types` - TypeScript type definitions (not compiled)
49+
- `cli` - Command-line interface
50+
51+
**Compressor packages** - Each wraps a specific minification library:
52+
- JS: `esbuild`, `google-closure-compiler`, `oxc`, `swc`, `terser`, `uglify-js`
53+
- CSS: `clean-css`, `cssnano`, `csso`, `esbuild`, `lightningcss`
54+
- HTML: `html-minifier`
55+
- Other: `jsonminify`, `no-compress` (passthrough)
56+
57+
**Deprecated** (still available but unmaintained upstream):
58+
- `babel-minify` - Babel 6 only, use `terser` instead
59+
- `uglify-es` - Unmaintained, use `terser` instead
60+
- `yui` - Java-based, use modern alternatives instead
61+
- `crass` - Unmaintained, use `lightningcss` or `clean-css` instead
62+
- `sqwish` - Unmaintained, use `lightningcss` or `clean-css` instead
63+
64+
### Dependencies
65+
66+
`core` depends on `utils` and `run`. All compressor packages depend on `core`. The build command (`bun run build`) builds `utils` and `run` first, then all other packages.
67+
68+
### Package Pattern
69+
70+
All packages follow the same structure:
71+
```text
72+
packages/<name>/
73+
├── src/index.ts # Main export
74+
├── __tests__/ # Vitest tests
75+
├── package.json
76+
└── tsconfig.json
77+
```
78+
79+
Build: `tsdown` (configured via `tsdown.config.ts`)
80+
81+
## Changesets
82+
83+
This project uses [Changesets](https://github.com/changesets/changesets) for versioning.
84+
- **Add changeset**: `bun run changeset` (interactive prompt)
85+
- **Version packages**: `bun run changeset:version`
86+
- Required for any user-facing changes (features, fixes, breaking changes)
87+
88+
## Adding a New Compressor
89+
90+
1. Create `packages/<name>/` with standard structure
91+
2. Export async function matching `Compressor` type from `@node-minify/types`
92+
3. Function receives `{ settings, content }`, returns `{ code, map? }`
93+
4. Add tests using shared helpers from `tests/fixtures.ts`
94+
95+
Example compressor signature:
96+
```ts
97+
import type { CompressorResult, MinifierOptions } from "@node-minify/types";
98+
99+
export async function myCompressor({
100+
settings,
101+
content,
102+
}: MinifierOptions): Promise<CompressorResult> {
103+
// ... minification logic
104+
return { code: minifiedCode, map: sourceMap };
105+
}
106+
```
107+
108+
## Testing
109+
110+
Tests use shared fixtures from `tests/fixtures.ts`:
111+
- `runOneTest({ options, compressorLabel, compressor })` - Run a single test case
112+
- `tests` object contains test suites: `commonjs`, `commoncss`, `commonhtml`, `uglifyjs`, etc.
113+
114+
Types are in `packages/types/src/types.d.ts` (not `index.ts`).
115+
116+
## Key Types
117+
118+
From `@node-minify/types`:
119+
- `Settings` - User-facing options for `minify()` function
120+
- `MinifierOptions` - What compressors receive (`{ settings, content }`)
121+
- `CompressorResult` - What compressors return (`{ code, map? }`)
122+
- `Compressor` - Function type: `(args: MinifierOptions) => Promise<CompressorResult>`
123+
124+
## Common Patterns
125+
126+
### File Operations (from @node-minify/utils)
127+
```ts
128+
import { readFile, writeFile, getFilesizeInBytes } from "@node-minify/utils";
129+
130+
const content = await readFile("src/app.js");
131+
await writeFile({ file: "dist/app.min.js", content, index: 0 });
132+
const size = await getFilesizeInBytes("dist/app.min.js");
133+
```
134+
135+
### Deprecation Warnings
136+
```ts
137+
import { warnDeprecation } from "@node-minify/utils";
138+
139+
warnDeprecation("@node-minify/old-package", "Use @node-minify/new-package instead");
140+
```
141+
142+
### Async / Parallel Patterns
143+
144+
The codebase uses async functions for file operations and parallel compression.
145+
146+
#### Async File Operations
147+
```ts
148+
import {
149+
getContentFromFilesAsync,
150+
isValidFileAsync,
151+
} from "@node-minify/utils";
152+
153+
// Async file reading (preferred for non-blocking IO)
154+
const content = await getContentFromFilesAsync("src/app.js");
155+
const contents = await getContentFromFilesAsync(["src/a.js", "src/b.js"]);
156+
157+
// Async file validation
158+
if (await isValidFileAsync("src/app.js")) {
159+
// file exists and is readable
160+
}
161+
```
162+
163+
#### Parallel Compression (Array Inputs)
164+
165+
The core `minify()` function automatically processes array inputs in parallel:
166+
```ts
167+
// Core handles parallelization internally via Promise.all
168+
await minify({
169+
compressor: terser,
170+
input: ["src/a.js", "src/b.js", "src/c.js"],
171+
output: ["dist/a.min.js", "dist/b.min.js", "dist/c.min.js"],
172+
});
173+
```
174+
175+
#### Custom Parallel Processing
176+
177+
For custom parallel operations:
178+
```ts
179+
// Basic parallel execution
180+
const results = await Promise.all(
181+
files.map(async (file, index) => {
182+
const content = await getContentFromFilesAsync(file);
183+
return run({ settings, content, index });
184+
})
185+
);
186+
187+
// With error handling per item (use allSettled)
188+
const results = await Promise.allSettled(
189+
files.map(file => compressSingleFile({ ...settings, input: file }))
190+
);
191+
// Map results back to inputs
192+
results.forEach((result, i) => {
193+
if (result.status === "fulfilled") {
194+
console.log(`${files[i]}: success`);
195+
} else {
196+
console.error(`${files[i]}: ${result.reason}`);
197+
}
198+
});
199+
```
200+
201+
#### Concurrency Limits
202+
203+
For large arrays, limit concurrent operations to avoid resource exhaustion:
204+
```ts
205+
// Using p-limit (install: bun add p-limit)
206+
import pLimit from "p-limit";
207+
208+
const limit = pLimit(5); // max 5 concurrent
209+
const results = await Promise.all(
210+
files.map(file => limit(() => compressSingleFile({ ...settings, input: file })))
211+
);
212+
213+
// Or batch processing
214+
async function processBatches<T>(items: T[], batchSize: number, fn: (item: T) => Promise<unknown>) {
215+
for (let i = 0; i < items.length; i += batchSize) {
216+
await Promise.all(items.slice(i, i + batchSize).map(fn));
217+
}
218+
}
219+
```
220+
221+
**Guidelines:**
222+
- Use `Promise.all` when all operations must succeed (fail-fast)
223+
- Use `Promise.allSettled` when you need results for all items regardless of individual failures
224+
- Limit concurrency to 5-10 for file operations, 2-4 for CPU-intensive compressions
225+
- Always maintain 1:1 mapping between inputs and outputs for traceability
226+
227+
## Troubleshooting
228+
229+
- **Build fails**: Run `bun run build:deps` first to build `utils` and `run`
230+
- **Type errors**: Ensure `bun run build` completed; types come from compiled `dist/`
231+
- **Test isolation**: Tests use `tests/tmp/` for output files (gitignored)
232+
- **Clean rebuild**: `bun run clean && bun run build`

0 commit comments

Comments
 (0)