ArcGIS JS SDK → Honua JS SDK migration punch list
This is an honest accounting of where the Honua JS SDK's two public-facing claims stand:
- "Parity with the ArcGIS JS SDK" — the symbol surface app
authors can
importand call against. - "Automated app conversion" — the
src/migrationcodemod that rewrites ArcGIS imports/constructors to Honua compat shims.
It is intentionally written so a reader can tell what is shipped, what is partial, and what is not started. None of the entries below are aspirational — they are concrete work items.
What is true today
Parity surface (src/esri-compat/)
The compat module ships shims for the constructors enumerated in
src/migration/codemod.ts::SUPPORTED_ARCGIS_MODULE_KIND_BY_PATH.
Every kind in that map has a row in parity-matrix.ts, and every
row is asserted by test/migration-parity-matrix.test.ts. The
shims back onto real Honua surfaces in src/core/ (HonuaClient,
HonuaFeatureService, HonuaImageService, HonuaMapService, …) rather
than being throwaway no-ops. Specifically:
- 2D
FeatureLayer,GraphicsLayer,GroupLayer,MapImageLayer,TileLayer,RouteLayer,Basemap,VectorTileLayer,GeoJSONLayer,WMSLayer,WFSLayer,ImageryLayer Graphic,Point,Polyline,Polygon,Extent,SpatialReference,Color,FeatureSet- Symbols:
SimpleLineSymbol,SimpleMarkerSymbol,PictureMarkerSymbol,TextSymbol,LabelClass,SimpleFillSymbol - Renderers:
SimpleRenderer,UniqueValueRenderer,ClassBreaksRenderer - Views:
Map,MapView,SceneView(2D-only behavior),WebMap - Widgets (deterministic 2D-only set):
Home,BasemapToggle,BasemapGallery,BasemapLayerList,LayerList,Legend,Popup,Search,Track,Swipe,Feature,FeatureForm,FeatureTemplates,FeatureTable,DistanceMeasurement2D,AreaMeasurement2D,Compass,Fullscreen,Zoom,Attribution,ScaleBar,Locate,Bookmarks,Print,Sketch,Editor,Expand,TimeSlider,Directions,CoordinateConversion,Measurement - Tasks & support:
RouteTask,Locator,LocatorSearchSource,Query,OAuthInfo,IdentityManager,esriRequest,esriConfig,reactiveUtils,FeatureFilter
The MapViewLayerViewCompat returned from view.whenLayerView()
now carries filter / effect / visible state and forwards
filter predicates through queryFeatures / queryFeatureCount /
queryObjectIds, merging where, objectIds, geometry,
spatialRelationship, and timeExtent with any per-call options
(layer-view filter ∧ caller-provided options).
Sketch and Editor now accept ArcGIS-shaped snappingOptions
(enabled, distance, featureEnabled, selfEnabled,
featureSources) and map them onto the renderer-neutral
SnappingConfig from @honua/sdk-js/contract
(snappingOptionsToSnappingConfig). The contract snapping engine
(SnapIndex + resolveSnapCandidate) resolves vertex / edge /
feature candidates deterministically over a packed-grid spatial
index, invalidates on optimistic edit apply/rollback
(createSnapIndexEditSessionHooks), and the MapLibre runtime
binding (bindEditSketchSnapping in @honua/sdk-js/runtime)
wires pointer-move, snap/unsnap events, and a default indicator
layer. selfEnabled is stored for parity but self-snapping needs
the host to index the active sketch source; 3D snapping is out of
scope with the rest of the SceneView surface.
Automated app conversion (src/migration/codemod.ts)
The codemod is a real AST rewriter built on the TypeScript Compiler API (it does not use regex). It:
- Detects
import { X } from "@arcgis/core/..."and rewrites bindings to the matchingXCompatsymbol fromesri-compat. - Detects
new X(opts)for every kind above and rewrites to the compat constructor when the option literal is "safe" — i.e., a top-level object literal whose properties are all in the per-kind allowlist (FEATURE_LAYER_ALLOWED_PROPS,MAP_VIEW_ALLOWED_PROPS, …). The new layer kinds added in this pass —feature-filter,vector-tile-layer,geojson-layer,wms-layer,wfs-layer,imagery-layer— go through the sameisSafeAllowedPropertiesCallswitch and gain TODO annotations on unsafe call sites. - Emits
manualTodosandmanualTodoReasonson abyKindmatrix for every untouched call site, so app authors can drive cleanup against a checklist. - Emits a parity report (
src/migration/report.ts) with four gates (arcgis-usage-detected,no-manual-todos,no-unhandled-modules,no-blocking-flags) and a readiness verdict. A scan that recognized no ArcGIS usage reportsno-usage-detectedrather than letting the other three gates pass vacuously into aready;--fail-on-no-usagefails a CI lane on it. - Knows the
esri-leafletmigration target as a separate column, withESRI_LEAFLET_COMPAT_FALLBACK_KINDSgating which kinds are treated as deterministic vs. assisted. - Knows the
honua-maplibremigration target as a third column, gated byHONUA_MAPLIBRE_NATIVE_KINDS(feature-layer,map-image-layer,tile-layer,map,map-view). The native rewrite emits@honua/sdk-js/maphelpers plus MapLibre style/layer objects instead of compat shims; everything outside that set falls through to a manual TODO. Seedocs/migration-honua-maplibre.mdfor the per-target rewrite surface, CLI invocations, report fields, and the manual-gap list against issue #205.
Everything in this section is covered by tests under test/
(migration-codemod.test.ts is 75 cases; migration-report.test.ts,
migration-gating.test.ts, and migration-parity-matrix.test.ts
hold the surface stable).
What the claim does not yet cover
Parity gaps
3D / SceneView.
SceneViewexists as a compat class but it shares the 2DMapViewbehavior under the hood. Anything that requires a WebGL/CesiumJS path (environment,viewingMode: "global",goTowith altitude,Ground,ElevationLayer, scene layers,Camera,qualityProfile) is not implemented. The codemod sets thescene-3d-detectedblocking flag when it sees such imports, so the gate fails closed.Scene layers.
SceneLayer,BuildingSceneLayer,IntegratedMeshLayer,PointCloudLayer,MeshLayer,ElevationLayer,VoxelLayerhave no shims. These all need server-side glTF/I3S support (honua-serverticket).Geoprocessor/NetworkAnalyst(beyond RouteTask).Locatoris no longer in this bucket:LocatorCompatshimsaddressToLocations,addressesToLocations,locationToAddress, andsuggestLocationsonto the provider-pluggable geocoding contract insrc/geocoding/, andLocatorSearchSourceCompatis theSearchwidget's address backend — client-side, with nohonua-serversurface. The caller supplies the endpoint (Nominatim / Photon / Pelias / a Honua GeocodeServer), so the codemod rewrites the constructor but cannot choose the provider; that one wiring step stays assisted, and@arcgis/core/rest/locatorfunction imports are rewritten with a TODO for the URL-to-LocatorCompatswap. Provider capability misses (nosuggeston Nominatim) throwHonuaCapabilityNotSupportedErrorinstead of degrading to a forward geocode. Batch geocoding is client-side — one provider request per address, not a server batch endpoint. Still unshimmed: service-area, OD-cost-matrix, closest-facility, and non-network geoprocessing, which do need their own surfaces undersrc/core/and matching compat classes.Queryargument depth. Shipped (Task E). The codemod now deep-transformsnew Query({...})argument literals into the HonuaQueryFeaturesRequestshape: renamesstart→resultOffset,num→resultRecordCount,outSpatialReference→outSr,spatialRelationship→spatialRel; splitsgeometry: { type, ...rest }into a siblinggeometryTypefield with the matchingesriGeometry*enum value; and lowercasesoutStatistics[i].statisticTypeagainst the STATISTIC_TYPE_MAP keyset (count/sum/min/max/avg/stddev/var). Divergent / dynamic shapes emit a manual TODO with a precise reason — specifically:timeExtent(no Honua field; useextraParams.time),quantizationParameters(server-side optimization with no Honua equivalent),relationParam(no Honua support),geometrywithout a string-literaltypediscriminator, andoutStatisticsitems with a non-literalstatisticTypeor an unknown enum value. The shimQueryCompataccepts either ArcGIS or Honua property spellings (spatialRelationship/spatialRel,outSpatialReference/outSr,num/resultRecordCount,start/resultOffset) so codemod output and hand-written callers both validate.Popup actions / popup templates with
actions,outFields: ["*"]expansions, andfieldInfosformatters. These rewrite cleanly only for the simple case; arrow-function field-infoformatcallbacks fall through to manual TODO.Widget UI behavior. The widget shims accept the same constructor options ArcGIS does, but only
LegendCompatandLayerListCompatrender, through the Honua widget host (HonuaWidgetHost) and only after the app registers the web-component kit — see widget kit registration. Their visual parity (icons, ARIA, CSS class names that downstream apps style against) is not byte-identical; apps that rely oncalcite-action---prefixed selectors will need style work. Every other container-bearing shim is state-model-only and needs the application to render it.Typed-surface divergences from the emulated ArcGIS shape. The shims are structurally compatible with the ArcGIS surface they emulate except where this list says otherwise. The audit of array-valued properties (#1013) settled as follows.
Array mutability — fixed, no divergence. ArcGIS declares these as mutable arrays and the shims now do too:
FeatureFilterCompat.objectIds,GeoJSONLayerCompat.outFields,GeoJSONLayerCompat.fields,ImageryLayerCompat.bandIds,WFSLayerCompat.outFields. Each is copied out of the caller's input on construct/update, solayer.outFields.push("TYPE")is honored (the shim reads the array it stored) without aliasing the array the caller passed in. The corresponding*Optionsinput properties stayReadonlyArrayon purpose: an input that accepts a readonly array is strictly more permissive than ArcGIS's, so it is not a divergence.FeatureFilterCompat.objectIdselement type — deliberate, kept. ArcGIS declaresnumber[]; the shim declaresArray<number | string>. Honua's non-Esri sources (OGC API Features, GeoJSON, STAC) carry string feature ids, and narrowing the property tonumberwould make the shim unable to hold ids it has to round-trip throughqueryFeatures/queryObjectIds. Because(number | string)[]is not assignable tonumber[], handing a compat filter straight to an un-migrated ArcGIS construct still does not typecheck — so the shim exposesFeatureFilterCompat.toEsriProperties(), which projects onto the exact ArcGISFeatureFilterPropertiesshape and throws on an id ArcGIS cannot represent rather than silently dropping it and returning a filter that selects a different feature set. The codemod does not create that handoff on its own any more; see codemod gap 7.reactiveUtils.watchsemantics (partial). Compatwatchis property-name-based and synchronous. ArcGISwatchaccepts an accessor function and returns aWatchHandlewithpause()/resume(). Single-property accessors —reactiveUtils.watch(() => view.scale, h)whereviewis a tracked compat instance — are now rewritten by the codemod toreactiveUtils.watch(view, "scale", h). Multi-property accessors (() => [view.scale, view.zoom]) and complex bodies still fall through to a manual TODO.pause()/resume()on the handle is still unsupported.
Codemod gaps
These are the load-bearing pieces required to turn "rewrites known constructors" into "automated app conversion":
- Event-name remap (Task D, partial).
The codemod now rewrites well-known divergent event names on
.on()and.watch()calls in files that have at least one ArcGIS import being touched. The dictionary is inARCGIS_TO_COMPAT_EVENT_REMAPinsrc/migration/codemod.tsand currently coverslayerview-create*,layerview-destroy,visibility-change,extent-change,rotation-change,scale-change,zoom-change,center-change,spatial-reference-change,padding-change,constraints-change,highlight-options-change,basemap-change,ground-change,portal-item-change,active-basemap-change,camera-change,quality-profile-change,viewing-mode-change, andrefresh. Receiver-aware scoping is now in place: the rewrite only fires when the receiver is an Identifier whose binding came fromnew XCompat(...)whereXwas a tracked ArcGIS import (viaresolveAssignedIdentifierForNewExpression). Receivers that don't trace back to a tracked compat instance (e.g.,someEmitter.on("layerview-create", h)) are left alone even when the file has ArcGIS imports. What is still missing: pointer/keyboard event handlers (click,pointer-down,key-down, …) which compat doesn't emit yet. - Dynamic / CJS imports (Task F, partial).
Dynamic ESM imports (
import("@arcgis/core/...")) are rewritten through theisArcGisDynamicImportCallpass and have test coverage for SceneView, esriConfig, esriRequest, and IdentityManager. CommonJSrequire("@arcgis/core/...")is now auto-rewritten for thehonua-compattarget when constructor options are safe: the codemod emitsconst { XCompat } = require("@honua/sdk-esri-compat");and rewritesnew X(opts)tonew XCompat(opts)in.cjsfiles and.jsfiles containing CommonJS markers. The originalconst X = require("@arcgis/core/...")declaration is now pruned when no other references toXremain in the file (constructor calls are the only references in the typical case; the codemod scans for non-constructor uses and preserves the require if it sees any). Theesri-leaflettarget still emits a manual TODO for CJS require constructors. - Module re-exports beyond the registered set. The codemod
already follows
localArcGisReExportsfor one level. Deeper barrel-file chains (./esri.ts→./layers/index.ts→ …) aren't followed. view.map.add(layer)argument-shape transformations. When anew FeatureLayer({ ... })is rewritten, its option literal is passed through verbatim. We do not translate ArcGIS-shaped properties whose Honua-side names differ (e.g.,renderer.fieldcasing,popupTemplate.contentblock shapes). The shim accepts the ArcGIS shape at runtime, but the report should flag known-divergent property values so app authors can confirm intent.- WebMap JSON → MapLibre style derivation (
honua-maplibretarget). Partially shipped.src/map/webmap-maplibre.tsexposeswebmapJsonToMapLibreStyle(webmap, options), a pure converter that delegates the heavy lifting to the existingsrc/webmap/pipeline —parseWebMaporchestratesconvertBasemap,convertOperationalLayer,convertRenderer,convertPopupInfo,convertLabelingInfo, andconvertExtent/convertInitialViewpoint— and returns aHonuaStyleSpecificationready formap.setStyle(...)plus a structuredmanualGaps: WebMapMapLibreManualGap[]array.manualGapsis the single source of truth for unsupported constructs: every entry carries{ kind, reason, path?, layerId?, expression?, context? }and thekindtaxonomy coversarcade-expression(popupexpressionInfosand complex Arcade label expressions),unsupported-renderer(heatmap, dotDensity, and any other renderer the converter doesn't handle),scene-3d(top-levelground/camera/viewingMode,ArcGISSceneServiceLayer,elevationInfo,heightInfo,sceneProperties),dashboard-reference,experience-builder-reference,custom-widget-reference(applicationProperties.viewing.widgetsOnScreenand the top-levelwidgetsarray used by host shells), plus pass-throughs forunsupported-symbol,unsupported-layer-type,unsupported-feature-collection, andunsupported-webmap-version. The migration codemod now wires this converter into itshonua-maplibreconstructor-rewriting pass: anynew WebMap({ ... })call whose argument is a static WebMap JSON object literal (top-leveloperationalLayersand/orbaseMap, fully literal values, noportalItem) is rewritten towebmapJsonToMapLibreStyle({ ... })from@honua/sdk-js/map, and the helper'smanualGapsarray is propagated into the migration report as per-gapweb-mapmanual TODOs (Arcade, unsupported renderer, scene/3D, dashboard shell, …). What still requires manual review: dynamic / portal-loaded WebMaps (new WebMap({ portalItem: { id: someId } }),WebMap.load(), identifier references, computed properties, spreads) still emit aweb-mapmanual TODO with no rewrite — they need a runtime fetch the codemod cannot synthesize. The gap surface is also intentionally honest about its ceiling, since Arcade execution, scene/3D rendering, Dashboard widget hosts, Experience Builder pages, and custom-widget plugins all require runtime support the JS SDK cannot synthesize from JSON alone. Each emitted gap is a// TODOanchor for an app author, not a promise of automated migration. - Compat → ArcGIS seams (detected, not bridged). Shipped as
detection (#1012). A file can hold an in-scope producer whose
only consumer is out of scope — the shape the OSS-corpus deep
lane hit in
lujoh/owls_of_bavaria, where aFeatureFilterfeeds an@arcgis/core/layers/support/FeatureEffectthe codemod does not migrate. Rewriting only the producer leaves a compat value in an un-migrated ArcGIS constructor's hands: a seam that does not typecheck, previously scored as a clean auto-migration. The codemod now detects the handoff syntactically —new X()directly in an out-of-scope ArcGIS consumer's argument, a local bound to one, or a local function that returns one, plus assignment onto an out-of-scope instance's property — holds the rewrite back, and emits a manual TODO naming both sides. Seam call sites are counted inmanualCallSitesand broken out asmetrics.seamCallSites/JsMigrationReport.seamCallSites; they are never counted as auto-migrated. What is not done: the codemod does not bridge the seam (no insertedtoEsriProperties()call, no consumer rewrite), and the analysis is file-local and bounded by design — a value that crosses a module boundary or moves through a data structure it cannot follow is not reported. - End-to-end demo conversion. Shipped at
examples/arcgis-source-app/+test/migration-e2e.test.ts. The sample is a hand-written parcel viewer exercisingFeatureLayer(withoutFields+popupTemplate),Map,MapView, an untouchedview.on("click", …)handler, and an event-name-remappedlayer.on("layerview-create", …)handler. The e2e test copies it into a tempdir, runs the codemod withtarget: "honua-compat", asserts every expected rewrite landed and the readiness gate is"ready", and then runstsc --noEmitagainst the migrated source withpathsresolving@honua/sdk-esri-compatto the workspacesrc/esri-compat/entry. The CI job runs this harness immediately after the unit test step. Remaining work: extend the sample to additional layer kinds (Imagery / VectorTile / WMS / WFS / GeoJSON) so the harness exercises the full surface, not just the parcel viewer slice.
What's required to make the claim defensibly true
In rough effort order:
| Bucket | Effort | Notes |
|---|---|---|
| Codemod event-name remap (Task D) | 1–2 days | Pure JS-side work; needs an event-name dictionary plus AST pass. |
| Codemod dynamic-import + CJS (Task F) | 1–2 days | Mirror the static-ESM path; care needed for awaited dynamic imports' destructuring. |
Shipped: codemod renames start/num/outSpatialReference/spatialRelationship, splits geometry into geometry+geometryType, normalizes outStatistics[i].statisticType against STATISTIC_TYPE_MAP, and emits precise manual TODOs for timeExtent, quantizationParameters, relationParam, dynamic statisticType, and geometry without a literal type. QueryCompat accepts both ArcGIS and Honua property spellings. |
||
Shipped: LocatorCompat + LocatorSearchSourceCompat ride the provider-pluggable geocoding contract, so no HonuaGeocodeService was needed. The provider endpoint is caller-configured, which keeps the wiring step assisted. |
||
| Geoprocessor compat (Task C, remainder) | 1–2 weeks | Requires a HonuaGeoprocessService surface (server work); service-area / OD-cost-matrix / closest-facility likewise. |
| Scene-layer compat (Task A-rest) | 4–8 weeks | Requires I3S/glTF pipeline on honua-server; not pure-JS work. |
Shipped: examples/arcgis-source-app/ + test/migration-e2e.test.ts, wired into the JS SDK CI workflow after the unit-test step. |
With Tasks D/F/E and the geocoding half of Task C now shipped,
"automated app conversion" reaches deterministic constructor rewrite +
event-name remap + dynamic/CJS imports + Query argument deep-transform +
Locator/Search address backends, plus a manual-TODO report for
everything else. The remaining rows (Geoprocessor/NetworkAnalyst and
Scene-layer compat) still gate the unqualified "parity with ArcGIS JS
SDK" claim — that should remain qualified as "deterministic 2D parity;
scene/3D and advanced tasks remain assisted migration" until they
land.