honua-maplibre migration target
The honua-maplibre codemod target rewrites a curated subset of
@arcgis/core constructors directly into @honua/sdk-js/map helpers
plus MapLibre GL source/layer definitions, instead of routing them
through the Esri-shaped compat shims. It is the codemod option that
produces MapLibre-native output for apps that want to leave the
ArcGIS JS API behind end-to-end.
This page documents the slice that shipped in PR #208 (commit
af1ebee). It does not close issue #205 — open acceptance items are
called out under Manual gaps.
How it differs from the other targets
The migration codemod (src/migration/codemod.ts) exposes three
targets selected with --target on the CLI:
| Target | Output shape | What it rewrites natively |
|---|---|---|
honua-compat (alias honua) |
@honua/sdk-esri-compat shims that mimic the ArcGIS JS API surface |
Every kind in REWRITE_SPECS (SUPPORTED_ARCGIS_MODULE_KIND_BY_PATH) — full 2D constructor surface (layers, views, widgets, controls, tasks, support). |
honua-maplibre |
@honua/sdk-js/map helpers + raw MapLibre style/layer objects |
The five kinds in HONUA_MAPLIBRE_NATIVE_KINDS: feature-layer, map-image-layer, tile-layer, map, map-view. Everything else still falls through to a manual TODO. |
esri-leaflet |
Mixed esri-leaflet primitives + compat fallback | feature-layer, map-image-layer, tile-layer natively; the remaining 2D surface via ESRI_LEAFLET_COMPAT_FALLBACK_KINDS. |
honua-compat is the broadest deterministic option — it covers the
full REWRITE_SPECS matrix and is the default. honua-maplibre
trades surface coverage for a MapLibre-native output: it only emits
native helpers for the layer/map/view core, and everything else
(widgets, controls, renderers, geometry constructors, tasks)
becomes a manual TODO because there is no @honua/sdk-js/map
helper for it yet. esri-leaflet sits in between — three native
layer kinds with a compat fallback for the rest.
Auto-migrated, assisted, and unsupported kinds
The exact set of kinds the honua-maplibre target rewrites natively
is defined in src/migration/codemod.ts as
HONUA_MAPLIBRE_NATIVE_KINDS and consumed by
TARGET_SUPPORTED_KINDS["honua-maplibre"]:
feature-layer(@arcgis/core/layers/FeatureLayer) →createHonuaFeatureServiceLayer({ url, ... })returning aHonuaFeatureServiceSourceSpecificationplus a default MapLibre render layer (circle/line/fillchosen vialayerType).map-image-layer(@arcgis/core/layers/MapImageLayer) →createHonuaMapServiceLayer({ url, ... })returning aHonuaMapServiceSourceSpecificationplus a default raster render layer.tile-layer(@arcgis/core/layers/TileLayer) →createHonuaTileServiceLayer({ url, ... })returning a MapLibre raster source pointing at<url>/tile/{z}/{y}/{x}plus a raster render layer.map(@arcgis/core/Map) →createHonuaMapLibreStyle({ basemap, layers, ... })returning aHonuaStyleSpecification(MapLibre style version 8) that aggregates the migrated layers' sources and layers.map-view(@arcgis/core/views/MapView) →createHonuaMapLibreMapOptions({ container, style, center, zoom, bearing, pitch }), the options bag passed tonew maplibregl.Map(...).
The helper signatures live in src/map/maplibre-target.ts.
Rendering feature-service layers (custom source → geojson)
createHonuaTileServiceLayer and createHonuaMapServiceLayer emit
MapLibre-native raster sources that render as soon as you add them to
a map. createHonuaFeatureServiceLayer is different: its source uses
the custom honua-feature-service type, which MapLibre's renderer does
not understand. Adding that source to a maplibre-gl Map directly is
a silent no-op — no error, no features.
To render feature-service layers, run them through the MapLibre runtime
adapter exported from @honua/sdk-js/map. It fetches features via the
SDK query path and produces a standard geojson source MapLibre renders
natively:
import maplibregl from "maplibre-gl";
import { HonuaClient } from "@honua/sdk-js";
import {
createHonuaFeatureServiceLayer,
loadHonuaFeatureServiceGeoJson,
} from "@honua/sdk-js/map";
const client = new HonuaClient({ baseUrl: "https://gis.example.com" });
const layer = createHonuaFeatureServiceLayer({ url, outFields: ["*"] });
// Replace the custom honua-feature-service source with a fetched geojson source.
const source = await loadHonuaFeatureServiceGeoJson(client, layer.source.url);
map.addSource(layer.sourceId, source);
map.addLayer(layer.layer);
For a whole HonuaMap, registerHonuaFeatureServiceSources(map, honuaMap, client) walks every honua-feature-service source and live-patches it
on the MapLibre map (adding a new geojson source or calling setData
on an existing one). Both helpers live in
src/map/feature-service-adapter.ts.
Every other constructor kind in REWRITE_SPECS — geometries,
symbols, renderers, WebMap, SceneView, GraphicsLayer,
GroupLayer, VectorTileLayer, GeoJSONLayer, WMSLayer,
WFSLayer, ImageryLayer, FeatureFilter, all widgets (LayerList,
Legend, Popup, Search, Sketch, Editor, TimeSlider, …),
all controls (Home, Zoom, Compass, ScaleBar, Fullscreen,
BasemapToggle, …), Query, OAuthInfo, IdentityManager,
esriRequest, esriConfig, reactiveUtils, RouteTask, etc. —
is not in HONUA_MAPLIBRE_NATIVE_KINDS. The codemod emits a
manual TODO with a reason at each call site and lists the unhandled
ArcGIS module under unhandledArcGisModules in the report. The
runtime parity matrix
(src/migration/runtime-matrix.ts::inferHonuaMapLibreRuntimeStatus)
categorizes these surfaces as assisted for honua-maplibre,
except for the feature-layer, map-image-layer, and the
map-view.navigation-go-to capability which are tagged native.
Use the canonical fixture
test/fixtures/esri-maplibre-simple-app/
as a worked example — it imports Map, MapView, FeatureLayer,
MapImageLayer, and TileLayer from @arcgis/core and exercises
exactly the kinds that honua-maplibre can rewrite natively.
CLI flag and invocations
The codemod is selected with --target honua-maplibre on the
codemod, fixtures, and demo subcommands of
node dist/src/migration/cli.js. The accepted target tokens are
honua (alias of honua-compat), honua-compat, honua-maplibre,
and esri-leaflet — anything else is rejected by the parser.
# Dry-run the codemod against the bundled MapLibre fixture
node dist/src/migration/cli.js codemod test/fixtures/esri-maplibre-simple-app \
--target honua-maplibre \
--report reports/maplibre-fixture-report.json
# Write the rewrite in place with inline TODOs for manual sites
node dist/src/migration/cli.js codemod test/fixtures/esri-maplibre-simple-app \
--target honua-maplibre \
--write --annotate-todos \
--report reports/maplibre-fixture-report.json
# Run the codemod against the hand-written parcel viewer example
node dist/src/migration/cli.js codemod examples/arcgis-source-app \
--target honua-maplibre \
--report reports/arcgis-source-app-maplibre-report.json
# Per-fixture readiness metrics for a single MapLibre fixture, no gating
node dist/src/migration/cli.js fixtures test/fixtures \
--target honua-maplibre \
--fixtures esri-maplibre-simple-app \
--report reports/maplibre-fixture-metrics.json
The shared codemod flags (--write, --annotate-todos,
--report <path>, --compat-import-path <pkg>, --fail-on-manual,
--fail-on-unhandled, --fail-on-blocked, --max-manual-ratio,
--max-manual-intervention-ratio) all apply unchanged. With
--target honua-maplibre the codemod only rewrites the five native
kinds; manual TODOs and unhandled-module entries are expected to
appear for any constructor outside that set, and gating flags like
--fail-on-manual will fail closed against apps with widgets,
renderers, or non-2D-core surfaces.
For contributor validation, use npm run test:migration:cli. Its
prerequisite prepares the SDK once before Vitest starts, and every CLI spec
executes the same manifest-owned dist/src/migration/cli.js artifact. The
atomic preparation manifest hashes every compiler/control input and the
complete dist/ tree, binds workers to one run ID, and revalidates both trees
at teardown. A direct Vitest invocation may run source-only tests without a
manifest; a test that consumes built output fails with an actionable
npm run prepare:test-sdk prerequisite error. Composed CI and publish lanes
use their :prepared variants after the single root build.
Migration report fields
The report writer is buildJsMigrationReport
(src/migration/report.ts); its JsMigrationReport shape is what
gets written to the path passed to --report. The fields you can
key off when consuming a honua-maplibre report are:
codemodTarget:"honua-maplibre"for runs launched with--target honua-maplibre. The union is"honua-compat" | "esri-leaflet" | "honua-maplibre".rootDir: absolute path of the codemod root that was scanned.scanSummary/scanReport: thescanArcGisUsage/summarizeArcGisScanoutput (import counts, flags likescene-3d-detected,advanced-widget-or-networking-detected).codemodResult.metrics.totalCodemodScopedCallSites,codemodResult.metrics.autoMigratedCallSites,codemodResult.metrics.manualCallSites,codemodResult.metrics.byKind[<kind>](per-kindauto/manual/totalcounts).manualRewriteMetric:{ numerator, denominator, ratio, scope }wherenumerator/denominator= manual call sites / total codemod-scoped call sites.ratiois the value compared against--max-manual-ratio.manualInterventionMetric:{ numerator, denominator, ratio, scope, manualCodemodCallSites, unhandledUsageHits }— extends the rewrite metric by addingunhandledArcGisModuleshits into both numerator and denominator. Compared against--max-manual-intervention-ratio.readiness:"ready" | "assisted" | "blocked"— derived fromgates. Blocking flags (scene-3d-detected,advanced-widget-or-networking-detected) forceblocked.gates: array of{ gate: "no-manual-todos" | "no-unhandled-modules" | "no-blocking-flags", passed, detail }. These are the MapLibre readiness gates: withhonua-maplibre, expectno-manual-todosandno-unhandled-modulesto fail on any app that uses widgets, renderers, geometry constructors, or other kinds outsideHONUA_MAPLIBRE_NATIVE_KINDS.manualTodos: per-call-site{ file, line, column, kind, reason, difficulty? }.manualTodoReasons: rolled-up reasons sorted by count, each with akindslist — useful for grouping cleanup work by category.manualTodosByKind: dense map of everyCodemodConstructorKindto its manual-call-site count for this run.unhandledArcGisModules:{ modulePath, usageStyle, count }entries for ArcGIS modules outside the codemod's per-target supported set.usageStyleis"static-import","dynamic-import", or"require".
The parity matrix tooling (matrix / runtime-matrix subcommands)
also emits a honuaMapLibre summary block alongside honuaCompat
and esriLeaflet with native/assisted/unsupported counts —
see summarizeJsParityMatrix and summarizeJsRuntimeParity.
Manual gaps
Issue #205 is a slice ladder, not a single landing. honua-maplibre
ships the native-rewrite path for the layer/map/view core; the
remaining acceptance items live in
docs/migration-punch-list.md and stay
open until they ship. The notable ones, framed against the punch
list:
- WebMap → MapLibre style conversion. The
web-mapkind is not inHONUA_MAPLIBRE_NATIVE_KINDS, sonew WebMap(...)constructors emit a manual TODO under this target. Server-side / CLI WebMap conversion lives behind thecontent-webmapsubcommand and is tracked separately. - Widget visual parity. Per the punch list "Widget UI behavior"
entry: the compat shims accept ArcGIS option shapes but render
through the Honua widget host with non-byte-identical visuals.
honua-maplibredoes not paper over this — widget constructors fall to manual TODO instead of being silently rewritten. - 3D / SceneView and scene layers.
scene-view,SceneLayer/BuildingSceneLayer/IntegratedMeshLayer/PointCloudLayer/MeshLayer/ElevationLayer/VoxelLayerare flagged with thescene-3d-detectedblocking flag and forcereadiness: "blocked". MapLibre is 2D-only by design; 3D parity is not in this target's scope. Locator/Geoprocessor/NetworkAnalyst(beyondRouteTask). Not inHONUA_MAPLIBRE_NATIVE_KINDS; manual TODO with the unhandled-module list pointing at the missing surfaces.- Renderers, symbols, popup templates,
reactiveUtils.watch, event-name remap,Querydeep-transform. These ship forhonua-compat(some shipped as Tasks D/E/F in the punch list) but they do not exist as native MapLibre helpers — underhonua-maplibrethey remain manual TODO surfaces. - Playwright smoke / runtime evidence lane for MapLibre output. Open acceptance item on #205.
For the broader migration story — including the honua-compat
parity surface, the codemod gates, and the test corpus — start at
docs/migration-punch-list.md and the
fixture-only Esri sample corpus in
docs/esri-sample-corpus.md.