diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index 03cc850ac8..103af5e3d4 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -1,13 +1,12 @@ # @astrojs/netlify -This adapter allows Astro to deploy your SSR site to [Netlify](https://www.netlify.com/). +This adapter allows Astro to deploy your [`hybrid` or `server` rendered site](https://docs.astro.build/en/core-concepts/rendering-modes/#on-demand-rendered) to [Netlify](https://www.netlify.com/). Learn how to deploy your Astro site in our [Netlify deployment guide](https://docs.astro.build/en/guides/deploy/netlify/). - [Why Astro Netlify](#why-astro-netlify) - [Installation](#installation) - [Usage](#usage) -- [Configuration](#configuration) - [Examples](#examples) - [Troubleshooting](#troubleshooting) - [Contributing](#contributing) @@ -17,13 +16,13 @@ Learn how to deploy your Astro site in our [Netlify deployment guide](https://do If you're using Astro as a static site builder—its behavior out of the box—you don't need an adapter. -If you wish to [use server-side rendering (SSR)](https://docs.astro.build/en/guides/server-side-rendering/), Astro requires an adapter that matches your deployment runtime. +If you wish to [use on-demand rendering, also known as server-side rendering (SSR)](https://docs.astro.build/en/guides/server-side-rendering/), Astro requires an adapter that matches your deployment runtime. [Netlify](https://www.netlify.com/) is a deployment platform that allows you to host your site by connecting directly to your GitHub repository. This adapter enhances the Astro build process to prepare your project for deployment through Netlify. ## Installation -Add the Netlify adapter to enable SSR in your Astro project with the following `astro add` command. This will install the adapter and make the appropriate changes to your `astro.config.mjs` file in one step. +Add the Netlify adapter with the following `astro add` command. This will install the adapter and make the appropriate changes to your `astro.config.mjs` file in one step. ```sh # Using NPM @@ -49,7 +48,7 @@ If you prefer to install the adapter manually instead, complete the following tw ```diff lang="js" // astro.config.mjs import { defineConfig } from 'astro/config'; - + import netlify from '@astrojs/netlify/functions'; + + import netlify from '@astrojs/netlify'; export default defineConfig({ + output: 'server', @@ -57,60 +56,59 @@ If you prefer to install the adapter manually instead, complete the following tw }); ``` -### Run middleware in Edge Functions +## Usage -When deploying to Netlify Functions, you can choose to use an Edge Function to run your Astro middleware. +[Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) -To enable this, set the `edgeMiddleware` config option to `true`: +Follow the instructions to [build your site locally](https://docs.astro.build/en/guides/deploy/#building-your-site-locally). After building, you will have a `.netlify/` folder containing both [Netlify Functions](https://docs.netlify.com/functions/overview/) in the `.netlify/functions-internal/` folder and [Netlify Edge Functions](https://docs.netlify.com/edge-functions/overview/) in the`.netlify/edge-functions/` folder. -```diff lang="js" - // astro.config.mjs - import { defineConfig } from 'astro/config'; - import netlify from '@astrojs/netlify/functions'; +To deploy your site, install the [Netlify CLI](https://docs.netlify.com/cli/get-started/) and run: - export default defineConfig({ - output: 'server', - adapter: netlify({ -+ edgeMiddleware: true, - }), - }); +```sh +netlify deploy ``` -#### Pass edge context to your site +The [Netlify Blog post on Astro](https://www.netlify.com/blog/how-to-deploy-astro/) and the [Netlify Docs](https://docs.netlify.com/integrations/frameworks/astro/) provide more information on how to use this integration to deploy to Netlify. -Netlify Edge Functions provide a [context object](https://docs.netlify.com/edge-functions/api/#netlify-specific-context-object) including metadata about the request, such as a user’s IP, geolocation data, and cookies. -To expose values from this context to your site, create a `netlify-edge-middleware.ts` (or `.js`) file in your project’s [source directory](https://docs.astro.build/en/reference/configuration-reference/#srcdir). This file must export a function that returns the data to add to [Astro’s `locals` object](https://docs.astro.build/en/reference/api-reference/#astrolocals), which is available in middleware and Astro routes. +### Accessing edge context from your site -In this example, `visitorCountry` and `hasEdgeMiddleware` would both be added to Astro’s `locals` object: +Netlify Edge Functions provide a [context object](https://docs.netlify.com/edge-functions/api/#netlify-specific-context-object) that includes metadata about the request such as a user’s IP, geolocation data, and cookies. + +This can be accessed through the `Astro.locals.netlify.context` object: + +```astro +--- +const { geo: { city } } = Astro.locals.netlify.context +--- +

Hello there, friendly visitor from {city}!

+``` + +If you're using TypeScript, you can get proper typings by updating `src/env.d.ts` to use `NetlifyLocals`: ```ts -// src/netlify-edge-middleware.ts -import type { Context } from 'https://edge.netlify.com'; +// src/env.d.ts +/// +/// -export default function ({ request, context }: { request: Request; context: Context }) { - // Return serializable data to add to Astro.locals - return { - visitorCountry: context.geo.country.name, - hasEdgeMiddleware: true, - }; +type NetlifyLocals = import('@astrojs/netlify').NetlifyLocals + +declare namespace App { + interface Locals extends NetlifyLocals { + ... + } } ``` -> **Note** -> Netlify Edge Functions run in [a Deno environment](https://docs.netlify.com/edge-functions/api/#runtime-environment), so import statements in this file must use Deno’s URL syntax. +This is not available on prerendered pages. -`netlify-edge-middleware.ts` must provide a function as its default export. This function: +### Running Astro middleware in Edge Functions -- must return a JSON-serializable object, which cannot include types like `Map`, `function`, `Set`, etc. -- will always run first, before any other middleware and routes. -- cannot return a response or redirect. +Any Astro middleware is applied to pre-rendered pages at build-time, and to on-demand-rendered pages at runtime. -### Per-page functions +To implement redirects, access control or custom response headers for pre-rendered pages, run your middleware on Netlify Edge Functions by enabling the `edgeMiddleware` option: -The Netlify adapter builds to a single function by default. Astro 2.7 added support for splitting your build into separate entry points per page. If you use this configuration, the Netlify adapter will generate a separate function for each page. This can help reduce the size of each function so they are only bundling code used on that page. - -```js +```diff lang="js" // astro.config.mjs import { defineConfig } from 'astro/config'; import netlify from '@astrojs/netlify/functions'; @@ -118,12 +116,22 @@ import netlify from '@astrojs/netlify/functions'; export default defineConfig({ output: 'server', adapter: netlify({ - functionPerRoute: true, ++ edgeMiddleware: true, }), }); ``` -### Static sites +Configuring `edgeMiddleware: true` will deploy your middleware as an Edge Function, and run it on all routes - including pre-rendered pages. However, locals specified in the middleware won't be available to any pre-rendered pages, because they've already been fully-rendered at build-time. + +### Netlify Image CDN support + +This adapter uses the [Netlify Image CDN](https://docs.netlify.com/image-cdn/) to transform images on-the-fly without impacting build times. +It's implemented using an [Astro Image Service](https://docs.astro.build/en/reference/image-service-reference/) under the hood. + +> **Note** +> This adapter does not support the `image.domains` and `image.remotePatterns` config properties in your Astro config. To [specify remote paths for Netlify Image CDN](https://docs.netlify.com/image-cdn/overview/#remote-path), use the `remote_images` field in `netlify.toml`. + +### Static sites & Redirects For static sites you usually don't need an adapter. However, if you use `redirects` configuration in your Astro config, the Netlify adapter can be used to translate this to the proper `_redirects` format. @@ -146,117 +154,40 @@ Once you run `astro build` there will be a `dist/_redirects` file. Netlify will > **Note** > You can still include a `public/_redirects` file for manual redirects. Any redirects you specify in the redirects config are appended to the end of your own. -### On-demand Builders +### Caching Pages -[Netlify On-demand Builders](https://docs.netlify.com/configure-builds/on-demand-builders/) are serverless functions used to generate web content as needed that’s automatically cached on Netlify’s Edge CDN. You can enable these functions using the [`builders` configuration](#builders). +On-demand rendered pages without any dynamic content can be cached to improve performance and lower resource usage. +Enabling the `cacheOnDemandPages` option in the adapter will cache all server-rendered pages for up to one year: -By default, all pages will be rendered on first visit and the rendered result will be reused for every subsequent visit until you redeploy. To set a revalidation time, call the [`runtime.setBuildersTtl(ttl)` local](https://docs.astro.build/en/reference/api-reference/#astrolocals) with the duration (in seconds). +```ts +// astro.config.mjs +export default defineConfig({ + output: 'server', + adapter: netlify({ + cacheOnDemandPages: true + }), +}); +``` + +This can be changed on a per-page basis by adding caching headers to your response: -The following example sets a revalidation time of 45, causing Netlify to store the rendered HTML for 45 seconds. ```astro --- // src/pages/index.astro import Layout from '../components/Layout.astro'; -if (import.meta.env.PROD) { - Astro.locals.runtime.setBuildersTtl(45); -} +Astro.response.headers.set('CDN-Cache-Control', "public, max-age=45, must-revalidate") --- - {new Date(Date.now())} + {new Date()} ``` -It is important to note that On-demand Builders ignore query params when checking for cached pages. For example, if `example.com/?x=y` is cached, it will be served for `example.com/?a=b` (different query params) and `example.com/` (no query params) as well. - -## Usage - -[Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) - -After [performing a build](https://docs.astro.build/en/guides/deploy/#building-your-site-locally) the `netlify/` folder will contain [Netlify Functions](https://docs.netlify.com/functions/overview/) in the `netlify/functions/` folder. - -Now you can deploy. Install the [Netlify CLI](https://docs.netlify.com/cli/get-started/) and run: - -```sh -netlify deploy --build -``` - -The [Netlify Blog post on Astro](https://www.netlify.com/blog/how-to-deploy-astro/) and the [Netlify Documentation](https://docs.netlify.com/integrations/frameworks/astro/) provide more information on how to use this integration to deploy to Netlify. - -## Configuration - -To configure this adapter, pass an object to the `netlify()` function call in `astro.config.mjs` - there's only one possible configuration option: - -### dist - -We build to the `dist` directory at the base of your project. To change this, use the `dist` option: - -```js -// astro.config.mjs -import { defineConfig } from 'astro/config'; -import netlify from '@astrojs/netlify/functions'; - -export default defineConfig({ - output: 'server', - adapter: netlify({ - dist: new URL('./dist/', import.meta.url), - }), -}); -``` - -And then point to the dist in your `netlify.toml`: - -```toml -# netlify.toml -[functions] -directory = "dist/functions" -``` - -### builders - -You can enable On-demand Builders using the `builders` option: - -```js -// astro.config.mjs -import { defineConfig } from 'astro/config'; -import netlify from '@astrojs/netlify/functions'; - -export default defineConfig({ - output: 'server', - adapter: netlify({ - builders: true, - }), -}); -``` - -On-demand Builders are only available with the `@astrojs/netlify/functions` adapter and are not compatible with Edge Functions. - -### binaryMediaTypes - -> This option is only needed for the Functions adapter and is not needed for Edge Functions. - -Netlify Functions requires binary data in the `body` to be base64 encoded. The `@astrojs/netlify/functions` adapter handles this automatically based on the `Content-Type` header. - -We check for common mime types for audio, image, and video files. To include specific mime types that should be treated as binary data, include the `binaryMediaTypes` option with a list of binary mime types. - -```js -// src/pages/image.jpg.ts -import fs from 'node:fs'; - -export function GET() { - const buffer = fs.readFileSync('../image.jpg'); - - // Return the buffer directly, @astrojs/netlify will base64 encode the body - return new Response(buffer, { - status: 200, - headers: { - 'content-type': 'image/jpeg', - }, - }); -} -``` +With [fine-grained cache control](https://www.netlify.com/blog/swr-and-fine-grained-cache-control/), Netlify supports +standard caching headers like `CDN-Cache-Control` or `Vary`. +Refer to the docs to learn about implementing e.g. time to live (TTL) or stale while revalidate (SWR) caching: https://docs.netlify.com/platform/caching ## Examples diff --git a/packages/integrations/netlify/builders-types.d.ts b/packages/integrations/netlify/builders-types.d.ts deleted file mode 100644 index 7c778be4fd..0000000000 --- a/packages/integrations/netlify/builders-types.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -interface NetlifyLocals { - runtime: { - /** - * On-demand Builders support an optional time to live (TTL) pattern that allows you to set a fixed duration of time after which a cached builder response is invalidated. This allows you to force a refresh of a builder-generated response without a new deploy. - * @param ttl time to live, in seconds - */ - setBuildersTtl(ttl: number): void; - }; -} diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 4688041738..18cf16d603 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -19,11 +19,10 @@ "homepage": "https://docs.astro.build/en/guides/integrations-guide/netlify/", "exports": { ".": "./dist/index.js", - "./functions": "./dist/integration-functions.js", - "./static": "./dist/integration-static.js", - "./netlify-functions.js": "./dist/netlify-functions.js", - "./edge-functions": "./dist/integration-edge-functions.js", - "./netlify-edge-functions.js": "./dist/netlify-edge-functions.js", + "./static": "./dist/static.js", + "./functions": "./dist/functions.js", + "./ssr-function.js": "./dist/ssr-function.js", + "./image-service.js": "./dist/image-service.js", "./package.json": "./package.json" }, "files": [ @@ -32,8 +31,8 @@ "scripts": { "build": "tsc", "test-fn": "mocha --exit --timeout 20000 --file \"./test/setup.js\" test/functions/", - "test-edge": "deno test --allow-run --allow-read --allow-net --allow-env --allow-write ./test/edge-functions/", - "test": "pnpm test-fn", + "test-static": "mocha --exit --timeout 20000 --file \"./test/setup.js\" test/static/", + "test": "pnpm test-fn && pnpm test-static", "test:hosted": "mocha --exit --timeout 30000 test/hosted" }, "dependencies": { diff --git a/packages/integrations/netlify/src/env.d.ts b/packages/integrations/netlify/src/env.d.ts deleted file mode 100644 index f964fe0cff..0000000000 --- a/packages/integrations/netlify/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/packages/integrations/netlify/src/functions.ts b/packages/integrations/netlify/src/functions.ts new file mode 100644 index 0000000000..7a84087af0 --- /dev/null +++ b/packages/integrations/netlify/src/functions.ts @@ -0,0 +1,6 @@ +import netlifyIntegration, { type NetlifyIntegrationConfig } from "./index.js" + +export default function functionsIntegration(config: NetlifyIntegrationConfig) { + console.warn("The @astrojs/netlify/functions import is deprecated and will be removed in a future release. Please use @astrojs/netlify instead.") + return netlifyIntegration(config) +} \ No newline at end of file diff --git a/packages/integrations/netlify/src/image-service.ts b/packages/integrations/netlify/src/image-service.ts new file mode 100644 index 0000000000..385f6996f8 --- /dev/null +++ b/packages/integrations/netlify/src/image-service.ts @@ -0,0 +1,57 @@ +import type { ExternalImageService, ImageMetadata } from 'astro'; +import { AstroError } from 'astro/errors'; +import { baseService } from 'astro/assets' + +const SUPPORTED_FORMATS = ['avif', 'jpg', 'png', 'webp']; +const QUALITY_NAMES: Record = { low: 25, mid: 50, high: 90, max: 100 }; + +export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata { + return typeof src === 'object'; +} + +function removeLeadingForwardSlash(path: string) { + return path.startsWith('/') ? path.substring(1) : path; +} + +const service: ExternalImageService = { + getURL(options) { + const query = new URLSearchParams(); + + const fileSrc = isESMImportedImage(options.src) + ? removeLeadingForwardSlash(options.src.src) + : options.src; + + query.set('url', fileSrc); + + if (options.format) query.set('fm', options.format); + if (options.width) query.set('w', '' + options.width); + if (options.height) query.set('h', '' + options.height); + if (options.quality) query.set('q', '' + options.quality); + + return '/.netlify/images?' + query; + }, + getHTMLAttributes: baseService.getHTMLAttributes, + getSrcSet: baseService.getSrcSet, + validateOptions(options) { + if (options.format && !SUPPORTED_FORMATS.includes(options.format)) { + throw new AstroError( + `Unsupported image format "${options.format}"`, + `Use one of ${SUPPORTED_FORMATS.join(', ')} instead.` + ); + } + + if (options.quality) { + options.quality = + typeof options.quality === 'string' ? QUALITY_NAMES[options.quality] : options.quality; + if (options.quality < 1 || options.quality > 100) { + throw new AstroError( + `Invalid quality for picture "${options.src}"`, + `Quality needs to be between 1 and 100.` + ); + } + } + return options; + }, +}; + +export default service; diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index a374020f9c..09451a2da3 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -1,2 +1,311 @@ -export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js'; -export { netlifyStatic } from './integration-static.js'; +import type { AstroConfig, AstroIntegration, RouteData } from 'astro'; +import { writeFile, mkdir, appendFile, rm } from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { build } from 'esbuild'; +import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects'; +import { version as packageVersion } from '../package.json'; +import type { Context } from '@netlify/functions'; +import { AstroError } from 'astro/errors'; +import type { IncomingMessage } from 'http'; + +export interface NetlifyLocals { + netlify: { + context: Context; + }; +} + +const isStaticRedirect = (route: RouteData) => + route.type === 'redirect' && (route.redirect || route.redirectRoute); + +const clearDirectory = (dir: URL) => rm(dir, { recursive: true }).catch(() => {}); + +export interface NetlifyIntegrationConfig { + /** + * If enabled, On-Demand-Rendered pages are cached for up to a year. + * This is useful for pages that are not updated often, like a blog post, + * but that you have too many of to pre-render at build time. + * + * You can override this behavior on a per-page basis + * by setting the `Cache-Control`, `CDN-Cache-Control` or `Netlify-CDN-Cache-Control` header + * from within the Page: + * + * ```astro + * // src/pages/cached-clock.astro + * Astro.response.headers.set('CDN-Cache-Control', "public, max-age=45, must-revalidate"); + * --- + *

{Date.now()}

