Bundling
Bundling edge scripts for bunny.net 🐰
Introduction
This guide will provide you with the information you need to bundle your code for the edge scripting runtime.
Rather than providing packages, this guide will give you configuration tips and examples, leaving you free to choose and configure your own bundler however you see fit.
All configuration examples use esbuild
, which is recommended if you do not have a preferred bundler.
Limitations
To fully understand bundling for edge scripts, it is important to understand the limitations of edge scripts.
Script files
Only a single, bundled script file may be uploaded. That script does not have access to the file system.
As a result, you should bundle your script with all of its dependencies from node_modules
together. With esbuild, these settings include:
{ bundle: true, packages: 'bundle',}
Be sure to exclude built-in modules:
import { builtinModules } from "node:module";const allBuiltinModules = [ ...builtinModules, ...builtinModules.map((builtinModule) => `node:${builtinModule}`),];{ external: allBuiltinModules,}
The esm
format is preferred for your final bundle:
{ format: 'esm',}
Script size
The script size is limited to 1MB
. While Bunny hopes to increase this limit in the future, they will have to re-design and re-implement parts of the edge script infrastructure after they figure out the best path forward. Currently, there is no ETA for increasing this limit.
As a result, you should always minify your script when possible. With esbuild, these settings include:
{ keepNames: !MINIFY, minify: MINIFY, sourcemap: !MINIFY,}
If your script is larger than 1MB
, you can externalize chunks and load them via your preferred CDN.
Loading additional scripts
Bunny allows loading scripts from:
- ✅ Popular CDNs
- ⚠️ A string
- ❌ Using
await import()
on a string, blob, or object url will fail with errors about disallowedblob:null/*
anddata:*
origins - ❌ Using eval on a script bundled via the
commonjs
oresm
formats will throw errors - ⚠️ Using eval on a script bundled via the deprecated
UMD
orIIFE
formats, which set their exports on theglobalThis
object may work
- ❌ Using
- ⚠️ A private storage zone
Loading a script or asset from a Pull Zone will charge your account bandwidth. However, the only way to load private scripts and assets is with the help of Pull Zone edge rules (e.g. blocking requests missing an api-key=abc123
url query parameter). Be sure to select the Edge Tier SSD for the Storage Zone and the Standard Network for the Pull Zone for optimal performance.
These URL imports are not cached and add to your cold start time.
In addition, the maximum script startup time is 500ms
, so you should load other modules inside your script handler and cache them in variables outside the handler scope. For example:
let resvg: Awaited<ReturnType<typeof initResvg>>;BunnySDK.net.http.serve(async (request: Request): Promise<Response> => { if (!resvg) { resvg = await initResvg() } // continue scripting using the lazy loaded module}
Deno runtime
Edge scripts run on a modified Deno runtime.
Runtime version
The Deno version used by edge scripts is currently 1.x
(1.46.3
at the time of writing). You can check the runtime version by logging the Deno.version
object.
It is not possible to select the Deno version and updates occur without notice. It is nice that the runtime is continually updated.
Global objects
Deno 1.x
has several limitations and unexpected behaviors:
- No global Node.js objects (e.g.
process
,Buffer
,global
, etc.) - A global
window
object
While Deno 2.x
changes which globals are available among other updates, you will likely still run into bundling issues with complex packages due to the lack of a file system and broken relative url imports.
To add the common Node.js globals, which many third-party packages depend on, you can use a bundler to add a banner line of code to the top of your compiled script:
{ banner: { js: 'import * as process from "node:process";import { Buffer } from "node:buffer";globalThis.process ??= process;globalThis.Buffer ??= Buffer;globalThis.global ??= globalThis;', }}
While this banner is often required to make other third-party packages function, you can assume that all packages with environmental detection are thoroughly broken with the presence of both the window
and process
objects combined with the lack of a file system and broken relative url imports.
Permissions
Please be aware that many Deno permissions are disabled in the edge scripting runtime. Bunny has not clarified what permissions are enabled, and the Deno.Permissions
functions to find out are disabled.
Built-in modules
Deno requires protocol imports
Please note that all built-in modules in Deno must be loaded with the node:*
protocol import syntax.
This limitation means that your bundler must transform all require and import statements for built-in modules to use the syntax with the node:
prefix. To help, feel free to use these bundler plugins:
If you write your own plugin, please let us know and we will add it to the list!
Supported modules
Edge scripts support a subset of built-in Node.js modules:
- assert
- assert/strict
- async_hooks
- buffer
- console
- crypto
- diagnostics_channel
- dns
- dns/promises
- domain
- events
- module
- net
- os
- path
- path/posix
- perf_hooks
- process
- punycode
- querystring
- readline
- readline/promises
- stream
- stream/consumers
- stream/promises
- stream/web
- string_decoder
- timers
- timers/promises
- url
- util
- util/types
- zlib
Despite being supported, many runtime functions throw errors:
- Common functions like
process.cwd()
will throw errors - Some modules have stubs, and will throw errors when you try to use them (e.g. more than half of the
os
methods) - Though Deno supports browser objects, some, like
navigator.languages
andWorker
, will throw errors
Unfortunately, many scripts that try to only use functions if they exist in the environment will still fail, because the function stubs do exist in the edge scripting runtime and will throw an error when called.
The bundler can help patch some functions using define
and inject
; however, it only works on global objects, not imported modules.
process-cwd-shim.js
let processCwdShim = () => ''export { processCwdShim as 'process.cwd' }
{ inject: './process-cwd-shim.js'}
There is no equivalant to Deno’s compatibility page or the runtime-compat website. There is no documentation on which functions are supported or stubbed. A function that is supported may change to a stub without any warning.
Unsupported modules
Any modules not in the list above are unsupported:
- path/win32
- tty
- fs
- repl
- worker_threads
- v8
- http
- trace_events
- dgram
- wasi
- cluster
- vm
- constants
- https
- child_process
- http2
- inspector/promises
- sys
- tls
- inspector
- fs/promises
Your script must not import any of these modules.
Unfortunately, these unsupported built-in modules mean that popular server frameworks like Express.js, Nest.js, etc. are not compatible with edge scripts.
Environment bundling
To deal with unsupported modules and broken environment detection, you can leverage your bundler.
Platform
Many bundlers allow you to set the platform (e.g. browser
or node
). For example:
{ platform: 'browser',}
The browser
platform is almost always compatible with edge scripts, though it may have worse performance compared to the node
platform.
Export conditions
If the platform is not specific enough, you can specify package.json
export conditions (e.g. deno
, worker
, browser
, node
, import
, production
, etc.). Common export conditions can be found in the Webpack, Node.js, and WinterCG documentation.
To understand the order in which export conditions are resolved and how to write them, please visit the Node.js documentation and read these examples.
For example:
{ conditions: ['browser', 'worker', 'import', 'default'],}
Aliases
Some package entrypoints need to be aliased. If the package:
- Does not provide an exports field for all exports
- Does not have a compatible default export for the edge scripting runtime
- Has an undocumented export for the edge scripting runtime
- Needs to be overridden
For example, to bundle the @bunny.net/edgescript-sdk
package properly with your script for production, you will need to set an alias for the undocumented entrypoint:
{ alias: { '@bunny.net/edgescript-sdk': './node_modules/@bunny.net/edgescript-sdk/esm-bunny/lib.mjs', },}
However, you will need to continue to use the default exports from @bunny.net/edgescript-sdk
to run your code locally.
If you are using a monorepo, you may need to use require.resolve
or import.meta.resolve
to find the correct node_modules
path. Please note that the alias path must always be a relative path. For example:
{ alias: { '@bunny.net/edgescript-sdk': `./${path.relative( process.cwd(), path.fileURLToPath( import.meta.resolve('@bunny.net/edgescript-sdk/esm-bunny/lib.mjs', import.meta.url) ) )}`, '@bunny.net/edgescript-sdk': `./${path.relative( process.cwd(), require.resolve('@bunny.net/edgescript-sdk/esm-bunny/lib.mjs', { paths: [process.cwd()], }), )}`, },}
Production
Ensure your bundler is bundling for production:
{ define: { 'process.env.NODE_ENV': '"production"' },}
Otherwise, your script may include development dependencies and be unable to tree-shake development code. You only have 1MB
—every line matters!
Unreliable bundling
Always bundle your script yourself. Do not depend on the edge scripting runtime or a cdn to bundle it.
Otherwise, your script may error when the same script would run if you bundled it yourself.
The module resolution of Deno is different than the edge scripting runtime, so even if your url imports work in development locally, there is no guarentee they will work in production.
There are esbuild plugins to handle Deno url imports, but they are optional.
C++ addons
It is not feasible to use C++ addons without a file system or the ability to pick a stable Deno version to compile against.
WASM support
Wasm in edge scripting is supported thanks to the Deno runtime.
Bundling
Unfortunately, most wasm packages will not work with Deno. Common issues include:
- Broken environment detection
- Inability to load WASM files
- Bundled WASM files larger than the script size limit
File size workarounds
There are two main workarounds for bundling external files like WASM, embedded databases, and other assets with edge scripts:
- Compressing the asset with
gzip
, encoding the asset withbase64
, and inlining the asset in the bundle - Compressing the asset with
gzip
, publishing the asset to a URL, and fetching the asset from a known CDN URL (free of charge) or Pull Zone URL (charges bandwidth)
Compressing the asset with gzip, downloading it, and decompressing it is faster than directly loading the uncompressed file due to slow network speeds.
Unfortunately, these workarounds often require patches or forks from existing packages to work.
Be aware that loading assets from a CDN will add to your cold start time.
To add the asset to the bundle, consider using loaders with your bundler:
{ loader: { '.gz': 'base64', },}
With loaders, you can directly import the files into your code:
import exampleBase64GzipWasm from './example.wasm.gz'
Decode and uncompress your files:
async function decodeBase64AndGunzip( base64String: string,): Promise<Uint8Array> { return await new Response( new Response( Uint8Array.from( globalThis.atob(base64String.replaceAll('-', '+').replaceAll('_', '/')), (x) => x.codePointAt(0), ), ).body.pipeThrough(new DecompressionStream('gzip')), { headers: { 'Content-Type': 'application/wasm', }, }, ).bytes()}
To load from a CDN, fetch from the url:
async function fetchAndGunzip(url: string): Promise<Uint8Array> { return await new Response( (await fetch(url)).body.pipeThrough(new DecompressionStream('gzip')), { headers: { 'Content-Type': 'application/wasm', }, }, ).bytes()}
The fastest way to initialize wasm modules is with .instantiateStreaming()
, which is preferred over .instantiate()
.
The Content-Type
header of the response matters if you pass the response directly to WASM initializers, without calling .bytes()
or .arrayBuffer()
.
WASM with Workers
Unfortunately, web workers are not supported. While Bunny is looking into the issue, there is no ETA for support. That means packages like duckdb
or vips
cannot run.
Patching packages
Patching third-party packages is an integral part to ensuring your script can run, especially when environmental detection is broken.
For npm, the recommended package patcher is patch-package
. Other package managers like pnpm, have a command to patch packages.
We have provided pre-bundled and pre-patched packages for use with edge scripts:
- Utilities
- Adapters
- Databases
- Imaging
- Fonts
More packages coming soon… If you have published packages for edge scripts, please let us know and we will add it to the list!
Testing
Running locally
When running scripts locally or running in development mode and watching for file changes, using Node.js is fine.
If you want to run your script in both Deno and Node.js before it is bundled, you will need to use the deno.json
file to manage your dependencies, configure tsconfig.json
settings, and configure JSX settings.
Before deploying, always compile and test a production build with Deno with the same version as the edge scripting runtime.
If a script does not work locally with Deno, it will not work in production. Try to use as few permissions as possible.
Unfortunately, there is no way to know for sure whether a script is compatible with the edge scripting runtime, which is not open source. There is no way to test compatibility without a live deployment.
Pre-deploy checks
Helpful checks you can run after you compile your script include:
- Ensuring your script does not import any unsupported modules
- Ensuring your script is less than
1MB
in size
Conclusion
Thank you for reading! Hopefully these tips and tricks help you bundle your edge scripts. If you have feedback or find anything that is not in this guide, please leave a message in the discord channel #edge-scripting-beta
.