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
- Works with npm and JSR - No additional configuration needed
- Automatic tree shaking - Only includes what you need
- Minification included - Via esbuild
- 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
- Logs associated with HTTP requests
- Automatic traces of async operations
- Performance metrics
- 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 withoutnode: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-globalsneededglobal- Alias forglobalThissetImmediate/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% compatibilitynode:events- >95% compatibilitynode:querystring- >95% compatibilitynode:crypto- Significant improvementsnode: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
- deno bundle really works - Finally I can generate reliable production bundles
- Asset imports are game-changing - Especially useful for apps handling many static resources
- Stable OpenTelemetry - Effortless observability is incredible for debugging
- 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
- Deno 2.4 Release Notes - Official release notes
- deno bundle Documentation - Bundling documentation
- Import Attributes Guide - Import attributes guide
- OpenTelemetry in Deno - Observability documentation
- Node.js Compatibility - Node.js compatibility status
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