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,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 three gates (no-manual-todos,no-unhandled-modules,no-blocking-flags) and a readiness verdict. - 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). Locator/Geoprocessor/NetworkAnalyst(beyond RouteTask). Today onlyRouteTaskis shimmed againstHonuaRouteService. The remaining task surfaces (geocoding, service-area, OD-cost-matrix, closest-facility, non-network geoprocessing) 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 they render through the
Honua widget host (
HonuaWidgetHost). 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. 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. - 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. |
||
| Locator + Geoprocessor compat (Task C) | 1–2 weeks | Requires HonuaGeocodeService, HonuaGeoprocessService surfaces (server work). |
| 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 the first three rows (Tasks D/F/E) now shipped, "automated app conversion" reaches deterministic constructor rewrite + event-name remap + dynamic/CJS imports + Query argument deep-transform, plus a manual-TODO report for everything else. The remaining rows (Locator/Geoprocessor 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.