Skip to content

fix: keep dot runs out of chunk and asset file names - #393

Open
everton-dgn wants to merge 6 commits into
solidjs:nextfrom
everton-dgn:fix/route-chunk-dot-runs
Open

everton-dgn wants to merge 6 commits into
solidjs:nextfrom
everton-dgn:fix/route-chunk-dot-runs

Conversation

@everton-dgn

Copy link
Copy Markdown

Fixes #391

Problem

A catch-all route module like [...404].tsx builds to _...404_-<hash>.js: the bundler's default sanitizeFileName swaps the brackets and keeps the dots. Vite names the route's CSS after the chunk, so it comes out as _..-<hash>.css. Any server, CDN or middleware that refuses URLs containing .. refuses both files, and the lazy route fails to hydrate.

Fix

The main plugin wraps output.sanitizeFileName in a post-order outputOptions hook: the configured sanitizer runs first (or the default rule, when none is set), then runs of dots in the last segment of the name collapse to one. [...404] now builds to _.404_-<hash>.js.

  • sanitizeFileName rather than chunkFileNames, because it also covers assets such as the CSS file above.
  • order: 'post', so a sanitizer set by another plugin's outputOptions hook gets wrapped instead of replacing the wrapper.
  • The server build is wrapped too. The server bundle writes the URLs of the assets it imports, so collapsing on the client only would point server-rendered src/href attributes at files the client never wrote (an imported mark..svg became mark-<hash>.svg on the client while the SSR markup said mark.-<hash>.svg).
  • Only the last segment is touched. With preserveModules the name carries the module's directories, ../ included, and changing those makes Rolldown reject the name.
  • A run collapses to one dot instead of being dropped, since the extension is split off the sanitized name: dropping it would turn mark..svg into marksvg-<hash>, with no extension.
  • Rolldown's default rule is native and not exported, so the plugin restates Rollup's.
  • sanitizeFileName: false is left alone, as the way to ask for raw names.

Verification

examples/start-ssr gets a lazy catch-all route, src/routes/[...rest].tsx, with its own CSS and an image named mark..svg, and a file-names mode in test/run.mjs (node test/run.mjs file-names). It builds the example with the default sanitizer, with a user function from the config, with the same function from another plugin's outputOptions hook, and with sanitizeFileName: false, then serves the build through the example's server.js, which already skips static files for URLs containing ... It also builds a small library with preserveModules from a directory named test-dot..lib.

With the plugin built from next the mode fails 11 of 16: the chunk and CSS keep their dots, server.js answers them with the SSR page, and Rolldown rejects the two builds with a user function. With the fix it passes 16/16. Reverting each piece fails it too: collapsing on the client only, collapsing before the user's function, dropping order: 'post', or collapsing the whole path.

The file-names mode isn't part of the PR workflow, so I ran the full pnpm test of the examples the hook affects:

  • examples/start-ssr: run.mjs 661/661, http-bridge 10/10, components-warning 11/11, webworker-warning 12/12, dedupe 8/8
  • examples/start-client: 65/65
  • examples/ssr: 12/12, boundary 8/8
  • examples/css-matrix: 87/87, bridge 19/19
  • examples/start-env: 47/47

pnpm exec tsc --noEmit -p . is clean.

A catch-all route module such as `[...404].tsx` built to
`_...404_-<hash>.js`, with a `_..-<hash>.css` asset named after it.
Hosts and middleware that reject any URL containing `..` refused both
files, so the lazy route failed to hydrate.

Client builds now wrap `output.sanitizeFileName` in an `outputOptions`
hook: the user's sanitizer (or the bundler default) runs first, then
every run of dots collapses to one. `sanitizeFileName: false` is left
alone, and server output is unchanged.

The start-ssr example gains a lazy `[...rest]` route and a file-names
mode covering the default build, a user sanitizer and the opt-out.

Fixes solidjs#391
The collapse only ran for client builds, but the server bundle writes
the URLs of the assets it imports, computed with its own sanitizer. An
asset named with a dot run (`logo..png`) then built to `logo-<hash>.png`
on the client while the server-rendered markup pointed at
`logo.-<hash>.png`, a file nobody wrote, and hydration keeps the
server's attribute.

The wrapper now applies to every build environment, so both sides name
assets the same way. Server chunk names collapse too.

The catch-all route in start-ssr renders `mark..svg`, and the
file-names mode checks that its server-rendered `src` names a file
under dist/client that server.js serves, and that no path under
dist/server contains `..`.
The main plugin is pre-enforced, so its `outputOptions` hook ran before
every normal plugin's. A later plugin that set `sanitizeFileName` from
its own hook replaced the wrapper and brought `..` back. The hook is
now post-order: it runs after every pre and normal `outputOptions`
hook and wraps whatever they set. A post hook further down the plugin
array can still override it.

The file-names mode now uses a user sanitizer that turns the brackets
into dots, so `[...rest]` only loses its `..` when the collapse runs
after the user function; the old `~` sanitizer passed in either order.
A new SANITIZE_FILE_NAME=plugin variant sets the same function from a
later plugin's `outputOptions` hook.
Rolldown refuses a `[name]` substitution that starts with `..`, since it
reads as a relative path. When the dots a user sanitizer produces are
not collapsed (no wrapper on that build, or the collapse running before
the user function), the custom and plugin builds fail outright, and the
exception ended the file-names mode before the remaining variants ran.

Each variant now records the build error as its failure and the mode
moves on.
With preserveModules the name carries the module's directories, including
`../` segments, and collapsing those makes the bundler reject the name.
Also moves the file-names mode to port 3185 and says in the changeset that
only the last segment of a name is collapsed.
@changeset-bot

changeset-bot Bot commented Oct 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 13a51f7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/vite-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant