Hacker News — AI on Front Page · · 46 min read

Deno 2.8

Mirrored from Hacker News — AI on Front Page for archival readability. Support the source by reading on the original site.

215 pts · 98 comments on Hacker News

Deno 2.8 is here. This is our biggest minor release to date and we’re excited to share it with you.

To upgrade to Deno 2.8, 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

New subcommands

deno audit fix

deno audit (shipped in 2.6) reports vulnerabilities in npm packages in your dependency tree. The new deno audit fix subcommand goes one step further and automatically upgrades affected packages to the nearest patched version that still satisfies your version constraints (#32909, #34273). The same behavior is also available as a --fix flag on deno audit:

$ deno audit fix
╭ body-parser vulnerable to denial of service when url encoding is enabled
│ Severity:   high
│ Package:    body-parser
│ Vulnerable: <1.20.3
╰ Info:       https://github.com/advisories/GHSA-qwcr-r2fm-qrc7

╭ Express.js Open Redirect in malformed URLs
│ Severity:   moderate
│ Package:    express
│ Vulnerable: <4.19.2
╰ Info:       https://github.com/advisories/GHSA-rv95-896h-c2vc

Found 2 vulnerabilities
Severity: 0 low, 1 moderate, 1 high, 0 critical

Fixed 1 vulnerability:
  body-parser 1.19.0 -> 1.20.3

1 vulnerability could not be fixed automatically:
  express (major upgrade to 5.0.0)

Anything that needs a major-version bump is listed separately, so you can decide whether to relax the constraint. Learn more about deno audit fix.

deno bump-version

deno bump-version updates the version field in your deno.json or package.json (#30562):

$ deno bump-version patch         # 1.4.6 -> 1.4.7
$ deno bump-version minor         # 1.4.6 -> 1.5.0
$ deno bump-version major         # 1.4.6 -> 2.0.0
$ deno bump-version prerelease    # 1.4.7-0 -> 1.4.7-1

In a workspace it does more. Run it at the workspace root and the same increment is applied to every member package, with matching jsr: version constraints in the root config and import map rewritten in place so cross-package references stay in sync (#33689):

$ deno bump-version patch    # bumps every workspace member

Without an increment argument, workspace mode switches to deriving per-package bumps from Conventional Commits between a base ref and the current branch. It honors scoped commits, wildcard * scopes, BREAKING / ! for major bumps, prerelease increments, and 0.x.y semver semantics, and treats any manual version edits since the base ref as authoritative.

$ deno bump-version --base=main --dry-run

--dry-run prints the planned changes without writing anything, and --start / --base let you pin the comparison range when the default “current branch since the latest tag” isn’t what you want.

Learn more about deno bump-version.

deno ci

CI scripts and Dockerfiles want one thing from an install: “give me exactly what the lockfile says, and fail loudly if anything is off.” Until now that meant remembering the right combination of flags on deno install. Deno 2.8 adds a dedicated deno ci subcommand (#34235):

$ deno ci

It errors if deno.lock is missing, removes any existing node_modules directory, and then runs the install with --frozen so the lockfile must match the config file exactly. Drop it into your CI step or Dockerfile and you get an obvious, greppable signal of “reproducible install” without having to think about flags. --prod and --skip-types work the same way they do on deno install.

deno pack

deno pack is closer to tsc + npm pack combined than to npm pack alone: it builds a Deno or JSR project into an npm-publishable tarball in one shot (#32139). Given a deno.json like:

deno.json
{
  "name": "@scope/my-lib",
  "version": "1.0.0",
  "exports": "./mod.ts"
}

…running deno pack produces a scope-my-lib-1.0.0.tgz that’s ready for npm publish. The tarball contains:

  • A generated package.json with type: "module", conditional exports (types/import/default), and the extracted runtime dependencies.
  • Your TypeScript transpiled to JavaScript.
  • .d.ts declaration files extracted via the same fast-check pipeline deno publish uses (pass --allow-slow-types to skip).
  • README and LICENSE files if present in the project root.

Along the way deno pack rewrites specifiers so the published package works inside the npm ecosystem: jsr:@std/path becomes @jsr/std__path, npm:express@4 becomes express, relative ./utils.ts imports become ./utils.js, and node: builtins are left alone. If your code calls Deno.* APIs, the package automatically picks up @deno/shim-deno as a dependency so it runs on Node too (opt out with --no-deno-shim).

File selection is graph-based: only modules reachable from your declared exports are bundled, not whatever sits in the directory. Tarballs are deterministic (sorted entries, fixed timestamps and permissions), which matters for reproducible builds and content-addressed registries.

$ deno pack                            # build the tarball
$ deno pack --dry-run                  # preview the file list
$ deno pack --set-version 2.0.0        # override version without editing deno.json
$ deno pack --output my-package.tgz    # write to a specific path
$ deno pack --ignore=tests/            # exclude test files
$ deno pack --allow-dirty              # pack with uncommitted changes

Learn more about deno pack.

deno transpile

A new subcommand strips types from TypeScript, JSX, and TSX and writes plain JavaScript to disk. No bundling, no module rewriting, no config. Just the emit step.

greeter.ts
interface User {
  name: string;
  balance: number;
}

export function greet(user: User): string {
  return `Hello ${user.name}, you have $${user.balance.toFixed(2)}`;
}
$ deno transpile greeter.ts -o greeter.js
greeter.js
export function greet(user) {
  return `Hello ${user.name}, you have $${user.balance.toFixed(2)}`;
}

deno transpile accepts multiple files, --outdir for batch output, --source-map separate|inline, and --declaration to emit .d.ts alongside the JS. Useful when you need to publish a JS-only artifact or pre-build TS for a runtime that doesn’t speak it natively.

Learn more about deno transpile.

deno why

deno why <package> explains why a package is installed by walking from your direct dependencies down to the package in question (#32908). It’s the equivalent of npm explain / pnpm why / yarn why. It works with both npm and JSR dependencies (#34227).

Given a project that mixes both registries:

deno.json
{
  "imports": {
    "express": "npm:express@^4",
    "dax": "jsr:@david/dax@^0.43"
  }
}

deno why traces an npm transitive back to its npm entry point:

$ deno why qs
[email protected]
  npm:express@4 > [email protected]

[email protected]
  npm:express@4 > [email protected] > [email protected]

…and a JSR transitive back to its JSR entry point, with each path through the tree listed separately:

$ deno why @std/path
@std/[email protected]
  jsr:@david/[email protected] > @std/[email protected]
  jsr:@david/[email protected] > @david/[email protected] > @std/[email protected]
  jsr:@david/[email protected] > @std/[email protected] > @std/[email protected]
  jsr:@david/[email protected] > @david/[email protected] > @std/[email protected] > @std/[email protected]

Pin to a specific version with deno why [email protected] or deno why @std/[email protected] when you only care about one branch of the tree. Learn more about deno why.

Deno now defaults to npm:

Deno 2.8 drops the npm: prefix requirement at the CLI: deno add and deno install now treat unprefixed names as npm packages by default (#33246), so the command you type matches what every Node developer already types out of muscle memory.

# Before 2.8
$ deno add express
error: express is missing a prefix. Did you mean `deno install npm:express`?

# 2.8
$ deno add express
Add npm:[email protected]

Dependencies:
+ npm:[email protected]

The npm: prefix still works (and is still required in import specifiers), but you don’t have to type it at the CLI. JSR packages keep the jsr: prefix so the two registries stay unambiguous.

With this change deno install becomes a drop-in for npm install, yarn, or pnpm install in an existing Node project. It reads package.json, writes a compatible node_modules layout, and installs 3.66x faster than 2.7 on a cold cache; warm installs are faster still thanks to Deno’s shared global cache across projects. Reach for Deno as your package manager and keep running everything else on Node. Learn more about deno install.

Node.js API compatibility

Node.js compatibility has been an important focus for us in the past couple years. And we’re happy to announce that we made a huge leap forward in Deno 2.8: pass rate against Node’s own test suite jumped from roughly 42% in Deno 2.7 to 76.4% in Deno 2.8 (3,405 of 4,457 tests passing); 500 commits landed since Deno 2.7, touching nearly every node: module.

We keep close track of this percentage at node-test-viewer.deno.dev:

Node.js test suite pass rate over time on Linux, Windows, and Darwin, climbing from ~42% in January 2026 to the low-to-mid 70s by May 2026.
Don’t mind that 100% blip in January 😅

Head-to-head against Bun 1.3.14 on the same suite:

Node.js test suite pass rate (4,457 tests)
Deno v2.8
76.4% (3,405)
Bun 1.3.14
40.6% (1,810)
Excluding tests that bail out early: Deno 2.8 72.4% (3,229 / 4,457) vs Bun 1.3.14 36.4% (1,623 / 4,457).

Deno 2.8 also makes Node compatibility cheaper in real projects: many Node built-in modules are now lazy-loaded, so programs that don’t touch them start faster (importing one of those modules later pays a small deferred load cost). Several node:* hot paths also picked up dedicated optimizations; see the Performance section below for benchmark numbers.

Performance

Deno 2.8 ships meaningful speedups across the package manager, node:* compatibility, HTTP serving, and the Web platform. Measured on Linux against Deno 2.7.1:

Deno 2.7 (gray) vs 2.8 (blue)
Cold npm install
lower is better
v2.7
3,319 ms
v2.8
906 ms
3.66x faster
node:buffer base64
lower is better
v2.7
2,594 ms
v2.8
844 ms
3.07x faster
node:http throughput
higher is better
v2.7
8,339 req/s
v2.8
18,431 req/s
2.21x faster
node:crypto scrypt
lower is better
v2.7
1,533 ms
v2.8
724 ms
2.12x faster
node:http p99 latency
lower is better
v2.7
20.86 ms
v2.8
11.89 ms
1.75x faster
node:http chunked writes
higher is better
v2.7
6,635 req/s
v2.8
11,521 req/s
1.74x faster
Chunked writes p99
lower is better
v2.7
25.39 ms
v2.8
15.68 ms
1.62x faster
node:fs recursive cpSync
lower is better
v2.7
432 ms
v2.8
290 ms
1.49x faster
Worker MessagePort ping-pong
lower is better
v2.7
1,678 ms
v2.8
1,270 ms
1.32x faster
Bars share a scale per order of magnitude. Process benchmarks: 30 hyperfine samples. HTTP benchmarks: 10 samples of 30-second oha runs.

Cold npm installs. An entrypoint importing React, Vite, Babel parser, and ESLint installs 3.66x faster in Deno 2.8, 3,319ms down to 906ms, on a fresh DENO_DIR. Across 30 samples, the bootstrap 95% confidence interval for the speedup was 3.53x to 3.75x. A few of the changes that fed into that number:

  • Abbreviated packuments (#32364). The npm registry exposes a smaller “abbreviated” metadata document (application/vnd.npm.install-v1+json) that includes only the fields a resolver needs; Deno now uses this smaller document for resolution and only fetches the full packument if it needs to.
  • Parallel npm resolution (#32416). The resolver used to walk parent nodes one at a time. Deno 2.8 fans out across parent nodes too, so independent branches of the dependency tree no longer wait on each other.
  • Decompression off the async event loop (#32400). Large packument gzip decompression could stall other HTTP/2 streams sharing the same connection. Deno 2.8 routes registry-body decompression through a blocking thread pool, freeing the event loop for more concurrent requests.
  • Tarball extraction split into CPU and I/O phases (#32408). Tarball extract used to be a single tight loop. It now splits into a CPU-bound decompression phase and an I/O-bound filesystem write phase. Pairs with libdeflater + a preallocated buffer (#32511) (a faster gzip decoder than the stock flate2) and fewer syscalls during tarball extraction (#32541).

node:http. Hello-world node:http more than doubles throughput (2.21x) and cuts p99 latency by roughly 40%; chunked responses see comparable gains (1.74x throughput, 1.62x tail latency).

base64 across the board. A single change, switching base64 encode/decode to simdutf (#32743), drives 3.07x faster node:buffer base64 (2,594ms down to 844ms) and the same kind of speedup for atob / btoa and every Web API path that touches base64.

Other node:* hot paths. scryptSync from node:crypto is now 2.12x faster, Rust-backed recursive node:fs cpSync gets 1.49x faster, and exchanging messages over MessagePort between Workers is now 1.32x faster.

Deno.serve. Native Deno.serve got a direct dispatch into the JS handler, a fast path for fully-buffered response bodies, and lighter Vary handling (#33845, #33844, #33892). A hello-world benchmark sees 1.13x increase in throughput and 1.20x lower median p99 latency.

Other optimizations. A pile of smaller wins that show up everywhere:

  • TextEncoder / TextDecoder fast paths for ASCII / Latin-1 / short strings (#32735, #33674, #33675, #34055).
  • Linear-time set / delete on FormData, URLSearchParams, and Headers (#33961), no more quadratic blowups on large header sets.
  • URLPattern ops drop serde overhead and GC pressure (#32766), so middleware that matches every request gets cheaper.
  • Zero-copy V8-to-Rust string conversion in op slow-paths (#32688) and a SIMD ASCII fast path for op_decode (#33720), which Response.text(), File.text(), and FormData parsing all ride on.
  • V8 thread pool capped at 4 threads (#33697), trimming ~1 MB RSS on a typical desktop.
  • malloc_trim after module loading (#32662) and on Worker termination (#32617), fixing 3–5x RSS bloat on Linux when loading large TypeScript codebases.

import defer

Deno now supports the TC39 import defer proposal: a module can be loaded and parsed without running its top-level code. The module is then only evaluated the first time you touch one of its exports (#32360).

This feature is useful for trimming startup time when a module is expensive to evaluate but rarely used on a given codepath.

deferred.js
console.log("deferred module evaluated");
export const value = 42;
main.js
import defer * as deferred from "./deferred.js";

console.log("before access");
console.log(`value: ${deferred.value}`);
console.log("after first access");
$ deno run main.js
before access
deferred module evaluated
value: 42
after first access

The deferred module evaluated line lands between before access and the property read. Module evaluation is delayed until something actually needs it.

The same semantics are available with import.defer() for dynamic imports. A common pattern: pre-load both branches of a decision, but only evaluate the one you actually pick.

png-decoder.js
console.log("PNG decoder evaluated");
export function decode(bytes) {/* ... */}
jpeg-decoder.js
console.log("JPEG decoder evaluated");
export function decode(bytes) {/* ... */}
main.js
const png = await import.defer("./png-decoder.js");
const jpeg = await import.defer("./jpeg-decoder.js");

const format = Deno.args[0]; // "png" or "jpeg"
const decoder = format === "png" ? png : jpeg;

const bytes = Deno.readFileSync(`input.${format}`);
console.log(decoder.decode(bytes));
$ deno run -R main.js png
PNG decoder evaluated

Both modules are fetched and parsed up front, but only the selected one is evaluated.

import defer Works in .ts and .tsx files with no extra setup. deno check and the LSP both understand the syntax:

main.ts
import defer * as deferred from "./deferred.ts";

const n: number = deferred.value;

Learn more about import defer.

TypeScript 6.0.3

The bundled TypeScript compiler is updated to 6.0.3 (#32944). 6.0 is a transition release the TypeScript team uses to land breaking changes and deprecations before the native-port 7.0 ships; see Microsoft’s Announcing TypeScript 6.0 post for the full list.

deno check, deno bundle, the LSP, and deno compile all use the new version automatically. No flag, no config change.

lib.node included by default

deno check and the LSP now include lib.node in every type-check by default (#33823). Before 2.8 you had to add "node" to compilerOptions.lib in deno.json (or sprinkle /// <reference types="node" /> across files) to get NodeJS.*, Buffer, process, and the rest of Node’s ambient types to resolve. Now they’re just there:

node_globals.ts
// 2.8: type-checks with no `compilerOptions.lib` configuration
const buf: Buffer = Buffer.from("hello");
const t: NodeJS.Timeout = setTimeout(() => {}, 0);
console.log(process.versions.node);

lib.node is implemented on top of @types/node, and Deno pulls that package from npm whose major version matches the Node release Deno reports in process.versions.node. Today that’s the Node 24.x types, in line with the version returned by:

node_version.ts
console.log(process.versions.node); // e.g. 24.2.0

If you’d rather pin a different version of @types/node (for example because your project standardizes on Node 22, or because you need a newer patch), just declare it as a dependency and Deno will use yours instead of the bundled copy:

package.json
{
  "devDependencies": {
    "@types/node": "^22.10.0"
  }
}
deno.json
{
  "imports": {
    "@types/node": "npm:@types/node@^22.10.0"
  }
}

In practice the default behavior means npm packages with Node-typed APIs (i.e. nearly all of them) type-check cleanly when imported via npm:, and library authors writing for both runtimes can rely on NodeJS.Timeout, Buffer, and friends without telling Deno consumers to configure their TypeScript.

The trade-off is that Node-only globals like process and Buffer are now in scope at the type level even when you don’t want them, which can quietly encourage code that won’t run in the browser. The no-process-global and no-node-globals lint rules used to be on by default to catch this; in 2.8 they are off by default but still available (#33247). Re-enable them in deno.json if your project targets multiple runtimes:

deno.json
{
  "lint": {
    "rules": {
      "include": ["no-process-global", "no-node-globals"]
    }
  }
}

The runtime globals themselves are unchanged. This is purely a type-level addition. Learn more about including Node types.

Debugging

A major addition to debugging capabilities in 2.8 is that Chrome DevTools can now inspect Deno’s network traffic. Run your program with --inspect-wait (or --inspect / --inspect-brk), open chrome://inspect in Chromium, click Inspect on the Deno target, and the DevTools Network tab now shows every fetch(), node:http / node:https client request, and WebSocket your program makes (including server-side WebSockets opened via Deno.upgradeWebSocket()), with request and response headers, status codes, bodies, and timing, exactly the way you’d see network traffic in a browser tab.

server.ts
const res = await fetch("https://api.github.com/repos/denoland/deno");
console.log(res.status, (await res.json()).stargazers_count);
$ deno run --inspect-wait --allow-net server.ts
Debugger listening on ws://127.0.0.1:9229/...
Visit chrome://inspect to connect to the debugger.
Deno is waiting for debugger to connect.

DevTools Network tab showing a `fetch()` request to api.github.com with the JSON response previewed inline.

Under the hood this required implementing the Network CDP domain on the inspector side and wiring fetch(), node:http, and WebSocket into it on the runtime side:

The same events also surface through node:inspector for programmatic clients and through any other CDP frontend (VS Code’s JavaScript debugger, the standalone chrome-devtools-frontend, etc.), so tooling that already speaks CDP against Node can attach to Deno without changes. Learn more about inspecting network traffic.

CPU profiling

Deno 2.8 ships a built-in CPU profiler that matches Node’s --cpu-prof flag, plus a few extras. Start the profiler with --cpu-prof and Deno writes a V8 CPU profile to disk when the program exits (#31909):

$ deno run --cpu-prof main.ts
$ ls
CPU.20260519.022823.34721.0.001.cpuprofile  main.ts

The .cpuprofile file opens directly in Chrome DevTools (Performance panel, then Load profile) or any tool that speaks the V8 profile format (eg. V8’s profview). For times when you don’t want to load a profile into a UI, two new output formats land alongside it:

  • --cpu-prof-flamegraph writes a self-contained, interactive SVG you can open in any browser, no extra tooling required (#32572).
  • --cpu-prof-md writes a human-readable Markdown report with the hottest functions, the call tree, and per-function details.

Combine them in a single run:

$ deno run --cpu-prof --cpu-prof-flamegraph --cpu-prof-md main.ts
$ ls
CPU.20260519.022823.34721.0.001.cpuprofile
CPU.20260519.022823.34721.0.001.svg
CPU.20260519.022823.34721.0.001.md

The Markdown report is the fastest way to triage a slow run from a terminal:

# CPU Profile

| Duration | Samples | Interval | Functions |
| -------: | ------: | -------: | --------: |
| 187.81ms |      74 |   1000us |        34 |

**Top 10:** `fib` 100.0%

## Hot Functions (Self Time)

|  Self% |    Self | Total% |   Total | Function | Location  |
| -----: | ------: | -----: | ------: | -------- | --------- |
| 100.0% | 72.00ms | 100.0% | 72.00ms | `fib`    | main.ts:1 |

The SVG flamegraph is interactive. Click any frame to zoom, hover to see exact timings:

Learn more about CPU profiling in Deno.

Package and workspace management

catalog: protocol

Monorepos that share dependency versions across packages used to require manual coordination: every member’s package.json had to be updated in lockstep when a shared dep was bumped. Deno 2.8 adopts pnpm’s catalog: protocol, letting you declare versions once in the workspace root and reference them by name from each member (#32947).

Declare a default catalog in the workspace root:

deno.json
{
  "workspace": ["./packages/api", "./packages/web"],
  "catalog": {
    "hono": "^4.6.0",
    "zod": "^3.23.0"
  }
}

Then reference it from any member with the bare catalog: specifier:

packages/api/package.json
{
  "name": "api",
  "dependencies": {
    "hono": "catalog:",
    "zod": "catalog:"
  }
}

For projects that need multiple catalogs (e.g. one for production, one for build tooling), use named catalogs under the plural catalogs field:

deno.json
{
  "workspace": ["./packages/api"],
  "catalogs": {
    "runtime": { "hono": "^4.6.0" },
    "tools": { "typescript": "^6.0.0" }
  }
}
packages/api/package.json
{
  "dependencies": { "hono": "catalog:runtime" },
  "devDependencies": { "typescript": "catalog:tools" }
}

catalog: also resolves correctly inside package.json overrides (#33799) and inside workspaces declared in the object form (#33816), so the protocol works the same way no matter where you reach for it. Learn more about the catalog: protocol.

Cross-platform npm installs

A lot of popular npm packages ship platform-specific native binaries via optionalDependencies: esbuild, sharp, rollup, the SWC family, and so on. Deno reads the os and cpu fields declared on each optional dependency, compares them to the host platform, and only fetches the binary you can actually run. Installs stay small, fast, and free of untrusted binaries from platforms you don’t ship to.

The trade-off shows up when you do want a different platform: building a Linux ARM64 Docker image from a macOS dev laptop, prepping a CI artifact for Windows, or pre-populating a cache for a deploy target. The new --os and --arch flags on deno install tell the resolver to pretend it’s on a different host, mirroring npm install --os --cpu (#32785):

$ deno install --os=linux --arch=arm64

Supported --os values: aix, android, darwin, freebsd, linux, openbsd, sunos, win32. Supported --arch values: arm, arm64, ia32, mips, mipsel, ppc, ppc64, s390, s390x, x64. Learn more about cross-platform installs.

--prod flag

Production deploys rarely need devDependencies or @types/* packages. Until now deno install always pulled them in anyway, padding the install size and adding npm packages you don’t actually ship. The new --prod flag skips both (#33248):

$ deno install --prod

Drop it into your Dockerfile or CI release step and your production image gets only the dependencies it needs to run. Learn more about --prod.

Hoisted node_modules

Deno’s default node_modules layout is isolated: each package gets its own symlink-resolved tree, so it can only see the dependencies it explicitly declared. That’s the right default for new projects, but some legacy npm tooling assumes the flat, hoisted layout that npm install produces, where every package lives at the top level of node_modules and can require() anything it finds.

deno.json gets a new nodeModulesLinker field for those cases (#32788):

deno.json
{
  "nodeModulesDir": "manual",
  "nodeModulesLinker": "hoisted"
}

Valid values are "isolated" (the default) and "hoisted". Use the latter when porting an existing Node project that relies on the npm-style layout. Learn more about isolated vs. hoisted node_modules.

.npmrc support

Several gaps in Deno’s .npmrc handling were closed this release.

min-release-age is the headline addition. The feature itself shipped in 2.6: Deno refuses to install a package version younger than the configured age, which catches the vast majority of npm supply-chain attacks before they land in your tree (malicious versions are typically detected and yanked within a few days of publishing). In 2.8 you can also configure it from .npmrc, matching the npm convention so existing tooling keeps working (#33983):

.npmrc
min-release-age=72h

The remaining improvements unblock common authentication and configuration scenarios with private registries:

  • certfile and keyfile for mutual-TLS authentication (#32655)
  • email field on _auth entries, used by some legacy on-prem registries (#32616)
  • NPM_CONFIG_REGISTRY correctly overrides the registry declared in .npmrc (#32394)

Learn more about .npmrc configuration.

file: and link: dependencies in npm packages

file: and link: specifiers point at a local path on the publisher’s machine and only make sense during development. Plenty of published npm packages still ship with one accidentally left in their package.json:

some-published-package/package.json
{
  "name": "some-package",
  "version": "1.2.3",
  "dependencies": {
    "lodash": "^4.17.0",
    "local-helpers": "file:../local-helpers"
  }
}

That stray file: entry used to break Deno with a cryptic error during resolution:

$ deno install
error: Invalid version requirement. Unexpected character.
  [email protected] -> local-helpers

In 2.8, file: and link: specifiers are silently skipped while parsing registry metadata, so packages that carry stray local-path deps install cleanly (#32876). The actual code those deps reference is bundled into the published tarball anyway, so nothing’s lost by ignoring them. Learn more about file: and link: dependencies.

--package-json flag

Projects migrating from Node often end up with both a package.json and a deno.json. By default, deno add, deno install, deno remove, and deno uninstall modify deno.json because that’s where Deno-native projects keep their dependencies. The new --package-json flag forces those subcommands to target package.json instead, useful when you want the npm-style manifest to remain the source of truth for your team’s tooling (#33199):

$ deno add --package-json express
$ deno install --package-json
$ deno remove --package-json lodash

Learn more about --package-json.

Bug fixes

Deno’s package management code is a moving target. Every release brings a lot of small correctness fixes, and 2.8 is no exception: 35 of them landed. A few worth calling out explicitly:

  • Peer dependency resolution got two meaningful improvements: a fix for cases where a peer dep ended up installed in multiple conflicting versions and caused hangs (#32358), and memoization of peer-cache hit checks that previously blew up combinatorially on large workspaces (#32609).
  • Aliased package.json dependencies ("my-name": "npm:foo@1") are now all linked into node_modules, not just the canonical one (#33068).
  • Global installs regenerate their lockfile correctly when you pass --force (#33970).
  • deno update --lockfile-only no longer rewrites your config file alongside the lockfile, restoring the contract of the --lockfile-only name (#33746).

deno compile updates

deno compile keeps moving toward “point it at a project, get a binary back” as the default workflow. Two new features cover most of that ground in 2.8, plus a batch of fixes for binaries that re-launch themselves the way many npm published CLIs do.

Framework detection

Running deno compile . (or deno compile ./myapp) now auto-detects the web framework you’re using, runs deno task build to produce build output, and generates the right entrypoint for it (#33164). The supported list covers most of the popular options: Next.js, Astro, Fresh, Remix, SvelteKit, Nuxt, SolidStart, TanStack Start, and Vite SSR.

$ deno compile .
Compile file:///project/main.ts to file:///project/myapp
Detected Vite SSR project
Running deno task build...
...

Entrypoints use import.meta.dirname so paths resolve against the virtual filesystem inside the compiled binary, which means a Next.js or Astro build shipped as a single executable now works without a separate runtime wrapper. Learn more about framework detection.

Other improvements

For projects with large npm dependency trees deno compile used to go silent for tens of seconds. It now reports progress through each phase (#33874): an animated progress bar in an interactive terminal, or per-phase log lines in CI and piped output. Operations that finish in under 120ms render nothing, so fast paths stay quiet.

A batch of fixes targets compiled npm CLIs that re-launch themselves (e.g. @google/gemini-cli). child_process.spawn and child_process.fork skip the Node-to-Deno CLI argument translation when running inside a standalone binary (#32980), the duplicate exe path is stripped from argv when a standalone binary relaunches itself (#33016), and process.argv[1] now resolves to Deno.execPath() in compiled binaries instead of the entrypoint URL (#32990). The self-extracting cache directory also moves to a hidden directory next to the executable (#32329) so it no longer clutters the binary’s parent directory.

A few smaller fixes round out the section: --env-file resolves parent-directory paths again and a missing env file no longer aborts deno compile (#32686); bundling CSS treats same-document fragment URLs as external (#33492); and Deno.bundle reports a clearer error when called from inside a compiled binary (#33503).

OpenTelemetry

Deno’s built-in OpenTelemetry integration gets two new exporters and a way to route permission audits straight into your OTel pipeline.

Console exporter

Set OTEL_EXPORTER_OTLP_PROTOCOL=console to print spans, logs, and metrics to stderr in a human-readable format. No collector required. Handy when you’re debugging instrumentation locally (#32717).

$ OTEL_DENO=true OTEL_EXPORTER_OTLP_PROTOCOL=console deno run -A main.ts
SPAN outer span [00000000000000000000000000000001/0000000000000001] Internal 1ms
  scope: example-tracer
SPAN inner span [00000000000000000000000000000001/0000000000000002] Internal 0ms
  parent: 0000000000000001
  scope: example-tracer
  key: value
LOG [INFO] "hello from inner"
  scope: deno
  trace: 00000000000000000000000000000001/0000000000000002

gRPC OTLP exporter

The OTLP exporter now speaks gRPC alongside the existing HTTP/protobuf transport. Point it at your collector’s gRPC port and you’re done (#30365).

$ OTEL_DENO=true \
  OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
  OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com:4317 \
  deno run -A main.ts

Permission audits as OTel logs

The permission audit log (introduced in 2.5) can now be fed straight into your OTel exporter. Set DENO_AUDIT_PERMISSIONS=otel and every permission check becomes an OTel log event correlated with the surrounding span, so you can alert on unexpected file or network access across your fleet without scraping JSONL files (#32501).

$ OTEL_DENO=true DENO_AUDIT_PERMISSIONS=otel deno run -A main.ts

Learn more about OpenTelemetry in Deno.

Other improvements

  • Span attributes copied from HTTP requests onto per-route metrics (#32720)
  • Array values supported in OTel attribute maps (#32748)
  • Server spans for 4xx responses no longer marked as errors (#32722)
  • log.iostream attribute added to console logs (#32723)
  • exception.* attributes added to OTel log records (#32726)

Testing and coverage

Deno’s built-in test runner and coverage tool both pick up a few useful improvements this release.

Sanitizers off by default

The sanitizeOps and sanitizeResources options on Deno.test() now default to false instead of true (#33250). These sanitizers fail a test when async ops or resources outlive it, and in practice they have been a frequent source of confusing failures, especially for code that uses setTimeout, node:http, or other APIs whose cleanup is loosely scoped. The new default matches what most people expect: tests pass when their assertions pass, and you opt back into the stricter behavior when you actually want it.

You can re-enable sanitizers for a single test:

leak_test.ts
Deno.test(
  "leaks a timer",
  { sanitizeOps: true, sanitizeResources: true },
  () => {
    setTimeout(() => {}, 1000);
  },
);

…for every test in a file via the new module-level API:

strict_test.ts
// Apply to every Deno.test() in this file.
Deno.test.sanitizer({ ops: true, resources: true });

Deno.test("leaks a timer", () => {
  setTimeout(() => {}, 1000);
});

…or globally in deno.json:

{
  "test": {
    "sanitizeOps": true,
    "sanitizeResources": true
  }
}

Learn more about test sanitizers.

Per-test timeouts

Deno.test() now accepts a timeout option (in milliseconds) that fails a test if it runs longer than expected, instead of hanging your CI run (#33815):

slow_test.ts
Deno.test("slow operation", { timeout: 100 }, async () => {
  await new Promise((r) => setTimeout(r, 500));
});
$ deno test
slow operation ... FAILED (102ms)

 ERRORS

slow operation => ./server_test.ts:5:6
error: Test timed out after 100ms.

The timeout is per-test, so a slow test no longer drags down everything else in the suite. Pair it with --parallel to keep total wall-clock time predictable. Learn more about test timeouts.

Function coverage

deno coverage now reports per-function coverage alongside line and branch coverage in both the text summary and the HTML report (#32507). Useful when a file has high line coverage but most of its API surface is untested:

$ deno test --coverage=cov
$ deno coverage cov
| File      | Branch % | Function % | Line % |
| --------- | -------- | ---------- | ------ |
| math.ts   |    100.0 |       66.7 |   66.7 |
| All files |    100.0 |       66.7 |   66.7 |

In this example two of three exported functions in math.ts are tested: line and function coverage agree here, but the new column makes it obvious at a glance which functions are missing tests. Line percentage alone can hide that when a file has a few large untested functions. Learn more about function coverage.

Other fixes

  • deno test dedupes test modules discovered through multiple workspace members so each test only runs once (#32380)
  • deno test --watch restarts the suite when an --env-file changes, matching the watch behavior for source edits (#32461)
  • Coverage line and branch counts are now correct in edge cases involving partially overlapping zero-count ranges (#32312)
  • deno coverage warns instead of erroring out when a source file referenced by the profile is missing, so a stale data file doesn’t kill the whole report (#32398)

Web APIs

Deno 2.8 keeps closing the gap with the browser platform, with two new APIs, expanded Web Crypto coverage, and a long tail of fixes and perf wins across fetch, streams, TextEncoder/TextDecoder, and friends.

Canvas and geometry primitives

Two browser-platform APIs that server-side code has long been asking for: OffscreenCanvas (#29357) lands as a stable global, and the Geometry Interfaces Module Level 1 spec is implemented behind --unstable-webgpu (#27527).

OffscreenCanvas is the same API the browser exposes for off-thread canvas work: create a canvas without a DOM, get a rendering context, and transfer it between workers via postMessage. Deno’s implementation supports the "bitmaprenderer" and "webgpu" contexts (the "2d" and WebGL contexts are not implemented and getContext returns null for them). A common pattern is to decode an image, place the resulting ImageBitmap on the canvas, and encode the canvas back out via convertToBlob:

thumb.ts
const res = await fetch(
  "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=480",
);
const bitmap = await createImageBitmap(await res.blob());
const { width, height } = bitmap;

const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("bitmaprenderer")!;
ctx.transferFromImageBitmap(bitmap); // consumes the bitmap

const out = await canvas.convertToBlob({ type: "image/png" });
await Deno.writeFile("out.png", new Uint8Array(await out.arrayBuffer()));
$ deno run --allow-net --allow-write=. thumb.ts

That covers headless format conversion, thumbnail generation, and social-card rendering without a headless browser. Pair it with the "webgpu" context for off-window GPU-rendered targets.

The geometry interfaces add DOMPoint, DOMRect, DOMQuad, and DOMMatrix (plus their Readonly variants). These are the same matrix and rectangle types the browser uses for transforms and hit-testing, and they’re the missing primitive for code that wants to share geometry math between the browser and Deno:

geometry.ts
const m = new DOMMatrix()
  .translate(100, 50)
  .rotate(30)
  .scale(2);

const p = new DOMPoint(10, 0).matrixTransform(m);
console.log(p.x, p.y); // 117.32050807568878 60

const r = new DOMRect(0, 0, 100, 50);
console.log(r.right, r.bottom); // 100, 50
$ deno run --unstable-webgpu geometry.ts

Learn more about OffscreenCanvas and Geometry Interfaces.

Cloneable and transferable values

The web platform draws a careful line between two ways of moving JavaScript values across realm and worker boundaries: serializable objects can be deep-copied with structuredClone, and transferable objects can have their ownership moved with zero copy. Deno 2.8 closes long-standing gaps in both, so you can finally write efficient multi-threaded programs without serializing state to JSON by hand.

The headline fix is that non-serializable Web types are finally transferable when correctly listed in the transfer array of structuredClone or postMessage (#33491):

transfer.js
const stream = new ReadableStream({
  start(c) {
    c.enqueue("hello");
    c.close();
  },
});

// Before 2.8: threw DataCloneError.
// 2.8: ownership of `stream` moves into the cloned value.
const cloned = structuredClone({ stream }, { transfer: [stream] });

worker.postMessage({ stream: cloned.stream }, [cloned.stream]);

The full set of types Deno can now transfer is Headers, Request, Response, ReadableStream, WritableStream, and TransformStream. The full set of types Deno can now serialize (deep-copy) is Blob, File, CryptoKey, DOMException, X509Certificate, EventLoopDelayHistogram, and RecordableHistogram.

Learn more about structuredClone and transferable objects.

Other fixes

  • crypto.subtle.digest accepts "SHA3-256", "SHA3-384", and "SHA3-512" (#32342).
  • P-521 (the largest NIST curve) gains first-class support across sign, verify, and ECDH derive (#32602), with EC key export fixed for all formats (#32412, #34087). Importing X25519, X448, and Ed25519 raw keys also validates their length now instead of silently accepting any byte string (#33944).
  • CacheStorage.keys() and Cache.keys() are implemented (#33275), filling in the iteration entry points on the Cache API.
  • AbortSignal.any() no longer leaks memory when wrapping long-lived signals (#32916).
  • subtle.importKey no longer panics on a wrong algorithm name (#32410); it throws as specified.
  • getRandomValues throws TypeMismatchError for non-TypedArray inputs (#33470).
  • fetch retries on stale pooled HTTP/1.1 connections (#32566) and stops mutating the caller’s options in Deno.createHttpClient (#33497). The response resource is closed cleanly when an abort races op completion (#33928), and Node Readable request bodies stream through a byte ReadableStream (#33432).
  • Deno.listenDatagram defaults its hostname to 0.0.0.0 (#33496), matching Deno.listen.
  • HTTP upgrade handlers can now reject with non-101 status codes (#32615), an empty Host header is treated as missing (#33234), and a WebSocket H2 stream reset no longer panics (#33982).
  • MessageEvent ports convert via the WebIDL sequence iteration protocol (#33652), and source is now retained from MessageEventInit (#33500).
  • A few Node-aligned error code touch-ups: ERR_MISSING_ARGS on URL.revokeObjectURL() (#33471), ERR_ILLEGAL_CONSTRUCTOR (#33535) and ERR_INVALID_THIS (#33467) on the corresponding TypeErrors, and AbortSignal.timeout’s error message matches Node’s (#33460).
  • console.log supports the %j JSON format specifier (#32684), and console.dirxml now routes through the log printer (#33443).
  • QuotaExceededError is upgraded to a DOMException-derived interface (#32244), removeEventListener handles a null options argument (#32605), and Event.returnValue respects the cancelable and passive flags (#33651).
  • ReadableStreamBYOBRequest.view is narrowed to Uint8Array (#33477), a late write racing with TransformStream.cancel no longer hangs (#33478), and a WebTransport datagram-overflow infinite loop is fixed (#33075).
  • GPUQueue.writeBuffer() accepts plain ArrayBuffer data sources (#33152).
  • WebSocket response headers handle non-ASCII bytes correctly (#32594).

Task runner

deno task got a small quality-of-life improvement that matters once you start running tasks in parallel: every output line is now prefixed with the task name that produced it, so interleaved stdout from concurrent tasks no longer reads as one tangled stream (#33805). Given a config that fans out to two builds:

deno.json
{
  "tasks": {
    "client": "echo building client && sleep 1 && echo client ready",
    "server": "echo building server && sleep 1 && echo server ready",
    "dev": { "dependencies": ["client", "server"] }
  }
}

…running deno task dev interleaves the two scripts cleanly:

$ deno task dev
Task client echo building client && sleep 1 && echo client ready
[client] building client
Task server echo building server && sleep 1 && echo server ready
[server] building server
[client] client ready
[server] server ready
deno task fanning out to three child tasks in parallel, with each output line prefixed and color-coded by task name.

The prefixes are color-coded per task and stay attached even when a task forks subprocesses, so a parallel build + test + lint workflow stays legible without piping everything through a separate multiplexer.

The task shell itself also picks up a couple of POSIX features: set -e / set -o errexit (and set +e to turn it back off) aborts the surrounding sequential list on the first non-zero exit, and the POSIX null command : is now a builtin. Both make it easier to port shell scripts into tasks blocks without resorting to a separate bash -c wrapper.

The rest of the changes are workspace fixes that make --filter and recursive task discovery match user expectations:

  • --filter now matches a workspace member by directory name as well as package name (#33499), and falls back to the directory name only when an explicit workspace root check passes (#33540)
  • Recursive task name completion works inside workspaces, so shell tab-completion sees tasks declared in any member (#32422)
  • Backticks in arguments forwarded to a task are escaped correctly instead of being re-evaluated by the task shell (#34151)

Learn more about task dependencies and parallel output.

deno upgrade changes

Two improvements to deno upgrade worth flagging: it’s a lot smaller over the wire now, and you can use it to grab a Deno build straight from a PR.

Delta updates

deno upgrade now downloads a binary diff between your current version and the target instead of the full release archive, when one is available (#33274). A typical patch-level upgrade goes from a roughly 48 MB download to 3-6 MB, an 87-93% bandwidth reduction. Useful for everyone, but especially for CI runners and ephemeral environments that pull Deno on every job.

The implementation chains up to three bsdiff patches to reach the target (e.g. 2.7.14 to 2.8.0 to 2.8.1 to 2.8.2). Every patch and every intermediate binary is SHA-256-checked against the published checksums, so a bad delta can’t quietly corrupt your install. If any step fails (missing delta asset, checksum mismatch, patch error), Deno transparently falls back to a full download. The --no-delta flag forces the full path if you ever want to bypass deltas entirely. Learn more about delta updates.

Install from a PR

deno upgrade pr <number> downloads the binary that CI built for a given pull request and replaces your local deno with it, so you can try a fix or new feature without building from source (#33252). It uses the gh CLI under the hood, so you need it installed and authenticated:

# Try the binary CI built for PR #34227
$ deno upgrade pr 34227

# Save to a path instead of replacing your current install
$ deno upgrade --output ./deno-pr pr 34227

# See what would happen without doing it
$ deno upgrade --dry-run pr 34227

Under the hood deno upgrade queries gh pr view, walks the PR’s CI runs to find the right artifact for your platform (preferring release over debug), downloads it, sanity-checks it with deno -V, then swaps it in. Faster than a git clone && cargo build round-trip when you just want to verify a fix. Learn more about installing a build from a PR.

Module loader hooks

Technically a piece of Node.js API compatibility, but big enough to deserve its own section: Deno 2.8 implements Node’s module.registerHooks() API (#34081). Loader hooks let you customize module loading at runtime: intercept resolution, transform source, redirect specifiers, mock dependencies for tests, or add instrumentation, all without rebuilding Deno or running a separate build step.

As a concrete example, here’s a 14-line loader that teaches Deno how to import a .css file as the stylesheet text:

css-loader.ts
import module from "node:module";

module.registerHooks({
  load(url, context, nextLoad) {
    if (!url.endsWith(".css")) {
      return nextLoad(url, context);
    }
    const css = Deno.readTextFileSync(new URL(url));
    return {
      format: "module",
      source: `export default ${JSON.stringify(css)};`,
      shortCircuit: true,
    };
  },
});
button.css
.button {
  padding: 8px 16px;
  border-radius: 4px;
  background: #66c2ff;
}

Pre-load the hooks with --import so they register before user code runs:

main.ts
import buttonCss from "./button.css";

console.log(buttonCss);
$ deno run --allow-read --import ./css-loader.ts main.ts
.button {
  padding: 8px 16px;
  border-radius: 4px;
  background: #66c2ff;
}

You could also get a CSS file as text via the standard import attributes proposal (import css from "./button.css" with { type: "text" }) that we shipped back in Deno v2.4 . The point of the example above isn’t .css specifically; it’s showing the mechanics of writing a custom loader for arbitrary file types or transformations.

Loader hooks also work inside binaries produced by deno compile, which makes them useful for distributing self-contained CLIs that ship with custom resolution baked in. Here’s a virtual-module loader that exposes a virtual:greeting specifier from inside the compiled binary:

greeter.ts
import module from "node:module";

module.registerHooks({
  resolve(specifier, context, nextResolve) {
    if (specifier !== "virtual:greeting") {
      return nextResolve(specifier, context);
    }
    return { url: "virtual:greeting", shortCircuit: true };
  },
  load(url, context, nextLoad) {
    if (url !== "virtual:greeting") return nextLoad(url, context);
    return {
      format: "module",
      source: `export default "Hello from a virtual module!";`,
      shortCircuit: true,
    };
  },
});

const { default: greeting } = await import("virtual:greeting");
console.log(greeting);
$ deno compile -A --output greeter greeter.ts
$ ./greeter
Hello from a virtual module!

We chose not to implement the module.register() API, since Node has deprecated it and plans to remove it.

Learn more about module loader hooks in Deno.

setTimeout and setInterval

A change we’ve been planning for a long time finally lands in 2.8: setTimeout and setInterval now return Node’s Timeout object instead of an opaque number, matching node:timers behavior at the global scope (#33249).

This is technically a breaking change, but in practice it should affect very few programs. The common case keeps working untouched:

timer.ts
const t = setTimeout(() => console.log("hi"), 1000);
clearTimeout(t);

The only code that breaks is code that relied on the return value being a number, e.g. storing it in a number-typed field, doing arithmetic on it, or runtime-checking its type:

timer_id.ts
// Before 2.8: id was a number
const id: number = setTimeout(fn, 0);

// 2.8: assign to NodeJS.Timeout instead
const id: NodeJS.Timeout = setTimeout(fn, 0);
timer_check.ts
// Before 2.8: timer was a number, this branch ran
if (typeof timer === "number") {
  clearTimeout(timer);
}

// 2.8: timer is a NodeJS.Timeout object, branch never runs.
// Just clear unconditionally:
clearTimeout(timer);

Why is it worth even this small amount of churn? Three reasons:

  1. Performance. The compatibility shim we used to wrap web-style setTimeout around node:timers sat on a hot event-loop path. Removing it cuts overhead on every timer call.
  2. Tech debt. Maintaining two parallel timer implementations (web and Node) plus a global proxy that converted between them was a constant source of subtle bugs.
  3. Simpler mental model. There is now one global setTimeout in Deno, it behaves the way node:timers does, and the same Timeout object flows through both styles of code. The global proxy that used to intercept these calls is gone.

The NodeJS.Timeout type used above resolves out of the box, thanks to lib.node now being included by default.

Miscellaneous

A few smaller changes worth flagging that don’t fit neatly under any of the sections above:

  • The NODE_EXTRA_CA_CERTS environment variable is now honored at the root certificate store level (#33148), so extra CA certificates apply to every TLS code path: fetch(), Deno.connectTls(), node:https, and node:tls. Useful behind corporate MITM proxies or when you need to trust an internal CA across a whole script.
  • Linux release builds run again on systems with glibc older than 2.27 (#33259), restoring compatibility with a long tail of older distros and CI images.
  • deno compile configuration grows include and exclude fields, so you can declare bundled data files in deno.json instead of repeating --include flags on every invocation (#33024, docs).
  • Several additional Unix-style signals (SIGUSR1, SIGUSR2, and others) are now usable on Windows for Node compatibility (#32689).
  • deno x accepts a --package / -p flag for the case where the package name differs from the binary name (e.g. running tsc from npm:typescript) (#32855).
  • deno eval auto-detects CommonJS vs ES module syntax in the input snippet, so quick one-liners work without picking a flag (#32472).
  • Deno.upgradeWebSocket now wires up cleanly to node:http upgrade events, so Node-style servers can hand off WebSocket connections to Deno’s native implementation (#33342).
  • Deno.watchFs paths are normalized so events no longer carry leading ./ segments that broke string-equality checks against the watched paths (#33490).
  • FFI calls hold onto their argument backing stores for the duration of the call, preventing a use-after-free when async FFI work outlives the caller’s stack frame (#32775).
  • The event loop now wakes correctly when V8 posts foreground tasks from background threads, fixing a class of subtle hangs around async module evaluation (#32450).
  • deno_graph is bumped to 0.108.2 for proper handling of WebAssembly multi-value return types (#34070).
  • deno doc learns to render npm packages (#32435), with follow-up fixes for npm entrypoints that ship without types (#34147). The HTML output also picks up Prism highlighting for JSX/TSX code blocks (#33255) and cleaner operator rendering in dark mode (#33267).
  • --watch restarts now send SIGTERM and dispatch unload / process.exit before tearing the process down, so cleanup handlers actually run (#32564, #32664). The grace period before the hard kill drops from 5 seconds to 500 milliseconds (#33099), and --watch-exclude patterns are now respected for every file change event, not just the initial scan (#33854).
  • Text imports (with { type: "text" }) are stable and no longer require --unstable-raw-imports (#34238, introduced in 2.4). Bytes imports (with { type: "bytes" }) remain behind the flag. Learn more about import attributes.
  • deno publish no longer panics during provenance generation when run on CI providers other than GitHub (#33802).
  • Vite’s import.meta.hot references now type-check correctly (#32127).

V8 14.9

Deno 2.8 upgrades the V8 engine from 14.6 to 14.9 (#34226).

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.8: Amol Yadav, Ashwin Naren, Avocet, BitToby, Dan Dascalescu, Daniel Osvaldo Rahmanto, Daniil Sivak, David Sherret, em, Felipe Cardozo, Fibi, Hajime-san, Hunnyboy1217, Janosh Riebesell, Jean Ibarz, Jimmy, John L. Carveth, Josh Fleming, kaju, Kenta Moriuchi, Kit Dallege, KnorpelSenf, KT, Kyle Kelley, Lach, Leo Zaki, lif, Luca Barbato, Luna, Marvin Hagemeister, Michael Horstmann, Nayeem Rahman, Nik B, Olivér Falvai, Pietro Marchini, r3wretrhy, Rano | Ranadeep, Rohan Santhosh Kumar, RoomWithOutRoof, Shivam Tiwari, tmimmanuel, Varun Chawla, and web-dev0521.

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.8. You can view the full list of pull requests merged in Deno 2.8 on GitHub.

That’s all for 2.8, thanks for reading and see you in the next release.

Discussion (0)

Sign in to join the discussion. Free account, 30 seconds — email code or GitHub.

Sign in →

No comments yet. Sign in and be the first to say something.

More from Hacker News — AI on Front Page