Skip to content

webrun-modules: a missing file must fail, not become an empty module #9

Description

@mkotelnikov

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:

  1. 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.
  2. 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.
  3. 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

  • GET /{pkg}@{ver}/{missing}.js returns 404 instead of 200 with an empty module
  • GET /{pkg}@{ver}/{missing} still returns 404 — regression guard on behaviour that is correct today
  • A project file importing a nonexistent relative path fails the build instead of emitting an empty artifact
  • onMissingFile returning a string supplies the source; returning undefined throws
  • A package whose exports names an unshipped file produces an error identifying the package and the declared target
  • No emitted artifact is an empty module
  • The pending changeset records the behaviour change and the new option

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreviewedContent reviewed and approved (dev-workflow Gate 1)status:ready-for-devSelected for development (dev-workflow Gate 2)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions