country-map: key the refetch by value, let only the newest response win, show the wait

Three defects on country-map/country-map, which is production -- DX Life's
country pages. Found by auditing the element rather than from a report.

1. ANY profile-config change refetched all 2,106 records. The watch getter
   returned a NEW ARRAY literal, and Vue compares a watch value with Object.is,
   so a fresh array always reads as changed; both computeds read the injected
   cfg, so the effect re-ran on every config change and the callback then fired
   unconditionally. MEASURED with real Vue: flipping isDark refetched the whole
   endpoint and redrew the map, and so did a host adding pinHint. A joined
   STRING compares by value, so only a real language or country change refetches
   -- which must still refetch, because the API localises the names.
   This was latent waste until the composite re-forward made config writes
   routine. It is now on every upstream locale switch, so the two changes are
   complementary rather than independent.

2. A SLOW EARLIER RESPONSE OVERWROTE A NEWER ONE. loadPoints is async and had no
   sequencing at all -- 0 abort or generation tokens in the file -- so two runs
   could be in flight and the one that STARTED first could FINISH last. Both
   assigned points, so the map showed the previous country's pins with no error
   and nothing for the reader to notice. Every run now takes a ticket and a stale
   ticket writes nothing: not the points, not loadFailed, not the spinner the
   current run is still using. fetch is left to complete rather than aborted,
   because the response is cached for the next switch back.

3. `loading` WAS WRITE-ONLY: assigned in three places and read in none, zero
   template references. A reader on a country page waited on an empty map -- no
   outline, no pins, no message -- which is indistinguishable from a country that
   genuinely has no people groups, and this element fetches the whole list before
   it can draw one pin. "Loading results..." was already in the catalogue in all
   eleven locales, so saying so cost no new string and no parity debt.

check:country-map-refetch guards all three. It drives the REAL reactive chain
with real Vue and takes the watch key expression OUT OF THE SHIPPED SFC, because
bug 1 is entirely a question of what Vue considers changed and is invisible to
any test that fakes reactivity or re-types the watch.

Mutation-tested, 5 mutations, all caught by name. The first version of the gate
MISSED one: a bare /if \(!current\(\)\) return/ grep matched the guard in the
catch block, so removing only the guard before the points write still passed.
The assertions are positional now -- the guard must sit immediately before the
assignment it protects -- and the catch guard is asserted separately. That hole
existed because the behavioural half of the test reproduced the guard's shape
inside the gate instead of reading the component, which is the self-consistent
oracle this repo's other geometry gate explicitly warns against.

Registered in scripts and gates:pre, verified by re-parsing: one key, 107
checks. Bundle rebuilt (BUNDLE=country-map), then tree, then fix:sri.
This commit is contained in:
Doxa Maps 2026-09-29 19:11:37 +00:00
parent 32cf3f2354
commit e643e8a30e
7 changed files with 208 additions and 25 deletions

View file

@ -0,0 +1,154 @@
#!/usr/bin/env node
/**
* check-country-map-refetch — the country map refetches when the LANGUAGE or the COUNTRY changes,
* and not when anything else does; only the newest request may write; and the wait is visible.
*
* ★ THE BUGS THIS GUARDS, all three measured on country-map/country-map (production: DX Life's
* country pages), 2026-09-29.
*
* 1. ANY profile-config change refetched all 2,106 records. The watch getter returned a new ARRAY
* literal and Vue compares a watch value with Object.is, so a fresh array always reads as
* changed; both computeds read the injected cfg, so the effect re-ran on every config change and
* the callback then fired unconditionally. MEASURED with real Vue: flipping isDark refetched,
* and so did a host adding pinHint. Latent waste until the composite re-forward made config
* writes routine -- then it fired on every locale switch upstream.
* 2. A SLOW EARLIER RESPONSE OVERWROTE A NEWER ONE. loadPoints is async with no sequencing (0 abort
* or generation tokens in the file), so two in-flight runs both assigned points and the one that
* started first could finish last. The map then showed the PREVIOUS country's pins, with no
* error and nothing for the reader to notice.
* 3. `loading` WAS WRITE-ONLY: assigned in three places, read in none, zero template references.
* The reader waited on an empty map -- no outline, no pins, no message -- which is
* indistinguishable from a country that genuinely has no people groups.
*
* ★ IT DRIVES THE REAL REACTIVE CHAIN WITH REAL VUE, not a re-typed copy: the same injected-cfg
* computed, the same countryCodes/languageCode computeds, and the WATCH EXPRESSION READ OUT OF THE
* SHIPPED SFC. A hand-copied watch would pass forever while the component drifted, and a test that
* faked reactivity could not see bug 1 at all -- it is entirely a question of what Vue considers
* changed.
*
* Exit 0 pass · 1 fail · 2 the watch could not be found in the SFC (never a silent pass).
*/
import { readFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { ref, computed, watch, nextTick } from 'vue'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const SRC = join(ROOT, 'plugin-designs/country-map/country-map.vue')
const src = readFileSync(SRC, 'utf8')
let bad = 0
const ok = (cond, what) => { if (!cond) bad++; console.log(` ${cond ? 'ok ' : 'FAIL'} ${what}`) }
// ── THE WATCH KEY, taken from the shipped file ───────────────────────────────
// Lazy, and NOT [^,]+ -- the key expression legitimately contains a comma (join(',')), which is
// what made a comma-excluding pattern report NOT MEASURED on a correct file.
const m = src.match(/watch\(\s*\(\)\s*=>\s*(.+?),\s*loadPoints\s*\)/s)
if (!m) {
console.error('COUNTRY-MAP-REFETCH: NOT MEASURED — the watch(() => …, loadPoints) call was not found.')
console.error(' It may have been renamed or restructured. Point this gate at it rather than deleting it:')
console.error(' an unkeyed refetch is how this shipped the first time.')
process.exit(2)
}
const keyExpr = m[1].trim()
// ── 1. ONLY A REAL CHANGE REFETCHES ──────────────────────────────────────────
{
const raw = ref(JSON.stringify({ countryCodes: ['SDN'], languageCode: 'en', isDark: false }))
const cfg = computed(() => { try { return JSON.parse(raw.value) || {} } catch { return {} } })
const countryCodes = computed(() => {
const v = cfg.value.countryCodes
if (Array.isArray(v)) return v.filter(Boolean).map((c) => String(c).toUpperCase())
if (typeof v === 'string') return v.split(',').map((c) => c.trim().toUpperCase()).filter(Boolean)
return []
})
const languageCode = computed(() => cfg.value.languageCode || cfg.value.locale || cfg.value.lang || 'en')
// The SHIPPED expression, evaluated against those computeds.
const getKey = new Function('languageCode', 'countryCodes', `return () => (${keyExpr})`)(languageCode, countryCodes)
let fetches = 0
watch(getKey, () => { fetches++ })
await nextTick()
const set = async (o) => { raw.value = JSON.stringify(o); await nextTick() }
let n = fetches
await set({ countryCodes: ['SDN'], languageCode: 'en', isDark: true })
ok(fetches === n, `an unrelated key (isDark) does NOT refetch (${fetches - n} refetch(es)) — the 2,106-record bug`)
n = fetches
await set({ countryCodes: ['SDN'], languageCode: 'en', isDark: true, pinHint: false })
ok(fetches === n, `an added unrelated key (pinHint) does NOT refetch (${fetches - n})`)
n = fetches
await set({ countryCodes: ['SDN'], languageCode: 'ar', isDark: true, pinHint: false })
ok(fetches === n + 1, `a REAL locale change DOES refetch (${fetches - n}) — the API localises the names`)
n = fetches
await set({ countryCodes: ['TCD'], languageCode: 'ar', isDark: true, pinHint: false })
ok(fetches === n + 1, `a REAL country change DOES refetch (${fetches - n})`)
n = fetches
await set({ countryCodes: ['TCD'], languageCode: 'ar', isDark: false, pinHint: false })
ok(fetches === n, `flipping isDark back does NOT refetch (${fetches - n})`)
// Equal-but-reordered is a genuine change of the list, so it may refetch; equal-and-identical must not.
n = fetches
await set({ countryCodes: ['TCD'], languageCode: 'ar', isDark: false, pinHint: false, extra: 1 })
ok(fetches === n, `an unrelated key added mid-session does NOT refetch (${fetches - n})`)
}
// ── 2. ONLY THE NEWEST REQUEST WRITES ────────────────────────────────────────
// The shipped guard is a generation counter; this asserts the PROPERTY, driving two overlapping
// runs whose responses land out of order.
{
ok(/let runId = 0/.test(src) && /const mine = \+\+runId/.test(src),
'loadPoints takes a generation ticket')
// ★ POSITIONAL, NOT A BARE GREP. This first read `/if \(!current\(\)\) return/` anywhere in the
// file -- and there are TWO such guards, one before the points write and one in the catch. A
// mutation that removed ONLY the points guard still matched the catch one, so the gate passed on
// the reintroduced race. Found by mutation-testing this gate, which is the whole reason to do it.
// The guard must sit immediately before the assignment it protects.
ok(/if \(!current\(\)\) return\s*\n\s*points\.value = /.test(src),
'the staleness guard sits immediately before the points write')
ok(/catch \(error\) \{\s*\n\s*if \(!current\(\)\) return/.test(src),
'and a stale FAILURE does not raise loadFailed on the current run')
ok(/if \(current\(\)\) loading\.value = false/.test(src),
'a stale run does not clear the spinner the current one is still using')
// Behavioural: reproduce the guard's shape and prove out-of-order cannot win.
let runId = 0
let points = 'none'
const load = async (label, delay) => {
const mine = ++runId
const current = () => mine === runId
await new Promise((r) => setTimeout(r, delay))
if (!current()) return
points = label
}
const slowFirst = load('OLD-country', 40) // started first, finishes last
const fastSecond = load('NEW-country', 5)
await Promise.all([slowFirst, fastSecond])
ok(points === 'NEW-country',
`the response that STARTED first and FINISHED last does not win (points = ${points})`)
}
// ── 3. THE WAIT IS VISIBLE ───────────────────────────────────────────────────
{
const tpl = src.slice(src.indexOf('<template>'))
ok(/v-if="loading"/.test(tpl), '`loading` is rendered, not write-only')
ok(/t\('Loading results\.\.\.'\)/.test(tpl),
'it uses the catalogue string that already exists in all eleven locales')
ok(/v-else-if="loadFailed"/.test(tpl),
'the failure message is else-if, so a reader never sees "loading" and "failed" at once')
const assigns = (src.match(/loading\.value = /g) || []).length
ok(assigns >= 2, `loading is still actually driven (${assigns} assignment(s))`)
}
if (bad) {
console.error(`\nCOUNTRY-MAP-REFETCH: FAIL — ${bad} case(s). The country map either refetches 2,106`)
console.error('records on unrelated config changes, lets a stale response win, or leaves the reader')
console.error('staring at an empty map with nothing to explain it.')
process.exit(1)
}
console.log('\nCOUNTRY-MAP-REFETCH: PASS — refetch is keyed by value, newest-writer-wins, and the wait is visible.')

View file

@ -237,6 +237,7 @@
"check:poster-action-print": "node internal/check-poster-action-print.mjs",
"check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs",
"check:popup-pan-controls": "node internal/check-popup-pan-clears-controls.mjs",
"check:country-map-refetch": "node internal/check-country-map-refetch.mjs",
"check:control-inset": "node internal/check-control-inset-measured.mjs",
"check:viewport-meta": "node internal/check-viewport-meta.mjs",
"check:viewport-meta:self-test": "VIEWPORT_META_SELFTEST=1 node internal/check-viewport-meta.mjs",
@ -340,7 +341,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:auto-engine check:engine-contract 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:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
"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:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated 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 check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch",
"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:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated 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 check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch",
"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",

View file

@ -92,7 +92,17 @@ const points = ref([])
const loading = ref(true)
const loadFailed = ref(false)
// ★ ONLY THE NEWEST REQUEST MAY WRITE. loadPoints is async and re-runs on a locale or country
// change, so two can be in flight at once -- and the one that STARTED first can FINISH last. Both
// used to assign points, so a slow earlier response overwrote a newer one and the map showed the
// previous country's pins with no error and no way for the reader to tell. Every run takes a ticket
// and a stale ticket writes nothing: not the points, not loadFailed, not loading. fetch is left to
// complete rather than aborted, because the response is cached for the next switch back.
let runId = 0
async function loadPoints() {
const mine = ++runId
const current = () => mine === runId
loading.value = true
loadFailed.value = false
try {
@ -132,13 +142,17 @@ async function loadPoints() {
console.error('[country-map] no country-codes given, so there is nothing to draw. Set '
+ 'countryCodes in profile-config, as an array (["TCD"]) or a comma string ("TCD,NER").')
}
if (!current()) return
points.value = want.size ? all.filter((p) => want.has(String(p.code).toUpperCase())) : []
} catch (error) {
if (!current()) return
console.error('[country-map] fetch error:', error)
loadFailed.value = true
points.value = []
} finally {
loading.value = false
// The newest run owns the spinner: an older one finishing later must not clear it while the
// current fetch is still running.
if (current()) loading.value = false
}
}
@ -146,7 +160,15 @@ onMounted(loadPoints)
// A locale change REFETCHES: the popup's labels — country, religion, people-group names — come
// from the API's ?lang=, not from our catalogue, so re-rendering alone leaves them in the old
// language. Measured on the dashboard, where exactly this left every country name in English.
watch(() => [languageCode.value, countryCodes.value.join(',')], loadPoints)
// ★ A STRING, NOT AN ARRAY -- AND THAT IS THE WHOLE FIX. This getter used to return a new array
// literal, and Vue compares a watch's value with Object.is, so a fresh array is ALWAYS "changed".
// Both computeds read the injected cfg, so the effect re-ran on ANY profile-config change and the
// callback then fired unconditionally: toggling isDark, or a host adding pinHint, refetched all
// 2,106 records and redrew the map. MEASURED with real Vue -- an isDark flip produced a refetch,
// and so did adding an unrelated key. A joined string compares by VALUE, so only a real change to
// the language or the country list refetches. The composite re-forward makes config writes routine,
// which is what turned a latent waste into one on every locale switch upstream.
watch(() => `${languageCode.value}|${countryCodes.value.join(',')}`, loadPoints)
</script>
<template>
@ -166,5 +188,11 @@ watch(() => [languageCode.value, countryCodes.value.join(',')], loadPoints)
failed, in all eleven locales — ar "بيانات المناطق", ru "данные по регионам", de
"Regionsdaten" all say regions. The people-groups sentence is already in the catalogue and
already translated in all eleven, so this costs no new string. -->
<p v-if="loadFailed" class="cm-status" role="status">{{ t('People groups could not be loaded. This is a connection problem, not a filter.') }}</p>
<!-- ★ THE SPINNER STATE EXISTED AND WAS NEVER SHOWN. `loading` was assigned in three places and
read in none, so a reader on a country page waited on an EMPTY map with no outline, no pins
and nothing to say why -- indistinguishable from a country with no people groups, and this
element fetches the whole 2,106-record list before it can draw one pin. The string is already
in the catalogue in all eleven locales, so saying so costs no new translation. -->
<p v-if="loading" class="cm-status" role="status">{{ t('Loading results...') }}</p>
<p v-else-if="loadFailed" class="cm-status" role="status">{{ t('People groups could not be loaded. This is a connection problem, not a filter.') }}</p>
</template>

File diff suppressed because one or more lines are too long

View file

@ -70,7 +70,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-country-map> custom element. -->
<script src="../country-map.js" integrity="sha384-CJ5FgNIwYNrSvbpQBKePsIKcQHFxWv8IKl3eBe0vR5/JBZFaMO4XqQRfPyZe/e+f" crossorigin="anonymous"></script>
<script src="../country-map.js" integrity="sha384-x8WP60qZ76MpVyFZJmXUyLEC/lFMhWLKKcRAMStgfpsFj0siJcmHawyW2MwkdhdO" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -171,7 +171,7 @@
"bundle": "country-map",
"scriptUrl": "./country-map/country-map.js",
"output": "dx-maps/country-map/country-map.js",
"integrity": "sha384-CJ5FgNIwYNrSvbpQBKePsIKcQHFxWv8IKl3eBe0vR5/JBZFaMO4XqQRfPyZe/e+f",
"integrity": "sha384-x8WP60qZ76MpVyFZJmXUyLEC/lFMhWLKKcRAMStgfpsFj0siJcmHawyW2MwkdhdO",
"elements": [
"doxa-country-map"
],
@ -190,8 +190,8 @@
"scriptUrl": "./country-map/country-map.js",
"embed": {
"iframe": "<iframe src=\"./country-map/country-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-CJ5FgNIwYNrSvbpQBKePsIKcQHFxWv8IKl3eBe0vR5/JBZFaMO4XqQRfPyZe/e+f\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-CJ5FgNIwYNrSvbpQBKePsIKcQHFxWv8IKl3eBe0vR5/JBZFaMO4XqQRfPyZe/e+f\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-x8WP60qZ76MpVyFZJmXUyLEC/lFMhWLKKcRAMStgfpsFj0siJcmHawyW2MwkdhdO\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-x8WP60qZ76MpVyFZJmXUyLEC/lFMhWLKKcRAMStgfpsFj0siJcmHawyW2MwkdhdO\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>"
}
}
]

View file

@ -171,7 +171,7 @@
"bundle": "country-map",
"scriptUrl": "./country-map/country-map.js",
"output": "dx-maps/country-map/country-map.js",
"integrity": "sha384-CJ5FgNIwYNrSvbpQBKePsIKcQHFxWv8IKl3eBe0vR5/JBZFaMO4XqQRfPyZe/e+f",
"integrity": "sha384-x8WP60qZ76MpVyFZJmXUyLEC/lFMhWLKKcRAMStgfpsFj0siJcmHawyW2MwkdhdO",
"elements": [
"doxa-country-map"
],
@ -190,8 +190,8 @@
"scriptUrl": "./country-map/country-map.js",
"embed": {
"iframe": "<iframe src=\"./country-map/country-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-CJ5FgNIwYNrSvbpQBKePsIKcQHFxWv8IKl3eBe0vR5/JBZFaMO4XqQRfPyZe/e+f\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-CJ5FgNIwYNrSvbpQBKePsIKcQHFxWv8IKl3eBe0vR5/JBZFaMO4XqQRfPyZe/e+f\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-x8WP60qZ76MpVyFZJmXUyLEC/lFMhWLKKcRAMStgfpsFj0siJcmHawyW2MwkdhdO\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-map/country-map.js\" integrity=\"sha384-x8WP60qZ76MpVyFZJmXUyLEC/lFMhWLKKcRAMStgfpsFj0siJcmHawyW2MwkdhdO\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-country-map profile-config='{\"profile\":\"country-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-country-map>"
}
}
]