feat(guilds): name the core contribution each declared slot wants #15
@@ -189,10 +189,13 @@ registry.registerExtension(ID, 'player.invite.accepted', InviteGameAccountStep)
|
||||
// populates, but core does not own the word "guild" and publishes no Team page of
|
||||
// its own — so the page is ours and core contributes the activity feed to it.
|
||||
//
|
||||
// Declared under this module's own namespace, which core enforces. Core's fill is
|
||||
// applied after every module chunk has evaluated, so declaring it here is early
|
||||
// enough; on a core that knows nothing of Teams it simply stays empty.
|
||||
registry.declareModuleSlot(ID, 'uo.guild.detail')
|
||||
// Declared under this module's own namespace, which core enforces. The second
|
||||
// argument is what gets core's content into the place: **core offers a
|
||||
// CONTRIBUTION and never names a slot**, so this module says where each one goes
|
||||
// and keeps its own word for the place. Core's fills are applied after every
|
||||
// module chunk has evaluated, so declaring here is early enough; on a core that
|
||||
// knows nothing of Teams the slot simply stays empty.
|
||||
registry.declareModuleSlot(ID, 'uo.guild.detail', { core: 'team.activity' })
|
||||
|
||||
// A SECOND place on the same page, for core's Team forum (TEAMS.md Part 5). Two
|
||||
// declarations rather than one, because a slot holds one component and this module
|
||||
@@ -200,14 +203,14 @@ registry.declareModuleSlot(ID, 'uo.guild.detail')
|
||||
// the feed reads as part of the guild's story, the forum is a room you go into.
|
||||
// Neither knows the other exists, and a core that fills only one leaves the other
|
||||
// empty.
|
||||
registry.declareModuleSlot(ID, 'uo.guild.forum')
|
||||
registry.declareModuleSlot(ID, 'uo.guild.forum', { core: 'team.forum' })
|
||||
|
||||
// And a THIRD, at the top of the same page, for core's per-Team notification
|
||||
// control (TEAMS.md §6.3). Same reasoning as the other two and a different place:
|
||||
// muting a guild is an action ON this page, so it sits with the page's heading
|
||||
// rather than after its content. Core resolves whether this viewer is in the
|
||||
// Team at all — this module neither knows nor asks.
|
||||
registry.declareModuleSlot(ID, 'uo.guild.header')
|
||||
registry.declareModuleSlot(ID, 'uo.guild.header', { core: 'team.notify' })
|
||||
|
||||
// `module.json`'s `coreApi` range is checked by the loader before this file is
|
||||
// ever served, so there is nothing to re-check here. It is logged because a
|
||||
|
||||
@@ -39,12 +39,18 @@ const CHUNK = path.resolve(HERE, '..', 'dist', 'entry.js')
|
||||
// nothing here renders, so a named stub is enough to be imported and passed on.
|
||||
const stub = (name) => Object.assign(() => null, { displayName: name })
|
||||
|
||||
// Core's contribution catalogue, as of MODULE_API 1.6.0. Written down rather than
|
||||
// imported — this suite runs against the BUILT chunk with no core in the process
|
||||
// — which means it is a claim about core that has to be re-read when core's list
|
||||
// changes. That is the same trade the rest of this fake makes.
|
||||
const CORE_CONTRIBUTIONS = ['team.activity', 'team.forum', 'team.notify']
|
||||
|
||||
function fakeRg() {
|
||||
const routes = { public: [], admin: [], player: [] }
|
||||
const nav = { public: [], admin: [], player: [] }
|
||||
const providers = new Map()
|
||||
const extensions = new Map()
|
||||
const declaredSlots = new Set()
|
||||
const declaredSlots = new Map()
|
||||
return {
|
||||
version: '1.3.0',
|
||||
react,
|
||||
@@ -74,13 +80,18 @@ function fakeRg() {
|
||||
extensions.set(slot, { id, Component })
|
||||
},
|
||||
// The INVERTED direction (core API 1.6.0): this module declares a place on
|
||||
// its OWN page and core fills it. Core enforces the namespace, so the fake
|
||||
// does too — a chunk that declared an unnamespaced slot would pass here and
|
||||
// throw in a browser.
|
||||
declareModuleSlot(id, name) {
|
||||
// its OWN page and core fills it. Core enforces the namespace and the
|
||||
// contribution name, so the fake does too — a chunk that declared an
|
||||
// unnamespaced slot, or asked for a contribution core does not offer, would
|
||||
// pass here and throw in a browser.
|
||||
declareModuleSlot(id, name, options = {}) {
|
||||
if (!name.startsWith(`${id}.`)) throw new Error(`declareModuleSlot: "${name}" must be namespaced "${id}."`)
|
||||
if (declaredSlots.has(name)) throw new Error(`extension slot "${name}" already declared`)
|
||||
declaredSlots.add(name)
|
||||
const wants = options.core ?? null
|
||||
if (wants !== null && !CORE_CONTRIBUTIONS.includes(wants)) {
|
||||
throw new Error(`declareModuleSlot: "${name}" asks for core contribution "${wants}", which core does not offer`)
|
||||
}
|
||||
declaredSlots.set(name, wants)
|
||||
},
|
||||
routesFor: (area) => routes[area],
|
||||
navFor: (area) => nav[area],
|
||||
@@ -209,7 +220,7 @@ it('registers under exactly one module id, matching the manifest', () => {
|
||||
assert.deepEqual([...owners], [manifest.id])
|
||||
})
|
||||
|
||||
it('declares its own guild slots, for core to fill', () => {
|
||||
it('declares its own guild slots, each naming the core contribution it wants', () => {
|
||||
// The inverted direction (TEAMS.md Part 3). Teams are a core primitive with no
|
||||
// core page: core owns the activity feed and the forum, this module owns the
|
||||
// word "guild", so this module declares the places and core puts them in.
|
||||
@@ -219,10 +230,15 @@ it('declares its own guild slots, for core to fill', () => {
|
||||
// away this module's ability to place them separately on its own page — and it
|
||||
// does place them separately, the control above the roster and the other two
|
||||
// below it.
|
||||
assert.deepEqual([...registered.declaredSlots], [
|
||||
'uo.guild.detail',
|
||||
'uo.guild.forum',
|
||||
'uo.guild.header',
|
||||
//
|
||||
// The second argument is what actually gets core's content here. **Core offers
|
||||
// a contribution and never names a slot** — the first cut of this reached only
|
||||
// this module, because core filled the literal name `uo.guild.detail` and any
|
||||
// other game's page went empty with no error.
|
||||
assert.deepEqual([...registered.declaredSlots.entries()], [
|
||||
['uo.guild.detail', 'team.activity'],
|
||||
['uo.guild.forum', 'team.forum'],
|
||||
['uo.guild.header', 'team.notify'],
|
||||
])
|
||||
})
|
||||
|
||||
@@ -230,7 +246,7 @@ it('every declared slot is rendered by the page that owns it', () => {
|
||||
// A slot nothing renders is a slot core fills into the void. Asserted against
|
||||
// the source rather than the chunk, since the chunk is minified.
|
||||
const page = fs.readFileSync(path.resolve(HERE, '..', 'src', 'routes', 'public', 'Guild.jsx'), 'utf8')
|
||||
for (const name of registered.declaredSlots) {
|
||||
for (const name of registered.declaredSlots.keys()) {
|
||||
assert.match(page, new RegExp(`name="${name.replace(/\./g, '\.')}"`))
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user