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

Skip to content

feat: add MinifyPlugin for inline script/style minification#705

Merged
harlan-zw merged 4 commits into
mainfrom
feat/minify-plugin
Apr 5, 2026
Merged

feat: add MinifyPlugin for inline script/style minification#705
harlan-zw merged 4 commits into
mainfrom
feat/minify-plugin

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

πŸ”— Linked issue

Resolves #525

❓ Type of change

  • πŸ“– Documentation
  • 🐞 Bug fix
  • πŸ‘Œ Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

πŸ“š Description

Inline <script> and <style> tags in SSR responses are shipped unminified. This adds a cross-framework minification system with two complementary layers:

Runtime plugin (MinifyPlugin from unhead/plugins): Hooks into ssr:render to minify HeadTag.innerHTML before serialization. Uses lightweight pure-JS minifiers with zero native dependencies (safe for edge/serverless). Handles dynamic content like useHead() injections, hydration payloads, JSON-LD. Accepts custom minifier functions for users who want heavier optimization.

Build-time Vite/Webpack transform (MinifyTransform in @unhead/addons): AST-walks useHead()/useServerHead() calls and pre-minifies static innerHTML/textContent string literals at compile time using rolldown (Vite 8+) or esbuild (Vite 7) for JS and lightningcss for CSS. These heavy dependencies never enter the SSR runtime bundle since they only run in the Vite transform hook.

Standalone utilities (unhead/minify): Exports minifyJS, minifyCSS, minifyJSON for framework build pipelines to consume directly.

Also 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 (without hooks) instead of the outer head wrapper.

Usage

// Runtime: lightweight minification per SSR request
import { MinifyPlugin } from 'unhead/plugins'

const head = createHead({
  plugins: [MinifyPlugin()]
})

// Build-time: enable source transform in Vite config
// (enabled by default in @unhead/addons vite plugin)
import UnheadAddons from '@unhead/addons/vite'

export default defineConfig({
  plugins: [UnheadAddons()]
})

Summary by CodeRabbit

  • New Features

    • Minification for JavaScript, CSS, and JSON in head tags (configurable, threshold-aware)
    • Build-time and SSR plugins to automatically minify inline scripts/styles when enabled
    • New public "minify" entry exposing standalone minify utilities for JS/CSS/JSON
    • Additional optional minifier backends available (esbuild, rolldown, lightningcss) via addon entrypoints
    • Head now exposes a public render() method for server rendering
  • Tests

    • Added unit tests covering JS/CSS/JSON minifiers and plugin behaviors (options, thresholds, skips)

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@harlan-zw has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 58 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 5 minutes and 58 seconds.

βŒ› How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f81b37a2-9747-4281-bf3d-d503090e4dac

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between d453ce3 and d21e2c0.

β›” Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (30)
  • docs/head/1.guides/2.advanced/9.vite-plugin.md
  • docs/head/1.guides/plugins/minify.md
  • packages/addons/build.config.ts
  • packages/addons/package.json
  • packages/addons/src/minify/esbuild.ts
  • packages/addons/src/minify/lightningcss.ts
  • packages/addons/src/minify/rolldown.ts
  • packages/addons/src/unplugin/MinifyTransform.ts
  • packages/addons/src/unplugin/types.ts
  • packages/addons/src/unplugin/vite.ts
  • packages/addons/src/unplugin/webpack.ts
  • packages/addons/test/minify.test.ts
  • packages/addons/test/minifyTransform.test.ts
  • packages/unhead/build.config.ts
  • packages/unhead/package.json
  • packages/unhead/src/minify/css.ts
  • packages/unhead/src/minify/index.ts
  • packages/unhead/src/minify/js.ts
  • packages/unhead/src/minify/json.ts
  • packages/unhead/src/plugins/index.ts
  • packages/unhead/src/plugins/minify.ts
  • packages/unhead/src/server/createHead.ts
  • packages/unhead/test/unit/minify.test.ts
  • packages/unhead/test/unit/plugins/minify.test.ts
  • test/exports/addons.yaml
  • test/exports/react.yaml
  • test/exports/solid-js.yaml
  • test/exports/svelte.yaml
  • test/exports/unhead.yaml
  • test/exports/vue.yaml
πŸ“ Walkthrough

Walkthrough

Adds build-time and SSR minification: a Vite/Webpack unplugin to pre-minify inline content in useHead() calls, standalone minify utilities (CSS/JS/JSON), a server-side MinifyPlugin, new addon minifier entrypoints/peers, updated build/exports, and tests; bundler externals adjusted for minifier packages.

Changes

Cohort / File(s) Summary
Build Configs
packages/addons/build.config.ts, packages/unhead/build.config.ts
Added build entries for minify bundles and marked minifier tool packages as externals.
Unplugin (build-time)
packages/addons/src/unplugin/MinifyTransform.ts, packages/addons/src/unplugin/types.ts, packages/addons/src/unplugin/vite.ts, packages/addons/src/unplugin/webpack.ts
New MinifyTransform unplugin to parse AST and pre-minify inline useHead() script/style literals; added minify option to unplugin types and conditional registration in Vite/Webpack factories.
Minify utilities
packages/unhead/src/minify/index.ts, packages/unhead/src/minify/css.ts, packages/unhead/src/minify/js.ts, packages/unhead/src/minify/json.ts
Added pure‑JS minifiers minifyCSS, minifyJS, minifyJSON and re-exported via a minify entrypoint.
SSR Plugin
packages/unhead/src/plugins/minify.ts, packages/unhead/src/plugins/index.ts
New MinifyPlugin with ssr:render hook that minifies rendered <script>/<style> innerHTML using built-in or custom minifiers; exports options type.
Addon minifier backends
packages/addons/src/minify/rolldown.ts, packages/addons/src/minify/esbuild.ts, packages/addons/src/minify/lightningcss.ts
Added factories to create async MinifyFn wrappers for rolldown, esbuild, and lightningcss integrations.
Package exports & peers
packages/addons/package.json, packages/unhead/package.json
Added ./minify and ./minify/* export subpaths and typings; added optional peerDependencies for esbuild, lightningcss, and rolldown.
Server API change
packages/unhead/src/server/createHead.ts
createHead now creates a renderer once and exposes an explicit render method on the returned head instance.
Tests
packages/unhead/test/unit/minify.test.ts, packages/unhead/test/unit/plugins/minify.test.ts, packages/addons/test/minify.test.ts, packages/addons/test/minifyTransform.test.ts
New unit tests covering minifier utilities, SSR plugin behavior, addon minifier backends, and the MinifyTransform unplugin.
Export manifests / test fixtures
test/exports/*.yaml, test/exports/addons.yaml
Updated export manifests to include ./minify functions and MinifyPlugin, and added addon backend exports for minifier factories.

Sequence Diagram(s)

sequenceDiagram
    participant Build as Build Process
    participant Transform as MinifyTransform\n(Unplugin)
    participant Utils as Minify Utilities
    participant Output as Bundle Output

    Build->>Transform: Provide source modules
    Transform->>Transform: Parse AST, locate useHead()/useServerHead()
    Transform->>Utils: Request JS/CSS/JSON minification
    Utils->>Utils: Run chosen minifier (js/css/json)
    Utils-->>Transform: Return minified string
    Transform->>Output: Replace literal, emit updated code + sourcemap
Loading
sequenceDiagram
    participant App as Application
    participant Server as Server
    participant Renderer as SSR Renderer
    participant Plugin as MinifyPlugin
    participant Utils as Minify Utilities
    participant HTML as Rendered HTML

    App->>Server: createHead() (register plugins)
    Server->>Renderer: renderer(head) -> produce tags
    Renderer->>Plugin: ssr:render(tags)
    Plugin->>Utils: Minify tag.innerHTML (script/style/json)
    Utils-->>Plugin: Return minified content
    Plugin->>HTML: Replace tag content if shorter
    HTML->>App: Return optimized HTML
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibbled comments, whisked the space away,
Trimmed scripts and styles for a lighter day.
Build-time hopping, SSR delight,
Tiny bundles gleam in moonlit byte.
β€” a rabbit’s minify, soft and spry πŸ₯•

πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (4 passed)
Check name Status Explanation
Title check βœ… Passed The title clearly describes the main feature addition: MinifyPlugin for minifying inline scripts and styles, which is the primary objective of this changeset.
Description check βœ… Passed The description follows the template structure with linked issue, type of change marked, and comprehensive explanation covering all three implementation layers and the bug fix.
Linked Issues check βœ… Passed The PR fully addresses issue #525 objectives: adds minification via Vite plugin (MinifyTransform), enables catching JS issues through transformation, provides build-time and runtime solutions.
Out of Scope Changes check βœ… Passed All changes are directly related to minification feature: minifier implementations, plugin integration, build configuration, tests, and export mappings. The SSR hooks bugfix is a necessary enabler.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/minify-plugin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.6 kB 4.3 kB
Server (Minimal) 10.3 kB 4.2 kB
Vue Client (Minimal) 11.6 kB 4.8 kB
Vue Server (Minimal) 11.3 kB 4.6 kB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
packages/unhead/src/server/createHead.ts (1)

14-14: Redundant renderer creation.

createServerRenderer({ tagWeight: capoTagWeight }) is called twice: once at line 14 (passed to createUnhead) and again at line 50. The first renderer is immediately discarded when head.render is overridden at line 54.

Consider passing a no-op or lightweight stub to createUnhead since its renderer is unused:

♻️ Suggested refactor to avoid redundant allocation
-  const core = createUnhead<T, SSRHeadPayload>(createServerRenderer({ tagWeight: capoTagWeight }), {
+  const core = createUnhead<T, SSRHeadPayload>(() => ({} as SSRHeadPayload), {
     _tagWeight: capoTagWeight,

Alternatively, document why both are needed if there's a reason not immediately apparent.

Also applies to: 50-54

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/src/server/createHead.ts` at line 14, The code creates a real
server renderer twiceβ€”once passed into createUnhead and again when overriding
head.renderβ€”wasting allocation; modify the implementation so
createServerRenderer({ tagWeight: capoTagWeight }) is only created once and
reused for head.render, or pass a lightweight/no-op stub renderer into
createUnhead (instead of the full createServerRenderer) and keep the actual
renderer assigned to head.render; locate createUnhead, createServerRenderer and
head.render in the file and replace the redundant createServerRenderer call with
either the reused renderer instance or a minimal stub implementation so the
expensive renderer is not allocated twice.
packages/addons/src/unplugin/MinifyTransform.ts (3)

118-125: Consider handling parse errors gracefully.

parseSync from oxc-parser may throw on syntax errors in the source file. If this happens during the transform hook, it could fail the build. Since this runs on user code, a try/catch with a warning might provide better DX.

Optional: Graceful parse error handling
    async transform(code, id) {
      if (!code.includes('useHead') && !code.includes('useServerHead'))
        return

+     let ast
+     try {
+       ast = parseSync(id, code)
+     }
+     catch (e) {
+       // Let the main compiler handle syntax errors
+       return
+     }
+
      const scopeTracker = new ScopeTracker()
-     const ast = parseSync(id, code)
      const s = new MagicString(code)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/addons/src/unplugin/MinifyTransform.ts` around lines 118 - 125, The
transform hook currently calls parseSync(id, code) which can throw on syntax
errors; wrap that call in a try/catch inside the async transform function
(before using ScopeTracker, MagicString, and pendingMinifications) and on catch
emit a non-fatal warning that includes the id and the error message (e.g.,
this.warn or console.warn) and then return null or the original code to avoid
failing the build; ensure subsequent logic only runs when parseSync succeeded.

101-105: Include filter takes precedence over excludeβ€”verify this is intentional.

The current ordering checks include first (returns true) before exclude (returns false). This means a file matching both patterns will be processed. The conventional expectation is often that exclude takes precedence.

If this is intentional, consider documenting it in the MinifyTransformOptions interface.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/addons/src/unplugin/MinifyTransform.ts` around lines 101 - 105, The
include-vs-exclude precedence in MinifyTransform.ts currently returns true if
options.filter.include matches before checking options.filter.exclude, so a file
matching both will be processed; decide which behavior you want and either (A)
change the runtime logic in the match path to check options.filter.exclude first
(if a match, return false) then check options.filter.include (return true) to
make exclude take precedence, or (B) keep the current order but document the
intentional precedence by adding a clear note to the MinifyTransformOptions
interface (and comments near the matching code) stating that include wins over
exclude; update unit tests accordingly to reflect the chosen precedence.

47-61: Empty catch blocks silently swallow minifier import/execution failures.

Both rolldown and esbuild fallback paths have empty catch blocks. While this enables graceful degradation when dependencies aren't available, it also silently swallows actual minification errors (e.g., syntax errors in the JS being minified).

Consider whether build-time syntax errors in inline scripts should surface to the developer rather than pass through silently.

Optional: Log warnings on minification failures
      try {
        const rolldownPath = 'rolldown/experimental'
        const { minify } = await import(/* `@vite-ignore` */ rolldownPath) as { minify: (filename: string, code: string) => Promise<{ code: string }> }
        const result = await minify('inline.js', code)
        return result.code.trim()
      }
