Remove Netlify adapter from core (#8574)

* New link

* More explicit

* Add placeholder package.json

* lockfile

* add keyworkds
This commit is contained in:
Matthew Phillips 2023-09-28 04:48:26 +08:00 committed by GitHub
parent a10a798c18
commit 4ed410db50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 1940 additions and 3129 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,279 +1,3 @@
# @astrojs/netlify # @astrojs/netlify
This adapter allows Astro to deploy your SSR site to [Netlify](https://www.netlify.com/). The Netlify adapter package has moved. Please see [the new repository for the Netlify adapter](https://github.com/withastro/adapters/tree/main/packages/netlify).
Learn how to deploy your Astro site in our [Netlify deployment guide](https://docs.astro.build/en/guides/deploy/netlify/).
- <strong>[Why Astro Netlify](#why-astro-netlify)</strong>
- <strong>[Installation](#installation)</strong>
- <strong>[Usage](#usage)</strong>
- <strong>[Configuration](#configuration)</strong>
- <strong>[Examples](#examples)</strong>
- <strong>[Troubleshooting](#troubleshooting)</strong>
- <strong>[Contributing](#contributing)</strong>
- <strong>[Changelog](#changelog)</strong>
## Why Astro Netlify
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.
[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.
```sh
# Using NPM
npx astro add netlify
# Using Yarn
yarn astro add netlify
# Using PNPM
pnpm astro add netlify
```
### Add dependencies manually
If you prefer to install the adapter manually instead, complete the following two steps:
1. Install the Netlify adapter to your projects dependencies using your preferred package manager. If youre using npm or arent sure, run this in the terminal:
```bash
npm install @astrojs/netlify
```
1. Add two new lines to your `astro.config.mjs` project configuration file.
```js ins={3, 6-7}
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify(),
});
```
### Run middleware in Edge Functions
When deploying to Netlify Functions, you can choose to use an Edge Function to run your Astro middleware.
To enable this, set the `edgeMiddleware` config option to `true`:
```js ins={9}
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify({
edgeMiddleware: true,
}),
});
```
#### Pass edge context to your site
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 users 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 projects [source directory](https://docs.astro.build/en/reference/configuration-reference/#srcdir). This file must export a function that returns the data to add to [Astros `locals` object](https://docs.astro.build/en/guides/middleware/#locals), which is available in middleware and Astro routes.
In this example, `visitorCountry` and `hasEdgeMiddleware` would both be added to Astros `locals` object:
```ts
// src/netlify-edge-middleware.ts
import type { Context } from 'https://edge.netlify.com';
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,
};
}
```
> **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 Denos URL syntax.
`netlify-edge-middleware.ts` must provide a function as its default export. This function:
- 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.
### Per-page functions
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
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify({
functionPerRoute: true,
}),
});
```
### Static sites
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.
```js
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/static';
export default defineConfig({
adapter: netlify(),
redirects: {
'/blog/old-post': '/blog/new-post',
},
});
```
Once you run `astro build` there will be a `dist/_redirects` file. Netlify will use that to properly route pages in production.
> **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
[Netlify On-demand Builders](https://docs.netlify.com/configure-builds/on-demand-builders/) are serverless functions used to generate web content as needed thats automatically cached on Netlifys Edge CDN. You can enable these functions using the [`builders` configuration](#builders).
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/guides/middleware/#locals) with the duration (in seconds).
The following example sets a revalidation time of 45, causing Netlify to store the rendered HTML for 45 seconds.
```astro
---
import Layout from '../components/Layout.astro';
if (import.meta.env.PROD) {
Astro.locals.runtime.setBuildersTtl(45);
}
---
<Layout title="Astro on Netlify">
{new Date(Date.now())}
</Layout>
```
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 title="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 {12}
// 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',
},
});
}
```
## Examples
- The [Astro Netlify Edge Starter](https://github.com/sarahetter/astro-netlify-edge-starter) provides an example and a guide in the README.
- [Browse Astro Netlify projects on GitHub](https://github.com/search?q=path%3A**%2Fastro.config.mjs+%40astrojs%2Fnetlify&type=code) for more examples!
## Troubleshooting
For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
You can also check our [Astro Integration Documentation][astro-integration] for more on integrations.
## Contributing
This package is maintained by Astro's Core team. You're welcome to submit an issue or PR!
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for a history of changes to this integration.
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/

View file

@ -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;
};
}

View file

@ -1,63 +1,6 @@
{ {
"name": "@astrojs/netlify", "name": "@astrojs/netlify",
"description": "Deploy your site to Netlify", "private": true,
"version": "3.0.2", "keywords": [],
"type": "module", "dont_remove": "This is a placeholder for the same of the docs smoke test"
"types": "./dist/index.d.ts",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/integrations/netlify"
},
"keywords": [
"withastro",
"astro-adapter"
],
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://docs.astro.build/en/guides/integrations-guide/netlify/",
"exports": {
".": "./dist/index.js",
"./functions": "./dist/integration-functions.js",
"./netlify-functions.js": "./dist/netlify-functions.js",
"./edge-functions": "./dist/integration-edge-functions.js",
"./netlify-edge-functions.js": "./dist/netlify-edge-functions.js",
"./package.json": "./package.json"
},
"files": [
"dist"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"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:hosted": "mocha --exit --timeout 30000 test/hosted"
},
"dependencies": {
"@astrojs/underscore-redirects": "workspace:*",
"@netlify/functions": "^2.0.1",
"esbuild": "^0.19.2"
},
"peerDependencies": {
"astro": "workspace:^3.1.4"
},
"devDependencies": {
"@netlify/edge-functions": "^2.0.0",
"@netlify/edge-handler-types": "^0.34.1",
"@types/node": "^18.17.8",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
"chai-jest-snapshot": "^2.0.0",
"cheerio": "1.0.0-rc.12",
"mocha": "^10.2.0",
"vite": "^4.4.9"
},
"astro": {
"external": true
}
} }

View file

@ -1,2 +0,0 @@
export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js';
export { netlifyStatic } from './integration-static.js';

View file

@ -1,142 +0,0 @@
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
import { writeFile } from 'node:fs/promises';
import { extname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
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<RouteData, URL>;
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') {
console.warn(
`[@astrojs/netlify] \`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`
);
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 };

View file

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

View file

@ -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<URL> {
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: "/*"
}
`;
}

View file

@ -1,225 +0,0 @@
import { builder, type Handler } 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<string, unknown> = {};
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;
}

View file

@ -1,114 +0,0 @@
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
import type { AstroConfig, RouteData } from 'astro';
import esbuild from 'esbuild';
import fs from 'node:fs';
import npath from 'node:path';
import { fileURLToPath } from 'node:url';
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<RouteData, string>,
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 {}
}
}

View file

@ -1,27 +0,0 @@
import { expect } from 'chai';
import netlifyAdapter from '../../dist/index.js';
import { loadFixture, testIntegration } from './test-utils.js';
describe('404 page', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/404/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/404/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});
it('404 route is included in the redirect file', async () => {
const redir = await fixture.readFile('/_redirects');
const expr = new RegExp('/* /.netlify/functions/entry 404');
expect(redir).to.match(expr);
});
});

View file

@ -1,62 +0,0 @@
import { expect } from 'chai';
import { loadFixture, testIntegration } from './test-utils.js';
import netlifyAdapter from '../../dist/index.js';
describe('Base64 Responses', () => {
/** @type {import('../../../astro/test/test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/base64-response/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/base64-response/dist/', import.meta.url),
binaryMediaTypes: ['font/otf'],
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});
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');
});
});

View file

@ -1,37 +0,0 @@
import { expect } from 'chai';
import { loadFixture, testIntegration } from './test-utils.js';
import netlifyAdapter from '../../dist/index.js';
describe('Builders', () => {
/** @type {import('../../../astro/test/test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/builders/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/builders/dist/', import.meta.url),
builders: true,
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});
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);
});
});

View file

@ -1,41 +0,0 @@
import { expect } from 'chai';
import { loadFixture, testIntegration } from './test-utils.js';
import netlifyAdapter from '../../dist/index.js';
describe('Cookies', () => {
/** @type {import('../../../astro/test/test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/cookies/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/cookies/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});
it('Can set multiple', async () => {
const entryURL = new URL(
'./fixtures/cookies/.netlify/functions-internal/entry.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'],
});
});
});

View file

@ -1,33 +0,0 @@
import { expect } from 'chai';
import netlifyAdapter from '../../dist/index.js';
import { loadFixture, testIntegration } from './test-utils.js';
describe('Dynamic pages', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/dynamic-route/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/dynamic-route/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});
it('Dynamic pages are included in the redirects file', async () => {
const redir = await fixture.readFile('/_redirects');
expect(redir).to.match(/\/products\/:id/);
});
it('Prerendered routes are also included using placeholder syntax', async () => {
const redir = await fixture.readFile('/_redirects');
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');
});
});

View file

@ -1,44 +0,0 @@
import netlifyAdapter from '../../dist/index.js';
import { testIntegration, loadFixture } from './test-utils.js';
import { expect } from 'chai';
describe('Middleware', () => {
it('with edge handle file, should successfully build the middleware', async () => {
/** @type {import('./test-utils').Fixture} */
const fixture = await loadFixture({
root: new URL('./fixtures/middleware-with-handler-file/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/middleware-with-handler-file/dist/', import.meta.url),
edgeMiddleware: true,
}),
site: `http://example.com`,
integrations: [testIntegration()],
build: {
excludeMiddleware: true,
},
});
await fixture.build();
const contents = await fixture.readFile('../.netlify/edge-functions/edgeMiddleware.js');
expect(contents.includes('"Hello world"')).to.be.true;
});
it('without edge handle file, should successfully build the middleware', async () => {
/** @type {import('./test-utils').Fixture} */
const fixture = await loadFixture({
root: new URL('./fixtures/middleware-without-handler-file/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/middleware-without-handler-file/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
build: {
excludeMiddleware: true,
},
});
await fixture.build();
const contents = await fixture.readFile('../.netlify/edge-functions/edgeMiddleware.js');
expect(contents.includes('"Hello world"')).to.be.false;
});
});

View file

@ -1 +0,0 @@
**/netlify

View file

@ -1,11 +0,0 @@
---
---
<html>
<head>
<title>Not found</title>
</head>
<body>
<h1>Not found</h1>
</body>
</html>

View file

@ -1,11 +0,0 @@
---
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -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'
}
});
}

View file

@ -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'
}
});
}