+ * ``` + */ + cacheOnDemandPages?: boolean; + + /** + * If disabled, Middleware is applied to prerendered pages at build-time, and to on-demand-rendered pages at runtime. + * Only disable when your Middleware does not need to run on prerendered pages. + * If you use Middleware to implement authentication, redirects or similar things, you should should likely enabled it. + * + * If enabled, Astro Middleware is deployed as an Edge Function and applies to all routes. + * Caveat: Locals set in Middleware are not applied to prerendered pages, because they've been rendered at build-time and are served from the CDN. + * + * @default disabled + */ + edgeMiddleware?: boolean; +} + +export default function netlifyIntegration( + integrationConfig?: NetlifyIntegrationConfig +): AstroIntegration { + const isRunningInNetlify = Boolean( + process.env.NETLIFY || process.env.NETLIFY_LOCAL || process.env.NETLIFY_DEV + ); + + let _config: AstroConfig; + let outDir: URL; + let rootDir: URL; + let astroMiddlewareEntryPoint: URL | undefined = undefined; + + const ssrOutputDir = () => new URL('./.netlify/functions-internal/ssr/', rootDir); + const middlewareOutputDir = () => new URL('.netlify/edge-functions/middleware/', rootDir); + + const cleanFunctions = async () => + await Promise.all([clearDirectory(middlewareOutputDir()), clearDirectory(ssrOutputDir())]); + + async function writeRedirects(routes: RouteData[], dir: URL) { + const fallback = _config.output === 'static' ? '/.netlify/static' : '/.netlify/functions/ssr'; + const redirects = createRedirectsFromAstroRoutes({ + config: _config, + dir, + routeToDynamicTargetMap: new Map( + routes + .filter(isStaticRedirect) // all other routes are handled by SSR + .map((route) => { + // this is needed to support redirects to dynamic routes + // on static. not sure why this is needed, but it works. + route.distURL ??= route.redirectRoute?.distURL; + + return [route, fallback]; + }) + ), + }); + + if (!redirects.empty()) { + await appendFile(new URL('_redirects', outDir), '\n' + redirects.print() + '\n'); + } + } + + async function writeSSRFunction() { + await writeFile( + new URL('./ssr.mjs', ssrOutputDir()), + ` + import createSSRHandler from './entry.mjs'; + export default createSSRHandler(${JSON.stringify({ + cacheOnDemandPages: Boolean(integrationConfig?.cacheOnDemandPages), + })}); + export const config = { name: "Astro SSR", generator: "@astrojs/netlify@${packageVersion}", path: "/*", preferStatic: true }; + ` + ); + } + + async function writeMiddleware(entrypoint: URL) { + await mkdir(middlewareOutputDir(), { recursive: true }); + await writeFile( + new URL('./entry.mjs', middlewareOutputDir()), + ` + import { onRequest } from "${fileURLToPath(entrypoint).replaceAll('\\', '/')}"; + import { createContext, trySerializeLocals } from 'astro/middleware'; + + export default async (request, context) => { + const ctx = createContext({ + request, + params: {} + }); + ctx.locals = { netlify: { context } } + const next = () => { + const { netlify, ...otherLocals } = ctx.locals; + request.headers.set("x-astro-locals", trySerializeLocals(otherLocals)); + return context.next(); + }; + + return onRequest(ctx, next); + } + + export const config = { + name: "Astro Middleware", + generator: "@astrojs/netlify@${packageVersion}", + path: "/*", excludedPath: ["/_astro/*", "/.netlify/images/*"] + }; + ` + ); + + // taking over bundling, because Netlify bundling trips over NPM modules + await build({ + entryPoints: [fileURLToPath(new URL('./entry.mjs', middlewareOutputDir()))], + target: 'es2022', + platform: 'neutral', + outfile: fileURLToPath(new URL('./middleware.mjs', middlewareOutputDir())), + allowOverwrite: true, + format: 'esm', + bundle: true, + minify: false, + }); + } + + function getLocalDevNetlifyContext(req: IncomingMessage): Context { + const isHttps = req.headers['x-forwarded-proto'] === 'https'; + const parseBase64JSON = (header: string): T | undefined => { + if (typeof req.headers[header] === 'string') { + try { + return JSON.parse(Buffer.from(req.headers[header] as string, 'base64').toString('utf8')); + } catch {} + } + }; + + const context: Context = { + account: parseBase64JSON('x-nf-account-info') ?? { + id: 'mock-netlify-account-id', + }, + deploy: { + id: + typeof req.headers['x-nf-deploy-id'] === 'string' + ? req.headers['x-nf-deploy-id'] + : 'mock-netlify-deploy-id', + }, + site: parseBase64JSON('x-nf-site-info') ?? { + id: 'mock-netlify-site-id', + name: 'mock-netlify-site.netlify.app', + url: `${isHttps ? 'https' : 'http'}://localhost:${isRunningInNetlify ? 8888 : 4321}`, + }, + geo: parseBase64JSON('x-nf-geo') ?? { + city: 'Mock City', + country: { code: 'mock', name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + + // @ts-expect-error: these are smhw missing from the Netlify types - fix is on the way + timezone: 'UTC', + longitude: 0, + latitude: 0, + }, + ip: + typeof req.headers['x-nf-client-connection-ip'] === 'string' + ? req.headers['x-nf-client-connection-ip'] + : req.socket.remoteAddress ?? '127.0.0.1', + server: { + region: 'local-dev', + }, + requestId: + typeof req.headers['x-nf-request-id'] === 'string' + ? req.headers['x-nf-request-id'] + : 'mock-netlify-request-id', + get cookies(): never { + throw new Error('Please use Astro.cookies instead.'); + }, + json: (input) => Response.json(input), + log: console.log, + next: () => { + throw new Error('`context.next` is not implemented for serverless functions'); + }, + get params(): never { + throw new Error("context.params don't contain any usable content in Astro."); + }, + rewrite() { + throw new Error('context.rewrite is not available in Astro.'); + }, + }; + + return context; + } + + return { + name: '@astrojs/netlify', + hooks: { + 'astro:config:setup': async ({ config, updateConfig }) => { + rootDir = config.root; + await cleanFunctions(); + + outDir = new URL('./dist/', rootDir); + + updateConfig({ + outDir, + build: { + redirects: false, + client: outDir, + server: ssrOutputDir(), + }, + vite: { + server: { + watch: { + ignored: [fileURLToPath(new URL('./.netlify/**', rootDir))], + }, + }, + }, + image: { + service: { + entrypoint: isRunningInNetlify ? '@astrojs/netlify/image-service.js' : undefined, + }, + }, + }); + }, + 'astro:config:done': ({ config, setAdapter }) => { + rootDir = config.root; + _config = config; + + if (config.image.domains.length || config.image.remotePatterns.length) { + throw new AstroError( + "config.image.domains and config.image.remotePatterns aren't supported by the Netlify adapter.", + 'See https://github.com/withastro/adapters/tree/main/packages/netlify#image-cdn for more.' + ); + } + + setAdapter({ + name: '@astrojs/netlify', + serverEntrypoint: '@astrojs/netlify/ssr-function.js', + exports: ['default'], + adapterFeatures: { + functionPerRoute: false, + edgeMiddleware: integrationConfig?.edgeMiddleware ?? false, + }, + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + // keeping this as experimental at least until Netlify Image CDN is out of beta + supportKind: 'experimental', + // still using Netlify Image CDN instead + isSharpCompatible: true, + isSquooshCompatible: true, + }, + }, + }); + }, + 'astro:build:ssr': async ({ middlewareEntryPoint }) => { + astroMiddlewareEntryPoint = middlewareEntryPoint; + }, + 'astro:build:done': async ({ routes, dir, logger }) => { + await writeRedirects(routes, dir); + logger.info('Emitted _redirects'); + + if (_config.output !== 'static') { + await writeSSRFunction(); + logger.info('Generated SSR Function'); + } + + if (astroMiddlewareEntryPoint) { + await writeMiddleware(astroMiddlewareEntryPoint); + logger.info('Generated Middleware Edge Function'); + } + }, + + // local dev + 'astro:server:setup': async ({ server }) => { + server.middlewares.use((req, res, next) => { + const locals = Symbol.for('astro.locals'); + Reflect.set(req, locals, { + ...Reflect.get(req, locals), + netlify: { context: getLocalDevNetlifyContext(req) }, + }); + next(); + }); + }, + }, + }; +} diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts deleted file mode 100644 index 8812a8e89d..0000000000 --- a/packages/integrations/netlify/src/integration-functions.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { writeFile } from 'node:fs/promises'; -import { extname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; -import { generateEdgeMiddleware } from './middleware.js'; -import type { Args } from './netlify-functions.js'; -import { createRedirects } from './shared.js'; - -export const NETLIFY_EDGE_MIDDLEWARE_FILE = 'netlify-edge-middleware'; -export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; - -export function getAdapter({ functionPerRoute, edgeMiddleware, ...args }: Args): AstroAdapter { - return { - name: '@astrojs/netlify/functions', - serverEntrypoint: '@astrojs/netlify/netlify-functions.js', - exports: ['handler'], - args, - adapterFeatures: { - functionPerRoute, - edgeMiddleware, - }, - supportedAstroFeatures: { - hybridOutput: 'stable', - staticOutput: 'stable', - serverOutput: 'stable', - assets: { - supportKind: 'stable', - isSharpCompatible: true, - isSquooshCompatible: true, - }, - }, - }; -} - -interface NetlifyFunctionsOptions { - dist?: URL; - builders?: boolean; - binaryMediaTypes?: string[]; - edgeMiddleware?: boolean; - functionPerRoute?: boolean; -} - -function netlifyFunctions({ - dist, - builders, - binaryMediaTypes, - functionPerRoute = false, - edgeMiddleware = false, -}: NetlifyFunctionsOptions = {}): AstroIntegration { - let _config: AstroConfig; - let _entryPoints: Map; - let ssrEntryFile: string; - let _middlewareEntryPoint: URL; - return { - name: '@astrojs/netlify', - hooks: { - 'astro:config:setup': ({ config, updateConfig }) => { - const outDir = dist ?? new URL('./dist/', config.root); - updateConfig({ - outDir, - build: { - redirects: false, - client: outDir, - server: new URL('./.netlify/functions-internal/', config.root), - }, - }); - }, - 'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => { - if (middlewareEntryPoint) { - _middlewareEntryPoint = middlewareEntryPoint; - } - _entryPoints = entryPoints; - }, - 'astro:config:done': ({ config, setAdapter }) => { - setAdapter( - getAdapter({ - binaryMediaTypes, - builders, - functionPerRoute, - edgeMiddleware, - }) - ); - _config = config; - ssrEntryFile = config.build.serverEntry.replace(/\.m?js/, ''); - - if (config.output === 'static') { - // eslint-disable-next-line no-console - console.warn( - `[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.` - ); - // eslint-disable-next-line no-console - console.warn( - `[@astrojs/netlify] Otherwise, this adapter is not required to deploy a static site to Netlify.` - ); - } - }, - 'astro:build:done': async ({ routes, dir }) => { - const functionsConfig = { - version: 1, - config: { - nodeModuleFormat: 'esm', - }, - }; - const functionsConfigPath = join(fileURLToPath(_config.build.server), 'entry.json'); - await writeFile(functionsConfigPath, JSON.stringify(functionsConfig)); - - const type = builders ? 'builders' : 'functions'; - const kind = type ?? 'functions'; - - if (_entryPoints.size) { - const routeToDynamicTargetMap = new Map(); - for (const [route, entryFile] of _entryPoints) { - const wholeFileUrl = fileURLToPath(entryFile); - - const extension = extname(wholeFileUrl); - const relative = wholeFileUrl - .replace(fileURLToPath(_config.build.server), '') - .replace(extension, '') - .replaceAll('\\', '/'); - const dynamicTarget = `/.netlify/${kind}/${relative}`; - - routeToDynamicTargetMap.set(route, dynamicTarget); - } - await createRedirects(_config, routeToDynamicTargetMap, dir); - } else { - const dynamicTarget = `/.netlify/${kind}/${ssrEntryFile}`; - const map: [RouteData, string][] = routes.map((route) => { - return [route, dynamicTarget]; - }); - const routeToDynamicTargetMap = new Map(Array.from(map)); - - await createRedirects(_config, routeToDynamicTargetMap, dir); - } - if (_middlewareEntryPoint) { - const outPath = fileURLToPath(new URL('./.netlify/edge-functions/', _config.root)); - const netlifyEdgeMiddlewareHandlerPath = new URL( - NETLIFY_EDGE_MIDDLEWARE_FILE, - _config.srcDir - ); - await generateEdgeMiddleware( - _middlewareEntryPoint, - outPath, - netlifyEdgeMiddlewareHandlerPath - ); - } - }, - }, - }; -} - -export { netlifyFunctions as default, netlifyFunctions }; diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts deleted file mode 100644 index af28498674..0000000000 --- a/packages/integrations/netlify/src/integration-static.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { AstroIntegration, RouteData } from 'astro'; -import { createRedirects } from './shared.js'; - -export function netlifyStatic(): AstroIntegration { - let _config: any; - return { - name: '@astrojs/netlify', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ - build: { - // Do not output HTML redirects because we are building a `_redirects` file. - redirects: false, - }, - }); - }, - 'astro:config:done': ({ config }) => { - _config = config; - }, - 'astro:build:done': async ({ dir, routes }) => { - const mappedRoutes: [RouteData, string][] = routes.map((route) => [ - route, - `/.netlify/static/`, - ]); - const routesToDynamicTargetMap = new Map(Array.from(mappedRoutes)); - await createRedirects(_config, routesToDynamicTargetMap, dir); - }, - }, - }; -} diff --git a/packages/integrations/netlify/src/middleware.ts b/packages/integrations/netlify/src/middleware.ts deleted file mode 100644 index 3c2f4f6974..0000000000 --- a/packages/integrations/netlify/src/middleware.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { ASTRO_LOCALS_HEADER } from './integration-functions.js'; -import { DENO_SHIM } from './shared.js'; - -/** - * It generates a Netlify edge function. - * - */ -export async function generateEdgeMiddleware( - astroMiddlewareEntryPointPath: URL, - outPath: string, - netlifyEdgeMiddlewareHandlerPath: URL -): Promise { - const entryPointPathURLAsString = JSON.stringify( - fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/') - ); - - const code = edgeMiddlewareTemplate(entryPointPathURLAsString, netlifyEdgeMiddlewareHandlerPath); - const bundledFilePath = join(outPath, 'edgeMiddleware.js'); - const esbuild = await import('esbuild'); - await esbuild.build({ - stdin: { - contents: code, - resolveDir: process.cwd(), - }, - target: 'es2020', - platform: 'browser', - outfile: bundledFilePath, - allowOverwrite: true, - format: 'esm', - bundle: true, - minify: false, - banner: { - js: DENO_SHIM, - }, - }); - return pathToFileURL(bundledFilePath); -} - -function edgeMiddlewareTemplate(middlewarePath: string, netlifyEdgeMiddlewareHandlerPath: URL) { - const filePathEdgeMiddleware = fileURLToPath(netlifyEdgeMiddlewareHandlerPath); - let handlerTemplateImport = ''; - let handlerTemplateCall = '{}'; - if (existsSync(filePathEdgeMiddleware + '.js') || existsSync(filePathEdgeMiddleware + '.ts')) { - const stringified = JSON.stringify(filePathEdgeMiddleware.replace(/\\/g, '/')); - handlerTemplateImport = `import handler from ${stringified}`; - handlerTemplateCall = `handler({ request, context })`; - } else { - } - return ` - ${handlerTemplateImport} -import { onRequest } from ${middlewarePath}; -import { createContext, trySerializeLocals } from 'astro/middleware'; -export default async function middleware(request, context) { - const url = new URL(request.url); - const ctx = createContext({ - request, - params: {} - }); - ctx.locals = ${handlerTemplateCall}; - const next = async () => { - request.headers.set(${JSON.stringify(ASTRO_LOCALS_HEADER)}, trySerializeLocals(ctx.locals)); - return await context.next(); - }; - - return onRequest(ctx, next); -} - -export const config = { - path: "/*" -} -`; -} diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts deleted file mode 100644 index 9c9d8ad3a3..0000000000 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { type Handler, builder } from '@netlify/functions'; -import type { SSRManifest } from 'astro'; -import { App } from 'astro/app'; -import { applyPolyfills } from 'astro/app/node'; -import { ASTRO_LOCALS_HEADER } from './integration-functions.js'; - -applyPolyfills(); - -export interface Args { - builders?: boolean; - binaryMediaTypes?: string[]; - edgeMiddleware: boolean; - functionPerRoute: boolean; -} - -function parseContentType(header?: string) { - return header?.split(';')[0] ?? ''; -} - -const clientAddressSymbol = Symbol.for('astro.clientAddress'); - -export const createExports = (manifest: SSRManifest, args: Args) => { - const app = new App(manifest); - - const builders = args.builders ?? false; - const binaryMediaTypes = args.binaryMediaTypes ?? []; - const knownBinaryMediaTypes = new Set([ - 'audio/3gpp', - 'audio/3gpp2', - 'audio/aac', - 'audio/midi', - 'audio/mpeg', - 'audio/ogg', - 'audio/opus', - 'audio/wav', - 'audio/webm', - 'audio/x-midi', - 'image/avif', - 'image/bmp', - 'image/gif', - 'image/vnd.microsoft.icon', - 'image/heif', - 'image/jpeg', - 'image/png', - 'image/svg+xml', - 'image/tiff', - 'image/webp', - 'video/3gpp', - 'video/3gpp2', - 'video/mp2t', - 'video/mp4', - 'video/mpeg', - 'video/ogg', - 'video/x-msvideo', - 'video/webm', - ...binaryMediaTypes, - ]); - - const myHandler: Handler = async (event) => { - const { httpMethod, headers, rawUrl, body: requestBody, isBase64Encoded } = event; - const init: RequestInit = { - method: httpMethod, - headers: new Headers(headers as any), - }; - // Attach the event body the request, with proper encoding. - if (httpMethod !== 'GET' && httpMethod !== 'HEAD') { - const encoding = isBase64Encoded ? 'base64' : 'utf-8'; - init.body = - typeof requestBody === 'string' ? Buffer.from(requestBody, encoding) : requestBody; - } - - const request = new Request(rawUrl, init); - - const routeData = app.match(request); - const ip = headers['x-nf-client-connection-ip']; - Reflect.set(request, clientAddressSymbol, ip); - - let locals: Record = {}; - - if (request.headers.has(ASTRO_LOCALS_HEADER)) { - let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER); - if (localsAsString) { - locals = JSON.parse(localsAsString); - } - } - - let responseTtl = undefined; - - locals.runtime = builders - ? { - setBuildersTtl(ttl: number) { - responseTtl = ttl; - }, - } - : {}; - - const response: Response = await app.render(request, routeData, locals); - const responseHeaders = Object.fromEntries(response.headers.entries()); - - const responseContentType = parseContentType(responseHeaders['content-type']); - const responseIsBase64Encoded = knownBinaryMediaTypes.has(responseContentType); - - let responseBody: string; - if (responseIsBase64Encoded) { - const ab = await response.arrayBuffer(); - responseBody = Buffer.from(ab).toString('base64'); - } else { - responseBody = await response.text(); - } - - const fnResponse: any = { - statusCode: response.status, - headers: responseHeaders, - body: responseBody, - isBase64Encoded: responseIsBase64Encoded, - ttl: responseTtl, - }; - - const cookies = response.headers.get('set-cookie'); - if (cookies) { - fnResponse.multiValueHeaders = { - 'set-cookie': Array.isArray(cookies) ? cookies : splitCookiesString(cookies), - }; - } - - // Apply cookies set via Astro.cookies.set/delete - if (app.setCookieHeaders) { - const setCookieHeaders = Array.from(app.setCookieHeaders(response)); - fnResponse.multiValueHeaders = fnResponse.multiValueHeaders || {}; - if (!fnResponse.multiValueHeaders['set-cookie']) { - fnResponse.multiValueHeaders['set-cookie'] = []; - } - fnResponse.multiValueHeaders['set-cookie'].push(...setCookieHeaders); - } - - return fnResponse; - }; - - const handler = builders ? builder(myHandler) : myHandler; - - return { handler }; -}; - -/* - From: https://github.com/nfriedly/set-cookie-parser/blob/5cae030d8ef0f80eec58459e3583d43a07b984cb/lib/set-cookie.js#L144 - Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas - that are within a single set-cookie field-value, such as in the Expires portion. - This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 - Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 - React Native's fetch does this for *every* header, including set-cookie. - Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 - Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation -*/ -function splitCookiesString(cookiesString: string): string[] { - if (Array.isArray(cookiesString)) { - return cookiesString; - } - if (typeof cookiesString !== 'string') { - return []; - } - - let cookiesStrings = []; - let pos = 0; - let start; - let ch; - let lastComma; - let nextStart; - let cookiesSeparatorFound; - - function skipWhitespace() { - while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { - pos += 1; - } - return pos < cookiesString.length; - } - - function notSpecialChar() { - ch = cookiesString.charAt(pos); - - return ch !== '=' && ch !== ';' && ch !== ','; - } - - while (pos < cookiesString.length) { - start = pos; - cookiesSeparatorFound = false; - - while (skipWhitespace()) { - ch = cookiesString.charAt(pos); - if (ch === ',') { - // ',' is a cookie separator if we have later first '=', not ';' or ',' - lastComma = pos; - pos += 1; - - skipWhitespace(); - nextStart = pos; - - while (pos < cookiesString.length && notSpecialChar()) { - pos += 1; - } - - // currently special character - if (pos < cookiesString.length && cookiesString.charAt(pos) === '=') { - // we found cookies separator - cookiesSeparatorFound = true; - // pos is inside the next cookie, so back up and return it. - pos = nextStart; - cookiesStrings.push(cookiesString.substring(start, lastComma)); - start = pos; - } else { - // in param ',' or param separator ';', - // we continue from that comma - pos = lastComma + 1; - } - } else { - pos += 1; - } - } - - if (!cookiesSeparatorFound || pos >= cookiesString.length) { - cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); - } - } - - return cookiesStrings; -} diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts deleted file mode 100644 index fca3d5f0c0..0000000000 --- a/packages/integrations/netlify/src/shared.ts +++ /dev/null @@ -1,114 +0,0 @@ -import fs from 'node:fs'; -import npath from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects'; -import type { AstroConfig, RouteData } from 'astro'; -import esbuild from 'esbuild'; - -export const DENO_SHIM = `globalThis.process = { - argv: [], - env: Deno.env.toObject(), -};`; - -export interface NetlifyEdgeFunctionsOptions { - dist?: URL; -} - -export interface NetlifyEdgeFunctionManifestFunctionPath { - function: string; - path: string; -} - -export interface NetlifyEdgeFunctionManifestFunctionPattern { - function: string; - pattern: string; -} - -export type NetlifyEdgeFunctionManifestFunction = - | NetlifyEdgeFunctionManifestFunctionPath - | NetlifyEdgeFunctionManifestFunctionPattern; - -export interface NetlifyEdgeFunctionManifest { - functions: NetlifyEdgeFunctionManifestFunction[]; - version: 1; -} - -export async function createRedirects( - config: AstroConfig, - routeToDynamicTargetMap: Map, - dir: URL -) { - const _redirectsURL = new URL('./_redirects', dir); - - const _redirects = createRedirectsFromAstroRoutes({ - config, - routeToDynamicTargetMap, - dir, - }); - const content = _redirects.print(); - - // Always use appendFile() because the redirects file could already exist, - // e.g. due to a `/public/_redirects` file that got copied to the output dir. - // If the file does not exist yet, appendFile() automatically creates it. - await fs.promises.appendFile(_redirectsURL, content, 'utf-8'); -} - -export async function createEdgeManifest(routes: RouteData[], entryFile: string, dir: URL) { - const functions: NetlifyEdgeFunctionManifestFunction[] = []; - for (const route of routes) { - if (route.pathname) { - functions.push({ - function: entryFile, - path: route.pathname, - }); - } else { - functions.push({ - function: entryFile, - // Make route pattern serializable to match expected - // Netlify Edge validation format. Mirrors Netlify's own edge bundler: - // https://github.com/netlify/edge-bundler/blob/main/src/manifest.ts#L34 - pattern: route.pattern.source.replace(/\\\//g, '/').toString(), - }); - } - } - - const manifest: NetlifyEdgeFunctionManifest = { - functions, - version: 1, - }; - - const baseDir = new URL('./.netlify/edge-functions/', dir); - await fs.promises.mkdir(baseDir, { recursive: true }); - - const manifestURL = new URL('./manifest.json', baseDir); - const _manifest = JSON.stringify(manifest, null, ' '); - await fs.promises.writeFile(manifestURL, _manifest, 'utf-8'); -} - -export async function bundleServerEntry(entryUrl: URL, serverUrl?: URL, vite?: any | undefined) { - const pth = fileURLToPath(entryUrl); - await esbuild.build({ - target: 'es2020', - platform: 'browser', - entryPoints: [pth], - outfile: pth, - allowOverwrite: true, - format: 'esm', - bundle: true, - external: ['@astrojs/markdown-remark', 'astro/middleware'], - banner: { - js: DENO_SHIM, - }, - }); - - // Remove chunks, if they exist. Since we have bundled via esbuild these chunks are trash. - if (vite && serverUrl) { - try { - const chunkFileNames = - vite?.build?.rollupOptions?.output?.chunkFileNames ?? `chunks/chunk.[hash].mjs`; - const chunkPath = npath.dirname(chunkFileNames); - const chunksDirUrl = new URL(chunkPath + '/', serverUrl); - await fs.promises.rm(chunksDirUrl, { recursive: true, force: true }); - } catch {} - } -} diff --git a/packages/integrations/netlify/src/ssr-function.ts b/packages/integrations/netlify/src/ssr-function.ts new file mode 100644 index 0000000000..c2b6ed14c3 --- /dev/null +++ b/packages/integrations/netlify/src/ssr-function.ts @@ -0,0 +1,56 @@ +import type { Context } from '@netlify/functions'; +import type { SSRManifest } from 'astro'; +import { App } from 'astro/app'; +import { applyPolyfills } from 'astro/app/node'; + +applyPolyfills(); + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface Args {} + +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + +export const createExports = (manifest: SSRManifest, _args: Args) => { + const app = new App(manifest); + + function createHandler(integrationConfig: { cacheOnDemandPages: boolean }) { + return async function handler(request: Request, context: Context) { + const routeData = app.match(request); + Reflect.set(request, clientAddressSymbol, context.ip); + + let locals: Record = {}; + + if (request.headers.has('x-astro-locals')) { + locals = JSON.parse(request.headers.get('x-astro-locals')!); + } + + locals.netlify = { context }; + + const response = await app.render(request, routeData, locals); + + if (app.setCookieHeaders) { + for (const setCookieHeader of app.setCookieHeaders(response)) { + response.headers.append('Set-Cookie', setCookieHeader); + } + } + + if (integrationConfig.cacheOnDemandPages) { + // any user-provided Cache-Control headers take precedence + const hasCacheControl = [ + 'Cache-Control', + 'CDN-Cache-Control', + 'Netlify-CDN-Cache-Control', + ].some((header) => response.headers.has(header)); + + if (!hasCacheControl) { + // caches this page for up to a year + response.headers.append('CDN-Cache-Control', 'public, max-age=31536000, must-revalidate'); + } + } + + return response; + }; + } + + return { default: createHandler }; +}; diff --git a/packages/integrations/netlify/src/static.ts b/packages/integrations/netlify/src/static.ts new file mode 100644 index 0000000000..4748f384aa --- /dev/null +++ b/packages/integrations/netlify/src/static.ts @@ -0,0 +1,6 @@ +import netlifyIntegration from "./index.js" + +export default function staticIntegration() { + console.warn("The @astrojs/netlify/static import is deprecated and will be removed in a future release. Please use @astrojs/netlify instead.") + return netlifyIntegration() +} \ No newline at end of file diff --git a/packages/integrations/netlify/src/types.d.ts b/packages/integrations/netlify/src/types.d.ts new file mode 100644 index 0000000000..0df35e9e9e --- /dev/null +++ b/packages/integrations/netlify/src/types.d.ts @@ -0,0 +1 @@ +declare module "*.json"; \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/404.test.js b/packages/integrations/netlify/test/functions/404.test.js deleted file mode 100644 index 7a5103e377..0000000000 --- a/packages/integrations/netlify/test/functions/404.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import { fileURLToPath } from 'url'; -import { expect } from 'chai'; -import fs from 'fs/promises'; -import { cli } from './test-utils.js'; - -const root = new URL('./fixtures/404/', import.meta.url).toString(); - -describe('404 page', () => { - before(async () => { - await cli('build', '--root', fileURLToPath(root)); - }); - - it('404 route is included in the redirect file', async () => { - const redir = await fs.readFile(new URL('./dist/_redirects', root), 'utf-8'); - const expr = new RegExp('/* /.netlify/functions/entry 404'); - expect(redir).to.match(expr); - }); -}); diff --git a/packages/integrations/netlify/test/functions/base.test.js b/packages/integrations/netlify/test/functions/base.test.js deleted file mode 100644 index afb3fe0b82..0000000000 --- a/packages/integrations/netlify/test/functions/base.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { fileURLToPath } from 'url'; -import { expect } from 'chai'; -import fs from 'fs/promises'; -import { cli } from './test-utils.js'; - -const root = new URL('./fixtures/base/', import.meta.url).toString(); - -describe('Base', () => { - before(async () => { - await cli('build', '--root', fileURLToPath(root)); - }); - - it('Path is prepended by base', async () => { - const redir = await fs.readFile(new URL('./dist/_redirects', root), 'utf-8'); - const baseRouteIndex = redir.indexOf('/test/ /.netlify/functions/entry 200'); - const imageEndpoint = redir.indexOf('/test/_image /.netlify/functions/entry 200'); - - expect(baseRouteIndex).to.not.be.equal(-1); - expect(imageEndpoint).to.not.be.equal(-1); - }); -}); diff --git a/packages/integrations/netlify/test/functions/base64-response.test.js b/packages/integrations/netlify/test/functions/base64-response.test.js deleted file mode 100644 index 4ea36ade3e..0000000000 --- a/packages/integrations/netlify/test/functions/base64-response.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { fileURLToPath } from 'url'; -import { expect } from 'chai'; -import { cli } from './test-utils.js'; - -const root = new URL('./fixtures/base64-response/', import.meta.url).toString(); - -describe('Base64 Responses', () => { - before(async () => { - await cli('build', '--root', fileURLToPath(root)); - }); - - it('Can return base64 encoded strings', async () => { - const entryURL = new URL( - './fixtures/base64-response/.netlify/functions-internal/entry.mjs', - import.meta.url - ); - const { handler } = await import(entryURL); - const resp = await handler({ - httpMethod: 'GET', - headers: {}, - rawUrl: 'http://example.com/image', - body: '{}', - isBase64Encoded: false, - }); - expect(resp.statusCode, 'successful response').to.equal(200); - expect(resp.isBase64Encoded, 'includes isBase64Encoded flag').to.be.true; - - const buffer = Buffer.from(resp.body, 'base64'); - expect(buffer.toString(), 'decoded base64 string matches').to.equal('base64 test string'); - }); - - it('Can define custom binaryMediaTypes', async () => { - const entryURL = new URL( - './fixtures/base64-response/.netlify/functions-internal/entry.mjs', - import.meta.url - ); - const { handler } = await import(entryURL); - const resp = await handler({ - httpMethod: 'GET', - headers: {}, - rawUrl: 'http://example.com/font', - body: '{}', - isBase64Encoded: false, - }); - expect(resp.statusCode, 'successful response').to.equal(200); - expect(resp.isBase64Encoded, 'includes isBase64Encoded flag').to.be.true; - - const buffer = Buffer.from(resp.body, 'base64'); - expect(buffer.toString(), 'decoded base64 string matches').to.equal('base64 test font'); - }); -}); diff --git a/packages/integrations/netlify/test/functions/builders.test.js b/packages/integrations/netlify/test/functions/builders.test.js deleted file mode 100644 index b5b5c04c20..0000000000 --- a/packages/integrations/netlify/test/functions/builders.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { fileURLToPath } from 'url'; -import { expect } from 'chai'; -import { cli } from './test-utils.js'; - -const root = new URL('./fixtures/builders/', import.meta.url).toString(); - -describe('Builders', () => { - before(async () => { - await cli('build', '--root', fileURLToPath(root)); - }); - - it('A route can set builders ttl', async () => { - const entryURL = new URL( - './fixtures/builders/.netlify/functions-internal/entry.mjs', - import.meta.url - ); - const { handler } = await import(entryURL); - const resp = await handler({ - httpMethod: 'GET', - headers: {}, - rawUrl: 'http://example.com/', - isBase64Encoded: false, - }); - expect(resp.ttl).to.equal(45); - }); -}); diff --git a/packages/integrations/netlify/test/functions/cookies.test.js b/packages/integrations/netlify/test/functions/cookies.test.js index c183b34b3e..54f7764991 100644 --- a/packages/integrations/netlify/test/functions/cookies.test.js +++ b/packages/integrations/netlify/test/functions/cookies.test.js @@ -1,31 +1,23 @@ -import { fileURLToPath } from 'url'; import { expect } from 'chai'; -import { cli } from './test-utils.js'; - -const root = new URL('./fixtures/cookies/', import.meta.url).toString(); +import { loadFixture } from "@astrojs/test-utils" describe('Cookies', () => { + let fixture; + before(async () => { - await cli('build', '--root', fileURLToPath(root)); + fixture = await loadFixture({ root: new URL('./fixtures/cookies/', import.meta.url) }); + await fixture.build(); }); it('Can set multiple', async () => { const entryURL = new URL( - './fixtures/cookies/.netlify/functions-internal/entry.mjs', + './fixtures/cookies/.netlify/functions-internal/ssr/ssr.mjs', import.meta.url ); - const { handler } = await import(entryURL); - const resp = await handler({ - httpMethod: 'POST', - headers: {}, - rawUrl: 'http://example.com/login', - body: '{}', - isBase64Encoded: false, - }); - expect(resp.statusCode).to.equal(301); - expect(resp.headers.location).to.equal('/'); - expect(resp.multiValueHeaders).to.be.deep.equal({ - 'set-cookie': ['foo=foo; HttpOnly', 'bar=bar; HttpOnly'], - }); + const { default: handler } = await import(entryURL); + const resp = await handler(new Request('http://example.com/login', { method: "POST", body: '{}' }), {}) + expect(resp.status).to.equal(301); + expect(resp.headers.get("location")).to.equal('/'); + expect(resp.headers.getSetCookie()).to.eql(['foo=foo; HttpOnly', 'bar=bar; HttpOnly']); }); }); diff --git a/packages/integrations/netlify/test/functions/dynamic-route.test.js b/packages/integrations/netlify/test/functions/dynamic-route.test.js deleted file mode 100644 index d5616a9250..0000000000 --- a/packages/integrations/netlify/test/functions/dynamic-route.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import { fileURLToPath } from 'url'; -import { expect } from 'chai'; -import fs from 'fs/promises'; -import { cli } from './test-utils.js'; - -const root = new URL('./fixtures/dynamic-route/', import.meta.url).toString(); - -describe('Dynamic pages', () => { - before(async () => { - await cli('build', '--root', fileURLToPath(root)); - }); - - it('Dynamic pages are included in the redirects file', async () => { - const redir = await fs.readFile(new URL('./dist/_redirects', root), 'utf-8'); - expect(redir).to.match(/\/products\/:id/); - }); - - it('Prerendered routes are also included using placeholder syntax', async () => { - const redir = await fs.readFile(new URL('./dist/_redirects', root), 'utf-8'); - expect(redir).to.include('/pets/:cat /pets/:cat/index.html 200'); - expect(redir).to.include('/pets/:dog /pets/:dog/index.html 200'); - expect(redir).to.include('/pets /.netlify/functions/entry 200'); - }); -}); diff --git a/packages/integrations/netlify/test/functions/edge-middleware.test.js b/packages/integrations/netlify/test/functions/edge-middleware.test.js index 0f5001c467..412066d1d3 100644 --- a/packages/integrations/netlify/test/functions/edge-middleware.test.js +++ b/packages/integrations/netlify/test/functions/edge-middleware.test.js @@ -1,26 +1,45 @@ -import { fileURLToPath } from 'url'; import { expect } from 'chai'; -import fs from 'fs/promises'; -import { cli } from './test-utils.js'; +import { loadFixture } from "@astrojs/test-utils" describe('Middleware', () => { - it('with edge handle file, should successfully build the middleware', async () => { - const root = new URL('./fixtures/middleware-with-handler-file/', import.meta.url).toString(); - await cli('build', '--root', fileURLToPath(root)); - const contents = await fs.readFile( - new URL('./.netlify/edge-functions/edgeMiddleware.js', root), - 'utf-8' - ); - expect(contents.includes('"Hello world"')).to.be.true; - }); + const root = new URL('./fixtures/middleware/', import.meta.url) - it('without edge handle file, should successfully build the middleware', async () => { - const root = new URL('./fixtures/middleware-without-handler-file/', import.meta.url).toString(); - await cli('build', '--root', fileURLToPath(root)); - const contents = await fs.readFile( - new URL('./.netlify/edge-functions/edgeMiddleware.js', root), - 'utf-8' - ); - expect(contents.includes('"Hello world"')).to.be.false; - }); + describe("edgeMiddleware: false", () => { + let fixture + before(async () => { + process.env.EDGE_MIDDLEWARE = 'false'; + fixture = await loadFixture({ root }); + await fixture.build(); + }) + + it('emits no edge function', async () => { + expect(fixture.pathExists('../.netlify/edge-functions/middleware/middleware.mjs')).to.be.false + }); + + it('applies middleware to static files at build-time', async () => { + // prerendered page has middleware applied at build time + const prerenderedPage = await fixture.readFile('prerender/index.html') + expect(prerenderedPage).to.contain("Middleware") + }); + }) + + + describe("edgeMiddleware: true", () => { + let fixture + before(async () => { + process.env.EDGE_MIDDLEWARE = 'true'; + fixture = await loadFixture({ root }); + await fixture.build(); + }) + + it('emits an edge function', async () => { + const contents = await fixture.readFile('../.netlify/edge-functions/middleware/middleware.mjs') + expect(contents.includes('"Hello world"')).to.be.false; + }) + + it('does not apply middleware during prerendering', async () => { + const prerenderedPage = await fixture.readFile('prerender/index.html') + expect(prerenderedPage).to.contain("") + }) + }) }); diff --git a/packages/integrations/netlify/test/functions/fixtures/404/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/404/astro.config.mjs deleted file mode 100644 index 933d0491e2..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/404/astro.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import netlify from '@astrojs/netlify'; -import { defineConfig } from 'astro/config'; - -export default defineConfig({ - output: 'server', - adapter: netlify(), - site: `http://example.com`, -}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/404/src/pages/404.astro b/packages/integrations/netlify/test/functions/fixtures/404/src/pages/404.astro deleted file mode 100644 index b60b5e55a1..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/404/src/pages/404.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- - ---- - - - Not found - - -

