Skip to main content
Deno 2.4 is here with deno bundle, bytes/text imports, stabilized OTel and more
Learn more
Deno 2.4

Deno 2.4: deno bundle is back

To upgrade to Deno 2.4, run the following in your terminal:

deno upgrade

If Deno is not yet installed, run one of the following commands to install or learn how to install it here.

# Using Shell (macOS and Linux):
curl -fsSL https://deno.land/install.sh | sh

# Using PowerShell (Windows):
iwr https://deno.land/install.ps1 -useb | iex

What’s new in Deno 2.4

deno bundle

Deno 2.4 restores the deno bundle subcommand for creating single-file JavaScript or TypeScript bundles. It supports both server-side and browser platforms, works with npm and JSR dependencies, and includes automatic tree shaking and minification via esbuild.

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

# Set platform to browser (defaults to "deno")
$ deno bundle --platform browser --output bundle.js app.jsx 

# Also generate an external sourcemap
$ deno bundle --platform browser --output bundle.js --sourcemap=external app.jsx

Bundling JavaScript with React for the client.

For long time Deno users, you may remember we used to have deno bundle, which was subsequently deprecated. JavaScript bundling is an incredibly complex problem, and we did not believe we could build deno bundle properly. Now that deno bundle uses esbuild under the hood, it is here to stay.

We’re working on a runtime API to make bundling available programmatically in the future. On top of that, we plan to add plugins, allowing you to customize how the bundler processes modules during the build process.

For more information visit deno bundle reference and the bundling docs page

Importing text and bytes

Have you ever wanted to include a data-file of some sort in your JavaScript module graph? Maybe a markdown file, icons, or a binary blob? Until now you would have had to load these files at runtime, carefully ensuring the assets were shipped alongside your code:

const image = Deno.readFileSync(import.meta.resolve("./image.png"));
const text = await Deno.readTextFile(import.meta.resolve("./log.txt"));

In Deno 2.4, you can link these files into the JavaScript graph with the flag --unstable-raw-imports:

import message from "./hello.txt" with { type: "text" };
import bytes from "./hello.txt" with { type: "bytes" };
import imageBytes from "./image.png" with { type: "bytes" };

console.log("Message:", message);
// Message: Hello, Deno!

console.log("Bytes:", bytes);
// Bytes: Uint8Array(12) [
//    72, 101, 108, 108, 111,
//    44,  32,  68, 101, 110,
//   111,  33
// ]

Deno.serve((_req) => {
  return new Response(imageBytes, {
    status: 200,
    headers: {
      "Content-Type": "image/png",
      "Content-Length": imageBytes.byteLength.toString(),
    },
  });
});
// Shows image.png at localhost:8000

You can use this with deno bundle, simplifying the process of importing non-JavaScript files in your application and avoiding manual file I/O or middleware.

main.ts
import icon from "./icon.png" with { type: "bytes" };

console.log("Icon", icon);
$ deno bundle --unstable-raw-imports -o app.js main.ts
⚠️ deno bundle is experimental and subject to changes
Bundled 2 modules in 7ms
  app.js 268B

$ deno app.js
Icon Uint8Array(69) [
  137, 80, 78, 71, 13,  10,  26,  10,   0,  0,  0, 13,
   73, 72, 68, 82,  0,   0,   0,   1,   0,  0,  0,  1,
    8,  2,  0,  0,  0, 144, 119,  83, 222,  0,  0,  0,
   12, 73, 68, 65, 84, 120, 218,  99,  96, 96, 96,  0,
    0,  0,  4,  0,  1, 200, 234, 235, 249,  0,  0,  0,
    0, 73, 69, 78, 68, 174,  66,  96, 130
]

Additionally, this feature works with deno compile, so that you can embed assets into your final compiled binary.

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

const DICT = dictData.split(" ");

while (true) {
  let input = prompt("> Enter the word to check: ");
  input = input.trim();
  if (!input) {
    continue;
  }

  if (!DICT.includes(input)) {
    console.error(`${input} is not a known word`);
  } else {
    console.log(`${input} is a known word`);
  }
}
$ deno compile --unstable-raw-imports spellcheck.js
Compile file:///dev/spellcheck.js to spellcheck

$ ./spellcheck
> Enter the word to check:  deno
deno is a known word
> Enter the word to check:  asdf
asdf is not a known word

