feat: code splitting (#3593)

Co-authored-by: Angelos Bouklis <me@arjix.dev>
This commit is contained in:
JellyBrick
2025-07-12 00:00:03 +09:00
committed by GitHub
parent c04dc92d39
commit b53ece5836
15 changed files with 189 additions and 120 deletions

View File

@ -11,9 +11,9 @@ export function getPlugins() {
return store.get('plugins') as Record<string, PluginConfig>; return store.get('plugins') as Record<string, PluginConfig>;
} }
export function isEnabled(plugin: string) { export async function isEnabled(plugin: string) {
const pluginConfig = deepmerge( const pluginConfig = deepmerge(
allPlugins[plugin].config ?? { enabled: false }, (await allPlugins())[plugin].config ?? { enabled: false },
(store.get('plugins') as Record<string, PluginConfig>)[plugin] ?? {}, (store.get('plugins') as Record<string, PluginConfig>)[plugin] ?? {},
); );
return pluginConfig !== undefined && pluginConfig.enabled; return pluginConfig !== undefined && pluginConfig.enabled;

View File

@ -4,7 +4,7 @@ import { languageResources } from 'virtual:i18n';
export const loadI18n = async () => export const loadI18n = async () =>
await init({ await init({
resources: languageResources, resources: await languageResources(),
lng: 'en', lng: 'en',
fallbackLng: 'en', fallbackLng: 'en',
interpolation: { interpolation: {

View File

@ -62,10 +62,10 @@ import { defaultAuthProxyConfig } from '@/plugins/auth-proxy-adapter/config';
import type { PluginConfig } from '@/types/plugins'; import type { PluginConfig } from '@/types/plugins';
if (!is.macOS()) { if (!is.macOS()) {
delete allPlugins['touchbar']; delete (await allPlugins())['touchbar'];
} }
if (!is.windows()) { if (!is.windows()) {
delete allPlugins['taskbar-mediacontrol']; delete (await allPlugins())['taskbar-mediacontrol'];
} }
// Catch errors and log them // Catch errors and log them
@ -139,13 +139,13 @@ if (is.linux()) {
app.setName('com.github.th_ch.youtube_music'); app.setName('com.github.th_ch.youtube_music');
// Stops chromium from launching its own MPRIS service // 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'); app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
} }
} }
if (config.get('options.proxy')) { if (config.get('options.proxy')) {
const authProxyEnabled = config.plugins.isEnabled('auth-proxy-adapter'); const authProxyEnabled = await config.plugins.isEnabled('auth-proxy-adapter');
let proxyToUse = ''; let proxyToUse = '';
if (authProxyEnabled) { if (authProxyEnabled) {
@ -183,19 +183,23 @@ function onClosed() {
mainWindow = null; 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( ipcMain.handle(
'ytmd:get-config', 'ytmd:get-config',
(_, id: string) => (_, id: string) =>
deepmerge( deepmerge(
allPlugins[id].config ?? { enabled: false }, allPluginStubs[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {}, config.get(`plugins.${id}`) ?? {},
) as PluginConfig, ) as PluginConfig,
); );
ipcMain.handle('ytmd:set-config', (_, name: string, obj: object) => 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) => { config.watch((newValue, oldValue) => {
@ -214,7 +218,7 @@ const initHook = (win: BrowserWindow) => {
if (!isEqual) { if (!isEqual) {
const oldConfig = oldPluginConfigList[id] as PluginConfig; const oldConfig = oldPluginConfigList[id] as PluginConfig;
const config = deepmerge( const config = deepmerge(
allPlugins[id].config ?? { enabled: false }, allPluginStubs[id].config ?? { enabled: false },
newPluginConfig ?? {}, newPluginConfig ?? {},
) as PluginConfig; ) as PluginConfig;
@ -229,7 +233,7 @@ const initHook = (win: BrowserWindow) => {
forceUnloadMainPlugin(id, win); forceUnloadMainPlugin(id, win);
} }
if (allPlugins[id]?.restartNeeded) { if (allPluginStubs[id]?.restartNeeded) {
showNeedToRestartDialog(id); showNeedToRestartDialog(id);
} }
} }
@ -250,8 +254,8 @@ const initHook = (win: BrowserWindow) => {
}); });
}; };
const showNeedToRestartDialog = (id: string) => { const showNeedToRestartDialog = async (id: string) => {
const plugin = allPlugins[id]; const plugin = (await allPlugins())[id];
const dialogOptions: Electron.MessageBoxOptions = { const dialogOptions: Electron.MessageBoxOptions = {
type: 'info', type: 'info',
@ -325,7 +329,7 @@ async function createMainWindow() {
const windowSize = config.get('window-size'); const windowSize = config.get('window-size');
const windowMaximized = config.get('window-maximized'); const windowMaximized = config.get('window-maximized');
const windowPosition: Electron.Point = config.get('window-position'); 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 = { const defaultTitleBarOverlayOptions: Electron.TitleBarOverlay = {
color: '#00000000', color: '#00000000',
@ -369,7 +373,7 @@ async function createMainWindow() {
}, },
...decorations, ...decorations,
}); });
initHook(win); await initHook(win);
initTheme(win); initTheme(win);
await loadAllMainPlugins(win); await loadAllMainPlugins(win);
@ -614,12 +618,12 @@ app.on('activate', async () => {
} }
}); });
const getDefaultLocale = (locale: string) => const getDefaultLocale = async (locale: string) =>
Object.keys(languageResources).includes(locale) ? locale : null; Object.keys(await languageResources()).includes(locale) ? locale : null;
app.whenReady().then(async () => { app.whenReady().then(async () => {
if (!config.get('options.language')) { if (!config.get('options.language')) {
const locale = getDefaultLocale(app.getLocale()); const locale = await getDefaultLocale(app.getLocale());
if (locale) { if (locale) {
config.set('options.language', locale); config.set('options.language', locale);
} }

View File

@ -20,13 +20,17 @@ const createContext = (
id: string, id: string,
win: BrowserWindow, win: BrowserWindow,
): BackendContext<PluginConfig> => ({ ): BackendContext<PluginConfig> => ({
getConfig: () => getConfig: async () =>
deepmerge( deepmerge(
allPlugins[id].config ?? { enabled: false }, (await allPlugins())[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {}, config.get(`plugins.${id}`) ?? {},
) as PluginConfig, ) as PluginConfig,
setConfig: (newConfig) => { setConfig: async (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config); config.setPartial(
`plugins.${id}`,
newConfig,
(await allPlugins())[id].config,
);
}, },
ipc: { ipc: {
@ -96,7 +100,7 @@ export const forceLoadMainPlugin = async (
id: string, id: string,
win: BrowserWindow, win: BrowserWindow,
): Promise<void> => { ): Promise<void> => {
const plugin = mainPlugins[id]; const plugin = (await mainPlugins())[id];
if (!plugin) return; if (!plugin) return;
try { try {
@ -133,7 +137,7 @@ export const loadAllMainPlugins = async (win: BrowserWindow) => {
const pluginConfigs = config.plugins.getPlugins(); const pluginConfigs = config.plugins.getPlugins();
const queue: Promise<void>[] = []; const queue: Promise<void>[] = [];
for (const [plugin, pluginDef] of Object.entries(mainPlugins)) { for (const [plugin, pluginDef] of Object.entries(await mainPlugins())) {
const config = deepmerge(pluginDef.config, pluginConfigs[plugin] ?? {}); const config = deepmerge(pluginDef.config, pluginConfigs[plugin] ?? {});
if (config.enabled) { if (config.enabled) {
queue.push(forceLoadMainPlugin(plugin, win)); queue.push(forceLoadMainPlugin(plugin, win));

View File

@ -17,19 +17,23 @@ const createContext = (
id: string, id: string,
win: BrowserWindow, win: BrowserWindow,
): MenuContext<PluginConfig> => ({ ): MenuContext<PluginConfig> => ({
getConfig: () => getConfig: async () =>
deepmerge( deepmerge(
allPlugins[id].config ?? { enabled: false }, (await allPlugins())[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {}, config.get(`plugins.${id}`) ?? {},
) as PluginConfig, ) as PluginConfig,
setConfig: (newConfig) => { setConfig: async (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config); config.setPartial(
`plugins.${id}`,
newConfig,
(await allPlugins())[id].config,
);
}, },
window: win, window: win,
refresh: async () => { refresh: async () => {
await setApplicationMenu(win); 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'); win.webContents.send('refresh-in-app-menu');
} }
}, },
@ -37,7 +41,7 @@ const createContext = (
export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => { export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
try { try {
const plugin = allPlugins[id]; const plugin = (await allPlugins())[id];
if (!plugin) return; if (!plugin) return;
const menu = plugin.menu?.(createContext(id, win)); 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) => { export const loadAllMenuPlugins = async (win: BrowserWindow) => {
const pluginConfigs = config.plugins.getPlugins(); 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( const config = deepmerge(
pluginDef.config ?? { enabled: false }, pluginDef.config ?? { enabled: false },
pluginConfigs[pluginId] ?? {}, pluginConfigs[pluginId] ?? {},

View File

@ -15,13 +15,17 @@ const loadedPluginMap: Record<
PluginDef<unknown, unknown, unknown> PluginDef<unknown, unknown, unknown>
> = {}; > = {};
const createContext = (id: string): PreloadContext<PluginConfig> => ({ const createContext = (id: string): PreloadContext<PluginConfig> => ({
getConfig: () => getConfig: async () =>
deepmerge( deepmerge(
allPlugins[id].config ?? { enabled: false }, (await allPlugins())[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {}, config.get(`plugins.${id}`) ?? {},
) as PluginConfig, ) as PluginConfig,
setConfig: (newConfig) => { setConfig: async (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config); 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) => { export const forceLoadPreloadPlugin = async (id: string) => {
try { try {
const plugin = preloadPlugins[id]; const plugin = (await preloadPlugins())[id];
if (!plugin) return; if (!plugin) return;
const hasStarted = await startPlugin(id, plugin, { 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(); 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( const config = deepmerge(
pluginDef.config ?? { enable: false }, pluginDef.config ?? { enable: false },
pluginConfigs[pluginId] ?? {}, pluginConfigs[pluginId] ?? {},

View File

@ -18,7 +18,7 @@ const loadedPluginMap: Record<
export const createContext = <Config extends PluginConfig>( export const createContext = <Config extends PluginConfig>(
id: string, id: string,
): RendererContext<Config> => ({ ): RendererContext<Config> => ({
getConfig: async () => getConfig: () =>
window.ipcRenderer.invoke('ytmd:get-config', id) as Promise<Config>, window.ipcRenderer.invoke('ytmd:get-config', id) as Promise<Config>,
setConfig: async (newConfig) => { setConfig: async (newConfig) => {
await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig); await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig);
@ -47,7 +47,7 @@ export const forceUnloadRendererPlugin = async (id: string) => {
delete unregisterStyleMap[id]; delete unregisterStyleMap[id];
delete loadedPluginMap[id]; delete loadedPluginMap[id];
const plugin = rendererPlugins[id]; const plugin = (await rendererPlugins())[id];
if (!plugin) return; if (!plugin) return;
const hasStopped = await stopPlugin(id, plugin, { const hasStopped = await stopPlugin(id, plugin, {
@ -71,7 +71,7 @@ export const forceUnloadRendererPlugin = async (id: string) => {
}; };
export const forceLoadRendererPlugin = async (id: string) => { export const forceLoadRendererPlugin = async (id: string) => {
const plugin = rendererPlugins[id]; const plugin = (await rendererPlugins())[id];
if (!plugin) return; if (!plugin) return;
const hasEvaled = await startPlugin(id, plugin, { const hasEvaled = await startPlugin(id, plugin, {
@ -117,7 +117,7 @@ export const forceLoadRendererPlugin = async (id: string) => {
export const loadAllRendererPlugins = async () => { export const loadAllRendererPlugins = async () => {
const pluginConfigs = window.mainConfig.plugins.getPlugins(); 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] ?? {}); const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {});
if (config.enabled) { if (config.enabled) {

View File

@ -29,21 +29,21 @@ import packageJson from '../package.json';
export type MenuTemplate = Electron.MenuItemConstructorOptions[]; export type MenuTemplate = Electron.MenuItemConstructorOptions[];
// True only if in-app-menu was loaded on launch // 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, plugin: string,
label = '', label = '',
description: string | undefined = undefined, description: string | undefined = undefined,
isNew = false, isNew = false,
hasSubmenu = false, hasSubmenu = false,
refreshMenu: (() => void) | undefined = undefined, refreshMenu: (() => void) | undefined = undefined,
): Electron.MenuItemConstructorOptions => ({ ): Promise<Electron.MenuItemConstructorOptions> => ({
label: label || plugin, label: label || plugin,
sublabel: isNew ? t('main.menu.plugins.new') : undefined, sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: description, toolTip: description,
type: 'checkbox', type: 'checkbox',
checked: config.plugins.isEnabled(plugin), checked: await config.plugins.isEnabled(plugin),
click(item: Electron.MenuItem) { click(item: Electron.MenuItem) {
if (item.checked) { if (item.checked) {
config.plugins.enable(plugin); config.plugins.enable(plugin);
@ -71,19 +71,21 @@ export const mainMenuTemplate = async (
const { navigationHistory } = win.webContents; const { navigationHistory } = win.webContents;
await loadAllMenuPlugins(win); await loadAllMenuPlugins(win);
const menuResult = Object.entries(getAllMenuTemplate()).map( const allPluginsStubs = await allPlugins();
([id, template]) => {
const plugin = allPlugins[id]; const menuResult = await Promise.all(
Object.entries(getAllMenuTemplate()).map(async ([id, template]) => {
const plugin = allPluginsStubs[id];
const pluginLabel = plugin?.name?.() ?? id; const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined; const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion const isNew = plugin?.addedVersion
? satisfies(packageJson.version, plugin.addedVersion) ? satisfies(packageJson.version, plugin.addedVersion)
: false; : false;
if (!config.plugins.isEnabled(id)) { if (!(await config.plugins.isEnabled(id))) {
return [ return [
id, id,
pluginEnabledMenu( await pluginEnabledMenu(
id, id,
pluginLabel, pluginLabel,
pluginDescription, pluginDescription,
@ -101,7 +103,7 @@ export const mainMenuTemplate = async (
sublabel: isNew ? t('main.menu.plugins.new') : undefined, sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: pluginDescription, toolTip: pluginDescription,
submenu: [ submenu: [
pluginEnabledMenu( await pluginEnabledMenu(
id, id,
t('main.menu.plugins.enabled'), t('main.menu.plugins.enabled'),
undefined, undefined,
@ -114,14 +116,15 @@ export const mainMenuTemplate = async (
], ],
} satisfies Electron.MenuItemConstructorOptions, } satisfies Electron.MenuItemConstructorOptions,
] as const; ] as const;
}, }),
); );
const availablePlugins = Object.keys(allPlugins); const availablePlugins = Object.keys(await allPlugins());
const pluginMenus = availablePlugins const pluginMenus = await Promise.all(
availablePlugins
.sort((a, b) => { .sort((a, b) => {
const aPluginLabel = allPlugins[a]?.name?.() ?? a; const aPluginLabel = allPluginsStubs[a]?.name?.() ?? a;
const bPluginLabel = allPlugins[b]?.name?.() ?? b; const bPluginLabel = allPluginsStubs[b]?.name?.() ?? b;
return aPluginLabel.localeCompare(bPluginLabel); return aPluginLabel.localeCompare(bPluginLabel);
}) })
@ -129,7 +132,7 @@ export const mainMenuTemplate = async (
const predefinedTemplate = menuResult.find((it) => it[0] === id); const predefinedTemplate = menuResult.find((it) => it[0] === id);
if (predefinedTemplate) return predefinedTemplate[1]; if (predefinedTemplate) return predefinedTemplate[1];
const plugin = allPlugins[id]; const plugin = allPluginsStubs[id];
const pluginLabel = plugin?.name?.() ?? id; const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined; const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion const isNew = plugin?.addedVersion
@ -144,9 +147,11 @@ export const mainMenuTemplate = async (
true, true,
innerRefreshMenu, innerRefreshMenu,
); );
}); }),
);
const availableLanguages = Object.keys(languageResources); const langResources = await languageResources();
const availableLanguages = Object.keys(langResources);
return [ return [
{ {
@ -445,7 +450,7 @@ export const mainMenuTemplate = async (
availableLanguages availableLanguages
.map( .map(
(lang): Electron.MenuItemConstructorOptions => ({ (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', type: 'checkbox',
checked: (config.get('options.language') ?? 'en') === lang, checked: (config.get('options.language') ?? 'en') === lang,
click() { click() {

View File

@ -45,7 +45,7 @@ export const onPlayerApiReady = async (
}, 2500); }, 2500);
/** Restore saved volume and setup tooltip */ /** Restore saved volume and setup tooltip */
function firstRun() { async function firstRun() {
if (typeof options.savedVolume === 'number') { if (typeof options.savedVolume === 'number') {
// Set saved volume as tooltip // Set saved volume as tooltip
setTooltip(options.savedVolume); setTooltip(options.savedVolume);
@ -66,7 +66,7 @@ export const onPlayerApiReady = async (
injectVolumeHud(noVid); injectVolumeHud(noVid);
if (!noVid) { if (!noVid) {
setupVideoPlayerOnwheel(); setupVideoPlayerOnwheel();
if (!window.mainConfig.plugins.isEnabled('video-toggle')) { if (!await window.mainConfig.plugins.isEnabled('video-toggle')) {
// Video-toggle handles hud positioning on its own // Video-toggle handles hud positioning on its own
const videoMode = () => const videoMode = () =>
api.getPlayerResponse().videoDetails?.musicVideoType !== api.getPlayerResponse().videoDetails?.musicVideoType !==
@ -280,7 +280,7 @@ export const onPlayerApiReady = async (
); );
context.ipc.on('setVolume', (value: number) => setVolume(value)); context.ipc.on('setVolume', (value: number) => setVolume(value));
firstRun(); await firstRun();
}; };
export const onConfigChange = (config: PreciseVolumePluginConfig) => { export const onConfigChange = (config: PreciseVolumePluginConfig) => {

View File

@ -309,8 +309,8 @@ function registerMPRIS(win: BrowserWindow) {
player.volume = Number.parseFloat((newVol / 100).toFixed(2)); player.volume = Number.parseFloat((newVol / 100).toFixed(2));
}); });
player.on('volume', (newVolume: number) => { player.on('volume', async (newVolume: number) => {
if (config.plugins.isEnabled('precise-volume')) { if (await config.plugins.isEnabled('precise-volume')) {
// With precise volume we can set the volume to the exact value. // With precise volume we can set the volume to the exact value.
win.webContents.send('setVolume', ~~(newVolume * 100)); win.webContents.send('setVolume', ~~(newVolume * 100));
} else { } else {

View File

@ -159,9 +159,9 @@ export default createPlugin({
const config = await getConfig(); const config = await getConfig();
this.config = config; this.config = config;
const moveVolumeHud = window.mainConfig.plugins.isEnabled( const moveVolumeHud = (await window.mainConfig.plugins.isEnabled(
'precise-volume', 'precise-volume',
) ))
? (preciseVolumeMoveVolumeHud as (_: boolean) => void) ? (preciseVolumeMoveVolumeHud as (_: boolean) => void)
: () => {}; : () => {};

View File

@ -17,7 +17,7 @@ import { loadI18n, setLanguage } from '@/i18n';
loadI18n().then(async () => { loadI18n().then(async () => {
await setLanguage(config.get('options.language') ?? 'en'); await setLanguage(config.get('options.language') ?? 'en');
loadAllPreloadPlugins(); await loadAllPreloadPlugins();
}); });
ipcRenderer.on('plugin:unload', async (_, id: string) => { ipcRenderer.on('plugin:unload', async (_, id: string) => {

View File

@ -3,18 +3,17 @@ declare module 'virtual:plugins' {
type Plugin = PluginDef<unknown, unknown, unknown, PluginConfig>; type Plugin = PluginDef<unknown, unknown, unknown, PluginConfig>;
export const mainPlugins: Record<string, Plugin>; export const mainPlugins: () => Promise<Record<string, Plugin>>;
export const preloadPlugins: Record<string, Plugin>; export const preloadPlugins: () => Promise<Record<string, Plugin>>;
export const rendererPlugins: Record<string, Plugin>; export const rendererPlugins: () => Promise<Record<string, Plugin>>;
export const allPlugins: Record< export const allPlugins: () => Promise<
string, Record<string, Omit<Plugin, 'backend' | 'preload' | 'renderer'>>
Omit<Plugin, 'backend' | 'preload' | 'renderer'>
>; >;
} }
declare module 'virtual:i18n' { declare module 'virtual:i18n' {
import type { LanguageResources } from '@/i18n/resources/@types'; import type { LanguageResources } from '@/i18n/resources/@types';
export const languageResources: LanguageResources; export const languageResources: () => Promise<LanguageResources>;
} }

View File

@ -4,9 +4,6 @@ import { fileURLToPath } from 'node:url';
import { globSync } from 'glob'; import { globSync } from 'glob';
import { Project } from 'ts-morph'; 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 __dirname = dirname(fileURLToPath(import.meta.url));
const globalProject = new Project({ const globalProject = new Project({
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
@ -27,20 +24,20 @@ export const i18nImporter = () => {
const src = globalProject.createSourceFile( const src = globalProject.createSourceFile(
'vm:i18n', 'vm:i18n',
(writer) => { (writer) => {
// prettier-ignore writer.writeLine('export const languageResources = async () => {');
writer.writeLine(' const entries = await Promise.all([');
for (const { name, path } of plugins) { for (const { name, path } of plugins) {
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); const relativePath = relative(resolve(srcPath, '..'), path).replace(
writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`); /\\/g,
} '/',
);
writer.blankLine(); writer.writeLine(
` import('./${relativePath}').then((mod) => ({ "${name}": { translation: mod.default } })),`,
writer.writeLine('export const languageResources = {'); );
for (const { name } of plugins) {
writer.writeLine(` "${name}": {`);
writer.writeLine(` translation: ${snakeToCamel(name)}Json,`);
writer.writeLine(' },');
} }
writer.writeLine(' ]);');
writer.writeLine(' return Object.assign({}, ...entries);');
writer.writeLine('};'); writer.writeLine('};');
writer.blankLine(); writer.blankLine();
}, },

View File

@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
import { globSync } from 'glob'; import { globSync } from 'glob';
import { Project } from 'ts-morph'; import { Project } from 'ts-morph';
const snakeToCamel = (text: string) => const kebabToCamel = (text: string) =>
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@ -43,31 +43,83 @@ export const pluginVirtualModuleGenerator = (
const src = globalProject.createSourceFile( const src = globalProject.createSourceFile(
'vm:pluginIndexes', 'vm:pluginIndexes',
(writer) => { (writer) => {
// prettier-ignore
for (const { name, path } of plugins) { for (const { name, path } of plugins) {
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); const relativePath = relative(resolve(srcPath, '..'), path).replace(
writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`); /\\/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(); writer.blankLine();
// Context-specific exports // 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) { for (const { name } of plugins) {
const checkMode = mode === 'main' ? 'backend' : mode; const checkMode = mode === 'main' ? 'backend' : mode;
// HACK: To avoid situation like importing renderer plugins in main // HACK: To avoid situation like importing renderer plugins in main
writer.writeLine( 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.writeLine('};');
writer.blankLine(); writer.blankLine();
// All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'> // All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'>
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) { 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.writeLine('};');
writer.blankLine(); writer.blankLine();
}, },