View file

@ -1,11 +0,0 @@
---
Astro.locals.runtime.setBuildersTtl(45)
---
<html>
<head>
<title>Astro on Netlify</title>
</head>
<body>
<h1>{new Date(Date.now())}</h1>
</body>
</html>

View file

@ -1,6 +0,0 @@
<html>
<head><title>Testing</title></head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -1,12 +0,0 @@
export function post() {
const headers = new Headers();
headers.append('Set-Cookie', `foo=foo; HttpOnly`);
headers.append('Set-Cookie', `bar=bar; HttpOnly`);
headers.append('Location', '/');
return new Response('', {
status: 301,
headers,
});
}

View file

@ -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;
---
<div>Good cat, {cat}!</div>
<a href="/">back</a>

View file

@ -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;
---
<div>Good dog, {dog}!</div>
<a href="/">back</a>

View file

@ -1,12 +0,0 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
</body>
</html>

View file

@ -1,11 +0,0 @@
---
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -1,5 +0,0 @@
export const onRequest = (context, next) => {
context.locals.title = 'Middleware';
return next();
};

View file

@ -1,5 +0,0 @@
export default function ({ request, context }) {
return {
title: 'Hello world',
};
}

View file

@ -1,12 +0,0 @@
---
const title = Astro.locals.title;
---
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
</body>
</html>

View file

@ -1,5 +0,0 @@
export const onRequest = (context, next) => {
context.locals.title = 'Middleware';
return next();
};

View file

@ -1,12 +0,0 @@
---
const title = Astro.locals.title;
---
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
</body>
</html>

View file

@ -1,8 +0,0 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>

View file

@ -1,8 +0,0 @@
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Blog</h1>
</body>
</html>

View file

@ -1,11 +0,0 @@
---
export const prerender = import.meta.env.PRERENDER;
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>

View file

@ -1,9 +0,0 @@
---
export const prerender = false;
---
<html>
<head><title>Testing</title></head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -1,3 +0,0 @@
---
return Astro.redirect('/');
---

View file

@ -1,27 +0,0 @@
---
export const prerender = false;
export const getStaticPaths = (async () => {
const posts = [
{ slug: 'one', data: {draft: false, title: 'One'} },
{ slug: 'two', data: {draft: false, title: 'Two'} }
];
return posts.map((post) => {
return {
params: { slug: post.slug },
props: { draft: post.data.draft, title: post.data.title },
};
});
})
const { slug } = Astro.params;
const { title } = Astro.props;
---
<html>
<head>
<title>{ title }</title>
</head>
<body>
<h1>{ title }</h1>
</body>
</html>

View file

@ -1,8 +0,0 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>

