The console showed 24 Vue warnings, 8 each for "profileModules", "appI18n" and "Symbol(pinia)" — "already provides property with key … inherited from its parent element. It will be overwritten with the new value." They come from a composite mounting child custom elements: Vue's CE layer lets a child app inherit its parent ELEMENT's provides, and app.provide() warns on any key already reachable. ALL THREE MUST BE THE CHILD'S OWN, so neither silencing the warning nor honouring it by not providing would have been right. profileModules is the plugin's own .vue glob — a child rendering its parent's components is a different and worse bug. pinia is per-instance so two embeds on one page never bleed store state. And the i18n instance is the one ProfileLoader drives from THIS element's profile-config, which is the whole of the permanently-English fix from earlier today. Inheriting any of the three would be the regression, not the warning. So the shadow is made DELIBERATE at the line that does it. provideOwn() writes to the app's own provides when the key is already reachable and uses the public app.provide() when it is not, so the ordinary path is untouched. pinia needs the other shape: its symbol is module-private, so it can only be provided by app.use(), and the key cannot be named — withoutInheritedProvides() hides the inherited chain for the duration of the install and restores it in a FINALLY, so a throwing install cannot leave an element unable to see its parent. The helpers are split into library/elements/provide-own.js with NO framework import, because defineMapElement.js imports ProfileLoader.vue and is therefore unloadable by the gate that proves it right. Third time today that split has been necessary; the gate now exercises the shipped helpers rather than a copy. check-provide-shadowing asserts BOTH halves — the child ends up with its own value, AND the parent's other provides still resolve afterwards — against a Vue-shaped fake app that warns exactly as runtime-core does. Its control is the naive call, which must still warn. Negative-tested three ways, including moving the restore out of the finally. check-element-bootstrap now accepts either spelling and was re-checked for the thing it actually guards: removing the provide entirely still turns it red.
98 lines
6.5 KiB
JavaScript
98 lines
6.5 KiB
JavaScript
/**
|
|
* defineMapElement.js — the one correct way to turn a profile into a custom element.
|
|
*
|
|
* Every plugin that ships a `<tag>` 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 <html lang>, 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'
|
|
import { provideOwn, withoutInheritedProvides } from './provide-own.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}
|
|
*
|
|
* ★ AND EACH ELEMENT'S VALUES ARE ITS OWN, WHICH VUE WARNS ABOUT WHEN THEY NEST. A composite mounts
|
|
* child custom elements, and Vue's CE layer lets a child app inherit its parent element's provides.
|
|
* runtime-core's app.provide() then warns on every key that is already reachable —
|
|
* 'App already provides property with key "profileModules" inherited from its parent element.
|
|
* It will be overwritten with the new value.'
|
|
* — 8 of those per key, 24 in the Driver's console (2026-09-28), for three keys where overwriting is
|
|
* the POINT. `profileModules` is the plugin's own .vue glob and a child must not render its parent's
|
|
* components; pinia is per-instance so two embeds never bleed state; and the i18n instance is what
|
|
* ProfileLoader drives from THIS element's profile-config, which is the whole of the permanently-
|
|
* English fix. Inheriting any of the three would be the bug, not the warning.
|
|
*
|
|
* So the shadow is made DELIBERATE rather than silenced: provideOwn() writes straight to the app's own
|
|
* provides when the key is already reachable, and uses the public app.provide() when it is not. Vue's
|
|
* warning exists to catch an ACCIDENTAL overwrite; this says, at the one line that does it, that this
|
|
* one is meant. Nothing about the resolved values changes — a child gets exactly what it got before.
|
|
*
|
|
* ★ 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.
|
|
*/
|
|
/**
|
|
* provide(), minus the dev warning, for a value that is MEANT to shadow a parent element's.
|
|
* Falls through to the public API whenever the key is absent, so the ordinary path is unchanged and
|
|
* only the intentional shadow touches internals.
|
|
*/
|
|
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()
|
|
// ★ pinia PROVIDES ITS OWN KEY DURING install(), and its symbol is not exported, so it cannot be
|
|
// shadowed by name the way the two below are. Hiding the INHERITED provides for the duration of
|
|
// the install makes the key genuinely absent, so app.use takes its ordinary path with no warning,
|
|
// and the chain is restored on the next line — every other inherited value still resolves.
|
|
// A per-element pinia is deliberate: two embeds on one page must never bleed store state.
|
|
withoutInheritedProvides(app, () => 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.
|
|
provideOwn(app, 'appI18n', i18n)
|
|
provideOwn(app, 'profileModules', profileModules)
|
|
if (configureApp) configureApp(app, { pinia, i18n })
|
|
},
|
|
})
|
|
}
|
|
|
|
export default defineMapElement
|