Not found

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/404/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/404/src/pages/index.astro deleted file mode 100644 index 5ed06d2517..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/404/src/pages/index.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- - ---- - - - Testing - - -

Testing

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/base/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/base/astro.config.mjs deleted file mode 100644 index 1205043601..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base/astro.config.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import netlify from '@astrojs/netlify'; -import { defineConfig } from 'astro/config'; - -export default defineConfig({ - base: "/test", - trailingSlash: "always", - output: 'server', - adapter: netlify(), - site: `http://example.com`, -}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/base/package.json b/packages/integrations/netlify/test/functions/fixtures/base/package.json deleted file mode 100644 index 2b0b141257..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/netlify-base", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/netlify": "workspace:" - } -} diff --git a/packages/integrations/netlify/test/functions/fixtures/base/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/base/src/env.d.ts deleted file mode 100644 index 8c34fb45e7..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/base/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/base/src/pages/index.astro deleted file mode 100644 index 5ed06d2517..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base/src/pages/index.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- - ---- - - - Testing - - -

Testing

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/base64-response/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/base64-response/astro.config.mjs deleted file mode 100644 index f79a23fb06..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base64-response/astro.config.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import netlify from '@astrojs/netlify'; -import { defineConfig } from 'astro/config'; - -export default defineConfig({ - output: 'server', - adapter: netlify({ - binaryMediaTypes: ['font/otf'], - }), - site: `http://example.com`, -}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/base64-response/package.json b/packages/integrations/netlify/test/functions/fixtures/base64-response/package.json deleted file mode 100644 index 1e4d64dd45..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base64-response/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/netlify-base64-response", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/netlify": "workspace:" - } -} diff --git a/packages/integrations/netlify/test/functions/fixtures/base64-response/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/base64-response/src/env.d.ts deleted file mode 100644 index 8c34fb45e7..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base64-response/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/base64-response/src/pages/font.js b/packages/integrations/netlify/test/functions/fixtures/base64-response/src/pages/font.js deleted file mode 100644 index abe2677f03..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base64-response/src/pages/font.js +++ /dev/null @@ -1,11 +0,0 @@ - -export function GET() { - const buffer = Buffer.from('base64 test font', 'utf-8') - - return new Response(buffer, { - status: 200, - headers: { - 'Content-Type': 'font/otf' - } - }); -} diff --git a/packages/integrations/netlify/test/functions/fixtures/base64-response/src/pages/image.js b/packages/integrations/netlify/test/functions/fixtures/base64-response/src/pages/image.js deleted file mode 100644 index 3cd266481c..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/base64-response/src/pages/image.js +++ /dev/null @@ -1,11 +0,0 @@ - -export function GET() { - const buffer = Buffer.from('base64 test string', 'utf-8') - - return new Response(buffer, { - status: 200, - headers: { - 'content-type': 'image/jpeg;foo=foo' - } - }); -} diff --git a/packages/integrations/netlify/test/functions/fixtures/builders/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/builders/astro.config.mjs deleted file mode 100644 index e5339a7653..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/builders/astro.config.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import netlify from '@astrojs/netlify'; -import { defineConfig } from 'astro/config'; - -export default defineConfig({ - output: 'server', - adapter: netlify({ - builders: true, - }), - site: `http://example.com`, -}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/builders/package.json b/packages/integrations/netlify/test/functions/fixtures/builders/package.json deleted file mode 100644 index 998de0393f..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/builders/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/netlify-builders", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/netlify": "workspace:" - } -} diff --git a/packages/integrations/netlify/test/functions/fixtures/builders/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/builders/src/env.d.ts deleted file mode 100644 index 8c34fb45e7..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/builders/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro deleted file mode 100644 index ab8853785f..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -Astro.locals.runtime.setBuildersTtl(45) ---- - - - Astro on Netlify - - -

