Skip to content

fix(bundler): preserve useSeoMeta import when sibling call is untransformable#749

Merged
harlan-zw merged 1 commit into
mainfrom
fix/seo-meta-transform-preserve-import
Apr 22, 2026
Merged

fix(bundler): preserve useSeoMeta import when sibling call is untransformable#749
harlan-zw merged 1 commit into
mainfrom
fix/seo-meta-transform-preserve-import

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

UseSeoMetaTransform stripped useSeoMeta from the import as soon as any call got rewritten to useHead, so an untransformable sibling call (dynamic first arg, spread properties) threw ReferenceError: useSeoMeta is not defined at runtime. This PR tracks untransformable call sites and keeps the original named import when either a value reference or an untransformable call still needs it, with a sandboxed e2e test that only binds names the rewritten import declares so regressions surface as real ReferenceErrors rather than slipping past snapshot assertions.

…formable

`UseSeoMetaTransform` rewrote the static call to `useHead(...)` and stripped
`useSeoMeta` from the import, even when another call site (dynamic first arg,
spread properties, no args) kept using `useSeoMeta(...)`. The bundle then
threw `ReferenceError: useSeoMeta is not defined` at runtime.

Track untransformable call sites alongside transformable ones, and keep the
original named import when either a value reference *or* an untransformable
call requires it.

Adds a runtime sandbox in the test that only binds names the rewritten import
declares, so regressions surface as real `ReferenceError`s instead of passing
silently under snapshot assertions.
@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The useSeoMeta transformation logic now tracks call sites that cannot be statically transformed into useHead, preserving their original imports while rewriting only the transformable calls.

Changes

Cohort / File(s) Summary
Transform Logic
packages/bundler/src/unplugin/UseSeoMetaTransform.ts
Added untransformedCallees set to track useSeoMeta/useServerSeoMeta calls that fail transformation or lack static first-argument properties. Updated import-specifier rewriting to preserve the original imported name when referenced as a value or when the call couldn't be transformed.
Test Coverage
packages/bundler/test/useSeoMetaTransform.test.ts
Introduced runTransformed() helper for executing and spying on transformed code. Added regression tests verifying mixed transformable/untransformable scenarios: both spread-property patterns and non-static arguments are handled correctly, with appropriate imports retained alongside useHead.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 When callees resist transformation's dance,
We track their untamed circumstance,
Some morph to useHead with graceful ease,
While useSeoMeta stays to please—
Transformation respects what cannot change! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 accurately describes the main bug fix: preserving the useSeoMeta import when a sibling call cannot be statically transformed, which directly addresses the core issue in the changeset.
Description check ✅ Passed The description provides clear context (before/after example), explains the root cause and solution, includes a comprehensive test plan, and covers the motivation. However, it does not follow the provided template structure with checkboxes for type of change, and lacks a linked issue section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/seo-meta-transform-preserve-import

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

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.7 kB 4.4 kB
Server (Minimal) 10.6 kB 4.3 kB
Vue Client (Minimal) 11.8 kB 4.9 kB
Vue Server (Minimal) 11.6 kB 4.7 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bundler/src/unplugin/UseSeoMetaTransform.ts (1)

270-295: ⚠️ Potential issue | 🟡 Minor

Pre-existing limitation worth noting: aliased imports and multi-source imports.

The preservation logic at lines 282–283 keys both valueReferenced and untransformedCallees by the original imported name. Two edge cases remain unaddressed:

  1. Aliased imports — for import { useSeoMeta as SEOMETA } from 'unhead' with an untransformable SEOMETA(dynamic) call, the code re-adds useSeoMeta (the original imported name) to the specifier list rather than preserving the useSeoMeta as SEOMETA binding. The call site would still throw ReferenceError: SEOMETA is not defined. The existing "alt import as name" test covers only transformable cases.
  2. Multiple imports of the same name from different sources — e.g., useSeoMeta from both unhead and @unhead/vue collapse to a single name in the set, so an untransformable call from one source can cause over-preservation on the other's import.

