@octanejs/react-map-gl 0.0.1
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/LICENSE +62 -0
- package/README.md +112 -0
- package/UPSTREAM.md +216 -0
- package/package.json +61 -0
- package/src/components/attribution-control.ts +27 -0
- package/src/components/fullscreen-control.ts +35 -0
- package/src/components/geolocate-control.ts +88 -0
- package/src/components/layer.ts +135 -0
- package/src/components/map.tsrx +173 -0
- package/src/components/marker.ts +217 -0
- package/src/components/navigation-control.ts +27 -0
- package/src/components/popup.ts +100 -0
- package/src/components/scale-control.ts +44 -0
- package/src/components/source.tsrx +146 -0
- package/src/components/use-control.ts +73 -0
- package/src/components/use-map.tsrx +64 -0
- package/src/index.ts +33 -0
- package/src/mapbox/create-ref.ts +114 -0
- package/src/mapbox/mapbox.ts +714 -0
- package/src/mapbox/proxy-transform.ts +155 -0
- package/src/types/common.ts +41 -0
- package/src/types/events.ts +130 -0
- package/src/types/internal.ts +20 -0
- package/src/types/lib.ts +70 -0
- package/src/types/octane.ts +27 -0
- package/src/types/style-spec.ts +51 -0
- package/src/utils/apply-react-style.ts +26 -0
- package/src/utils/assert.ts +10 -0
- package/src/utils/children.ts +26 -0
- package/src/utils/compare-class-names.ts +34 -0
- package/src/utils/deep-equal.ts +66 -0
- package/src/utils/set-globals.ts +62 -0
- package/src/utils/style-utils.ts +65 -0
- package/src/utils/transform.ts +115 -0
- package/src/utils/use-isomorphic-layout-effect.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
@octanejs/react-map-gl is a port of @vis.gl/react-mapbox (react-map-gl).
|
|
2
|
+
Adapted algorithms, types and reused modules retain the upstream notice below.
|
|
3
|
+
|
|
4
|
+
Copyright Vis.gl contributors.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
This contains code from MapboxGL-js
|
|
25
|
+
|
|
26
|
+
Copyright (c) 2014, Mapbox
|
|
27
|
+
|
|
28
|
+
All rights reserved.
|
|
29
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
30
|
+
are permitted provided that the following conditions are met:
|
|
31
|
+
|
|
32
|
+
* Redistributions of source code must retain the above copyright notice,
|
|
33
|
+
this list of conditions and the following disclaimer.
|
|
34
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
35
|
+
this list of conditions and the following disclaimer in the documentation
|
|
36
|
+
and/or other materials provided with the distribution.
|
|
37
|
+
* Neither the name of Mapbox GL JS nor the names of its contributors
|
|
38
|
+
may be used to endorse or promote products derived from this software
|
|
39
|
+
without specific prior written permission.
|
|
40
|
+
|
|
41
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
42
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
43
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
44
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
45
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
46
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
47
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
48
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
49
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
50
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
51
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
52
|
+
|
|
53
|
+
-------------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
Contains code from Mapbox GL Style JSON
|
|
56
|
+
|
|
57
|
+
Mapbox Open Styles are copyright (c) 2014, Mapbox, all rights reserved.
|
|
58
|
+
|
|
59
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
60
|
+
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
61
|
+
|
|
62
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @octanejs/react-map-gl
|
|
2
|
+
|
|
3
|
+
Mapbox GL JS bindings for Octane — a port of
|
|
4
|
+
[`@vis.gl/react-mapbox@8.1.2`](https://github.com/visgl/react-map-gl), the
|
|
5
|
+
package `react-map-gl/mapbox` re-exports.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @octanejs/react-map-gl mapbox-gl
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`mapbox-gl` is an optional peer dependency (`>= 3.5.0`) and is never bundled.
|
|
12
|
+
From v2 it ships under the Mapbox Terms of Service and bills per map load, so
|
|
13
|
+
you bring your own copy and your own access token.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Layer, Map, Marker, NavigationControl, Source } from '@octanejs/react-map-gl';
|
|
17
|
+
import 'mapbox-gl/dist/mapbox-gl.css';
|
|
18
|
+
|
|
19
|
+
export function CityMap() @{
|
|
20
|
+
<Map
|
|
21
|
+
mapboxAccessToken={import.meta.env.VITE_MAPBOX_TOKEN}
|
|
22
|
+
initialViewState={{ longitude: -122.4, latitude: 37.8, zoom: 11 }}
|
|
23
|
+
mapStyle="mapbox://styles/mapbox/streets-v12"
|
|
24
|
+
style={{ width: '100%', height: 480 }}
|
|
25
|
+
>
|
|
26
|
+
<NavigationControl />
|
|
27
|
+
<Marker longitude={-122.4} latitude={37.8}>
|
|
28
|
+
<span class="pin">📍</span>
|
|
29
|
+
</Marker>
|
|
30
|
+
<Source id="places" type="geojson" data={places}>
|
|
31
|
+
<Layer id="places-circles" type="circle" paint={{ 'circle-radius': 6 }} />
|
|
32
|
+
</Source>
|
|
33
|
+
</Map>
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Compatibility
|
|
38
|
+
|
|
39
|
+
Complete against the pinned upstream public surface: `Map` (also the default
|
|
40
|
+
export), `Marker`, `Popup`, `Source`, `Layer`, `AttributionControl`,
|
|
41
|
+
`FullscreenControl`, `GeolocateControl`, `NavigationControl`, `ScaleControl`,
|
|
42
|
+
`useControl`, `MapProvider`, `useMap`, and every published type.
|
|
43
|
+
|
|
44
|
+
Both `@octanejs/react-map-gl` and `@octanejs/react-map-gl/mapbox` resolve to the
|
|
45
|
+
same surface, so either import style works.
|
|
46
|
+
|
|
47
|
+
Upstream's framework-neutral half — the Mapbox engine, the proxy transform, the
|
|
48
|
+
map ref and six utility modules — is reused **byte-for-byte**, and upstream's own
|
|
49
|
+
specs for it run here against both upstream's source and this package's copies.
|
|
50
|
+
|
|
51
|
+
## Intentional differences
|
|
52
|
+
|
|
53
|
+
**Refs are plain props.** Octane has no `forwardRef`, so `Map`, `Marker`, `Popup`
|
|
54
|
+
and `GeolocateControl` declare `ref` as an ordinary prop. `<Map ref={mapRef} />`
|
|
55
|
+
is unchanged; only your own `forwardRef` wrappers need removing.
|
|
56
|
+
|
|
57
|
+
**`<Source>` publishes its id through context.** Upstream clones each child with
|
|
58
|
+
`{source: id}`. Octane cannot clone a compiled children block, so the id travels
|
|
59
|
+
by context instead. The enclosing source still wins over a `source` set on the
|
|
60
|
+
layer, exactly as `cloneElement` made it win — but the id now reaches *any*
|
|
61
|
+
descendant `<Layer>`, not just direct children. Give a layer an explicit source
|
|
62
|
+
and keep it outside the `<Source>` if it must not belong to it.
|
|
63
|
+
|
|
64
|
+
**`<Marker>` picks its element from what your children rendered.** Upstream asks
|
|
65
|
+
`React.Children.forEach` whether it was handed a truthy child, and gives the
|
|
66
|
+
marker its own element to portal into if so. A compiled children block is opaque,
|
|
67
|
+
and evaluating it to look inside would re-run any hooks it contains against the
|
|
68
|
+
same call-site slots, so this binding infers the answer from what the block
|
|
69
|
+
rendered and rebuilds the marker once it knows. Children that render something,
|
|
70
|
+
render nothing, or first render after mount all behave as they do upstream. The
|
|
71
|
+
one difference: a child that stays truthy while never rendering anything gets
|
|
72
|
+
Mapbox's default pin here, where upstream leaves an empty, invisible element.
|
|
73
|
+
|
|
74
|
+
**Teardown is not synchronous with unmount.** Effect cleanups run on the passive
|
|
75
|
+
drain that follows `root.unmount()`, so `map.remove()` — and with it the WebGL
|
|
76
|
+
context and worker pool — happens one drain later. Do not assume the map's
|
|
77
|
+
resources are released the instant `unmount()` returns.
|
|
78
|
+
|
|
79
|
+
**Out of scope.** `react-map-gl/mapbox-legacy` (mapbox-gl v1) and
|
|
80
|
+
`@vis.gl/react-maplibre`.
|
|
81
|
+
|
|
82
|
+
## Server rendering
|
|
83
|
+
|
|
84
|
+
`Map` renders its container — with your `style` merged over the binding's
|
|
85
|
+
defaults, so the box is reserved and hydration does not shift the page — and
|
|
86
|
+
omits every child. `mapbox-gl` is imported inside an effect, so nothing on the
|
|
87
|
+
server path touches WebGL or a browser global.
|
|
88
|
+
|
|
89
|
+
`hydrateRoot` adopts that container rather than replacing it: the map is created
|
|
90
|
+
inside the node the server emitted, so the reserved box, its style and anything
|
|
91
|
+
you rendered around it survive hydration.
|
|
92
|
+
|
|
93
|
+
## Evidence, and its limits
|
|
94
|
+
|
|
95
|
+
See [`UPSTREAM.md`](./UPSTREAM.md) for the pin, the full export crosswalk, and
|
|
96
|
+
the disposition of every upstream test file. The short version:
|
|
97
|
+
|
|
98
|
+
- upstream's five framework-neutral specs run byte-exact against both source
|
|
99
|
+
trees;
|
|
100
|
+
- upstream's seven component specs need a live Mapbox token and real WebGL under
|
|
101
|
+
puppeteer, so they are ported against a test double — weaker evidence than a
|
|
102
|
+
pristine run, and recorded as such;
|
|
103
|
+
- a differential lane runs six fixtures through this binding and through the
|
|
104
|
+
published `@vis.gl/react-mapbox@8.1.2` on React with that same double, which is
|
|
105
|
+
what makes the double trustworthy: the map shell and its overlays,
|
|
106
|
+
`<Source>`/`<Layer>` add-update-remove, in-place popup option edits alongside
|
|
107
|
+
control add and remove, reaching the map by id from outside it to fly the
|
|
108
|
+
camera, `useControl` called straight from a consumer module, and a marker
|
|
109
|
+
choosing between the default pin and a custom element.
|
|
110
|
+
|
|
111
|
+
Not covered: real WebGL, tile loading, pointer interaction, `reuseMaps`,
|
|
112
|
+
external `gl` contexts, and RTL text plugin loading.
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Upstream @vis.gl/react-mapbox audit
|
|
2
|
+
|
|
3
|
+
This port targets the immutable release `@vis.gl/react-mapbox@8.1.2`:
|
|
4
|
+
|
|
5
|
+
- repository: `https://github.com/visgl/react-map-gl`;
|
|
6
|
+
- tag: `v8.1.2`, tag object `fb9c6230b59eb0cde25b53a2d82481d83c968866`;
|
|
7
|
+
- tag commit: `b1e46fcfb9d5de9bb179ca54e7d27617caa28c65`;
|
|
8
|
+
- source root: `modules/react-mapbox/src`; test root `modules/react-mapbox/test`;
|
|
9
|
+
- license: MIT, Copyright Vis.gl contributors.
|
|
10
|
+
|
|
11
|
+
`react-map-gl@8.1.2` is a re-export shell: its `./mapbox` subpath is this
|
|
12
|
+
package, `./maplibre` is `@vis.gl/react-maplibre`, and `./mapbox-legacy` is a
|
|
13
|
+
mapbox-gl v1 compatibility build. The port therefore targets the real source
|
|
14
|
+
package, and publishes both `.` and `./mapbox` so either import works.
|
|
15
|
+
|
|
16
|
+
The advertised peer range is `mapbox-gl >= 3.5.0`, matching upstream. The
|
|
17
|
+
immutable oracle is `mapbox-gl@3.9.0`, the release upstream develops against.
|
|
18
|
+
`mapbox-gl` is an optional peer and is never vendored: from v2 it ships under
|
|
19
|
+
the Mapbox Terms of Service rather than an OSS license, and it bills per map
|
|
20
|
+
load against an access token.
|
|
21
|
+
|
|
22
|
+
## Source boundary
|
|
23
|
+
|
|
24
|
+
Fourteen upstream modules carry no React import and are reused **byte-for-byte**
|
|
25
|
+
under a five-line provenance banner (`@ts-nocheck` plus the reason). Strip the
|
|
26
|
+
banner and the bytes equal `upstream/src/<same path>` exactly, which is what
|
|
27
|
+
makes an upstream bump a readable diff:
|
|
28
|
+
|
|
29
|
+
- `mapbox/mapbox.ts`, `mapbox/proxy-transform.ts`, `mapbox/create-ref.ts`;
|
|
30
|
+
- `types/common.ts`, `types/events.ts`, `types/internal.ts`, `types/lib.ts`,
|
|
31
|
+
`types/style-spec.ts`;
|
|
32
|
+
- `utils/assert.ts`, `utils/compare-class-names.ts`, `utils/deep-equal.ts`,
|
|
33
|
+
`utils/set-globals.ts`, `utils/style-utils.ts`, `utils/transform.ts`.
|
|
34
|
+
|
|
35
|
+
Upstream compiles without `strictNullChecks` or `noImplicitAny`; this repository
|
|
36
|
+
is strict. Rather than edit vendored source to satisfy a constraint its authors
|
|
37
|
+
never applied, those files carry `@ts-nocheck` and are typechecked at upstream's
|
|
38
|
+
own settings through `upstream/`. Everything the port authors is fully strict.
|
|
39
|
+
|
|
40
|
+
`upstream/tsconfig.json` is deliberately not vendored: it is vis.gl monorepo
|
|
41
|
+
build config, excluded from the published package by its own `files` field, and
|
|
42
|
+
its `extends` path does not resolve outside that repository.
|
|
43
|
+
|
|
44
|
+
Everything else is re-implemented against Octane hooks.
|
|
45
|
+
|
|
46
|
+
## Export crosswalk
|
|
47
|
+
|
|
48
|
+
Every runtime and type export of `modules/react-mapbox/src/index.ts` at the pin.
|
|
49
|
+
|
|
50
|
+
| Export | Kind | Disposition | Evidence |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `Map` (also `default`) | component | ported | `tests/upstream/map.test.ts` (7 cases), `tests/ssr/ssr.test.ts`, `tests/hydration/hydration.test.ts`, differential |
|
|
53
|
+
| `Marker` | component | ported | `tests/upstream/marker.test.ts` (4 cases), differential |
|
|
54
|
+
| `Popup` | component | ported | `tests/upstream/popup.test.ts` (2 cases), `differential:1`, `differential:3` |
|
|
55
|
+
| `Source` | component | ported, one divergence | `tests/upstream/source-layer.test.ts` (1 case), `differential:2` |
|
|
56
|
+
| `Layer` | component | ported, one divergence | `tests/upstream/source-layer.test.ts` (2 cases), `differential:2` |
|
|
57
|
+
| `AttributionControl` | component | ported | `tests/upstream/controls.test.ts:1` |
|
|
58
|
+
| `FullscreenControl` | component | ported | `tests/upstream/controls.test.ts:2` |
|
|
59
|
+
| `GeolocateControl` | component | ported | `tests/upstream/controls.test.ts:3` |
|
|
60
|
+
| `NavigationControl` | component | ported | `tests/upstream/controls.test.ts:4`, `:7` |
|
|
61
|
+
| `ScaleControl` | component | ported | `tests/upstream/controls.test.ts:5`, `:6` |
|
|
62
|
+
| `useControl` | hook | ported | exercised by all five controls; `:6` pins in-place reconfiguration; `differential:5` pins the consumer-facing hook itself |
|
|
63
|
+
| `MapProvider` | component | ported | `tests/upstream/use-map.test.ts` (2 cases), `differential:4` |
|
|
64
|
+
| `useMap` | hook | ported | `tests/upstream/use-map.test.ts` (2 cases), `differential:4` |
|
|
65
|
+
| `MapProps` | type | ported | `typetests/*/types.test-d.ts` group 1–2 |
|
|
66
|
+
| `MapRef` | type | reused verbatim (`mapbox/create-ref.ts`) | `tests/upstream/map.test.ts:1` |
|
|
67
|
+
| `MarkerProps` | type | ported | type suites group 3–4 |
|
|
68
|
+
| `PopupProps` | type | ported | type suites (shape mirrors `MarkerProps`) |
|
|
69
|
+
| `SourceProps` | type | ported | type suites group 5 |
|
|
70
|
+
| `LayerProps` | type | ported | type suites group 6 |
|
|
71
|
+
| `AttributionControlProps` | type | ported | compiled by the adapted type suite |
|
|
72
|
+
| `FullscreenControlProps` | type | ported | compiled by the adapted type suite |
|
|
73
|
+
| `GeolocateControlProps` | type | ported | compiled by the adapted type suite |
|
|
74
|
+
| `NavigationControlProps` | type | ported | compiled by the adapted type suite |
|
|
75
|
+
| `ScaleControlProps` | type | ported | compiled by the adapted type suite |
|
|
76
|
+
| `export * from './types/common'` | types | reused verbatim | pristine + adapted util lanes |
|
|
77
|
+
| `export * from './types/events'` | types | reused verbatim | pristine + adapted util lanes |
|
|
78
|
+
| `export * from './types/lib'` | types | reused verbatim | pristine + adapted util lanes |
|
|
79
|
+
| `export * from './types/style-spec'` | types | reused verbatim | pristine + adapted util lanes |
|
|
80
|
+
|
|
81
|
+
### Open gaps
|
|
82
|
+
|
|
83
|
+
- **`reuseMaps` / `Mapbox.recycle()`** is wired through from the reused engine
|
|
84
|
+
and typed, but no test exercises the recycle path. It is not covered by
|
|
85
|
+
upstream's suite either. Treat it as unverified.
|
|
86
|
+
- **`gl` (external WebGL context)**, **`RTLTextPlugin` loading** and
|
|
87
|
+
**`workerClass`/`workerUrl`** reach the reused `set-globals`/`mapbox` modules
|
|
88
|
+
unchanged and have no behavioral test here. Upstream has none either.
|
|
89
|
+
- **`react-map-gl/mapbox-legacy`** (mapbox-gl v1) is out of scope, matching the
|
|
90
|
+
current-major-only decision. Not applicable rather than missing.
|
|
91
|
+
- **`@vis.gl/react-maplibre`** is a separate upstream package and out of scope.
|
|
92
|
+
|
|
93
|
+
## Test disposition
|
|
94
|
+
|
|
95
|
+
Every artifact under `modules/react-mapbox/test` at the pin.
|
|
96
|
+
|
|
97
|
+
| Upstream file | Disposition |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `test/utils/apply-react-style.spec.js` | **run byte-exact**, both lanes |
|
|
100
|
+
| `test/utils/compare-class-names.spec.js` | **run byte-exact**, both lanes |
|
|
101
|
+
| `test/utils/deep-equal.spec.js` | **run byte-exact**, both lanes |
|
|
102
|
+
| `test/utils/style-utils.spec.js` | **run byte-exact**, both lanes |
|
|
103
|
+
| `test/utils/transform.spec.js` | **run byte-exact**, both lanes |
|
|
104
|
+
| `test/utils/mapbox-gl-mock/*.js` (5) | vendored; `transform.js`, `lng_lat.js` and `lng_lat_bounds.js` also back the component double |
|
|
105
|
+
| `test/utils/test-utils.jsx` | not applicable — `waitForMapLoad`/`sleep` are replaced by explicit draining |
|
|
106
|
+
| `test/utils/token.js` | not applicable — no live Mapbox token in CI |
|
|
107
|
+
| `test/components/map.spec.jsx` | **ported** → `tests/upstream/map.test.ts`; `Map#uncontrolled` is out of scope (asserts animation monotonicity across a real raf render loop) |
|
|
108
|
+
| `test/components/marker.spec.jsx` | **ported** → `tests/upstream/marker.test.ts` |
|
|
109
|
+
| `test/components/popup.spec.jsx` | **ported** → `tests/upstream/popup.test.ts` |
|
|
110
|
+
| `test/components/source.spec.jsx` | **ported** → `tests/upstream/source-layer.test.ts` |
|
|
111
|
+
| `test/components/layer.spec.jsx` | **ported** → `tests/upstream/source-layer.test.ts` |
|
|
112
|
+
| `test/components/controls.spec.jsx` | **ported** → `tests/upstream/controls.test.ts` |
|
|
113
|
+
| `test/components/use-map.spec.jsx` | **ported** → `tests/upstream/use-map.test.ts` |
|
|
114
|
+
| `test/components/index.js`, `test/utils/index.js` | not applicable — ocular aggregators |
|
|
115
|
+
|
|
116
|
+
Upstream ships **no type tests** at the pin: there is no `__typetest__`
|
|
117
|
+
directory and no tsd/expect-type harness. Both type lanes are therefore
|
|
118
|
+
port-authored against one shared assertion list (`typetests/assertions.md`), one
|
|
119
|
+
compiled against the published upstream typings with `tsc` and one against this
|
|
120
|
+
package with `tsrx-tsc`. The permitted transformations between them are listed
|
|
121
|
+
in that file; anything else is drift.
|
|
122
|
+
|
|
123
|
+
## Port-authored test classification
|
|
124
|
+
|
|
125
|
+
Every test file this package owns, with exactly one classification and the React
|
|
126
|
+
evidence it is paired against. Unpaired files are **not** React-parity evidence
|
|
127
|
+
and are not counted as such by any lane.
|
|
128
|
+
|
|
129
|
+
| File | Classification | Pairing |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| `tests/upstream-util/pristine.test.ts` | unmodified upstream | runs upstream's own specs against `upstream/src` |
|
|
132
|
+
| `tests/upstream-util/adapted.test.ts` | unmodified upstream | same specs, same bytes, against the reused modules |
|
|
133
|
+
| `tests/upstream/{map,marker,popup,source-layer,controls,use-map}.test.ts` | adapted upstream | each case cites the upstream case it re-authors |
|
|
134
|
+
| `tests/differential/parity.test.ts` | React/Octane differential | the published `@vis.gl/react-mapbox@8.1.2` on React |
|
|
135
|
+
| `tests/ssr/ssr.test.ts` | Octane-only framework contract | unpaired — upstream ships no SSR test and React's server output is not this port's contract |
|
|
136
|
+
| `tests/hydration/hydration.test.ts` | Octane-only framework contract | unpaired — hydration marker and adoption behavior is Octane's, not upstream's |
|
|
137
|
+
| `tests/runtime/lifecycle.test.ts` | Octane-only divergence | unpaired — pins teardown ordering the upstream suite cannot observe |
|
|
138
|
+
| `tests/runtime/smoke.test.ts` | Octane-only framework contract | unpaired — package surface and entry points |
|
|
139
|
+
| `tests/harness/tape-adapter.test.ts` | harness negative controls | unpaired — proves a removed, renamed or unexecuted upstream case fails validation |
|
|
140
|
+
| `typetests/{pristine,adapted}/types.test-d.ts` | port-authored type lanes | paired with each other through `typetests/assertions.md` |
|
|
141
|
+
|
|
142
|
+
## How the evidence is bounded
|
|
143
|
+
|
|
144
|
+
The five util specs are the only upstream tests that run unmodified, and they
|
|
145
|
+
run **twice**: once against `upstream/src` and once against the modules this
|
|
146
|
+
port reuses. Both passing is what backs the byte-for-byte reuse claim.
|
|
147
|
+
|
|
148
|
+
The seven component specs cannot run as pristine evidence. Each constructs a
|
|
149
|
+
real `mapbox-gl@3` Map with a live `VITE_MAPBOX_TOKEN` and waits on
|
|
150
|
+
`isStyleLoaded()` under puppeteer; CI can hold neither the account nor the map
|
|
151
|
+
loads. They are ported against `tests/_mocks/mapbox-gl.ts`, a port-authored
|
|
152
|
+
double — **weaker evidence than a pristine run, and recorded as such**.
|
|
153
|
+
|
|
154
|
+
What keeps the double honest is `tests/differential/parity.test.ts`: five
|
|
155
|
+
fixtures run through this binding and through the *published*
|
|
156
|
+
`@vis.gl/react-mapbox@8.1.2` on real React, sharing that same double. A double
|
|
157
|
+
that flattered the Octane binding would have to flatter React identically. They
|
|
158
|
+
cover the map shell and portalled overlay content (`differential:1`),
|
|
159
|
+
`<Source>`/`<Layer>` add, update and remove (`differential:2`), in-place popup
|
|
160
|
+
option edits alongside control add and remove (`differential:3`), and reaching
|
|
161
|
+
the map by id from a component outside it and flying the camera
|
|
162
|
+
(`differential:4`), and `useControl` called straight from a consumer module
|
|
163
|
+
(`differential:5`).
|
|
164
|
+
|
|
165
|
+
`<Source>` and `<Layer>` emit no DOM, so `differential:2` reads the resulting
|
|
166
|
+
style back off the live map through `useMap()` and renders it into the page —
|
|
167
|
+
the comparison is then over what each binding told mapbox-gl to do, not over
|
|
168
|
+
markup neither of them produces.
|
|
169
|
+
|
|
170
|
+
Not covered by any of this: real WebGL, real tile loading, real pointer
|
|
171
|
+
interaction, and anything that needs a Mapbox account. A token-gated real-map
|
|
172
|
+
lane remains open work.
|
|
173
|
+
|
|
174
|
+
## Divergences
|
|
175
|
+
|
|
176
|
+
Four, two of them recorded in `audit/react-parity.json` and bound to a case:
|
|
177
|
+
|
|
178
|
+
1. **`react-map-gl-source-id-by-context`** — upstream delivers a `<Source>` id to
|
|
179
|
+
child layers with `cloneElement(child, {source: id})`. Octane cannot clone a
|
|
180
|
+
compiled children block, so the id travels by context. Same override
|
|
181
|
+
semantics; the id now reaches any descendant `<Layer>`, not only direct
|
|
182
|
+
children. `ported:source-layer:3` pins the half both models share — a layer
|
|
183
|
+
inherits the generated id and an explicit `source` still loses to the
|
|
184
|
+
enclosing one — with a direct child, so it cannot observe the half where they
|
|
185
|
+
part. `tests/runtime/source-context.test.ts` mounts a layer below a wrapper
|
|
186
|
+
component, which is the shape `cloneElement` would have missed.
|
|
187
|
+
2. **`react-map-gl-refs-as-props`** — `forwardRef` does not exist in Octane, so
|
|
188
|
+
`Map`, `Marker`, `Popup` and `GeolocateControl` take `ref` as a plain prop.
|
|
189
|
+
`<Map ref={mapRef} />` is unchanged for consumers.
|
|
190
|
+
3. **Teardown timing** — effect cleanups run on the passive drain after
|
|
191
|
+
`root.unmount()`, not inside it, so the map's WebGL context and workers are
|
|
192
|
+
released one drain later. This is an Octane runtime property rather than a
|
|
193
|
+
binding behavior and no parity lane observes it, so it is deliberately not a
|
|
194
|
+
manifest divergence; it is pinned by `tests/runtime/lifecycle.test.ts`.
|
|
195
|
+
4. **`Marker` element chosen from rendered output** — upstream asks
|
|
196
|
+
`React.Children.forEach` whether it was handed a truthy child and, if so,
|
|
197
|
+
gives the marker its own element to portal into. A `.tsrx` children block is
|
|
198
|
+
an opaque render function, and evaluating it to look inside would re-run any
|
|
199
|
+
hooks it contains against the same call-site slots, so the binding infers the
|
|
200
|
+
answer from what the block rendered instead. It portals into an element it
|
|
201
|
+
owns from the first render — never `marker.getElement()`, or content arriving
|
|
202
|
+
late would land inside Mapbox's pin and both would draw — and rebuilds the
|
|
203
|
+
marker once the answer is known, in either direction. Same result as upstream
|
|
204
|
+
for a block that renders something and for one that renders nothing, and for
|
|
205
|
+
content that appears after mount. It differs for a child that is truthy but
|
|
206
|
+
renders nothing for the component's whole life: upstream leaves an empty
|
|
207
|
+
custom element, so the marker is invisible, and the port draws the default
|
|
208
|
+
pin. Pinned by `tests/runtime/marker-element.test.ts` and, against real
|
|
209
|
+
React, by `differential:6`.
|
|
210
|
+
|
|
211
|
+
## Regenerating
|
|
212
|
+
|
|
213
|
+
`node scripts/generate-parity-manifest.mjs` rebuilds `audit/react-parity.json`
|
|
214
|
+
and the runtime inventories from actual Vitest runs. Editing a lane file without
|
|
215
|
+
rerunning it fails `pnpm react-parity:check` on the integrity hash, which is the
|
|
216
|
+
point.
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/react-map-gl",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.22.2"
|
|
9
|
+
},
|
|
10
|
+
"description": "Mapbox GL JS bindings for Octane — a port of @vis.gl/react-mapbox, the package react-map-gl/mapbox re-exports.",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Dominic Gannaway",
|
|
13
|
+
"email": "dg@domgan.com"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
21
|
+
"directory": "packages/react-map-gl"
|
|
22
|
+
},
|
|
23
|
+
"main": "src/index.ts",
|
|
24
|
+
"module": "src/index.ts",
|
|
25
|
+
"types": "src/index.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md",
|
|
30
|
+
"UPSTREAM.md"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./src/index.ts",
|
|
34
|
+
"./mapbox": "./src/index.ts"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"mapbox-gl": ">=3.5.0",
|
|
38
|
+
"octane": "0.1.29"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"mapbox-gl": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@mapbox/point-geometry": "1.1.0",
|
|
47
|
+
"@tsrx/react": "^0.2.56",
|
|
48
|
+
"@types/react": "^19.2.17",
|
|
49
|
+
"@types/react-dom": "^19.2.3",
|
|
50
|
+
"@vis.gl/react-mapbox": "8.1.2",
|
|
51
|
+
"esbuild": "^0.28.1",
|
|
52
|
+
"mapbox-gl": "3.9.0",
|
|
53
|
+
"react": "^19.2.7",
|
|
54
|
+
"react-dom": "^19.2.7",
|
|
55
|
+
"vitest": "^4.1.10",
|
|
56
|
+
"octane": "0.1.29"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"test": "vitest run --root ../.. --project react-map-gl"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { memo, useEffect } from 'octane';
|
|
2
|
+
import { applyReactStyle } from '../utils/apply-react-style';
|
|
3
|
+
import { useControl } from './use-control';
|
|
4
|
+
|
|
5
|
+
import type { ControlPosition, AttributionControlOptions } from '../types/lib';
|
|
6
|
+
import type { CSSProperties } from '../types/octane';
|
|
7
|
+
|
|
8
|
+
export type AttributionControlProps = AttributionControlOptions & {
|
|
9
|
+
/** Placement of the control relative to the map. */
|
|
10
|
+
position?: ControlPosition;
|
|
11
|
+
/** CSS style override, applied to the control's container */
|
|
12
|
+
style?: CSSProperties;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function _AttributionControl(props: AttributionControlProps) {
|
|
16
|
+
const ctrl = useControl(({ mapLib }) => new mapLib.AttributionControl(props), {
|
|
17
|
+
position: props.position,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
applyReactStyle(ctrl._container, props.style);
|
|
22
|
+
}, [props.style]);
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const AttributionControl = memo(_AttributionControl);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* global document */
|
|
2
|
+
import { memo, useEffect } from 'octane';
|
|
3
|
+
import { applyReactStyle } from '../utils/apply-react-style';
|
|
4
|
+
import { useControl } from './use-control';
|
|
5
|
+
|
|
6
|
+
import type { ControlPosition, FullscreenControlOptions } from '../types/lib';
|
|
7
|
+
import type { CSSProperties } from '../types/octane';
|
|
8
|
+
|
|
9
|
+
export type FullscreenControlProps = Omit<FullscreenControlOptions, 'container'> & {
|
|
10
|
+
/** Id of the DOM element which should be made full screen. By default, the map container
|
|
11
|
+
* element will be made full screen. */
|
|
12
|
+
containerId?: string;
|
|
13
|
+
/** Placement of the control relative to the map. */
|
|
14
|
+
position?: ControlPosition;
|
|
15
|
+
/** CSS style override, applied to the control's container */
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function _FullscreenControl(props: FullscreenControlProps) {
|
|
20
|
+
const ctrl = useControl(
|
|
21
|
+
({ mapLib }) =>
|
|
22
|
+
new mapLib.FullscreenControl({
|
|
23
|
+
container: props.containerId ? document.getElementById(props.containerId) : undefined,
|
|
24
|
+
}),
|
|
25
|
+
{ position: props.position },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
applyReactStyle(ctrl._controlContainer, props.style);
|
|
30
|
+
}, [props.style]);
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const FullscreenControl = memo(_FullscreenControl);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { memo, useEffect, useImperativeHandle, useRef } from 'octane';
|
|
2
|
+
import { applyReactStyle } from '../utils/apply-react-style';
|
|
3
|
+
import { useControl } from './use-control';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ControlPosition,
|
|
7
|
+
GeolocateControlInstance,
|
|
8
|
+
GeolocateControlOptions,
|
|
9
|
+
} from '../types/lib';
|
|
10
|
+
import type { GeolocateEvent, GeolocateResultEvent, GeolocateErrorEvent } from '../types/events';
|
|
11
|
+
import type { CSSProperties, Ref } from '../types/octane';
|
|
12
|
+
|
|
13
|
+
export type GeolocateControlProps = GeolocateControlOptions & {
|
|
14
|
+
/** Placement of the control relative to the map. */
|
|
15
|
+
position?: ControlPosition;
|
|
16
|
+
/** CSS style override, applied to the control's container */
|
|
17
|
+
style?: CSSProperties;
|
|
18
|
+
|
|
19
|
+
/** Called on each Geolocation API position update that returned as success. */
|
|
20
|
+
onGeolocate?: (e: GeolocateResultEvent) => void;
|
|
21
|
+
/** Called on each Geolocation API position update that returned as an error. */
|
|
22
|
+
onError?: (e: GeolocateErrorEvent) => void;
|
|
23
|
+
/** Called on each Geolocation API position update that returned as success but user position
|
|
24
|
+
* is out of map `maxBounds`. */
|
|
25
|
+
onOutOfMaxBounds?: (e: GeolocateResultEvent) => void;
|
|
26
|
+
/** Called when the GeolocateControl changes to the active lock state. */
|
|
27
|
+
onTrackUserLocationStart?: (e: GeolocateEvent) => void;
|
|
28
|
+
/** Called when the GeolocateControl changes to the background state. */
|
|
29
|
+
onTrackUserLocationEnd?: (e: GeolocateEvent) => void;
|
|
30
|
+
/** Octane passes refs as ordinary props; there is no forwardRef. */
|
|
31
|
+
ref?: Ref<GeolocateControlInstance>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function _GeolocateControl(props: GeolocateControlProps) {
|
|
35
|
+
const thisRef = useRef({ props });
|
|
36
|
+
|
|
37
|
+
const ctrl = useControl(
|
|
38
|
+
({ mapLib }) => {
|
|
39
|
+
const gc = new mapLib.GeolocateControl(props);
|
|
40
|
+
|
|
41
|
+
// Hack: fix GeolocateControl reuse
|
|
42
|
+
// GeolocateControl's UI creation is asynchronous. Removing and adding it back
|
|
43
|
+
// causes the UI to be initialized twice.
|
|
44
|
+
//
|
|
45
|
+
// Upstream reaches for this because React strict mode mounts the component
|
|
46
|
+
// twice. Octane has no StrictMode double-invoke, so the guard is inert here;
|
|
47
|
+
// it is kept because it is also correct for an ordinary remount, and deleting
|
|
48
|
+
// upstream behavior on a technicality is how ports drift.
|
|
49
|
+
const setupUI = gc._setupUI.bind(gc);
|
|
50
|
+
gc._setupUI = (args) => {
|
|
51
|
+
if (!gc._container.hasChildNodes()) {
|
|
52
|
+
setupUI(args);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
gc.on('geolocate', (e) => {
|
|
57
|
+
thisRef.current.props.onGeolocate?.(e as GeolocateResultEvent);
|
|
58
|
+
});
|
|
59
|
+
gc.on('error', (e) => {
|
|
60
|
+
thisRef.current.props.onError?.(e as GeolocateErrorEvent);
|
|
61
|
+
});
|
|
62
|
+
gc.on('outofmaxbounds', (e) => {
|
|
63
|
+
thisRef.current.props.onOutOfMaxBounds?.(e as GeolocateResultEvent);
|
|
64
|
+
});
|
|
65
|
+
gc.on('trackuserlocationstart', (e) => {
|
|
66
|
+
thisRef.current.props.onTrackUserLocationStart?.(e as GeolocateEvent);
|
|
67
|
+
});
|
|
68
|
+
gc.on('trackuserlocationend', (e) => {
|
|
69
|
+
thisRef.current.props.onTrackUserLocationEnd?.(e as GeolocateEvent);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return gc;
|
|
73
|
+
},
|
|
74
|
+
{ position: props.position },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
thisRef.current.props = props;
|
|
78
|
+
|
|
79
|
+
useImperativeHandle(props.ref, () => ctrl, []);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
applyReactStyle(ctrl._container, props.style);
|
|
83
|
+
}, [props.style]);
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const GeolocateControl = memo(_GeolocateControl);
|