Skip to content

[bug] @astrojs/cloudflare: frontmatter dep-scan tokenizer does not skip regex literals, breaking top-level return rewriting #17697

Description

@nzakas

Astro Info

Astro                    v6.4.8
Node                     v24.18.1
System                   Linux (x64)
Package Manager          npm
Output                   server
Adapter                  @astrojs/cloudflare (v13.7.0)
Integrations             @astrojs/preact

If this issue only occurs in one browser, which browser is a problem?

N/A — build-time issue.

Describe the Bug

@astrojs/cloudflare's dep-scan plugin (packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts) rewrites top-level returnthrow in .astro frontmatter so esbuild's scan won't reject it. To avoid rewriting the word return inside strings and comments, it tokenizes with a single regex:

const RETURN_REPLACE_RE =
  /(\/\/[^\n]*|\/\*[\s\S]*?\*\/|`(?:[^`\\]|\\.)*`|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(?<!\.)\breturn(\s*;|\b)/g;

The skip group handles line comments, block comments, and the three string forms — but not regex literals. A regex literal containing a quote character therefore desynchronizes quote pairing for the entire remainder of the frontmatter.

Concretely, given .replace(/"/g, "&quot;"):

  1. The " inside /"/g is taken as an opening string delimiter.
  2. The alternative "(?:[^"\\]|\\.)*" matches "/g, " — from the regex literal's quote to the opening quote of "&quot;".
  3. The closing quote of "&quot;" is now read as an opening delimiter, and the "string" runs on until the next " anywhere below.

From that point the tokenizer is offset by one quote for the rest of the file. Every subsequent return looks like it is inside a string literal, so none are rewritten, and esbuild fails the whole dependency scan with Top-level return cannot be used inside an ECMAScript module.

The user-visible symptom is [vite] (!) Failed to run dependency scan. Skipping dependency pre-bundling. followed by one Top-level return error per surviving return — pointing at frontmatter that is perfectly valid Astro. In our app one such regex literal produced 13 errors from a single page.

Two things make this hard to diagnose:

  • It only reproduces on a cold Vite cache. With a warm node_modules/.vite the scan is skipped and the dev server starts clean, so it looks intermittent.
  • The reported error location is the user's .astro file, and the "considered to be an ECMAScript module because of the export keyword here" note points at a synthetic export default {} line the plugin itself appends — a line that does not exist in the source.

A //-forming literal such as /\//g hits a related path: the //[^\n]* alternative matches it as a line comment and swallows the rest of the line. That is usually harmless (it consumes an even number of quotes), but it is the same root cause.

This is the next case after #16551 and #16203 in the same plugin; the (?<!\.) lookbehind and the string/comment skip group added there both work correctly, regex literals are just not covered.

Minimal reproduction

Frontmatter is enough — no project required:

---
function escapeHtml(value) {
	return value.replace(/"/g, "&quot;");
}

if (Astro.request.method !== "GET") {
	return new Response("Method Not Allowed", { status: 405 });
}
---
<p>{escapeHtml("hi")}</p>

Driving the shipped plugin directly:

import { astroFrontmatterScanPlugin } from "@astrojs/cloudflare/dist/esbuild-plugin-astro-frontmatter.js";
import * as esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["./repro.astro"],
  bundle: true, write: false, logLevel: "silent",
  plugins: [astroFrontmatterScanPlugin()],
});

Result:

ERROR: Top-level return cannot be used inside an ECMAScript module @ line 6

The return on line 6 is the only one left unrewritten — the one inside escapeHtml was rewritten correctly, before the desync. Deleting only the " from the regex literal (e.g. value.replaceAll(String.fromCharCode(34), "&quot;")) makes the identical file build fine, which isolates the cause to the regex literal.

In a real project, reproduce with:

rm -rf node_modules/.vite && npx astro dev

What's the expected result?

Frontmatter containing a regex literal with a quote character should scan without error. Top-level return in frontmatter is valid Astro, and the regex literal is valid JavaScript.

The fix is to add a regex-literal alternative to the skip group. That needs the usual division-vs-regex disambiguation (a / starts a regex literal only where an expression is expected), which is awkward in a single regex — so it may be worth replacing the hand-rolled tokenizer with a real one. The plugin already parses with esbuild downstream; esbuild.transform(..., { loader: 'ts' }) or a lightweight tokenizer would remove this whole class of bug rather than the next quote-shaped instance of it.

A cheaper interim improvement: when the rewrite leaves a top-level return behind, the scan failure is not actionable as reported. Since the plugin's only job here is dependency discovery, failing soft — returning export default {} for that file instead of failing the entire scan — would keep one unparseable page from disabling pre-bundling for the whole project.

Workaround

Avoid regex literals containing " (or forming //) in .astro frontmatter; plain-string replaceAll() works:

value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll('"', "&quot;");

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P3: minor bugAn edge case that only affects very specific usage (priority)pkg: cloudflareRelated to the Cloudflare adaptertriage: fix rejectedReporter says the proposed fix does not work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions