Skip to content

fix: encode NTFS-reserved characters in route-derived artifact filenames#721

Open
YevheniiKotyrlo wants to merge 1 commit into
onejs:mainfrom
YevheniiKotyrlo:fix/windows-route-artifact-names
Open

fix: encode NTFS-reserved characters in route-derived artifact filenames#721
YevheniiKotyrlo wants to merge 1 commit into
onejs:mainfrom
YevheniiKotyrlo:fix/windows-route-artifact-names

Conversation

@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor

Fixes #718.

Summary

Fixes the Windows build bug where dynamic routes silently disappear from prod builds (and one serve then 404s them). Route patterns (/:param, /*) and concrete generateStaticParams values flow into generated artifact filenames — preload/loader assets via cleanUrl() and the static HTML path in buildPage — and < > : " | ? * are reserved on Windows filesystems:

  • a colon-led segment fails the write → build.ts catches it per-page, warns ⚠ skipping page …: ENOENT, and exits 0 → the route is missing from routeToBuildInfo → bare 404 in prod. This hits every root-level (or route-group-rooted, e.g. (group)/[param]) dynamic route, every non-SSR dynamic route at any depth (via …/:param.html), and every catch-all (via *).
  • a mid-name colon "succeeds" into an NTFS alternate data stream → the preload bytes live on an invisible zero-byte stub (blog_ + hidden stream), vanishing from any copied/deployed dist.

Two of the repo's own test packages catch this on Windows: test-app-cases (dynamic-at-root + catch-all prod phases) and test-spa-shell-routing (8 prod reload failures — exactly the four dropped routes). It's been masked because turbo test without --continue cancelled everything behind the first Windows failure.

Changes

  • cleanUrl() encodes reserved characters as =hh (hex char code; literal = self-escaped first so decoding is unambiguous), and getPathFromLoaderPath() decodes them — same module, so build naming, client fetch URLs, and server-side request decoding stay symmetric.
  • buildPage's htmlPath artifact gets the same encoding (the manifest stores the encoded name, so serve-side lookups are consistent by construction).
  • Identity for clean paths: artifact names for every currently-working route are byte-identical, and serve-side postfix matching (PRELOAD_JS_POSTFIX_REGEX etc.) is untouched.
  • 11 new tests in cleanUrl.test.ts: roundtrips for patterns/catch-alls/hostile param values/literal =/hex-lookalikes, plus no-reserved-chars-emitted assertions.

Test plan

Windows 11 (Bun 1.3.13), monorepo at v1.18.0:

  • tests/test-app-casesdynamic-route-at-root-with-assets and catch-all-route-at-root-with-assets prod phases go 404 → 200 (routes restored to routeToBuildInfo, zero skipping page warnings); full package green in dev and prod
  • tests/test-spa-shell-routing8 failed prod tests → 27/27 in both phases; all four previously-dropped routes (dashboard/[appId], thread/[id], (chat)/[serverId], (chat)/[serverId]/[channelId]) registered again
  • packages/one vitest — 534 passed / 0 failed (incl. the 11 new tests)
  • oxlint 0/0, oxfmt --check clean, tsc --noEmit clean

On Linux/macOS, output is unchanged for clean paths — and pattern-named artifacts become portable as a bonus (no more literal :/* in dist/ filenames for deploy tooling to reject).

Notes

Detailed diagnosis (NTFS ADS forensics, per-face scoping matrix, masking analysis) is in the linked issue. The behavioral question worth a maintainer call: pattern-built artifacts for non-prerendered dynamic routes now get stable encoded names — if you'd rather skip emitting those artifacts entirely for pattern paths, happy to rework in that direction instead.

Dynamic-route patterns (/:param, /*) and concrete generateStaticParams values
flow into generated artifact names: preload/loader assets via cleanUrl() and
the static HTML path in buildPage. The characters < > : " | ? * are reserved
on Windows filesystems - a colon-led segment fails the write outright and a
mid-name colon silently creates an NTFS alternate data stream. The per-page
build error is caught and logged as "skipping page" while the build exits 0,
so on Windows every root-level (or route-group-rooted) dynamic route and
every non-SSR dynamic route silently disappears from routeToBuildInfo and
`one serve` returns a bare 404 for it; surviving nested +ssr routes write
their preloads into invisible ADS streams that vanish from any copied or
deployed dist.

Encode reserved characters as =hh (hex char code, with literal = self-escaped
first so decoding is unambiguous) in cleanUrl()'s filename transform and in
the htmlPath artifact, and decode them in getPathFromLoaderPath(). The
encoding is identity for paths without reserved characters, so artifact names
for every currently-working route are byte-identical; serve-side postfix
matching (PRELOAD_JS_POSTFIX_REGEX etc.) is unaffected.

Validated on Windows 11: tests/test-app-cases dynamic-route-at-root and
catch-all prod phases go 404 -> 200 (routes restored to the manifest, zero
"skipping page" warnings) and test-spa-shell-routing goes from 8 failed prod
tests to 27/27 in both dev and prod; packages/one vitest 534 passed with 11
new roundtrip/reserved-character tests. On Linux/macOS output is unchanged
for clean paths, and pattern-named artifacts become portable (no more :/* in
dist filenames for deploy tooling to reject).
@YevheniiKotyrlo YevheniiKotyrlo force-pushed the fix/windows-route-artifact-names branch from e52f0fb to b4f7ba7 Compare June 26, 2026 12:39
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.

Windows: prod builds silently drop dynamic routes (404) — route patterns leak NTFS-reserved characters into artifact filenames

1 participant