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}`,
+ );
+ });
+});