@@ -265,6 +265,10 @@ export const isNamespace = (
265265 * name (`{ cookie: { options: cookieOptions } }` -> `c.cookie.options`
266266 * reads/writes the var). Storage mounts whole (`c.db.user.findOne`).
267267 * Groups holding nothing (however deep) drop out.
268+ *
269+ * A `merge: true` var keeps its slot when a later module exports a
270+ * namespace under the same key - those helpers are folded into the var
271+ * value by {@link collectMergeSeeds}, not bound as a rival namespace.
268272 */
269273export const collectUsable = (
270274 modules : readonly Module [ ] ,
@@ -274,6 +278,14 @@ export const collectUsable = (
274278 value !== null &&
275279 "$models" in value &&
276280 typeof ( value as { $adapter ?: unknown } ) . $adapter === "function" ;
281+ const isMergeVar = ( value : unknown ) =>
282+ isVar ( value ) && ( value as { $merge ?: boolean } ) . $merge === true ;
283+ const isGroup = ( value : unknown ) =>
284+ value !== null &&
285+ typeof value === "object" &&
286+ ! isFn ( value ) &&
287+ ! isVar ( value ) &&
288+ ! isStorageValue ( value ) ;
277289 const walk = ( mod : Record < string , unknown > ) : Record < string , unknown > => {
278290 const out : Record < string , unknown > = { } ;
279291 for ( const [ name , value ] of Object . entries ( mod ) ) {
@@ -286,11 +298,105 @@ export const collectUsable = (
286298 }
287299 return out ;
288300 } ;
301+ const assign = (
302+ target : Record < string , unknown > ,
303+ name : string ,
304+ value : unknown ,
305+ ) => {
306+ const existing = target [ name ] ;
307+ if ( isMergeVar ( existing ) && isGroup ( value ) ) return ;
308+ if ( isMergeVar ( value ) && isGroup ( existing ) ) {
309+ target [ name ] = value ;
310+ return ;
311+ }
312+ if ( isGroup ( existing ) && isGroup ( value ) ) {
313+ const group = { ...( existing as Record < string , unknown > ) } ;
314+ for ( const [ k , v ] of Object . entries ( value as Record < string , unknown > ) ) {
315+ assign ( group , k , v ) ;
316+ }
317+ target [ name ] = group ;
318+ return ;
319+ }
320+ target [ name ] = value ;
321+ } ;
289322 const fns : Record < string , unknown > = { } ;
290- for ( const mod of resolveModules ( modules ) ) Object . assign ( fns , walk ( mod ) ) ;
323+ for ( const mod of resolveModules ( modules ) ) {
324+ for ( const [ name , value ] of Object . entries ( walk ( mod ) ) ) {
325+ assign ( fns , name , value ) ;
326+ }
327+ }
291328 return fns ;
292329} ;
293330
331+ /**
332+ * Initial values for `merge: true` vars: walk `use` modules in order and
333+ * shallow-merge (1) each merge-var's `default` and (2) namespaces exported
334+ * under the same key as a merge var. Later leaf keys win.
335+ */
336+ export const collectMergeSeeds = (
337+ modules : readonly Module [ ] ,
338+ ) : Record < string , unknown > => {
339+ const resolved = resolveModules ( modules ) ;
340+ const seeds : Record < string , unknown > = { } ;
341+ const keyToVar = new Map < string , string > ( ) ;
342+
343+ const isMergeVar = (
344+ value : unknown ,
345+ ) : value is VarDefination < string , unknown > & {
346+ $merge : true ;
347+ default ?: unknown ;
348+ name : string ;
349+ } => isVar ( value ) && ( value as { $merge ?: boolean } ) . $merge === true ;
350+
351+ const mergeInto = ( name : string , contribution : unknown ) => {
352+ if ( contribution === undefined ) return ;
353+ const current = seeds [ name ] ;
354+ if (
355+ contribution !== null &&
356+ typeof contribution === "object" &&
357+ ! Array . isArray ( contribution ) &&
358+ current !== null &&
359+ typeof current === "object" &&
360+ ! Array . isArray ( current )
361+ ) {
362+ seeds [ name ] = {
363+ ...( current as Record < string , unknown > ) ,
364+ ...( contribution as Record < string , unknown > ) ,
365+ } ;
366+ } else {
367+ seeds [ name ] = contribution ;
368+ }
369+ } ;
370+
371+ const index = ( mod : Record < string , unknown > ) => {
372+ for ( const [ key , value ] of Object . entries ( mod ) ) {
373+ if ( isMergeVar ( value ) ) {
374+ keyToVar . set ( key , value . name ) ;
375+ } else if ( isNamespace ( value ) ) {
376+ index ( value ) ;
377+ }
378+ }
379+ } ;
380+ for ( const mod of resolved ) index ( mod ) ;
381+
382+ const contribute = ( mod : Record < string , unknown > ) => {
383+ for ( const [ key , value ] of Object . entries ( mod ) ) {
384+ if ( isMergeVar ( value ) ) {
385+ mergeInto ( value . name , value . default ) ;
386+ continue ;
387+ }
388+ if ( isNamespace ( value ) ) {
389+ const varName = keyToVar . get ( key ) ;
390+ if ( varName ) mergeInto ( varName , value ) ;
391+ else contribute ( value ) ;
392+ }
393+ }
394+ } ;
395+ for ( const mod of resolved ) contribute ( mod ) ;
396+
397+ return seeds ;
398+ } ;
399+
294400export const isOn = ( value : any ) : value is OnEntry < string > =>
295401 value ?. $on === true ;
296402
0 commit comments