Neither is introduced by this PR — flagging only so it can be tracked / guarded with a follow-up test if desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bundler/src/unplugin/UseSeoMetaTransform.ts` around lines 270 - 295,
The current preservation logic in the import rewrite loop (importRewrites,
importNode, newSpecifiers, s.overwrite) uses valueReferenced and
untransformedCallees keyed by the original imported name, which breaks aliased
imports and multi-source imports; update the check to key by the local binding
name (spec.local.name) and preserve alias syntax (emit "importedName as
localName" when spec.local.name !== spec.imported.name) so that an
untransformable call referencing an alias (e.g., SEOMETA) re-adds the correct
aliased specifier instead of just the original name, and include the import
source module (e.g., attach importNode.source.value) in the tracking keys or use
a composite key [source, localName] to avoid collapsing identically named
imports from different modules.
🧹 Nitpick comments (1)
packages/bundler/test/useSeoMetaTransform.test.ts (1)

17-39: Nice regression harness, but note the 'unhead'-only source assumption.

runTransformed only recognizes import { ... } from 'unhead' via the regex on line 23 and is only consumed by the new test on line 113, which imports from 'unhead' — so this is correct for the new tests. Worth being explicit that this helper is not usable for the @unhead/vue / #imports tests if someone tries to repurpose it later. Also, imported can contain aliased entries like useHead as x; those are silently dropped because name in spies would be false, which is the right behavior for current tests but another subtle assumption.

Minor, non-blocking — either a short doc comment on the helper or tightening the filter would be enough.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bundler/test/useSeoMetaTransform.test.ts` around lines 17 - 39, The
runTransformed helper currently only matches "import { ... } from 'unhead'" via
the importMatch regex and drops aliased imports (e.g., "useHead as x") because
imported entries are filtered by simple name presence in the spies map; update
runTransformed to either (a) add a short doc comment above the function
explaining the unhead-only assumption and that aliased imports are intentionally
ignored, or (b) tighten parsing by normalizing imported names (split on "as" and
trim the original specifier) so aliases map back to the spy keys before building
params/args; reference runTransformed, importMatch, imported and spies when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/bundler/src/unplugin/UseSeoMetaTransform.ts`:
- Around line 270-295: The current preservation logic in the import rewrite loop
(importRewrites, importNode, newSpecifiers, s.overwrite) uses valueReferenced
and untransformedCallees keyed by the original imported name, which breaks
aliased imports and multi-source imports; update the check to key by the local
binding name (spec.local.name) and preserve alias syntax (emit "importedName as
localName" when spec.local.name !== spec.imported.name) so that an
untransformable call referencing an alias (e.g., SEOMETA) re-adds the correct
aliased specifier instead of just the original name, and include the import
source module (e.g., attach importNode.source.value) in the tracking keys or use
a composite key [source, localName] to avoid collapsing identically named
imports from different modules.

---

Nitpick comments:
In `@packages/bundler/test/useSeoMetaTransform.test.ts`:
- Around line 17-39: The runTransformed helper currently only matches "import {
... } from 'unhead'" via the importMatch regex and drops aliased imports (e.g.,
"useHead as x") because imported entries are filtered by simple name presence in
the spies map; update runTransformed to either (a) add a short doc comment above
the function explaining the unhead-only assumption and that aliased imports are
intentionally ignored, or (b) tighten parsing by normalizing imported names
(split on "as" and trim the original specifier) so aliases map back to the spy
keys before building params/args; reference runTransformed, importMatch,
imported and spies when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ac0048b-3fab-4007-af76-5aa6e8fffb12

📥 Commits

Reviewing files that changed from the base of the PR and between 0c0feee and 8ea4834.

📒 Files selected for processing (2)
  • packages/bundler/src/unplugin/UseSeoMetaTransform.ts
  • packages/bundler/test/useSeoMetaTransform.test.ts

@harlan-zw harlan-zw merged commit f717645 into main Apr 22, 2026
8 checks passed
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.

1 participant