Install and Configure Temporal Polyfill in Vite

To use the Temporal API in a Vite app today, install @js-temporal/polyfill, add it to optimizeDeps.include so pre-bundling does not crash HMR, import Temporal directly in each module, and verify under a TZ matrix. Part of Getting Started with the Temporal API.

Why This Scenario Is Tricky

Two Vite-specific failure modes bite teams adding the polyfill. First, Vite pre-bundles dependencies with esbuild for dev. The polyfill ships as CommonJS/ESM interop, and if it is not listed in optimizeDeps.include, Vite may fail to convert it on first request — the dev server returns 504 (Outdated Optimize Dep) and HMR breaks. Second, because Vite serves ES modules out of order in dev, any module that reads a global Temporal can execute before main.ts has assigned it, throwing ReferenceError: Temporal is not defined. The robust fix is to import Temporal directly in every date-critical module rather than relying on global assignment timing.

A third, subtler issue is environment parity: CI runners default to TZ=UTC while developer machines use a local zone. Tests that pass locally then fail in CI because a ZonedDateTime resolves a different offset. The fix is to test under an explicit TZ matrix from the start.

Direct import beats global timing in ViteIn dev, a feature module may load before main.ts assigns globalThis.Temporal, so reading the global throws. Importing Temporal directly from the polyfill resolves the dependency before the module body runs, so it always works.Module load order in devGlobal assignment (fragile)feature.tsreads globalmain.tssets globalruns first -> ReferenceErrorDirect import (reliable)feature.tsimport Temporalpolyfillreadyresolved before body runsoptimizeDeps.include: ['@js-temporal/polyfill']pre-bundles the CJS/ESM interop so HMR does not 504test under TZ=UTC and TZ=America/New_York for parity

Minimal Working Solution

Install with a pinned version, then add the one Vite config line that prevents the HMR crash.

npm install @js-temporal/polyfill --save-exact   # pin: arithmetic must not change silently
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    // Pre-bundle so esbuild handles the CJS/ESM interop and HMR stays stable.
    include: ['@js-temporal/polyfill'],
  },
});
// any date-critical module — import directly, do not depend on a global
import { Temporal } from '@js-temporal/polyfill';

const today = Temporal.Now.plainDateISO(); // ISO 8601 (Gregorian) calendar

Full Production Version

Add a manual chunk so the polyfill caches separately, conditional loading so native-Temporal browsers skip the download, and TypeScript types for the global if you choose to expose one.

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    include: ['@js-temporal/polyfill'], // stabilize dev pre-bundling
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Split the polyfill into its own long-cacheable chunk.
          'temporal-polyfill': ['@js-temporal/polyfill'],
        },
      },
    },
  },
});
// src/temporal-loader.ts — download the polyfill only when there is no native Temporal
export async function loadTemporal(): Promise<typeof import('@js-temporal/polyfill').Temporal> {
  if (typeof globalThis.Temporal !== 'undefined') {
    return globalThis.Temporal as any; // native (Chrome 144+, Firefox 139+) — no download
  }
  const { Temporal } = await import('@js-temporal/polyfill'); // dynamic import = own chunk
  (globalThis as any).Temporal = Temporal; // assign yourself; the polyfill does not
  return Temporal;
}
// src/temporal.d.ts — only needed if you read the global Temporal anywhere
import type { Temporal as TemporalType } from '@js-temporal/polyfill';

declare global {
  const Temporal: typeof TemporalType; // removes TS2304 and restores autocomplete
  interface Window { Temporal: typeof TemporalType; }
}
export {};

If date logic is on the critical path everywhere, prefer an eager import in main.ts over loadTemporal() — the synchronous import removes the race condition entirely, and the cost is modest. For exactly how small that cost is and how to trim it, see Temporal polyfill bundle size and tree-shaking.

Verification Snippet

Prove the polyfill resolves correctly and that arithmetic is independent of the host zone.

// src/verify-temporal.ts
import { Temporal } from '@js-temporal/polyfill';

export function verifyTemporal(): void {
  // ZonedDateTime resolves the IANA offset; epochNanoseconds is a BigInt.
  const zdt = Temporal.ZonedDateTime.from(
    '2023-11-05T01:30:00[America/New_York]',
    { disambiguation: 'compatible' } // fall-back overlap — pick the legacy-compatible instant
  );
  console.assert(typeof zdt.epochNanoseconds === 'bigint', 'epochNanoseconds is BigInt');
  console.assert(zdt.toInstant().toString().endsWith('Z'), 'Instant serializes as UTC');

  // Leap day is valid and survives a round-trip to string.
  const feb29 = Temporal.PlainDate.from('2024-02-29');
  console.assert(feb29.toString() === '2024-02-29', 'leap day valid');

  console.log('Temporal runtime OK');
}
# Run under both zones so a host-zone assumption fails in CI, not in production.
TZ=UTC node --import tsx src/verify-temporal.ts
TZ=America/New_York node --import tsx src/verify-temporal.ts

Common Pitfalls

FAQ

Why does Vite throw "Temporal is not defined" in dev?

A module read the global Temporal before main.ts assigned it, or the polyfill was not pre-bundled. Import Temporal directly from @js-temporal/polyfill in each module and add the package to optimizeDeps.include.

Can the polyfill coexist with date-fns or Day.js?

Yes. Keep them in separate modules and migrate incrementally. Bridge with Temporal.Instant.fromEpochMilliseconds(date.getTime()) going in and new Date(Number(instant.epochMilliseconds)) coming out.

How do I keep DST tests stable in CI?

Never depend on the runner's TZ. Pass explicit IANA identifiers to Temporal constructors and run the suite under a TZ matrix (at minimum UTC and one DST zone) so host-zone drift surfaces immediately.