-     catch {}
+     catch (e) {
+       // Fallback to esbuild if rolldown unavailable
+       if (!(e instanceof Error && e.message.includes('Cannot find'))) {
+         console.warn('[unhead:minify] JS minification failed:', e)
+       }
+     }
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/addons/src/unplugin/MinifyTransform.ts` around lines 47 - 61, The
empty catch blocks in MinifyTransform (around the dynamic import of
rolldownPath/minify and the esbuild.transform fallback) silently swallow errors;
change each catch to capture the error (e.g., catch (err)) and emit a clear
warning including which minifier failed, the identifier 'inline.js', and the
error details (use console.warn or an existing logger) so minification failures
are visible while still allowing the next fallback to run; keep the graceful
fallback behavior but ensure the final failure path returns null only after
logging the last error.
packages/unhead/test/unit/plugins/minify.test.ts (2)

68-84: Consider adding coverage for importmap type.

The SKIP_JS_TYPES set includes both speculationrules and importmap, but only speculationrules is tested. Adding a similar test for importmap would ensure complete coverage of the skip logic.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/test/unit/plugins/minify.test.ts` around lines 68 - 84, Add a
test mirroring the existing speculationrules case to cover the importmap skip
path: in the test file use createServerHeadWithContext with MinifyPlugin(), push
a script tag with type 'importmap' and innerHTML set to a JSON string (similar
to speculationContent), call head.render() and assert that headTags contains the
raw JSON string; this ensures the SKIP_JS_TYPES logic (including 'importmap') is
exercised alongside the existing speculationrules test.

49-66: Consider adding a test for malformed JSON-LD resilience.

The current tests verify happy-path JSON-LD minification. Given that minifyJSON throws on invalid JSON (and the related issue flagged in minify.ts), consider adding a test that verifies the plugin doesn't crash when encountering malformed JSON-LD content.

Suggested test case (once the fix is applied to minify.ts)
it('handles malformed JSON-LD gracefully', () => {
  const head = createServerHeadWithContext({
    plugins: [MinifyPlugin()],
  })
  const invalidJson = '{ "name": "test", invalid }'
  head.push({
    script: [{
      type: 'application/ld+json',
      innerHTML: invalidJson,
    }],
  })

  // Should not throw and should preserve original content
  const { headTags } = head.render()
  expect(headTags).toContain(invalidJson)
})
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/test/unit/plugins/minify.test.ts` around lines 49 - 66, Add a
unit test that verifies MinifyPlugin and its minifyJSON handling are resilient
to malformed JSON-LD: using createServerHeadWithContext({ plugins:
[MinifyPlugin()] }), push a script with type 'application/ld+json' whose
innerHTML is invalid JSON (e.g. '{ "name": "test", invalid }'), call
head.render(), and assert that render does not throw and that headTags contains
the original invalid JSON string (i.e., the plugin preserved the content instead
of crashing or removing it). This ensures minifyJSON errors are caught and
original innerHTML is left unchanged.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/unhead/src/minify/css.ts`:
- Around line 18-22: The loop that parses CSS strings can read past the end when
encountering a trailing backslash: in the while block that uses variables i,
len, code, quote and appends to result, ensure you check bounds before
performing the second code[i++] after detecting a backslash; if i is at or
beyond len after consuming the backslash, append the backslash to result (or
handle as a terminated string) and break instead of accessing code[i++], so the
code safely handles a final unescaped backslash.