{new Date(Date.now())}

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/cookies/astro.config.mjs index 908422c349..933d0491e2 100644 --- a/packages/integrations/netlify/test/functions/fixtures/cookies/astro.config.mjs +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/astro.config.mjs @@ -3,8 +3,6 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ output: 'server', - adapter: netlify({ - - }), + adapter: netlify(), site: `http://example.com`, }); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/package.json b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/package.json deleted file mode 100644 index 040987227e..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/netlify-dynamic", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/netlify": "workspace:" - } -} diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/env.d.ts deleted file mode 100644 index 8c34fb45e7..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[cat].astro b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[cat].astro deleted file mode 100644 index f86ee6ca93..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[cat].astro +++ /dev/null @@ -1,27 +0,0 @@ ---- -export const prerender = true - -export function getStaticPaths() { - return [ - { - params: {cat: 'cat1'}, - props: {cat: 'cat1'} - }, - { - params: {cat: 'cat2'}, - props: {cat: 'cat2'} - }, - { - params: {cat: 'cat3'}, - props: {cat: 'cat3'} - }, - ]; -} - -const { cat } = Astro.props; - ---- - -
Good cat, {cat}!
- -back diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[dog].astro b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[dog].astro deleted file mode 100644 index 0f3300f045..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[dog].astro +++ /dev/null @@ -1,27 +0,0 @@ ---- -export const prerender = true - -export function getStaticPaths() { - return [ - { - params: {dog: 'dog1'}, - props: {dog: 'dog1'} - }, - { - params: {dog: 'dog2'}, - props: {dog: 'dog2'} - }, - { - params: {dog: 'dog3'}, - props: {dog: 'dog3'} - }, - ]; -} - -const { dog } = Astro.props; - ---- - -
Good dog, {dog}!
- -back diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/index.astro b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/index.astro deleted file mode 100644 index d1423f8ef2..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/index.astro +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - Astro - - -

