chore: use import defer

This commit is contained in:
PalmDevs 2025-09-10 00:48:42 +07:00
parent 622df65cbf
commit 5956d19125
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
12 changed files with 181 additions and 35 deletions

View file

@ -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: [

View file

@ -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}')`
}

View file

@ -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
},
})
},

View file

@ -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
},
},
)

View file

@ -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
},
},
)

View file

@ -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,

View file

@ -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,
},
}

View file

@ -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,
},
}

View file

@ -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,
},
}

View file

@ -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,
},
}

View file

@ -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,
},
}

View file

@ -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 => {