diff --git a/.changeset/fix-db-cloudflare-build.md b/.changeset/fix-db-cloudflare-build.md new file mode 100644 index 000000000000..f5958a1eea30 --- /dev/null +++ b/.changeset/fix-db-cloudflare-build.md @@ -0,0 +1,5 @@ +--- +'@astrojs/db': patch +--- + +Fixes `astro build --remote` failing with "Invalid URL string" when using `@astrojs/db` with `@astrojs/cloudflare`. The temporary Vite server created during build now filters out Cloudflare's environment-replacing plugins, preventing the workerd runtime from eagerly loading the manifest placeholder. diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index 6449e6044e4e..ca06bb3702b5 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -242,17 +242,41 @@ async function executeSeedFile({ } } +/** + * Filter out adapter plugins that replace the SSR environment with a non-Node.js + * runtime (e.g. @cloudflare/vite-plugin creates a workerd-based environment). + * The temp server only needs a standard Node.js SSR environment for seed execution. + */ +export function filterPluginsForTempServer(plugins: UserConfig['plugins']): UserConfig['plugins'] { + return (plugins ?? []).flat().filter((plugin) => { + if ( + plugin && + typeof plugin === 'object' && + 'name' in plugin && + typeof plugin.name === 'string' + ) { + if (plugin.name.startsWith('vite-plugin-cloudflare')) return false; + } + return true; + }); +} + /** * Inspired by Astro content collection config loader. */ async function getTempViteServer({ viteConfig }: { viteConfig: UserConfig }) { + const filteredPlugins = filterPluginsForTempServer(viteConfig.plugins); + const tempViteServer = await createServer( - mergeConfig(viteConfig, { - server: { middlewareMode: true, hmr: false, watch: null, ws: false }, - optimizeDeps: { noDiscovery: true }, - ssr: { external: [] }, - logLevel: 'silent', - }), + mergeConfig( + { ...viteConfig, plugins: filteredPlugins }, + { + server: { middlewareMode: true, hmr: false, watch: null, ws: false }, + optimizeDeps: { noDiscovery: true }, + ssr: { external: [] }, + logLevel: 'silent', + }, + ), ); const hotSend = tempViteServer.environments.client.hot.send; diff --git a/packages/db/test/unit/filter-plugins.test.ts b/packages/db/test/unit/filter-plugins.test.ts new file mode 100644 index 000000000000..1ed19da26ec1 --- /dev/null +++ b/packages/db/test/unit/filter-plugins.test.ts @@ -0,0 +1,79 @@ +import assert from 'node:assert'; +import test, { describe } from 'node:test'; +import { filterPluginsForTempServer } from '../../dist/core/integration/index.js'; + +describe('filterPluginsForTempServer', () => { + test('removes vite-plugin-cloudflare plugins', () => { + const plugins = [ + { name: 'astro:build' }, + { name: 'vite-plugin-cloudflare' }, + { name: 'vite-plugin-cloudflare:dev' }, + { name: 'vite-plugin-cloudflare:config' }, + { name: 'astro:assets' }, + ]; + const filtered = filterPluginsForTempServer(plugins); + const names = (filtered ?? []).flatMap((p) => + p && typeof p === 'object' && 'name' in p ? [p.name] : [], + ); + assert.deepStrictEqual(names, ['astro:build', 'astro:assets']); + }); + + test('preserves all plugins when no cloudflare plugins present', () => { + const plugins = [ + { name: 'astro:build' }, + { name: 'astro:assets' }, + { name: '@astrojs/react' }, + ]; + const filtered = filterPluginsForTempServer(plugins); + const names = (filtered ?? []).flatMap((p) => + p && typeof p === 'object' && 'name' in p ? [p.name] : [], + ); + assert.deepStrictEqual(names, ['astro:build', 'astro:assets', '@astrojs/react']); + }); + + test('handles undefined plugins', () => { + const filtered = filterPluginsForTempServer(undefined); + assert.deepStrictEqual(filtered, []); + }); + + test('handles empty plugins array', () => { + const filtered = filterPluginsForTempServer([]); + assert.deepStrictEqual(filtered, []); + }); + + test('handles nested plugin arrays (flattens)', () => { + const plugins = [ + [{ name: 'astro:build' }, { name: 'vite-plugin-cloudflare:rsc' }], + { name: 'astro:assets' }, + ]; + const filtered = filterPluginsForTempServer(plugins); + const names = (filtered ?? []).flatMap((p) => + p && typeof p === 'object' && 'name' in p ? [p.name] : [], + ); + assert.deepStrictEqual(names, ['astro:build', 'astro:assets']); + }); + + test('handles null/falsy entries in plugins array', () => { + const plugins = [null, undefined, false, { name: 'astro:build' }] as const; + const filtered = filterPluginsForTempServer(plugins as any); + assert.strictEqual((filtered ?? []).length, 4); // null, undefined, false pass the name check + }); + + test('preserves non-cloudflare adapter plugins', () => { + const plugins = [ + { name: '@astrojs/cloudflare:cf-imports' }, + { name: '@astrojs/cloudflare:environment' }, + { name: 'vite-plugin-cloudflare' }, + { name: 'vite-plugin-cloudflare:dev' }, + ]; + const filtered = filterPluginsForTempServer(plugins); + const names = (filtered ?? []).flatMap((p) => + p && typeof p === 'object' && 'name' in p ? [p.name] : [], + ); + // Only vite-plugin-cloudflare* are removed; @astrojs/cloudflare:* are kept + assert.deepStrictEqual(names, [ + '@astrojs/cloudflare:cf-imports', + '@astrojs/cloudflare:environment', + ]); + }); +});