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.
102 lines
5.3 KiB
JavaScript
102 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-provide-shadowing — a nested custom element keeps its OWN pinia, i18n and profileModules, and
|
|
* says so deliberately instead of tripping Vue's overwrite warning 24 times.
|
|
*
|
|
* THE DEFECT (Driver's console, 2026-09-28): 24 Vue warnings —
|
|
* 'App already provides property with key "profileModules" / "appI18n" / "Symbol(pinia)" inherited
|
|
* from its parent element. It will be overwritten with the new value.'
|
|
* eight of each, from a composite mounting child custom elements.
|
|
*
|
|
* ★ THE OVERWRITE IS THE POINT, WHICH IS WHY SILENCING IT WOULD HAVE BEEN WRONG AND FIXING IT BY NOT
|
|
* PROVIDING WOULD HAVE BEEN WORSE. `profileModules` is the plugin's own .vue glob — a child rendering
|
|
* its parent's components is a different bug; pinia is per-instance so two embeds never bleed 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 landed the same day. Inheriting any of the three would be
|
|
* the regression.
|
|
*
|
|
* So this asserts BOTH halves: the child still ends up with its OWN value (the fix is not undone), and
|
|
* the inherited chain is left intact afterwards (the parent's OTHER values still resolve).
|
|
*
|
|
* Exercised against the REAL exported helpers, not a copy — a copied helper would pass while the
|
|
* shipped one rotted. Exit 0 pass · 1 fail.
|
|
*/
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const { provideOwn, withoutInheritedProvides } =
|
|
await import(resolve(ROOT, 'library/elements/provide-own.js'))
|
|
|
|
const fails = []
|
|
const ok = (what, cond, d = '') => { if (!cond) fails.push(`${what}${d ? `\n ${d}` : ''}`) }
|
|
|
|
// A Vue-shaped app: provides inherits from the parent ELEMENT's provides, and app.provide() warns
|
|
// exactly as runtime-core does when the key is already reachable.
|
|
const makeApp = (parentProvides = {}) => {
|
|
const provides = Object.create(parentProvides)
|
|
const warnings = []
|
|
return {
|
|
warnings,
|
|
_context: { provides },
|
|
provide(key, value) {
|
|
if (key in provides) warnings.push(String(key))
|
|
provides[key] = value
|
|
},
|
|
}
|
|
}
|
|
|
|
// ── provideOwn: an inherited key is shadowed WITHOUT a warning ───────────────────────────────────
|
|
{
|
|
const parent = { appI18n: 'PARENT-I18N', somethingElse: 'keep-me' }
|
|
const app = makeApp(parent)
|
|
provideOwn(app, 'appI18n', 'MY-I18N')
|
|
ok('the child ends up with its OWN i18n', app._context.provides.appI18n === 'MY-I18N')
|
|
ok('...as an OWN property, not the parent\'s', Object.hasOwn(app._context.provides, 'appI18n'))
|
|
ok('...without a warning, because the shadow is deliberate', app.warnings.length === 0,
|
|
`warned about: ${app.warnings.join(', ')}`)
|
|
ok('the parent\'s OTHER values still resolve', app._context.provides.somethingElse === 'keep-me')
|
|
ok('and the parent is NOT mutated', parent.appI18n === 'PARENT-I18N')
|
|
}
|
|
// ── provideOwn: an ABSENT key takes the ordinary public path ─────────────────────────────────────
|
|
{
|
|
const app = makeApp({})
|
|
provideOwn(app, 'profileModules', { a: 1 })
|
|
ok('a key nobody provided is set through the public API', app._context.provides.profileModules.a === 1)
|
|
ok('...and still does not warn, because there was nothing to overwrite', app.warnings.length === 0)
|
|
}
|
|
// ── withoutInheritedProvides: for a key we cannot name (pinia's symbol) ──────────────────────────
|
|
{
|
|
const SYM = Symbol('pinia')
|
|
const parent = { [SYM]: 'PARENT-PINIA', other: 'x' }
|
|
const app = makeApp(parent)
|
|
withoutInheritedProvides(app, () => app.provide(SYM, 'MY-PINIA'))
|
|
ok('the child ends up with its own pinia', app._context.provides[SYM] === 'MY-PINIA')
|
|
ok('...with no warning', app.warnings.length === 0, `warned about: ${app.warnings.map(String).join(', ')}`)
|
|
ok('★ the inherited chain is RESTORED — the parent\'s other values resolve again',
|
|
app._context.provides.other === 'x')
|
|
ok('the parent is not mutated', parent[SYM] === 'PARENT-PINIA')
|
|
}
|
|
// ── and it restores the chain even when the callback throws ──────────────────────────────────────
|
|
{
|
|
const parent = { other: 'x' }
|
|
const app = makeApp(parent)
|
|
try { withoutInheritedProvides(app, () => { throw new Error('install blew up') }) } catch { /* expected */ }
|
|
ok('a throwing install still leaves the element able to see its parent',
|
|
app._context.provides.other === 'x')
|
|
}
|
|
// ── the control: the naive call MUST warn, or these assertions prove nothing ─────────────────────
|
|
{
|
|
const app = makeApp({ appI18n: 'PARENT' })
|
|
app.provide('appI18n', 'MINE')
|
|
ok('a plain app.provide() over an inherited key DOES warn (the behaviour being avoided)',
|
|
app.warnings.length === 1)
|
|
}
|
|
|
|
if (fails.length) {
|
|
console.error('PROVIDE-SHADOWING: FAIL')
|
|
for (const f of fails) console.error(` ✗ ${f}`)
|
|
process.exit(1)
|
|
}
|
|
console.log('PROVIDE-SHADOWING: PASS — a nested element keeps its own pinia, i18n and profileModules with no '
|
|
+ 'overwrite warning, the parent\'s other provides still resolve, and the chain is restored even if install throws.')
|