Bytes and text imports add to Deno’s existing ability to import JSON and Wasm files natively:

import file from "./version.json" with { type: "json" };
console.log(file.version);
// "1.0.0"

import { add } from "./add.wasm";
console.log(add(1, 2));
// 3

While the ability to import various file types have been around (such as in Next.js and other frameworks), these approaches are not spec-friendly and introduce unnecessary complexity via domain-specific languages and ahead-of-time compilers that modify the language.

We’ve wanted to add importing other file types earlier, but also want to be aligned with the spec and avoid introducing breaking changes. Astute observers might note that this feature actually goes ahead of current spec (here’s the proposal for import attributes). However, due to ongoing discussions and proposed upcoming features about this feature, we are confident this implementation is in the right direction.

Built-in OpenTelemetry is now stable

In 2.2, we introduced built-in OpenTelemetry support, which auto-instruments the collection of logs, metrics, and traces for your project. We’re excited to announce that it is now stable in 2.4, which means you no longer need --unstable-otel flag:

$ OTEL_DENO=1 deno --allow-net server.ts
Listening on http://localhost:8000/

Deno’s OpenTelemetry support simplifies observability in your JavaScript projects, as it automatically associates logs with HTTP requests, works in Node with no additional config, and is available to all your projects in our new Deno Deploy:

Deno’s built-in OTel auto-instruments console.logs and associates them with HTTP requests. Here we click view a trace from a log, then view all logs within that HTTP request.

Note that you will still need to set the environment variable OTEL_DENO=1 to enable instrumentation, since it uses additional resources and don’t want it on all the time.

For more information about Deno’s OpenTelemetry support, check out the following resources:

Modify the Deno environment with the new --preload flag

We’ve landed a new --preload flag that will execute code before your main script. This is useful if you are building your own platform and need to modify globals, load data, connect to databases, install dependencies, or provide other APIs:

setup.ts
delete Deno;
globalThis.window = globalThis;
main.ts
console.log(Deno);
$ deno --preload setup.ts main.ts

error: Uncaught (in promise) ReferenceError: Deno is not defined
console.log(Deno);
            ^
    at file:///main.ts:1:13

While we’ve traditionally been cautious about allowing user code to modify the runtime, it’s become clear that there are enough circumstances in which this is needed. This flag is available to use in deno run, deno test and deno bench subcommands.

Easier dependency management with deno update

We’ve added a new subcommand, deno update, which allows you to update your dependencies to the latest versions:

Updating dependencies to their latest versions with deno update --latest.

This command will update npm and JSR dependencies listed in deno.json or package.json files to latest semver compatible versions.

# Ignore semver constraints
deno update --latest

# Filter packages that match "@std/*"
deno update "@std/*"

# Include all workspace members
deno update --recursive

deno outdated --update did the same thing, but to save you the headache of typing additional characters, we added the alias deno update.

For more information about deno update, check out the Deno docs.

Collect script coverage with deno run --coverage

deno test --coverage will execute your test suite while collecting coverage information, which is a breeze to use. However, in some situations like if you need to spawn a subprocess as part of the test suite, deno test would not collect the coverage of the subprocess, making the report spotty.

Deno 2.4 alleviates this problem by introducing the --coverage flag that you can pass to deno run.

You can also use DENO_COVERAGE_DIR environment variable instead.

main.ts
function foo(a: number, b: number) {
  if (a > 0) {
    console.log(a);
  } else {
    console.log(a);
  }

  if (b > 0) {
    console.log(b);
  } else {
    console.log(b);
  }
}

const [a = 0, b = 0] = Deno.args;

foo(+a, +b);
$ deno run --coverage main.ts
0
0

$ deno coverage
| File      | Branch % | Line % |
| --------- | -------- | ------ |
| main.ts   |      0.0 |   57.1 |
| All files |      0.0 |   57.1 |

To learn more about coverage collection and presentation visit Deno docs.

Oh by the way, HTML coverage report now supports dark mode 🤫.

DENO_COMPAT=1

We’ve introduced a new environment variable, DENO_COMPAT=1 , that will tell Deno to enable the below flags to improve ergonomics when using Deno in package.json-first projects:

For instance:

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

# 2.4
DENO_COMPAT=1 deno app.js

In the future, the scope of this environmental variable might be expanded further — eg. to not require npm: prefixes when deno installing packages.

We are also stabilizing the --unstable-sloppy-imports flag to become --sloppy-imports, which tells Deno to infer file extensions from imports that do not include them. While Deno generally encourages requiring file extensions to avoid inefficiencies such as probing the filesystem, the --sloppy-imports flag is a way to run code that uses extensionless imports.

Permission changes

The --allow-net flag now supports subcomain wildcards and CIDR ranges:

subdomain_wildcards.ts
const res1 = await fetch("http://test.foo.localhost");
const res2 = await fetch("http://test.bar.localhost");
$ deno --allow-net=*.foo.localhost subdomain_wildcards.ts
Response {
  url: "http://test.localhost",
  ...
}
┏ ⚠️  Deno requests net access to "test.bar.localhost:80".
┠─ Requested by `fetch()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-net
┠─ Run again with --allow-net to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >
cidr_ranges.ts
const res1 = await fetch("http://192.168.0.128:8080");
const res2 = await fetch("http://192.168.0.1:8080");
$ deno --allow-net=192.168.0.128/25 main.ts
Response {
  url: ""http://192.168.0.128:8080"",
  ...
}
┏ ⚠️  Deno requests net access to "192.168.0.1:8080".
┠─ Requested by `fetch()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-net
┠─ Run again with --allow-net to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

Deno 2 added --allow-import flag that allows you to specify which remote hosts Deno can download and execute code from.

This release adds a complimentary --deny-import flag that allows to explicitly block certain hosts.

By default Deno allows to import code from several hosts:

  • deno.land:443
  • jsr.io:443
  • esm.sh:443
  • cdn.jsdelivr.net:443
  • raw.githubusercontent.com:443
  • user.githubusercontent.com:443
main.ts
import * as cowsay from "https://cdn.jsdelivr.net/npm/cowsay";
$ deno main.ts

Now, you can disallow importing code from one of the default hosts:

$ deno --deny-import=cdn.jsdelivr.net main.ts
error: Requires import access to "cdn.jsdelivr.net:443", run again with the --allow-import flag
    at file:///main.ts:1:25

To further supplement --allow-import usages, the Deno.permissions runtime API has been fixed to support "import" type:

console.log(Deno.permissions.query({ name: "import" }));
// Promise { PermissionStatus { state: "prompt", onchange: null } }

Finally, the Deno.execPath() API doesn’t require read permissions anymore:

main.ts
console.log(Deno.execPath());
# Deno 2.3
$ deno main.ts
┏ ⚠️  Deno requests read access to <exec_path>.
┠─ Requested by `Deno.execPath()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-read
┠─ Run again with --allow-read to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >

# ✅ Now
$ deno main.ts
/dev/.deno/bin/deno

We made this update because in common scenarios, requiring read permission ends up less secure than not requiring permissions. For example, if a user wants to spawn a Deno subprocess using the current executable, they would do something like:

spawn.ts
new Deno.Command(Deno.execPath(), { args: ["eval", "1+1"] }).outputSync();

And to run it:

# Deno 2.3
$ deno --allow-read --allow-run=deno spawn.ts

# ✅ Now
$ deno --allow-run=deno

Before Deno 2.4, it was possible to limit the scope of --allow-read flag, but it required upfront knowledge of where the Deno executable was or a bit of shell expansion trickery. Most users understandably opted to give a blanket --allow-read flag instead.

Not requiring read permissions allows the above program to be run with just --allow-run=deno flag.

Conditional package.json exports

This version ships with support for conditional exports in npm packages.

Conditional exports is a mechanism that allows npm packages to use different exports depending on the user provided conditions.

The stark example here is the react-server condition that libraries from the React ecosystem can use to swap out exports for use with Server Components.

app.jsx
import react from "npm:react@19.1.0";

console.log(react.Component);
$ deno app.jsx
Component: [Function: Component]

$ deno --conditions=react-server app.jsx
undefined

You can specify arbitrary conditions and as many of them as you want. If the flag is not used Deno uses following conditions by default: deno, node, import, default.

We expect most users to not have to use this flag directly, but is a crucial option for the tooling you might depend on.

deno run bare specifiers

Deno v2.4 now supports using bare specifiers as entry points in deno run.

Imagine you had a deno.json like this:

deno.json
{
  "imports": {
    "lume/": "https://deno.land/x/lume@v3.0.4/",
    "file-server": "jsr:@std/http/file-server",
    "cowsay": "npm:cowsay"
  }
}

You might have expected to be able to deno run file-server and be done with it. Unfortunately, before Deno 2.4, this didn’t work:

# Before Deno v2.4
$ deno run -ERW lume/cli.ts
error: Module not found "file:///dev/lume/cli.ts".

$ deno run file-server
error: Module not found "file:///dev/file-server".

$ deno run -ER cowsay "Hello there!"
error: Module not found "file:///dev/cowsay".

However, in 2.4:

# ✅ In Deno 2.4
$ deno run -ERW lume/cli.ts
🍾 Site built into ./_site

$ deno run file-server
Listening on ...

$ deno run -ER cowsay "Hello there!"
 ______________
< Hello there! >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

In most cases you can also omit run subcommand, like so deno -ER cowsay "Hello there!"

deno fmt XML and SVG files

deno fmt will know automatically format .xml and .svg files for you:

circle.svg
<svg    width="100"    height="100">
  <circle cx="50" cy="50" 
    r="40" stroke="green" stroke-width="4" 
  fill="yellow" />
</svg>
$ deno fmt
/dev/circle.svg
Formatted 1 file

$ cat circle.svg
<svg width="100" height="100">
  <circle
    cx="50"
    cy="50"
    r="40"
    stroke="green"
    stroke-width="4"
    fill="yellow"
  />
</svg>

Additionally, formatting .mustache templates is now supported, but requires --unstable-component flag or unstable.fmt-component option in deno.json.

Better tsconfig.json support

Deno automatically discovers tsconfig.json files next to deno.json and/or package.json files.

This release also brings better handling for tsconfig.json files, with added support for "references", "extends", "files", "include" and "exclude" options. That means you can expect a much better experience when working with popular frontend frameworks like Vue, Svelte, Solid, Qwik and more.

Simpler Node globals

Deno v2.0 added the process global for improved compatibility with existing npm ecosystem. But there are more global variables that were not available in Deno for user code, but were available for npm packages:

  • Buffer
  • global
  • setImmediate
  • clearImmediate

Additionally, globals like setTimeout and setInterval (as well as their clear* counterparts) are different globals depending if the context was user code or in an npm dependency.

To achieve this bifurcation, we used a fairly complex setup hooking into V8 internal APIs to decide if a global is available at a particular call site or not. While the solution was clever, it comes with a high performance cost — on each access of one of these globals, we were walking the stack and checking if the call site is coming from an npm package. Finally, the mental overhead of understanding which globals are available where is non-trivial.

In this release, we’ve made Buffer, global, setImmediate and clearImmediate available to user code as well. This helps if you are running an existing Node.js project that relies on these globals, as you no longer need to use the --unstable-node-globals flag to expose them, since they are now available everywhere all the time.

Local npm packages

In Deno v2.3, we introduced a way to use local npm packages. We received a lot of positive feedback about this issue, but some of you raised concerns that the patch option can be easily confused with npm patch.

The option has been renamed to links (to better reflect npm link equivalent functionality). You will receive a deprecation warning when using patch option instead of links.

deno.json
{
+ "links": [
- "patch": [
    "../cowsay"
  ]
}

In the future we might support npm patch equivalent functionlity out of the box. If you depend on this tool, let us know.

Node.js API support

We’ve improved support for Node.js APIs again. Aside from the numerous additions and fixes that you can dive into in the list below, the highlight is support for the glob API, as well as achieving over 95% compatibility for modules such as node:buffer, node:events, node:querystring, node:quic and node:wasm.

We plan to keep increasing these compatibility numbers in the next release with focus on modules such as node:crypto, node:http, node:tls, node:worker and node:zlib.

Additionally, Deno 2.4 will use @types/node version 22.15.14 by default when type checking.

Finally, here’s the full list of Node.js API fixes:

LSP improvements

The Deno LSP received numerous improvements including:

Other fun things

  • fetch now works over Unix and Vsock sockets - create an instance of Deno.HttpClient with a proper proxy option:
fetch.ts
const client = Deno.createHttpClient({
  proxy: {
    transport: "unix",
    path: "/path/to/unix.sock",
  },
  // or
  proxy: {
    transport: "vsock",
    cid: 2,
    port: 80,
  },
});

await fetch("http://localhost/ping", { client });
  • deno serve now supports onListen() callbacks:
server.ts
export default {
  fetch(req) {
    return new Response("Hello world!");
  },
  onListen(addr) {
    console.log(
      `😎 Hello world server started on ${addr.hostname}:${addr.port}`,
    );
  },
} satisfies Deno.ServeDefaultExport;
$ deno serve server.ts
😎 Hello world server started on 0.0.0.0:8000
  • Better management of Jupyter kernels with deno jupyter:
# Give kernel an explicit name
$ deno jupyter --install --name="deno_kernel"
✅ Deno kernelspec installed successfully at /Jupyter/kernels/deno_kernel.

# Don't overwrite existing kernel by mistake
$ deno jupyter --install --name="deno_kernel"
error: Deno kernel already exists at /Jupyter/kernels/deno_kernel, run again with `--force` to overwrite it

# Give a display name to the kernel
deno jupyter --install --display="Deno 2.4 kernel"
# Now you will see "Deno 2.4 kernel" in the kernel selector UI
  • Lint plugins can now access comments
my-plugin.ts
export default {
  name: "my-lint-plugin",
  rules: {
    "my-lint-rule": {
      create(context) {
        return {
          Program(node) {
            console.log("Top level comments", node.comments);
          },
          FunctionDeclaration(node) {
            const all = ctx.sourceCode.getAllComments();
            const before = ctx.sourceCode.getCommentsBefore(node);
            const after = ctx.sourceCode.getCommentsAfter(node);
            const inside = ctx.sourceCode.getCommentsInside(node);

            console.log({ program, all, before, after, inside });
          },
        };
      },
    },
  },
} satisfies Deno.lint.Plugin;
  • deno bench and deno coverage tables are now Markdown compatible

You can now directly copy-paste them into Markdown documents with proper rendering

# Before
-----------------------------------
File          | Branch % | Line % |
-----------------------------------
 parser.ts    |     87.1 |   86.4 |
 tokenizer.ts |     80.0 |   92.3 |
-----------------------------------
 All files    |     86.1 |   87.4 |
-----------------------------------

# After
| File         | Branch % | Line % |
| ------------ | -------- | ------ |
| parser.ts    |     87.1 |   86.4 |
| tokenizer.ts |     80.0 |   92.3 |
| All files    |     86.1 |   87.4 |
  • Listening for Ctrl+Close (SIGHUP) now works correctly on Windows
signal.ts
// This will now work correctly on Windows
Deno.addSignalListener("SIGHUP", () => {
  console.log("Got SIGHUP");
});

Acknowledgments

We couldn’t build Deno without the help of our community! Whether by answering questions in our community Discord server or reporting bugs, we are incredibly grateful for your support. In particular, we’d like to thank the following people for their contributions to Deno 2.4: 林炳权, André Lima, Andy Vu, Asher Gomez, Boye Lillejord-Nygård, chirsz, Christian Svensson, Clarence Delmacio Manuel, ctrl+d, Cyan, Daniel Osvaldo R, David Emanuel Buchmann, Edilson Pateguana, Efe, Elias Rhouzlane, familyboat, Hajime-san, ikkz, James Bronder, Janosh Riebesell, JasperVanEsveld, Jeff Hykin, Jonh Alexis, Kenta Moriuchi, Kingsword, Laurence Rowe, Lino Le Van, LongYinan, Marshall Walker, MikaelUrankar, nana4gonta, narumincho, Nicholas Berlette, scarf, Scott Kyle, sgasho, Simon Lecoq, stefnotch, Timothy J. Aveni, ud2, Volker Schlecht, and zino.

Would you like to join the ranks of Deno contributors? Check out our contribution docs here, and we’ll see you on the list next time.

Believe it or not, the changes listed above still don’t tell you everything that got better in 2.4. You can view the full list of pull requests merged in Deno 2.4 on GitHub.

Thank you for catching up with our 2.4 release, and we hope you love building with Deno!

🚨️ There have been major updates to Deno Deploy! 🚨️

and much more!

Get early access today.