Fix native build: access Camera via runtime plugin, not @capacitor/camera import

Local CAPACITOR_BUILD failed: Rollup couldn't resolve import
"@capacitor/camera" from CameraCapture.tsx — the package is listed in
package.json but isn't present in node_modules on the build machine.

Root pattern (same class as the @capacitor/core crash): a package that
must NOT be resolved at build time was being imported by specifier.

Fix:
- CameraCapture.tsx no longer imports '@capacitor/camera' at all. The
  Camera plugin is accessed via the runtime-injected
  window.Capacitor.Plugins.Camera (Capacitor injects it natively), using
  string enum values ('dataUrl' / 'CAMERA'). No bare specifier remains in
  the bundle, and it degrades gracefully if the plugin is absent.
- vite.config: @capacitor/camera moved back to `external` (belt-and-braces;
  it's no longer imported anyway). @capacitor/core stays BUNDLED (static
  import in platform.ts — the white-screen fix). nostr-signer-capacitor-plugin
  and the 4 uninstalled @capacitor/* plugins remain external.

Verified: no @capacitor/camera specifier and no unresolved @capacitor/core
import in the built bundle; no static imports of any externalized package
remain in src/. Bump to 1.0.67.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-24 18:40:19 +07:00
parent ac362a9a10
commit 3a96184001
3 changed files with 45 additions and 18 deletions

View file

@ -1,7 +1,7 @@
{
"name": "skate-spots",
"private": true,
"version": "1.0.66",
"version": "1.0.67",
"type": "module",
"homepage": "https://skate-spots.com",
"scripts": {

View file

@ -42,11 +42,34 @@ async function isNative(): Promise<boolean> {
}
}
// The Camera plugin is provided by Capacitor at runtime on native. We access it
// through the runtime-injected `window.Capacitor.Plugins.Camera` rather than a
// static/dynamic `import('@capacitor/camera')`, so the bundle contains no bare
// `@capacitor/camera` specifier (which a native WebView can't resolve, and which
// isn't installed on every build machine). CameraResultType/CameraSource are
// plain string enums in the plugin, so we pass their string values directly.
interface CapacitorCameraPlugin {
getPhoto(options: {
resultType: string;
source: string;
quality: number;
correctOrientation: boolean;
}): Promise<{ dataUrl?: string }>;
}
function getNativeCamera(): CapacitorCameraPlugin | null {
const cap = (window as unknown as {
Capacitor?: { Plugins?: { Camera?: CapacitorCameraPlugin } };
}).Capacitor;
return cap?.Plugins?.Camera ?? null;
}
async function captureNative(): Promise<File | null> {
const { Camera, CameraResultType, CameraSource } = await import('@capacitor/camera');
const Camera = getNativeCamera();
if (!Camera) throw new Error('Camera plugin unavailable');
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
source: CameraSource.Camera,
resultType: 'dataUrl', // CameraResultType.DataUrl
source: 'CAMERA', // CameraSource.Camera
quality: 90,
correctOrientation: true,
});

View file

@ -44,22 +44,26 @@ export default defineConfig(() => ({
transformMixedEsModules: true,
},
rollupOptions: {
// IMPORTANT: @capacitor/core and @capacitor/camera MUST be bundled (they are
// installed, statically imported, and their JS API layer is real code the
// WebView needs). Leaving them external emitted a bare `import "@capacitor/core"`
// specifier that a native WebView cannot resolve → white-screen boot crash.
// They are therefore intentionally ABSENT from this list.
// Bundling policy — the distinction that matters is STATIC vs DYNAMIC import,
// and whether the package resolves on the build machine:
//
// Kept external:
// - nostr-signer-capacitor-plugin: installed from GitHub with a broken package
// entry point, so it CANNOT be bundled. It's only ever used via a guarded
// dynamic import() at runtime on native (see lib/capacitorAmberSigner.ts,
// wrapped in try/catch → returns null on failure), so external is correct
// and safe — a resolution failure degrades gracefully instead of crashing.
// - @capacitor/app|haptics|keyboard|status-bar: NOT installed and never
// statically imported (accessed via window.Capacitor.Plugins), so keeping
// them external prevents a stray import from breaking the build.
// BUNDLED (absent from this list):
// - @capacitor/core — imported STATICALLY (lib/platform.ts). It MUST be
// bundled: leaving it external emitted a bare `import "@capacitor/core"`
// specifier the native WebView can't resolve → white-screen boot crash.
// Capacitor guarantees core is installed, so bundling always resolves.
//
// EXTERNAL (listed below):
// - @capacitor/camera — used only via a GUARDED dynamic import() on native
// (components/CameraCapture.tsx). It may not be installed on every build
// machine, and it's optional/native-only, so it must stay external. The
// dynamic import() remains a runtime call that degrades gracefully.
// - nostr-signer-capacitor-plugin — installed from GitHub with a broken
// package entry point; cannot be bundled. Guarded dynamic import() only.
// - @capacitor/app|haptics|keyboard|status-bar — NOT installed and never
// statically imported (accessed via window.Capacitor.Plugins at runtime).
external: [
'@capacitor/camera',
'nostr-signer-capacitor-plugin',
'@capacitor/app',
'@capacitor/haptics',