diff --git a/.changeset/fix-cf-frontmatter-scan-html-namespace.md b/.changeset/fix-cf-frontmatter-scan-html-namespace.md new file mode 100644 index 000000000000..c6e49f8e1899 --- /dev/null +++ b/.changeset/fix-cf-frontmatter-scan-html-namespace.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': patch +--- + +Fixes `.astro` files failing with `No matching export in "html:..." for import "default"` when default-imported from a `.ts` file diff --git a/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts b/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts index db8d117637ff..45812449edd6 100644 --- a/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts +++ b/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts @@ -17,7 +17,11 @@ export function astroFrontmatterScanPlugin(): ESBuildPlugin { return { name: 'astro-frontmatter-scan', setup(build) { - build.onLoad({ filter: /\.astro$/ }, async (args) => { + // Scope to the "file" namespace so that .astro files resolved into the + // "html" namespace (e.g. when a .ts file default-imports a component) + // fall through to Vite's built-in html-type handler, which appends + // `export default {}` and avoids "No matching export" errors. + build.onLoad({ filter: /\.astro$/, namespace: 'file' }, async (args) => { try { const code = await readFile(args.path, 'utf-8'); diff --git a/packages/integrations/cloudflare/test/fixtures/ts-astro-import/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/astro.config.mjs new file mode 100644 index 000000000000..339f0e2a49c0 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/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/ts-astro-import/src/components/Inner.astro b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/components/Inner.astro new file mode 100644 index 000000000000..f701dcb883f5 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/components/Inner.astro @@ -0,0 +1,5 @@ +--- +const message = 'hello from inner'; +--- + +{message} diff --git a/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/components/Outer.astro b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/components/Outer.astro new file mode 100644 index 000000000000..1f76020eecbc --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/components/Outer.astro @@ -0,0 +1,7 @@ +--- +import Inner from './Inner.astro'; +--- + +
+ +
diff --git a/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/lib/ui.ts b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/lib/ui.ts new file mode 100644 index 000000000000..f5e449a43cdd --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/lib/ui.ts @@ -0,0 +1,13 @@ +// A .ts file that default-imports .astro components — the same pattern +// used by @storyblok/astro's virtual:import-storyblok-components. +// +// During esbuild dep scanning, these .astro imports land in the "html" +// namespace. Without `namespace: "file"` on the astro-frontmatter-scan +// onLoad handler, the plugin intercepts the load and returns only the +// frontmatter — which has no `export default` — breaking the import with +// `No matching export in "html:..." for import "default"`. Regression +// guard for #16203. +import Inner from '../components/Inner.astro'; +import Outer from '../components/Outer.astro'; + +export const components = { Inner, Outer }; diff --git a/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/pages/index.astro new file mode 100644 index 000000000000..d2d5c3c4ab1f --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/ts-astro-import/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +import { components } from '../lib/ui'; + +const Outer = components.Outer; +--- + + + + + + diff --git a/packages/integrations/cloudflare/test/ts-astro-import.test.js b/packages/integrations/cloudflare/test/ts-astro-import.test.js new file mode 100644 index 000000000000..c31c8c365f2d --- /dev/null +++ b/packages/integrations/cloudflare/test/ts-astro-import.test.js @@ -0,0 +1,70 @@ +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 { AstroLogger } from '../../../astro/dist/core/logger/core.js'; + +describe('ts file default-importing an .astro component', () => { + /** @type {import('../../../astro/test/test-utils').Fixture} */ + let fixture; + const logs = []; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ts-astro-import/', + }); + + // Clear the Vite cache so dep optimization runs from scratch + // and the esbuild scan actually exercises the plugin path under test. + 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 AstroLogger({ + level: 'error', + destination: new Writable({ + objectMode: true, + write(event, _, callback) { + logs.push(event); + callback(); + }, + }), + }), + }); + }); + + it('should not produce "No matching export" error when a .ts module default-imports a .astro component', async () => { + // Regression test for #16203. Without `namespace: 'file'` on the + // astro-frontmatter-scan onLoad handler, Vite's dep scanner resolves + // `.astro` files into the `html` namespace and the plugin still + // intercepts them, returning only the frontmatter (no `export default`) + // and producing: + // No matching export in "html:/.../Component.astro" for import "default" + const noMatchingExportLog = logs.find( + (log) => + log.message && + log.message.includes('No matching export') && + log.message.includes('html:') && + log.message.includes('for import "default"'), + ); + + assert.ok( + !noMatchingExportLog, + `Should not see "No matching export in 'html:...' for import 'default'" message, but got: ${noMatchingExportLog?.message}`, + ); + }); + + it('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}`, + ); + }); +});