diff --git a/scripts/build.ts b/scripts/build.ts index efb0df5..db168c5 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -8,6 +8,7 @@ import pkg from '../package.json' import asRequire from './plugins/as-require' import hermesSwcPlugin from './plugins/hermes-swc' import hermesCPlugin from './plugins/hermesc' +import importDefer from './plugins/import-defer' import shimAliases from './plugins/shim-aliases' const ShimsDir = `${import.meta.dir}/../shims` @@ -90,6 +91,7 @@ export default async function build(dev = Dev, log = true) { asRequire(), shimAliases(ShimsDir), importGlobPlugin(), + importDefer(), hermesSwcPlugin(), hermesCPlugin({ flags: [ diff --git a/scripts/plugins/import-defer.ts b/scripts/plugins/import-defer.ts new file mode 100644 index 0000000..92ce710 --- /dev/null +++ b/scripts/plugins/import-defer.ts @@ -0,0 +1,127 @@ +import MagicString from 'magic-string' +import type { RolldownPlugin } from 'rolldown' + +// `import defer * as ns from './bar.js'` +const DEFER_NAMESPACE_IMPORT_REGEX = + /import\s+defer\s+(\*\s+as\s+\w+)\s+from\s+(['"])([^'"]+)\2\s*;?/g + +/** + * Really wacky support for `import defer` statements by transforming them to `require()`. + * + * `import defer` statements are transformed to provide on-demand/lazy loading. + * + * This is achieved by creating a scoped closure to cache the module. + * - The deferred namespace import is converted to a function that triggers the `require()` when called. + * - Usages like `ns.foo` are rewritten to `ns().foo` to execute the loader. + */ +export default function importDefer() { + return { + name: 'import-defer', + async transform(code) { + const s = new MagicString(code) + let hasReplacements = false + + const allBindingMatches = [ + ...code.matchAll(DEFER_NAMESPACE_IMPORT_REGEX), + ] + + for (const match of allBindingMatches) { + const [fullMatch, bindingsStr, , modulePath] = match + const bindings = parseBindings(bindingsStr) + + if (bindings.length > 0) { + const localName = bindings[0].local + + // Replace the original import defer statement with the IIFE that creates the lazy function. + s.overwrite( + match.index, + match.index + fullMatch.length, + generateDeferReplacement(bindings, modulePath), + ) + hasReplacements = true + + // Find all usages of the imported identifier + const usageRegex = new RegExp(`\\b${localName}\\b`, 'g'); + + for (const usageMatch of code.matchAll(usageRegex)) { + // Original import declaration + if ( + usageMatch.index >= match.index && + usageMatch.index < match.index + fullMatch.length + ) { + continue + } + + const startIndex = usageMatch.index + const endIndex = startIndex + usageMatch[0].length + + // Check for context to decide on the replacement by inspecting surrounding characters. + // Look for the next non-whitespace character. + let nextChar = '' + let i = endIndex + while (i < code.length) { + if (!/\s/.test(code[i])) { + nextChar = code[i] + break + } + i++ + } + + // Skip object property key: `Identifier: ...` + if (nextChar === ':') continue + + let prevChar = '' + i = startIndex - 1 + while (i >= 0) { + if (!/\s/.test(code[i])) { + prevChar = code[i] + break + } + i-- + } + + // Shorthand object property: { ..., Identifier, ... } + const isShorthand = + (prevChar === '{' || prevChar === ',') && + (nextChar === '}' || nextChar === ',') + + if (isShorthand) + s.overwrite( + startIndex, + endIndex, + `${localName}: ${localName}()`, + ) + // All other usages (value, property access). `doThing(Identifier)`, `Identifier.foo` + else s.overwrite(startIndex, endIndex, `${localName}()`) + } + } + } + + if (!hasReplacements) return null + + return { + code: s.toString(), + map: s.generateMap({ hires: true }), + } + }, + } satisfies RolldownPlugin +} + +type Bindings = { local: string; imported: string }[] + +function parseBindings(bindingsStr: string): Bindings { + const remainingStr = bindingsStr.trim() + const namespaceMatch = remainingStr.match(/^\*\s+as\s+(\w+)/) + if (namespaceMatch) return [{ local: namespaceMatch[1], imported: '*' }] + + return [] +} + +function generateDeferReplacement(bindings: Bindings, modulePath: string) { + if (!bindings.length) return '' + + const [{ local: localName }] = bindings + const cacheVar = `_cache_${modulePath.replace(/[^a-zA-Z0-9]/g, '_')}` + + return `var ${cacheVar};const ${localName}=()=>${cacheVar}??=require('${modulePath}')` +} diff --git a/src/plugins/init/api.components/index.ts b/src/plugins/init/api.components/index.ts index 6f4a59d..9974c32 100644 --- a/src/plugins/init/api.components/index.ts +++ b/src/plugins/init/api.components/index.ts @@ -1,3 +1,4 @@ +import defer * as Components from '@revenge-mod/components' import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' import { PluginFlags } from '@revenge-mod/plugins/constants' import { defineLazyProperties } from '@revenge-mod/utils/object' @@ -14,7 +15,7 @@ registerPlugin( init({ unscoped }) { defineLazyProperties(unscoped, { components: () => { - return require('@revenge-mod/components') + return Components }, }) }, diff --git a/src/plugins/init/api.discord/index.ts b/src/plugins/init/api.discord/index.ts index 37b8fd8..3fbfe9a 100644 --- a/src/plugins/init/api.discord/index.ts +++ b/src/plugins/init/api.discord/index.ts @@ -1,4 +1,10 @@ +import defer * as DiscordActions from '@revenge-mod/discord/actions' +import defer * as DiscordCommon from '@revenge-mod/discord/common' import { Logger } from '@revenge-mod/discord/common' +import defer * as DiscordDesign from '@revenge-mod/discord/design' +import defer * as DiscordFlux from '@revenge-mod/discord/flux' +import defer * as DiscordModulesMainTabsV2 from '@revenge-mod/discord/modules/main_tabs_v2' +import defer * as DiscordNative from '@revenge-mod/discord/native' import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' import { PluginFlags } from '@revenge-mod/plugins/constants' import { @@ -32,7 +38,7 @@ registerPlugin( {} as PluginApiDiscord.Modules, { mainTabsV2: () => { - return require('@revenge-mod/discord/modules/main_tabs_v2') + return DiscordModulesMainTabsV2 }, settings: () => ({ ...require('@revenge-mod/discord/modules/settings'), @@ -43,19 +49,19 @@ registerPlugin( } as PluginApiDiscord), { actions: () => { - return require('@revenge-mod/discord/actions') + return DiscordActions }, common: () => { - return require('@revenge-mod/discord/common') + return DiscordCommon }, flux: () => { - return require('@revenge-mod/discord/flux') + return DiscordFlux }, design: () => { - return require('@revenge-mod/discord/design') + return DiscordDesign }, native: () => { - return require('@revenge-mod/discord/native') + return DiscordNative }, }, ) diff --git a/src/plugins/init/api.externals/index.ts b/src/plugins/init/api.externals/index.ts index 6499139..d70656b 100644 --- a/src/plugins/init/api.externals/index.ts +++ b/src/plugins/init/api.externals/index.ts @@ -1,3 +1,7 @@ +import defer * as Browserify from '@revenge-mod/externals/browserify' +import defer * as ReactNativeClipboard from '@revenge-mod/externals/react-native-clipboard' +import defer * as ReactNavigation from '@revenge-mod/externals/react-navigation' +import defer * as Shopify from '@revenge-mod/externals/shopify' import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' import { PluginFlags } from '@revenge-mod/plugins/constants' import { defineLazyProperties } from '@revenge-mod/utils/object' @@ -17,16 +21,16 @@ registerPlugin( (unscoped.externals = {} as PluginApiExternals), { Browserify: () => { - return require('@revenge-mod/externals/browserify') + return Browserify }, ReactNativeClipboard: () => { - return require('@revenge-mod/externals/react-native-clipboard') + return ReactNativeClipboard }, ReactNavigation: () => { - return require('@revenge-mod/externals/react-navigation') + return ReactNavigation }, Shopify: () => { - return require('@revenge-mod/externals/shopify') + return Shopify }, }, ) diff --git a/src/plugins/preinit/api.utils/index.ts b/src/plugins/preinit/api.utils/index.ts index 8e8dd11..8d27fd6 100644 --- a/src/plugins/preinit/api.utils/index.ts +++ b/src/plugins/preinit/api.utils/index.ts @@ -1,5 +1,13 @@ import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' import { PluginFlags } from '@revenge-mod/plugins/constants' +import defer * as UtilsCallback from '@revenge-mod/utils/callback' +import defer * as UtilsDiscord from '@revenge-mod/utils/discord' +import defer * as UtilsError from '@revenge-mod/utils/error' +import defer * as UtilsObject from '@revenge-mod/utils/object' +import defer * as UtilsPromise from '@revenge-mod/utils/promise' +import defer * as UtilsProxy from '@revenge-mod/utils/proxy' +import defer * as UtilsReact from '@revenge-mod/utils/react' +import defer * as UtilsTree from '@revenge-mod/utils/tree' registerPlugin( { @@ -12,17 +20,17 @@ registerPlugin( { preInit({ unscoped }) { unscoped.utils = { - callback: require('@revenge-mod/utils/callback'), - error: require('@revenge-mod/utils/error'), - object: require('@revenge-mod/utils/object'), - promise: require('@revenge-mod/utils/promise'), - proxy: require('@revenge-mod/utils/proxy'), - tree: require('@revenge-mod/utils/tree'), + callback: UtilsCallback, + error: UtilsError, + object: UtilsObject, + promise: UtilsPromise, + proxy: UtilsProxy, + tree: UtilsTree, } }, init({ unscoped: { utils } }) { - utils.discord = require('@revenge-mod/utils/discord') - utils.react = require('@revenge-mod/utils/react') + utils.discord = UtilsDiscord + utils.react = UtilsReact }, }, PluginFlags.Enabled, diff --git a/src/plugins/start/developer-kit/definitions/AssetBrowserSetting.tsx b/src/plugins/start/developer-kit/definitions/AssetBrowserSetting.tsx index 6df2067..c9b9a7b 100644 --- a/src/plugins/start/developer-kit/definitions/AssetBrowserSetting.tsx +++ b/src/plugins/start/developer-kit/definitions/AssetBrowserSetting.tsx @@ -1,5 +1,6 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { RouteNames, Setting } from '../constants' +import defer * as AssetBrowserSettingScreen from '../screens/AssetBrowserSettingScreen' import type { SettingsItem } from '@revenge-mod/discord/modules/settings' const AssetBrowserSetting: SettingsItem = { @@ -9,8 +10,7 @@ const AssetBrowserSetting: SettingsItem = { title: () => 'Asset Browser', screen: { route: RouteNames[Setting.AssetBrowser], - getComponent: () => - require('../screens/AssetBrowserSettingScreen').default, + getComponent: () => AssetBrowserSettingScreen.default, }, } diff --git a/src/plugins/start/developer-kit/definitions/RevengeDeveloperSetting.tsx b/src/plugins/start/developer-kit/definitions/RevengeDeveloperSetting.tsx index f116922..a9b3ec6 100644 --- a/src/plugins/start/developer-kit/definitions/RevengeDeveloperSetting.tsx +++ b/src/plugins/start/developer-kit/definitions/RevengeDeveloperSetting.tsx @@ -1,5 +1,6 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { RouteNames, Setting } from '../constants' +import defer * as RevengeDeveloperSettingScreen from '../screens/RevengeDeveloperSettingScreen' import type { SettingsItem } from '@revenge-mod/discord/modules/settings' const RevengeDeveloperSetting: SettingsItem = { @@ -9,8 +10,7 @@ const RevengeDeveloperSetting: SettingsItem = { title: () => 'Developer', screen: { route: RouteNames[Setting.RevengeDeveloper], - getComponent: () => - require('../screens/RevengeDeveloperSettingScreen').default, + getComponent: () => RevengeDeveloperSettingScreen.default, }, } diff --git a/src/plugins/start/developer-kit/definitions/TestErrorBoundarySetting.tsx b/src/plugins/start/developer-kit/definitions/TestErrorBoundarySetting.tsx index 267fc78..aeb57e9 100644 --- a/src/plugins/start/developer-kit/definitions/TestErrorBoundarySetting.tsx +++ b/src/plugins/start/developer-kit/definitions/TestErrorBoundarySetting.tsx @@ -1,5 +1,6 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { RouteNames, Setting } from '../constants' +import defer * as TestErrorBoundarySettingScreen from '../screens/TestErrorBoundarySettingScreen' import type { SettingsItem } from '@revenge-mod/discord/modules/settings' const TestErrorBoundarySetting: SettingsItem = { @@ -12,8 +13,7 @@ const TestErrorBoundarySetting: SettingsItem = { title: () => 'Test ErrorBoundary', screen: { route: RouteNames[Setting.TestErrorBoundary], - getComponent: () => - require('../screens/TestErrorBoundarySettingScreen').default, + getComponent: () => TestErrorBoundarySettingScreen.default, }, } diff --git a/src/plugins/start/settings/definitions/RevengePluginsSetting.tsx b/src/plugins/start/settings/definitions/RevengePluginsSetting.tsx index 489a56f..bb31754 100644 --- a/src/plugins/start/settings/definitions/RevengePluginsSetting.tsx +++ b/src/plugins/start/settings/definitions/RevengePluginsSetting.tsx @@ -7,6 +7,7 @@ import { PluginFlags } from '@revenge-mod/plugins/constants' import { useReRender } from '@revenge-mod/utils/react' import { useEffect } from 'react' import { RouteNames, Setting } from '../constants' +import defer * as RevengePluginsSettingScreen from '../screens/RevengePluginsSettingScreen' import type { SettingsItem } from '@revenge-mod/discord/modules/settings' import type { AnyPlugin } from '@revenge-mod/plugins/_' @@ -18,8 +19,7 @@ const RevengePluginsSetting: SettingsItem = { useTrailing: () => `${useEnabledPluginCount()} enabled`, screen: { route: RouteNames[Setting.RevengePlugins], - getComponent: () => - require('../screens/RevengePluginsSettingScreen').default, + getComponent: () => RevengePluginsSettingScreen.default, }, } diff --git a/src/plugins/start/settings/definitions/RevengeSetting.tsx b/src/plugins/start/settings/definitions/RevengeSetting.tsx index ae6277c..7e7017d 100644 --- a/src/plugins/start/settings/definitions/RevengeSetting.tsx +++ b/src/plugins/start/settings/definitions/RevengeSetting.tsx @@ -2,6 +2,7 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { FullVersion } from '~/constants' import RevengeIcon from '~assets/RevengeIcon' import { RouteNames, Setting } from '../constants' +import defer * as RevengeSettingScreen from '../screens/RevengeSettingScreen' import type { SettingsItem } from '@revenge-mod/discord/modules/settings' const RevengeSetting: SettingsItem = { @@ -12,7 +13,7 @@ const RevengeSetting: SettingsItem = { useTrailing: () => FullVersion, screen: { route: RouteNames[Setting.Revenge], - getComponent: () => require('../screens/RevengeSettingScreen').default, + getComponent: () => RevengeSettingScreen.default, }, } diff --git a/src/plugins/start/settings/register-routes.tsx b/src/plugins/start/settings/register-routes.tsx index edef8a3..c150e2c 100644 --- a/src/plugins/start/settings/register-routes.tsx +++ b/src/plugins/start/settings/register-routes.tsx @@ -6,19 +6,16 @@ import { import { pEmitter } from '@revenge-mod/plugins/_' import { proxify } from '@revenge-mod/utils/proxy' import { useLayoutEffect } from 'react' +import defer * as NavigatorHeaderWithIcon_ from './components/NavigatorHeaderWithIcon' import { Setting } from './constants' import type { StackScreenProps } from '@react-navigation/stack' import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation' import type { PluginApi } from '@revenge-mod/plugins/types' -type NavigatorHeaderWithIcon = - typeof import('./components/NavigatorHeaderWithIcon')['default'] +type NavigatorHeaderWithIcon = (typeof NavigatorHeaderWithIcon_)['default'] -let NavigatorHeaderWithIcon = proxify( - (): NavigatorHeaderWithIcon => - (NavigatorHeaderWithIcon = - require('./components/NavigatorHeaderWithIcon').default), - {}, +let NavigatorHeaderWithIcon: NavigatorHeaderWithIcon = proxify( + () => (NavigatorHeaderWithIcon = NavigatorHeaderWithIcon_.default), ) pEmitter.on('started', plugin => {