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.

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 arcgis-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.

Use the artifact and browser gates directly, or exercise the same reviewed journey through the sample kit in source and packed SDK modes:

npm run demo:migration-workbench:artifacts:check
npm run test:playwright:migration-workbench
npm run samples:run -- verify --sample migration-workbench --sdk-mode source
npm run samples:run -- verify --sample migration-workbench --sdk-mode packed

The catalog carries current packed-build, browser, accessibility, console, responsive, screenshot, performance, fixture, and live receipts for this journey (#549); its live-evidence lane proves liveness by re-running the real honua-migrate CLI right now, since the workbench itself never makes a non-loopback network request. Gallery projection is tracked by #550.

Widget kit registration

This is a required step, not a footnote. A migrated app that constructs LegendCompat or LayerListCompat must register the Honua web-component kit once, or those two widgets render nothing.

Be precise about the scope, because it cuts both ways:

Shim With a registered kit Without one
LegendCompat (<honua-legend>) renders the delegated component state-model-only, container stays empty, diagnostic fires
LayerListCompat (<honua-layer-list>) renders the delegated component state-model-only, container stays empty, diagnostic fires
every other container-bearing shim (SearchCompat, MeasurementCompat, ExpandCompat, BasemapGalleryCompat, SketchCompat, …) still state-model-only state-model-only, no diagnostic

Registration is necessary for the first two and not sufficient for the rest: about two dozen shims accept a container option, but only LegendCompat and LayerListCompat construct a HonuaWidgetHost today. The others carry the ArcGIS state model — properties, watch, events, methods — and expect the application to render their state itself. Registering the kit does not give them UI, and they never emit the missing-kit diagnostic, so do not read a silent SearchCompat as a registration problem.

The two delegating shims draw UI through the Honua web components. The compat entry point never imports that kit — not even dynamically: /esri-compat is bundle-budgeted, and any intra-package import would pull the whole component set plus its geometry closure into every compat bundle. The application injects it instead, as early as the entry module runs:

import { registerHonuaWidgetKit } from "@honua/sdk-esri-compat";

// Eager (module object) or lazy (loader) — both work.
registerHonuaWidgetKit(() => import("@honua/sdk-js/web-components"));

Register before you construct widgets. The call is idempotent, and passing undefined unregisters: the delegating shims return to state-model-only mode and any component they had already mounted is removed from its container on the next refresh, so unregistering never strands stale UI.

Without a kit those two shims degrade to state-model-only: legend.items is populated, layerList.toggle() mutates layers, events still fire — and the container you passed stays empty. Since a migrated app in that state simply looks broken, the first mount without a registered kit emits a one-time diagnostic (honua-io/honua-sdk-js#957):

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, which keeps register-then-unregister cycles honest.

Three consequences worth planning for:

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"]:

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/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:

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:

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.