Skip to content

fix(vite-dev-server): serve public files given to the runtime from the Service Worker - #64

Merged
kazupon merged 3 commits into
mainfrom
fix/sw-public-files
Oct 1, 2026
Merged

kazupon merged 3 commits into
mainfrom
fix/sw-public-files

Conversation

@kazupon

@kazupon kazupon commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Closes #62

The Service Worker now serves public files (/public/…) that are passed to ready() or added with the file methods.

Cause

The Service Worker's dev server read the public directory once, when it started. At that point its virtual filesystem held only /public/.gitkeep, because the project files arrive later through V_FS_* messages. The list was never updated afterwards, so servePublicMiddleware skipped every public file, and the request got the SPA fallback index.html.

Fix

  • The new syncPublicFiles() (public-files-sync.ts) updates the list on the watcher's add and unlink events. Vite does the same in the watcher handlers of _createServer(), and the Web Worker already did it in setupHMR().
  • The list is updated synchronously in the event. The Service Worker acknowledges a file change after applying it (feat(vrowzer): resolve file operations after the Workers apply them #61). So when addFile(), updateFile() or deleteFile() resolves, the change is visible for public files too.
  • Two differences from Vite:
    • The public directory is checked with a trailing slash, so that a sibling such as /publicity.js is not taken for a public file.
    • Vite also removes the etag of a module with the same path. The Service Worker has no module graph, so that part is not needed.
  • The README and the JSDoc of addFile() no longer describe this as a limitation.

Behavior change

A public file now wins over a module with the same path, as in Vite. If both /public/x.js and /x.js exist, a request for /x.js gets the public file, without any transform. Before, public files were never served, so the module was returned.

Not included

  • Files that arrive while the Service Worker starts: an add event that comes after the initial read of the directory, but before the watcher listener is attached, would be missed.
  • Binary files given to ready(): both Workers write them as the string [object ArrayBuffer], because they are sent and written as text.
    • This also affects public images given to ready(), which are now served with that content.
    • Binary files added later with addFile() are not affected.
    • This will be tracked separately.
  • Debug logging: every Service Worker middleware still logs each request with console.log. This will be cleaned up separately.

Tests

  • Unit tests:

    • public-files-sync.test.ts covers:

      • adding and deleting files under the public directory, including nested directories;
      • files outside it, including /publicity.js and /public/../secret.txt;
      • change events and path normalization;
      • that the list is already updated when notify() returns.

      These tests failed before the change, because the function did not exist.

    • static.test.ts: the middleware follows changes to the list after it has been created. This test passed before the change. It records that the middleware itself needs no change.

  • Integration tests in the shared playground, which read through the Service Worker from the host page:

    • a public file given to ready() is served with its content and Content-Type;
    • public files added, updated and deleted with the file methods are served, or no longer served, right after the promises resolve, without polling;
    • binary public files keep their bytes;
    • a public file is served instead of a module with the same path.

    All four tests fail when the Service Worker does not call syncPublicFiles().

Validated locally with:

  • vp run test:unit: 119 files, 1,907 tests
  • vp run test:integration:vrowzer: 64 passed and 1 conditional skip, in two consecutive runs and once more after the docs change
  • vp run test:e2e: 46 passed, 6 existing test.skip
  • oxlint (no new warnings), knip, the format check of the changed files, and typecheck

The Service Worker's dev server read the public directory once when it started, when only /public/.gitkeep existed, and never updated the list. The project files arrive through V_FS_* messages after that, so servePublicMiddleware skipped every public file and the request got the SPA fallback index.html.

syncPublicFiles() updates the list on the watcher's add and unlink events, as Vite does in the watcher handlers of _createServer(). It runs synchronously in the event, so the list is current before the Service Worker acknowledges a file change. Unlike Vite, it checks the public directory with a trailing slash, so that a sibling such as /publicity.js is not taken for a public file.
The playground now passes a public file to ready(). The tests read through the Service Worker from the host page and check that a public file given to ready() is served with its content and Content-Type, and that public files added, updated and deleted with the file methods are served, or no longer served, as soon as the promises resolve. They also check that binary public files keep their bytes, and that a public file is served instead of a module with the same path. All four tests fail when the Service Worker does not keep its public file list in sync.

The file operation helper of the #42 tests is shared as runFileOperation().
Public files given to ready() or added with the file methods are now served by the Service Worker, so the README and the JSDoc of addFile() no longer describe them as a limitation. The API references are regenerated.
@kazupon
kazupon merged commit 750be3a into main Oct 1, 2026
4 checks passed
@kazupon
kazupon deleted the fix/sw-public-files branch October 1, 2026 08:46
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.

Serve public files given to the runtime from the Service Worker

1 participant