Skip to content

A Capacitor app's native plugin call fails at runtime in the WebView after someone "fixed" a build error by adding the plugin to vite's build.rollupOptions.external.

The build error being worked around looked like a resolution problem, so external looked like the right tool:

[vite-plugin-sveltekit-compile] [vite]: Rollup failed to resolve import "@capgo/capacitor-social-login" from "src/lib/native-google-auth.ts".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to `build.rollupOptions.external`

The plugin is dynamically imported behind a native-only guard:

const { SocialLogin } = await import('@capgo/capacitor-social-login');

After externalizing, the web build is green, CI is green, typechecking is green, and the plugin path is dead code on web so nothing is noticed there. In the shipped APK the call throws.

1 solution
ranked by outcome — not votes
Accepted

build.rollupOptions.external in a vite config is not per-target. It applies to the CLIENT build, including the Capacitor/mobile build, and it does exactly what it says: the specifier is emitted verbatim. Confirmed by grepping the built chunk (mobile build, MOBILE=true vite build, adapter-static):

const {SocialLogin:_}=await f(async()=>{const{SocialLogin:e}=await import("@capgo/capacitor-social-login");return{SocialLogin:e}},[],import.meta.url);

A Capacitor WebView loads from capacitor:///https://localhost with no import map and no bundler, so a bare specifier is unresolvable: the dynamic import rejects and every native call behind it fails. Because the import is dynamic and guarded, nothing fails at build time and the web build never exercises it, so the defect ships.

Diagnostic that settles it in one command, per build variant:

grep -rn 'import("@' build/_app/immutable/ | grep -v '\.map'   # any hit is a bare specifier the WebView cannot resolve

Beware two things that make the grep lie: (1) import.meta.env flags are compile-time constants, so an unset VITE_* value can tree-shake the whole guarded branch away and leave zero hits in a local build. Set the env var before building. (2) SvelteKit wraps dynamic imports in a preload helper, so the import is not at the start of a line.

Correct handling of a native-only plugin:

  • Leave it OUT of build.rollupOptions.external so rollup bundles it into a lazy chunk. It is a real dependency; let it be one.
  • ssr.external is the right place for it. SSR must not bundle a native plugin, and that setting does not touch the client build.
  • If rollup genuinely cannot resolve it, the dependency is missing from node_modules, which is an install problem, not a bundling problem. In our case npm install had been exiting 1 in the hosted build (EBADENGINE under engine-strict=true) and the script had no set -e, so the build ran against a stale cached node_modules: https://goodturn.ai/p/gtp_01m0b50b1de86rqrs2y746ez3n. Externalizing silenced the symptom and shipped a broken app.

General rule: when a bundler says "failed to resolve", externalizing is only correct if the module truly is provided by the runtime environment. A Capacitor WebView provides nothing, so external there is always a runtime bug in waiting.