diff --git a/src/config/plugins.ts b/src/config/plugins.ts index 52121a0b..a91bfd97 100644 --- a/src/config/plugins.ts +++ b/src/config/plugins.ts @@ -11,9 +11,9 @@ export function getPlugins() { return store.get('plugins') as Record; } -export function isEnabled(plugin: string) { +export async function isEnabled(plugin: string) { const pluginConfig = deepmerge( - allPlugins[plugin].config ?? { enabled: false }, + (await allPlugins())[plugin].config ?? { enabled: false }, (store.get('plugins') as Record)[plugin] ?? {}, ); return pluginConfig !== undefined && pluginConfig.enabled; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 42c03174..4ba3dce7 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -4,7 +4,7 @@ import { languageResources } from 'virtual:i18n'; export const loadI18n = async () => await init({ - resources: languageResources, + resources: await languageResources(), lng: 'en', fallbackLng: 'en', interpolation: { diff --git a/src/index.ts b/src/index.ts index 848d534e..6ab031aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,10 +62,10 @@ import { defaultAuthProxyConfig } from '@/plugins/auth-proxy-adapter/config'; import type { PluginConfig } from '@/types/plugins'; if (!is.macOS()) { - delete allPlugins['touchbar']; + delete (await allPlugins())['touchbar']; } if (!is.windows()) { - delete allPlugins['taskbar-mediacontrol']; + delete (await allPlugins())['taskbar-mediacontrol']; } // Catch errors and log them @@ -139,13 +139,13 @@ if (is.linux()) { app.setName('com.github.th_ch.youtube_music'); // Stops chromium from launching its own MPRIS service - if (config.plugins.isEnabled('shortcuts')) { + if (await config.plugins.isEnabled('shortcuts')) { app.commandLine.appendSwitch('disable-features', 'MediaSessionService'); } } if (config.get('options.proxy')) { - const authProxyEnabled = config.plugins.isEnabled('auth-proxy-adapter'); + const authProxyEnabled = await config.plugins.isEnabled('auth-proxy-adapter'); let proxyToUse = ''; if (authProxyEnabled) { @@ -183,19 +183,23 @@ function onClosed() { mainWindow = null; } -ipcMain.handle('ytmd:get-main-plugin-names', () => Object.keys(mainPlugins)); +ipcMain.handle('ytmd:get-main-plugin-names', async () => + Object.keys(await mainPlugins()), +); + +const initHook = async (win: BrowserWindow) => { + const allPluginStubs = await allPlugins(); -const initHook = (win: BrowserWindow) => { ipcMain.handle( 'ytmd:get-config', (_, id: string) => deepmerge( - allPlugins[id].config ?? { enabled: false }, + allPluginStubs[id].config ?? { enabled: false }, config.get(`plugins.${id}`) ?? {}, ) as PluginConfig, ); ipcMain.handle('ytmd:set-config', (_, name: string, obj: object) => - config.setPartial(`plugins.${name}`, obj, allPlugins[name].config), + config.setPartial(`plugins.${name}`, obj, allPluginStubs[name].config), ); config.watch((newValue, oldValue) => { @@ -214,7 +218,7 @@ const initHook = (win: BrowserWindow) => { if (!isEqual) { const oldConfig = oldPluginConfigList[id] as PluginConfig; const config = deepmerge( - allPlugins[id].config ?? { enabled: false }, + allPluginStubs[id].config ?? { enabled: false }, newPluginConfig ?? {}, ) as PluginConfig; @@ -229,7 +233,7 @@ const initHook = (win: BrowserWindow) => { forceUnloadMainPlugin(id, win); } - if (allPlugins[id]?.restartNeeded) { + if (allPluginStubs[id]?.restartNeeded) { showNeedToRestartDialog(id); } } @@ -250,8 +254,8 @@ const initHook = (win: BrowserWindow) => { }); }; -const showNeedToRestartDialog = (id: string) => { - const plugin = allPlugins[id]; +const showNeedToRestartDialog = async (id: string) => { + const plugin = (await allPlugins())[id]; const dialogOptions: Electron.MessageBoxOptions = { type: 'info', @@ -325,7 +329,7 @@ async function createMainWindow() { const windowSize = config.get('window-size'); const windowMaximized = config.get('window-maximized'); const windowPosition: Electron.Point = config.get('window-position'); - const useInlineMenu = config.plugins.isEnabled('in-app-menu'); + const useInlineMenu = await config.plugins.isEnabled('in-app-menu'); const defaultTitleBarOverlayOptions: Electron.TitleBarOverlay = { color: '#00000000', @@ -369,7 +373,7 @@ async function createMainWindow() { }, ...decorations, }); - initHook(win); + await initHook(win); initTheme(win); await loadAllMainPlugins(win); @@ -614,12 +618,12 @@ app.on('activate', async () => { } }); -const getDefaultLocale = (locale: string) => - Object.keys(languageResources).includes(locale) ? locale : null; +const getDefaultLocale = async (locale: string) => + Object.keys(await languageResources()).includes(locale) ? locale : null; app.whenReady().then(async () => { if (!config.get('options.language')) { - const locale = getDefaultLocale(app.getLocale()); + const locale = await getDefaultLocale(app.getLocale()); if (locale) { config.set('options.language', locale); } diff --git a/src/loader/main.ts b/src/loader/main.ts index c763bc31..b6b5f5ea 100644 --- a/src/loader/main.ts +++ b/src/loader/main.ts @@ -20,13 +20,17 @@ const createContext = ( id: string, win: BrowserWindow, ): BackendContext => ({ - getConfig: () => + getConfig: async () => deepmerge( - allPlugins[id].config ?? { enabled: false }, + (await allPlugins())[id].config ?? { enabled: false }, config.get(`plugins.${id}`) ?? {}, ) as PluginConfig, - setConfig: (newConfig) => { - config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config); + setConfig: async (newConfig) => { + config.setPartial( + `plugins.${id}`, + newConfig, + (await allPlugins())[id].config, + ); }, ipc: { @@ -96,7 +100,7 @@ export const forceLoadMainPlugin = async ( id: string, win: BrowserWindow, ): Promise => { - const plugin = mainPlugins[id]; + const plugin = (await mainPlugins())[id]; if (!plugin) return; try { @@ -133,7 +137,7 @@ export const loadAllMainPlugins = async (win: BrowserWindow) => { const pluginConfigs = config.plugins.getPlugins(); const queue: Promise[] = []; - for (const [plugin, pluginDef] of Object.entries(mainPlugins)) { + for (const [plugin, pluginDef] of Object.entries(await mainPlugins())) { const config = deepmerge(pluginDef.config, pluginConfigs[plugin] ?? {}); if (config.enabled) { queue.push(forceLoadMainPlugin(plugin, win)); diff --git a/src/loader/menu.ts b/src/loader/menu.ts index d06c872b..13e47f93 100644 --- a/src/loader/menu.ts +++ b/src/loader/menu.ts @@ -17,19 +17,23 @@ const createContext = ( id: string, win: BrowserWindow, ): MenuContext => ({ - getConfig: () => + getConfig: async () => deepmerge( - allPlugins[id].config ?? { enabled: false }, + (await allPlugins())[id].config ?? { enabled: false }, config.get(`plugins.${id}`) ?? {}, ) as PluginConfig, - setConfig: (newConfig) => { - config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config); + setConfig: async (newConfig) => { + config.setPartial( + `plugins.${id}`, + newConfig, + (await allPlugins())[id].config, + ); }, window: win, refresh: async () => { await setApplicationMenu(win); - if (config.plugins.isEnabled('in-app-menu')) { + if (await config.plugins.isEnabled('in-app-menu')) { win.webContents.send('refresh-in-app-menu'); } }, @@ -37,7 +41,7 @@ const createContext = ( export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => { try { - const plugin = allPlugins[id]; + const plugin = (await allPlugins())[id]; if (!plugin) return; const menu = plugin.menu?.(createContext(id, win)); @@ -68,7 +72,7 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => { export const loadAllMenuPlugins = async (win: BrowserWindow) => { const pluginConfigs = config.plugins.getPlugins(); - for (const [pluginId, pluginDef] of Object.entries(allPlugins)) { + for (const [pluginId, pluginDef] of Object.entries(await allPlugins())) { const config = deepmerge( pluginDef.config ?? { enabled: false }, pluginConfigs[pluginId] ?? {}, diff --git a/src/loader/preload.ts b/src/loader/preload.ts index 5e86c7e2..48477049 100644 --- a/src/loader/preload.ts +++ b/src/loader/preload.ts @@ -15,13 +15,17 @@ const loadedPluginMap: Record< PluginDef > = {}; const createContext = (id: string): PreloadContext => ({ - getConfig: () => + getConfig: async () => deepmerge( - allPlugins[id].config ?? { enabled: false }, + (await allPlugins())[id].config ?? { enabled: false }, config.get(`plugins.${id}`) ?? {}, ) as PluginConfig, - setConfig: (newConfig) => { - config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config); + setConfig: async (newConfig) => { + config.setPartial( + `plugins.${id}`, + newConfig, + (await allPlugins())[id].config, + ); }, }); @@ -48,7 +52,7 @@ export const forceUnloadPreloadPlugin = async (id: string) => { export const forceLoadPreloadPlugin = async (id: string) => { try { - const plugin = preloadPlugins[id]; + const plugin = (await preloadPlugins())[id]; if (!plugin) return; const hasStarted = await startPlugin(id, plugin, { @@ -78,10 +82,10 @@ export const forceLoadPreloadPlugin = async (id: string) => { } }; -export const loadAllPreloadPlugins = () => { +export const loadAllPreloadPlugins = async () => { const pluginConfigs = config.plugins.getPlugins(); - for (const [pluginId, pluginDef] of Object.entries(preloadPlugins)) { + for (const [pluginId, pluginDef] of Object.entries(await preloadPlugins())) { const config = deepmerge( pluginDef.config ?? { enable: false }, pluginConfigs[pluginId] ?? {}, diff --git a/src/loader/renderer.ts b/src/loader/renderer.ts index a8ce8ed4..8ca76b02 100644 --- a/src/loader/renderer.ts +++ b/src/loader/renderer.ts @@ -18,7 +18,7 @@ const loadedPluginMap: Record< export const createContext = ( id: string, ): RendererContext => ({ - getConfig: async () => + getConfig: () => window.ipcRenderer.invoke('ytmd:get-config', id) as Promise, setConfig: async (newConfig) => { await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig); @@ -47,7 +47,7 @@ export const forceUnloadRendererPlugin = async (id: string) => { delete unregisterStyleMap[id]; delete loadedPluginMap[id]; - const plugin = rendererPlugins[id]; + const plugin = (await rendererPlugins())[id]; if (!plugin) return; const hasStopped = await stopPlugin(id, plugin, { @@ -71,7 +71,7 @@ export const forceUnloadRendererPlugin = async (id: string) => { }; export const forceLoadRendererPlugin = async (id: string) => { - const plugin = rendererPlugins[id]; + const plugin = (await rendererPlugins())[id]; if (!plugin) return; const hasEvaled = await startPlugin(id, plugin, { @@ -117,7 +117,7 @@ export const forceLoadRendererPlugin = async (id: string) => { export const loadAllRendererPlugins = async () => { const pluginConfigs = window.mainConfig.plugins.getPlugins(); - for (const [pluginId, pluginDef] of Object.entries(rendererPlugins)) { + for (const [pluginId, pluginDef] of Object.entries(await rendererPlugins())) { const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {}); if (config.enabled) { diff --git a/src/menu.ts b/src/menu.ts index 0f560e71..5505fcb2 100644 --- a/src/menu.ts +++ b/src/menu.ts @@ -29,21 +29,21 @@ import packageJson from '../package.json'; export type MenuTemplate = Electron.MenuItemConstructorOptions[]; // True only if in-app-menu was loaded on launch -const inAppMenuActive = config.plugins.isEnabled('in-app-menu'); +const inAppMenuActive = await config.plugins.isEnabled('in-app-menu'); -const pluginEnabledMenu = ( +const pluginEnabledMenu = async ( plugin: string, label = '', description: string | undefined = undefined, isNew = false, hasSubmenu = false, refreshMenu: (() => void) | undefined = undefined, -): Electron.MenuItemConstructorOptions => ({ +): Promise => ({ label: label || plugin, sublabel: isNew ? t('main.menu.plugins.new') : undefined, toolTip: description, type: 'checkbox', - checked: config.plugins.isEnabled(plugin), + checked: await config.plugins.isEnabled(plugin), click(item: Electron.MenuItem) { if (item.checked) { config.plugins.enable(plugin); @@ -71,19 +71,21 @@ export const mainMenuTemplate = async ( const { navigationHistory } = win.webContents; await loadAllMenuPlugins(win); - const menuResult = Object.entries(getAllMenuTemplate()).map( - ([id, template]) => { - const plugin = allPlugins[id]; + const allPluginsStubs = await allPlugins(); + + const menuResult = await Promise.all( + Object.entries(getAllMenuTemplate()).map(async ([id, template]) => { + const plugin = allPluginsStubs[id]; const pluginLabel = plugin?.name?.() ?? id; const pluginDescription = plugin?.description?.() ?? undefined; const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false; - if (!config.plugins.isEnabled(id)) { + if (!(await config.plugins.isEnabled(id))) { return [ id, - pluginEnabledMenu( + await pluginEnabledMenu( id, pluginLabel, pluginDescription, @@ -101,7 +103,7 @@ export const mainMenuTemplate = async ( sublabel: isNew ? t('main.menu.plugins.new') : undefined, toolTip: pluginDescription, submenu: [ - pluginEnabledMenu( + await pluginEnabledMenu( id, t('main.menu.plugins.enabled'), undefined, @@ -114,39 +116,42 @@ export const mainMenuTemplate = async ( ], } satisfies Electron.MenuItemConstructorOptions, ] as const; - }, + }), ); - const availablePlugins = Object.keys(allPlugins); - const pluginMenus = availablePlugins - .sort((a, b) => { - const aPluginLabel = allPlugins[a]?.name?.() ?? a; - const bPluginLabel = allPlugins[b]?.name?.() ?? b; + const availablePlugins = Object.keys(await allPlugins()); + const pluginMenus = await Promise.all( + availablePlugins + .sort((a, b) => { + const aPluginLabel = allPluginsStubs[a]?.name?.() ?? a; + const bPluginLabel = allPluginsStubs[b]?.name?.() ?? b; - return aPluginLabel.localeCompare(bPluginLabel); - }) - .map((id) => { - const predefinedTemplate = menuResult.find((it) => it[0] === id); - if (predefinedTemplate) return predefinedTemplate[1]; + return aPluginLabel.localeCompare(bPluginLabel); + }) + .map((id) => { + const predefinedTemplate = menuResult.find((it) => it[0] === id); + if (predefinedTemplate) return predefinedTemplate[1]; - const plugin = allPlugins[id]; - const pluginLabel = plugin?.name?.() ?? id; - const pluginDescription = plugin?.description?.() ?? undefined; - const isNew = plugin?.addedVersion - ? satisfies(packageJson.version, plugin.addedVersion) - : false; + const plugin = allPluginsStubs[id]; + const pluginLabel = plugin?.name?.() ?? id; + const pluginDescription = plugin?.description?.() ?? undefined; + const isNew = plugin?.addedVersion + ? satisfies(packageJson.version, plugin.addedVersion) + : false; - return pluginEnabledMenu( - id, - pluginLabel, - pluginDescription, - isNew, - true, - innerRefreshMenu, - ); - }); + return pluginEnabledMenu( + id, + pluginLabel, + pluginDescription, + isNew, + true, + innerRefreshMenu, + ); + }), + ); - const availableLanguages = Object.keys(languageResources); + const langResources = await languageResources(); + const availableLanguages = Object.keys(langResources); return [ { @@ -445,7 +450,7 @@ export const mainMenuTemplate = async ( availableLanguages .map( (lang): Electron.MenuItemConstructorOptions => ({ - label: `${languageResources[lang].translation.language?.name ?? 'Unknown'} (${languageResources[lang].translation.language?.['local-name'] ?? 'Unknown'})`, + label: `${langResources[lang].translation.language?.name ?? 'Unknown'} (${langResources[lang].translation.language?.['local-name'] ?? 'Unknown'})`, type: 'checkbox', checked: (config.get('options.language') ?? 'en') === lang, click() { diff --git a/src/plugins/precise-volume/renderer.ts b/src/plugins/precise-volume/renderer.ts index 39714d29..084b2bfa 100644 --- a/src/plugins/precise-volume/renderer.ts +++ b/src/plugins/precise-volume/renderer.ts @@ -45,7 +45,7 @@ export const onPlayerApiReady = async ( }, 2500); /** Restore saved volume and setup tooltip */ - function firstRun() { + async function firstRun() { if (typeof options.savedVolume === 'number') { // Set saved volume as tooltip setTooltip(options.savedVolume); @@ -66,7 +66,7 @@ export const onPlayerApiReady = async ( injectVolumeHud(noVid); if (!noVid) { setupVideoPlayerOnwheel(); - if (!window.mainConfig.plugins.isEnabled('video-toggle')) { + if (!await window.mainConfig.plugins.isEnabled('video-toggle')) { // Video-toggle handles hud positioning on its own const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== @@ -280,7 +280,7 @@ export const onPlayerApiReady = async ( ); context.ipc.on('setVolume', (value: number) => setVolume(value)); - firstRun(); + await firstRun(); }; export const onConfigChange = (config: PreciseVolumePluginConfig) => { diff --git a/src/plugins/shortcuts/mpris.ts b/src/plugins/shortcuts/mpris.ts index 5737a64b..3cd4a8ed 100644 --- a/src/plugins/shortcuts/mpris.ts +++ b/src/plugins/shortcuts/mpris.ts @@ -309,8 +309,8 @@ function registerMPRIS(win: BrowserWindow) { player.volume = Number.parseFloat((newVol / 100).toFixed(2)); }); - player.on('volume', (newVolume: number) => { - if (config.plugins.isEnabled('precise-volume')) { + player.on('volume', async (newVolume: number) => { + if (await config.plugins.isEnabled('precise-volume')) { // With precise volume we can set the volume to the exact value. win.webContents.send('setVolume', ~~(newVolume * 100)); } else { diff --git a/src/plugins/video-toggle/index.tsx b/src/plugins/video-toggle/index.tsx index 7a00c53d..dc904b39 100644 --- a/src/plugins/video-toggle/index.tsx +++ b/src/plugins/video-toggle/index.tsx @@ -159,9 +159,9 @@ export default createPlugin({ const config = await getConfig(); this.config = config; - const moveVolumeHud = window.mainConfig.plugins.isEnabled( + const moveVolumeHud = (await window.mainConfig.plugins.isEnabled( 'precise-volume', - ) + )) ? (preciseVolumeMoveVolumeHud as (_: boolean) => void) : () => {}; diff --git a/src/preload.ts b/src/preload.ts index 9e8bf7dd..81c5a669 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -17,7 +17,7 @@ import { loadI18n, setLanguage } from '@/i18n'; loadI18n().then(async () => { await setLanguage(config.get('options.language') ?? 'en'); - loadAllPreloadPlugins(); + await loadAllPreloadPlugins(); }); ipcRenderer.on('plugin:unload', async (_, id: string) => { diff --git a/src/virtual-module.d.ts b/src/virtual-module.d.ts index ab940f3d..a38a6175 100644 --- a/src/virtual-module.d.ts +++ b/src/virtual-module.d.ts @@ -3,18 +3,17 @@ declare module 'virtual:plugins' { type Plugin = PluginDef; - export const mainPlugins: Record; - export const preloadPlugins: Record; - export const rendererPlugins: Record; + export const mainPlugins: () => Promise>; + export const preloadPlugins: () => Promise>; + export const rendererPlugins: () => Promise>; - export const allPlugins: Record< - string, - Omit + export const allPlugins: () => Promise< + Record> >; } declare module 'virtual:i18n' { import type { LanguageResources } from '@/i18n/resources/@types'; - export const languageResources: LanguageResources; + export const languageResources: () => Promise; } diff --git a/vite-plugins/i18n-importer.mts b/vite-plugins/i18n-importer.mts index 1efe3736..3fffbaf4 100644 --- a/vite-plugins/i18n-importer.mts +++ b/vite-plugins/i18n-importer.mts @@ -4,9 +4,6 @@ import { fileURLToPath } from 'node:url'; import { globSync } from 'glob'; import { Project } from 'ts-morph'; -const snakeToCamel = (text: string) => - text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); - const __dirname = dirname(fileURLToPath(import.meta.url)); const globalProject = new Project({ tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), @@ -27,20 +24,20 @@ export const i18nImporter = () => { const src = globalProject.createSourceFile( 'vm:i18n', (writer) => { - // prettier-ignore + writer.writeLine('export const languageResources = async () => {'); + writer.writeLine(' const entries = await Promise.all(['); for (const { name, path } of plugins) { - const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); - writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`); - } + const relativePath = relative(resolve(srcPath, '..'), path).replace( + /\\/g, + '/', + ); - writer.blankLine(); - - writer.writeLine('export const languageResources = {'); - for (const { name } of plugins) { - writer.writeLine(` "${name}": {`); - writer.writeLine(` translation: ${snakeToCamel(name)}Json,`); - writer.writeLine(' },'); + writer.writeLine( + ` import('./${relativePath}').then((mod) => ({ "${name}": { translation: mod.default } })),`, + ); } + writer.writeLine(' ]);'); + writer.writeLine(' return Object.assign({}, ...entries);'); writer.writeLine('};'); writer.blankLine(); }, diff --git a/vite-plugins/plugin-importer.mts b/vite-plugins/plugin-importer.mts index 2ebd1b76..44bd3e98 100644 --- a/vite-plugins/plugin-importer.mts +++ b/vite-plugins/plugin-importer.mts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { globSync } from 'glob'; import { Project } from 'ts-morph'; -const snakeToCamel = (text: string) => +const kebabToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -43,31 +43,83 @@ export const pluginVirtualModuleGenerator = ( const src = globalProject.createSourceFile( 'vm:pluginIndexes', (writer) => { - // prettier-ignore for (const { name, path } of plugins) { - const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); - writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`); - } + const relativePath = relative(resolve(srcPath, '..'), path).replace( + /\\/g, + '/', + ); + if (mode === 'main') { + // dynamic import (for main) + writer.writeLine( + `const ${kebabToCamel(name)}PluginImport = () => import('./${relativePath}');`, + ); + writer.writeLine( + `const ${kebabToCamel(name)}Plugin = async () => (await ${kebabToCamel(name)}PluginImport()).default;`, + ); + writer.writeLine( + `const ${kebabToCamel(name)}PluginStub = async () => (await ${kebabToCamel(name)}PluginImport()).pluginStub;`, + ); + } else { + // static import (preload does not support dynamic import) + writer.writeLine( + `import ${kebabToCamel(name)}PluginImport, { pluginStub as ${kebabToCamel(name)}PluginStubImport } from "./${relativePath}";`, + ); + writer.writeLine( + `const ${kebabToCamel(name)}Plugin = () => Promise.resolve(${kebabToCamel(name)}PluginImport);`, + ); + writer.writeLine( + `const ${kebabToCamel(name)}PluginStub = () => Promise.resolve(${kebabToCamel(name)}PluginStubImport);`, + ); + } + } writer.blankLine(); // Context-specific exports - writer.writeLine(`export const ${mode}Plugins = {`); + writer.writeLine(`let ${mode}PluginsCache = null;`); + writer.writeLine(`export const ${mode}Plugins = async () => {`); + writer.writeLine( + ` if (${mode}PluginsCache) return await ${mode}PluginsCache;`, + ); + writer.writeLine( + ' const { promise, resolve } = Promise.withResolvers();', + ); + writer.writeLine(' ' + `${mode}PluginsCache = promise;`); + writer.writeLine(' const pluginEntries = await Promise.all(['); for (const { name } of plugins) { const checkMode = mode === 'main' ? 'backend' : mode; // HACK: To avoid situation like importing renderer plugins in main writer.writeLine( - ` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`, + ` ${kebabToCamel(name)}Plugin().then((plg) => plg['${checkMode}'] ? ["${name}", plg] : null),`, ); } + writer.writeLine(' ]);'); + writer.writeLine( + ' resolve(pluginEntries.filter((entry) => entry).reduce((acc, [name, plg]) => { acc[name] = plg; return acc; }, {}));', + ); + writer.writeLine(` return await ${mode}PluginsCache;`); writer.writeLine('};'); writer.blankLine(); // All plugins export (stub only) // Omit - writer.writeLine('export const allPlugins = {'); + writer.writeLine('let allPluginsCache = null;'); + writer.writeLine('export const allPlugins = async () => {'); + writer.writeLine(' if (allPluginsCache) return await allPluginsCache;'); + writer.writeLine( + ' const { promise, resolve } = Promise.withResolvers();', + ); + writer.writeLine(' allPluginsCache = promise;'); + writer.writeLine(' const stubEntries = await Promise.all(['); for (const { name } of plugins) { - writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`); + writer.writeLine( + ` ${kebabToCamel(name)}PluginStub().then((stub) => ["${name}", stub]),`, + ); } + writer.writeLine(' ]);'); + writer.writeLine( + ' resolve(stubEntries.reduce((acc, [name, plg]) => { acc[name] = plg; return acc; }, {}));', + ); + writer.writeLine(' return await promise;'); writer.writeLine('};'); writer.blankLine(); },