@@ -10,8 +10,6 @@ import { client } from '~/helpers/create-apollo-client';
1010import { createHtmlEntityDecoder } from '~/helpers/i18n-utils' ;
1111import en_US from '~/locales/en_US.json' ;
1212
13- import type { App as VueApp } from 'vue' ;
14-
1513// Import Pinia for use in Vue apps
1614import { globalPinia } from '~/store/globalPinia' ;
1715
@@ -22,7 +20,7 @@ const apolloClient = (typeof window !== 'undefined' && window.apolloClient) || c
2220declare global {
2321 interface Window {
2422 globalPinia : typeof globalPinia ;
25- __unifiedApp ?: VueApp ;
23+ LOCALE_DATA ?: string ;
2624 }
2725}
2826
@@ -38,7 +36,7 @@ function setupI18n() {
3836
3937 // Check for window locale data
4038 if ( typeof window !== 'undefined' ) {
41- const windowLocaleData = ( window as unknown as { LOCALE_DATA ?: string } ) . LOCALE_DATA || null ;
39+ const windowLocaleData = window . LOCALE_DATA || null ;
4240 if ( windowLocaleData ) {
4341 try {
4442 parsedMessages = JSON . parse ( decodeURIComponent ( windowLocaleData ) ) ;
@@ -64,19 +62,26 @@ function setupI18n() {
6462
6563// Helper function to parse props from HTML attributes
6664function parsePropsFromElement ( element : Element ) : Record < string , unknown > {
65+ // Early exit if no attributes
66+ if ( ! element . hasAttributes ( ) ) return { } ;
67+
6768 const props : Record < string , unknown > = { } ;
69+ // Pre-compile attribute skip list into a Set for O(1) lookup
70+ const skipAttrs = new Set ( [ 'class' , 'id' , 'style' , 'data-vue-mounted' ] ) ;
6871
6972 for ( const attr of element . attributes ) {
7073 const name = attr . name ;
71- const value = attr . value ;
7274
7375 // Skip Vue internal attributes and common HTML attributes
74- if ( name . startsWith ( 'data-v-' ) || name === 'class' || name === 'id' || name === 'style' ) {
76+ if ( skipAttrs . has ( name ) || name . startsWith ( 'data-v-' ) ) {
7577 continue ;
7678 }
7779
80+ const value = attr . value ;
81+ const first = value . trimStart ( ) [ 0 ] ;
82+
7883 // Try to parse JSON values (handles HTML-encoded JSON)
79- if ( value . startsWith ( '{' ) || value . startsWith ( '[' ) ) {
84+ if ( first === '{' || first === '[' ) {
8085 try {
8186 // Decode HTML entities first
8287 const decoded = value
@@ -126,75 +131,95 @@ export function mountUnifiedApp() {
126131 // Now render components to their locations using the shared context
127132 const mountedComponents : Array < { element : HTMLElement ; unmount : ( ) => void } > = [ ] ;
128133
129- // Components are already in priority order in component-registry
134+ // Batch all selector queries first to identify which components are needed
135+ const componentsToMount : Array < { mapping : ( typeof componentMappings ) [ 0 ] ; element : HTMLElement } > = [ ] ;
136+
137+ // Build a map of all selectors to their mappings for quick lookup
138+ const selectorToMapping = new Map < string , ( typeof componentMappings ) [ 0 ] > ( ) ;
130139 componentMappings . forEach ( ( mapping ) => {
131- const { selector, appId } = mapping ;
132- const selectors = Array . isArray ( selector ) ? selector : [ selector ] ;
133-
134- // Find first matching element
135- for ( const sel of selectors ) {
136- const element = document . querySelector ( sel ) as HTMLElement ;
137- if ( element && ! element . hasAttribute ( 'data-vue-mounted' ) ) {
138- // Get the async component from mapping
139- const component = mapping . component ;
140-
141- // Skip if no component is defined
142- if ( ! component ) {
143- console . error ( `[UnifiedMount] No component defined for ${ appId } ` ) ;
144- continue ;
145- }
140+ const selectors = Array . isArray ( mapping . selector ) ? mapping . selector : [ mapping . selector ] ;
141+ selectors . forEach ( ( sel ) => selectorToMapping . set ( sel , mapping ) ) ;
142+ } ) ;
146143
147- // Parse props from element
148- const props = parsePropsFromElement ( element ) ;
149-
150- // Wrap component in UApp for Nuxt UI support
151- const wrappedComponent = {
152- name : `${ appId } -wrapper` ,
153- setup ( ) {
154- return ( ) =>
155- h (
156- UApp ,
157- { } ,
158- {
159- default : ( ) => h ( component , props ) ,
160- }
161- ) ;
162- } ,
163- } ;
164-
165- // Create vnode with shared app context
166- const vnode = createVNode ( wrappedComponent ) ;
167- vnode . appContext = app . _context ; // Share the app context
168-
169- // Clear the element and render the component into it
170- element . innerHTML = '' ;
171- render ( vnode , element ) ;
172-
173- // Mark as mounted
174- element . setAttribute ( 'data-vue-mounted' , 'true' ) ;
175- element . classList . add ( 'unapi' ) ;
176-
177- // Store for cleanup
178- mountedComponents . push ( {
179- element,
180- unmount : ( ) => render ( null , element ) ,
181- } ) ;
182-
183- break ;
144+ // Query all selectors at once
145+ const allSelectors = Array . from ( selectorToMapping . keys ( ) ) . join ( ',' ) ;
146+
147+ // Early exit if no selectors to query
148+ if ( ! allSelectors ) {
149+ console . debug ( '[UnifiedMount] Mounted 0 components' ) ;
150+ return app ;
151+ }
152+
153+ const foundElements = document . querySelectorAll ( allSelectors ) ;
154+ const processedMappings = new Set < ( typeof componentMappings ) [ 0 ] > ( ) ;
155+
156+ foundElements . forEach ( ( element ) => {
157+ if ( ! element . hasAttribute ( 'data-vue-mounted' ) ) {
158+ // Find which mapping this element belongs to
159+ for ( const [ selector , mapping ] of selectorToMapping ) {
160+ if ( element . matches ( selector ) && ! processedMappings . has ( mapping ) ) {
161+ componentsToMount . push ( { mapping, element : element as HTMLElement } ) ;
162+ processedMappings . add ( mapping ) ;
163+ break ;
164+ }
184165 }
185166 }
186167 } ) ;
187168
188- // Store reference for debugging
189- if ( typeof window !== 'undefined' ) {
190- window . __unifiedApp = app ;
191- window . __mountedComponents = mountedComponents ;
192- }
169+ // Now mount only the components that exist
170+ componentsToMount . forEach ( ( { mapping, element } ) => {
171+ const { appId } = mapping ;
172+ const component = mapping . component ;
173+
174+ // Skip if no component is defined
175+ if ( ! component ) {
176+ console . error ( `[UnifiedMount] No component defined for ${ appId } ` ) ;
177+ return ;
178+ }
179+
180+ // Parse props from element
181+ const props = parsePropsFromElement ( element ) ;
182+
183+ // Wrap component in UApp for Nuxt UI support
184+ const wrappedComponent = {
185+ name : `${ appId } -wrapper` ,
186+ setup ( ) {
187+ return ( ) =>
188+ h (
189+ UApp ,
190+ { } ,
191+ {
192+ default : ( ) => h ( component , props ) ,
193+ }
194+ ) ;
195+ } ,
196+ } ;
197+
198+ // Create vnode with shared app context
199+ const vnode = createVNode ( wrappedComponent ) ;
200+ vnode . appContext = app . _context ; // Share the app context
201+
202+ // Clear the element and render the component into it
203+ element . replaceChildren ( ) ;
204+ render ( vnode , element ) ;
205+
206+ // Mark as mounted
207+ element . setAttribute ( 'data-vue-mounted' , 'true' ) ;
208+ element . classList . add ( 'unapi' ) ;
209+
210+ // Store for cleanup
211+ mountedComponents . push ( {
212+ element,
213+ unmount : ( ) => render ( null , element ) ,
214+ } ) ;
215+ } ) ;
216+
217+ console . debug ( `[UnifiedMount] Mounted ${ mountedComponents . length } components` ) ;
193218
194219 return app ;
195220}
196221
197222// Replace the old autoMountAllComponents with the new unified approach
198223export function autoMountAllComponents ( ) {
199- mountUnifiedApp ( ) ;
224+ return mountUnifiedApp ( ) ;
200225}
0 commit comments