vanilla-builder 0.1.0 → 0.3.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/README.md CHANGED
@@ -1,163 +1,607 @@
1
1
  # vanilla-builder
2
2
 
3
- [![CI](https://github.com/MarioACo/vanilla-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/MarioACo/vanilla-builder/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/vanilla-builder)](https://www.npmjs.com/package/vanilla-builder) [![MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
3
+ [![CI](https://github.com/MarioACo/vanilla-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/MarioACo/vanilla-builder/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/vanilla-builder)](https://www.npmjs.com/package/vanilla-builder) [![Node](https://img.shields.io/badge/node-%3E%3D16-brightgreen)](#requirements) [![MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
4
4
 
5
5
  **English** · [Español](README.es.md)
6
6
 
7
- Bundle, minify and obfuscate **vanilla** web projects (HTML, CSS and JavaScript, no framework) **without losing anything**.
8
- It follows the real dependencies of every page, verifies the output before publishing it and never deletes what it did not create.
7
+ **A production build for plain HTML, CSS and JavaScript sites — no framework, no config, nothing lost.**
9
8
 
10
- - **Zero config**: point it at a folder with `.html` files and you are done.
11
- - **Lossless**: every project file ends up with a known fate (processed, bundled, copied or deliberately skipped), and the output is verified before it replaces `dist/`.
12
- - **Lightweight**: 57 KB of its own code; the base install is 8 packages (most of it is the esbuild binary). The obfuscator and terser are only installed if you use them.
13
- - **Node 16 or newer** (tested on 16, 18, 20, 22 and 24).
9
+ Point it at a folder with `.html` files. It finds every script, stylesheet, image and page they use, bundles and minifies them, can obfuscate your JavaScript (while leaving third-party libraries alone), checks the result, and only then writes `dist/`.
14
10
 
15
- ## Install
11
+ ```text
12
+ my-site/ my-site/dist/
13
+ ├── index.html ├── index.html ← minified, paths updated
14
+ ├── about.html ├── about.html
15
+ ├── css/main.css (+ @imports) ├── css/main.css ← one file, minified
16
+ ├── js/app.js (+ imports) → ├── js/app.js ← bundled, minified, optionally obfuscated
17
+ ├── js/vendor/jquery.min.js ├── js/vendor/jquery.min.js ← detected as a library, never obfuscated
18
+ ├── img/… ├── img/… ← copied as is
19
+ └── .env └── (not published: secrets are excluded)
20
+ ```
21
+
22
+ ## Contents
23
+
24
+ - [Why vanilla-builder](#why-vanilla-builder)
25
+ - [How it compares](#how-it-compares)
26
+ - [Requirements](#requirements)
27
+ - [Quick start](#quick-start)
28
+ - [Common tasks](#common-tasks)
29
+ - [How it works](#how-it-works)
30
+ - [Third-party libraries and obfuscation](#third-party-libraries-and-obfuscation)
31
+ - [TypeScript](#typescript)
32
+ - [Configuration](#configuration)
33
+ - [Plugins](#plugins)
34
+ - [GitHub Action](#github-action)
35
+ - [Command-line reference](#command-line-reference)
36
+ - [Safety guarantees](#safety-guarantees)
37
+ - [Performance](#performance)
38
+ - [JavaScript API](#javascript-api)
39
+ - [Troubleshooting](#troubleshooting)
40
+ - [Known limitations](#known-limitations)
41
+ - [Contributing](#contributing)
42
+
43
+ ## Why vanilla-builder
44
+
45
+ Tools like Vite or webpack assume an app with an entry point and a module graph. Many real sites are not like that: several HTML pages, classic `<script>` tags that share globals, a copied `jquery.min.js`, a service worker with a list of files. vanilla-builder is made for those sites.
46
+
47
+ - **Zero configuration.** Every complete `.html` file is an entry point. Nothing to declare.
48
+ - **Nothing is lost.** Every file in the project ends with a known fate (processed, bundled, copied or deliberately excluded) and you can see which one. Files it cannot trace are still published.
49
+ - **Verified output.** The build is written to a temporary folder, checked (every reference resolves, every JS file parses, every source file is accounted for) and only then swapped into `dist/`. A failed build never leaves you with a broken `dist/`.
50
+ - **Safe for classic scripts.** It only merges or minifies scripts when it can prove the global variables stay the same.
51
+ - **Obfuscation that respects libraries.** Your code is protected; jQuery, Bootstrap and npm packages are detected and left as they are. You can correct the detection in one line.
52
+ - **TypeScript without setup.** `<script src="app.ts">` works; the output is `.js`.
53
+ - **Fast.** Parallel threads and a persistent cache: rebuilding an obfuscated 40-page site takes under 2 seconds ([numbers](#performance)).
54
+ - **Production extras built in.** Target browsers from `browserslist`, optional image optimization, size budgets that fail CI, plugins, and a GitHub Action with a size report on every pull request.
55
+
56
+ ## How it compares
57
+
58
+ The same test sites built with each tool's default settings for a multi-page site, then opened in Chromium next to the original ([full results and method](bench/compare/RESULTS.md), reproducible with `cd bench/compare && npm install && node run.mjs`):
59
+
60
+ | Test site | vanilla-builder | Parcel 2.16 | Vite 8.3 |
61
+ | --- | --- | --- | --- |
62
+ | Classic scripts sharing globals, a worker, a copied library | ✅ identical | ❌ build fails (worker created from a string) | ❌ 404s: classic scripts are not published |
63
+ | ES modules, import map, service worker, CSS module script | ✅ identical | ❌ build fails (service worker from a string) | ❌ build fails (CSS import attribute) |
64
+ | npm packages from modules and classic scripts | ✅ identical | ✅ identical | ❌ 404s: classic scripts are not published |
65
+
66
+ Build time and size on a generated 40-page site (cold, no cache), counting only outputs whose pages work:
67
+
68
+ | | Time | Output (gzip) |
69
+ | --- | ---: | ---: |
70
+ | vanilla-builder, publishing only referenced files (`"unreferenced": "skip"`) | **1.2 s** | **64.8 KB** |
71
+ | vanilla-builder, default (also publishes unreferenced files) | 1.2 s | 156.6 KB |
72
+ | Parcel | 2.1–2.9 s | 102.6 KB |
73
+ | Vite | 0.3 s | — (output broken: 12 problems on 3 sample pages, such as 404s for classic scripts) |
74
+
75
+ These tools aim at different things: Vite and Parcel shine for apps with an entry point and a module graph. vanilla-builder is for sites that are a folder of pages and must keep working exactly as before.
76
+
77
+ ## Requirements
78
+
79
+ - **Node.js 16 or newer.** Tested on Node 16, 18, 20, 22 and 24, on Linux, Windows and macOS.
80
+ - Optional, only if you use them: [`javascript-obfuscator`](https://www.npmjs.com/package/javascript-obfuscator) for `--obfuscate`, [`terser`](https://www.npmjs.com/package/terser) for `--minifier terser`, [`browserslist`](https://www.npmjs.com/package/browserslist) for target browsers and [`sharp`](https://www.npmjs.com/package/sharp) (Node 20+) for `--images`.
81
+
82
+ On Node 16 and 17, npm prints an `EBADENGINE` warning because esbuild declares Node 18+. It is only a warning; the full test suite passes on Node 16.
83
+
84
+ ## Quick start
85
+
86
+ **1. Install it in your project:**
16
87
 
17
88
  ```bash
18
89
  npm install --save-dev vanilla-builder
19
- npm install --save-dev javascript-obfuscator # only if you obfuscate
20
- npm install --save-dev terser # only if you want maximum compression
21
90
  ```
22
91
 
23
- > On Node 16 and 17 npm prints an `EBADENGINE` warning because esbuild declares Node 18+. It is only a warning:
24
- > the whole test suite (browser tests included) passes when building with Node 16.
92
+ **2. Build:**
93
+
94
+ ```bash
95
+ npx builder build
96
+ ```
97
+
98
+ The result is in `dist/`. The terminal shows a table with each output file and its size before, after, gzipped and brotli-compressed, then confirms the verification:
25
99
 
26
- ## Usage
100
+ ```text
101
+ ✔ 3 html · 8 js · 1 css · 1 json · 4 svg · 1 text (0.21 s)
102
+ ✔ Verification: 18 files written, 17 references checked, 8 valid JS, 0 errors
103
+ ✔ Coverage: 21/21 source files with a known destination
104
+ ```
105
+
106
+ **3. Try the result in the browser:**
27
107
 
28
108
  ```bash
29
- npx builder build my-site # builds into my-site/dist
30
- npx builder build my-site --obfuscate # also obfuscates your JavaScript (medium level)
31
- npx builder analyze my-site # shows what will happen to each file, writes nothing
32
- npx builder init my-site # creates builder.config.json from what it detected
33
- npx builder dev my-site # builds, watches and serves dist/ with live reload
34
- npx builder serve my-site # serves dist/ to try it out
109
+ npx builder serve # http://127.0.0.1:5173
35
110
  ```
36
111
 
37
- Common `build` options (all of them have a config equivalent):
112
+ Optionally, add scripts to your `package.json`:
38
113
 
39
- | Option | What it does |
40
- | --- | --- |
41
- | `--obfuscate [low\|medium\|high\|max]` | Obfuscates your JS. Third-party libraries are detected and left untouched. |
42
- | `--dep <pattern>` / `--not-dep <pattern>` | Manually mark a dependency / correct a detection. |
43
- | `--obfuscate-deps` | Obfuscate dependencies too. |
44
- | `--minifier terser` | Second compression pass with terser (even smaller JS). |
45
- | `--compress [gzip\|brotli]` | Also emits `.gz` and `.br` files for servers that serve precompressed assets. |
46
- | `--hash` | Hashed JS/CSS file names for long-term caching. |
47
- | `--sourcemap [linked\|hidden\|inline]` | Source maps. |
48
- | `--target es2018` | Transpile to a specific level (default: whatever your own code already uses). |
49
- | `--drop-console` | Removes `console.*` and `debugger`. |
50
- | `--entry`, `--include`, `--ignore` | Explicit entries, forced files and exclusions (repeatable). |
51
- | `--json` | JSON output for CI (exit code 1 if anything fails). |
114
+ ```json
115
+ {
116
+ "scripts": {
117
+ "dev": "builder dev",
118
+ "build": "builder build"
119
+ }
120
+ }
121
+ ```
122
+
123
+ > Commands accept a folder: `npx builder build my-site` builds `my-site/` into `my-site/dist/`. Without one, the current folder is used.
124
+ >
125
+ > Messages are in English, or in Spanish when your system is (`--lang es|en` or `BUILDER_LANG` to choose). `--json` field names are always English.
126
+
127
+ Prefer to start from something that works? See the runnable [examples](examples/): a classic multi-page site, a TypeScript app and an obfuscated app with size budgets.
128
+
129
+ ## Common tasks
130
+
131
+ ### Develop with live reload
132
+
133
+ ```bash
134
+ npx builder dev # builds, watches for changes, serves dist/ and reloads the browser
135
+ npx builder dev --port 3000 --open
136
+ ```
137
+
138
+ When only CSS changes, stylesheets are swapped in place without reloading the page, so you keep its state.
139
+
140
+ ### Publish a static site (Netlify, GitHub Pages, S3, nginx…)
141
+
142
+ ```bash
143
+ npx builder build --hash --compress
144
+ ```
145
+
146
+ - `--hash` renames JS and CSS to `name-<hash>.js` so you can cache them forever; HTML references are updated.
147
+ - `--compress` also writes `.gz` and `.br` next to each file for servers that serve precompressed files (nginx `gzip_static`, `brotli_static`).
148
+
149
+ If the site is served from a subfolder (`https://example.com/shop/`), add `--base /shop/` so absolute paths (`/js/app.js`) resolve correctly.
150
+
151
+ ### Protect your JavaScript
152
+
153
+ ```bash
154
+ npm install --save-dev javascript-obfuscator
155
+ npx builder build --obfuscate # medium level (recommended)
156
+ npx builder build --obfuscate high
157
+ ```
158
+
159
+ Third-party libraries are detected and not obfuscated. Check what was detected with `npx builder deps` — see [Third-party libraries](#third-party-libraries-and-obfuscation).
160
+
161
+ ### Make JavaScript as small as possible
162
+
163
+ ```bash
164
+ npm install --save-dev terser
165
+ npx builder build --minifier terser --drop-console
166
+ ```
167
+
168
+ ### Support older browsers
169
+
170
+ Declare your browsers the standard way and Builder uses them for CSS and JavaScript: missing vendor prefixes are added (`-webkit-backdrop-filter`), nested CSS is flattened and newer JavaScript syntax is rewritten.
171
+
172
+ ```json
173
+ // package.json
174
+ { "browserslist": ["> 0.5%", "last 2 versions", "not dead"] }
175
+ ```
176
+
177
+ ```bash
178
+ npm install --save-dev browserslist
179
+ ```
180
+
181
+ A `.browserslistrc` file works too, and so does `--browsers "<query>"`. Without browsers, your code keeps its own syntax level.
182
+
183
+ ### Optimize images
184
+
185
+ ```bash
186
+ npm install --save-dev sharp
187
+ npx builder build --images # PNG lossless, JPEG/WebP at quality 80
188
+ npx builder build --image-quality 70
189
+ ```
190
+
191
+ PNG files are recompressed without loss and checked pixel by pixel. JPEG and WebP are re-encoded at the chosen quality (this is lossy, visually equivalent at the default), keeping EXIF orientation and color profiles. File names stay the same, and an image is only replaced when the result is smaller.
192
+
193
+ ### Keep sizes under control
194
+
195
+ ```json
196
+ {
197
+ "budgets": {
198
+ "js/**/*.js": "50 KB",
199
+ "css/app.css": "20 KB gzip",
200
+ "total": "1 MB brotli"
201
+ }
202
+ }
203
+ ```
204
+
205
+ If any file (or the total) goes over its budget, the build fails before touching `dist/` and lists what is over. In CI you can also pass `--budget "total=1MB gzip"`.
206
+
207
+ ### See what will happen before building
208
+
209
+ ```bash
210
+ npx builder analyze
211
+ ```
212
+
213
+ Lists every file with what will happen to it, the detected libraries and workers, broken references that already exist in your source, and excluded files. Nothing is written.
214
+
215
+ ### Use it in CI
216
+
217
+ ```bash
218
+ npx builder build --json > build-report.json
219
+ ```
52
220
 
53
- Run `npx builder help` for the full list.
221
+ - Exit code `0` on success and non-zero if the build or the verification fails; in that case `dist/` is left untouched and the JSON has `"ok": false` with the error.
222
+ - The JSON includes outputs, sizes, warnings, detected dependencies and time per phase (`perf.phases`).
223
+ - To reuse the cache between runs, set `BUILDER_CACHE_DIR` to a folder your CI caches.
224
+ - On GitHub, the [GitHub Action](#github-action) does all of this and adds a size report to each run and pull request.
54
225
 
55
- ## What it detects on its own
226
+ ## How it works
56
227
 
57
- Starting from each `.html` file, it follows:
228
+ 1. **Discover.** Every complete HTML page is parsed. From it, Builder follows scripts (classic and modules, inline and import maps), stylesheets (`@import`, `url()`, `image-set()`), images and `srcset`, media, icons, manifests, `og:image`, iframes, links to other pages, dynamic `import()`, `new URL('./x', import.meta.url)`, workers, service workers, `importScripts()` and paths written as strings in your code (such as a precache list).
229
+ 2. **Process.**
230
+ - ES modules are bundled with [esbuild](https://esbuild.github.io/) (code splitting included). The syntax level of your own code is kept: nothing is transpiled unless you ask with `--target`.
231
+ - Consecutive classic scripts are merged only when it is safe, then minified.
232
+ - CSS is bundled and minified; HTML is minified with a conservative minifier that keeps `<pre>`, `<textarea>` and templates intact.
233
+ - JSON loses only whitespace. Everything else (images, fonts, data) is copied.
234
+ 3. **Protect (optional).** Your JavaScript is obfuscated; libraries are not.
235
+ 4. **Verify.** In a temporary folder: every reference resolves, every JS file parses, sizes match, every source file has a destination.
236
+ 5. **Publish.** The temporary folder replaces `dist/`.
58
237
 
59
- - Classic and `type="module"` `<script src>`, inline scripts and **import maps**.
60
- - `<link rel="stylesheet">`, CSS `@import` and `url()` (including `image-set()`), `<style>` and `style` attributes.
61
- - Images, `srcset`, video, audio, icons, manifest, `og:image`, `<iframe>`, `<object>`, SVG `<use>`…
62
- - Links to other pages, dynamic `import()`, `new URL('./x', import.meta.url)`.
63
- - Workers, service workers and `importScripts()`.
64
- - Paths written as strings in JS (for example a service worker precache list): those files are also published at their original path.
238
+ Files nothing references are still minified and published at their original path (`unreferenced: "process"`), so a `fetch(variable)` or a server template that loads a file keeps working.
65
239
 
66
- What cannot be followed (a `fetch(variable)`, a server template…) **is not lost**: the folder structure is preserved and everything unreferenced is still published (`unreferenced: "process"`).
240
+ ## Third-party libraries and obfuscation
67
241
 
68
- ## Guarantees
242
+ Obfuscating jQuery, Bootstrap or an npm package protects nothing of yours, makes it slower and may break its license. When obfuscating, these are treated as libraries and left untouched:
69
243
 
70
- | Typical bundler risk | How vanilla-builder avoids it |
244
+ | Signal | Example |
71
245
  | --- | --- |
72
- | Concatenating classic scripts breaks globals | Only merges consecutive scripts with the same `defer`/strict mode, no `document.currentScript`, no `let/const` redeclarations, and a safe `;` separator. |
73
- | Minification creates new globals or renames yours | Compares globals before and after; if they differ it falls back to a more conservative minification or keeps the original. |
74
- | The obfuscator breaks exports or `with { type }` attributes | Checks exports, imports and attributes after obfuscating; if they do not match, that file is not obfuscated. |
75
- | esbuild silently drops `with { type: 'css' }` | Import attributes are forcibly preserved. |
76
- | HTML minification changes the text | Own minifier on top of parse5 plus a structural check (elements, attributes and text). `<pre>`, `<textarea>` and templates stay intact; if the CSS uses `white-space: pre`, whitespace is not collapsed. |
77
- | Stale `integrity` (SRI) blocks scripts | Recomputed whenever the content changes. |
78
- | Legacy encodings or BOM | Bytes are preserved (HTML) or JS/CSS is emitted with ASCII escapes. |
79
- | Reformatted JSON loses precision | Only whitespace outside strings is removed: `2.0` and huge numbers stay the same. |
80
- | Wiping the wrong `dist/` | Never empties a folder it did not create (marked with `.builder-manifest.json`), nor the project folder, nor the root. |
81
- | Half-finished build | Writes into a temp folder, verifies it, and only then replaces `dist/`. |
82
- | Publishing secrets | `.env*`, `.pem/.key` keys, `node_modules`, `package.json` and tool configs are excluded by default. |
246
+ | Lives in a library folder | `vendor/`, `node_modules/`, `third-party/`, `external/`… |
247
+ | Well-known library name | `jquery-3.7.1.min.js`, `bootstrap.bundle.js`, `chart.umd.js` |
248
+ | Already compiled | `.min.js`, `.umd.js`, or already-minified code |
249
+ | Named like a package you depend on | `js/swiper.js` when `package.json` lists `swiper` in `dependencies` |
250
+ | Library wrapper | UMD/AMD (`typeof define === "function" && define.amd`) |
251
+ | License header with a version | `/*! Foo v1.2.0 */` |
252
+
253
+ npm packages imported by your modules (`import confetti from 'canvas-confetti'`) are bundled from `node_modules`. When obfuscating, they are split into separate files in `assets/vendor/`, left unobfuscated and shared between pages.
254
+
255
+ ### Check and correct the detection
256
+
257
+ ```bash
258
+ npx builder deps
259
+ ```
260
+
261
+ ```text
262
+ Third-party libraries (not obfuscated) (3)
263
+ • js/vendor/jquery.min.js — in vendor/
264
+ • js/chart.js — named like the "chart.js" package in your package.json
265
+ • js/slider.js — UMD/AMD wrapper (published library format)
266
+
267
+ Your code (obfuscated with --obfuscate) (1)
268
+ • js/app.js
269
+ ```
270
+
271
+ Add `--json` for `{ "dependencies": [{ "rel", "reason" }], "own": [] }`.
272
+
273
+ If something is classified wrong, fix it in whichever place suits you:
274
+
275
+ #### In the HTML
276
+
277
+ ```html
278
+ <script src="js/my-lib.js" data-builder="dependency"></script> <!-- a library: don't obfuscate -->
279
+ <script src="js/app.min.js" data-builder="own"></script> <!-- my code: obfuscate -->
280
+ ```
83
281
 
84
- The test suite opens every example site **original and built in Chromium** and requires identical results, with no errors and no 404s, across 5 variants (default, high obfuscation + hash, max obfuscation, terser + precompression, unminified).
282
+ #### At the top of the file
85
283
 
86
- ## Third-party dependencies and obfuscation
284
+ ```js
285
+ /* @builder-dependency */ // or /* @builder-own */
286
+ ```
87
287
 
88
- Obfuscating jQuery, Bootstrap or an npm package protects nothing of yours, makes them slower and may violate their license. So when obfuscating:
288
+ #### From the terminal
89
289
 
90
- - Files in `vendor/`, `node_modules/`, `third-party/`…, well-known library names (`jquery-3.7.1.min.js`, `bootstrap.bundle.js`…), `.min.js`/`.umd.js` files, already-minified code and license headers with a version (`/*! Foo v1.2.0 */`) are detected as dependencies.
91
- - npm packages imported by your module code are split into `assets/vendor/` (not obfuscated, with their shared state intact).
92
- - `builder analyze` lists what it detected and why.
290
+ Writes `builder.config.json` for you and keeps your other settings:
291
+
292
+ ```bash
293
+ npx builder deps add "js/libs/**" # these are libraries
294
+ npx builder deps own js/app.min.js # this is my code
295
+ npx builder deps reset js/app.min.js # back to automatic detection
296
+ ```
93
297
 
94
- To correct it by hand:
298
+ #### In the configuration
95
299
 
96
300
  ```json
97
301
  {
98
- "obfuscate": "high",
99
302
  "dependencies": {
100
303
  "include": ["js/my-library.js"],
101
- "exclude": ["js/app.min.js"],
102
- "obfuscate": false
304
+ "exclude": ["js/app.min.js"]
103
305
  }
104
306
  }
105
307
  ```
106
308
 
107
- `include` marks dependencies that were not detected; `exclude` says a file is yours and should be obfuscated; `obfuscate: true` obfuscates everything.
309
+ Priority: configuration › marks in the HTML or file › automatic detection. To obfuscate everything, libraries included, use `--obfuscate-deps` (`"dependencies": { "obfuscate": true }`).
310
+
311
+ ### Obfuscation levels
312
+
313
+ | Level | Use it for |
314
+ | --- | --- |
315
+ | `low` | Renaming and string hiding. Almost no runtime cost. |
316
+ | `medium` | **Recommended.** Adds control-flow flattening, dead code and encoded strings. |
317
+ | `high` | Stronger transformations; larger and slower code. |
318
+ | `max` | Everything, plus protection against reformatting. Heaviest. |
319
+
320
+ Every obfuscated file is checked: the global variables of classic scripts and the imports/exports of modules must stay the same, or that file is published unobfuscated with a warning. Options that change behavior (`debugProtection`, `disableConsoleOutput`, `renameGlobals`) are never enabled automatically; pass them explicitly: `"obfuscate": { "preset": "high", "debugProtection": true }`. License comments (`/*! … */`, `@license`) are preserved.
108
321
 
109
- Levels: `low` (almost free), `medium` (recommended), `high` and `max` (also defends against being reformatted or tampered with; heavier and slower). Options that change behavior (`debugProtection`, `disableConsoleOutput`, `renameGlobals`) are never turned on automatically; pass them with an object: `"obfuscate": { "preset": "high", "debugProtection": true }`. Your license comments (`/*! … */`, `@license`) are kept.
322
+ ## TypeScript
110
323
 
111
- ## Configuration (`builder.config.json`)
324
+ Reference `.ts` files from your HTML — or the `.js` name, as TypeScript recommends — and build. No extra install.
325
+
326
+ ```html
327
+ <script type="module" src="js/main.ts"></script>
328
+ ```
329
+
330
+ - `.ts`, `.mts` and `.cts` files are transpiled with esbuild using your `tsconfig.json`. Types are removed; the syntax level is kept.
331
+ - The output is `.js` (`.mjs`, `.cjs`) and the HTML is updated to point to it.
332
+ - These keep working: `import './util'`, `import './util.ts'`, `import './util.js'` (when the file is `util.ts`), `import './components'` (folder with `index.ts`) and `new URL('./worker.ts', import.meta.url)`.
333
+ - `.d.ts` and `*.test.ts` files are not published.
334
+ - Types are not checked. Run `tsc --noEmit` in your editor or CI for that.
335
+
336
+ ## Configuration
337
+
338
+ Everything works without configuration. To change defaults, create `builder.config.json` in the project folder (`npx builder init` writes one based on your project). Every option can also be set from the command line, which takes precedence. Misspelled options are rejected with a suggestion.
112
339
 
113
340
  ```json
114
341
  {
115
342
  "outDir": "dist",
116
- "entries": "auto",
117
- "include": [],
118
- "ignore": [],
119
- "unreferenced": "process",
120
- "obfuscate": false,
121
- "dependencies": { "detect": true, "obfuscate": false, "include": [], "exclude": [] },
122
- "hash": false,
123
- "sourcemap": false,
124
- "compress": false,
125
- "base": "/",
126
- "sri": "auto",
127
- "html": { "minify": true, "removeComments": true, "collapseWhitespace": "auto" },
128
- "css": { "minify": true, "bundle": true, "target": "esnext" },
129
- "js": { "minify": true, "bundle": true, "target": "auto", "minifier": "esbuild", "mergeClassic": "auto", "drop": [], "legalComments": "eof", "chunkDir": "assets" }
343
+ "obfuscate": "medium",
344
+ "hash": true,
345
+ "compress": true,
346
+ "ignore": ["drafts/**"],
347
+ "js": { "drop": ["console", "debugger"] }
130
348
  }
131
349
  ```
132
350
 
133
- - `entries`: `"auto"` takes every complete HTML file; it also accepts a list of `.html`, `.js` or `.css` files.
134
- - `unreferenced`: what to do with files nothing references: `process` (minify and publish), `copy` or `skip`.
135
- - `js.target: "auto"` uses the syntax level of your own code: it neither transpiles nor introduces newer syntax than you already use.
136
- - `builder.config.mjs` is also accepted (export an object or a function). Misspelled options raise an error with a suggestion.
351
+ `builder.config.mjs` is also accepted; it can export an object or a function (sync or async) that returns one.
352
+
353
+ ### All options
354
+
355
+ | Option | Default | Description |
356
+ | --- | --- | --- |
357
+ | `root` | `"."` | Project folder, relative to the config file. |
358
+ | `outDir` | `"dist"` | Output folder. Builder refuses to empty a folder it did not create. |
359
+ | `entries` | `"auto"` | `"auto"` uses every complete HTML page. Or a list of `.html`, `.js` or `.css` files/patterns. |
360
+ | `include` | `[]` | Patterns always published, even if nothing references them (e.g. `"data/**"`). |
361
+ | `ignore` | `[]` | Patterns never published (e.g. `"drafts/**"`). |
362
+ | `defaultIgnore` | `true` | Exclude secrets (`.env*`, `*.pem`, `*.key`), `node_modules`, `package.json`, lockfiles, tool configs, tests and `.d.ts`. |
363
+ | `unreferenced` | `"process"` | Files nothing references: `"process"` (minify and publish), `"copy"` (publish as is) or `"skip"`. |
364
+ | `base` | `"/"` | Public base path of the site, for absolute URLs (`"/shop/"`). |
365
+ | `hash` | `false` | Content hash in JS/CSS file names (`assets/app-3f9a1c2b.js`). |
366
+ | `sourcemap` | `false` | `true` (linked `.map` files), `"hidden"` (no comment in the code) or `"inline"`. |
367
+ | `compress` | `false` | `true` (gzip and brotli), `"gzip"` or `"brotli"`: precompressed copies next to each file. |
368
+ | `sri` | `"auto"` | Subresource Integrity. `"auto"` recomputes existing `integrity` attributes; `true` also adds them to every local script and stylesheet; `false` removes them. |
369
+ | `verify` | `true` | Verify the output before publishing. Turning it off is not recommended. |
370
+ | `clean` | `true` | Replace `outDir` completely. With `false`, new files are copied over the existing ones. |
371
+ | `cache` | `true` | Reuse results from previous builds. |
372
+ | `workers` | `"auto"` | Threads for obfuscation and terser: `"auto"`, a number, or `0` for none. |
373
+ | `browsers` | `"auto"` | Target browsers: `"auto"` reads `.browserslistrc` or `browserslist` in `package.json`; or a query (`"> 0.5%, not dead"`); or `false`. Applies to `css.target` and `js.target` when they are at their defaults. Needs `browserslist`. |
374
+ | `images` | `false` | `true` or `{ "quality": 1-100 }`: optimize PNG (lossless), JPEG and WebP. Needs `sharp`. |
375
+ | `budgets` | `false` | Size limits by output pattern or `"total"`: `{ "js/*.js": "50 KB", "total": "1 MB gzip" }`. Over budget = failed build. |
376
+ | `plugins` | `[]` | See [Plugins](#plugins). |
377
+ | `obfuscate` | `false` | `false`, `true` (= `"medium"`), `"low"`, `"medium"`, `"high"`, `"max"`, or an object of [javascript-obfuscator options](https://github.com/javascript-obfuscator/javascript-obfuscator#javascript-obfuscator-options) with an optional `preset`. |
378
+ | `dependencies.detect` | `true` | Detect third-party libraries automatically. |
379
+ | `dependencies.include` | `[]` | Patterns that are libraries (not obfuscated). |
380
+ | `dependencies.exclude` | `[]` | Patterns that are your code (obfuscated even if they look like a library). |
381
+ | `dependencies.obfuscate` | `false` | Obfuscate libraries too. |
382
+ | `html.minify` | `true` | Minify HTML. |
383
+ | `html.removeComments` | `true` | Remove HTML comments. Meaningful ones are kept: `<!--! … -->`, IE conditionals, SSI and ESI. |
384
+ | `html.collapseWhitespace` | `"auto"` | Collapse whitespace between tags. `"auto"` turns it off if your CSS uses `white-space: pre`. |
385
+ | `css.minify` | `true` | Minify CSS. |
386
+ | `css.bundle` | `true` | Inline `@import`ed files. |
387
+ | `css.target` | `"esnext"` | CSS syntax target, e.g. `["chrome90", "safari15"]`. |
388
+ | `js.minify` | `true` | Minify JavaScript. |
389
+ | `js.bundle` | `true` | Bundle ES modules. With `false`, each module is minified in place. |
390
+ | `js.target` | `"auto"` | `"auto"` keeps the syntax level of your code; or `"es2018"`, `["chrome90", "safari15"]`… |
391
+ | `js.minifier` | `"esbuild"` | `"terser"` adds a second, stronger compression pass. |
392
+ | `js.mergeClassic` | `"auto"` | Merge consecutive classic `<script>` tags when it is safe (off when source maps are on). |
393
+ | `js.drop` | `[]` | Remove `"console"` and/or `"debugger"`. |
394
+ | `js.keepNames` | `false` | Keep function and class names (`fn.name`). |
395
+ | `js.define` | `{}` | Replace global identifiers at build time, e.g. `{ "DEBUG": "false" }`. |
396
+ | `js.legalComments` | `"eof"` | Where license comments go: `"eof"`, `"inline"` or `"none"`. |
397
+ | `js.chunkDir` | `"assets"` | Folder for shared chunks, hashed files and vendor packages. |
398
+ | `json.minify` | `true` | Remove whitespace from `.json` and `.webmanifest` (values are never rewritten). |
399
+
400
+ ## Plugins
401
+
402
+ For anything Builder does not do on its own. A plugin is an object with hooks; declare it in `builder.config.mjs`:
403
+
404
+ ```js
405
+ // builder.config.mjs
406
+ export default {
407
+ plugins: [
408
+ {
409
+ name: 'version',
410
+ filter: /\.(html|js)$/, // optional: which files transform() sees
411
+ transform: ({ path, code }) => code.replaceAll('__VERSION__', '1.4.0'),
412
+ buildStart: ({ config, mode }) => {}, // before anything is read
413
+ buildEnd: ({ result, outDir }) => {}, // after dist/ is written (build only)
414
+ },
415
+ ],
416
+ };
417
+ ```
418
+
419
+ - `transform` receives the **source** of each HTML, CSS, JS and TS file and returns the new code (or nothing to leave it). It runs before processing, so hashes, SRI and the final verification all see the plugin's result.
420
+ - In `builder.config.json`, name an installed package instead: `"plugins": ["my-plugin", ["other-plugin", { "option": 1 }]]`. The package exports the plugin object, or a function that receives the options and returns it.
421
+ - If a plugin throws, the build stops with a message naming the plugin and the file.
422
+
423
+ ## GitHub Action
424
+
425
+ ```yaml
426
+ # .github/workflows/build.yml
427
+ on: [push, pull_request]
428
+ permissions:
429
+ contents: read
430
+ pull-requests: write # only for comment: true
431
+ jobs:
432
+ build:
433
+ runs-on: ubuntu-latest
434
+ steps:
435
+ - uses: actions/checkout@v4
436
+ - uses: MarioACo/vanilla-builder@v0.3.0
437
+ with:
438
+ args: --hash --compress # any build options
439
+ comment: true # size report on pull requests
440
+ ```
441
+
442
+ It builds with a cache kept between runs, writes a size summary on the run page, comments on pull requests (updating the same comment on each push) and fails the job when the build, the verification or a budget fails. Inputs: `directory`, `args`, `version` (when the project does not install vanilla-builder), `node-version`, `cache`, `comment`. Outputs: `total-size`, `total-gzip`, `report` (path to the JSON report).
443
+
444
+ ## Command-line reference
445
+
446
+ ```text
447
+ builder <command> [folder] [options]
448
+ ```
449
+
450
+ | Command | Description |
451
+ | --- | --- |
452
+ | `build` | Build the project into `dist/`. |
453
+ | `dev` | Build, watch for changes, serve `dist/` and reload the browser. |
454
+ | `serve` | Serve the existing `dist/`. |
455
+ | `analyze` | Show what would happen to every file. Writes nothing. |
456
+ | `deps` | List which JavaScript files are libraries and which are yours. |
457
+ | `deps add\|own\|reset <pattern…>` | Mark files as libraries, as your code, or back to automatic. Use `--dir <folder>` for another project. |
458
+ | `init` | Create `builder.config.json` from what was detected (`--force` to overwrite). |
459
+ | `help`, `--version` | Help and version. |
460
+
461
+ ### Build options
462
+
463
+ | Option | Description |
464
+ | --- | --- |
465
+ | `--out <folder>` | Output folder (default `dist`). |
466
+ | `--config <file>` | Configuration file (default `builder.config.json`). |
467
+ | `--entry <file>` | Explicit entry point. Repeatable. |
468
+ | `--include <pattern>` / `--ignore <pattern>` | Always publish / never publish. Repeatable. |
469
+ | `--obfuscate [level]` | Obfuscate your JavaScript: `low`, `medium` (default), `high`, `max`. |
470
+ | `--dep <pattern>` / `--not-dep <pattern>` | Mark as library / as your code. Repeatable. |
471
+ | `--obfuscate-deps` | Obfuscate libraries too. |
472
+ | `--minifier terser` | Extra compression pass with terser. |
473
+ | `--compress [gzip\|brotli]` | Write precompressed `.gz`/`.br` files (both by default). |
474
+ | `--hash` | Content hash in JS/CSS file names. |
475
+ | `--sourcemap [linked\|hidden\|inline]` | Generate source maps. |
476
+ | `--target <target>` | JavaScript target, e.g. `es2018`. |
477
+ | `--drop-console` | Remove `console.*` and `debugger`. |
478
+ | `--no-minify` | Do not minify (useful to debug a problem). |
479
+ | `--no-merge` | Do not merge classic scripts. |
480
+ | `--unreferenced <mode>` | `process`, `copy` or `skip`. |
481
+ | `--base <path>` | Public base path, e.g. `/shop/`. |
482
+ | `--workers <n\|auto>` | Number of threads (`0` = none). |
483
+ | `--browsers <query>` / `--no-browsers` | Target browsers (browserslist query) / ignore the project's browsers. |
484
+ | `--images`, `--image-quality <1-100>` | Optimize images (needs `sharp`). |
485
+ | `--budget <pattern=size>` | Size budget, e.g. `"total=1MB gzip"`. Repeatable; replaces the configured budgets. |
486
+ | `--lang <en\|es>` | Message language. |
487
+ | `--no-cache` | Ignore the cache. |
488
+ | `--no-verify` | Skip the final verification (not recommended). |
489
+ | `--force` | Allow emptying an output folder Builder did not create. |
490
+ | `--keep-failed` | Keep the temporary folder if verification fails, to inspect it. |
491
+ | `--json` | Machine-readable result. |
492
+ | `--verbose` / `--quiet` | More detail (notes, copied and excluded files) / errors only. |
493
+
494
+ `dev` and `serve` also accept `--port <n>` (default `5173`), `--host <host>` (default `127.0.0.1`) and `--open`. Unknown options are rejected with a suggestion (`--obfusacte` → did you mean `--obfuscate`?).
495
+
496
+ ## Safety guarantees
497
+
498
+ | Common build-tool risk | What vanilla-builder does |
499
+ | --- | --- |
500
+ | Merging classic scripts breaks shared globals | Merges only consecutive scripts with the same `defer` and strict mode, without `document.currentScript` or conflicting `let`/`const`, using a safe separator. |
501
+ | Minification renames or adds globals | Compares globals before and after; falls back to a safer minification, or keeps the original. |
502
+ | Obfuscation breaks a module | Compares imports, exports and import attributes after obfuscating; on mismatch, the file is published unobfuscated. |
503
+ | HTML minification changes text | Structural comparison of elements, attributes and text; `<pre>`, `<textarea>` and templates untouched. |
504
+ | Stale `integrity` attributes block scripts | Recomputed whenever the content changes. |
505
+ | Legacy encodings or BOM get corrupted | HTML bytes are preserved; JS and CSS are emitted with ASCII escapes. |
506
+ | JSON numbers lose precision | Only whitespace outside strings is removed. |
507
+ | Deleting the wrong folder | Never empties a folder it did not create (marked by `.builder-manifest.json`), the project folder, your home folder or a drive root. |
508
+ | A failed build leaves a broken site | Output is written and verified in a temporary folder before replacing `dist/`. |
509
+ | Publishing secrets | `.env*`, keys, `node_modules`, `package.json` and tool configs are excluded by default. |
510
+
511
+ Every release is tested by opening example sites — original and built — in a real Chromium and requiring identical behavior with no console errors and no 404s, in five variants (default, high obfuscation + hash, max obfuscation, terser + precompression, unminified), building with Node 16 and with current Node.
137
512
 
138
- ## API
513
+ ## Performance
514
+
515
+ Measured with `npm run bench` on a generated site with 40 pages, 240 modules, 120 classic scripts, 30 CSS files and 200 images (median of 3 runs; Intel i7-11700F, Node 24, Windows):
516
+
517
+ | Build | First build | Rebuild (cache) | v0.1.0 |
518
+ | --- | ---: | ---: | ---: |
519
+ | Default | 1.9 s | 1.4 s | 3.7 s |
520
+ | `--obfuscate` | 7.0 s | 1.8 s | 11.5–17.9 s |
521
+ | `--hash --compress` | 2.1 s | 1.1 s | 2.6–3.8 s |
522
+ | `--minifier terser` | 3.7 s | 1.6 s | 5.6–6.0 s |
523
+
524
+ - **Threads.** Obfuscation, terser and the final syntax check run in worker threads (`"auto"` ≈ physical cores, up to 8). Threads start only when there is enough work to pay for their startup, so small projects are not slower.
525
+ - **Persistent cache.** Slow results are stored by content in `node_modules/.cache/vanilla-builder` (or the system temp folder). Same input, options and tool versions produce the same output byte for byte — the test suite checks it. Override the location with `BUILDER_CACHE_DIR`; disable with `--no-cache`.
526
+ - **Deterministic.** Independent files are processed in parallel but registered in their original order, so the output never depends on timing.
527
+ - To see where time goes in your project, look at `perf.phases` in `--json`.
528
+
529
+ ## JavaScript API
139
530
 
140
531
  ```js
141
532
  import { build, analyze } from 'vanilla-builder';
142
533
 
143
- const result = await build({ dir: 'my-site', overrides: { obfuscate: 'high' } });
144
- console.log(result.report.totals, result.warnings);
534
+ const result = await build({
535
+ dir: 'my-site', // project folder (default: current folder)
536
+ overrides: { obfuscate: 'high' }, // same shape as builder.config.json
537
+ });
538
+
539
+ console.log(result.report.totals); // { before, size, gzip, brotli, files }
540
+ console.log(result.warnings); // string[]
541
+ console.log(result.dependencies); // [{ rel, reason }]
145
542
  ```
146
543
 
147
- ## Development
544
+ - `build(options)` builds and writes `outDir`. Options: `dir`, `config` (path to a config file), `overrides`, `force`, `keepFailed`, `log` (function called with progress messages).
545
+ - `analyze(options)` returns the same result without writing anything.
546
+ - Errors are thrown as `BuildError` (exported), with a `details` array.
547
+ - `startServer({ dir, port, host })` serves a folder, as `builder serve` does.
148
548
 
149
- ```bash
150
- npm test # unit + integration (Node 18+; on Node 16: npm run test:node16)
151
- npm run test:browser # real equivalence in Chromium (finds Chrome/Edge or uses BUILDER_CHROMIUM)
152
- BUILDER_NODE=/path/to/node16 npm run test:browser # build with another Node and verify in the browser
153
- ```
549
+ ## Troubleshooting
550
+
551
+ ### `The output folder … already has files and was not created by Builder`
552
+
553
+ Builder never empties a folder it did not create. Choose another folder with `--out`, empty it yourself, or pass `--force` if you are sure.
154
554
 
155
- ## Known limits
555
+ ### `Missing optional package "javascript-obfuscator"` (or `terser`, `browserslist`, `sharp`)
156
556
 
157
- - No obfuscator makes code impossible to read: it only makes it much more expensive.
158
- - With `hash: true`, JS/CSS files are renamed; if your code requests them by string, the original path is also published and you get a warning.
159
- - Collapsing HTML whitespace changes `textContent` (not what you see). To keep it byte for byte: `"html": { "collapseWhitespace": false }`.
160
- - `import()` with computed paths and no fixed prefix makes that group of modules minify file by file in place (without bundling), so paths do not break.
557
+ Install it in your project, e.g. `npm install --save-dev javascript-obfuscator`. These are optional so the base install stays small.
558
+
559
+ ### `target browsers are set in package.json (browserslist), but the "browserslist" package is missing`
560
+
561
+ Your project declares browsers (maybe for another tool) but `browserslist` is not installed, so Builder keeps your code as it is. Install it (`npm install --save-dev browserslist`) to use them, or set `"browsers": false` to silence the warning.
562
+
563
+ ### A file is missing from `dist/`
564
+
565
+ Run `npx builder analyze --verbose`. It lists every file with its destination, and every excluded file with the reason. Files excluded by default (secrets, configs, tests) can still be published with `--include "<pattern>"`.
566
+
567
+ ### A library was obfuscated, or my code was not
568
+
569
+ Run `npx builder deps` to see the reason for each file, then correct it with `data-builder`, a `@builder-*` comment or `npx builder deps add|own` ([details](#check-and-correct-the-detection)).
570
+
571
+ ### `Two different results want to be written to …`
572
+
573
+ This usually means a folder in your project has the same name as `js.chunkDir` (`assets` by default), or the same module is used under different import maps. Set `js.chunkDir` to another name.
574
+
575
+ ### `"base" must start with "/"` when using Git Bash on Windows
576
+
577
+ Git Bash converts arguments that look like Unix paths, so `--base /shop/` arrives as `C:/Program Files/Git/shop/`. Put `"base": "/shop/"` in `builder.config.json`, or run `MSYS_NO_PATHCONV=1 npx builder build --base /shop/`. PowerShell and cmd are not affected.
578
+
579
+ ### Something behaves differently after the build
580
+
581
+ Build with `--no-minify` to rule out minification, or without `--obfuscate` to rule out obfuscation. If you find a difference, please [open an issue](https://github.com/MarioACo/vanilla-builder/issues) with the smallest example you can share.
582
+
583
+ ## Known limitations
584
+
585
+ - No obfuscator makes code impossible to read; it only makes it much more expensive to understand.
586
+ - With `hash`, JS and CSS are renamed. If your code requests one of them by a path written as text, the original path is published too and you get a warning.
587
+ - Collapsing HTML whitespace changes `textContent` but not what is displayed. To keep HTML text byte for byte, set `"html": { "collapseWhitespace": false }`.
588
+ - `import()` with a fully computed path makes that group of modules minify file by file instead of bundling, so paths keep working.
589
+ - TypeScript: no type checking, no `.tsx`/JSX, and source maps point to the type-stripped JavaScript.
590
+ - `--images` re-encodes JPEG and WebP with loss (PNG is lossless). Modern formats (WebP/AVIF from JPEG) are not generated, because that would require changing your HTML.
591
+
592
+ ## Contributing
593
+
594
+ Issues and pull requests are welcome.
595
+
596
+ ```bash
597
+ git clone https://github.com/MarioACo/vanilla-builder.git
598
+ cd vanilla-builder
599
+ npm install
600
+ npm test # unit and integration tests (Node 18+; on Node 16: npm run test:node16)
601
+ npm run test:browser # original vs built sites in Chromium (uses Chrome/Edge or BUILDER_CHROMIUM)
602
+ npm run bench # performance on a generated site
603
+ cd bench/compare && npm install && node run.mjs # comparison with Parcel and Vite
604
+ ```
161
605
 
162
606
  ## License
163
607