diff --git a/scripts/_shared.ts b/scripts/_shared.ts new file mode 100644 index 0000000..5e74a04 --- /dev/null +++ b/scripts/_shared.ts @@ -0,0 +1,10 @@ +import { stat } from 'fs/promises' + +export const exists = async (path: string) => { + try { + await stat(path) + return true + } catch { + return false + } +} diff --git a/scripts/build.ts b/scripts/build.ts index 98bd6ba..a8d6a09 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,19 +1,23 @@ -import { $, main } from 'bun' import chalk from 'chalk' -import { exists, mkdir, readdir, rm, writeFile } from 'fs/promises' -import { parse } from 'path' +import { execSync } from 'child_process' +import { mkdir, readdir, rm, writeFile } from 'fs/promises' +import { dirname, parse } from 'path' import { rolldown } from 'rolldown' import { importGlobPlugin } from 'rolldown/experimental' -import pkg from '../package.json' +import { fileURLToPath } from 'url' +import pkg from '../package.json' with { type: 'json' } +import { exists } from './_shared' 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` -const AssetsDir = `${import.meta.dir}/../src/assets` -const GeneratedAssetsDir = `${import.meta.dir}/../dist/assets/generated` +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const ShimsDir = `${__dirname}/../shims` +const AssetsDir = `${__dirname}/../src/assets` +const GeneratedAssetsDir = `${__dirname}/../dist/assets/generated` await rm(GeneratedAssetsDir, { recursive: true, force: true }) .then(() => @@ -25,7 +29,7 @@ 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() +if (import.meta.main) build() export default async function build(dev = Dev, log = true) { const start = performance.now() @@ -35,7 +39,10 @@ export default async function build(dev = Dev, log = true) { if (log) console.info(chalk.cyanBright('\u{1F5BB} Assets generated')) if (log) console.info(chalk.gray('\u{1F5CE} Compiling JS...')) - const COMMIT = (await $`git rev-parse HEAD`.text()).trim().substring(0, 7) + const COMMIT = execSync('git rev-parse HEAD') + .toString() + .trim() + .substring(0, 7) const REPO = 'revenge-mod/revenge-bundle-next' const bundle = await rolldown({ @@ -67,7 +74,9 @@ export default async function build(dev = Dev, log = true) { __BUILD_VERSION__: JSON.stringify(pkg.version), __BUILD_COMMIT__: JSON.stringify(COMMIT), __BUILD_BRANCH__: JSON.stringify( - (await $`git rev-parse --abbrev-ref HEAD`.text()).trim(), + execSync('git rev-parse --abbrev-ref HEAD') + .toString() + .trim(), ), __DEV__: String(dev), @@ -79,6 +88,11 @@ export default async function build(dev = Dev, log = true) { }, }, tsconfig: 'tsconfig.json', + // propertyReadSideEffects: false works around a Rolldown bug where + // property reads on tree-shaken import bindings are kept as "side + // effects" while their imports are removed, producing free-variable + // references (e.g. `FibonacciHeap.MinFibonacciHeap` from mnemonist's + // barrel) that throw ReferenceError at runtime. treeshake: true, moduleTypes: { '.webp': 'dataurl', diff --git a/scripts/dev.ts b/scripts/dev.ts index 3e7ad1e..29379b0 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -1,14 +1,20 @@ import watcher from '@parcel/watcher' import { debounce } from '@revenge-mod/utils/callback' import chalk from 'chalk' +import { readFile } from 'fs/promises' +import { createServer } from 'http' import os from 'os' import { join } from 'path' -import pkg from '../package.json' +import { crc32 } from 'zlib' +import pkg from '../package.json' with { type: 'json' } import build from './build' const prod = process.argv.includes('--prod') const lanHost = process.argv.includes('--lan') +const Port = 4040 +const BundlePath = './dist/revenge.bundle' + console.info(chalk.redBright(`\nRevenge ${chalk.white(`v${pkg.version}`)}\n`)) const cwdify = (path: string) => join(process.cwd(), path) @@ -43,51 +49,46 @@ watcher.subscribe(process.cwd(), (err, events) => { ) }) -const server = Bun.serve({ - hostname: lanHost ? '0.0.0.0' : '127.0.0.1', - async fetch(req, srv) { - try { - if (needRebuild) await debouncedBuild() +const server = createServer(async (req, res) => { + try { + if (needRebuild) await debouncedBuild() + console.debug( + chalk.gray( + `\u{1F79B} Receiving request from ${req.socket.remoteAddress}`, + ), + ) + + const bundle = await readFile(BundlePath).catch(() => null) + if (!bundle) + throw new Error('Could not serve the bundle! No file found.') + + const hash = crc32(bundle).toString(16) + + if (req.headers['if-none-match'] === hash) { console.debug( - chalk.gray( - `\u{1F79B} Receiving request from ${srv.requestIP(req)!.address}`, - ), + chalk.gray('\u{1F4BE} ETag matched, responding with 304'), ) - const file = Bun.file('./dist/revenge.bundle') - - if (await file.exists()) { - const hash = Bun.hash - .crc32(await file.arrayBuffer()) - .toString(16) - - if (req.headers.get('If-None-Match') === hash) { - console.debug( - chalk.gray( - '\u{1F4BE} ETag matched, responding with 304', - ), - ) - - return new Response(null, { - status: 304, - }) - } - - return new Response(file, { - status: 200, - headers: { - ETag: hash, - }, - }) - } else throw new Error('Could not serve the bundle! No file found.') - } catch (e) { - console.error(e) - throw new Error('Build failed. Check console for details.') + res.writeHead(304) + res.end() + return } - }, - port: 4040, + + res.writeHead(200, { + ETag: hash, + 'Content-Length': bundle.byteLength, + }) + res.end(bundle) + } catch (e) { + console.error(e) + + res.writeHead(500) + res.end('Build failed. Check console for details.') + } }) +server.listen(Port, lanHost ? '0.0.0.0' : '127.0.0.1') + if (lanHost) console.info(chalk.gray('\u24D8 Listening on all interfaces...')) else console.info( @@ -99,12 +100,12 @@ else if (!prod) console.info(chalk.gray('\u24D8 Use --prod to build for production')) else console.info(chalk.gray('\u24D8 Building for production...')) -console.info(chalk.cyanBright(`\u24D8 Serving on port ${server.port}`)) +console.info(chalk.cyanBright(`\u24D8 Serving on port ${Port}`)) console.info(chalk.gray('\u24D8 Accessible on:')) for (const int of Object.values(os.networkInterfaces())) if (int) for (const det of int) { if (det.family !== 'IPv4' || (!det.internal && !lanHost)) continue - console.info(chalk.gray(`- http://${det.address}:${server.port}`)) + console.info(chalk.gray(`- http://${det.address}:${Port}`)) } diff --git a/scripts/plugins/as-require.ts b/scripts/plugins/as-require.ts index 8369a9c..893192a 100644 --- a/scripts/plugins/as-require.ts +++ b/scripts/plugins/as-require.ts @@ -8,8 +8,10 @@ import type { RolldownPlugin } from 'rolldown' * - Dynamic side-effect imports: `import('module')` and `await import('module')` */ export default function asRequire() { + const name = 'as-require' + return { - name: 'as-require', + name, transform(code, id) { if (!code.includes('// @as-require')) return null @@ -45,7 +47,7 @@ export default function asRequire() { } this.warn( - `[${this.pluginName}] Warning: Could not transform statement in ${id}:\n${statement}\n` + + `[${name}] Warning: Could not transform statement in ${id}:\n${statement}\n` + `The line after the comment must be a side-effect import or a dynamic import with .then().`, ) diff --git a/scripts/plugins/hermesc.ts b/scripts/plugins/hermesc.ts index f9911bc..a86b6ae 100644 --- a/scripts/plugins/hermesc.ts +++ b/scripts/plugins/hermesc.ts @@ -1,3 +1,4 @@ +import { spawnSync } from 'child_process' import { existsSync, readFileSync } from 'fs' import type { OutputChunk, RolldownPlugin } from 'rolldown' @@ -37,27 +38,40 @@ export default function hermesCPlugin({ const file = bundle['revenge.js'] as OutputChunk if (!file) throw new Error('No code to compile') - const cmdlist = [binPath, '-emit-binary', ...(flags ?? [])] + const argList = ['-emit-binary', ...(flags ?? [])] - const cmd = Bun.spawnSync<'pipe', 'pipe'>(cmdlist, { - // @ts-expect-error: Types are incorrect, but this works - stdin: new Blob([file.code]), - stdout: 'pipe', - }) + const cmd = + // TODO: Move away from Bun: https://github.com/oven-sh/bun/issues/25498 + typeof Bun !== 'undefined' + ? (() => { + const cmd = Bun.spawnSync([binPath, ...argList], { + // @ts-expect-error: Works but types are incorrect + stdio: [new Blob([file.code]), 'pipe'], + }) - if (cmd.exitCode) { + return { + status: cmd.exitCode, + stdout: cmd.stdout, + stderr: cmd.stderr, + } + })() + : spawnSync(binPath, argList, { + stdio: ['pipe', 'pipe'], + input: file.code, + }) + + if (cmd.status !== 0) { if (cmd.stderr.length) throw new Error( `Got error from hermesc: ${cmd.stderr.toString()}`, ) - else - throw new Error(`hermesc exited with code: ${cmd.exitCode}`) + else throw new Error(`hermesc exited with code: ${cmd.status}`) } 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(' ')}`, + `No output from hermesc. Probably a compilation error.\nTry running the command manually: ${binPath} ${argList.join(' ')}`, ) this.emitFile({