DEV Community

Cover image for Zero-config Cesium.js in Vite — introducing vite-plugin-cesium-engine
Jérôme
Jérôme

Posted on

Zero-config Cesium.js in Vite — introducing vite-plugin-cesium-engine

If you've ever tried to use CesiumJS with Vite, you know the ritual. Before you can render a globe you have to:

  • Copy WASM workers and assets to your output directory
  • Set window.CESIUM_BASE_URL before any Cesium module loads
  • Inject a <link> tag for CesiumWidget.css
  • Somehow get your Ion access token into the bundle

Every project starts the same way: copy-paste from a StackOverflow answer, tweak until it works in dev, discover it breaks in prod, repeat.

I built vite-plugin-cesium-engine to make all of that disappear.


What it targets

There are already a couple of Cesium Vite plugins out there, but they all target the full cesium package — the one that comes with the entire Viewer UI. If you want the lean @cesium/engine core (no widgets, full control over your own UI), you were on your own.

This plugin is purpose-built for @cesium/engine only.


Installation

# npm
npm i -D @cesium/engine vite-plugin-cesium-engine

# pnpm
pnpm add -D @cesium/engine vite-plugin-cesium-engine

# yarn
yarn add -D @cesium/engine vite-plugin-cesium-engine
Enter fullscreen mode Exit fullscreen mode

Basic usage

Add it to your Vite config:

// vite.config.ts
import { defineConfig } from "vite";
import { cesiumEngine } from "vite-plugin-cesium-engine";

export default defineConfig({
  plugins: [cesiumEngine()],
});
Enter fullscreen mode Exit fullscreen mode

That's the entire setup. No CESIUM_BASE_URL, no asset copy scripts, no CSS imports. Then use Cesium directly:

import { CesiumWidget, Terrain } from "@cesium/engine";

const widget = new CesiumWidget(document.getElementById("app")!, {
  terrain: Terrain.fromWorldTerrain(),
});
Enter fullscreen mode Exit fullscreen mode

The plugin automatically:

  • Copies WASM workers, built files, and CesiumWidget.css to your output directory
  • Injects window.CESIUM_BASE_URL into your HTML before any module loads
  • Injects the CesiumWidget.css <link> tag
  • Serves assets directly from node_modules during vite dev (no copying needed)

Framework examples

The setup is identical across frameworks — only the lifecycle hooks differ.

React

// src/main.tsx
import { useEffect, useRef } from "react";
import { CesiumWidget, Terrain } from "@cesium/engine";

export default function App() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;
    const widget = new CesiumWidget(containerRef.current, {
      terrain: Terrain.fromWorldTerrain(),
    });
    return () => widget.destroy();
  }, []);

  return <div ref={containerRef} style={{ width: "100%", height: "100vh" }} />;
}
Enter fullscreen mode Exit fullscreen mode

Vue

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import { CesiumWidget, Terrain } from "@cesium/engine";

const container = ref<HTMLDivElement>();
let widget: CesiumWidget;

onMounted(() => {
  widget = new CesiumWidget(container.value!, {
    terrain: Terrain.fromWorldTerrain(),
  });
});

onBeforeUnmount(() => widget?.destroy());
</script>

<template>
  <div ref="container" style="width: 100%; height: 100%" />
</template>
Enter fullscreen mode Exit fullscreen mode

Svelte

<script lang="ts">
  import { onMount } from "svelte";
  import { CesiumWidget, Terrain } from "@cesium/engine";

  let container: HTMLDivElement;

  onMount(() => {
    const widget = new CesiumWidget(container, {
      terrain: Terrain.fromWorldTerrain(),
    });
    return () => widget.destroy();
  });
</script>

<div bind:this={container} style="width: 100%; height: 100%" />
Enter fullscreen mode Exit fullscreen mode

Vanilla TypeScript

// src/main.ts
import { CesiumWidget, Terrain } from "@cesium/engine";

const widget = new CesiumWidget(document.getElementById("app")!, {
  terrain: Terrain.fromWorldTerrain(),
});

if (import.meta.hot) {
  import.meta.hot.dispose(() => widget.destroy());
}
Enter fullscreen mode Exit fullscreen mode

Complete, ready-to-run starter projects for all four frameworks are available in the examples/ directory of the repository.


Ion token — baked in at build time

Set your Cesium Ion access token through the plugin options and it gets injected at build time — no Ion.defaultAccessToken = ... in your app code, no runtime env variable reading:

cesiumEngine({
  ionToken: process.env.CESIUM_ION_TOKEN,
})
Enter fullscreen mode Exit fullscreen mode

Per-environment tokens

Pass a { [mode]: token } map to use different tokens per Vite mode:

cesiumEngine({
  ionToken: {
    development: process.env.CESIUM_ION_TOKEN_DEV,
    production:  process.env.CESIUM_ION_TOKEN_PROD,
  },
})
Enter fullscreen mode Exit fullscreen mode
vite dev    # injects CESIUM_ION_TOKEN_DEV
vite build  # injects CESIUM_ION_TOKEN_PROD
Enter fullscreen mode Exit fullscreen mode

The plugin also validates that the token looks like a valid JWT at startup and warns you if it doesn't — catching .env substitution failures before they become runtime surprises.


Virtual module — typed runtime constants

The plugin exposes a virtual:cesium module so your app code can read CESIUM_BASE_URL and ION_TOKEN without touching window globals:

// tsconfig.json
{
  "compilerOptions": {
    "types": ["vite-plugin-cesium-engine/virtual"]
  }
}
Enter fullscreen mode Exit fullscreen mode
import { CESIUM_BASE_URL, ION_TOKEN } from "virtual:cesium";

console.log(CESIUM_BASE_URL); // "/cesium/"
console.log(ION_TOKEN);       // your token, or null
Enter fullscreen mode Exit fullscreen mode

Both values are resolved at build time and tree-shaken if unused.


Custom asset paths

For CDN deployments or frameworks with opinionated public directories (Laravel, Rails, etc.), you can control exactly where assets land and where they're served from:

// vite.config.ts
export default defineConfig({
  base: "/app/",
  plugins: [
    cesiumEngine({
      assetsPath: "vendor/cesium",        // dist/vendor/cesium/
      cesiumBaseUrl: "/app/vendor/cesium", // served from here
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

The plugin warns at startup if cesiumBaseUrl doesn't start with Vite's base, so misconfigurations are caught immediately rather than at runtime in production.


Debug mode

If something isn't wiring up as expected, debug: true prints a full summary at dev-server startup:

cesiumEngine({ debug: true })
Enter fullscreen mode Exit fullscreen mode
[cesium-engine] mode         : development
[cesium-engine] vite base    : ""
[cesium-engine] cesiumBaseUrl: "/cesium"
[cesium-engine] assetsPath   : "cesium"
[cesium-engine] ionToken     : eyJhbGciOiJ… (mode: development)
[cesium-engine] copying assets:
  ThirdParty/*.wasm → cesium/ThirdParty
  Build/*           → cesium
  Source/Assets/    → cesium
  Widget/*.css      → cesium/Widget
Enter fullscreen mode Exit fullscreen mode

Known warning: protobufjs eval

You may see this Rollup warning in your build output:

[EVAL] Use of direct eval function is strongly discouraged
  node_modules/protobufjs/dist/minimal/protobuf.js
Enter fullscreen mode Exit fullscreen mode

This comes from protobufjs, a transitive dependency of @cesium/engine. The eval is intentional in that library and not a security issue. Suppress it in your vite.config.ts:

build: {
  rollupOptions: {
    onwarn(warning, defaultHandler) {
      if (warning.code === "EVAL" && warning.id?.includes("protobufjs")) return;
      defaultHandler(warning);
    },
  },
},
Enter fullscreen mode Exit fullscreen mode

Zero runtime dependencies

The plugin has no runtime dependencies — it uses only Node built-ins (node:fs, node:path) and Vite's own plugin API. What gets installed alongside it is exactly nothing beyond what you already have.


Links

Feedback, issues, and PRs are very welcome.

Top comments (0)