Skip to content

esm.sh-bundled @developmentseed/deck.gl-geotiff loses proj4 projection classes (utm, lcc, …); breaks CDN / no-build-step consumers #560

@tylere

Description

@tylere

Summary

I'm having problems using @developmentseed/deck.gl-geotiff from esm.sh
- Tyler

(The following was written by Claude Code and reviewed by Tyler)

Loading @developmentseed/deck.gl-geotiff@0.7.0 from a CDN that aggressively tree-shakes (esm.sh's ?bundle-deps) drops proj4's non-fundamental projection classes from the bundle, so any COG whose CRS isn't merc / longlat / tmerc fails at the first proj4(sourceProjection, "EPSG:4326") call with:

Error: Could not get projection name from: [object Object]

Repro

<script type="module">
  import { COGLayer } from "https://esm.sh/@developmentseed/deck.gl-geotiff@0.7.0?bundle-deps";

  // Any WGS84 UTM COG — e.g. the same Sentinel-2 TCI used in cog-basic.
  const url = "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/18/T/WL/2026/1/S2B_18TWL_20260101_0_L2A/TCI.tif";

  // No `epsgResolver` supplied — uses the package default (epsg.io PROJJSON
  // + wkt-parser), same as examples/cog-basic.
  new COGLayer({ id: "cog", geotiff: url });
</script>

Result: the layer initializes, fetches metadata, calls epsgResolver(32618), gets back a definition with projName: "utm", hands it to proj4(...). proj4 throws — its Projection.projections registry doesn't have an entry for "utm".

Why local builds work and CDN doesn't

The examples/cog-basic app handles UTM, NZTM, EPSG:2056, etc. with no custom epsgResolver and no extra setup. Vite + Rollup keep proj4's projection-class side effects, so the locally-bundled proj4 has a complete Projection.projections registry.

esm.sh's ?bundle-deps runs a more aggressive tree-shaker: from its POV, only proj4's default-exported function is referenced from @developmentseed/proj and cog-layer.ts, so it eliminates the projection-class registrations as dead code.

I verified this is the failure mode by:

  • Logging the def object returned from a custom resolver: { projName: "utm", zone: 18, datumCode: "WGS84", units: "m" } — well-formed, but bundled proj4 still rejects it.
  • Switching to ?bundle-deps&external=proj4 plus an HTML import map pointing both the consumer page and the bundled module at the same full proj4 instance — works. Confirms the issue is the proj4 inside the bundle, not the def object.
  • Substituting an equivalent tmerc def (projName: "tmerc") for UTM — works, because tmerc is a fundamental projection that bundled proj4 retains. (Verified visually correct against the cog-basic example for EPSG:32618.)

Why this matters

CDN consumers can't supply an HTML <script type="importmap"> in every host environment. In particular:

  • Embedded contexts (anywidget widgets, Observable, sandboxed iframes) can't reach the parent page's <head>.
  • Inline data: URL iframes have null origins; an importmap inside doesn't help with workers, and authoring one per embed is awkward.

A no-build-step CDN flow is the simplest possible adoption path. The package works perfectly in a real build — but right now it silently fails on most projected CRSs when loaded from esm.sh.

Suggested fixes (in increasing order of footprint)

  1. One-line side-effect import in packages/proj/src/index.ts:

import "proj4/lib/projections/index.js";

This is a side-effect-only import; tree-shakers preserve it. It pulls in the same projection registrations proj4's main entry does, but in a form bundlers can't eliminate.

  1. Explicit registration (smaller bundle than Initial repo setup #1):
import { Projection } from "proj4/lib/Proj.js";
import utm from "proj4/lib/projections/utm.js";
import lcc from "proj4/lib/projections/lcc.js";
import aea from "proj4/lib/projections/aea.js";
import stere from "proj4/lib/projections/stere.js";
// …whichever set the package commits to supporting.
[utm, lcc, aea, stere /* … */].forEach((p) => Projection.projections.add(p));
  1. Re-export proj4 from the public API so downstream consumers can register custom CRSs against the same instance (and bundlers can see the reference):

export { default as proj4 } from "proj4";

Useful regardless of the projections fix.

Environment

  • @developmentseed/deck.gl-geotiff@0.7.0
  • @developmentseed/proj@0.7.0
  • esm.sh build (?bundle-deps)
  • Chrome 132 / Firefox 128 — both reproduce
  • proj4@2.11.0 (the version esm.sh resolves)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions