From 5f93aa31451a924bf2fb6f6a80872c9271fb717d Mon Sep 17 00:00:00 2001 From: Morne Alberts Date: Thu, 30 Apr 2026 17:25:11 +0200 Subject: [PATCH 1/3] Centralize neowiki.registration fire on ext.neowiki module load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows-up to https://github.com/ProfessionalWiki/NeoWiki/pull/803 The previous setup duplicated `mw.hook( 'neowiki.registration' ).fire()` in each of the six mount-point initializers in `neowiki.ts`, plus a seventh in RedHerb's SubjectFinder init added by PR #803. Each fire constructed a fresh `FrontendRegistrar` over the same singleton registries — pure boilerplate that any new custom mount point would have to remember to replicate or risk extension property types silently failing to register. Extract a single `registerExtensions()` (mirroring the PHP `NeoWikiExtension::ensureExtensionsRegistered()`) and call it once at `ext.neowiki` module load, before the initializers. Subscribers that load before or after benefit equally thanks to `mw.hook`'s documented replay-on-late-subscribe behavior, already covered by `HookRegistration.spec.ts`. The seven redundant per-mount fires are removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- resources/ext.neowiki/src/neowiki.ts | 29 +++++-------------- tests/RedHerb/resources/subjectFinder/init.js | 10 +------ 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/resources/ext.neowiki/src/neowiki.ts b/resources/ext.neowiki/src/neowiki.ts index a734025f..eff5d06c 100644 --- a/resources/ext.neowiki/src/neowiki.ts +++ b/resources/ext.neowiki/src/neowiki.ts @@ -34,6 +34,13 @@ export function registerSubjectCreatorClickHandler( pinia: Pinia, signal?: Abort }, { signal } ); } +function registerExtensions(): void { + const ext = NeoWikiExtension.getInstance(); + mw.hook( 'neowiki.registration' ).fire( + new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), + ); +} + function initializeNeoWikiApp(): void { queueMicrotask( () => { const neowikiApp = document.querySelector( '#mw-content-text > #ext-neowiki-app' ); @@ -46,9 +53,6 @@ function initializeNeoWikiApp(): void { const pageHasMainSubject = ( neowikiApp as HTMLElement ).dataset.mwNeowikiPageHasMainSubject === 'true'; const ext = NeoWikiExtension.getInstance(); - mw.hook( 'neowiki.registration' ).fire( - new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), - ); const app = createMwApp( NeoWikiApp, { showSubjectCreator, @@ -70,10 +74,6 @@ function initializeSchemaView(): void { if ( viewSchema !== null ) { const ext = NeoWikiExtension.getInstance(); - mw.hook( 'neowiki.registration' ).fire( - new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), - ); - const revisionId = mw.config.get( 'wgRevisionId' ); const schemaName = mw.config.get( 'wgTitle' ) as SchemaName; @@ -107,9 +107,6 @@ function initializeSchemasPage(): void { if ( schemasPage !== null ) { const ext = NeoWikiExtension.getInstance(); - mw.hook( 'neowiki.registration' ).fire( - new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), - ); const app = createMwApp( SchemasPage ); app.use( ext.getPinia() ); @@ -126,10 +123,6 @@ function initializeLayoutView(): void { if ( viewLayout !== null ) { const ext = NeoWikiExtension.getInstance(); - mw.hook( 'neowiki.registration' ).fire( - new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), - ); - const revisionId = mw.config.get( 'wgRevisionId' ); const layoutName = mw.config.get( 'wgTitle' ) as LayoutName; @@ -159,9 +152,6 @@ function initializeLayoutsPage(): void { if ( layoutsPage !== null ) { const ext = NeoWikiExtension.getInstance(); - mw.hook( 'neowiki.registration' ).fire( - new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), - ); const app = createMwApp( LayoutsPage ); app.use( ext.getPinia() ); @@ -178,10 +168,6 @@ function initializeSubjectsManagerPage(): void { if ( subjectsManager !== null ) { const ext = NeoWikiExtension.getInstance(); - mw.hook( 'neowiki.registration' ).fire( - new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), - ); - const app = createMwApp( SubjectsManagerPage ).directive( 'tooltip', CdxTooltip ); const pinia = ext.getPinia(); app.use( pinia ); @@ -196,6 +182,7 @@ const isTestEnvironment = typeof window !== 'undefined' && ( window as unknown as { neoWikiTestMode?: boolean } ).neoWikiTestMode === true; if ( !isTestEnvironment ) { + registerExtensions(); initializeNeoWikiApp(); initializeSchemaView(); initializeLayoutView(); diff --git a/tests/RedHerb/resources/subjectFinder/init.js b/tests/RedHerb/resources/subjectFinder/init.js index 9a271749..b403fc56 100644 --- a/tests/RedHerb/resources/subjectFinder/init.js +++ b/tests/RedHerb/resources/subjectFinder/init.js @@ -12,15 +12,7 @@ return; } - var ext = nw.NeoWikiExtension.getInstance(); - mw.hook( 'neowiki.registration' ).fire( - new nw.FrontendRegistrar( - ext.getTypeSpecificComponentRegistry(), - ext.getPropertyTypeRegistry() - ) - ); - - var pinia = ext.getPinia(); + var pinia = nw.NeoWikiExtension.getInstance().getPinia(); var app = Vue.createMwApp( SubjectFinderPanel ) .directive( 'tooltip', codex.CdxTooltip ); app.use( pinia ); From 8f82a3b268ccaebe1f58479c1a232e7b5227b751 Mon Sep 17 00:00:00 2001 From: Morne Alberts Date: Thu, 30 Apr 2026 17:25:18 +0200 Subject: [PATCH 2/3] Test that ext.neowiki fires neowiki.registration on load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boot is now the single point of truth for `neowiki.registration`. A regression that drops or misplaces the module-load fire would silently break every subscriber and only surface in the browser. Pin the contract with a unit test: with `neoWikiTestMode = false`, importing `@/neowiki` must fire the hook with a registrar wired to the live `NeoWikiExtension` registries — registering a fake type via the fired registrar must be observable on the singleton's `PropertyTypeRegistry` *and* `TypeSpecificComponentRegistry`. This catches a missing fire, a fully detached registrar, and a half-detached registrar (where one registry is fresh and the other live — would only surface as broken Vue rendering in the browser). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../integration/HookRegistration.spec.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/resources/ext.neowiki/tests/integration/HookRegistration.spec.ts b/resources/ext.neowiki/tests/integration/HookRegistration.spec.ts index d6cfa242..2459c155 100644 --- a/resources/ext.neowiki/tests/integration/HookRegistration.spec.ts +++ b/resources/ext.neowiki/tests/integration/HookRegistration.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { markRaw } from 'vue'; import { FrontendRegistrar } from '@/presentation/FrontendRegistrar'; import { TypeSpecificComponentRegistry } from '@/TypeSpecificComponentRegistry'; @@ -79,3 +79,30 @@ describe( 'neowiki.registration hook end-to-end', () => { expect( typeRegistry.getTypeNames() ).toContain( 'late' ); } ); } ); + +describe( 'ext.neowiki module load', () => { + beforeEach( () => setupMwHook() ); + + afterEach( () => { + ( window as unknown as { neoWikiTestMode?: boolean } ).neoWikiTestMode = true; + } ); + + it( 'fires neowiki.registration with a registrar wired to the live extension registries', async () => { + ( window as unknown as { neoWikiTestMode?: boolean } ).neoWikiTestMode = false; + + vi.resetModules(); + await import( '@/neowiki' ); + const { NeoWikiExtension } = await import( '@/NeoWikiExtension' ); + + let receivedRegistrar: FrontendRegistrar | null = null; + ( globalThis as any ).mw.hook( 'neowiki.registration' ).add( ( r: FrontendRegistrar ) => { + receivedRegistrar = r; + } ); + + receivedRegistrar!.registerPropertyType( fakeRegistration( 'boot-fake' ) ); + + const ext = NeoWikiExtension.getInstance(); + expect( ext.getPropertyTypeRegistry().getTypeNames() ).toContain( 'boot-fake' ); + expect( ext.getTypeSpecificComponentRegistry().getPropertyTypes() ).toContain( 'boot-fake' ); + } ); +} ); From 9be907e9b86df7c0126f9f1766307b7889fdf95d Mon Sep 17 00:00:00 2001 From: Morne Alberts Date: Fri, 1 May 2026 11:42:19 +0200 Subject: [PATCH 3/3] Rename registerExtensions to fireRegistrationHook Co-Authored-By: Claude Opus 4.7 (1M context) --- resources/ext.neowiki/src/neowiki.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/ext.neowiki/src/neowiki.ts b/resources/ext.neowiki/src/neowiki.ts index eff5d06c..08d58693 100644 --- a/resources/ext.neowiki/src/neowiki.ts +++ b/resources/ext.neowiki/src/neowiki.ts @@ -34,7 +34,7 @@ export function registerSubjectCreatorClickHandler( pinia: Pinia, signal?: Abort }, { signal } ); } -function registerExtensions(): void { +function fireRegistrationHook(): void { const ext = NeoWikiExtension.getInstance(); mw.hook( 'neowiki.registration' ).fire( new FrontendRegistrar( ext.getTypeSpecificComponentRegistry(), ext.getPropertyTypeRegistry() ), @@ -182,7 +182,7 @@ const isTestEnvironment = typeof window !== 'undefined' && ( window as unknown as { neoWikiTestMode?: boolean } ).neoWikiTestMode === true; if ( !isTestEnvironment ) { - registerExtensions(); + fireRegistrationHook(); initializeNeoWikiApp(); initializeSchemaView(); initializeLayoutView();