Skip to content

new Worker(new URL(..., import.meta.url)) is broken: client import.meta.url is replaced with a file:///ROOT/... literal #2600

Description

@keufcp

Summary

The standard bundler-friendly Web Worker pattern recommended by Next.js/webpack/Vite:

const worker = new Worker(new URL('../echo.worker.ts', import.meta.url), { type: 'module' })

does not work under vinext, in either vinext dev or vinext build. The worker script itself is bundled and emitted correctly, but the URL base is broken: vinext's import-meta-url plugin replaces client-side import.meta.url with the deterministic literal file:///ROOT/<relative path> (dist/plugins/import-meta-url.js, importMetaUrlValue()), so the resolved worker URL has a file:// origin and the browser rejects the construction with a SecurityError (Failed to construct 'Worker': Script at 'file:///...' cannot be accessed from origin 'http://localhost:...').

Reproduction

Minimal App Router project (5 files):

package.json

{
  "name": "vinext-worker-repro",
  "private": true,
  "type": "module",
  "scripts": { "dev": "vinext dev", "build": "vinext build" },
  "dependencies": { "next": "16.1.6", "react": "^19.2.6", "react-dom": "^19.2.6" },
  "devDependencies": { "@vitejs/plugin-rsc": "^0.5.27", "vinext": "0.2.1", "vite": "^8.0.16" }
}

vite.config.ts

import vinext from "vinext";
import { defineConfig } from "vite";

export default defineConfig({ plugins: [vinext()] });

src/app/layout.tsx

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

src/app/page.tsx

'use client'

import { useState } from 'react'

export default function Home() {
  const [result, setResult] = useState('(not run)')

  function run() {
    try {
      const worker = new Worker(new URL('../echo.worker.ts', import.meta.url), {
        type: 'module',
      })
      worker.onmessage = (e) => setResult(`worker replied: ${e.data}`)
      worker.postMessage('ping')
    } catch (err) {
      setResult(`error: ${err}`)
    }
  }

  return (
    <main>
      <button onClick={run}>start worker</button>
      <p>{result}</p>
    </main>
  )
}

src/echo.worker.ts

self.onmessage = (e: MessageEvent<string>) => {
  self.postMessage(`echo: ${e.data}`)
}

Observed output

vinext dev — the served transform of src/app/page.tsx contains:

new Worker(new URL(/* @vite-ignore */ "/src/echo.worker.ts?worker_file&type=module", '' + "file:///ROOT/src/app/page.tsx"), { type: "module" })

vinext build — the client chunk in dist/ contains:

new Worker(new URL(`/_next/static/echo.worker-C7Yrb_A6.js`,`file:///ROOT/src/app/page.tsx`), ...)

In both cases Vite's worker plugin correctly rewrites the first argument to the served/emitted script path, but the base (import.meta.url) has already been replaced with the file:///ROOT/... literal, so the final URL resolves to a file:// origin and worker construction throws.

Expected

The worker URL should resolve against the page origin (e.g. rewrite these new URL bases to self.location.href, or skip the import.meta.url replacement when it is used as a new URL base that Vite's worker/asset plugins rewrite).

Environment

  • vinext 0.2.1 (also reproduced with 1.0.0-beta.1)
  • vite 8.1.4, next 16.1.6, @vitejs/plugin-rsc 0.5.27
  • Node v22.16.0, Windows 11 (the replacement is platform-independent, so this should reproduce on any OS)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions