diff --git a/.changeset/smooth-tables-tan.md b/.changeset/smooth-tables-tan.md new file mode 100644 index 000000000..fc9a1eb8a --- /dev/null +++ b/.changeset/smooth-tables-tan.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/netlify': patch +--- + +Netlify Edge: Forward requests for static assets diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 5ef0bce64..c8b27c35a 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -13,8 +13,11 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): route.routeData = deserializeRouteData(serializedRoute.routeData); } + const assets = new Set(serializedManifest.assets); + return { ...serializedManifest, + assets, routes, }; } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 95ccfeba1..aa4481141 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -26,10 +26,12 @@ export interface SSRManifest { pageMap: Map; renderers: SSRLoadedRenderer[]; entryModules: Record; + assets: Set; } -export type SerializedSSRManifest = Omit & { +export type SerializedSSRManifest = Omit & { routes: SerializedRouteInfo[]; + assets: string[]; }; export type AdapterCreateExports = ( diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 0288f8bc9..b06f3c4f1 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -10,7 +10,7 @@ import { fileURLToPath } from 'url'; import * as vite from 'vite'; import { createBuildInternals } from '../../core/build/internal.js'; import { info } from '../logger/core.js'; -import { appendForwardSlash, prependForwardSlash } from '../../core/path.js'; +import { prependForwardSlash } from '../../core/path.js'; import { emptyDir, removeDir } from '../../core/util.js'; import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'; import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js'; diff --git a/packages/astro/src/core/build/vite-plugin-ssr.ts b/packages/astro/src/core/build/vite-plugin-ssr.ts index aa07fe3a8..656d01d81 100644 --- a/packages/astro/src/core/build/vite-plugin-ssr.ts +++ b/packages/astro/src/core/build/vite-plugin-ssr.ts @@ -7,6 +7,8 @@ import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types'; import { serializeRouteData } from '../routing/index.js'; import { eachPageData } from './internal.js'; import { addRollupInput } from './add-rollup-input.js'; +import { fileURLToPath } from 'url'; +import glob from 'fast-glob'; import { virtualModuleId as pagesVirtualModuleId } from './vite-plugin-pages.js'; import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; @@ -65,11 +67,19 @@ if(_start in adapter) { } return void 0; }, - generateBundle(_opts, bundle) { - const manifest = buildManifest(buildOpts, internals); + async generateBundle(_opts, bundle) { + const staticFiles = await glob('**/*', { + cwd: fileURLToPath(buildOpts.buildConfig.client), + }); + + const manifest = buildManifest(buildOpts, internals, staticFiles); + + for (const [_chunkName, chunk] of Object.entries(bundle)) { - if (chunk.type === 'asset') continue; + if (chunk.type === 'asset') { + continue; + }; if (chunk.modules[resolvedVirtualModuleId]) { const code = chunk.code; chunk.code = code.replace(replaceExp, () => { @@ -81,7 +91,7 @@ if(_start in adapter) { }; } -function buildManifest(opts: StaticBuildOptions, internals: BuildInternals): SerializedSSRManifest { +function buildManifest(opts: StaticBuildOptions, internals: BuildInternals, staticFiles: string[]): SerializedSSRManifest { const { astroConfig } = opts; const routes: SerializedRouteInfo[] = []; @@ -112,6 +122,7 @@ function buildManifest(opts: StaticBuildOptions, internals: BuildInternals): Ser pageMap: null as any, renderers: [], entryModules, + assets: staticFiles.map(s => '/' + s) }; return ssrManifest; diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 3d4bde0c3..d4a452f6a 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -33,6 +33,7 @@ "@astrojs/webapi": "^0.11.1" }, "devDependencies": { + "@netlify/edge-handler-types": "^0.34.1", "@netlify/functions": "^1.0.0", "astro": "workspace:*", "astro-scripts": "workspace:*" diff --git a/packages/integrations/netlify/src/netlify-edge-functions.ts b/packages/integrations/netlify/src/netlify-edge-functions.ts index 1bb8e2c3a..040228241 100644 --- a/packages/integrations/netlify/src/netlify-edge-functions.ts +++ b/packages/integrations/netlify/src/netlify-edge-functions.ts @@ -5,7 +5,14 @@ import { App } from 'astro/app'; export function createExports(manifest: SSRManifest) { const app = new App(manifest); - const handler = async (request: Request): Promise => { + const handler = async (request: Request): Promise => { + const url = new URL(request.url); + + // If this matches a static asset, just return and Netlify will forward it + // to its static asset handler. + if(manifest.assets.has(url.pathname)) { + return; + } if (app.match(request)) { return app.render(request); } diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/astro.config.mjs b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/astro.config.mjs new file mode 100644 index 000000000..c55135e43 --- /dev/null +++ b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/astro.config.mjs @@ -0,0 +1,11 @@ +import { defineConfig } from 'astro/config'; +import { netlifyEdgeFunctions } from '@astrojs/netlify'; + +export default defineConfig({ + adapter: netlifyEdgeFunctions({ + dist: new URL('./dist/', import.meta.url), + }), + experimental: { + ssr: true + } +}) diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/package.json b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/package.json new file mode 100644 index 000000000..6e548c151 --- /dev/null +++ b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/netlify-edge-root-dynamic", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/netlify": "workspace:*" + } +} diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/public/styles.css b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/public/styles.css new file mode 100644 index 000000000..eedeb9d0f --- /dev/null +++ b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/public/styles.css @@ -0,0 +1,3 @@ +body { + background: blue; +} diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/src/pages/[...all].astro b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/src/pages/[...all].astro new file mode 100644 index 000000000..b61f6fc44 --- /dev/null +++ b/packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic/src/pages/[...all].astro @@ -0,0 +1,9 @@ + + + Testing + + + +

Testing

+ + diff --git a/packages/integrations/netlify/test/edge-functions/root-dynamic.test.ts b/packages/integrations/netlify/test/edge-functions/root-dynamic.test.ts new file mode 100644 index 000000000..c6504e188 --- /dev/null +++ b/packages/integrations/netlify/test/edge-functions/root-dynamic.test.ts @@ -0,0 +1,16 @@ +// @ts-ignore +import { runBuild } from './test-utils.ts'; +// @ts-ignore +import { assertEquals, assert, DOMParser } from './deps.ts'; + +// @ts-ignore +Deno.test({ + name: 'Assets are preferred over HTML routes', + async fn() { + let close = await runBuild('./fixtures/root-dynamic/'); + const { default: handler } = await import('./fixtures/root-dynamic/dist/edge-functions/entry.js'); + const response = await handler(new Request('http://example.com/styles.css')); + assertEquals(response, undefined, 'No response because this is an asset'); + await close(); + }, +}); diff --git a/packages/integrations/netlify/tsconfig.json b/packages/integrations/netlify/tsconfig.json index 44baf375c..b057b6880 100644 --- a/packages/integrations/netlify/tsconfig.json +++ b/packages/integrations/netlify/tsconfig.json @@ -5,6 +5,10 @@ "allowJs": true, "module": "ES2020", "outDir": "./dist", - "target": "ES2020" + "target": "ES2020", + "typeRoots": [ + "node_modules/@types", + "node_modules/@netlify" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b5c2f51c..0c9fc3f8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1267,12 +1267,14 @@ importers: packages/integrations/netlify: specifiers: '@astrojs/webapi': ^0.11.1 + '@netlify/edge-handler-types': ^0.34.1 '@netlify/functions': ^1.0.0 astro: workspace:* astro-scripts: workspace:* dependencies: '@astrojs/webapi': link:../../webapi devDependencies: + '@netlify/edge-handler-types': 0.34.1 '@netlify/functions': 1.0.0 astro: link:../../astro astro-scripts: link:../../../scripts @@ -1287,6 +1289,14 @@ importers: '@astrojs/react': link:../../../../../react astro: link:../../../../../../astro + packages/integrations/netlify/test/edge-functions/fixtures/root-dynamic: + specifiers: + '@astrojs/netlify': workspace:* + astro: workspace:* + dependencies: + '@astrojs/netlify': link:../../../.. + astro: link:../../../../../../astro + packages/integrations/node: specifiers: '@astrojs/webapi': ^0.11.1 @@ -3485,6 +3495,12 @@ packages: vue: 3.2.33 dev: false + /@netlify/edge-handler-types/0.34.1: + resolution: {integrity: sha512-YTwn8cw89M4lRTmoUhl9s8ljSGMDt7FOIsxsrx7YrRz/RZlbh4Yuh4RU13DDafDRBEVuRbjGo93cnN621ZfBjA==} + dependencies: + web-streams-polyfill: 3.2.1 + dev: true + /@netlify/functions/1.0.0: resolution: {integrity: sha512-7fnJv3vr8uyyyOYPChwoec6MjzsCw1CoRUO2DhQ1BD6bOyJRlD4DUaOOGlMILB2LCT8P24p5LexEGx8AJb7xdA==} engines: {node: '>=8.3.0'}