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 return → throw 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, """):
- The
" inside /"/g is taken as an opening string delimiter.
- The alternative
"(?:[^"\\]|\\.)*" matches "/g, " — from the regex literal's quote to the opening quote of """.
- The closing quote of
""" 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, """);
}
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), """)) 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("&", "&").replaceAll("<", "<").replaceAll('"', """);
Participation
Astro Info
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-levelreturn→throwin.astrofrontmatter so esbuild's scan won't reject it. To avoid rewriting the wordreturninside strings and comments, it tokenizes with a single regex: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, """):"inside/"/gis taken as an opening string delimiter."(?:[^"\\]|\\.)*"matches"/g, "— from the regex literal's quote to the opening quote of"""."""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
returnlooks like it is inside a string literal, so none are rewritten, and esbuild fails the whole dependency scan withTop-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 oneTop-level returnerror per survivingreturn— 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:
node_modules/.vitethe scan is skipped and the dev server starts clean, so it looks intermittent..astrofile, and the "considered to be an ECMAScript module because of theexportkeyword here" note points at a syntheticexport default {}line the plugin itself appends — a line that does not exist in the source.A
//-forming literal such as/\//ghits 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:
Driving the shipped plugin directly:
Result:
The
returnon line 6 is the only one left unrewritten — the one insideescapeHtmlwas rewritten correctly, before the desync. Deleting only the"from the regex literal (e.g.value.replaceAll(String.fromCharCode(34), """)) makes the identical file build fine, which isolates the cause to the regex literal.In a real project, reproduce with:
What's the expected result?
Frontmatter containing a regex literal with a quote character should scan without error. Top-level
returnin 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
returnbehind, the scan failure is not actionable as reported. Since the plugin's only job here is dependency discovery, failing soft — returningexport 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.astrofrontmatter; plain-stringreplaceAll()works:Participation