Skip to content

ng serve serves broken JS (HTTP 500 / "Failed to fetch dynamically imported module") when a chunk contains a //# sourceMappingURL= line inside a template literal #33544

Description

@roman-garcia

Command

serve

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

No response

Description

When serving with sourcemaps enabled, createAngularMemoryPlugin's load hook removes sourcemap URL comments from the served chunk before handing it to Vite:

https://github.com/angular/angular-cli/blob/main/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts#L107

code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code,

I believe this is done to strip the chunk's own trailing comment, but the g and m flags make the regex match EVERY line in the file that starts with //# sourceMappingURL= , including lines that are content inside a template literal, not real comments.

The consequence is: any library that embeds bundled code as a string (the classic worker-loader inline-worker pattern, where a webpack-built worker bundle is embedded as a string and instantiated via new Blob(...)). The embedded bundle text ends with its own //# sourceMappingURL=xxx.worker.js.map line. The emitted chunk then looks like:

createInlineWorker(`/******/ (() => { // webpackBootstrap
...thousands of lines of worker code...
//# sourceMappingURL=abc123.worker.js.map`, null);

Note the template literal's closing backtick is on the same line as the embedded comment. The replace deletes that entire line — closing backtick and , null); included — producing an unterminated template literal. Vite's import-analysis then fails to parse the chunk and the dev server responds 500 with:

Failed to parse source for import analysis because the content contains invalid JS syntax.

In the browser, if the chunk is lazy, this surfaces as:

TypeError: Failed to fetch dynamically imported module: https://.../my-component.entry-XXXXXXXX.js

Important to note is that ng build is unaffected because the plugin only runs in the dev server.

Minimal Reproduction

  1. ng new repro --defaults && cd repro
  2. Add to src/main.ts (this simulates what a bundled inline-worker dependency looks like after esbuild re-emits it — a string with mixed quotes and \n escapes is printed as a multiline template literal):
export const workerSource =
  'console.log("double") + console.log(\'single\');\n' +
  'console.log("double") + console.log(\'single\');\n' +
  '//# sourceMappingURL=inline-worker.js.map';
console.log(workerSource.length);
  1. ng serve (development configuration, sourceMap enabled — the default)
  2. Open the app. The served main.js contains an unterminated template literal and fails to parse; Vite returns 500 with Failed to parse source for import analysis.

You can observe the mechanism standalone: esbuild emits the string above as

var workerSource = `console.log("double") + console.log('single');
console.log("double") + console.log('single');
//# sourceMappingURL=inline-worker.js.map`;

and applying code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') to it removes the last line including the closing backtick — invalid JS.

Exception or Error

[vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.
  Plugin: vite:import-analysis
  File: .angular/vite-root/<project>/chunk-XXXXXXXX.js


Browser (when the broken chunk is a dependency of a lazy chunk):


TypeError: Failed to fetch dynamically imported module: https://localhost:4200/.../component.entry-XXXXXXXX.js

Your Environment

- Angular CLI / `@angular/build`: 22.0.0 (regex verified still present in 22.0.5 and on `main`)
- Vite: 7.3.2
- Node: 24.x
- OS: Linux

Anything else relevant?

Suggested fix — only strip the chunk's own trailing comment by anchoring the match to the end of the file instead of using g/m:

code: mapContents ? code.replace(/(^|\n)\/\/# sourceMappingURL=[^\r\n]*\s*$/, '$1') : code,

As a workaround we've been running this as a patch-package patch and it fixes the issue. Mid-file occurrences (which are always string content, since esbuild already consumes real sourcemap comments during bundling) are preserved, and Vite still appends its own inline map when serving.

The same greedy pattern also appears in javascript-transformer-worker.ts (data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '')) and may deserve the same anchoring, although we have not observed a failure through that path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions