diff --git a/internal/check-element-bootstrap.mjs b/internal/check-element-bootstrap.mjs new file mode 100644 index 000000000..91aae35dc --- /dev/null +++ b/internal/check-element-bootstrap.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * check-element-bootstrap — every plugin that ships a custom element must build it the same way. + * + * ★ THE BUG THIS EXISTS FOR WAS LIVE IN FIVE PLUGINS AT ONCE. A profile element is + * defineCustomElement(ProfileLoader, { configureApp }) and the configureApp has four steps; the fourth, + * `app.provide('appI18n', i18n)`, is the one that gets dropped. ProfileLoader injects that instance + * (ProfileLoader.vue:176) to drive its locale from profile-config, so without it the drive never runs + * and every useI18n()/t() string is stuck at the boot locale — while setRuntimeLocale() on the next line + * still fires, so tStatic strings DO follow the host. The element renders a convincing MIXTURE of the + * reader's language and English. + * + * Nothing throws, because the t() shim falls back to its key. So it reads as "a few strings not + * translated yet" and it survived in country-posters, key-cities, gospel-resources, cities-internet and + * my-upg-100-list simultaneously — confirmed independently by regions-plugin, who censused all 19 entry + * files and found the same five and no sixth. + * + * ★ AND A CENSUS THAT COUNTED FUNCTION BODIES SAW FOUR OF SIXTEEN. That is why this gate greps for the + * SHAPE (`defineCustomElement(ProfileLoader`) rather than for a known list: eleven of the sites are not + * `function buildXElement()` at all, they are `const El = defineCustomElement(...)`, and a check written + * around the four that matched would have passed the twelve that did not. + * + * TWO ARMS: + * A. no plugin entry may call defineCustomElement(ProfileLoader, …) directly — use the helper; + * B. the helper itself must provide appI18n, PLANTED as a negative case, so the regression cannot + * return by editing library instead of a plugin. + * + * Exit 0 pass · 1 fail. + */ +import { readFileSync, existsSync, readdirSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const TAG = 'ELEMENT-BOOTSTRAP' +const HELPER = 'library/elements/defineMapElement.js' +const fails = [] + +// Stated exemptions. An element that wraps its OWN root component rather than ProfileLoader is a +// different thing and is not in scope: it has no ProfileLoader to inject anything. +const EXEMPT = new Map([]) + +// ── ARM A: no entry builds a profile element by hand ───────────────────────────────────────────── +const PD = join(ROOT, 'plugin-designs') +let entries = 0, viaHelper = 0 +for (const p of readdirSync(PD)) { + const f = join(PD, p, 'index.js') + if (!existsSync(f)) continue + const src = readFileSync(f, 'utf8') + const buildsProfileElement = /defineCustomElement\s*\(\s*ProfileLoader/.test(src) + const usesHelper = /defineMapElement\s*\(/.test(src) + if (!buildsProfileElement && !usesHelper) continue + entries++ + if (usesHelper) viaHelper++ + if (buildsProfileElement) { + if (EXEMPT.has(p)) continue + fails.push(`${p}/index.js calls defineCustomElement(ProfileLoader, …) directly.\n` + + ` Use ${HELPER}. Building it by hand is how five plugins shipped without\n` + + " provide('appI18n') and rendered a mixture of the reader's language and English.") + } +} +if (entries < 10) { + fails.push(`only ${entries} profile-element entries found — the detection stopped matching and this ` + + 'gate would pass while checking almost nothing.') +} + +// ── ARM B: the helper provides appI18n, proved by PLANTING its absence ─────────────────────────── +const helperSrc = existsSync(join(ROOT, HELPER)) ? readFileSync(join(ROOT, HELPER), 'utf8') : '' +if (!helperSrc) { + fails.push(`${HELPER} does not exist — every entry above is pointing at nothing.`) +} else { + // ★ COMMENTS STRIPPED FIRST, and this gate's own negative case is what caught the need. The helper's + // docstring QUOTES the provide line in prose, so a predicate over the raw source matched the + // documentation and reported the code present even after the code was removed — it would have passed + // a helper that only talked about providing appI18n. A check that reads comments is reading the + // author's intentions, not the program. + const code = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '') + const provides = (s) => /app\.provide\(\s*['"]appI18n['"]\s*,/.test(code(s)) + const installs = (s) => /app\.use\(\s*i18n\s*\)|app\.use\(\s*createAppI18n\(\)\s*\)/.test(code(s)) + if (!provides(helperSrc)) { + fails.push(`${HELPER} installs i18n but never provides it. ProfileLoader injects 'appI18n' to drive\n` + + ' the locale; without the provide the element is permanently at its boot locale, silently.') + } + if (!installs(helperSrc)) fails.push(`${HELPER} does not install an i18n instance at all.`) + // THE PLANT: the same predicate, run against a bootstrap with the provide removed, MUST report it. + // /gm, not /m: the docstring quotes the line too, so removing only the FIRST match deleted the + // DOCUMENTATION and left the code — the plant then "failed to detect" a provide that was still there. + // Caught by this assertion failing on a correct helper, which is the plant doing its job on itself. + const planted = helperSrc.replace(/^.*app\.provide\(\s*['"]appI18n['"].*$/gm, '') + if (provides(planted)) { + fails.push('the check for the provide cannot detect its absence — it is measuring nothing. ' + + 'A gate that passes its own negative case is worse than no gate.') + } +} + +if (fails.length) { + console.error(`${TAG}: FAIL`) + for (const f of fails) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log(`${TAG}: PASS — ${viaHelper}/${entries} profile elements are built through ${HELPER}, which provides ` + + "appI18n; an element built without it is detected, so the 'permanently English' regression cannot return quietly.") diff --git a/library/elements/defineMapElement.js b/library/elements/defineMapElement.js new file mode 100644 index 000000000..fc88c7c1a --- /dev/null +++ b/library/elements/defineMapElement.js @@ -0,0 +1,71 @@ +/** + * defineMapElement.js — the one correct way to turn a profile into a custom element. + * + * Every plugin that ships a `` wraps ProfileLoader in Vue's defineCustomElement and configures the + * app the same way: a per-instance Pinia, a per-instance i18n, and the profile's own .vue modules. Five + * plugins did it by hand. + * + * ★ THE FOURTH LINE IS THE ONE THAT MATTERS, AND IT IS THE ONE THAT GETS LEFT OUT. + * `app.provide('appI18n', i18n)` is not bookkeeping. ProfileLoader reads the instance back with + * `inject('appI18n', null)` (ProfileLoader.vue:176) and drives its global locale from profile-config; if + * nothing provided it, that watcher holds null and the drive never happens. `app.use(i18n)` alone LOOKS + * complete — the catalogue is installed, t() resolves, nothing throws — and every component that reads + * through useI18n()/t() is then stuck at the locale the app booted with, whatever the host asked for. + * + * ★ THE MEASUREMENT, from regions-dashboard/index.js:44-52, where this was hit and written down first: + * "app.use() alone is not enough: createAppI18n() resolves its locale ONCE from , and every + * generated leaf page bakes lang=\"en\", so the element was permanently English no matter what the + * host asked for. ProfileLoader drives appI18n.global.locale.value from profile-config.locale (its + * established contract, ProfileLoader.vue:148-169) — but only if appI18n is PROVIDED. MEASURED before + * this line: with a Spanish catalogue present in the bundle and es set, every string still rendered + * English, and silently, because the t() shim falls back to its key rather than throwing." + * + * That last clause is why it survived in five plugins at once: the failure is a SILENT FALLBACK, so + * nothing throws, and English looks like a working default to anyone developing in English. + * + * It fails quietly in the worst way: setRuntimeLocale() still runs, so the tStatic-based strings DO + * follow the host, and the plugin renders a convincing mixture of the reader's language and English. + * regions-dashboard's own comment records it being measured — a Spanish catalogue present, `es` set, and + * every t() string still English — and gospel-resources/index.js is shipping exactly that today. + * + * So the provide is not a step in a list a caller has to remember. It is the reason this helper exists, + * and check:element-bootstrap plants an element built without it as a negative case. + */ +import { defineCustomElement } from 'vue' +import { createPinia } from 'pinia' +import ProfileLoader from '../ProfileLoader.vue' +import { createAppI18n } from '../i18n/index.js' + +/** + * @param {Object} o + * @param {Object} o.profileModules the profile's `import.meta.glob('./*.vue')` map — per-plugin, which + * is why it is a parameter and not something this file can find + * @param {Function} [o.configureApp] extra per-plugin setup, run AFTER the standard three. It receives + * (app, { pinia, i18n }) so a plugin that needs the store can reach + * THE SAME pinia this created -- wall-of-people-groups takes a store + * off its own instance, and a helper that hid pinia would have given + * it a second one, which is two stores and a silent state split. + * @returns {CustomElementConstructor} + * + * ★ CALL IT ONCE PER TAG. Vue's defineCustomElement returns a CLASS, and registering one class under + * two tag names is undefined behaviour — doxa-simple-map ships two tags and calls this twice for that + * reason. A helper that cached and returned one class would reintroduce the bug it was written around. + */ +export function defineMapElement({ profileModules = {}, configureApp = null } = {}) { + return defineCustomElement(ProfileLoader, { + configureApp(app) { + // Per-instance, so two embeds on one page never bleed state into each other. + const pinia = createPinia() + app.use(pinia) + const i18n = createAppI18n() + app.use(i18n) + // ★ BY REFERENCE, NOT JUST INSTALLED. See the header: without this the element is stuck at its + // boot locale and says so to nobody. + app.provide('appI18n', i18n) + app.provide('profileModules', profileModules) + if (configureApp) configureApp(app, { pinia, i18n }) + }, + }) +} + +export default defineMapElement diff --git a/package.json b/package.json index b3d0dd949..eea350ea1 100644 --- a/package.json +++ b/package.json @@ -226,6 +226,7 @@ "check:scrim-viewport:self-test": "node internal/check-scrim-covers-viewport.mjs --self-test", "check:script-flags": "node internal/check-script-flags-reachable.mjs", "check:settled": "node internal/settled-tree-gate.mjs", + "check:element-bootstrap": "node internal/check-element-bootstrap.mjs", "check:setup-order": "node internal/check-setup-order.mjs", "check:sfc-parse": "node internal/check-sfc-script-parses.mjs", "check:shadow-css-parses": "node internal/check-shadow-css-parses.mjs", diff --git a/plugin-designs/all-people-groups/index.js b/plugin-designs/all-people-groups/index.js index 0cd99f297..7a6fb825b 100644 --- a/plugin-designs/all-people-groups/index.js +++ b/plugin-designs/all-people-groups/index.js @@ -9,22 +9,9 @@ * */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - const profileModules = import.meta.glob('./*.vue') -const AllPeopleGroupsElement = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } -}) +const AllPeopleGroupsElement = defineMapElement({ profileModules }) if (typeof customElements !== 'undefined' && !customElements.get('all-people-groups')) { customElements.define('all-people-groups', AllPeopleGroupsElement) diff --git a/plugin-designs/cities-internet/index.js b/plugin-designs/cities-internet/index.js index b26733015..a1759ad26 100644 --- a/plugin-designs/cities-internet/index.js +++ b/plugin-designs/cities-internet/index.js @@ -18,24 +18,17 @@ * bundle does not have. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. const profileModules = import.meta.glob('./*.vue') function buildCitiesInternetElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so multiple embeds on one page never bleed state. - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const CitiesInternetElement = buildCitiesInternetElement() diff --git a/plugin-designs/country-posters/index.js b/plugin-designs/country-posters/index.js index 0c4b93504..2bff029d6 100644 --- a/plugin-designs/country-posters/index.js +++ b/plugin-designs/country-posters/index.js @@ -29,11 +29,6 @@ * app.provide('profileModules', ...). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── const profileModules = import.meta.glob('./*.vue') @@ -51,13 +46,7 @@ const profileModules = import.meta.glob('./*.vue') * bleed state into each other. */ function buildCountryPostersElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - } - }) + return defineMapElement({ profileModules }) } const CountryPostersElement = buildCountryPostersElement() diff --git a/plugin-designs/demo-map/index.js b/plugin-designs/demo-map/index.js index f90b55d39..05946c9be 100644 --- a/plugin-designs/demo-map/index.js +++ b/plugin-designs/demo-map/index.js @@ -15,10 +15,7 @@ * */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' // ─── Bundle-private profile registry ───────────────────────────────────────── @@ -37,17 +34,13 @@ registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) // ─── Register the custom element ───────────────────────────────────────────── // One tag per bundle, named after the bundle. The guard prevents an // "already registered" error if the IIFE is ever loaded twice on one page. -const DemoMapElement = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) +const DemoMapElement = defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } + }, }) if (typeof customElements !== 'undefined' && !customElements.get('demo-map')) { diff --git a/plugin-designs/doxa-research-map/index.js b/plugin-designs/doxa-research-map/index.js index 5f00a59ec..641a90168 100644 --- a/plugin-designs/doxa-research-map/index.js +++ b/plugin-designs/doxa-research-map/index.js @@ -17,11 +17,6 @@ * hands the result to the loader via app.provide('profileModules'). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file — // so it captures only this bundle's profiles. @@ -41,18 +36,14 @@ const profileModules = import.meta.glob('./*.vue') * Pinia + i18n so multiple embeds on the same page don't bleed state. */ function buildDoxaResearchMapElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } - }) + }, +}) } const DoxaResearchMapElement = buildDoxaResearchMapElement() diff --git a/plugin-designs/doxa-simple-map/index.js b/plugin-designs/doxa-simple-map/index.js index b59c906aa..7d62270d1 100644 --- a/plugin-designs/doxa-simple-map/index.js +++ b/plugin-designs/doxa-simple-map/index.js @@ -28,10 +28,7 @@ * constructs), lazy-loads when Arabic/Hebrew text appears on the basemap. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' // ─── Bundle-private profile registry ───────────────────────────────────────── @@ -64,20 +61,16 @@ registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) * isolation is important when multiple embeds share a page. */ function buildDoxaMapElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) // Hand the bundle's profile registry to ProfileLoader (Q3 contract). // ProfileLoader inject('profileModules')s this — see ProfileLoader.vue. - app.provide('profileModules', profileModules) - } - }) + }, +}) } const DoxaMapElement = buildDoxaMapElement() diff --git a/plugin-designs/gospel-internet/index.js b/plugin-designs/gospel-internet/index.js index dacc76dc8..283a44aa0 100644 --- a/plugin-designs/gospel-internet/index.js +++ b/plugin-designs/gospel-internet/index.js @@ -13,24 +13,16 @@ * The RTL text plugin is installed by the map panel itself (installRtlTextPlugin before `new Map`), on * whichever engine resolveMapProvider picked. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' const profileModules = import.meta.glob('./*.vue') function buildBoardElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so two boards on one page never share state. - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const GospelInternetBoard = buildBoardElement() diff --git a/plugin-designs/gospel-resources/index.js b/plugin-designs/gospel-resources/index.js index a15e96ab5..b7b90212a 100644 --- a/plugin-designs/gospel-resources/index.js +++ b/plugin-designs/gospel-resources/index.js @@ -18,24 +18,17 @@ * bundle does not have. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. const profileModules = import.meta.glob('./*.vue') function buildGospelResourcesElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so multiple embeds on one page never bleed state. - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const GospelResourcesElement = buildGospelResourcesElement() diff --git a/plugin-designs/key-cities/index.js b/plugin-designs/key-cities/index.js index 3026cb27d..92cd41aa0 100644 --- a/plugin-designs/key-cities/index.js +++ b/plugin-designs/key-cities/index.js @@ -18,24 +18,17 @@ * bundle does not have. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. const profileModules = import.meta.glob('./*.vue') function buildKeyCitiesElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so multiple embeds on one page never bleed state. - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const KeyCitiesElement = buildKeyCitiesElement() diff --git a/plugin-designs/my-upg-100-list/index.js b/plugin-designs/my-upg-100-list/index.js index c9837a145..d6c65153b 100644 --- a/plugin-designs/my-upg-100-list/index.js +++ b/plugin-designs/my-upg-100-list/index.js @@ -27,11 +27,6 @@ * constructs), lazy-loads when Arabic/Hebrew text appears on the basemap. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file — // so it captures only this bundle's profiles. Handed to ProfileLoader via @@ -49,15 +44,13 @@ const profileModules = import.meta.glob('./*.vue') * isolation matters when multiple embeds share a page. */ function buildUpg100ListMapElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - app.use(createAppI18n()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Hand the bundle's profile registry to ProfileLoader (Q3 contract). // ProfileLoader inject('profileModules')s this — see ProfileLoader.vue. - app.provide('profileModules', profileModules) - } - }) + }, +}) } const Upg100ListMapElement = buildUpg100ListMapElement() diff --git a/plugin-designs/pplr-data-maps/index.js b/plugin-designs/pplr-data-maps/index.js index 7440c834c..02dcc6a56 100644 --- a/plugin-designs/pplr-data-maps/index.js +++ b/plugin-designs/pplr-data-maps/index.js @@ -5,10 +5,6 @@ * folder's one profile, pplr-data-maps.vue; its ten views live in src/views/. The Mapbox RTL * text plugin is registered once here: many PG names are Arabic, Urdu or Hebrew. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' const profileModules = import.meta.glob('./*.vue') @@ -16,13 +12,5 @@ const profileModules = import.meta.glob('./*.vue') // library/maps/rtl-text.js, on BOTH engines, before every map is constructed. Do not re-add a // hand-rolled mapboxgl-only block here -- it was a strict subset of that call. -const El = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } -}) +const El = defineMapElement({ profileModules }) if (typeof customElements !== 'undefined' && !customElements.get('pplr-data-maps')) customElements.define('pplr-data-maps', El) diff --git a/plugin-designs/regions-dashboard/index.js b/plugin-designs/regions-dashboard/index.js index 15e81e0f7..12d7d9032 100644 --- a/plugin-designs/regions-dashboard/index.js +++ b/plugin-designs/regions-dashboard/index.js @@ -21,11 +21,6 @@ * The profile-config attribute is parsed by ProfileLoader (in @map). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file, so it // captures only this bundle's profiles — flat .vue files sitting directly here, no @@ -38,9 +33,9 @@ const profileModules = import.meta.glob('./*.vue') * embeds ever share one page. */ function buildRegionsDashboardElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference, the way // doxa-simple-map/index.js:84-86 does. app.use() alone is not enough: createAppI18n() // resolves its locale ONCE from , and every generated leaf page bakes @@ -50,12 +45,8 @@ function buildRegionsDashboardElement() { // MEASURED before this line: with a Spanish catalogue present in the bundle and es set, // every string still rendered English, and silently, because the t() shim falls back to // its key rather than throwing. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } - }) + }, +}) } const RegionsDashboardElement = buildRegionsDashboardElement() diff --git a/plugin-designs/research-dashboard/index.js b/plugin-designs/research-dashboard/index.js index c019616ac..91ffd30e6 100644 --- a/plugin-designs/research-dashboard/index.js +++ b/plugin-designs/research-dashboard/index.js @@ -19,10 +19,7 @@ * are imported by the profile .vue — the glob only registers the profile screen(s). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' import { installRtlTextPlugin } from '@map/maps/rtl-text.js' // ★ WAS A CROSS-PLUGIN RELATIVE IMPORT into another plugin's PRIVATE folder @@ -64,9 +61,9 @@ installRtlTextPlugin() * so multiple embeds on the same page don't bleed state. */ function buildDoxaResearchDashboardElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // ★ HAND THE INSTANCE TO ProfileLoader, DON'T JUST INSTALL IT. // ProfileLoader is the root of this custom element, and vue-i18n's useI18n() cannot resolve // its injection at a custom-element root — so ProfileLoader takes the i18n INSTANCE by @@ -80,12 +77,8 @@ function buildDoxaResearchDashboardElement() { // The sibling map entry (plugin-designs/doxa-research-map/index.js:66) has always provided it; // this entry's own header calls itself a mirror of that file, which is how the divergence // survived: the two read as the same plumbing until you diff them. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } - }) + }, +}) } const DoxaResearchDashboardElement = buildDoxaResearchDashboardElement() diff --git a/plugin-designs/template-bundle/index.js b/plugin-designs/template-bundle/index.js index 887967491..868d4a514 100644 --- a/plugin-designs/template-bundle/index.js +++ b/plugin-designs/template-bundle/index.js @@ -19,10 +19,7 @@ * `profile` must match a .vue filename in THIS folder (without the .vue). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' // ─── Bundle-private profile registry ───────────────────────────────────────── @@ -47,17 +44,13 @@ registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) // ─── Register the custom element ───────────────────────────────────────────── // One tag per bundle, named after the bundle. The guard prevents an // "already registered" error if the IIFE is ever loaded twice on one page. -const TemplateBundleElement = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) +const TemplateBundleElement = defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } + }, }) if (typeof customElements !== 'undefined' && !customElements.get('template-bundle')) { diff --git a/plugin-designs/upg-neighbor/index.js b/plugin-designs/upg-neighbor/index.js index 8d16a84f5..b6321d4f1 100644 --- a/plugin-designs/upg-neighbor/index.js +++ b/plugin-designs/upg-neighbor/index.js @@ -7,10 +7,6 @@ * * Mapbox RTL text plugin: registered ONCE at module init — several SEND targets (Urdu/Arabic) are RTL. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' const profileModules = import.meta.glob('./*.vue') @@ -18,13 +14,5 @@ const profileModules = import.meta.glob('./*.vue') // library/maps/rtl-text.js, on BOTH engines, before every map is constructed. Do not re-add a // hand-rolled mapboxgl-only block here -- it was a strict subset of that call. -const El = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } -}) +const El = defineMapElement({ profileModules }) if (typeof customElements !== 'undefined' && !customElements.get('upg-neighbor')) customElements.define('upg-neighbor', El) diff --git a/plugin-designs/wall-of-people-groups/index.js b/plugin-designs/wall-of-people-groups/index.js index b7d53f924..25b9b0119 100644 --- a/plugin-designs/wall-of-people-groups/index.js +++ b/plugin-designs/wall-of-people-groups/index.js @@ -4,25 +4,17 @@ * profile; its components live in src/. The wall's components read the store as this.$store (the * original app's global property), so it is created here, per element, on the element's Pinia. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { usePeopleGroupsStore } from './src/stores/peopleGroupsStore.js' const profileModules = import.meta.glob('./*.vue') -const El = defineCustomElement(ProfileLoader, { - configureApp(app) { - const pinia = createPinia() - app.use(pinia) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) +const El = defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { const store = usePeopleGroupsStore(pinia) store.loadFromLocalStorage() app.config.globalProperties.$store = store - } + }, }) if (typeof customElements !== 'undefined' && !customElements.get('wall-of-people-groups')) customElements.define('wall-of-people-groups', El)