View file

@ -1,8 +0,0 @@
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Blog</h1>
</body>
</html>

View file

@ -1,74 +0,0 @@
import { expect } from 'chai';
import netlifyAdapter from '../../dist/index.js';
import { loadFixture, testIntegration } from './test-utils.js';
describe('Mixed Prerendering with SSR', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
process.env.PRERENDER = true;
fixture = await loadFixture({
root: new URL('./fixtures/prerender/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/prerender/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});
after(() => {
delete process.env.PRERENDER;
});
it('Wildcard 404 is sorted last', async () => {
const redir = await fixture.readFile('/_redirects');
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', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
process.env.PRERENDER = false;
fixture = await loadFixture({
root: new URL('./fixtures/prerender/', import.meta.url).toString(),
output: 'hybrid',
adapter: netlifyAdapter({
dist: new URL('./fixtures/prerender/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});
after(() => {
delete process.env.PRERENDER;
});
it('outputs a correct redirect file', async () => {
const redir = await fixture.readFile('/_redirects');
console.log(redir);
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);
});
});

View file

@ -1,63 +0,0 @@
import { expect } from 'chai';
import netlifyAdapter from '../../dist/index.js';
import { loadFixture, testIntegration } from './test-utils.js';
describe('SSG - Redirects', () => {
/** @type {import('../../../astro/test/test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('../functions/fixtures/redirects/', import.meta.url).toString(),
output: 'hybrid',
adapter: netlifyAdapter({
dist: new URL('../functions/fixtures/redirects/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [testIntegration()],
redirects: {
'/other': '/',
},
});
await fixture.build();
});
it('Creates a redirects file', async () => {
let redirects = await fixture.readFile('/_redirects');
let parts = redirects.split(/\s+/);
console.log(parts);
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();
});
it('Does not create .html files', async () => {
try {
await fixture.readFile('/other/index.html');
expect(false).to.equal(true, 'this file should not exist');
} catch {
expect(true).to.equal(true);
}
});
});

View file

@ -1,9 +0,0 @@
// 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"
`;

View file

@ -1,63 +0,0 @@
import { expect } from 'chai';
import netlifyAdapter from '../../dist/index.js';
import { loadFixture, 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;
}
});
});
});

View file

@ -1,34 +0,0 @@
// @ts-check
import { fileURLToPath } from 'node:url';
export * from '../../../../astro/test/test-utils.js';
/**
*
* @returns {import('../../../../astro/dist/types/@types/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);
}
},
},
};
}

View file

@ -1,3 +0,0 @@
The tests in this folder are done directly on a deployed Netlify website (hosted at https://curious-boba-495d6d.netlify.app) and are not run by the test suite. They instead run every week through a GitHub action.
The purpose of those tests is to make sure that everything works as expected while deployed. In a way, they're as E2E as it gets.

View file

@ -1,8 +0,0 @@
import netlify from '@astrojs/netlify';
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
output: 'server',
adapter: netlify(),
});

View file

@ -1,12 +0,0 @@
{
"name": "netlify-hosted-astro-project",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "astro build"
},
"dependencies": {
"@astrojs/netlify": "workspace:*",
"astro": "workspace:*"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 MiB

View file

@ -1 +0,0 @@
/// <reference types="astro/client-image" />

View file

@ -1,6 +0,0 @@
---
import { Image } from 'astro:assets';
import penguin from '../assets/penguin.png';
---
<Image src={penguin} width={300} alt="" />

View file

@ -1,5 +0,0 @@
---
const currentTime = new Date().getTime();
---
{currentTime}

View file

@ -1,21 +0,0 @@
import { expect } from 'chai';
const NETLIFY_TEST_URL = 'https://curious-boba-495d6d.netlify.app';
describe('Hosted Netlify Tests', () => {
it('Image endpoint works', async () => {
const image = await fetch(
NETLIFY_TEST_URL + '/_image?href=%2F_astro%2Fpenguin.e9c64733.png&w=300&f=webp'
);
expect(image.status).to.equal(200);
});
it('Server returns fresh content', async () => {
const responseOne = await fetch(NETLIFY_TEST_URL + '/time');
const responseTwo = await fetch(NETLIFY_TEST_URL + '/time');
expect(responseOne.body).to.not.equal(responseTwo.body);
});
});

View file

@ -1,12 +0,0 @@
import { use } from 'chai';
import chaiJestSnapshot from 'chai-jest-snapshot';
use(chaiJestSnapshot);
before(function () {
chaiJestSnapshot.resetSnapshotRegistry();
});
beforeEach(function () {
chaiJestSnapshot.configureUsingMochaContext(this);
});

View file

@ -1,6 +0,0 @@
<html>
<head><title>Testing</title></head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -1,3 +0,0 @@
---
return Astro.redirect('/');
---

View file

@ -1,25 +0,0 @@
---
export const getStaticPaths = (async () => {
const posts = [
{ slug: 'one', data: {draft: false, title: 'One'} },
{ slug: 'two', data: {draft: false, title: 'Two'} }
];
return posts.map((post) => {
return {
params: { slug: post.slug },
props: { draft: post.data.draft, title: post.data.title },
};
});
})
const { slug } = Astro.params;
const { title } = Astro.props;
---
<html>
<head>
<title>{ title }</title>
</head>
<body>
<h1>{ title }</h1>
</body>
</html>

View file

@ -1,50 +0,0 @@
import { expect } from 'chai';
import { loadFixture, testIntegration } from './test-utils.js';
import { netlifyStatic } from '../../dist/index.js';
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]',
},
});
await fixture.build();
});
it('Creates a redirects file', async () => {
let 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',
]);
});
});

View file

@ -1,29 +0,0 @@
// @ts-check
import { fileURLToPath } from 'node:url';
export * from '../../../../astro/test/test-utils.js';
/**
*
* @returns {import('../../../../astro/dist/types/@types/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)
),
},
},
},
});
},
},
};
}

View file

@ -1,8 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"outDir": "./dist",
"typeRoots": ["node_modules/@types", "node_modules/@netlify"]
}
}

File diff suppressed because it is too large Load diff