Skip to content

fix(dev): support plugin-provided vendor prebundle externals#24

Open
kosciak9 wants to merge 1 commit into
elixir-volt:masterfrom
kosciak9:fix/react-prebundle-externals
Open

fix(dev): support plugin-provided vendor prebundle externals#24
kosciak9 wants to merge 1 commit into
elixir-volt:masterfrom
kosciak9:fix/react-prebundle-externals

Conversation

@kosciak9
Copy link
Copy Markdown

Summary

Adds a Volt plugin hook for dev vendor prebundling externals.

This lets framework/integration plugins declare bare imports that should remain external while third-party vendor packages are prebundled, then rewrites those preserved imports back to Volt vendor URLs.

Use case

In a Phoenix + Volt React app, we use:

  • React
  • TanStack Router / Query / DB
  • Base UI via shadcn
  • lucide-react

Volt currently prebundles each vendor package independently in dev. Packages like @base-ui/react, @tanstack/react-query, and lucide-react may inline their own React copy during vendor prebundling. That produced duplicate React singletons and runtime errors such as:

  • Invalid hook call. Hooks can only be called inside of the body of a function component.
  • Cannot read properties of null (reading 'useRef')
  • Cannot read properties of null (reading 'useEffect')
  • Cannot read properties of null (reading 'useContext')

The app already had a plugin that canonicalized React-family imports to one /@vendor/react.js module, but vendor prebundling had no way to keep React external inside other prebundled vendor packages.

What changed

  • Adds optional Volt.Plugin.prebundle_externals/0
  • Adds Volt.PluginRunner.prebundle_externals/1
  • Passes plugin-provided externals to OXC.bundle/2 during dev vendor prebundling
  • Rewrites preserved external imports to canonical Volt vendor URLs using prebundle_alias/1
  • Avoids self-externalizing the canonical prebundle entry itself
  • Includes prebundle externals in the vendor browser/cache signature

Tried before this

Before adding this hook, we tried app-side workarounds:

  • Aliasing React-related packages through app config
  • Canonicalizing React-family imports with prebundle_alias/1
  • Creating a synthetic React vendor entry via prebundle_entry/1

Those helped top-level app imports, but did not prevent third-party vendor bundles from inlining React internally. Replacing Base UI/shadcn primitives with native React elements would have avoided the symptom, but was not acceptable because the goal is to support real Base UI/shadcn usage.

Verification

Verified in the downstream app:

  • @base-ui/react, TanStack, Floating UI, and lucide-react vendor bundles now import React from the singleton /@vendor/react.js?...
  • Page renders with Base UI/shadcn components
  • No React invalid-hook-call runtime errors
  • Browser console has 0 errors / 0 warnings

Verified in this repo:

  • mix compile --warnings-as-errors
  • MIX_ENV=test mix test

AI usage disclosure

This patch was developed with AI assistance. The AI helped investigate the duplicate React runtime issue, compare attempted solutions, draft the implementation, and run validation steps. The code and behavior were manually reviewed and verified with the test commands above. But I'm not very familiar with how Volt works, so I cannot take full responsibility for it.

@dannote
Copy link
Copy Markdown
Member

dannote commented May 30, 2026

Thanks for digging into this — the hook is the right shape, but I think this needs one more pass before merge. The new callback isn’t wired into Volt.Plugin.React, so the React duplicate-singleton case described here still won’t be fixed for normal Volt React apps unless users add a custom plugin. Could you add React’s common entrypoints to prebundle_externals/0 in the built-in React plugin, plus tests that assert a third-party vendor importing react is preserved and rewritten to the canonical /@vendor/react.js?v=... URL? Also, please avoid the string-replacement fallback in rewrite_external_import_reference_literals/2; Volt tries to keep source transformations parser-backed, and this can rewrite comments/string literals while still missing valid import syntax with whitespace/newlines. Since Volt.JS.Transforms.Imports.rewrite/3 already handles imports via OXC AST positions, we should rely on that path or fail/skip the rewrite when parsing fails.

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.

2 participants