Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f1f5d37

Browse files
harlan-zwclaude
andauthored
feat: add MinifyPlugin for inline script/style minification (#705)
* feat: add MinifyPlugin for inline script/style minification Add lightweight pure-JS minifiers (zero native deps) and a MinifyPlugin that hooks into ssr:render to minify inline script and style content before HTML serialization. Also adds a Vite/Webpack source transform plugin that pre-minifies static string literals using esbuild/rolldown and lightningcss at build time. Fixes a bug where SSR hooks (ssr:render, ssr:beforeRender, ssr:rendered) never fired because the server createHead renderer closure captured the inner core object instead of the outer head with hooks. * fix: address review feedback, deduplicate renderer creation * refactor: use subpath exports for build-time minifier peer deps Replace hacky try/catch dynamic imports in MinifyTransform with explicit subpath exports per peer dependency. Consumers now import the minifier they need directly: - `@unhead/addons/minify/rolldown` (Vite 8+) - `@unhead/addons/minify/esbuild` (Vite 7) - `@unhead/addons/minify/lightningcss` (CSS) Also fixes wrong oxc AST node types (ObjectProperty -> Property, StringLiteral -> Literal) and adds full test coverage for the MinifyTransform plugin and all three subpath minifiers. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: align minify API, add docs, address review feedback - Unify MinifyTransformOptions to use `js?: false | MinifyFn` and `css?: false | MinifyFn` matching MinifyPluginOptions pattern - Remove `threshold` from MinifyPluginOptions (keep internal default) - Guard against OOB reads on trailing backslash in CSS/JS minifiers - Make minifyJSON return input unchanged on malformed JSON instead of throwing - Remove unused MinifyFn import from types.ts - Fix import ordering in test file - Add minify plugin documentation - Update vite-plugin docs with minify option Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
1 parent f88c846 commit f1f5d37

31 files changed

Lines changed: 4212 additions & 2450 deletions

docs/head/1.guides/2.advanced/9.vite-plugin.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ While these plugins are optional, they're highly recommended for production appl
1515
The build plugins perform several important optimizations:
1616

1717
- Transform `useSeoMeta` calls into raw `useHead()`{lang="ts"} calls (~3kb savings)
18+
- Pre-minify static inline script/style content inside `useHead()` calls at build time
1819
- Apply tree-shaking optimizations specific to Unhead
1920

2021
## How Do I Install the Build Plugins?
@@ -81,6 +82,7 @@ UnheadVite({
8182
| Option | Type | Default | Description |
8283
|--------|------|---------|-------------|
8384
| `transformSeoMeta` | `boolean` | `true` | Transform `useSeoMeta` calls to `useHead()`{lang="ts"} |
85+
| `minify` | `boolean \| object` | `true` | Pre-minify static inline script/style content. See [Minify Plugin](/docs/head/guides/plugins/minify#build-time-minification) |
8486

8587
## How Do I Verify the Plugin Is Working?
8688

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
---
2+
title: "Minify"
3+
description: "Minify inline script and style tag content during SSR rendering. Zero-dependency lightweight minifiers for edge and serverless, with support for custom minifiers."
4+
navigation.title: "Minify"
5+
---
6+
7+
**Quick Answer:** The Minify plugin strips comments, collapses whitespace, and minifies inline `<script>` and `<style>` content during SSR rendering. It uses lightweight pure-JS minifiers by default — zero native dependencies, safe for edge and serverless.
8+
9+
## What Does This Plugin Do?
10+
11+
The Minify plugin hooks into the SSR render pipeline and minifies:
12+
13+
- **Inline scripts** — strips comments, collapses whitespace, preserves string literals and ASI safety
14+
- **Inline styles** — strips comments, collapses whitespace, removes trailing semicolons, strips leading zeros
15+
- **JSON-LD / application/json** — re-serializes to remove pretty-printing whitespace
16+
17+
It automatically skips `speculationrules` and `importmap` script types, and never increases content length.
18+
19+
## How Do I Set Up the Plugin?
20+
21+
Register the plugin when creating your head instance:
22+
23+
::code-block
24+
```ts [Input]
25+
import { MinifyPlugin } from 'unhead/plugins'
26+
27+
const head = createHead({
28+
plugins: [
29+
MinifyPlugin()
30+
]
31+
})
32+
```
33+
::
34+
35+
## What Options Can I Configure?
36+
37+
::code-block
38+
```ts [Input]
39+
export interface MinifyPluginOptions {
40+
/**
41+
* Custom JS minifier function. Set to `false` to disable JS minification.
42+
* Defaults to built-in lightweight minifier.
43+
*/
44+
js?: false | ((code: string) => string)
45+
/**
46+
* Custom CSS minifier function. Set to `false` to disable CSS minification.
47+
* Defaults to built-in lightweight minifier.
48+
*/
49+
css?: false | ((code: string) => string)
50+
/**
51+
* Minify JSON script types (application/ld+json, application/json).
52+
* @default true
53+
*/
54+
json?: boolean
55+
}
56+
```
57+
::
58+
59+
### Disable Specific Minifiers
60+
61+
::code-block
62+
```ts [Input]
63+
MinifyPlugin({
64+
js: false, // skip script minification
65+
json: false, // skip JSON-LD minification
66+
})
67+
```
68+
::
69+
70+
### Custom Minifiers
71+
72+
Provide your own synchronous minifier function for heavier optimization:
73+
74+
::code-block
75+
```ts [Input]
76+
MinifyPlugin({
77+
js: (code) => myCustomJSMinify(code),
78+
css: (code) => myCustomCSSMinify(code),
79+
})
80+
```
81+
::
82+
83+
Note: The `ssr:render` hook runs synchronously, so custom minifiers must be synchronous.
84+
85+
## Build-Time Minification
86+
87+
For build-time minification of static string literals inside `useHead()` / `useServerHead()` calls, use the Vite/Webpack transform plugin from `@unhead/addons`. This runs at build time using heavier tools (rolldown/esbuild for JS, lightningcss for CSS) that never enter your SSR runtime bundle.
88+
89+
::code-group
90+
91+
```bash [pnpm]
92+
pnpm add -D @unhead/addons
93+
```
94+
95+
```bash [yarn]
96+
yarn add -D @unhead/addons
97+
```
98+
99+
```bash [npm]
100+
npm install -D @unhead/addons
101+
```
102+
103+
::
104+
105+
Configure the Vite plugin with minifier functions:
106+
107+
::code-block
108+
```ts [vite.config.ts]
109+
import UnheadVite from '@unhead/addons/vite'
110+
import { createJSMinifier } from '@unhead/addons/minify/rolldown'
111+
import { createCSSMinifier } from '@unhead/addons/minify/lightningcss'
112+
113+
export default defineConfig({
114+
plugins: [
115+
UnheadVite({
116+
minify: {
117+
js: createJSMinifier(),
118+
css: createCSSMinifier(),
119+
}
120+
})
121+
]
122+
})
123+
```
124+
::
125+
126+
Available minifier backends:
127+
128+
| Package | Function | Use case |
129+
|---------|----------|----------|
130+
| `@unhead/addons/minify/rolldown` | `createJSMinifier()` | JS minification (Vite 8+) |
131+
| `@unhead/addons/minify/esbuild` | `createJSMinifier()` | JS minification (Vite 7) |
132+
| `@unhead/addons/minify/lightningcss` | `createCSSMinifier()` | CSS minification |
133+
134+
## Standalone Minifier Utilities
135+
136+
The built-in minifiers are also available as standalone functions for use in custom build pipelines:
137+
138+
::code-block
139+
```ts [Input]
140+
import { minifyCSS, minifyJS, minifyJSON } from 'unhead/minify'
141+
142+
const minifiedJS = minifyJS('// comment\nvar x = 1;')
143+
const minifiedCSS = minifyCSS('body { margin: 0; }')
144+
const minifiedJSON = minifyJSON('{ "name": "test" }')
145+
```
146+
::
147+
148+
## Related
149+
150+
- [Build Plugins](/docs/head/guides/advanced/vite-plugin) - Vite and Webpack build optimizations
151+
- [Validate Plugin](/docs/head/guides/plugins/validate) - Catch common SEO and head tag mistakes
152+
- [Inner Content](/docs/head/guides/core-concepts/inner-content) - Working with innerHTML and textContent

packages/addons/build.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export default defineBuildConfig({
77
{ input: 'src/index', name: 'index' },
88
{ input: 'src/unplugin/vite', name: 'vite' },
99
{ input: 'src/unplugin/webpack', name: 'webpack' },
10+
{ input: 'src/minify/rolldown', name: 'minify/rolldown' },
11+
{ input: 'src/minify/esbuild', name: 'minify/esbuild' },
12+
{ input: 'src/minify/lightningcss', name: 'minify/lightningcss' },
1013
],
1114
externals: [
1215
'vite',
@@ -15,6 +18,10 @@ export default defineBuildConfig({
1518
'unhead',
1619
'unplugin',
1720
'unhead/plugins',
21+
'unhead/minify',
1822
'unhead/utils',
23+
'esbuild',
24+
'lightningcss',
25+
'rolldown',
1926
],
2027
})

packages/addons/package.json

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232
"./webpack": {
3333
"types": "./dist/webpack.d.ts",
3434
"default": "./dist/webpack.mjs"
35+
},
36+
"./minify/rolldown": {
37+
"types": "./dist/minify/rolldown.d.ts",
38+
"default": "./dist/minify/rolldown.mjs"
39+
},
40+
"./minify/esbuild": {
41+
"types": "./dist/minify/esbuild.d.ts",
42+
"default": "./dist/minify/esbuild.mjs"
43+
},
44+
"./minify/lightningcss": {
45+
"types": "./dist/minify/lightningcss.d.ts",
46+
"default": "./dist/minify/lightningcss.mjs"
3547
}
3648
},
3749
"main": "dist/index.mjs",
@@ -44,6 +56,15 @@
4456
],
4557
"webpack": [
4658
"dist/webpack"
59+
],
60+
"minify/rolldown": [
61+
"dist/minify/rolldown"
62+
],
63+
"minify/esbuild": [
64+
"dist/minify/esbuild"
65+
],
66+
"minify/lightningcss": [
67+
"dist/minify/lightningcss"
4768
]
4869
}
4970
},
@@ -58,7 +79,21 @@
5879
"prepack": "pnpm run build"
5980
},
6081
"peerDependencies": {
61-
"unhead": "workspace:^"
82+
"unhead": "workspace:^",
83+
"esbuild": ">=0.17.0",
84+
"lightningcss": ">=1.20.0",
85+
"rolldown": ">=1.0.0-beta.0"
86+
},
87+
"peerDependenciesMeta": {
88+
"esbuild": {
89+
"optional": true
90+
},
91+
"lightningcss": {
92+
"optional": true
93+
},
94+
"rolldown": {
95+
"optional": true
96+
}
6297
},
6398
"dependencies": {
6499
"@rollup/pluginutils": "catalog:",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { MinifyFn } from '../unplugin/MinifyTransform'
2+
import { transform } from 'esbuild'
3+
4+
export function createJSMinifier(): MinifyFn {
5+
return async (code: string) => {
6+
const result = await transform(code, { minify: true, loader: 'js' })
7+
return result.code.trim()
8+
}
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { MinifyFn } from '../unplugin/MinifyTransform'
2+
import { transform } from 'lightningcss'
3+
4+
export function createCSSMinifier(): MinifyFn {
5+
return async (code: string) => {
6+
const result = transform({
7+
filename: 'inline.css',
8+
code: new TextEncoder().encode(code),
9+
minify: true,
10+
})
11+
return new TextDecoder().decode(result.code).trim()
12+
}
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { MinifyFn } from '../unplugin/MinifyTransform'
2+
import { minify } from 'rolldown/experimental'
3+
4+
export function createJSMinifier(): MinifyFn {
5+
return async (code: string) => {
6+
const result = await minify('inline.js', code)
7+
return result.code.trim()
8+
}
9+
}

0 commit comments

Comments
 (0)