feat: add MinifyPlugin for inline script/style minification#705
Conversation
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: β Files ignored due to path filters (1)
π Files selected for processing (30)
π WalkthroughWalkthroughAdds build-time and SSR minification: a Vite/Webpack unplugin to pre-minify inline content in Changes
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
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
Estimated code review effortπ― 4 (Complex) | β±οΈ ~60 minutes Poem
π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing Touchesπ§ͺ Generate unit tests (beta)
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. Comment |
Bundle Size Analysis
|
There was a problem hiding this comment.
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 tocreateUnhead) and again at line 50. The first renderer is immediately discarded whenhead.renderis overridden at line 54.Consider passing a no-op or lightweight stub to
createUnheadsince 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.
parseSyncfrom 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
includefirst (returnstrue) beforeexclude(returnsfalse). 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
MinifyTransformOptionsinterface.π€ 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 forimportmaptype.The
SKIP_JS_TYPESset includes bothspeculationrulesandimportmap, but onlyspeculationrulesis tested. Adding a similar test forimportmapwould 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
minifyJSONthrows on invalid JSON (and the related issue flagged inminify.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
π Files selected for processing (21)
packages/addons/build.config.tspackages/addons/src/unplugin/MinifyTransform.tspackages/addons/src/unplugin/types.tspackages/addons/src/unplugin/vite.tspackages/addons/src/unplugin/webpack.tspackages/unhead/build.config.tspackages/unhead/package.jsonpackages/unhead/src/minify/css.tspackages/unhead/src/minify/index.tspackages/unhead/src/minify/js.tspackages/unhead/src/minify/json.tspackages/unhead/src/plugins/index.tspackages/unhead/src/plugins/minify.tspackages/unhead/src/server/createHead.tspackages/unhead/test/unit/minify.test.tspackages/unhead/test/unit/plugins/minify.test.tstest/exports/react.yamltest/exports/solid-js.yamltest/exports/svelte.yamltest/exports/unhead.yamltest/exports/vue.yaml
| 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 | ||
| } |
There was a problem hiding this comment.
π§© 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 -20Repository: 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=10Repository: unjs/unhead
Length of output: 20871
π Script executed:
#!/bin/bash
# Examine the full minify function implementation
cat packages/unhead/src/minify/js.tsRepository: unjs/unhead
Length of output: 2158
π Script executed:
#!/bin/bash
# Examine the minify test file
wc -l packages/unhead/test/unit/plugins/minify.test.tsRepository: unjs/unhead
Length of output: 107
π Script executed:
#!/bin/bash
# Look at minify test content
cat packages/unhead/test/unit/plugins/minify.test.tsRepository: 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 -30Repository: 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 -20Repository: 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 jsRepository: 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 -40Repository: 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 2Repository: 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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/ // commentWould work, but:
if (/test/.test(x)) {}The /test/ might be mishandled depending on preceding context. More critically:
x = y / /* comment */ zWould 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.
| export function minifyJSON(code: string): string { | ||
| return JSON.stringify(JSON.parse(code)) |
There was a problem hiding this comment.
π§© 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:
- 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
- 2: https://sqlpey.com/javascript/javascript-number-precision-json/
- 3: https://stackoverflow.com/questions/63312821/why-json-number-changed-when-do-json-parse-a-string
- 4: https://stackoverflow.com/questions/25676165/javascript-how-do-i-remove-all-the-white-spaces-from-a-json-string-except-the-o
π Script executed:
# First, verify the exact code in the file
cat -n packages/unhead/src/minify/json.ts | head -20Repository: 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 -50Repository: 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 2Repository: 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.
| if (type && JSON_TYPES.has(type)) { | ||
| if (jsonMinify) { | ||
| const min = minifyJSON(content) | ||
| if (min.length < content.length) | ||
| tag.innerHTML = min | ||
| } | ||
| continue | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
π Files selected for processing (2)
packages/addons/src/unplugin/MinifyTransform.tspackages/unhead/src/server/createHead.ts
| 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() |
There was a problem hiding this comment.
π§© 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:
- 1: https://rolldown.rs/reference/function.minify
- 2: https://rolldown.rs/reference/Function.minify
- 3: https://rolldown.rs/reference/Interface.MinifyOptions
- 4: https://rolldown.rs/reference/Function.minifySync
- 5: https://rolldown.rs/reference/Interface.MinifyResult
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.
MinifyPlugin for inline script/style minification
There was a problem hiding this comment.
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
β Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
π Files selected for processing (12)
packages/addons/build.config.tspackages/addons/package.jsonpackages/addons/src/minify/esbuild.tspackages/addons/src/minify/lightningcss.tspackages/addons/src/minify/rolldown.tspackages/addons/src/unplugin/MinifyTransform.tspackages/addons/src/unplugin/types.tspackages/addons/src/unplugin/vite.tspackages/addons/src/unplugin/webpack.tspackages/addons/test/minify.test.tspackages/addons/test/minifyTransform.test.tstest/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
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]>
a4e2e15 to
d21e2c0
Compare
π Linked issue
Resolves #525
β Type of 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 (
MinifyPluginfromunhead/plugins): Hooks intossr:renderto minifyHeadTag.innerHTMLbefore serialization. Uses lightweight pure-JS minifiers with zero native dependencies (safe for edge/serverless). Handles dynamic content likeuseHead()injections, hydration payloads, JSON-LD. Accepts custom minifier functions for users who want heavier optimization.Build-time Vite/Webpack transform (
MinifyTransformin@unhead/addons): AST-walksuseHead()/useServerHead()calls and pre-minifies staticinnerHTML/textContentstring 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 Vitetransformhook.Standalone utilities (
unhead/minify): ExportsminifyJS,minifyCSS,minifyJSONfor framework build pipelines to consume directly.Also fixes a bug where SSR hooks (
ssr:render,ssr:beforeRender,ssr:rendered) never fired because the servercreateHeadrenderer closure captured the inner core object (without hooks) instead of the outer head wrapper.Usage
Summary by CodeRabbit
New Features
Tests