Skip to content

adapter-node 5.5.6: manualChunks hoists an unresolvable conditional require into the boot entry, so the server dies with ERR_MODULE_NOT_FOUND #16653

Description

@Stadly

Describe the bug

Starting with @sveltejs/adapter-node@5.5.6, a production build can contain a top-level static import of a package that is not installed anywhere, in build/instrumentation.server.js — the very first thing build/index.js imports. The server never starts:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@babel/preset-typescript' imported from /app/build/instrumentation.server.js
    at Object.getPackageJSONURL (node:internal/modules/package_json_reader:301:9)
    at packageResolve (node:internal/modules/esm/resolve:768:81)

The offending line in the emitted bundle is a bare side-effect import:

// build/instrumentation.server.js
import '@babel/preset-typescript/package.json';

Nothing in the dependency tree installs @babel/preset-typescript. The import comes from a require that CommonJS code guards behind a catch block and never reaches at runtime — @rollup/plugin-commonjs hoists it to a static ESM import, tree-shaking then removes the surrounding code but keeps the now-dangling side-effect import, and nodeResolve leaves it external because it cannot resolve it. Because adapter 5.5.5 moved this file to the build root and put it on the boot path, an import that was previously inert now kills the process before it listens.

This took down one of our deployments: the image builds fine, the container starts, runs its migrations, and then exits on the trace above with nothing else logged.

Reproduction

Minimal app, verified from scratch. Four files plus a package.json:

package.json

{
  "name": "repro-adapter-node-babel",
  "private": true,
  "type": "module",
  "scripts": { "build": "vite build" },
  "overrides": { "@sentry/vite-plugin": "5.3.0" },
  "devDependencies": {
    "@sentry/sveltekit": "10.63.0",
    "@sveltejs/adapter-node": "5.5.6",
    "@sveltejs/kit": "2.70.2",
    "@sveltejs/vite-plugin-svelte": "^7.2.0",
    "svelte": "^5.56.4",
    "vite": "^8.1.3"
  }
}

svelte.config.js

import adapter from "@sveltejs/adapter-node";

export default {
  kit: {
    adapter: adapter(),
    experimental: { instrumentation: { server: true } },
  },
};

vite.config.js

import { sveltekit } from "@sveltejs/kit/vite";

export default { plugins: [sveltekit()] };

src/instrumentation.server.js

import * as Sentry from "@sentry/sveltekit";

if (process.env.SENTRY_ENABLED === "true") {
  Sentry.init({ dsn: process.env.SENTRY_DSN });
}

Plus a trivial src/app.html and src/routes/+page.svelte. Then:

npm install
npx vite build
grep -n "preset-typescript" build/instrumentation.server.js
# 22:import '@babel/preset-typescript/package.json';

node build/index.js
# Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@babel/preset-typescript'
#   imported from .../build/instrumentation.server.js

The @sentry/vite-plugin override is only there to hold the transitive dependency at the version that still pulls @babel/core (5.3.0, via @sentry/bundler-plugin-core); 5.4.0 swapped that out, which incidentally hides the bug. Sentry is not special here — any dependency reachable from instrumentation.server.js that contains a guarded require of an uninstalled package will do.

Bisect

Same app, same lockfile, only the adapter version varying:

adapter-node instrumentation output path files with the dangling import node build/index.js
5.5.4 build/server/instrumentation.server.js 0 boots
5.5.5 build/instrumentation.server.js 0 boots
5.5.6 build/instrumentation.server.js 2 ERR_MODULE_NOT_FOUND
5.5.7 build/instrumentation.server.js 2 ERR_MODULE_NOT_FOUND

So it is 5.5.6, not the 5.5.5 restructure, that introduces the dangling import. 5.5.5 (#16069) moved the file to the build root and onto the boot path; 5.5.6 added the manualChunks option in #16115 — the fix for the #16092 top-level-await deadlock:

// packages/adapter-node/index.js
manualChunks(id) {
    if (id.startsWith(server)) {
        return id.slice(server_path_length);
    }
}

Forcing every module under the Vite server directory into its own chunk changes where rollup places external imports, and the dangling side-effect import ends up hoisted into the entry chunks — including the one the server boots from.

This is the whole of the functional 5.5.5 → 5.5.6 diff: the only other change in index.js is extracting ${tmp}/entries into an entries local. So manualChunks is not merely correlated with the regression, it is the sole candidate.

Why the import exists at all

The chain in the repro is instrumentation.server.js@sentry/sveltekit (whose server entry re-exports the sentrySvelteKit Vite plugin) → @sentry/vite-plugin@sentry/bundler-plugin-core@babel/core. Inside Babel:

// @babel/core/lib/config/files/module-types.js:165
} catch (error) {
    const packageJson = require("@babel/preset-typescript/package.json");
    if (_semver().lt(packageJson.version, "7.21.4")) { ... }
    throw error;
}

@babel/core declares @babel/preset-typescript neither as a dependency nor as a peer — it is a soft runtime lookup that is legitimately absent, which is why the require is guarded. The adapter marks only pkg.dependencies as external, so everything else is bundled, and this guarded require becomes a hard static dependency of the boot entry.

Worth noting the emitted import targets a .json file with no import attribute, so even installing @babel/preset-typescript would not make it loadable — under Node ESM it then fails with ERR_IMPORT_ATTRIBUTE_MISSING: Module ... needs an import attribute of "type: json" (verified separately). There is no user-side dependency fix available; the emitted import is invalid regardless of what is installed.

Suggested directions

  1. Don't let unresolved externals reach the instrumentation entry. Since it is --imported before anything else, an unresolvable specifier there is always fatal. Failing the build (or warning loudly) when the instrumentation chunk has an external that nodeResolve could not resolve would turn a silent production crash into a build error.
  2. Revisit the manualChunks shape from fix: avoid circular dependency #16115. The deadlock fix is doing more than retaining file structure — it is changing external-import placement. Narrowing it, or excluding the instrumentation entry from it, may address both.
  3. test(adapter-node): boot the built output when instrumentation.server.js is present #16305 would have caught this. It adds an adapter-node test that actually boots build/index.js with an instrumentation file present; that gap is exactly why a fatal boot regression shipped in a patch release. Landing a boot test looks more valuable than any single fix here.

Related

Severity

Silent and total: the build succeeds, the image builds, and the container exits at boot. Nothing in the build output flags an external that resolves nowhere.

System info

@sveltejs/kit: 2.70.2
@sveltejs/adapter-node: 5.5.6 / 5.5.7 (5.5.4, 5.5.5 unaffected)
vite: 8.1.3
svelte: 5.56.4
node: v24.17.0
npm: 11.13.0
OS: Linux 6.6.87.2 (WSL2)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions