type: guide
title: "honua-maplibre migration target"
description: "The honua-maplibre codemod target: MapLibre-native output for the layer/map/view core, widget kit registration, CLI flags, report fields and the manual gaps."
resource: "https://www.npmjs.com/package/@honua/honua-migrate"
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.
Canonical migration workbench
examples/migration-workbench
is the qualified arcgis-migration golden journey for the complete migration
experience. The repository build invokes the real honua-migrate CLI against
the Honua-authored geoservices-source-app fixture and commits a deterministic
report, patch, widget guidance, MapLibre assessment, generated compat target,
and SHA-256 manifest. The browser only projects and revalidates those
artifacts; it does not contain a second transform, accept uploads, read
credentials, or perform cloud import.
Which import
A new 2D map imports createHonua from @honua/sdk-js and maplibreRenderer from @honua/sdk-js/runtime. Server attach uses HonuaClient from @honua/sdk-js/honua. An ArcGIS app uses @honua/sdk-esri-compat and @honua/honua-migrate. Widget paint registers @honua/app-platform/web-components. @honua/sdk is HonuaClient, not createHonua.
Widget kit registration
This is a required step, not a footnote. A shim that constructs a
HonuaWidgetHost paints only after the application registers the kit. A shim
that does not construct a host stays state-model-only whether or not a kit is
registered, and it does not emit the missing-kit diagnostic.
Importing @honua/sdk-esri-compat does not load the kit. The compat entry is
bundle-budgeted, and any import of the kit from that entry would pull the
component set into every compat bundle. The application registers the tag.
Paint one tag through the side-effect-free registry in src/controls/registry.ts.
registerComponent dynamically imports web-components/elements.js, which does
not register tags on import, and calls defineHonuaWebComponent for that
catalog id only. Pass the registered constructor to registerHonuaWidgetKit
and omit defineHonuaWebComponents. If the kit object includes
defineHonuaWebComponents, the host calls it and every tag in the kit is
defined.
import { registerComponent } from "@honua/app-platform/controls";
import { registerHonuaWidgetKit } from "@honua/sdk-esri-compat";
await registerComponent("web-components.legend");
registerHonuaWidgetKit({
HonuaLegendElement: customElements.get("honua-legend"),
});
@honua/app-platform/web-components is the widget package. Importing it, or
the deprecated @honua/sdk-js/web-components forwarder, evaluates
defineHonuaWebComponents() and defines every tag. Use that import when the
app wants the whole kit. Use registerComponent when it wants one tag.
The honua-compat codemod in @honua/honua-migrate inserts one
registerHonuaWidgetKit(() => import("@honua/sdk-js/web-components")) call
when the file constructs a hosted compat widget (Legend, LayerList, TimeSlider,
Search, Measurement, Editor, FeatureForm, FeatureTable, Bookmarks,
BasemapGallery, BasemapToggle, Locate, Sketch, Print, Popup, Zoom, Home,
ScaleBar, Compass, Fullscreen, Attribution, or Directions). That emitted import
defines every tag. esri-leaflet output does not insert the call.
keep-esri output stays byte-identical. Replace the emitted loader with the
registerComponent call above when one tag is enough.
Register before you construct widgets. registerHonuaWidgetKit is idempotent,
and passing undefined unregisters: painting shims return to state-model-only
mode and any component they had already mounted is removed from its container
on the next refresh.
Without a kit, a painting shim still keeps its state model — legend.items
stays populated, layerList.toggle() still mutates layers, events still fire —
and the container you passed stays empty. The first mount in that state
emits a one-time diagnostic (honua-io/honua-sdk-js#957):
- a
console.warnnamingregisterComponentandregisterHonuaWidgetKit, and linking back to this section; and - a
widget-kit.missingevent on the shim'sCompatEventBus, carrying{ tagName, api, docs, message }for telemetry or a first-run banner.
legend.eventBus.on("widget-kit.missing", (event) => {
reportFirstRunProblem(event.payload.message);
});
The diagnostic fires once per runtime, on the first mount that finds no
kit — not once per widget instance — so subscribe before constructing widgets.
It re-arms whenever registerHonuaWidgetKit is called again.
@honua/sdk-esri-compat does not ship the kit. Install
@honua/app-platform to register tags. FeaturesCompat, AttachmentsCompat,
and ScaleRangeCompat construct hosts and are not on the stable client-compat
entry.
| Shim | Tag |
|---|---|
AttributionCompat |
honua-attribution |
AttachmentsCompat |
honua-attachments |
BasemapGalleryCompat, BasemapToggleCompat |
honua-basemap-control |
BookmarksCompat |
honua-bookmarks |
CompassCompat |
honua-compass |
DirectionsCompat |
honua-directions |
EditorCompat |
honua-editor |
FeatureFormCompat |
honua-feature-editor |
FeatureTableCompat |
honua-feature-table |
FeaturesCompat |
honua-feature-pager |
FullscreenCompat |
honua-fullscreen |
HomeCompat |
honua-home |
LayerListCompat |
honua-layer-list |
LegendCompat |
honua-legend |
LocateCompat |
honua-locate-control |
MeasurementCompat |
honua-measurement |
PopupCompat |
honua-feature-inspection |
PrintCompat |
honua-print-export |
ScaleBarCompat |
honua-scale-bar |
ScaleRangeCompat |
honua-scale-range |
SearchCompat |
honua-search |
SketchCompat |
honua-sketch-control |
TimeSliderCompat |
honua-time-slider |
ZoomCompat |
honua-zoom |
These container-bearing shims do not construct a host. They stay state-model-only: AreaMeasurement2DCompat, BasemapLayerListCompat, CoordinateConversionCompat, DistanceMeasurement2DCompat, ExpandCompat, FeatureCompat, FeatureTemplatesCompat, SwipeCompat, TableListCompat, TrackCompat.
How it differs from the other targets
src/migration/codemod.ts is the in-repo suite, not the npm program.
@honua/sdk-js/migration forwards to @honua/honua-migrate, and the CLI is
npx honua-js-migrate. The suite 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 * as 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/client-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/client-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/client-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/geoservices-source-app \
--target honua-maplibre \
--report reports/geoservices-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 client-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.
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
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. Underhonua-compatthey are rewritten, but rendering still depends on the app performing the widget kit registration the codemod does not yet insert. - 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.
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.