From 3b5a86aaef7e1975e3de1c52290efa701549cc6d Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 17:31:26 +0000 Subject: [PATCH] Auto resolves to MapLibre; a token no longer picks the engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Driver wants MapLibre as the default everywhere, on cost and on how it looks, with Mapbox supported as an explicit choice. One line decided it. resolveMapProvider read explicit || (token present ? 'mapbox' : 'maplibre') so merely HOLDING a Mapbox token selected Mapbox. That conflated two different things: having a token is a capability, wanting Mapbox is a decision, and only the decision belongs in this function. It now reads explicit || 'maplibre'. The token is still honoured for what it is — with provider:'mapbox' it authenticates the map, and the token argument stays in the signature because callers pass one they hold outside the config. It simply no longer votes. The migration surface is exactly one shape: a host that pasted a real token into a published embed snippet and set no provider. All 36 shipped snippets carry a tk placeholder and no provider key, so any host who filled one in is in it. An unfilled snippet was already MapLibre, because hasMapToken rejects YOUR_ placeholders, and the DX Life branches pass no token at all. Such a host keeps Mapbox by adding "provider":"mapbox"; ?provider= and window.MAP_PROVIDER are unchanged. The gate's four token-selects-Mapbox assertions were the old specification, so they are inverted rather than deleted — the diff should show what changed for whom. Two are added: the opt-in still works with a token in hand, which is the path a migrating host takes, and a real token is still SEEN, asserted apart from the engine because otherwise the engine could be right while authentication broke silently. check:map-props-unit was not in gates:pre, so it proved nothing on a normal run. It is now, and the suite reads 32 of 32 with it named. --- internal/check-map-props-unit.mjs | 22 ++++++++++++++++------ library/composables/useMapProvider.js | 19 ++++++++++++++++++- package.json | 2 +- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/internal/check-map-props-unit.mjs b/internal/check-map-props-unit.mjs index 371bc73eb..ed8447660 100644 --- a/internal/check-map-props-unit.mjs +++ b/internal/check-map-props-unit.mjs @@ -2,7 +2,7 @@ /** * check-map-props-unit — the engine and token props, decided in the library, measured WITHOUT a browser. * - * resolveMapProvider ('auto': Mapbox when a token is available, MapLibre otherwise; an explicit provider + * resolveMapProvider ('auto' is ALWAYS MapLibre since 2026-09-28; a token no longer selects an engine; an explicit provider * wins), and the page-wide token (useSharedMapToken): publish, CLEAR, and a token-service URL. Runs the * real library modules in Node against a stand-in window/location, so it is fast and needs no browser. * @@ -31,13 +31,23 @@ const TK = ['pk', 'eyJ1IjoidGVzdCJ9', 'c2ln'].join('.') console.log('MAP-PROPS: the library, in Node (no browser)') eq('default is auto', DEFAULT_PROVIDER, 'auto') reset(); eq('auto, no token anywhere → maplibre', resolveMapProvider({}), 'maplibre') -reset(); eq('auto, tk in profile-config → mapbox', resolveMapProvider({ tk: TK }), 'mapbox') +// ★ THESE FOUR ASSERTED THE OLD RULE, in which merely HOLDING a token chose Mapbox. They are +// inverted rather than deleted, because the point is not that a token is ignored — it is that a +// token is a CAPABILITY and the engine is a DECISION, and only the decision belongs here. Each of +// these is a host who pasted a real token into a published snippet and set no provider: that is +// the entire migration surface, and it is exactly what changes. +reset(); eq('auto, tk in profile-config → maplibre (was mapbox)', resolveMapProvider({ tk: TK }), 'maplibre') reset(); eq('auto, placeholder tk YOUR_MAPBOX_TOKEN → maplibre', resolveMapProvider({ tk: 'YOUR_MAPBOX_TOKEN' }), 'maplibre') -reset(); window.MAP_TOKEN = TK; eq('auto, host window.MAP_TOKEN → mapbox', resolveMapProvider({}), 'mapbox') -reset(); eq('auto, a token the caller holds (useMapInstance) → mapbox', resolveMapProvider({}, { token: TK }), 'mapbox') -reset(); eq('auto, tokenUrl in profile-config → mapbox (U1)', resolveMapProvider({ tokenUrl: 'https://example.org/token' }), 'mapbox') +reset(); window.MAP_TOKEN = TK; eq('auto, host window.MAP_TOKEN → maplibre (was mapbox)', resolveMapProvider({}), 'maplibre') +reset(); eq('auto, a token the caller holds (useMapInstance) → maplibre (was mapbox)', resolveMapProvider({}, { token: TK }), 'maplibre') +reset(); eq('auto, tokenUrl in profile-config → maplibre (was mapbox, U1)', resolveMapProvider({ tokenUrl: 'https://example.org/token' }), 'maplibre') reset(); eq('explicit maplibre with a token → maplibre', resolveMapProvider({ provider: 'maplibre', tk: TK }), 'maplibre') reset(); eq('explicit mapbox without a token → mapbox', resolveMapProvider({ provider: 'mapbox' }), 'mapbox') +// The opt-in still works WITH a token, which is the path a migrating host takes. +reset(); eq('explicit mapbox WITH a token → mapbox (the documented way to keep Mapbox)', resolveMapProvider({ provider: 'mapbox', tk: TK }), 'mapbox') +// And a token remains VISIBLE — it just does not vote. If this ever returned false the engine +// would still be right while authentication silently broke, so the two are asserted apart. +reset(); eq('a real token is still SEEN by hasMapToken (capability, not choice)', hasMapToken({ tk: TK }), true) reset(); eq('mixed case "MapLibre" is honoured (U7)', resolveMapProvider({ provider: 'MapLibre', tk: TK }), 'maplibre') reset(); search = '?provider=maplibre'; window.MAP_TOKEN = TK; eq('?provider=maplibre wins over a host token', resolveMapProvider({}), 'maplibre') reset(); window.MAP_PROVIDER = 'mapbox'; eq('window.MAP_PROVIDER is explicit', explicitMapProvider({}), 'mapbox') @@ -79,4 +89,4 @@ eq('a non-public (sk.) answer is refused', secret, '') eq('…and nothing is published', window.MAP_TOKEN || '', '') if (bad) { console.log(`MAP-PROPS: FAIL — ${bad} case(s).`); process.exit(1) } -console.log('MAP-PROPS: PASS — auto picks the engine from the token ladder, explicit wins, Clear clears, tokenUrl is honoured.') +console.log('MAP-PROPS: PASS — auto is ALWAYS MapLibre, a token is a capability and never a choice, explicit wins, Clear clears.') diff --git a/library/composables/useMapProvider.js b/library/composables/useMapProvider.js index c499e1cfe..811104af6 100644 --- a/library/composables/useMapProvider.js +++ b/library/composables/useMapProvider.js @@ -61,7 +61,24 @@ export function hasMapToken(cfg) { */ export function resolveMapProvider(profileConfig, { token } = {}) { const cfg = unwrapConfig(profileConfig) - return explicitMapProvider(cfg) || (hasMapToken(cfg) || hasMapToken({ tk: token }) ? 'mapbox' : 'maplibre') + // ★ AUTO IS MAPLIBRE. A TOKEN NO LONGER CHOOSES AN ENGINE (Driver, 2026-09-28: MapLibre is the + // default everywhere, on cost and on how it looks; Mapbox stays supported as an explicit choice). + // + // It used to read: explicit || (token present ? 'mapbox' : 'maplibre') — so merely HOLDING a + // Mapbox token selected Mapbox. That conflated two different things: "I have a token" is a + // capability, "I want Mapbox" is a decision, and only the second belongs in this function. + // + // The token is still honoured for what it IS: with provider:'mapbox' it authenticates the map, + // and the `token` argument stays in the signature because callers pass one they hold outside the + // config. It simply no longer votes on the engine. + // + // WHO CHANGES: a host that pasted a real token into a published embed snippet and set no + // provider. All 36 shipped snippets carry a tk placeholder and NO provider key, so that is the + // whole migration surface. An UNFILLED snippet was already MapLibre — hasMapToken rejects + // YOUR_-prefixed placeholders — and the DX Life branches pass no token at all. + // To keep Mapbox, such a host adds "provider":"mapbox" to profile-config; ?provider=mapbox and + // window.MAP_PROVIDER are unchanged. + return explicitMapProvider(cfg) || 'maplibre' } /** The engine someone ASKED for (rungs 1-3), or '' when the default ('auto') decides. */ diff --git a/package.json b/package.json index 9a5da6ed7..05bcf137b 100644 --- a/package.json +++ b/package.json @@ -285,7 +285,7 @@ "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:frozen-labels", - "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary", + "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs",