diff --git a/.gitignore b/.gitignore index cf34e5e..83dafcc 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,8 @@ dist/* !examples/basic/scripts/generate-static.mjs !examples/basic/scripts/check-snippets.mjs !examples/basic/scripts/snippet-interactions.mjs +!examples/basic/scripts/vite-maplibre-registry.mjs +!examples/basic/scripts/vite-maplibre-registry.test.mjs # scripts/ はソースしか置かないので丸ごと追跡する。1 本ずつ negation を足す運用だと # 足し忘れたスクリプトが `git add -A` で無言のまま無視され、CI のチェックアウトにだけ # 存在しない状態になる(unlink-local-workspaces.mjs で実際に踏んだ)。 diff --git a/examples/basic/package.json b/examples/basic/package.json index 1f8458e..9ae9fde 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -4,6 +4,7 @@ "private": true, "description": "Basic example app for MapConductor React SDK", "scripts": { + "test:maplibre-registry": "node --test scripts/vite-maplibre-registry.test.mjs", "check:snippets": "node scripts/check-snippets.mjs", "dev": "vite", "dev:https": "HTTPS=true vite", @@ -35,7 +36,7 @@ "@mapconductor/js-vectorstyle": "*", "@mapconductor/react-vectortile": "*", "@zip.js/zip.js": "^2.8.26", - "maplibre-gl": "^6.15.0", + "maplibre-gl": "^6.17.0", "prism-react-renderer": "^2.4.1", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/examples/basic/scripts/vite-maplibre-registry.mjs b/examples/basic/scripts/vite-maplibre-registry.mjs new file mode 100644 index 0000000..b7e4f1a --- /dev/null +++ b/examples/basic/scripts/vite-maplibre-registry.mjs @@ -0,0 +1,42 @@ +import { readFile } from 'node:fs/promises'; + +// Demo only: both renderers register built-in constructors (Object, Set, Error) +// under the same private property. MapLibre's Set registration makes Mapbox look +// up a class that is absent from its own registry, breaking all vector tiles. +// Keep the wire format ($name) intact; only isolate MapLibre's constructor key. +// Never apply this in the SDK package builds or modify installed dependencies. +const registryKey = '_mapconductorDemoMapLibreRegistryKey'; +const maplibreModule = /[/\\]maplibre-gl[/\\].*\.[cm]?js$/; + +function rewrite(code) { + return code.replaceAll('_classRegistryKey', registryKey); +} + +/** @returns {import('vite').Plugin} */ +export function isolateMapLibreRegistry() { + return { + name: 'demo-isolate-maplibre-registry', + enforce: 'pre', + config() { + return { + optimizeDeps: { + esbuildOptions: { + plugins: [{ + name: 'demo-isolate-maplibre-registry', + setup(build) { + build.onLoad({ filter: maplibreModule }, async ({ path }) => ({ + contents: rewrite(await readFile(path, 'utf8')), + loader: 'js', + })); + }, + }], + }, + }, + }; + }, + transform(code, id) { + if (!maplibreModule.test(id.split('?')[0]) || !code.includes('_classRegistryKey')) return; + return { code: rewrite(code), map: null }; + }, + }; +} diff --git a/examples/basic/scripts/vite-maplibre-registry.test.mjs b/examples/basic/scripts/vite-maplibre-registry.test.mjs new file mode 100644 index 0000000..bba9252 --- /dev/null +++ b/examples/basic/scripts/vite-maplibre-registry.test.mjs @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { test } from 'node:test'; +import { runInNewContext } from 'node:vm'; +import { build } from 'esbuild'; +import { isolateMapLibreRegistry } from './vite-maplibre-registry.mjs'; + +const require = createRequire(import.meta.url); +const maplibrePath = require.resolve('maplibre-gl/dist/maplibre-gl-dev.mjs'); +const maplibre = await readFile(maplibrePath, 'utf8'); +const mapbox = await readFile(require.resolve('mapbox-gl/dist/mapbox-gl-dev.js'), 'utf8'); +const plugin = isolateMapLibreRegistry(); + +function serializeWithBothRenderers(maplibreCode, order) { + // Exercise the installed libraries' actual registration/serialization code. + // Separate registries, shared built-in constructors, just like a browser page. + const registerStart = maplibreCode.indexOf('function register(name, klass, options = {})'); + assert.ok(registerStart >= 0, 'MapLibre registration code changed; review the workaround'); + const registerEnd = maplibreCode.indexOf('register("Object", Object);', registerStart); + assert.ok(registerEnd > registerStart); + const serializeStart = mapbox.indexOf('function serialize(input, transferables)'); + const serializeEnd = mapbox.indexOf('function deserialize(input)', serializeStart); + assert.ok(serializeStart >= 0 && serializeEnd > serializeStart); + const registerMaplibre = `(() => { + const registry = {}; + ${maplibreCode.slice(registerStart, registerEnd)} + register('Object', Object); register('Set', Set); register('Error', Error); + })();`; + const registerMapbox = `Object.defineProperty(Object, '_classRegistryKey', {value: 'Object'});`; + return runInNewContext(` + class ImageBitmap {} class ImageData {} class Headers {} + const assert = () => {}; // Mapbox's production assertions are compiled out. + const registry = {Object: {omit: []}}; + const isArrayBuffer = value => value instanceof ArrayBuffer; + ${order === 'mapbox-first' ? registerMapbox + registerMaplibre : registerMaplibre + registerMapbox} + ${mapbox.slice(serializeStart, serializeEnd)} + JSON.stringify(serialize({tiles: new Set([1, 2])})); + `); +} + +test('reproduces the installed MapLibre/Mapbox registry collision', () => { + assert.throws(() => serializeWithBothRenderers(maplibre, 'maplibre-first'), /omit/); +}); + +test('production transform permits both load orders without changing the wire format', () => { + const transformed = plugin.transform(maplibre, maplibrePath).code; + for (const order of ['maplibre-first', 'mapbox-first']) { + assert.deepEqual(JSON.parse(serializeWithBothRenderers(transformed, order)), { + tiles: {$name: 'Set', 1: 1, 2: 2}, + }); + } + assert.equal(plugin.transform(mapbox, require.resolve('mapbox-gl')), undefined); +}); + +test('Vite dependency optimization applies the same isolation to MapLibre', async () => { + const config = plugin.config(); + const result = await build({ + entryPoints: [require.resolve('maplibre-gl/dist/maplibre-gl.mjs')], + bundle: true, + write: false, + format: 'esm', + plugins: config.optimizeDeps.esbuildOptions.plugins, + }); + assert.ok(result.outputFiles[0].text.includes('_mapconductorDemoMapLibreRegistryKey')); + assert.ok(!result.outputFiles[0].text.includes('_classRegistryKey')); + assert.equal(await readFile(maplibrePath, 'utf8'), maplibre); +}); diff --git a/examples/basic/vite.config.ts b/examples/basic/vite.config.ts index 9d57f20..415a11f 100644 --- a/examples/basic/vite.config.ts +++ b/examples/basic/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'; import mkcert from 'vite-plugin-mkcert'; import { cpSync, existsSync, readFileSync, statSync } from 'node:fs'; import { extname, join, resolve } from 'node:path'; +import { isolateMapLibreRegistry } from './scripts/vite-maplibre-registry.mjs'; const cesiumSource = resolve(__dirname, '../../node_modules/cesium/Build/Cesium'); const basePath = process.env.VITE_BASE_PATH ?? '/'; @@ -92,7 +93,7 @@ export default defineConfig({ // worker format (iife) cannot code-split. Only the production build trips // on this — dev serves module workers natively. worker: { format: 'es' }, - plugins: [react(), cesiumStaticAssets(), mapconductorTileServiceWorker(), ...(useHttps ? [mkcert()] : [])], + plugins: [isolateMapLibreRegistry(), react(), cesiumStaticAssets(), mapconductorTileServiceWorker(), ...(useHttps ? [mkcert()] : [])], resolve: { dedupe: ['react', 'react-dom', '@mapconductor/js-sdk-core'], }, diff --git a/package-lock.json b/package-lock.json index b73f1da..9ee66b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,7 @@ "@mapconductor/react-marker-clustering": "*", "@mapconductor/react-vectortile": "*", "@zip.js/zip.js": "^2.8.26", - "maplibre-gl": "^6.15.0", + "maplibre-gl": "^6.17.0", "prism-react-renderer": "^2.4.1", "react": "^19.2.7", "react-dom": "^19.2.7", @@ -11887,9 +11887,9 @@ } }, "node_modules/mapbox-gl": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.33.0.tgz", - "integrity": "sha512-2HpIrVedccHedolZyMVG/7FXdpgmsLTENgEDsX71x3/gKDGfYMf1FCRY0prntknkn/JXcraUKXlBTv7vkr0EXg==", + "version": "3.34.2", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.34.2.tgz", + "integrity": "sha512-SfuzpqjokSjv4C9/JEjxAfqVHY37B7Kky/AOFpc+9D0Sx0KuIoV3keSlvvuHuM/jxw84iQ/Nh7yIK1VxuDBBHA==", "license": "SEE LICENSE IN LICENSE.txt", "workspaces": [ "src/style-spec", @@ -11899,9 +11899,9 @@ ] }, "node_modules/maplibre-gl": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.16.0.tgz", - "integrity": "sha512-gApO66EMtoDZuio680RUBvskR7ahFd/NNhLN4tBAs+Y7FeT4gQV8zVFgYVMRmFxQznOEzecW19w6xOd9XjZjiA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.17.0.tgz", + "integrity": "sha512-2chIZwpLGenoUQOcnm9Pk4SLbA8ppNFq8jlpXiq9vcYm3Y1wJ3fc82FL0GLwBaPYJCLzPv/mC46KEupVov75rQ==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/point-geometry": "^1.1.0", @@ -11909,7 +11909,7 @@ "@mapbox/unitbezier": "^1.0.0", "@mapbox/vector-tile": "^3.0.0", "@maplibre/geojson-vt": "^6.3.0", - "@maplibre/maplibre-gl-style-spec": "^26.4.4", + "@maplibre/maplibre-gl-style-spec": "^26.5.1", "@maplibre/mlt": "^1.3.0", "@maplibre/vt-pbf": "^4.3.3", "@types/geojson": "^7946.0.16", @@ -11938,9 +11938,9 @@ "license": "BSD-2-Clause" }, "node_modules/maplibre-gl/node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "26.4.4", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.4.tgz", - "integrity": "sha512-hG4RZR4PBd/+6A3ODdZ3AnQRF106ZQk1y3bhwDg8bH/A4Oz2tnRUJVVWs9BZa6gXZ1d3UKe89C+Mm+olGBELvA==", + "version": "26.5.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.5.1.tgz", + "integrity": "sha512-9ufQ5LR+Ldn87SILPVyRNz4wVm/sBrM2ZRv9VfjLRKYKIGAcvC1VFw/DEkJLAHqfMV8m03zDz1DUNTIUj/GvdA==", "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.3", @@ -15576,7 +15576,7 @@ "dependencies": { "@mapconductor/js-sdk-core": "^0.3.1", "@mapconductor/js-sdk-react": "^0.3.0", - "mapbox-gl": "^3.33.0" + "mapbox-gl": "^3.34.2" }, "devDependencies": { "react": "^19.3.1", @@ -15634,7 +15634,7 @@ "dependencies": { "@mapconductor/js-sdk-core": "^0.3.1", "@mapconductor/js-sdk-react": "^0.3.0", - "maplibre-gl": "^6.16.0" + "maplibre-gl": "^6.17.0" }, "devDependencies": { "react": "^19.3.1", diff --git a/react-for-mapbox b/react-for-mapbox index 22752f8..49715e3 160000 --- a/react-for-mapbox +++ b/react-for-mapbox @@ -1 +1 @@ -Subproject commit 22752f83f894559fc0801ca0a178b079aaaadfad +Subproject commit 49715e3fdf392c8ceec152a7f486d05be7574849 diff --git a/react-for-maplibre b/react-for-maplibre index 251a936..780cf56 160000 --- a/react-for-maplibre +++ b/react-for-maplibre @@ -1 +1 @@ -Subproject commit 251a936ec049dafab3df18753d3cb4f0c79dbe33 +Subproject commit 780cf56f9d4671c423f04847b56e398e86dfb166