diff --git a/.changeset/adapter-node-unresolvable-imports.md b/.changeset/adapter-node-unresolvable-imports.md new file mode 100644 index 000000000000..e8a40ce9c174 --- /dev/null +++ b/.changeset/adapter-node-unresolvable-imports.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +fix: fail the build when the output contains imports that resolve to no installed package diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 07df66d08aa5..078e77e5249c 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -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); @@ -137,7 +138,7 @@ export default function (opts = {}) { ] }); - await bundle.write({ + const bundled = await bundle.write({ dir: out, format: 'esm', sourcemap: true, @@ -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`,