Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/adapter-node-unresolvable-imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-node': patch
---

fix: fail the build when the output contains imports that resolve to no installed package
39 changes: 37 additions & 2 deletions packages/adapter-node/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { createRequire, isBuiltin } from 'node:module';
import { extname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { rolldown } from 'rolldown';

const files = fileURLToPath(new URL('./files', import.meta.url).href);
Expand Down Expand Up @@ -137,7 +138,7 @@ export default function (opts = {}) {
]
});

await bundle.write({
const bundled = await bundle.write({
dir: out,
format: 'esm',
sourcemap: true,
Expand All @@ -155,6 +156,40 @@ export default function (opts = {}) {
}
});

// Anything not in `dependencies` is bundled, so a bare import left in the output
// resolves against a deployment that will not contain it. That is fatal but silent:
// the build succeeds and the server dies as the module is evaluated, which for
// `instrumentation.server.js` is before it can log anything at all.
const require_from_project = createRequire(pathToFileURL('package.json'));
const emitted = new Set(bundled.output.map((chunk) => chunk.fileName));

/** @type {string[]} */
const unresolvable = [];

for (const chunk of bundled.output) {
if (chunk.type !== 'chunk') continue;

for (const source of [...chunk.imports, ...chunk.dynamicImports]) {
// imports of our own chunks are relative but reported without a leading `./`
if (emitted.has(source) || /^[./]/.test(source) || isBuiltin(source)) continue;

try {
require_from_project.resolve(source);
} catch {
unresolvable.push(` ${source} (imported by ${chunk.fileName})`);
}
}
}

if (unresolvable.length > 0) {
throw new Error(
'The build contains imports that resolve to no installed package, so the server ' +
`would fail to start with ERR_MODULE_NOT_FOUND:\n${unresolvable.join('\n')}\n\n` +
'If these are optional dependencies, add them to "dependencies" so they are ' +
'installed alongside the build, or stop importing them.'
);
}

if (builder.hasServerInstrumentationFile()) {
builder.instrument({
entrypoint: `${out}/index.js`,
Expand Down
Loading