diff --git a/.changeset/orange-boats-refuse.md b/.changeset/orange-boats-refuse.md new file mode 100644 index 000000000000..6feb31b812a2 --- /dev/null +++ b/.changeset/orange-boats-refuse.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': patch +--- + +Fixes an issue where `esbuild` would throw a "Top-level return cannot be used inside an ECMAScript module" error during dependency scanning in certain environments. diff --git a/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts b/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts index 1256737c770d..44fbc3739d20 100644 --- a/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts +++ b/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts @@ -24,9 +24,16 @@ export function astroFrontmatterScanPlugin(): ESBuildPlugin { // Extract frontmatter content between --- markers const frontmatterMatch = FRONTMATTER_RE.exec(code); if (frontmatterMatch) { - // Return the frontmatter as TypeScript for import scanning + // Replace `return` with `throw` to avoid esbuild's "Top-level return" error during scanning. + // This aligns with Astro's core compiler logic for frontmatter error handling. + // See: packages/astro/src/vite-plugin-astro/compile.ts + // + // Known Limitation: Using regex /\breturn\b/ will incorrectly match + // identifiers like `$return` or aliases like `import { return as ret }`. + const contents = frontmatterMatch[1].replace(/\breturn\b/g, 'throw '); + return { - contents: frontmatterMatch[1], + contents, loader: 'ts', }; } diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/top-level-return/astro.config.mjs new file mode 100644 index 000000000000..339f0e2a49c0 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/astro.config.mjs @@ -0,0 +1,7 @@ +import cloudflare from '@astrojs/cloudflare'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + adapter: cloudflare(), + output: 'server', +}); diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/package.json b/packages/integrations/cloudflare/test/fixtures/top-level-return/package.json new file mode 100644 index 000000000000..a3bbe0091594 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/package.json @@ -0,0 +1,12 @@ +{ + "name": "@test/astro-cloudflare-top-level-return", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "astro build" + }, + "dependencies": { + "@astrojs/cloudflare": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/src/lib/index.ts b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/lib/index.ts new file mode 100644 index 000000000000..bbb7aa5e95a7 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/lib/index.ts @@ -0,0 +1,7 @@ +export function guard() { + return false; +} + +const ret = 0; + +export { ret as return } \ No newline at end of file diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/pages/index.astro new file mode 100644 index 000000000000..171a79ab888c --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/pages/index.astro @@ -0,0 +1,25 @@ +--- +// This import statement is necessary to indicate that this code is an ECMAScript module. +import { guard } from "../lib/index.js" +// Un-commenting the following lines will trigger the following error: +// `X [ERROR] No matching export in "../lib/index.ts" for import "throw"` +// This is because 'return' is replaced with 'throw', and it's highly unlikely +// that the source library provides an export named 'throw', leading to a name mismatch. +// +// import { return as ret } from "../lib/index.js" +// console.log(ret) + +if (guard()) { + return Astro.redirect("/404") +} + +--- + + + + Top-level Return Test + + +

Top-level Return Test

+ + diff --git a/packages/integrations/cloudflare/test/top-level-return.test.js b/packages/integrations/cloudflare/test/top-level-return.test.js new file mode 100644 index 000000000000..c801eef880bc --- /dev/null +++ b/packages/integrations/cloudflare/test/top-level-return.test.js @@ -0,0 +1,62 @@ +import { rmSync } from 'node:fs'; +import { describe, before, it } from 'node:test'; +import { Writable } from 'node:stream'; +import { loadFixture } from './_test-utils.js'; +import assert from 'node:assert/strict'; +import { fileURLToPath } from 'node:url'; +import { Logger } from '../../../astro/dist/core/logger/core.js'; + +describe('Top-level Return', () => { + /** @type {import('../../../astro/test/test-utils').Fixture} */ + let fixture; + const logs = []; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/top-level-return/', + }); + + // Clear the Vite cache before testing + const viteCacheDir = new URL('./node_modules/.vite/', fixture.config.root); + + rmSync(fileURLToPath(viteCacheDir), { recursive: true, force: true }); + + await fixture.build({ + vite: { logLevel: 'error' }, + logger: new Logger({ + level: 'error', + dest: new Writable({ + objectMode: true, + write(event, _, callback) { + logs.push(event); + callback(); + }, + }), + }), + }); + }); + + it('should avoid esbuild top-level return error by replacing with void', async () => { + const topLevelReturnErrorLog = logs.find( + (log) => + log.message && + log.message.includes('Top-level return cannot be used inside an ECMAScript module'), + ); + + assert.ok( + !topLevelReturnErrorLog, + `Should not see "Top-level return cannot be used inside an ECMAScript module" message, but got: ${topLevelReturnErrorLog?.message}`, + ); + }); + + it('should not break JS syntax and should complete dependency scanning successfully', async () => { + const dependencyScanFailedLog = logs.find( + (log) => log.message && log.message.includes('Failed to run dependency scan'), + ); + + assert.ok( + !dependencyScanFailedLog, + `Should not see "Failed to run dependency scan" message, but got: ${dependencyScanFailedLog?.message}`, + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e39dc12b60d5..639ef3c4c4ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5101,6 +5101,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/cloudflare/test/fixtures/top-level-return: + dependencies: + '@astrojs/cloudflare': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/cloudflare/test/fixtures/vite-plugin: dependencies: '@astrojs/cloudflare':