2024-06-01 18:39:01 -03:00
/ *
* Vencord , a Discord client mod
* Copyright ( c ) 2024 Vendicated and contributors
* SPDX - License - Identifier : GPL - 3.0 - or - later
* /
import { Logger } from "@utils/Logger" ;
import { canonicalizeMatch } from "@utils/patches" ;
2025-07-10 00:38:39 +02:00
import { ModuleFactory } from "@vencord/discord-types/webpack" ;
2024-06-01 18:39:01 -03:00
import * as Webpack from "@webpack" ;
import { wreq } from "@webpack" ;
2026-04-16 04:19:42 +02:00
import { AnyModuleFactory } from "@webpack/types" ;
2026-03-19 09:32:30 +01:00
import pLimit from "p-limit" ;
2024-06-01 18:39:01 -03:00
2025-10-01 23:12:33 +02:00
function getWebpackChunkMap() {
const sym = Symbol ( ) ;
let v : Record < PropertyKey , string > | null = null ;
Object . defineProperty ( Object . prototype , sym , {
get ( ) {
v = this ;
return "" ;
} ,
configurable : true
} ) ;
wreq . u ( sym ) ;
delete Object . prototype [ sym ] ;
return v ;
}
2024-06-01 18:39:01 -03:00
export async function loadLazyChunks() {
2025-02-07 20:07:17 -03:00
const LazyChunkLoaderLogger = new Logger ( "LazyChunkLoader" ) ;
2026-03-19 09:32:30 +01:00
const queue = pLimit ( 50 ) ;
2025-02-07 20:07:17 -03:00
2024-06-01 18:39:01 -03:00
try {
LazyChunkLoaderLogger . log ( "Loading all chunks..." ) ;
2025-02-07 20:07:17 -03:00
const validChunks = new Set < PropertyKey > ( ) ;
const invalidChunks = new Set < PropertyKey > ( ) ;
const deferredRequires = new Set < PropertyKey > ( ) ;
2024-06-01 18:39:01 -03:00
2025-02-15 16:33:39 -03:00
const { promise : chunksSearchingDone , resolve : chunksSearchingResolve } = Promise . withResolvers < void > ( ) ;
2024-06-01 18:39:01 -03:00
// True if resolved, false otherwise
const chunksSearchPromises = [ ] as Array < ( ) = > boolean > ;
2025-12-26 23:38:50 -03:00
// This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesnt include those
2026-01-22 16:39:07 -03:00
const CompleteLazyChunkRegex = canonicalizeMatch ( /(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g ) ;
const PartialLazyChunkRegex = canonicalizeMatch ( /(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g ) ;
2024-06-01 18:39:01 -03:00
2024-11-14 14:51:58 -03:00
let foundCssDebuggingLoad = false ;
2024-11-13 18:44:21 -03:00
2024-06-01 18:39:01 -03:00
async function searchAndLoadLazyChunks ( factoryCode : string ) {
2024-11-13 18:44:21 -03:00
// Workaround to avoid loading the CSS debugging chunk which turns the app pink
2024-11-14 14:51:58 -03:00
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : ( foundCssDebuggingLoad = factoryCode . includes ( ".cssDebuggingEnabled&&" ) ) ;
2024-11-13 18:44:21 -03:00
2025-12-26 23:38:50 -03:00
const lazyChunks = factoryCode . matchAll ( hasCssDebuggingLoad ? CompleteLazyChunkRegex : PartialLazyChunkRegex ) ;
2025-02-07 20:07:17 -03:00
const validChunkGroups = new Set < [ chunkIds : PropertyKey [ ] , entryPoint : PropertyKey ] > ( ) ;
2024-06-01 18:39:01 -03:00
2024-11-05 16:49:27 -03:00
const shouldForceDefer = false ;
2024-06-01 18:39:01 -03:00
2026-03-19 15:12:09 +01:00
await Promise . all ( Array . from ( lazyChunks ) . map ( async ( [ , rawChunkIds , entryPoint ] ) = > {
2026-03-19 09:32:30 +01:00
const chunkIds = rawChunkIds
? . matchAll ( Webpack . ChunkIdsRegex )
. map ( m = > {
const numChunkId = Number ( m [ 1 ] ) ;
return Number . isNaN ( numChunkId ) ? m [ 1 ] : numChunkId ;
} )
. toArray ( )
? ? [ ] ;
2024-06-01 18:39:01 -03:00
if ( chunkIds . length === 0 ) {
return ;
}
let invalidChunkGroup = false ;
for ( const id of chunkIds ) {
2024-11-13 18:44:21 -03:00
if ( hasCssDebuggingLoad ) {
if ( chunkIds . length > 1 ) {
throw new Error ( "Found multiple chunks in factory that loads the CSS debugging chunk" ) ;
}
invalidChunks . add ( id ) ;
invalidChunkGroup = true ;
break ;
}
2024-06-01 18:39:01 -03:00
if ( wreq . u ( id ) == null || wreq . u ( id ) === "undefined.js" ) continue ;
2026-03-19 15:12:09 +01:00
const isWorkerAsset = await queue ( ( ) = >
fetch ( wreq . p + wreq . u ( id ) )
. then ( r = > r . text ( ) )
. then ( t = > /importScripts\(|self\.postMessage/ . test ( t ) )
) ;
2024-06-01 18:39:01 -03:00
2024-06-06 00:47:57 -03:00
if ( isWorkerAsset ) {
2024-06-01 18:39:01 -03:00
invalidChunks . add ( id ) ;
invalidChunkGroup = true ;
continue ;
}
validChunks . add ( id ) ;
}
if ( ! invalidChunkGroup ) {
2025-02-07 20:07:17 -03:00
const numEntryPoint = Number ( entryPoint ) ;
validChunkGroups . add ( [ chunkIds , Number . isNaN ( numEntryPoint ) ? entryPoint : numEntryPoint ] ) ;
2024-06-01 18:39:01 -03:00
}
2026-03-19 15:12:09 +01:00
} ) ) ;
2024-06-01 18:39:01 -03:00
// Loads all found valid chunk groups
await Promise . all (
Array . from ( validChunkGroups )
. map ( ( [ chunkIds ] ) = >
2026-03-19 15:12:09 +01:00
Promise . all ( chunkIds . map ( id = > wreq . e ( id ) ) )
2024-06-01 18:39:01 -03:00
)
) ;
// Requires the entry points for all valid chunk groups
for ( const [ , entryPoint ] of validChunkGroups ) {
try {
if ( shouldForceDefer ) {
deferredRequires . add ( entryPoint ) ;
continue ;
}
2025-02-07 20:07:17 -03:00
if ( wreq . m [ entryPoint ] ) wreq ( entryPoint ) ;
2024-06-01 18:39:01 -03:00
} catch ( err ) {
console . error ( err ) ;
}
}
// setImmediate to only check if all chunks were loaded after this function resolves
// We check if all chunks were loaded every time a factory is loaded
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
setTimeout ( ( ) = > {
let allResolved = true ;
for ( let i = 0 ; i < chunksSearchPromises . length ; i ++ ) {
const isResolved = chunksSearchPromises [ i ] ( ) ;
if ( isResolved ) {
// Remove finished promises to avoid having to iterate through a huge array everytime
chunksSearchPromises . splice ( i -- , 1 ) ;
} else {
allResolved = false ;
}
}
if ( allResolved ) chunksSearchingResolve ( ) ;
} , 0 ) ;
}
2025-02-07 20:07:17 -03:00
function factoryListener ( factory : AnyModuleFactory | ModuleFactory ) {
2024-06-01 18:39:01 -03:00
let isResolved = false ;
2025-02-07 20:07:17 -03:00
searchAndLoadLazyChunks ( String ( factory ) )
. then ( ( ) = > isResolved = true )
. catch ( ( ) = > isResolved = true ) ;
2024-06-01 18:39:01 -03:00
chunksSearchPromises . push ( ( ) = > isResolved ) ;
2025-02-07 20:07:17 -03:00
}
2024-06-01 18:39:01 -03:00
2025-02-07 20:07:17 -03:00
Webpack . factoryListeners . add ( factoryListener ) ;
2025-02-12 14:03:18 -03:00
for ( const moduleId in wreq . m ) {
factoryListener ( wreq . m [ moduleId ] ) ;
2024-06-01 18:39:01 -03:00
}
await chunksSearchingDone ;
2025-02-07 20:07:17 -03:00
Webpack . factoryListeners . delete ( factoryListener ) ;
2024-06-01 18:39:01 -03:00
// Require deferred entry points
for ( const deferredRequire of deferredRequires ) {
2025-02-07 20:07:17 -03:00
wreq ( deferredRequire ) ;
2024-06-01 18:39:01 -03:00
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
2025-10-01 23:12:33 +02:00
const chunkMap = getWebpackChunkMap ( ) ;
if ( ! chunkMap ) throw new Error ( "Failed to get chunk map" ) ;
2024-06-01 18:39:01 -03:00
2025-10-01 23:12:33 +02:00
const allChunks = Object . keys ( chunkMap ) . map ( id = > Number . isNaN ( Number ( id ) ) ? id : Number ( id ) ) ;
2024-06-01 18:39:01 -03:00
if ( allChunks . length === 0 ) throw new Error ( "Failed to get all chunks" ) ;
2025-02-07 20:07:17 -03:00
// Chunks which our regex could not catch to load
// It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently
2024-06-01 18:39:01 -03:00
const chunksLeft = allChunks . filter ( id = > {
return ! ( validChunks . has ( id ) || invalidChunks . has ( id ) ) ;
} ) ;
2026-03-19 09:32:30 +01:00
await Promise . all ( chunksLeft . map ( async id = > queue ( async ( ) = > {
2024-06-06 00:47:57 -03:00
const isWorkerAsset = await fetch ( wreq . p + wreq . u ( id ) )
2024-06-01 18:39:01 -03:00
. then ( r = > r . text ( ) )
2025-02-19 01:47:21 -03:00
. then ( t = > /importScripts\(|self\.postMessage/ . test ( t ) ) ;
2024-06-01 18:39:01 -03:00
2025-02-07 20:07:17 -03:00
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
2024-06-06 00:47:57 -03:00
if ( ! isWorkerAsset ) {
2025-02-07 20:07:17 -03:00
await wreq . e ( id ) ;
2024-06-01 18:39:01 -03:00
}
2026-03-19 09:32:30 +01:00
} ) ) ) ;
2024-06-01 18:39:01 -03:00
LazyChunkLoaderLogger . log ( "Finished loading all chunks!" ) ;
} catch ( e ) {
LazyChunkLoaderLogger . log ( "A fatal error occurred:" , e ) ;
}
}