Deno 2.4: The Bundle is Back
10 min read

Deno 2.4: The Bundle is Back

1931 words

Deno 2.4 has just been released, and I must admit it has pleasantly surprised me. Not only because of the number of new features, but because of one in particular that many of us thought would never return: deno bundle is back. And this time, it’s here to stay.

This release comes packed with improvements ranging from importing text files directly to stable observability with OpenTelemetry. Let’s explore what this release brings us.

The Triumphant Return of deno bundle

For those of us who have been with Deno for a while, deno bundle was one of those features we used constantly until it was deprecated in 2021. The Deno team admitted that bundling is a complex problem and they couldn’t do it well.

What changed? Now deno bundle uses esbuild under the hood, which means we have all the power and maturity of a battle-tested tool.

Basic Usage

# Bundle with minification
deno bundle --minify main.ts

# Specify platform (browser instead of deno)
deno bundle --platform browser --output bundle.js app.jsx

# Generate external sourcemap
deno bundle --platform browser --output bundle.js --sourcemap=external app.jsx

What I Like Most

  1. Works with npm and JSR - No additional configuration needed
  2. Automatic tree shaking - Only includes what you need
  3. Minification included - Via esbuild
  4. Browser and server support - One command, multiple targets

Practical Example

// main.ts
import { serve } from "jsr:@std/http/serve";
import cowsay from "npm:cowsay";

serve((req) => {
  const url = new URL(req.url);
  const message = url.searchParams.get("message") || "Hello from Deno!";

  return new Response(cowsay.say({ text: message }), {
    headers: { "content-type": "text/plain" }
  });
});
# Bundle for browser with npm dependencies
deno bundle --platform browser --minify --output server-bundle.js main.ts

# The result is a single file with everything included
ls -la server-bundle.js
# -rw-r--r-- 1 user user 45K server-bundle.js

Direct Text and Bytes Import

This is a feature I’ve been waiting for. Now you can import text files, binaries, JSON, and WASM directly into your JavaScript code.

Import Attributes in Action

// Import as text
import readme from "./README.md" with { type: "text" };
import config from "./config.json" with { type: "json" };

// Import as bytes
import imageData from "./logo.png" with { type: "bytes" };
import iconData from "./favicon.ico" with { type: "bytes" };

// Use in a server
Deno.serve((req) => {
  const url = new URL(req.url);

  if (url.pathname === "/readme") {
    return new Response(readme, {
      headers: { "content-type": "text/markdown" }
    });
  }

  if (url.pathname === "/logo") {
    return new Response(imageData, {
      headers: {
        "content-type": "image/png",
        "content-length": imageData.byteLength.toString()
      }
    });
  }

  return new Response("Not found", { status: 404 });
});

Works with Bundle and Compile

The best part is that this works with both deno bundle and deno compile:

# Include assets in bundle
deno bundle --unstable-raw-imports --output app.js main.ts

# Or compile to binary with embedded assets
deno compile --unstable-raw-imports --output app main.ts

Example with Embedded Dictionary

// spellcheck.ts
import dictData from "./dictionary.txt" with { type: "text" };

const WORDS = new Set(dictData.split("\n").map(w => w.trim().toLowerCase()));

function checkWord(word: string): boolean {
  return WORDS.has(word.toLowerCase());
}

// Simple CLI
while (true) {
  const input = prompt("> Enter word to check: ");
  if (!input) continue;

  console.log(
    checkWord(input)
      ? `✅ "${input}" is a valid word`
      : `❌ "${input}" is not in dictionary`
  );
}
# Compile with embedded dictionary
deno compile --unstable-raw-imports spellcheck.ts

# The resulting binary includes the entire dictionary
./spellcheck
> Enter word to check: javascript
"javascript" is a valid word

Stable OpenTelemetry

One of the most powerful features of Deno 2.4 is that OpenTelemetry is now stable. This means automatic observability without configuration.

Simple Activation

# No more --unstable-otel needed
OTEL_DENO=1 deno --allow-net server.ts

What You Get Automatically

  1. Logs associated with HTTP requests
  2. Automatic traces of async operations
  3. Performance metrics
  4. Integration with standard tools (Jaeger, Zipkin, etc.)

Example with Web Server

// server.ts
import { serve } from "jsr:@std/http/serve";

serve(async (req) => {
  console.log(`Processing ${req.method} ${req.url}`);

  // Simulate async work
  await new Promise(resolve => setTimeout(resolve, 100));

  // Operation that might fail
  if (Math.random() > 0.8) {
    console.error("Random error occurred!");
    return new Response("Internal Error", { status: 500 });
  }

  console.log("Request completed successfully");
  return new Response("Hello World!");
});
# Run with observability
OTEL_DENO=1 deno --allow-net server.ts

# Logs are automatically associated with traces
# You can send data to your favorite observability system

Modifying Environment with –preload

The new --preload flag allows you to execute code before your main script. This opens up interesting possibilities for environment setup.

Use Cases

// setup.ts - Modify globals
globalThis.DEBUG = true;
globalThis.API_BASE = "https://api.example.com";

// Custom polyfills
if (!globalThis.fetch) {
  globalThis.fetch = async () => new Response("Mock response");
}

// Remove APIs (for sandboxing)
delete Deno.writeFile;
delete Deno.remove;

console.log("Environment configured!");
// main.ts
console.log("DEBUG mode:", globalThis.DEBUG);
console.log("API Base:", globalThis.API_BASE);

// This will fail if setup.ts was executed
try {
  await Deno.writeFile("test.txt", new TextEncoder().encode("test"));
} catch (error) {
  console.log("Write blocked by preload script");
}
# Run with preload
deno --preload setup.ts main.ts

# Output:
# Environment configured!
# DEBUG mode: true
# API Base: https://api.example.com
# Write blocked by preload script

For Frameworks and Platforms

This is especially useful if you’re building your own platform or framework:

// platform-setup.ts
// Configure database
globalThis.db = await connect("sqlite:///app.db");

// Load configuration
globalThis.config = JSON.parse(await Deno.readTextFile("config.json"));

// Custom APIs
globalThis.sendEmail = async (to: string, subject: string, body: string) => {
  // Email implementation
};

globalThis.logEvent = (event: string, data: any) => {
  // Logging implementation
};

Simplified Dependency Management

deno update is the new command for updating dependencies:

# Update to semver-compatible versions
deno update

# Ignore semver constraints
deno update --latest

# Filter by pattern
deno update "@std/*"

# Include workspaces
deno update --recursive

In Practice

// deno.json before
{
  "imports": {
    "@std/http": "jsr:@std/http@0.220.0",
    "@std/assert": "jsr:@std/assert@0.218.0",
    "express": "npm:express@4.18.0"
  }
}
deno update --latest
// deno.json after
{
  "imports": {
    "@std/http": "jsr:@std/http@0.224.0",
    "@std/assert": "jsr:@std/assert@0.220.0",
    "express": "npm:express@4.19.2"
  }
}

Coverage with deno run

Now you can collect coverage directly from deno run:

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export function multiply(a: number, b: number): number {
  if (a === 0 || b === 0) {
    return 0;
  }
  return a * b;
}
// app.ts
import { add, multiply } from "./math.ts";

const result1 = add(5, 3);
const result2 = multiply(4, 2);

console.log({ result1, result2 });
# Run with coverage
deno run --coverage app.ts

# View report
deno coverage
# | File    | Branch % | Line % |
# | ------- | -------- | ------ |
# | math.ts | 50.0     | 85.7   |
# | app.ts  | 100.0    | 100.0  |

DENO_COMPAT=1: Simplified Compatibility

For existing Node.js projects, you now have an environment variable that enables multiple compatibility flags:

# Before (verbose)
deno --unstable-detect-cjs --unstable-node-globals --unstable-bare-node-builtins --unstable-sloppy-imports app.js

# Now (simple)
DENO_COMPAT=1 deno app.js

What DENO_COMPAT=1 Enables

  • --unstable-detect-cjs - Detect CommonJS modules
  • --unstable-node-globals - Node.js globals available
  • --unstable-bare-node-builtins - Import without node: prefix
  • --unstable-sloppy-imports - Imports without extensions

Example with Node.js Project

// legacy-app.js (existing Node.js code)
const fs = require('fs');
const path = require('path');
const { Buffer } = require('buffer');

// Import without extension (sloppy imports)
const utils = require('./utils');

// Node.js global
process.env.NODE_ENV = 'development';

console.log('App running on Node.js APIs');
# Works directly with DENO_COMPAT=1
DENO_COMPAT=1 deno legacy-app.js

Permission Improvements

Wildcards for Subdomains

# Allow all subdomains of example.com
deno --allow-net=*.example.com server.ts
// This will work
await fetch("https://api.example.com/data");
await fetch("https://cdn.example.com/assets");

// This will ask for permission
await fetch("https://other.com/api");

CIDR Ranges

# Allow local IP range
deno --allow-net=192.168.1.0/24 app.ts

Deny Imports

# Block specific CDNs
deno --deny-import=cdn.jsdelivr.net,unpkg.com app.ts

Conditional Exports

Support for npm package conditional exports:

// Works with React Server Components
import React from "npm:react@19.1.0";

console.log(React.Component);
# Normal behavior
deno app.jsx
# Component: [Function: Component]

# With react-server condition
deno --conditions=react-server app.jsx
# undefined

Bare Specifiers in deno run

Now you can use bare specifiers directly:

// deno.json
{
  "imports": {
    "file-server": "jsr:@std/http/file-server",
    "cowsay": "npm:cowsay",
    "lume/": "https://deno.land/x/lume@v3.0.4/"
  }
}
# All of these now work
deno run file-server
deno run cowsay "Hello World!"
deno run lume/cli.ts

# You can even omit 'run'
deno cowsay "Hello World!"

XML/SVG Formatting

deno fmt now formats XML and SVG files:

<!-- Before: poorly formatted -->
<svg width="100" height="100"><circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /></svg>
deno fmt
<!-- After: well formatted -->
<svg width="100" height="100">
  <circle
    cx="50"
    cy="50"
    r="40"
    stroke="green"
    stroke-width="4"
    fill="yellow"
  />
</svg>

Simplified Node.js Globals

Deno now exposes Node.js globals to user code without additional flags:

  • Buffer - No more --unstable-node-globals needed
  • global - Alias for globalThis
  • setImmediate / clearImmediate - For compatibility
// This works without additional flags
const buffer = Buffer.from("Hello World");
console.log(buffer.toString('base64'));

global.myGlobal = "accessible everywhere";
setImmediate(() => console.log("Next tick"));

Significant Node.js API Improvements

Deno 2.4 significantly improves Node.js compatibility:

New APIs Available

// fs.glob - File pattern search
import { glob } from "node:fs/promises";

for await (const file of glob("**/*.ts")) {
  console.log(file);
}

// crypto.Certificate - Certificate handling
import { Certificate } from "node:crypto";
const cert = new Certificate();

// dgram multicast - UDP multicast
import { createSocket } from "node:dgram";
const socket = createSocket('udp4');
socket.setBroadcast(true);
socket.setMulticastTTL(1);

Improved Compatibility

  • node:buffer - >95% compatibility
  • node:events - >95% compatibility
  • node:querystring - >95% compatibility
  • node:crypto - Significant improvements
  • node:http - Unix socket support

Other Interesting Improvements

Fetch over Unix Sockets

const client = Deno.createHttpClient({
  proxy: {
    transport: "unix",
    path: "/var/run/docker.sock",
  }
});

const response = await fetch("http://localhost/containers/json", { client });

Deno serve with callbacks

export default {
  fetch(req) {
    return new Response("Hello world!");
  },
  onListen(addr) {
    console.log(`🚀 Server running on ${addr.hostname}:${addr.port}`);
  },
} satisfies Deno.ServeDefaultExport;

Improved Jupyter

# Custom name for kernel
deno jupyter --install --name="deno_2_4"

# Custom display name
deno jupyter --install --display="Deno 2.4 Development Kernel"

Markdown-Compatible Tables

# deno coverage and deno bench now generate Markdown tables
deno coverage
# | File      | Branch % | Line % |
# | --------- | -------- | ------ |
# | parser.ts | 87.1     | 86.4   |
# | All files | 86.1     | 87.4   |

My Experience with 2.4

I’ve been testing Deno 2.4 for the past few weeks, and here are my impressions:

What I Like Most

  1. deno bundle really works - Finally I can generate reliable production bundles
  2. Asset imports are game-changing - Especially useful for apps handling many static resources
  3. Stable OpenTelemetry - Effortless observability is incredible for debugging
  4. DENO_COMPAT=1 - Migrating Node.js projects is much simpler

Use Cases Where It Shines

  • Single-page applications - With deno bundle and asset imports
  • CLI tools - With deno compile and embedded assets
  • Microservices - With automatic OpenTelemetry
  • Gradual migration - With DENO_COMPAT=1

Minor Annoyances

  • Import attributes require --unstable-raw-imports (for now)
  • Some flags are still verbose
  • Documentation for some new features is still in development

Conclusion

Deno 2.4 feels like a maturity release. The return of deno bundle with esbuild, import attributes, and stable OpenTelemetry are game-changing features.

For developers coming from Node.js, DENO_COMPAT=1 makes the transition virtually transparent. And for those of us already using Deno, the new features open possibilities that previously required external tools.

Should you upgrade? Definitely. This version brings substantial improvements without significant breaking changes.

# Upgrade to Deno 2.4
deno upgrade

# Check version
deno --version
# deno 2.4.0

Additional Resources


Did you find this article helpful? Share it with other JavaScript/TypeScript developers who might be interested in Deno 2.4’s new features. And if you have any questions or experiences to share, don’t hesitate to contact me.

Comments

Latest Posts

3 min

548 words

When working on large projects, it’s common to have test suites that can take several minutes to run. And when one of those tests fails early in the execution, it’s frustrating to wait for all the others to complete just to see the full results.

Jest includes a feature I’ve found very useful in development: the bail option, which allows stopping test execution after a certain number of failures. It’s one of those features that once you know and start using, you don’t understand how you lived without it.

6 min

1231 words

Are you tired of seeing imports like import Logger from "../../../utils/logger" in your Node.js projects? If you develop applications with complex folder structures, you’ve surely encountered the labyrinth of dots and slashes that relative imports can become. Fortunately, TypeScript offers an elegant solution: Path Aliases.

In this complete guide, you’ll learn to configure path aliases in Node.js projects with TypeScript, forever eliminating those confusing imports and significantly improving the readability and maintainability of your code.

6 min

1138 words

The first alpha version of PHP 8.5 has just been released, and I must confess it has me more excited than recent versions. It’s not just for the technical improvements (which are many), but because PHP 8.5 introduces features that will change the way we write code.

And when I say “change,” I mean the kind of changes that, once you use them, you can’t go back. Like when the null coalescing operator (??) appeared in PHP 7, or arrow functions in PHP 7.4.