@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 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);