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 disallowed blob:null/* and data:* origins
    • ❌ Using eval on a script bundled via the commonjs or esm formats will throw errors
    • ⚠️ Using eval on a script bundled via the deprecated UMD or IIFE formats, which set their exports on the globalThis object may work
  • ⚠️ 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 and Worker, 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:

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 with base64, 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:

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.