#14523·meteor

rspack integration: multiple issues migrating a real-world Blaze app - asset URLs ignore ROOT_URL path prefix, native npm addons fail to bundle, nested templates silently dropped

Author: vlaskyCreated Jul 5, 2026Updated Sep 12, 2026
LabelsType:BugProject:blazemodern-build-stack

Environment

Summary

We migrated a large production Blaze app (multi-directory, eager-loading era, MySQL-backed, served under a ROOT_URL path prefix) to the Rspack bundler and hit three independent problems. Each is reproducible on its own; together they made the migration considerably harder than the documentation suggests. App-side workarounds for all three are included below.

  1. Every URL the Rspack integration constructs omits the ROOT_URL path prefix, so the client bundle 404s and the app never boots.
  2. npm dependencies that ship compiled native addons (.node binaries) break the server bundle and must be externalized by hand.
  3. Blaze .html templates and .css files in nested directories are silently excluded from the build unless individually listed in meteor.modules.

Issue 1: All Rspack asset URLs ignore the ROOT_URL path prefix

When an app is served under a ROOT_URL path prefix (e.g. https://example.com/live/), every URL the Rspack integration constructs omits the prefix. In meteor run (development) the injected client bundle script tag 404s, so the server boots normally and the page renders its boilerplate, but the application client bundle never executes. This makes the integration unusable for any sub-path deployment, and the failure mode is silent and confusing.

Three faces of the same root cause:

  1. Dev client bundle script tag. boilerplate-generator prefixes every bundle script with rootUrlPathPrefix, but emits the Rspack custom script verbatim:

    // boilerplate-generator (web browser template)
    }) : template('  <script ... src="<%- src %>"></script>')({
      src: rootUrlPathPrefix + pathname          // <-- normal bundles get the prefix
    });
    }), process.env.METEOR_APP_CUSTOM_SCRIPT_URL ? template("  <script ... src=\"<%- src %>\"></script>")({
      src: process.env.METEOR_APP_CUSTOM_SCRIPT_URL   // <-- Rspack script does not
    }) : ''
    

    The env var is set in the rspack build plugin (os/lib/config.js) as /__rspack__/<file> with no prefix. Result: the page requests /__rspack__/client-rspack.js → 404, while /live/__rspack__/client-rspack.js (the mounted handler) serves fine.

  2. Injected CSS link. The <link href="/build-chunks/main.css"> head tag produced via HtmlRspackPlugin is also unprefixed.

  3. The integration's own compatibility redirects. rspack_server.js redirects /build-chunks/* and /build-assets/* to /__rspack__/... targets that are also unprefixed, so following the redirect still 404s.

Reproduction

  1. meteor create --blaze app && cd app && meteor add rspack (any app with a client mainModule and an imported stylesheet works)
  2. ROOT_URL=http://localhost:3000/live/ meteor run
  3. Open http://localhost:3000/live/: server-side boilerplate renders, but the browser console/network tab shows GET /__rspack__/client-rspack.js → 404 and no app JS runs. curl -I http://localhost:3000/live/__rspack__/client-rspack.js returns 200, confirming only the generated URL is wrong.

Expected

All Rspack-generated asset URLs should be prefixed with __meteor_runtime_config__.ROOT_URL_PATH_PREFIX, consistent with every other URL Meteor emits.

Workaround (app side)

In server startup code, before webapp generates the boilerplate:

// Fix 1: prefix the custom script URL
const rspackScriptUrl = process.env.METEOR_APP_CUSTOM_SCRIPT_URL;
const pathPrefix = process.env.ROOT_URL
  ? new URL(process.env.ROOT_URL).pathname.replace(/\/$/, "") : "";
if (rspackScriptUrl && pathPrefix && !rspackScriptUrl.startsWith(pathPrefix + "/")) {
  process.env.METEOR_APP_CUSTOM_SCRIPT_URL = pathPrefix + rspackScriptUrl;
}

// Fix 2: bounce unprefixed asset requests to their prefixed equivalents
if (pathPrefix) {
  const rawHandlers = WebApp.rawConnectHandlers || WebApp.rawHandlers;
  rawHandlers.use((req, res, next) => {
    if (/^\/(__rspack__|build-chunks|build-assets)\//.test(req.url)) {
      res.writeHead(307, { Location: pathPrefix + req.url });
      res.end();
      return;
    }
    next();
  });
}

With these two workarounds the app runs correctly under the prefix in dev and production-simulation modes.

Issue 2: npm packages with native addons (.node binaries) fail to bundle

The server Rspack build attempts to bundle npm dependencies that ship compiled native addons, and fails on the binary itself:

ERROR in ./node_modules/@vlasky/quoted-printable/binding.node
  × Module parse failed:
  ╰─▶ × JavaScript parse error: Unexpected character '�'
  help: You may need an appropriate loader to handle this file type.

ERROR in ./node_modules/@vlasky/shacrypt/shacrypt.js 3:26-51
  × Module not found: Can't resolve './build/Release/shacrypt'

(The second error is the extension-less require('./build/Release/shacrypt') convention used by virtually every node-gyp package; the suggested resolve.extensions fix would only lead back to the first error.)

Expected

Since node_modules is present at server runtime anyway, the integration should auto-externalize any server dependency that contains a .node binary / binding.gyp (or simply treat .node requires as commonjs externals by default), instead of failing the build and requiring users to discover this pattern themselves.

Workaround (app side)

Externalize each such package by hand in rspack.config.js:

externals: Meteor.isServer
  ? {
      '@vlasky/shacrypt': 'commonjs @vlasky/shacrypt',
      '@vlasky/quoted-printable': 'commonjs @vlasky/quoted-printable',
    }
  : undefined,

Issue 3: Nested Blaze templates and stylesheets are silently dropped

In a multi-directory Blaze app, nested client/**/*.html templates and client/**/*.css stylesheets are silently excluded from the build unless each file is individually listed in meteor.modules. The integration's ignore list keeps only top-level client/*.html and client/*.css on the Meteor side; everything deeper vanishes. The build succeeds without any warning and the app runs with all templates missing (Template.x is undefined at runtime, route controllers fail to register, etc.), which is very hard to diagnose because nothing errors at build time.

Expected

Either nested .html/.css under the entrypoint directory should be handled like their top-level counterparts, or the build should emit a warning when Blaze is enabled and unimported/unlisted .html files exist under the entrypoint directory. Documentation for migrating multi-directory Blaze apps would also help: the required combination (list every nested asset in meteor.modules AND import every .html from the client entry module so RequireExternalsPlugin wires them back to the Meteor compiler) is currently undocumented and was only discoverable by reading the integration source.

Workaround (app side)

  1. List every nested .html/.css/.less file in meteor.modules in package.json.
  2. Import every .html template file from the client entry module (before the JS modules that reference the templates).