esri-gl 0.9.0-alpha.13 → 0.9.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -368,7 +368,7 @@ For the smoothest React experience with declarative layer management:
368
368
 
369
369
  ```typescript
370
370
  import React, { useState } from 'react';
371
- import { Map } from 'react-map-gl';
371
+ import { Map } from 'react-map-gl/mapbox';
372
372
  import {
373
373
  EsriDynamicLayer,
374
374
  EsriFeatureLayer,
@@ -485,7 +485,7 @@ import type {
485
485
  IdentifyResult,
486
486
  EsriLayerProps
487
487
  } from 'esri-gl';
488
- import type { MapRef } from 'react-map-gl';
488
+ import type { MapRef } from 'react-map-gl/mapbox';
489
489
 
490
490
  // Fully typed component props
491
491
  interface MapComponentProps {
@@ -653,6 +653,8 @@ All type declarations are available in the `dist/` directory after building.
653
653
 
654
654
  ## Contributing
655
655
 
656
+ We welcome contributions! Please follow these steps:
657
+
656
658
  1. Fork the repository
657
659
  2. Create a feature branch: `git checkout -b feature/my-feature`
658
660
  3. Make changes and add tests
@@ -661,6 +663,17 @@ All type declarations are available in the `dist/` directory after building.
661
663
  6. Push to branch: `git push origin feature/my-feature`
662
664
  7. Submit a pull request
663
665
 
666
+ ### Pre-commit Hooks
667
+
668
+ This project uses [Husky](https://typicode.github.io/husky/) to automatically run quality checks before each commit:
669
+
670
+ - **Formatting & Linting**: Automatically formats and lints staged files using Prettier and ESLint
671
+ - **Testing**: Runs the full test suite to ensure no regressions
672
+
673
+ The hooks are automatically installed when you run `npm install`. If you need to skip them (not recommended), you can use `git commit --no-verify`.
674
+
675
+ For more details, see [.husky/README.md](.husky/README.md).
676
+
664
677
  ## License
665
678
 
666
679
  MIT License - see [LICENSE](LICENSE) file for details.
@@ -462,7 +462,11 @@ class DynamicMapService {
462
462
  this.rasterSrcOptions = rasterSrcOptions;
463
463
  this.esriServiceOptions = esriServiceOptions;
464
464
  this._createSource();
465
- if (this.options.getAttributionFromService) this.setAttributionFromService();
465
+ if (this.options.getAttributionFromService) {
466
+ this.setAttributionFromService().catch(() => {
467
+ // Silently handle attribution fetch errors to prevent unhandled rejections
468
+ });
469
+ }
466
470
  }
467
471
  get options() {
468
472
  return {
@@ -543,7 +547,10 @@ class DynamicMapService {
543
547
  };
544
548
  }
545
549
  _createSource() {
546
- this._map.addSource(this._sourceId, this._source);
550
+ // Check if source already exists before adding
551
+ if (!this._map.getSource(this._sourceId)) {
552
+ this._map.addSource(this._sourceId, this._source);
553
+ }
547
554
  }
548
555
  // This requires hooking into some undocumented methods
549
556
  _updateSource() {
@@ -562,11 +569,21 @@ class DynamicMapService {
562
569
  const src = this._map.getSource(this._sourceId);
563
570
  if (!src) return;
564
571
  try {
565
- src.tiles[0] = this._source.tiles[0];
572
+ // Ensure src.tiles exists before accessing it
573
+ if (src.tiles && Array.isArray(src.tiles) && this._source.tiles && this._source.tiles.length > 0) {
574
+ src.tiles[0] = this._source.tiles[0];
575
+ }
566
576
  src._options = this._source;
567
577
  if (src.setTiles) {
568
578
  // New MapboxGL >= 2.13.0
569
- src.setTiles(this._source.tiles);
579
+ // setTiles may return a promise - handle rejections to prevent console errors
580
+ const result = src.setTiles(this._source.tiles);
581
+ if (result && typeof result.catch === 'function') {
582
+ result.catch(() => {
583
+ // Silently ignore - setTiles rejections are often abort errors during rapid updates
584
+ // The outer try/catch will handle any synchronous errors
585
+ });
586
+ }
570
587
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
571
588
  } else if (this._map.style.sourceCaches) {
572
589
  // Old MapboxGL and MaplibreGL
@@ -582,8 +599,21 @@ class DynamicMapService {
582
599
  this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
583
600
  }
584
601
  } catch (error) {
585
- if (error instanceof DOMException && error.name === 'AbortError') {
586
- // Ignore aborted tile refresh; map will request new tiles on next frame
602
+ // Comprehensive abort detection - check all possible error shapes
603
+ // MapLibre can throw various forms of AbortError
604
+ const errorName = error?.name;
605
+ const errorMessage = error?.message;
606
+ const errorConstructor = error?.constructor?.name;
607
+ let stringified = '';
608
+ try {
609
+ stringified = String(error);
610
+ } catch {
611
+ // ignore
612
+ }
613
+ // Check every possible way an error could represent AbortError
614
+ const isAbortError = error instanceof DOMException && error.name === 'AbortError' || error instanceof Error && error.name === 'AbortError' || errorName === 'AbortError' || errorConstructor === 'AbortError' || errorMessage?.toLowerCase().includes('abort') || stringified.toLowerCase().includes('abort') || errorMessage?.includes('AbortError') || stringified.includes('AbortError') || stringified === 'Error: AbortError' || error === 'AbortError';
615
+ if (isAbortError) {
616
+ // Silently ignore aborted tile operations - MapLibre will retry
587
617
  return;
588
618
  }
589
619
  // Swallow occasional transient errors that can happen during style reloads
@@ -1146,7 +1176,54 @@ class DynamicMapService {
1146
1176
  this._updateSource();
1147
1177
  }
1148
1178
  remove() {
1149
- this._map.removeSource(this._sourceId);
1179
+ const map = this._map;
1180
+ if (!map || typeof map.removeSource !== 'function') {
1181
+ return;
1182
+ }
1183
+ try {
1184
+ const mapWithStyle = map;
1185
+ const mapLayerApi = map;
1186
+ const mapSourceApi = map;
1187
+ if (typeof mapWithStyle.getStyle === 'function') {
1188
+ const style = mapWithStyle.getStyle();
1189
+ const layers = style?.layers || [];
1190
+ layers.forEach(layer => {
1191
+ if (layer.source !== this._sourceId) return;
1192
+ if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
1193
+ return;
1194
+ }
1195
+ let hasLayer = false;
1196
+ try {
1197
+ hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
1198
+ } catch {
1199
+ hasLayer = false;
1200
+ }
1201
+ if (!hasLayer) return;
1202
+ try {
1203
+ mapLayerApi.removeLayer(layer.id);
1204
+ } catch (error) {
1205
+ console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
1206
+ }
1207
+ });
1208
+ }
1209
+ if (typeof mapSourceApi.getSource === 'function') {
1210
+ let hasSource = false;
1211
+ try {
1212
+ hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
1213
+ } catch {
1214
+ hasSource = false;
1215
+ }
1216
+ if (hasSource) {
1217
+ try {
1218
+ map.removeSource(this._sourceId);
1219
+ } catch (error) {
1220
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1221
+ }
1222
+ }
1223
+ }
1224
+ } catch (error) {
1225
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1226
+ }
1150
1227
  }
1151
1228
  }
1152
1229
 
@@ -1206,7 +1283,10 @@ class TiledMapService {
1206
1283
  };
1207
1284
  }
1208
1285
  _createSource() {
1209
- this._map.addSource(this._sourceId, this._source);
1286
+ // Check if source already exists before adding
1287
+ if (!this._map.getSource(this._sourceId)) {
1288
+ this._map.addSource(this._sourceId, this._source);
1289
+ }
1210
1290
  }
1211
1291
  setAttributionFromService() {
1212
1292
  if (this._serviceMetadata) {
@@ -1233,7 +1313,39 @@ class TiledMapService {
1233
1313
  this._map.getSource(this._sourceId);
1234
1314
  }
1235
1315
  remove() {
1236
- this._map.removeSource(this._sourceId);
1316
+ // Guard against disposed or invalid map
1317
+ if (!this._map || typeof this._map.removeSource !== 'function') {
1318
+ return;
1319
+ }
1320
+ try {
1321
+ // First, remove any layers that are using this source
1322
+ const mapWithStyle = this._map;
1323
+ if (mapWithStyle.getStyle && typeof mapWithStyle.getLayer === 'function') {
1324
+ const style = mapWithStyle.getStyle();
1325
+ const layers = style?.layers || [];
1326
+ const getLayer = mapWithStyle.getLayer;
1327
+ layers.forEach(layer => {
1328
+ if (layer.source === this._sourceId) {
1329
+ try {
1330
+ if (getLayer(layer.id)) {
1331
+ this._map.removeLayer(layer.id);
1332
+ }
1333
+ } catch {
1334
+ // Layer may already be removed
1335
+ }
1336
+ }
1337
+ });
1338
+ }
1339
+ // Then check if source exists before trying to remove it
1340
+ if (typeof this._map.getSource === 'function') {
1341
+ const source = this._map.getSource(this._sourceId);
1342
+ if (source) {
1343
+ this._map.removeSource(this._sourceId);
1344
+ }
1345
+ }
1346
+ } catch (error) {
1347
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1348
+ }
1237
1349
  }
1238
1350
  }
1239
1351
 
@@ -1294,7 +1406,11 @@ class ImageService {
1294
1406
  this.rasterSrcOptions = rasterSrcOptions;
1295
1407
  this.esriServiceOptions = esriServiceOptions;
1296
1408
  this._createSource();
1297
- if (this.options.getAttributionFromService) this.setAttributionFromService();
1409
+ if (this.options.getAttributionFromService) {
1410
+ this.setAttributionFromService().catch(() => {
1411
+ // Silently handle attribution fetch errors to prevent unhandled rejections
1412
+ });
1413
+ }
1298
1414
  }
1299
1415
  get options() {
1300
1416
  return {
@@ -1332,11 +1448,18 @@ class ImageService {
1332
1448
  };
1333
1449
  }
1334
1450
  _createSource() {
1335
- this._map.addSource(this._sourceId, this._source);
1451
+ // Check if source already exists before adding
1452
+ if (!this._map.getSource(this._sourceId)) {
1453
+ this._map.addSource(this._sourceId, this._source);
1454
+ }
1336
1455
  }
1337
1456
  // This requires hooking into some undocumented methods
1338
1457
  _updateSource() {
1339
1458
  const src = this._map.getSource(this._sourceId);
1459
+ if (!src) {
1460
+ // Source not yet added to map, nothing to update
1461
+ return;
1462
+ }
1340
1463
  src.tiles[0] = this._source.tiles[0];
1341
1464
  src._options = this._source;
1342
1465
  if (src.setTiles) {
@@ -1411,7 +1534,27 @@ class ImageService {
1411
1534
  this._updateSource();
1412
1535
  }
1413
1536
  remove() {
1414
- this._map.removeSource(this._sourceId);
1537
+ if (this._map && typeof this._map.removeSource === 'function') {
1538
+ try {
1539
+ // First, remove any layers that are using this source
1540
+ const mapWithStyle = this._map;
1541
+ if (mapWithStyle.getStyle) {
1542
+ const style = mapWithStyle.getStyle();
1543
+ const layers = style?.layers || [];
1544
+ layers.forEach(layer => {
1545
+ if (layer.source === this._sourceId && this._map.getLayer(layer.id)) {
1546
+ this._map.removeLayer(layer.id);
1547
+ }
1548
+ });
1549
+ }
1550
+ // Then check if source exists before trying to remove it
1551
+ if (this._map.getSource && this._map.getSource(this._sourceId)) {
1552
+ this._map.removeSource(this._sourceId);
1553
+ }
1554
+ } catch (error) {
1555
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1556
+ }
1557
+ }
1415
1558
  }
1416
1559
  }
1417
1560
 
@@ -1659,7 +1802,10 @@ class VectorTileService {
1659
1802
  };
1660
1803
  }
1661
1804
  _createSource() {
1662
- this._map.addSource(this._sourceId, this._source);
1805
+ // Check if source already exists before adding
1806
+ if (!this._map.getSource(this._sourceId)) {
1807
+ this._map.addSource(this._sourceId, this._source);
1808
+ }
1663
1809
  }
1664
1810
  _mapToLocalSource(style) {
1665
1811
  return {
@@ -1720,7 +1866,54 @@ class VectorTileService {
1720
1866
  // Vector tile services don't need dynamic updates like dynamic services
1721
1867
  }
1722
1868
  remove() {
1723
- this._map.removeSource(this._sourceId);
1869
+ const map = this._map;
1870
+ if (!map || typeof map.removeSource !== 'function') {
1871
+ return;
1872
+ }
1873
+ try {
1874
+ const mapWithStyle = map;
1875
+ const mapLayerApi = map;
1876
+ const mapSourceApi = map;
1877
+ if (typeof mapWithStyle.getStyle === 'function') {
1878
+ const style = mapWithStyle.getStyle();
1879
+ const layers = style?.layers || [];
1880
+ layers.forEach(layer => {
1881
+ if (layer.source !== this._sourceId) return;
1882
+ if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
1883
+ return;
1884
+ }
1885
+ let hasLayer = false;
1886
+ try {
1887
+ hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
1888
+ } catch {
1889
+ hasLayer = false;
1890
+ }
1891
+ if (!hasLayer) return;
1892
+ try {
1893
+ mapLayerApi.removeLayer(layer.id);
1894
+ } catch (error) {
1895
+ console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
1896
+ }
1897
+ });
1898
+ }
1899
+ if (typeof mapSourceApi.getSource === 'function') {
1900
+ let hasSource = false;
1901
+ try {
1902
+ hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
1903
+ } catch {
1904
+ hasSource = false;
1905
+ }
1906
+ if (hasSource) {
1907
+ try {
1908
+ map.removeSource(this._sourceId);
1909
+ } catch (error) {
1910
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1911
+ }
1912
+ }
1913
+ }
1914
+ } catch (error) {
1915
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1916
+ }
1724
1917
  }
1725
1918
  }
1726
1919
 
@@ -1803,46 +1996,35 @@ class FeatureService {
1803
1996
  this._createSource();
1804
1997
  }
1805
1998
  async _createSource() {
1999
+ if (!this._map) return;
1806
2000
  try {
1807
2001
  // Get service metadata
1808
2002
  this._serviceMetadata = await getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions);
1809
2003
  // Check if vector tiles should be used (default behavior)
1810
2004
  // Note: Most FeatureServers don't support vector tiles, so we'll detect and fallback
1811
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1812
- if (!isTestEnvironment) {
1813
- console.log('FeatureService: useVectorTiles setting:', this.esriServiceOptions.useVectorTiles);
1814
- }
1815
2005
  const vectorTileSupport = await this._checkVectorTileSupport();
1816
- if (!isTestEnvironment) {
1817
- console.log('FeatureService: Vector tile support detected:', vectorTileSupport);
1818
- }
1819
2006
  const useVectorTiles = this.esriServiceOptions.useVectorTiles !== false && vectorTileSupport;
1820
- if (!isTestEnvironment) {
1821
- console.log('FeatureService: Final decision - using vector tiles:', useVectorTiles);
1822
- }
1823
2007
  if (useVectorTiles) {
1824
2008
  // Create vector tile source
1825
2009
  const tileUrl = this._buildTileUrl();
1826
- if (!isTestEnvironment) {
1827
- console.log('FeatureService: Using vector tiles for FeatureService:', tileUrl);
2010
+ // Add vector source to map if it doesn't already exist
2011
+ if (!this._map.getSource(this._sourceId)) {
2012
+ this._map.addSource(this._sourceId, {
2013
+ type: 'vector',
2014
+ tiles: [tileUrl],
2015
+ maxzoom: 24,
2016
+ ...this.vectorSrcOptions
2017
+ });
1828
2018
  }
1829
- // Add vector source to map
1830
- this._map.addSource(this._sourceId, {
1831
- type: 'vector',
1832
- tiles: [tileUrl],
1833
- maxzoom: 24,
1834
- ...this.vectorSrcOptions
1835
- });
1836
2019
  } else {
1837
2020
  // Fallback to GeoJSON (most common for FeatureServers)
1838
2021
  const queryUrl = this._buildQueryUrl();
1839
- if (!isTestEnvironment) {
1840
- console.log('FeatureService: Using GeoJSON for FeatureService:', queryUrl);
2022
+ if (!this._map.getSource(this._sourceId)) {
2023
+ this._map.addSource(this._sourceId, {
2024
+ type: 'geojson',
2025
+ data: queryUrl
2026
+ });
1841
2027
  }
1842
- this._map.addSource(this._sourceId, {
1843
- type: 'geojson',
1844
- data: queryUrl
1845
- });
1846
2028
  }
1847
2029
  // Update attribution after source is added if available in service metadata
1848
2030
  if (this._serviceMetadata?.copyrightText) {
@@ -1867,42 +2049,20 @@ class FeatureService {
1867
2049
  const vectorTileUrl = this.esriServiceOptions.url.replace('/FeatureServer/', '/VectorTileServer/');
1868
2050
  // Only check if the URL actually changed (meaning it was a FeatureServer URL)
1869
2051
  if (vectorTileUrl === this.esriServiceOptions.url) {
1870
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1871
- if (!isTestEnvironment) {
1872
- console.log('FeatureService: Not a FeatureServer URL, falling back to GeoJSON');
1873
- }
1874
2052
  return false;
1875
2053
  }
1876
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1877
- if (!isTestEnvironment) {
1878
- console.log('FeatureService: Checking vector tile support at:', vectorTileUrl);
1879
- }
1880
2054
  const response = await fetch(vectorTileUrl + '?f=json', this.esriServiceOptions.fetchOptions);
1881
2055
  if (response.ok) {
1882
2056
  const data = await response.json();
1883
2057
  if (data && !data.error) {
1884
- if (!isTestEnvironment) {
1885
- console.log('FeatureService: Vector tile endpoint found and working:', vectorTileUrl);
1886
- console.log('FeatureService: Vector tile service data:', data);
1887
- }
1888
2058
  return true;
1889
2059
  } else {
1890
- if (!isTestEnvironment) {
1891
- console.log('FeatureService: Vector tile endpoint returned error:', data?.error);
1892
- }
1893
2060
  return false;
1894
2061
  }
1895
2062
  } else {
1896
- if (!isTestEnvironment) {
1897
- console.log('FeatureService: Vector tile endpoint returned HTTP', response.status);
1898
- }
1899
2063
  return false;
1900
2064
  }
1901
- } catch (error) {
1902
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1903
- if (!isTestEnvironment) {
1904
- console.log('FeatureService: Vector tile check failed, falling back to GeoJSON:', error);
1905
- }
2065
+ } catch {
1906
2066
  return false;
1907
2067
  }
1908
2068
  }
@@ -2047,10 +2207,6 @@ class FeatureService {
2047
2207
  const source = this._map.getSource(this._sourceId);
2048
2208
  if (source && 'setData' in source && typeof source.setData === 'function') {
2049
2209
  const newQueryUrl = this._buildQueryUrl();
2050
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
2051
- if (!isTestEnvironment) {
2052
- console.log('Updating FeatureService data with new bounding box:', newQueryUrl);
2053
- }
2054
2210
  // @ts-ignore - GeoJSON source setData method not in generic Source type
2055
2211
  source.setData(newQueryUrl);
2056
2212
  }
@@ -2131,8 +2287,26 @@ class FeatureService {
2131
2287
  // Note: maxRecordCount is a server capability; not settable via query params
2132
2288
  remove() {
2133
2289
  this._removeBoundingBoxUpdates();
2134
- if (this._map.getSource(this._sourceId)) {
2135
- this._map.removeSource(this._sourceId);
2290
+ if (this._map && typeof this._map.removeSource === 'function') {
2291
+ try {
2292
+ // First, remove any layers that are using this source
2293
+ const mapWithStyle = this._map;
2294
+ if (mapWithStyle.getStyle) {
2295
+ const style = mapWithStyle.getStyle();
2296
+ const layers = style?.layers || [];
2297
+ layers.forEach(layer => {
2298
+ if (layer.source === this._sourceId && this._map.getLayer && this._map.getLayer(layer.id)) {
2299
+ this._map.removeLayer(layer.id);
2300
+ }
2301
+ });
2302
+ }
2303
+ // Then check if source exists before trying to remove it
2304
+ if (this._map.getSource && this._map.getSource(this._sourceId)) {
2305
+ this._map.removeSource(this._sourceId);
2306
+ }
2307
+ } catch (error) {
2308
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
2309
+ }
2136
2310
  }
2137
2311
  }
2138
2312
  async queryFeatures(options) {