Astro

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/products/[id].astro b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/products/[id].astro deleted file mode 100644 index 5ed06d2517..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/products/[id].astro +++ /dev/null @@ -1,11 +0,0 @@ ---- - ---- - - - Testing - - -

Testing

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/astro.config.mjs deleted file mode 100644 index 3d9c59251c..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/astro.config.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import netlify from '@astrojs/netlify'; -import { defineConfig } from 'astro/config'; - -export default defineConfig({ - output: 'server', - adapter: netlify({ - edgeMiddleware: true, - }), - site: `http://example.com`, -}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/package.json b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/package.json deleted file mode 100644 index 3fe59b19bf..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/netlify-middleware-with-handler-file", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/netlify": "workspace:" - } -} diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/env.d.ts deleted file mode 100644 index 8c34fb45e7..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js deleted file mode 100644 index bf69edb3e8..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js +++ /dev/null @@ -1,5 +0,0 @@ -export default function ({ request, context }) { - return { - title: 'Hello world', - }; -} diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/astro.config.mjs deleted file mode 100644 index 3d9c59251c..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/astro.config.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import netlify from '@astrojs/netlify'; -import { defineConfig } from 'astro/config'; - -export default defineConfig({ - output: 'server', - adapter: netlify({ - edgeMiddleware: true, - }), - site: `http://example.com`, -}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/env.d.ts deleted file mode 100644 index 8c34fb45e7..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts deleted file mode 100644 index 8cab418c1c..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const onRequest = (context, next) => { - context.locals.title = 'Middleware'; - - return next(); -}; diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/middleware/astro.config.mjs similarity index 76% rename from packages/integrations/netlify/test/functions/fixtures/dynamic-route/astro.config.mjs rename to packages/integrations/netlify/test/functions/fixtures/middleware/astro.config.mjs index 3d9c59251c..aefd9805c1 100644 --- a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/astro.config.mjs +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/astro.config.mjs @@ -4,7 +4,7 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ output: 'server', adapter: netlify({ - edgeMiddleware: true, + edgeMiddleware: process.env.EDGE_MIDDLEWARE === 'true', }), site: `http://example.com`, }); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/package.json b/packages/integrations/netlify/test/functions/fixtures/middleware/package.json similarity index 100% rename from packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/package.json rename to packages/integrations/netlify/test/functions/fixtures/middleware/package.json diff --git a/packages/integrations/netlify/test/functions/fixtures/404/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/middleware/src/env.d.ts similarity index 100% rename from packages/integrations/netlify/test/functions/fixtures/404/src/env.d.ts rename to packages/integrations/netlify/test/functions/fixtures/middleware/src/env.d.ts diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/middleware/src/middleware.ts similarity index 100% rename from packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts rename to packages/integrations/netlify/test/functions/fixtures/middleware/src/middleware.ts diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/index.astro similarity index 100% rename from packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro rename to packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/index.astro diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/prerender.astro similarity index 80% rename from packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro rename to packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/prerender.astro index d97f706989..f0314c0533 100644 --- a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/prerender.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; const title = Astro.locals.title; --- diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/prerender/astro.config.mjs deleted file mode 100644 index e9e11092c0..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/prerender/astro.config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'astro/config'; -import netlifyAdapter from '../../../../dist/index.js'; - - -export default defineConfig({ - output: process.env.ASTRO_OUTPUT || 'server', - adapter: netlifyAdapter({ - dist: new URL('./dist/', import.meta.url), - }), - site: `http://example.com`, -}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/package.json b/packages/integrations/netlify/test/functions/fixtures/prerender/package.json deleted file mode 100644 index 78c1d7d827..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/prerender/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/netlify-prerender", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/netlify": "workspace:" - } -} diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/prerender/src/env.d.ts deleted file mode 100644 index 8c34fb45e7..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/prerender/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/404.astro b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/404.astro deleted file mode 100644 index ad5d44aa21..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/404.astro +++ /dev/null @@ -1,8 +0,0 @@ - - - Testing - - -

testing

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro deleted file mode 100644 index 852d00b7b5..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro +++ /dev/null @@ -1,8 +0,0 @@ - - - Blog - - -

Blog

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/one.astro b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/one.astro deleted file mode 100644 index 342e98cfaa..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/one.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -export const prerender = import.meta.env.PRERENDER; ---- - - - Testing - - -

testing

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/split-support/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/split-support/astro.config.mjs deleted file mode 100644 index 22ac46ac6a..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/split-support/astro.config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import netlify from '@astrojs/netlify'; -import { defineConfig } from 'astro/config'; - -export default defineConfig({ - output: 'server', - adapter: netlify({ - dist: new URL('./fixtures/split-support/dist/', import.meta.url), - functionPerRoute: true, - }), - site: `http://example.com`, -}); diff --git a/packages/integrations/netlify/test/functions/fixtures/split-support/package.json b/packages/integrations/netlify/test/functions/fixtures/split-support/package.json deleted file mode 100644 index 806de870fb..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/split-support/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/netlify-split-support", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/netlify": "workspace:" - } -} diff --git a/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/blog.astro b/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/blog.astro deleted file mode 100644 index 248c2218be..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/blog.astro +++ /dev/null @@ -1,8 +0,0 @@ - - - Testing - - -

testing

- - diff --git a/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/index.astro deleted file mode 100644 index 852d00b7b5..0000000000 --- a/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/index.astro +++ /dev/null @@ -1,8 +0,0 @@ - - - Blog - - -

Blog

