revenge-bundle-next/scripts/build.ts

273 lines
9.2 KiB
TypeScript
Raw Normal View History

2025-03-21 19:59:16 +07:00
import { transform } from '@swc/core'
import { $, main } from 'bun'
2025-03-21 19:59:16 +07:00
import chalk from 'chalk'
import { exists, mkdir, readdir, rm, writeFile } from 'fs/promises'
2025-06-03 16:24:58 +07:00
import { parse } from 'path'
import { rolldown } from 'rolldown'
import { aliasPlugin, importGlobPlugin } from 'rolldown/experimental'
import pkg from '../package.json'
2025-06-03 16:24:58 +07:00
import type { OutputChunk, RolldownPlugin } from 'rolldown'
2025-06-01 13:50:21 +07:00
const ShimsDir = `${import.meta.dir}/../shims`
const AssetsDir = `${import.meta.dir}/../src/assets`
const GeneratedAssetsDir = `${import.meta.dir}/../dist/assets/generated`
await rm(GeneratedAssetsDir, { recursive: true, force: true })
.then(() =>
console.debug(chalk.gray('\u{1F5BB} Deleted old generated assets')),
)
.catch()
const Dev =
process.argv.includes('--dev') || process.env.NODE_ENV === 'development'
// If this file is being run directly, build the project
if (main === import.meta.filename) build()
export default async function build(dev = Dev, log = true) {
2025-03-21 19:59:16 +07:00
const start = performance.now()
2025-05-07 02:53:31 +07:00
if (log) console.info(chalk.gray('\u{1F5BB} Generating assets...'))
2025-05-07 01:21:27 +07:00
await generateAssets()
2025-05-07 02:53:31 +07:00
if (log) console.info(chalk.cyanBright('\u{1F5BB} Assets generated'))
if (log) console.info(chalk.gray('\u{1F5CE} Compiling JS...'))
2025-05-07 01:21:27 +07:00
2025-03-21 19:59:16 +07:00
const bundle = await rolldown({
input: 'src/index.ts',
platform: 'neutral',
experimental: {
strictExecutionOrder: true,
},
resolve: {
tsconfigFilename: 'tsconfig.json',
},
2025-03-21 19:59:16 +07:00
treeshake: true,
keepNames: true,
moduleTypes: {
'.webp': 'dataurl',
},
define: {
__BUILD_VERSION__: JSON.stringify(pkg.version),
2025-06-03 16:24:58 +07:00
__BUILD_COMMIT__: JSON.stringify(
(await $`git rev-parse HEAD`.text()).trim().substring(0, 7),
),
__BUILD_BRANCH__: JSON.stringify(
(await $`git rev-parse --abbrev-ref HEAD`.text()).trim(),
),
2025-03-21 19:59:16 +07:00
__BUILD_ENV__: JSON.stringify(dev ? 'development' : 'production'),
2025-06-12 23:25:10 +07:00
// See types.d.ts for what these flags do
__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__: String(dev),
__BUILD_FLAG_DEBUG_PROXIFIED_VALUES__: 'false',
2025-05-05 19:13:05 +07:00
__BUILD_FLAG_LOG_PROMISE_REJECTIONS__: String(dev),
2025-03-21 19:59:16 +07:00
},
plugins: [
aliasPlugin({
entries: [
{
find: 'react/jsx-runtime',
2025-06-01 13:50:21 +07:00
replacement: `${ShimsDir}/react~jsx-runtime.ts`,
},
// Do not move React to the top!
// If you do that, react/jsx-runtime would resolve to ${ShimsDir}/react.ts/jsx-runtime instead.
{
find: 'react',
replacement: `${ShimsDir}/react.ts`,
},
{
find: 'react-native',
replacement: `${ShimsDir}/react-native.ts`,
},
],
}),
importGlobPlugin(),
swcPlugin(),
2025-03-21 19:59:16 +07:00
hermesCPlugin({
flags: [
dev ? '-Og' : '-O',
'-eager',
'-finline',
'-fno-static-require',
'-Wno-direct-eval',
'-Wno-undefined-variable',
],
2025-05-07 02:53:31 +07:00
before() {
2025-06-03 16:24:58 +07:00
if (log)
console.debug(
chalk.cyanBright(
'\u{1F5CE} JS compilation finished...',
),
)
if (log)
console.debug(
chalk.gray('\u{1F5CE} Compiling bytecode...'),
)
2025-05-07 02:53:31 +07:00
},
after() {
2025-06-03 16:24:58 +07:00
if (log)
console.debug(
chalk.cyanBright(
'\u{1F5CE} Bytecode compilation finished',
),
)
2025-03-21 19:59:16 +07:00
},
}),
],
})
await bundle.write({
minify: {
2025-06-12 23:25:10 +07:00
compress: false,
mangle: true,
},
2025-03-21 19:59:16 +07:00
file: 'dist/revenge.js',
format: 'iife',
})
if (log)
console.info(
chalk.greenBright(
2025-05-07 02:53:31 +07:00
`\u{2714} Compiled successfully! ${chalk.gray(`(took ${(performance.now() - start).toFixed(2)}ms)`)}`,
2025-03-21 19:59:16 +07:00
),
)
}
function swcPlugin() {
return {
name: 'swc',
transform: {
filter: {
id: /\.[cm]?[jt]sx?$/,
},
handler(code) {
return transform(code, {
jsc: {
transform: {
react: {
runtime: 'automatic',
},
},
parser: {
syntax: 'typescript',
tsx: true,
},
},
env: {
2025-06-03 16:24:58 +07:00
// https://github.com/facebook/hermes/blob/main/doc/Features.md
2025-03-21 19:59:16 +07:00
targets: 'fully supports es6',
include: [
'transform-async-generator-functions',
2025-03-21 19:59:16 +07:00
'transform-block-scoping',
'transform-classes',
'transform-duplicate-named-capturing-groups-regex',
2025-03-21 19:59:16 +07:00
'transform-named-capturing-groups-regex',
],
exclude: [
// Async functions are supported, only async arrow functions aren't
// Source: https://github.com/facebook/hermes/issues/1395
'transform-async-to-generator',
2025-03-21 19:59:16 +07:00
'transform-exponentiation-operator',
'transform-logical-assignment-operators',
2025-03-21 19:59:16 +07:00
'transform-nullish-coalescing-operator',
'transform-numeric-separator',
2025-03-21 19:59:16 +07:00
'transform-object-rest-spread',
'transform-optional-catch-binding',
2025-03-21 19:59:16 +07:00
'transform-optional-chaining',
'transform-parameters',
'transform-template-literals',
2025-03-21 19:59:16 +07:00
],
},
})
},
},
} satisfies RolldownPlugin
}
2025-06-03 16:24:58 +07:00
function hermesCPlugin({
after,
before,
flags,
}: {
flags?: string[]
before?: () => void
after?: () => void
} = {}) {
2025-03-21 19:59:16 +07:00
const paths = {
win32: 'hermesc.exe',
darwin: 'hermesc',
linux: 'hermesc',
2025-03-21 19:59:16 +07:00
}
2025-06-03 16:24:58 +07:00
if (!(process.platform in paths))
throw new Error(`Unsupported platform: ${process.platform}`)
2025-03-21 19:59:16 +07:00
const binPath = paths[process.platform as keyof typeof paths]
return {
name: 'hermesc',
generateBundle(_, bundle) {
2025-05-07 02:53:31 +07:00
if (before) before()
2025-03-21 19:59:16 +07:00
const file = bundle['revenge.js'] as OutputChunk
if (!file || !file.code) throw new Error('No code to compile')
// TODO(scripts/build): Remove this when we have a better way to add sourceURL
file.code += `//# sourceURL=Revenge`
const cmdlist = [
`./node_modules/@unbound-mod/hermesc/${process.platform}/${binPath}`,
'-emit-binary',
...(flags ?? []),
]
const cmd = Bun.spawnSync(cmdlist, {
2025-06-01 13:50:21 +07:00
// @ts-expect-error: Types are incorrect, but this works
stdin: new Blob([file.code]),
stdout: 'pipe',
})
2025-03-21 19:59:16 +07:00
const buf = cmd.stdout
if (!buf.length)
throw new Error(
`No output from hermesc. Probably a compilation error.\nTry running the command manually: ${cmdlist.join(' ')}`,
)
2025-03-21 19:59:16 +07:00
this.emitFile({
type: 'asset',
fileName: `${file.fileName.split('.')[0]!}.bundle`,
source: buf,
})
2025-05-07 02:53:31 +07:00
if (after) after()
2025-03-21 19:59:16 +07:00
},
} satisfies RolldownPlugin
}
2025-05-07 01:21:27 +07:00
async function generateAssets() {
2025-06-03 16:24:58 +07:00
if (!(await exists(GeneratedAssetsDir)))
await mkdir(GeneratedAssetsDir, { recursive: true })
2025-05-07 01:21:27 +07:00
const promises: Promise<void>[] = []
2025-05-07 01:21:27 +07:00
for (const file of await readdir(AssetsDir)) {
const { name, ext } = parse(file)
const path = `${AssetsDir}/${file}`
const path2 = `${GeneratedAssetsDir}/${name}.js`
if (await exists(path2)) continue
// We attempt to sanitize the path, but not the name as it should fail if name contains invalid characters
const uriPath = JSON.stringify(path)
const type = JSON.stringify(ext.slice(1))
promises.push(
writeFile(
path2,
`import{registerAsset}from'@revenge-mod/assets';import uri from${uriPath};const ${name}=registerAsset({name:'${name}',type:${type},uri});export default ${name}`,
),
2025-05-07 01:21:27 +07:00
)
}
await Promise.all(promises)
2025-05-07 01:21:27 +07:00
}