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:

  1. "Parity with the ArcGIS JS SDK" — the symbol surface app authors can import and call against.
  2. "Automated app conversion" — the src/migration codemod 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:

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:

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

  1. 3D / SceneView. SceneView exists as a compat class but it shares the 2D MapView behavior under the hood. Anything that requires a WebGL/CesiumJS path (environment, viewingMode: "global", goTo with altitude, Ground, ElevationLayer, scene layers, Camera, qualityProfile) is not implemented. The codemod sets the scene-3d-detected blocking flag when it sees such imports, so the gate fails closed.
  2. Scene layers. SceneLayer, BuildingSceneLayer, IntegratedMeshLayer, PointCloudLayer, MeshLayer, ElevationLayer, VoxelLayer have no shims. These all need server-side glTF/I3S support (honua-server ticket).
  3. Locator / Geoprocessor / NetworkAnalyst (beyond RouteTask). Today only RouteTask is shimmed against HonuaRouteService. The remaining task surfaces (geocoding, service-area, OD-cost-matrix, closest-facility, non-network geoprocessing) need their own surfaces under src/core/ and matching compat classes.
  4. Query argument depth. Shipped (Task E). The codemod now deep-transforms new Query({...}) argument literals into the Honua QueryFeaturesRequest shape: renames startresultOffset, numresultRecordCount, outSpatialReferenceoutSr, spatialRelationshipspatialRel; splits geometry: { type, ...rest } into a sibling geometryType field with the matching esriGeometry* enum value; and lowercases outStatistics[i].statisticType against 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; use extraParams.time), quantizationParameters (server-side optimization with no Honua equivalent), relationParam (no Honua support), geometry without a string-literal type discriminator, and outStatistics items with a non-literal statisticType or an unknown enum value. The shim QueryCompat accepts either ArcGIS or Honua property spellings (spatialRelationship / spatialRel, outSpatialReference / outSr, num / resultRecordCount, start / resultOffset) so codemod output and hand-written callers both validate.
  5. Popup actions / popup templates with actions, outFields: ["*"] expansions, and fieldInfos formatters. These rewrite cleanly only for the simple case; arrow-function field-info format callbacks fall through to manual TODO.
  6. 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 on calcite-action---prefixed selectors will need style work.
  7. reactiveUtils.watch semantics (partial). Compat watch is property-name-based and synchronous. ArcGIS watch accepts an accessor function and returns a WatchHandle with pause() / resume(). Single-property accessors — reactiveUtils.watch(() => view.scale, h) where view is a tracked compat instance — are now rewritten by the codemod to reactiveUtils.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":

  1. 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 in ARCGIS_TO_COMPAT_EVENT_REMAP in src/migration/codemod.ts and currently covers layerview-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, and refresh. Receiver-aware scoping is now in place: the rewrite only fires when the receiver is an Identifier whose binding came from new XCompat(...) where X was a tracked ArcGIS import (via resolveAssignedIdentifierForNewExpression). 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.
  2. Dynamic / CJS imports (Task F, partial). Dynamic ESM imports (import("@arcgis/core/...")) are rewritten through the isArcGisDynamicImportCall pass and have test coverage for SceneView, esriConfig, esriRequest, and IdentityManager. CommonJS require("@arcgis/core/...") is now auto-rewritten for the honua-compat target when constructor options are safe: the codemod emits const { XCompat } = require("@honua/sdk-esri-compat"); and rewrites new X(opts) to new XCompat(opts) in .cjs files and .js files containing CommonJS markers. The original const X = require("@arcgis/core/...") declaration is now pruned when no other references to X remain 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). The esri-leaflet target still emits a manual TODO for CJS require constructors.
  3. Module re-exports beyond the registered set. The codemod already follows localArcGisReExports for one level. Deeper barrel-file chains (./esri.ts./layers/index.ts → …) aren't followed.
  4. view.map.add(layer) argument-shape transformations. When a new 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.field casing, popupTemplate.content block shapes). The shim accepts the ArcGIS shape at runtime, but the report should flag known-divergent property values so app authors can confirm intent.
  5. WebMap JSON → MapLibre style derivation (honua-maplibre target). Partially shipped. src/map/webmap-maplibre.ts exposes webmapJsonToMapLibreStyle(webmap, options), a pure converter that delegates the heavy lifting to the existing src/webmap/ pipeline — parseWebMap orchestrates convertBasemap, convertOperationalLayer, convertRenderer, convertPopupInfo, convertLabelingInfo, and convertExtent/convertInitialViewpoint — and returns a HonuaStyleSpecification ready for map.setStyle(...) plus a structured manualGaps: WebMapMapLibreManualGap[] array. manualGaps is the single source of truth for unsupported constructs: every entry carries { kind, reason, path?, layerId?, expression?, context? } and the kind taxonomy covers arcade-expression (popup expressionInfos and complex Arcade label expressions), unsupported-renderer (heatmap, dotDensity, and any other renderer the converter doesn't handle), scene-3d (top-level ground / camera / viewingMode, ArcGISSceneServiceLayer, elevationInfo, heightInfo, sceneProperties), dashboard-reference, experience-builder-reference, custom-widget-reference (applicationProperties.viewing.widgetsOnScreen and the top-level widgets array used by host shells), plus pass-throughs for unsupported-symbol, unsupported-layer-type, unsupported-feature-collection, and unsupported-webmap-version. The migration codemod now wires this converter into its honua-maplibre constructor-rewriting pass: any new WebMap({ ... }) call whose argument is a static WebMap JSON object literal (top-level operationalLayers and/or baseMap, fully literal values, no portalItem) is rewritten to webmapJsonToMapLibreStyle({ ... }) from @honua/sdk-js/map, and the helper's manualGaps array is propagated into the migration report as per-gap web-map manual 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 a web-map manual 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 // TODO anchor for an app author, not a promise of automated migration.
  6. End-to-end demo conversion. Shipped at examples/arcgis-source-app/ + test/migration-e2e.test.ts. The sample is a hand-written parcel viewer exercising FeatureLayer (with outFields + popupTemplate), Map, MapView, an untouched view.on("click", …) handler, and an event-name-remapped layer.on("layerview-create", …) handler. The e2e test copies it into a tempdir, runs the codemod with target: "honua-compat", asserts every expected rewrite landed and the readiness gate is "ready", and then runs tsc --noEmit against the migrated source with paths resolving @honua/sdk-esri-compat to the workspace src/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.
Query argument deep-transform (Task E) 3–5 days 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.
E2E demo conversion harness (Task I) 1 week 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.