|
| 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