In `@packages/unhead/src/minify/js.ts`:
- Around line 27-38: The comment-handling code that checks ch === '/' and code[i
+ 1] for '//' and '/*' does not distinguish regex literals from division
operators; update the scanner around the '/' handling (the blocks that use
variables ch, code and index i) to first determine whether a regex literal is
valid in the current parsing context by inspecting the previous significant
token/character (skip whitespace/comments) β€” allow regex starts after tokens
like '(', ',', '=', ':', '[', '!', '?', '{', 'return', 'case', 'throw', 'in',
'of', 'typeof' etc.; if a regex is valid, parse the regex literal (handle
escapes and character classes until the closing '/' and optional flags) instead
of treating it as a comment or division, otherwise continue with the existing
single-line (//) and multi-line (/* */) comment logic; if you prefer not to
implement heuristics, add a clear note in the minifier documentation warning
about this regex-vs-division limitation.
- Around line 17-22: The loop handling string literals in
packages/unhead/src/minify/js.ts can read past the end when encountering a
trailing backslash; update the logic inside the while (i < len && code[i] !==
quote) loop that handles escapes so you check bounds before consuming the
escaped character: when you see a backslash (code[i] === '\\'), increment and
then only append code[i] if i < len, otherwise append the backslash (or
otherwise handle the malformed trailing escape) and break out to avoid
out-of-bounds access; reference the variables/function scope using code, i, len,
quote, and result to locate and modify this behavior.
- Around line 13-25: The template literal handling in the minifier (in the block
that checks if ch === '`' and uses variables quote, result, i, code, len)
doesn't handle `${...}` interpolations and will treat backticks inside
interpolations as closing the outer template; update the logic so that when
quote === '`' you scan the template and when you encounter a `${` you enter an
interpolation-state that parses until the matching `}` (tracking nested braces
and respecting strings/templates inside the interpolation), resuming outer
template scanning afterwards; ensure you still handle escapes (`\`) and include
closing backtick in result just like the other branches.

In `@packages/unhead/src/minify/json.ts`:
- Around line 4-5: minifyJSON currently parses then re-serializes which converts
numeric tokens to JS Number and silently corrupts integers >
Number.MAX_SAFE_INTEGER; update the minifyJSON implementation to avoid numeric
coercion by either (a) using a token-based minifier that strips whitespace only
(preserving numeric strings) instead of JSON.parse/JSON.stringify, or (b) use a
bigint-aware JSON library (e.g., json-bigint or similar) to parse/stringify and
preserve large integers, or (c) at minimum detect large integer literals during
minification and leave them as strings or passthrough tokens; modify the
function named minifyJSON accordingly and add a brief comment/docstring about
the chosen behavior so callers know how large integers are handled.

In `@packages/unhead/src/plugins/minify.ts`:
- Around line 66-73: The call to minifyJSON inside the JSON_TYPES branch can
throw (JSON.parse) and crash SSR; wrap the minifyJSON(content) call in a
try/catch so malformed JSON is ignored (or left unchanged) instead of throwing,
e.g. catch and skip assigning tag.innerHTML when an error occurs, keeping the
existing jsonMinify and continue logic; reference minifyJSON, JSON_TYPES,
tag.innerHTML and the jsonMinify flag to locate where to add the try/catch.

---

Nitpick comments:
In `@packages/addons/src/unplugin/MinifyTransform.ts`:
- Around line 118-125: The transform hook currently calls parseSync(id, code)
which can throw on syntax errors; wrap that call in a try/catch inside the async
transform function (before using ScopeTracker, MagicString, and
pendingMinifications) and on catch emit a non-fatal warning that includes the id
and the error message (e.g., this.warn or console.warn) and then return null or
the original code to avoid failing the build; ensure subsequent logic only runs
when parseSync succeeded.
- Around line 101-105: The include-vs-exclude precedence in MinifyTransform.ts
currently returns true if options.filter.include matches before checking
options.filter.exclude, so a file matching both will be processed; decide which
behavior you want and either (A) change the runtime logic in the match path to
check options.filter.exclude first (if a match, return false) then check
options.filter.include (return true) to make exclude take precedence, or (B)
keep the current order but document the intentional precedence by adding a clear
note to the MinifyTransformOptions interface (and comments near the matching
code) stating that include wins over exclude; update unit tests accordingly to
reflect the chosen precedence.
- Around line 47-61: The empty catch blocks in MinifyTransform (around the
dynamic import of rolldownPath/minify and the esbuild.transform fallback)
silently swallow errors; change each catch to capture the error (e.g., catch
(err)) and emit a clear warning including which minifier failed, the identifier
'inline.js', and the error details (use console.warn or an existing logger) so
minification failures are visible while still allowing the next fallback to run;
keep the graceful fallback behavior but ensure the final failure path returns
null only after logging the last error.

In `@packages/unhead/src/server/createHead.ts`:
- Line 14: The code creates a real server renderer twiceβ€”once passed into
createUnhead and again when overriding head.renderβ€”wasting allocation; modify
the implementation so createServerRenderer({ tagWeight: capoTagWeight }) is only
created once and reused for head.render, or pass a lightweight/no-op stub
renderer into createUnhead (instead of the full createServerRenderer) and keep
the actual renderer assigned to head.render; locate createUnhead,
createServerRenderer and head.render in the file and replace the redundant
createServerRenderer call with either the reused renderer instance or a minimal
stub implementation so the expensive renderer is not allocated twice.

In `@packages/unhead/test/unit/plugins/minify.test.ts`:
- Around line 68-84: Add a test mirroring the existing speculationrules case to
cover the importmap skip path: in the test file use createServerHeadWithContext
with MinifyPlugin(), push a script tag with type 'importmap' and innerHTML set
to a JSON string (similar to speculationContent), call head.render() and assert
that headTags contains the raw JSON string; this ensures the SKIP_JS_TYPES logic
(including 'importmap') is exercised alongside the existing speculationrules
test.
- Around line 49-66: Add a unit test that verifies MinifyPlugin and its
minifyJSON handling are resilient to malformed JSON-LD: using
createServerHeadWithContext({ plugins: [MinifyPlugin()] }), push a script with
type 'application/ld+json' whose innerHTML is invalid JSON (e.g. '{ "name":
"test", invalid }'), call head.render(), and assert that render does not throw
and that headTags contains the original invalid JSON string (i.e., the plugin
preserved the content instead of crashing or removing it). This ensures
minifyJSON errors are caught and original innerHTML is left unchanged.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 517f5afc-ae4d-461e-abc4-e2ac339956e3

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between ad1ab0f and 48a78af.

πŸ“’ Files selected for processing (21)
  • packages/addons/build.config.ts
  • packages/addons/src/unplugin/MinifyTransform.ts
  • packages/addons/src/unplugin/types.ts
  • packages/addons/src/unplugin/vite.ts
  • packages/addons/src/unplugin/webpack.ts
  • packages/unhead/build.config.ts
  • packages/unhead/package.json
  • packages/unhead/src/minify/css.ts
  • packages/unhead/src/minify/index.ts
  • packages/unhead/src/minify/js.ts
  • packages/unhead/src/minify/json.ts
  • packages/unhead/src/plugins/index.ts
  • packages/unhead/src/plugins/minify.ts
  • packages/unhead/src/server/createHead.ts
  • packages/unhead/test/unit/minify.test.ts
  • packages/unhead/test/unit/plugins/minify.test.ts
  • test/exports/react.yaml
  • test/exports/solid-js.yaml
  • test/exports/svelte.yaml
  • test/exports/unhead.yaml
  • test/exports/vue.yaml

Comment thread packages/unhead/src/minify/css.ts
Comment on lines +13 to +25
if (ch === '\'' || ch === '"' || ch === '`') {
const quote = ch
result += ch
i++
while (i < len && code[i] !== quote) {
if (code[i] === '\\') {
result += code[i++]
}
result += code[i++]
}
if (i < len)
result += code[i++] // closing quote
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are tests covering nested template literals
rg -n 'nested.*template|template.*nested|\$\{.*\`' packages/unhead/test/

Repository: unjs/unhead

Length of output: 203


🏁 Script executed:

#!/bin/bash
# Search for test files with template literals
find packages/unhead/test -name "*.ts" -o -name "*.js" | head -20

Repository: unjs/unhead

Length of output: 1043


🏁 Script executed:

#!/bin/bash
# Look for template literal usage in tests
rg '`.*\$\{' packages/unhead/test/ -A 2 -B 2 --max-count=10

Repository: unjs/unhead

Length of output: 20871


🏁 Script executed:

#!/bin/bash
# Examine the full minify function implementation
cat packages/unhead/src/minify/js.ts

Repository: unjs/unhead

Length of output: 2158


🏁 Script executed:

#!/bin/bash
# Examine the minify test file
wc -l packages/unhead/test/unit/plugins/minify.test.ts

Repository: unjs/unhead

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Look at minify test content
cat packages/unhead/test/unit/plugins/minify.test.ts

Repository: unjs/unhead

Length of output: 4812


🏁 Script executed:

#!/bin/bash
# Search for any nested template literals in the actual codebase
rg '\`.*\$\{.*\`' packages/unhead/src packages/unhead/test --type ts --type js -A 1 -B 1 | head -30

Repository: unjs/unhead

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Search for backtick usage in the codebase (simpler search)
rg '`' packages/unhead/src packages/unhead/test --type ts --type js | grep -E '\$\{' | head -20

Repository: unjs/unhead

Length of output: 2093


🏁 Script executed:

#!/bin/bash
# Look for any nested backticks specifically
rg '`.*`.*`' packages/unhead/src packages/unhead/test --type ts --type js

Repository: unjs/unhead

Length of output: 6089


🏁 Script executed:

#!/bin/bash
# Search in JavaScript/TypeScript files for template literal interpolation
rg '\$\{' packages/unhead/src --type ts --type js -B 2 -A 2 | head -40

Repository: unjs/unhead

Length of output: 2236


🏁 Script executed:

#!/bin/bash
# Search for minifyJS function usage in tests
rg 'minifyJS' packages/unhead/test --type ts -B 2 -A 2

Repository: unjs/unhead

Length of output: 3904


Nested template literals will break minification.

The string literal handling doesn't account for template literal interpolations (${...}). For code like:

const x = `outer ${`inner`} text`

The minifier will incorrectly treat the inner backtick as closing the outer template literal, corrupting the output. The issue occurs because the loop searches for a closing backtick without understanding that backticks inside ${...} expressions should not end the outer template.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/src/minify/js.ts` around lines 13 - 25, The template literal
handling in the minifier (in the block that checks if ch === '`' and uses
variables quote, result, i, code, len) doesn't handle `${...}` interpolations
and will treat backticks inside interpolations as closing the outer template;
update the logic so that when quote === '`' you scan the template and when you
encounter a `${` you enter an interpolation-state that parses until the matching
`}` (tracking nested braces and respecting strings/templates inside the
interpolation), resuming outer template scanning afterwards; ensure you still
handle escapes (`\`) and include closing backtick in result just like the other
branches.

Comment thread packages/unhead/src/minify/js.ts
Comment on lines +27 to +38
else if (ch === '/' && code[i + 1] === '/') {
i += 2
while (i < len && code[i] !== '\n')
i++
}
// multi-line comment
else if (ch === '/' && code[i + 1] === '*') {
i += 2
while (i < len && !(code[i] === '*' && code[i + 1] === '/'))
i++
i += 2
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Regex literals may be incorrectly parsed as division or comments.

The minifier doesn't distinguish regex literals from division operators. Code like:

const re = /foo/ // comment

Would work, but:

if (/test/.test(x)) {}

The /test/ might be mishandled depending on preceding context. More critically:

x = y / /* comment */ z

Would strip the comment correctly, but there's no regex-vs-division disambiguation which could cause issues with patterns like /pattern/g.

This is a known limitation of lightweight minifiers. Consider documenting that complex regex patterns may require the heavier esbuild-based minifier.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/src/minify/js.ts` around lines 27 - 38, The comment-handling
code that checks ch === '/' and code[i + 1] for '//' and '/*' does not
distinguish regex literals from division operators; update the scanner around
the '/' handling (the blocks that use variables ch, code and index i) to first
determine whether a regex literal is valid in the current parsing context by
inspecting the previous significant token/character (skip whitespace/comments) β€”
allow regex starts after tokens like '(', ',', '=', ':', '[', '!', '?', '{',
'return', 'case', 'throw', 'in', 'of', 'typeof' etc.; if a regex is valid, parse
the regex literal (handle escapes and character classes until the closing '/'
and optional flags) instead of treating it as a comment or division, otherwise
continue with the existing single-line (//) and multi-line (/* */) comment
logic; if you prefer not to implement heuristics, add a clear note in the
minifier documentation warning about this regex-vs-division limitation.

Comment thread packages/unhead/src/minify/json.ts Outdated
Comment on lines +4 to +5
export function minifyJSON(code: string): string {
return JSON.stringify(JSON.parse(code))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In JavaScript, can JSON.parse+JSON.stringify change numeric values (for example integers larger than Number.MAX_SAFE_INTEGER), and what are safe approaches for whitespace-only JSON minification without semantic changes?

πŸ’‘ Result:

Yes, JSON.parse + JSON.stringify can change numeric values, specifically integers larger than Number.MAX_SAFE_INTEGER (9007199254740991). JavaScript Numbers use IEEE 754 double-precision floating-point, which loses precision beyond this limit. JSON.parse converts JSON numbers to Numbers, causing rounding/truncation for large integers. JSON.stringify then serializes the imprecise Number back to JSON. Example: JSON.parse(JSON.stringify({id: 9007199254740993})) results in {id: 9007199254740992}. For safe whitespace-only JSON minification (removing spaces, newlines, tabs between tokens without altering data): - Parse with JSON.parse, then stringify with JSON.stringify(obj) (no space parameter). This produces minimal valid JSON, removing all unnecessary whitespace while preserving semantics and numeric precision (subject to JS Number limits). This approach is safe for whitespace-only changes as it follows JSON spec grammar where whitespace is optional outside strings. Alternatives like regex are unsafe as they risk corrupting strings or structure; always parse/stringify for structural data.

Citations:


🏁 Script executed:

# First, verify the exact code in the file
cat -n packages/unhead/src/minify/json.ts | head -20

Repository: unjs/unhead

Length of output: 253


🏁 Script executed:

# Look for tests related to minifyJSON
find . -type f \( -name "*.test.*" -o -name "*.spec.*" \) -exec grep -l "minifyJSON" {} \;

Repository: unjs/unhead

Length of output: 97


🏁 Script executed:

# Search for any usage or documentation of minifyJSON in the codebase
rg -i "minifyJSON|minify.*json" --type ts --type js -B 2 -A 2 | head -50

Repository: unjs/unhead

Length of output: 3154


🏁 Script executed:

# Check if there's any mention of JSON number handling or precision in the file or related files
rg -i "precision|safe.*int|MAX_SAFE|number.*mutate" packages/unhead/src/minify/ -B 2 -A 2

Repository: unjs/unhead

Length of output: 37


minifyJSON silently mutates numeric values beyond JavaScript's safe integer range.

The implementation parses then re-serializes JSON, which converts numbers to JavaScript's Number type (IEEE 754 double-precision). This loses precision for integers beyond Number.MAX_SAFE_INTEGER (9007199254740991). For example, 9007199254740993 becomes 9007199254740992.

While the current test suite only covers basic cases with small numbers, any JSON payload with large numeric values (common in APIs and structured data like LD+JSON) will be silently corrupted. For a minification utility, this is a correctness risk.

Consider documenting this limitation or implementing safe number handling (e.g., preserving numeric strings or using a library that handles bigint).

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/src/minify/json.ts` around lines 4 - 5, minifyJSON currently
parses then re-serializes which converts numeric tokens to JS Number and
silently corrupts integers > Number.MAX_SAFE_INTEGER; update the minifyJSON
implementation to avoid numeric coercion by either (a) using a token-based
minifier that strips whitespace only (preserving numeric strings) instead of
JSON.parse/JSON.stringify, or (b) use a bigint-aware JSON library (e.g.,
json-bigint or similar) to parse/stringify and preserve large integers, or (c)
at minimum detect large integer literals during minification and leave them as
strings or passthrough tokens; modify the function named minifyJSON accordingly
and add a brief comment/docstring about the chosen behavior so callers know how
large integers are handled.

Comment on lines +66 to +73
if (type && JSON_TYPES.has(type)) {
if (jsonMinify) {
const min = minifyJSON(content)
if (min.length < content.length)
tag.innerHTML = min
}
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unhandled exception from minifyJSON can crash SSR render.

minifyJSON calls JSON.parse(code) which throws SyntaxError on malformed JSON. If a script tag with type="application/ld+json" contains invalid JSON (possible from user content, template bugs, or dynamic data), the entire SSR render will crash.

Proposed fix: wrap in try/catch
            if (type && JSON_TYPES.has(type)) {
              if (jsonMinify) {
-               const min = minifyJSON(content)
-               if (min.length < content.length)
-                 tag.innerHTML = min
+               try {
+                 const min = minifyJSON(content)
+                 if (min.length < content.length)
+                   tag.innerHTML = min
+               }
+               catch {
+                 // leave invalid JSON unchanged
+               }
              }
              continue
            }
🧰 Tools
πŸͺ› ast-grep (0.41.1)

[warning] 69-69: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: tag.innerHTML = min
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 69-69: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: tag.innerHTML = min
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/src/plugins/minify.ts` around lines 66 - 73, The call to
minifyJSON inside the JSON_TYPES branch can throw (JSON.parse) and crash SSR;
wrap the minifyJSON(content) call in a try/catch so malformed JSON is ignored
(or left unchanged) instead of throwing, e.g. catch and skip assigning
tag.innerHTML when an error occurs, keeping the existing jsonMinify and continue
logic; reference minifyJSON, JSON_TYPES, tag.innerHTML and the jsonMinify flag
to locate where to add the try/catch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/addons/src/unplugin/MinifyTransform.ts (2)

242-254: Template literal gets converted to string literal.

When minifying a template literal (backticks), the replacement uses JSON.stringify(minified) which produces a double-quoted string literal. This is semantically equivalent but changes the code style. Ensure this is the intended behavior, especially if the original template literal relied on different escaping rules (e.g., unescaped single quotes).

If preserving template literal syntax is desired:

♻️ Alternative to preserve template literal syntax
       else if (prop.value?.type === 'TemplateLiteral' && prop.value.expressions.length === 0) {
         const raw = prop.value.quasis[0]?.value?.cooked as string
         if (!raw || raw.length < 20)
           continue

         pendingMinifications.push(
           minifyStringContent(raw, tagType).then((minified) => {
             if (minified && minified.length < raw.length) {
-              s.overwrite(prop.value.start, prop.value.end, JSON.stringify(minified))
+              // Escape backticks and ${} for template literal preservation
+              const escaped = minified.replace(/`/g, '\\`').replace(/\$\{/g, '\\${')
+              s.overwrite(prop.value.start, prop.value.end, `\`${escaped}\``)
             }
           }),
         )
       }
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/addons/src/unplugin/MinifyTransform.ts` around lines 242 - 254, The
current replacement turns a TemplateLiteral into a double-quoted string because
you call JSON.stringify(minified) when prop.value.type === 'TemplateLiteral' in
MinifyTransform.ts; to preserve original template-literal syntax (since there
are no expressions) change the overwrite to write a backtick-wrapped literal
instead of JSON.stringify: build a string that escapes backticks, backslashes
and any "${" sequences in the minified text and then call
s.overwrite(prop.value.start, prop.value.end, '`' + escapedMinified + '`'); keep
using the existing minifyStringContent, pendingMinifications and prop/value
checks and only switch to backtick-escaping logic when prop.value.type ===
'TemplateLiteral' and prop.value.expressions.length === 0.

47-61: Consider logging minifier initialization failures for debuggability.

The empty catch blocks silently swallow errors, making it difficult to diagnose why minification might not be working (e.g., missing dependencies, version mismatches). A debug-level log would help users troubleshoot.

♻️ Proposed improvement
       try {
         const rolldownPath = 'rolldown/experimental'
         const { minify } = await import(/* `@vite-ignore` */ rolldownPath) as { minify: (filename: string, code: string) => Promise<{ code: string }> }
         const result = await minify('inline.js', code)
         return result.code.trim()
       }
-      catch {}
+      catch (e) {
+        // rolldown not available, will try esbuild fallback
+      }
       // esbuild fallback (Vite 7)
       try {
         const esbuild = await import('esbuild' as string) as { transform: (code: string, opts: { minify: boolean, loader: string }) => Promise<{ code: string }> }
         const result = await esbuild.transform(code, { minify: true, loader: 'js' })
         return result.code.trim()
       }
-      catch {}
+      catch (e) {
+        // esbuild not available either
+      }
       return null
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/addons/src/unplugin/MinifyTransform.ts` around lines 47 - 61, The
two empty catch blocks in MinifyTransform.ts silently swallow errors for the
dynamic imports and minification attempts (rolldownPath/minify and
esbuild.transform); update both catch blocks to log the caught Error at debug
level (include a descriptive message and the error object), e.g., console.debug
or the module's logger, so failures to import or run
rolldown/experimental.minify and esbuild.transform are recorded with context
(mentioning the symbol names minify and esbuild.transform and the rolldownPath)
before proceeding to the next fallback or returning null.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/addons/src/unplugin/MinifyTransform.ts`:
- Around line 48-51: Update the dynamic import to use 'rolldown/utils' (replace
rolldownPath value) and change the type assertion for the imported minify to the
full MinifyResult signature: (filename: string, code: string) => Promise<{ code:
string; errors: OxcError[]; map?: SourceMap }>; after calling
minify('inline.js', code) check result.errors and surface them (e.g., throw or
return a rejected Error containing the errors) instead of ignoring them, then
return result.code.trim() when there are no errors; reference symbols:
rolldownPath, minify, result, and the OxcError/MinifyResult shape.

---

Nitpick comments:
In `@packages/addons/src/unplugin/MinifyTransform.ts`:
- Around line 242-254: The current replacement turns a TemplateLiteral into a
double-quoted string because you call JSON.stringify(minified) when
prop.value.type === 'TemplateLiteral' in MinifyTransform.ts; to preserve
original template-literal syntax (since there are no expressions) change the
overwrite to write a backtick-wrapped literal instead of JSON.stringify: build a
string that escapes backticks, backslashes and any "${" sequences in the
minified text and then call s.overwrite(prop.value.start, prop.value.end, '`' +
escapedMinified + '`'); keep using the existing minifyStringContent,
pendingMinifications and prop/value checks and only switch to backtick-escaping
logic when prop.value.type === 'TemplateLiteral' and
prop.value.expressions.length === 0.
- Around line 47-61: The two empty catch blocks in MinifyTransform.ts silently
swallow errors for the dynamic imports and minification attempts
(rolldownPath/minify and esbuild.transform); update both catch blocks to log the
caught Error at debug level (include a descriptive message and the error
object), e.g., console.debug or the module's logger, so failures to import or
run rolldown/experimental.minify and esbuild.transform are recorded with context
(mentioning the symbol names minify and esbuild.transform and the rolldownPath)
before proceeding to the next fallback or returning null.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 157510f4-ae75-47a8-a55e-5ca3d292596f

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 48a78af and baa3263.

πŸ“’ Files selected for processing (2)
  • packages/addons/src/unplugin/MinifyTransform.ts
  • packages/unhead/src/server/createHead.ts

Comment on lines +48 to +51
const rolldownPath = 'rolldown/experimental'
const { minify } = await import(/* @vite-ignore */ rolldownPath) as { minify: (filename: string, code: string) => Promise<{ code: string }> }
const result = await minify('inline.js', code)
return result.code.trim()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🌐 Web query:

rolldown experimental minify function API signature 2026

πŸ’‘ Result:

The experimental minify function in Rolldown is an asynchronous utility exported from 'rolldown/utils' for minifying JavaScript code using the Oxc minifier. API Signature: minify(filename: string, sourceText: string, options?: MinifyOptions | null): Promise<MinifyResult> There is also a synchronous variant: minifySync(filename: string, sourceText: string, options?: MinifyOptions | null): MinifyResult MinifyOptions includes properties like: - codegen?: boolean | CodegenOptions - compress?: boolean | CompressOptions - mangle?: boolean | MangleOptions - module?: boolean - sourcemap?: boolean - inputMap?: SourceMap MinifyResult includes: - code: string - errors: OxcError[] - map?: SourceMap This function is marked as experimental in the official Rolldown documentation as of 2026. Note that the async version may be slower due to thread spawning overhead. It is distinct from the output.minify configuration option used during bundling.

Citations:


Fix incorrect import path and incomplete type assertion for rolldown minify API.

The import should be from 'rolldown/utils', not 'rolldown/experimental'. Additionally, the type assertion is incompleteβ€”MinifyResult includes an errors: OxcError[] property that the code currently ignores, which means minification errors are silently discarded. The full return type is Promise<{ code: string; errors: OxcError[]; map?: SourceMap }>.

Current code
        const rolldownPath = 'rolldown/experimental'
        const { minify } = await import(/* `@vite-ignore` */ rolldownPath) as { minify: (filename: string, code: string) => Promise<{ code: string }> }
        const result = await minify('inline.js', code)
        return result.code.trim()
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/addons/src/unplugin/MinifyTransform.ts` around lines 48 - 51, Update
the dynamic import to use 'rolldown/utils' (replace rolldownPath value) and
change the type assertion for the imported minify to the full MinifyResult
signature: (filename: string, code: string) => Promise<{ code: string; errors:
OxcError[]; map?: SourceMap }>; after calling minify('inline.js', code) check
result.errors and surface them (e.g., throw or return a rejected Error
containing the errors) instead of ignoring them, then return result.code.trim()
when there are no errors; reference symbols: rolldownPath, minify, result, and
the OxcError/MinifyResult shape.

@harlan-zw harlan-zw changed the title feat: add MinifyPlugin for inline script/style minification feat: add MinifyPlugin for inline script/style minification Mar 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/addons/src/unplugin/types.ts`:
- Line 1: Remove the unused import symbol MinifyFn from the import statement in
types.ts so only MinifyTransformOptions is imported (i.e., update the import
line that currently reads import type { MinifyFn, MinifyTransformOptions } from
'./MinifyTransform' to import only MinifyTransformOptions); this eliminates the
unused symbol and any linter warning while keeping the referenced
MinifyTransformOptions type for the rest of the file.

In `@packages/addons/test/minifyTransform.test.ts`:
- Around line 1-4: The imports in minifyTransform.test.ts violate the ESLint
import-order rule: place type-only imports before value imports; move the type
import for MinifyFn (from '../src/unplugin/MinifyTransform') so it appears
before the value import of MinifyTransform, i.e., import the type first using
the "import type { MinifyFn } ..." statement and then import the value export
"MinifyTransform" afterwards to satisfy the rule.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad70bf70-d8a6-4533-8444-0f036c9ef556

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between baa3263 and d453ce3.

β›” Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (12)
  • packages/addons/build.config.ts
  • packages/addons/package.json
  • packages/addons/src/minify/esbuild.ts
  • packages/addons/src/minify/lightningcss.ts
  • packages/addons/src/minify/rolldown.ts
  • packages/addons/src/unplugin/MinifyTransform.ts
  • packages/addons/src/unplugin/types.ts
  • packages/addons/src/unplugin/vite.ts
  • packages/addons/src/unplugin/webpack.ts
  • packages/addons/test/minify.test.ts
  • packages/addons/test/minifyTransform.test.ts
  • test/exports/addons.yaml
βœ… Files skipped from review due to trivial changes (1)
  • test/exports/addons.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/addons/src/unplugin/webpack.ts
  • packages/addons/src/unplugin/vite.ts
  • packages/addons/build.config.ts

Comment thread packages/addons/src/unplugin/types.ts Outdated
Comment thread packages/addons/test/minifyTransform.test.ts Outdated
This was referenced Apr 5, 2026
harlan-zw and others added 4 commits April 6, 2026 03:07
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.
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]>
- 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]>
@harlan-zw harlan-zw force-pushed the feat/minify-plugin branch from a4e2e15 to d21e2c0 Compare April 5, 2026 17:14
@harlan-zw harlan-zw merged commit f1f5d37 into main Apr 5, 2026
6 checks passed
@harlan-zw harlan-zw mentioned this pull request Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run inline scripts through transformWithEsbuild() with a Vite plugin

1 participant