Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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 で実際に踏んだ)。
Expand Down
3 changes: 2 additions & 1 deletion examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions examples/basic/scripts/vite-maplibre-registry.mjs
Original file line number Diff line number Diff line change
@@ -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 };
},
};
}
68 changes: 68 additions & 0 deletions examples/basic/scripts/vite-maplibre-registry.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
3 changes: 2 additions & 1 deletion examples/basic/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '/';
Expand Down Expand Up @@ -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'],
},
Expand Down
26 changes: 13 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion react-for-mapbox
Submodule react-for-mapbox updated 1 files
+1 −1 package.json
2 changes: 1 addition & 1 deletion react-for-maplibre
Loading