feat(@astrojs/netlify): edge middleware support (#7632)
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> Co-authored-by: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com>
This commit is contained in:
parent
cc8e9de881
commit
4c93bd8154
25 changed files with 411 additions and 161 deletions
10
.changeset/great-days-judge.md
Normal file
10
.changeset/great-days-judge.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
'@astrojs/netlify': minor
|
||||
---
|
||||
|
||||
When a project uses the new option Astro `build.excludeMiddleware`, the
|
||||
`@astrojs/netlify/functions` adapter will automatically create an Edge Middleware
|
||||
that will automatically communicate with the Astro Middleware.
|
||||
|
||||
Check the [documentation](https://github.com/withastro/astro/blob/main/packages/integrations/netlify/README.md#edge-middleware-with-astro-middleware) for more details.
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -136,7 +136,7 @@ jobs:
|
|||
- name: Use Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.34.1
|
||||
deno-version: v1.35.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
|
|
@ -115,6 +115,65 @@ 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.
|
||||
|
||||
|
||||
### Edge Middleware with Astro middleware
|
||||
|
||||
The `@astrojs/netlify/functions` adapter can automatically create an edge function that will act as "Edge Middleware", from an Astro middleware in your code base.
|
||||
|
||||
This is an opt-in feature and the `build.excludeMiddleware` option needs to be set to `true`:
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import netlify from '@astrojs/netlify/functions';
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: netlify(),
|
||||
build: {
|
||||
excludeMiddleware: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Optionally, you can create a file recognized by the adapter named `netlify-edge-middleware.(js|ts)` in the [`srcDir`](https://docs.astro.build/en/reference/configuration-reference/#srcdir) folder to create [`Astro.locals`](https://docs.astro.build/en/reference/api-reference/#astrolocals).
|
||||
|
||||
Typings require the [`https://edge.netlify.com`](https://docs.netlify.com/edge-functions/api/#reference) types.
|
||||
|
||||
> Netlify edge functions run in a Deno environment, so you would need to import types using URLs.
|
||||
>
|
||||
> You can find more in the [Netlify documentation page](https://docs.netlify.com/edge-functions/api/#runtime-environment)
|
||||
|
||||
```ts
|
||||
// src/netlify-edge-middleware.ts
|
||||
import type { Context } from "https://edge.netlify.com";
|
||||
|
||||
export default function ({ request, context }: { request: Request, context: Context }): object {
|
||||
// do something with request and context
|
||||
return {
|
||||
title: "Spider-man's blog",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The data returned by this function will be passed to Astro middleware.
|
||||
|
||||
The function:
|
||||
|
||||
- must export a **default** function;
|
||||
- must **return** an `object`;
|
||||
- accepts an object with a `request` and `context` as properties;
|
||||
- `request` is typed as [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request);
|
||||
- `context` is typed as [`Context`](https://docs.netlify.com/edge-functions/api/#edge-function-types);
|
||||
|
||||
#### Limitations and constraints
|
||||
|
||||
When you opt-in to this feature, there are a few constraints to note:
|
||||
|
||||
- The Edge middleware will always be the **first** function to receive the `Request` and the last function to receive `Response`. This is an architectural constraint that follows the [boundaries set by Netlify](https://docs.netlify.com/edge-functions/overview/#use-cases).
|
||||
- Only `request` and `context` may be used to produce an `Astro.locals` object. Operations like redirects, etc. should be delegated to Astro middleware.
|
||||
- `Astro.locals` **must be serializable**. Failing to do so will result in a **runtime error**. This means that you **cannot** store complex types like `Map`, `function`, `Set`, etc.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
[Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/)
|
||||
|
|
|
@ -33,8 +33,8 @@
|
|||
"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 ./test/edge-functions/",
|
||||
"test": "npm run test-fn"
|
||||
"test-edge": "deno test --allow-run --allow-read --allow-net --allow-env --allow-write ./test/edge-functions/",
|
||||
"test": "pnpm test-fn"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/underscore-redirects": "^0.2.0",
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
|
||||
import esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as npath from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRedirects } from './shared.js';
|
||||
|
||||
interface BuildConfig {
|
||||
server: URL;
|
||||
client: URL;
|
||||
serverEntry: string;
|
||||
assets: string;
|
||||
}
|
||||
|
||||
const SHIM = `globalThis.process = {
|
||||
argv: [],
|
||||
env: Deno.env.toObject(),
|
||||
};`;
|
||||
import {
|
||||
bundleServerEntry,
|
||||
createEdgeManifest,
|
||||
createRedirects,
|
||||
type NetlifyEdgeFunctionsOptions,
|
||||
} from './shared.js';
|
||||
|
||||
export function getAdapter(): AstroAdapter {
|
||||
return {
|
||||
|
@ -25,92 +14,10 @@ export function getAdapter(): AstroAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
interface NetlifyEdgeFunctionsOptions {
|
||||
dist?: URL;
|
||||
}
|
||||
|
||||
interface NetlifyEdgeFunctionManifestFunctionPath {
|
||||
function: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface NetlifyEdgeFunctionManifestFunctionPattern {
|
||||
function: string;
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
type NetlifyEdgeFunctionManifestFunction =
|
||||
| NetlifyEdgeFunctionManifestFunctionPath
|
||||
| NetlifyEdgeFunctionManifestFunctionPattern;
|
||||
|
||||
interface NetlifyEdgeFunctionManifest {
|
||||
functions: NetlifyEdgeFunctionManifestFunction[];
|
||||
version: 1;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
async function bundleServerEntry({ serverEntry, server }: BuildConfig, vite: any) {
|
||||
const entryUrl = new URL(serverEntry, server);
|
||||
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'],
|
||||
banner: {
|
||||
js: SHIM,
|
||||
},
|
||||
});
|
||||
|
||||
// Remove chunks, if they exist. Since we have bundled via esbuild these chunks are trash.
|
||||
try {
|
||||
const chunkFileNames =
|
||||
vite?.build?.rollupOptions?.output?.chunkFileNames ?? `chunks/chunk.[hash].mjs`;
|
||||
const chunkPath = npath.dirname(chunkFileNames);
|
||||
const chunksDirUrl = new URL(chunkPath + '/', server);
|
||||
await fs.promises.rm(chunksDirUrl, { recursive: true, force: true });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {}): AstroIntegration {
|
||||
let _config: AstroConfig;
|
||||
let entryFile: string;
|
||||
let _buildConfig: BuildConfig;
|
||||
let _buildConfig: AstroConfig['build'];
|
||||
let _vite: any;
|
||||
return {
|
||||
name: '@astrojs/netlify/edge-functions',
|
||||
|
@ -164,7 +71,8 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
}
|
||||
},
|
||||
'astro:build:done': async ({ routes, dir }) => {
|
||||
await bundleServerEntry(_buildConfig, _vite);
|
||||
const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server);
|
||||
await bundleServerEntry(entryUrl, _buildConfig.server, _vite);
|
||||
await createEdgeManifest(routes, entryFile, _config.root);
|
||||
const dynamicTarget = `/.netlify/edge-functions/${entryFile}`;
|
||||
const map: [RouteData, string][] = routes.map((route) => {
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
|
||||
import { extname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { Args } from './netlify-functions.js';
|
||||
import { createRedirects } from './shared.js';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { generateEdgeMiddleware } from './middleware.js';
|
||||
|
||||
export const NETLIFY_EDGE_MIDDLEWARE_FILE = 'netlify-edge-middleware';
|
||||
export const ASTRO_LOCALS_HEADER = 'x-astro-locals';
|
||||
|
||||
export function getAdapter(args: Args = {}): AstroAdapter {
|
||||
return {
|
||||
|
@ -27,6 +31,7 @@ function netlifyFunctions({
|
|||
let _config: AstroConfig;
|
||||
let _entryPoints: Map<RouteData, URL>;
|
||||
let ssrEntryFile: string;
|
||||
let _middlewareEntryPoint: URL;
|
||||
return {
|
||||
name: '@astrojs/netlify',
|
||||
hooks: {
|
||||
|
@ -40,7 +45,10 @@ function netlifyFunctions({
|
|||
},
|
||||
});
|
||||
},
|
||||
'astro:build:ssr': ({ entryPoints }) => {
|
||||
'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
|
||||
if (middlewareEntryPoint) {
|
||||
_middlewareEntryPoint = middlewareEntryPoint;
|
||||
}
|
||||
_entryPoints = entryPoints;
|
||||
},
|
||||
'astro:config:done': ({ config, setAdapter }) => {
|
||||
|
@ -85,6 +93,18 @@ function netlifyFunctions({
|
|||
|
||||
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
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
75
packages/integrations/netlify/src/middleware.ts
Normal file
75
packages/integrations/netlify/src/middleware.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync } from 'node:fs';
|
||||
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: "/*"
|
||||
}
|
||||
`;
|
||||
}
|
|
@ -2,6 +2,7 @@ import { polyfill } from '@astrojs/webapi';
|
|||
import { builder, type Handler } from '@netlify/functions';
|
||||
import type { SSRManifest } from 'astro';
|
||||
import { App } from 'astro/app';
|
||||
import { ASTRO_LOCALS_HEADER } from './integration-functions.js';
|
||||
|
||||
polyfill(globalThis, {
|
||||
exclude: 'window document',
|
||||
|
@ -80,8 +81,14 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
|
|||
|
||||
const ip = headers['x-nf-client-connection-ip'];
|
||||
Reflect.set(request, clientAddressSymbol, ip);
|
||||
|
||||
const response: Response = await app.render(request, routeData);
|
||||
let locals = {};
|
||||
if (request.headers.has(ASTRO_LOCALS_HEADER)) {
|
||||
let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER);
|
||||
if (localsAsString) {
|
||||
locals = JSON.parse(localsAsString);
|
||||
}
|
||||
}
|
||||
const response: Response = await app.render(request, routeData, locals);
|
||||
const responseHeaders = Object.fromEntries(response.headers.entries());
|
||||
|
||||
const responseContentType = parseContentType(responseHeaders['content-type']);
|
||||
|
|
|
@ -1,6 +1,37 @@
|
|||
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
|
||||
import type { AstroConfig, RouteData } from 'astro';
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import esbuild from 'esbuild';
|
||||
import npath from 'path';
|
||||
|
||||
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,
|
||||
|
@ -21,3 +52,63 @@ export async function createRedirects(
|
|||
// 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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ export {
|
|||
assert,
|
||||
assertExists,
|
||||
} from 'https://deno.land/std@0.132.0/testing/asserts.ts';
|
||||
export * from 'https://deno.land/x/deno_dom/deno-dom-wasm.ts';
|
||||
export { DOMParser } from 'https://deno.land/x/deno_dom@v0.1.35-alpha/deno-dom-wasm.ts';
|
||||
export * from 'https://deno.land/std@0.142.0/streams/conversion.ts';
|
||||
export * as cheerio from 'https://cdn.skypack.dev/cheerio?dts';
|
||||
export * as fs from 'https://deno.land/std/fs/mod.ts';
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { runBuild, runApp } from './test-utils.ts';
|
||||
import { loadFixture } from './test-utils.ts';
|
||||
import { assertEquals, assert, DOMParser } from './deps.ts';
|
||||
|
||||
Deno.test({
|
||||
name: 'Dynamic imports',
|
||||
permissions: 'inherit',
|
||||
async fn() {
|
||||
await runBuild('./fixtures/dynimport/');
|
||||
const { runApp, runBuild } = await loadFixture('./fixtures/dynimport/');
|
||||
await runBuild();
|
||||
const stop = await runApp('./fixtures/dynimport/prod.js');
|
||||
|
||||
try {
|
||||
|
@ -14,8 +16,10 @@ Deno.test({
|
|||
|
||||
assert(html, 'got some html');
|
||||
const doc = new DOMParser().parseFromString(html, `text/html`);
|
||||
const div = doc.querySelector('#thing');
|
||||
assert(div, 'div exists');
|
||||
if (doc) {
|
||||
const div = doc.querySelector('#thing');
|
||||
assert(div, 'div exists');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
|
@ -3,30 +3,34 @@ import { assertEquals, assert, DOMParser } from './deps.ts';
|
|||
|
||||
Deno.env.set('SECRET_STUFF', 'secret');
|
||||
|
||||
// @ts-expect-error
|
||||
Deno.test({
|
||||
// TODO: debug why build cannot be found in "await import"
|
||||
ignore: true,
|
||||
name: 'Edge Basics',
|
||||
skip: true,
|
||||
async fn() {
|
||||
permissions: 'inherit',
|
||||
async fn(t) {
|
||||
const fixture = loadFixture('./fixtures/edge-basic/');
|
||||
await fixture.runBuild();
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/edge-basic/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/'));
|
||||
assertEquals(response.status, 200);
|
||||
const html = await response.text();
|
||||
assert(html, 'got some html');
|
||||
await t.step('Run the build', async () => {
|
||||
await fixture.runBuild();
|
||||
});
|
||||
await t.step('Should correctly render the response', async () => {
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/edge-basic/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/'));
|
||||
assertEquals(response.status, 200);
|
||||
const html = await response.text();
|
||||
assert(html, 'got some html');
|
||||
|
||||
const doc = new DOMParser().parseFromString(html, `text/html`)!;
|
||||
const div = doc.querySelector('#react');
|
||||
assert(div, 'div exists');
|
||||
const doc = new DOMParser().parseFromString(html, `text/html`)!;
|
||||
const div = doc.querySelector('#react');
|
||||
assert(div, 'div exists');
|
||||
|
||||
const envDiv = doc.querySelector('#env');
|
||||
assertEquals(envDiv?.innerText, 'secret');
|
||||
const envDiv = doc.querySelector('#env');
|
||||
assertEquals(envDiv?.innerText, 'secret');
|
||||
});
|
||||
|
||||
await fixture.cleanup();
|
||||
await t.step('Clean up', async () => {
|
||||
await fixture.cleanup();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import { netlifyEdgeFunctions } from '@astrojs/netlify';
|
||||
import react from "@astrojs/react";
|
||||
|
||||
// test env var
|
||||
process.env.SECRET_STUFF = 'secret'
|
||||
|
@ -9,6 +8,5 @@ export default defineConfig({
|
|||
adapter: netlifyEdgeFunctions({
|
||||
dist: new URL('./dist/', import.meta.url),
|
||||
}),
|
||||
integrations: [react()],
|
||||
output: 'server',
|
||||
})
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/react": "workspace:*",
|
||||
"@astrojs/netlify": "workspace:*",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0"
|
||||
"@astrojs/netlify": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function() {
|
||||
return (
|
||||
<div id="react">testing</div>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
---
|
||||
import ReactComponent from '../components/React.jsx';
|
||||
---
|
||||
<html>
|
||||
<head><title>Testing</title></head>
|
||||
<body>
|
||||
|
@ -9,7 +6,6 @@ import ReactComponent from '../components/React.jsx';
|
|||
<ul>
|
||||
<li><a href="/two/">Two</a></li>
|
||||
</ul>
|
||||
<ReactComponent />
|
||||
<div id="env">{import.meta.env.SECRET_STUFF}</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,12 +3,16 @@ import { assertEquals, assertExists, cheerio, fs } from './deps.ts';
|
|||
|
||||
Deno.test({
|
||||
name: 'Prerender',
|
||||
permissions: 'inherit',
|
||||
async fn(t) {
|
||||
const environmentVariables = {
|
||||
PRERENDER: 'true',
|
||||
};
|
||||
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
|
||||
await fixture.runBuild();
|
||||
const { runBuild, cleanup } = loadFixture('./fixtures/prerender/', environmentVariables);
|
||||
|
||||
await t.step('Run the build', async () => {
|
||||
await runBuild();
|
||||
});
|
||||
|
||||
await t.step('Handler can process requests to non-existing routes', async () => {
|
||||
const { default: handler } = await import(
|
||||
|
@ -16,7 +20,7 @@ Deno.test({
|
|||
);
|
||||
assertExists(handler);
|
||||
const response = await handler(new Request('http://example.com/index.html'));
|
||||
assertEquals(response, undefined, "No response because this route doesn't exist");
|
||||
assertEquals(response.status, 404, "No response because this route doesn't exist");
|
||||
});
|
||||
|
||||
await t.step('Prerendered route exists', async () => {
|
||||
|
@ -31,22 +35,28 @@ Deno.test({
|
|||
});
|
||||
|
||||
Deno.env.delete('PRERENDER');
|
||||
await fixture.cleanup();
|
||||
await cleanup();
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: 'Hybrid rendering',
|
||||
permissions: 'inherit',
|
||||
async fn(t) {
|
||||
const environmentVariables = {
|
||||
PRERENDER: 'false',
|
||||
};
|
||||
const fixture = loadFixture('./fixtures/prerender/', environmentVariables);
|
||||
await fixture.runBuild();
|
||||
await t.step('Run the build', async () => {
|
||||
await fixture.runBuild();
|
||||
});
|
||||
|
||||
const stop = await fixture.runApp('./fixtures/prerender/prod.js');
|
||||
await t.step('Can fetch server route', async () => {
|
||||
const response = await fetch('http://127.0.0.1:8085/');
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/prerender/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/'));
|
||||
assertEquals(response.status, 200);
|
||||
|
||||
const html = await response.text();
|
||||
|
@ -60,7 +70,7 @@ Deno.test({
|
|||
'./fixtures/prerender/.netlify/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/index.html'));
|
||||
assertEquals(response, undefined, "No response because this route doesn't exist");
|
||||
assertEquals(response.status, 404, "No response because this route doesn't exist");
|
||||
});
|
||||
|
||||
await t.step('Has no prerendered route', async () => {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
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),
|
||||
}),
|
||||
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;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
export const onRequest = (context, next) => {
|
||||
context.locals.title = 'Middleware';
|
||||
|
||||
return next();
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
export default function ({ request, context }) {
|
||||
return {
|
||||
title: 'Hello world',
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
const title = Astro.locals.title;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,5 @@
|
|||
export const onRequest = (context, next) => {
|
||||
context.locals.title = 'Middleware';
|
||||
|
||||
return next();
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
const title = Astro.locals.title;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -6,6 +6,7 @@
|
|||
"module": "ES2022",
|
||||
"outDir": "./dist",
|
||||
"target": "ES2021",
|
||||
"typeRoots": ["node_modules/@types", "node_modules/@netlify"]
|
||||
"typeRoots": ["node_modules/@types", "node_modules/@netlify"],
|
||||
"allowImportingTsExtensions": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4464,12 +4464,6 @@ importers:
|
|||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../../astro
|
||||
react:
|
||||
specifier: ^18.1.0
|
||||
version: 18.2.0
|
||||
react-dom:
|
||||
specifier: ^18.1.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
|
||||
packages/integrations/netlify/test/edge-functions/fixtures/prerender:
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue