Problem
loadRaw reads a file without checking that it exists. The read of a missing path yields empty content instead of failing, detectFormat classifies empty content as CJS, and the CJS transform wraps zero bytes into a syntactically valid module whose default export is {}. A missing file therefore becomes a working-looking module with no exports, and the failure surfaces far from its cause.
This is not confined to the request-time server. walkFrom calls loadRaw as well, so an empty module can be emitted into a static build tree — where it cannot be repaired at serve time.
Current behavior
tailwindcss@4.3.3 ships no defaultTheme.js; its root-level files are exactly index.css, package.json, preflight.css, theme.css, utilities.css.
GET /tailwindcss@4.3.3/defaultTheme.js -> 200, 433 bytes
const __ns = {
};
const module = { exports: {} };
const require = (s) => { ... };
Default export {}. No error, no 404.
The same path without an extension is correct:
GET /tailwindcss@4.3.3/defaultTheme -> 404
That asymmetry is the diagnosis. An extensionless id is not a module file, so it is served through rawBytes, which guards both branches — ctx.files.exists for project files, ctx.cache.exists for package files — and returns undefined. A .js id takes the transform path through loadRaw, fifteen lines above rawBytes in the same file, which guards neither.
The change
In packages/webrun-modules/src/preprocess/resolve.ts:
loadRaw guards both branches, mirroring rawBytes. On a miss, consult ctx.onMissingFile?.(id): a returned string is used as the source; undefined, or no callback, throws ModuleResolveError.
ensurePackage validates the manifest's own claim. When resolveEntry names a file the package does not ship, throw an error naming the package, the declared subpath and its declared target, so a malformed exports map is not mistaken for a webrun-modules fault.
- New option
onMissingFile?: (id: string) => string | undefined | Promise<string | undefined> on ModuleServerOptions and ProjectBuildOptions, carried on PreprocessContext. Absent by default — strict. It is the escape hatch for a consumer whose build this change turns red.
resolveRawFile is deliberately unchanged: it stays a probe helper, and the error belongs where the file is needed and the id is in hand.
Acceptance criteria
Out of scope
Making exports-mapped URLs such as /tailwindcss@4.3.3/defaultTheme serve content or redirect:
- Specifiers already work. A project file importing
tailwindcss/defaultTheme emits ./~deps/tailwindcss/defaultTheme.js, whose proxy re-exports from tailwindcss@4.3.3/dist/default-theme.mjs. resolveEntry honours the map wherever output is produced.
- The URL form cannot work under static serving. Output is served by a plain static HTTP server with no server-side logic. No file exists at that path, and a static server would give an extensionless file a non-JS content-type, which browsers refuse to execute as a module.
The honest 404 that #8 asked for needs no separate work: once loadRaw throws, the fetch handler's existing catch returns 404.
Note
Accepted risk: builds that currently report success while emitting empty modules will start failing. That success is a lie, and a static tree containing an empty module cannot be repaired at serve time. onMissingFile exists for anyone who needs to unblock in a hurry.
Problem
loadRawreads a file without checking that it exists. The read of a missing path yields empty content instead of failing,detectFormatclassifies empty content as CJS, and the CJS transform wraps zero bytes into a syntactically valid module whose default export is{}. A missing file therefore becomes a working-looking module with no exports, and the failure surfaces far from its cause.This is not confined to the request-time server.
walkFromcallsloadRawas well, so an empty module can be emitted into a static build tree — where it cannot be repaired at serve time.Current behavior
tailwindcss@4.3.3ships nodefaultTheme.js; its root-level files are exactlyindex.css,package.json,preflight.css,theme.css,utilities.css.Default export
{}. No error, no 404.The same path without an extension is correct:
That asymmetry is the diagnosis. An extensionless id is not a module file, so it is served through
rawBytes, which guards both branches —ctx.files.existsfor project files,ctx.cache.existsfor package files — and returnsundefined. A.jsid takes the transform path throughloadRaw, fifteen lines aboverawBytesin the same file, which guards neither.The change
In
packages/webrun-modules/src/preprocess/resolve.ts:loadRawguards both branches, mirroringrawBytes. On a miss, consultctx.onMissingFile?.(id): a returned string is used as the source;undefined, or no callback, throwsModuleResolveError.ensurePackagevalidates the manifest's own claim. WhenresolveEntrynames a file the package does not ship, throw an error naming the package, the declared subpath and its declared target, so a malformedexportsmap is not mistaken for a webrun-modules fault.onMissingFile?: (id: string) => string | undefined | Promise<string | undefined>onModuleServerOptionsandProjectBuildOptions, carried onPreprocessContext. Absent by default — strict. It is the escape hatch for a consumer whose build this change turns red.resolveRawFileis deliberately unchanged: it stays a probe helper, and the error belongs where the file is needed and the id is in hand.Acceptance criteria
GET /{pkg}@{ver}/{missing}.jsreturns 404 instead of 200 with an empty moduleGET /{pkg}@{ver}/{missing}still returns 404 — regression guard on behaviour that is correct todayonMissingFilereturning a string supplies the source; returningundefinedthrowsexportsnames an unshipped file produces an error identifying the package and the declared targetOut of scope
Making exports-mapped URLs such as
/tailwindcss@4.3.3/defaultThemeserve content or redirect:tailwindcss/defaultThemeemits./~deps/tailwindcss/defaultTheme.js, whose proxy re-exports fromtailwindcss@4.3.3/dist/default-theme.mjs.resolveEntryhonours the map wherever output is produced.The honest 404 that #8 asked for needs no separate work: once
loadRawthrows, the fetch handler's existingcatchreturns 404.Note
Accepted risk: builds that currently report success while emitting empty modules will start failing. That success is a lie, and a static tree containing an empty module cannot be repaired at serve time.
onMissingFileexists for anyone who needs to unblock in a hurry.