- - diff --git a/packages/integrations/netlify/test/functions/prerender.test.js b/packages/integrations/netlify/test/functions/prerender.test.js deleted file mode 100644 index 59bc2495f4..0000000000 --- a/packages/integrations/netlify/test/functions/prerender.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import { fileURLToPath } from 'url'; -import { expect } from 'chai'; -import fs from 'fs/promises'; -import { cli } from './test-utils.js'; - -const root = new URL('./fixtures/prerender/', import.meta.url).toString(); - -describe('Mixed Prerendering with SSR', () => { - before(async () => { - process.env.PRERENDER = true; - await cli('build', '--root', fileURLToPath(root)); - }); - - after(() => { - delete process.env.PRERENDER; - }); - - it('Wildcard 404 is sorted last', async () => { - const redir = await fs.readFile(new URL('./dist/_redirects', root), 'utf-8'); - const baseRouteIndex = redir.indexOf('/ /.netlify/functions/entry 200'); - const oneRouteIndex = redir.indexOf('/one /one/index.html 200'); - const fourOhFourWildCardIndex = redir.indexOf('/* /.netlify/functions/entry 404'); - - expect(oneRouteIndex).to.not.be.equal(-1); - expect(fourOhFourWildCardIndex).to.be.greaterThan(baseRouteIndex); - expect(fourOhFourWildCardIndex).to.be.greaterThan(oneRouteIndex); - }); -}); - -describe('Mixed Hybrid rendering with SSR', () => { - before(async () => { - process.env.PRERENDER = false; - process.env.ASTRO_OUTPUT = 'hybrid'; - await cli('build', '--root', fileURLToPath(root)); - }); - - after(() => { - delete process.env.PRERENDER; - }); - - it('outputs a correct redirect file', async () => { - const redir = await fs.readFile(new URL('./dist/_redirects', root), 'utf-8'); - const baseRouteIndex = redir.indexOf('/one /.netlify/functions/entry 200'); - const rootRouteIndex = redir.indexOf('/ /index.html 200'); - const fourOhFourIndex = redir.indexOf('/404 /404.html 200'); - const imageEndpoint = redir.indexOf('/_image /.netlify/functions/entry 200'); - - expect(rootRouteIndex).to.not.be.equal(-1); - expect(baseRouteIndex).to.not.be.equal(-1); - expect(fourOhFourIndex).to.not.be.equal(-1); - expect(imageEndpoint).to.not.be.equal(-1); - }); -}); diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js index 45b6140c31..c2a705b0b6 100644 --- a/packages/integrations/netlify/test/functions/redirects.test.js +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -1,40 +1,23 @@ -import { fileURLToPath } from 'url'; import { expect } from 'chai'; -import fs from 'fs/promises'; -import { cli } from './test-utils.js'; +import { loadFixture } from "@astrojs/test-utils" -const root = new URL('../functions/fixtures/redirects/', import.meta.url).toString(); +describe('SSR - Redirects', () => { + let fixture; -describe('SSG - Redirects', () => { before(async () => { - await cli('build', '--root', fileURLToPath(root)); + fixture = await loadFixture({ root: new URL('./fixtures/redirects/', import.meta.url) }); + await fixture.build(); }); it('Creates a redirects file', async () => { - let redirects = await fs.readFile(new URL('./dist/_redirects', root), 'utf-8'); + let redirects = await fixture.readFile('./_redirects'); let parts = redirects.split(/\s+/); expect(parts).to.deep.equal([ + '', '/other', '/', '301', - // This uses the dynamic Astro.redirect, so we don't know that it's a redirect - // until runtime. This is correct! - '/nope', - '/.netlify/functions/entry', - '200', - '/', - '/.netlify/functions/entry', - '200', - - // Image endpoint - '/_image', - '/.netlify/functions/entry', - '200', - - // A real route - '/team/articles/*', - '/.netlify/functions/entry', - '200', + '', ]); expect(redirects).to.matchSnapshot(); }); diff --git a/packages/integrations/netlify/test/functions/redirects.test.js.snap b/packages/integrations/netlify/test/functions/redirects.test.js.snap index 54095f0527..b781001e45 100644 --- a/packages/integrations/netlify/test/functions/redirects.test.js.snap +++ b/packages/integrations/netlify/test/functions/redirects.test.js.snap @@ -1,9 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SSG - Redirects Creates a redirects file 1`] = ` -"/other / 301 -/nope /.netlify/functions/entry 200 -/ /.netlify/functions/entry 200 -/_image /.netlify/functions/entry 200 -/team/articles/* /.netlify/functions/entry 200" +" +/other / 301 +" +`; + +exports[`SSR - Redirects Creates a redirects file 1`] = ` +" +/other / 301 +" `; diff --git a/packages/integrations/netlify/test/functions/split-support.test.js b/packages/integrations/netlify/test/functions/split-support.test.js deleted file mode 100644 index e3187adcaa..0000000000 --- a/packages/integrations/netlify/test/functions/split-support.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import { loadFixture } from '@astrojs/test-utils'; -import { expect } from 'chai'; -import netlifyAdapter from '../../dist/index.js'; -import { testIntegration } from './test-utils.js'; - -describe('Split support', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let _entryPoints; - - before(async () => { - fixture = await loadFixture({ - root: new URL('./fixtures/split-support/', import.meta.url).toString(), - output: 'server', - adapter: netlifyAdapter({ - dist: new URL('./fixtures/split-support/dist/', import.meta.url), - functionPerRoute: true, - }), - site: `http://example.com`, - integrations: [ - testIntegration({ - setEntryPoints(ep) { - _entryPoints = ep; - }, - }), - ], - }); - await fixture.build(); - }); - - it('outputs a correct redirect file', async () => { - const redir = await fixture.readFile('/_redirects'); - const lines = redir.split(/[\r\n]+/); - expect(lines.length).to.equal(3); - - expect(lines[0].includes('/blog')).to.be.true; - expect(lines[0].includes('blog.astro')).to.be.true; - expect(lines[0].includes('200')).to.be.true; - expect(lines[1].includes('/')).to.be.true; - expect(lines[1].includes('index.astro')).to.be.true; - expect(lines[1].includes('200')).to.be.true; - }); - - describe('Should create multiple functions', () => { - it('and hit 200', async () => { - if (_entryPoints) { - for (const [routeData, filePath] of _entryPoints) { - if (routeData.route !== '/_image') { - const { handler } = await import(filePath.toString()); - const resp = await handler({ - httpMethod: 'GET', - headers: {}, - rawUrl: `http://example.com${routeData.route}`, - body: '{}', - }); - expect(resp.statusCode).to.equal(200); - } - } - } else { - expect(false).to.be.true; - } - }); - }); -}); diff --git a/packages/integrations/netlify/test/functions/test-utils.js b/packages/integrations/netlify/test/functions/test-utils.js deleted file mode 100644 index c977af42e6..0000000000 --- a/packages/integrations/netlify/test/functions/test-utils.js +++ /dev/null @@ -1,34 +0,0 @@ -// @ts-check -import { fileURLToPath } from 'node:url'; - -export * from '../test-utils.js'; - -/** - * - * @returns {import('astro').AstroIntegration} - */ -export function testIntegration({ setEntryPoints } = {}) { - return { - name: '@astrojs/netlify/test-integration', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ - vite: { - resolve: { - alias: { - '@astrojs/netlify/netlify-functions.js': fileURLToPath( - new URL('../../dist/netlify-functions.js', import.meta.url) - ), - }, - }, - }, - }); - }, - 'astro:build:ssr': ({ entryPoints }) => { - if (entryPoints.size) { - setEntryPoints(entryPoints); - } - }, - }, - }; -} diff --git a/packages/integrations/netlify/test/static/fixtures/redirects/astro.config.mjs b/packages/integrations/netlify/test/static/fixtures/redirects/astro.config.mjs new file mode 100644 index 0000000000..c2a53c274d --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/redirects/astro.config.mjs @@ -0,0 +1,17 @@ +import netlify from '@astrojs/netlify'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'static', + adapter: netlify(), + site: `http://example.com`, + site: `http://example.com`, + redirects: { + '/other': '/', + '/two': { + status: 302, + destination: '/', + }, + '/blog/[...slug]': '/team/articles/[...slug]', + }, +}); \ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/404/package.json b/packages/integrations/netlify/test/static/fixtures/redirects/package.json similarity index 70% rename from packages/integrations/netlify/test/functions/fixtures/404/package.json rename to packages/integrations/netlify/test/static/fixtures/redirects/package.json index b08e71e06b..3e543bf353 100644 --- a/packages/integrations/netlify/test/functions/fixtures/404/package.json +++ b/packages/integrations/netlify/test/static/fixtures/redirects/package.json @@ -1,5 +1,5 @@ { - "name": "@test/netlify-fourohfour", + "name": "@test/netlify-static-redirects", "version": "0.0.0", "private": true, "dependencies": { diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js index 0c5751eb81..6e9adc00aa 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -1,50 +1,33 @@ import { expect } from 'chai'; -import { netlifyStatic } from '../../dist/index.js'; -import { loadFixture, testIntegration } from './test-utils.js'; +import { loadFixture } from "@astrojs/test-utils" describe('SSG - Redirects', () => { - /** @type {import('../../../astro/test/test-utils').Fixture} */ let fixture; before(async () => { - fixture = await loadFixture({ - root: new URL('./fixtures/redirects/', import.meta.url).toString(), - output: 'static', - adapter: netlifyStatic(), - site: `http://example.com`, - integrations: [testIntegration()], - redirects: { - '/other': '/', - '/two': { - status: 302, - destination: '/', - }, - '/blog/[...slug]': '/team/articles/[...slug]', - }, - }); + fixture = await loadFixture({ root: new URL('./fixtures/redirects/', import.meta.url) }); await fixture.build(); }); it('Creates a redirects file', async () => { - let redirects = await fixture.readFile('/_redirects'); + const redirects = await fixture.readFile('./_redirects'); let parts = redirects.split(/\s+/); expect(parts).to.deep.equal([ + '', + '/two', '/', '302', + '/other', '/', '301', - '/nope', - '/', - '301', '/blog/*', '/team/articles/*/index.html', '301', - '/team/articles/*', - '/team/articles/*/index.html', - '200', + + '', ]); }); }); diff --git a/packages/integrations/netlify/test/static/test-utils.js b/packages/integrations/netlify/test/static/test-utils.js deleted file mode 100644 index 44fcf84e0e..0000000000 --- a/packages/integrations/netlify/test/static/test-utils.js +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-check -import { fileURLToPath } from 'node:url'; - -export * from '../test-utils.js'; - -/** - * - * @returns {import('astro').AstroIntegration} - */ -export function testIntegration() { - return { - name: '@astrojs/netlify/test-integration', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ - vite: { - resolve: { - alias: { - '@astrojs/netlify/netlify-functions.js': fileURLToPath( - new URL('../../dist/netlify-functions.js', import.meta.url) - ), - }, - }, - }, - }); - }, - }, - }; -} diff --git a/packages/integrations/netlify/tsconfig.json b/packages/integrations/netlify/tsconfig.json index 1a4df9edb5..18443cddf2 100644 --- a/packages/integrations/netlify/tsconfig.json +++ b/packages/integrations/netlify/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.base.json", "include": ["src"], "compilerOptions": { - "outDir": "./dist", - "typeRoots": ["node_modules/@types", "node_modules/@netlify"] + "outDir": "./dist" } }