summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@skypack.dev> 2023-11-08 13:36:50 -0500
committerGravatar GitHub <noreply@github.com> 2023-11-08 13:36:50 -0500
commitf5bdfa272b4270b06bc539c2e382d6730987300c (patch)
tree9746f7e969f1f25427c5dc144faa8e42fdcf46d2
parentb09379428d6d147375daa5ec2eb1393e79bc357e (diff)
downloadastro-f5bdfa272b4270b06bc539c2e382d6730987300c.tar.gz
astro-f5bdfa272b4270b06bc539c2e382d6730987300c.tar.zst
astro-f5bdfa272b4270b06bc539c2e382d6730987300c.zip
Integration defined middleware (#8869)
* Rebase * Use an empty module if there is no real middleware * Add debug logging * Use normalizePath * Add a better example in the changesetp * Update .changeset/khaki-glasses-raise.md Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> * Update .changeset/khaki-glasses-raise.md Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> * Update .changeset/khaki-glasses-raise.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/khaki-glasses-raise.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/khaki-glasses-raise.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/khaki-glasses-raise.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update packages/astro/src/core/middleware/vite-plugin.ts Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Review comments * oops * Update .changeset/khaki-glasses-raise.md Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> --------- Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
-rw-r--r--.changeset/khaki-glasses-raise.md48
-rw-r--r--packages/astro/src/@types/astro.ts7
-rw-r--r--packages/astro/src/core/build/plugins/plugin-middleware.ts68
-rw-r--r--packages/astro/src/core/config/settings.ts1
-rw-r--r--packages/astro/src/core/create-vite.ts2
-rw-r--r--packages/astro/src/core/middleware/loadMiddleware.ts8
-rw-r--r--packages/astro/src/core/middleware/sequence.ts5
-rw-r--r--packages/astro/src/core/middleware/vite-plugin.ts124
-rw-r--r--packages/astro/src/integrations/index.ts11
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts3
-rw-r--r--packages/astro/test/fixtures/middleware space/astro.config.mjs23
-rw-r--r--packages/astro/test/fixtures/middleware space/integration-middleware-post.js13
-rw-r--r--packages/astro/test/fixtures/middleware space/integration-middleware-pre.js13
-rw-r--r--packages/astro/test/fixtures/middleware space/package.json3
-rw-r--r--packages/astro/test/middleware.test.js14
15 files changed, 268 insertions, 75 deletions
diff --git a/.changeset/khaki-glasses-raise.md b/.changeset/khaki-glasses-raise.md
new file mode 100644
index 000000000..4a0622a42
--- /dev/null
+++ b/.changeset/khaki-glasses-raise.md
@@ -0,0 +1,48 @@
+---
+'astro': minor
+---
+
+## Integration Hooks to add Middleware
+
+It's now possible in Astro for an integration to add middleware on behalf of the user. Previously when a third party wanted to provide middleware, the user would need to create a `src/middleware.ts` file themselves. Now, adding third-party middleware is as easy as adding a new integration.
+
+For integration authors, there is a new `addMiddleware` function in the `astro:config:setup` hook. This function allows you to specify a middleware module and the order in which it should be applied:
+
+```js
+// my-package/middleware.js
+import { defineMiddleware } from 'astro:middleware';
+
+export const onRequest = defineMiddleware(async (context, next) => {
+ const response = await next();
+
+ if(response.headers.get('content-type') === 'text/html') {
+ let html = await response.text();
+ html = minify(html);
+ return new Response(html, {
+ status: response.status,
+ headers: response.headers
+ });
+ }
+
+ return response;
+});
+```
+
+You can now add your integration's middleware and specify that it runs either before or after the application's own defined middleware (defined in `src/middleware.{js,ts}`)
+
+```js
+// my-package/integration.js
+export function myIntegration() {
+ return {
+ name: 'my-integration',
+ hooks: {
+ 'astro:config:setup': ({ addMiddleware }) => {
+ addMiddleware({
+ entrypoint: 'my-package/middleware',
+ order: 'pre'
+ });
+ }
+ }
+ };
+}
+```
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 47ed001f0..e1bf2cd8b 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1701,6 +1701,7 @@ export interface AstroSettings {
*/
clientDirectives: Map<string, string>;
devOverlayPlugins: string[];
+ middlewares: { pre: string[]; post: string[]; };
tsConfig: TSConfig | undefined;
tsConfigPath: string | undefined;
watchFiles: string[];
@@ -2279,6 +2280,7 @@ export interface AstroIntegration {
injectRoute: (injectRoute: InjectedRoute) => void;
addClientDirective: (directive: ClientDirectiveConfig) => void;
addDevOverlayPlugin: (entrypoint: string) => void;
+ addMiddleware: (mid: AstroIntegrationMiddleware) => void;
logger: AstroIntegrationLogger;
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
// This may require some refactoring of `scripts`, `styles`, and `links` into something
@@ -2349,6 +2351,11 @@ export type AstroMiddlewareInstance<R> = {
onRequest?: MiddlewareHandler<R>;
};
+export type AstroIntegrationMiddleware = {
+ order: 'pre' | 'post';
+ entrypoint: string;
+};
+
export interface AstroPluginOptions {
settings: AstroSettings;
logger: Logger;
diff --git a/packages/astro/src/core/build/plugins/plugin-middleware.ts b/packages/astro/src/core/build/plugins/plugin-middleware.ts
index 628a1cb70..7f3760612 100644
--- a/packages/astro/src/core/build/plugins/plugin-middleware.ts
+++ b/packages/astro/src/core/build/plugins/plugin-middleware.ts
@@ -1,70 +1,8 @@
-import type { Plugin as VitePlugin } from 'vite';
-import { getOutputDirectory } from '../../../prerender/utils.js';
-import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js';
-import { addRollupInput } from '../add-rollup-input.js';
import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
-
-export const MIDDLEWARE_MODULE_ID = '@astro-middleware';
-
-const EMPTY_MIDDLEWARE = '\0empty-middleware';
-
-export function vitePluginMiddleware(
- opts: StaticBuildOptions,
- internals: BuildInternals
-): VitePlugin {
- let resolvedMiddlewareId: string;
- return {
- name: '@astro/plugin-middleware',
- enforce: 'post',
- options(options) {
- return addRollupInput(options, [MIDDLEWARE_MODULE_ID]);
- },
-
- async resolveId(id) {
- if (id === MIDDLEWARE_MODULE_ID) {
- const middlewareId = await this.resolve(
- `${decodeURI(opts.settings.config.srcDir.pathname)}${MIDDLEWARE_PATH_SEGMENT_NAME}`
- );
- if (middlewareId) {
- resolvedMiddlewareId = middlewareId.id;
- return middlewareId.id;
- } else {
- return EMPTY_MIDDLEWARE;
- }
- }
- if (id === EMPTY_MIDDLEWARE) {
- return EMPTY_MIDDLEWARE;
- }
- },
-
- load(id) {
- if (id === EMPTY_MIDDLEWARE) {
- return 'export const onRequest = undefined';
- } else if (id === resolvedMiddlewareId) {
- this.emitFile({
- type: 'chunk',
- preserveSignature: 'strict',
- fileName: 'middleware.mjs',
- id,
- });
- }
- },
-
- writeBundle(_, bundle) {
- for (const [chunkName, chunk] of Object.entries(bundle)) {
- if (chunk.type === 'asset') {
- continue;
- }
- if (chunk.fileName === 'middleware.mjs') {
- const outputDirectory = getOutputDirectory(opts.settings.config);
- internals.middlewareEntryPoint = new URL(chunkName, outputDirectory);
- }
- }
- },
- };
-}
+import { vitePluginMiddlewareBuild } from '../../middleware/vite-plugin.js';
+export { MIDDLEWARE_MODULE_ID } from '../../middleware/vite-plugin.js';
export function pluginMiddleware(
opts: StaticBuildOptions,
@@ -75,7 +13,7 @@ export function pluginMiddleware(
hooks: {
'build:before': () => {
return {
- vitePlugin: vitePluginMiddleware(opts, internals),
+ vitePlugin: vitePluginMiddlewareBuild(opts, internals),
};
},
},
diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts
index cf4db7598..fca392c97 100644
--- a/packages/astro/src/core/config/settings.ts
+++ b/packages/astro/src/core/config/settings.ts
@@ -97,6 +97,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
renderers: [],
scripts: [],
clientDirectives: getDefaultClientDirectives(),
+ middlewares: { pre: [], post: [] },
watchFiles: [],
devOverlayPlugins: [],
timer: new AstroTimer(),
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index be62de671..fda837209 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -31,6 +31,7 @@ import astroScannerPlugin from '../vite-plugin-scanner/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
+import { vitePluginMiddleware } from './middleware/vite-plugin.js';
import { joinPaths } from './path.js';
interface CreateViteOptions {
@@ -134,6 +135,7 @@ export async function createVite(
astroContentVirtualModPlugin({ settings }),
astroContentImportPlugin({ fs, settings }),
astroContentAssetPropagationPlugin({ mode, settings }),
+ vitePluginMiddleware({ settings }),
vitePluginSSRManifest(),
astroAssetsPlugin({ settings, logger, mode }),
astroPrefetch({ settings }),
diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts
index b8528eb4b..46e1e32e2 100644
--- a/packages/astro/src/core/middleware/loadMiddleware.ts
+++ b/packages/astro/src/core/middleware/loadMiddleware.ts
@@ -1,6 +1,5 @@
-import type { AstroSettings } from '../../@types/astro.js';
-import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
import type { ModuleLoader } from '../module-loader/index.js';
+import { MIDDLEWARE_MODULE_ID } from './vite-plugin.js';
/**
* It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration.
@@ -9,12 +8,9 @@ import type { ModuleLoader } from '../module-loader/index.js';
*/
export async function loadMiddleware(
moduleLoader: ModuleLoader,
- srcDir: AstroSettings['config']['srcDir']
) {
- // can't use node Node.js builtins
- let middlewarePath = `${decodeURI(srcDir.pathname)}${MIDDLEWARE_PATH_SEGMENT_NAME}`;
try {
- const module = await moduleLoader.import(middlewarePath);
+ const module = await moduleLoader.import(MIDDLEWARE_MODULE_ID);
return module;
} catch {
return void 0;
diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts
index d8d71c66b..ca3dc90a0 100644
--- a/packages/astro/src/core/middleware/sequence.ts
+++ b/packages/astro/src/core/middleware/sequence.ts
@@ -7,7 +7,8 @@ import { defineMiddleware } from './index.js';
* It accepts one or more middleware handlers and makes sure that they are run in sequence.
*/
export function sequence(...handlers: MiddlewareEndpointHandler[]): MiddlewareEndpointHandler {
- const length = handlers.length;
+ const filtered = handlers.filter(h => !!h);
+ const length = filtered.length;
if (!length) {
const handler: MiddlewareEndpointHandler = defineMiddleware((context, next) => {
return next();
@@ -19,7 +20,7 @@ export function sequence(...handlers: MiddlewareEndpointHandler[]): MiddlewareEn
return applyHandle(0, context);
function applyHandle(i: number, handleContext: APIContext) {
- const handle = handlers[i];
+ const handle = filtered[i];
// @ts-expect-error
// SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually
// doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`.
diff --git a/packages/astro/src/core/middleware/vite-plugin.ts b/packages/astro/src/core/middleware/vite-plugin.ts
new file mode 100644
index 000000000..d25012516
--- /dev/null
+++ b/packages/astro/src/core/middleware/vite-plugin.ts
@@ -0,0 +1,124 @@
+import type { Plugin as VitePlugin } from 'vite';
+import { normalizePath } from 'vite';
+import { getOutputDirectory } from '../../prerender/utils.js';
+import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
+import { addRollupInput } from '../build/add-rollup-input.js';
+import type { BuildInternals } from '../build/internal.js';
+import type { StaticBuildOptions } from '../build/types.js';
+import type { AstroSettings } from '../../@types/astro.js';
+
+export const MIDDLEWARE_MODULE_ID = '@astro-middleware';
+const EMPTY_MIDDLEWARE = '\0empty-middleware';
+
+export function vitePluginMiddleware({
+ settings
+}: {
+ settings: AstroSettings
+}): VitePlugin {
+ let isCommandBuild = false;
+ let resolvedMiddlewareId: string | undefined = undefined;
+ const hasIntegrationMiddleware = settings.middlewares.pre.length > 0 || settings.middlewares.post.length > 0;
+
+ return {
+ name: '@astro/plugin-middleware',
+
+ config(opts, { command }) {
+ isCommandBuild = command === 'build';
+ return opts;
+ },
+
+ async resolveId(id) {
+ if (id === MIDDLEWARE_MODULE_ID) {
+ const middlewareId = await this.resolve(
+ `${decodeURI(settings.config.srcDir.pathname)}${MIDDLEWARE_PATH_SEGMENT_NAME}`
+ );
+ if (middlewareId) {
+ resolvedMiddlewareId = middlewareId.id;
+ return MIDDLEWARE_MODULE_ID;
+ } else if(hasIntegrationMiddleware) {
+ return MIDDLEWARE_MODULE_ID;
+ } else {
+ return EMPTY_MIDDLEWARE;
+ }
+ }
+ if (id === EMPTY_MIDDLEWARE) {
+ return EMPTY_MIDDLEWARE;
+ }
+ },
+
+ async load(id) {
+ if (id === EMPTY_MIDDLEWARE) {
+ return 'export const onRequest = undefined';
+ } else if (id === MIDDLEWARE_MODULE_ID) {
+ // In the build, tell Vite to emit this file
+ if(isCommandBuild) {
+ this.emitFile({
+ type: 'chunk',
+ preserveSignature: 'strict',
+ fileName: 'middleware.mjs',
+ id,
+ });
+ }
+
+ const preMiddleware = createMiddlewareImports(settings.middlewares.pre, 'pre');
+ const postMiddleware = createMiddlewareImports(settings.middlewares.post, 'post');
+
+ const source = `
+import { onRequest as userOnRequest } from '${resolvedMiddlewareId}';
+import { sequence } from 'astro:middleware';
+${preMiddleware.importsCode}${postMiddleware.importsCode}
+
+export const onRequest = sequence(
+ ${preMiddleware.sequenceCode}${preMiddleware.sequenceCode ? ',' : ''}
+ userOnRequest${postMiddleware.sequenceCode ? ',' : ''}
+ ${postMiddleware.sequenceCode}
+);
+`.trim();
+
+ return source;
+ }
+ },
+ };
+}
+
+function createMiddlewareImports(entrypoints: string[], prefix: string): {
+ importsCode: string
+ sequenceCode: string
+} {
+ let importsRaw = '';
+ let sequenceRaw = '';
+ let index = 0;
+ for(const entrypoint of entrypoints) {
+ const name = `_${prefix}_${index}`;
+ importsRaw += `import { onRequest as ${name} } from '${normalizePath(entrypoint)}';\n`;
+ sequenceRaw += `${index > 0 ? ',' : ''}${name}`
+ index++;
+ }
+
+ return {
+ importsCode: importsRaw,
+ sequenceCode: sequenceRaw
+ };
+}
+
+export function vitePluginMiddlewareBuild(
+ opts: StaticBuildOptions,
+ internals: BuildInternals
+): VitePlugin {
+ return {
+ name: '@astro/plugin-middleware-build',
+
+ options(options) {
+ return addRollupInput(options, [MIDDLEWARE_MODULE_ID]);
+ },
+
+ writeBundle(_, bundle) {
+ for (const [chunkName, chunk] of Object.entries(bundle)) {
+ if (chunk.type !== 'asset' && chunk.fileName === 'middleware.mjs') {
+ const outputDirectory = getOutputDirectory(opts.settings.config);
+ internals.middlewareEntryPoint = new URL(chunkName, outputDirectory);
+ }
+ }
+ },
+ };
+}
diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts
index 4c527cea3..23d2160a4 100644
--- a/packages/astro/src/integrations/index.ts
+++ b/packages/astro/src/integrations/index.ts
@@ -136,6 +136,17 @@ export async function runHookConfigSetup({
}
addedClientDirectives.set(name, buildClientDirectiveEntrypoint(name, entrypoint));
},
+ addMiddleware: ({ order, entrypoint }) => {
+ if(typeof updatedSettings.middlewares[order] === 'undefined') {
+ throw new Error(
+ `The "${integration.name}" integration is trying to add middleware but did not specify an order.`
+ );
+ }
+ logger.debug('middleware', `The integration ${integration.name} has added middleware that runs ${
+ order === 'pre' ? 'before' : 'after'
+ } any application middleware you define.`);
+ updatedSettings.middlewares[order].push(entrypoint);
+ },
logger: integrationLogger,
};
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index de1910227..2b0bcbea7 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -162,7 +162,6 @@ export async function handleRoute({
manifest,
}: HandleRoute): Promise<void> {
const env = pipeline.getEnvironment();
- const settings = pipeline.getSettings();
const config = pipeline.getConfig();
const moduleLoader = pipeline.getModuleLoader();
const { logger } = env;
@@ -177,7 +176,7 @@ export async function handleRoute({
let mod: ComponentInstance | undefined = undefined;
let options: SSROptions | undefined = undefined;
let route: RouteData;
- const middleware = await loadMiddleware(moduleLoader, settings.config.srcDir);
+ const middleware = await loadMiddleware(moduleLoader);
if (!matchedRoute) {
if (config.experimental.i18n) {
diff --git a/packages/astro/test/fixtures/middleware space/astro.config.mjs b/packages/astro/test/fixtures/middleware space/astro.config.mjs
new file mode 100644
index 000000000..3fee066cd
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware space/astro.config.mjs
@@ -0,0 +1,23 @@
+import { defineConfig } from 'astro/config';
+import { fileURLToPath } from 'node:url';
+
+export default defineConfig({
+ integrations: [
+ {
+ name: 'my-middleware',
+ hooks: {
+ 'astro:config:setup':({ addMiddleware }) => {
+ addMiddleware({
+ entrypoint: fileURLToPath(new URL('./integration-middleware-pre.js', import.meta.url)),
+ order: 'pre'
+ });
+
+ addMiddleware({
+ entrypoint: fileURLToPath(new URL('./integration-middleware-post.js', import.meta.url)),
+ order: 'post'
+ });
+ }
+ }
+ }
+ ]
+});
diff --git a/packages/astro/test/fixtures/middleware space/integration-middleware-post.js b/packages/astro/test/fixtures/middleware space/integration-middleware-post.js
new file mode 100644
index 000000000..4cc63c6b7
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware space/integration-middleware-post.js
@@ -0,0 +1,13 @@
+import { sequence, defineMiddleware } from 'astro:middleware';
+
+export const onRequest = defineMiddleware((context, next) => {
+ if(context.url.pathname === '/integration-post') {
+ return new Response(JSON.stringify({ post: 'works' }), {
+ headers: {
+ 'content-type': 'application/json'
+ }
+ });
+ }
+
+ return next();
+});
diff --git a/packages/astro/test/fixtures/middleware space/integration-middleware-pre.js b/packages/astro/test/fixtures/middleware space/integration-middleware-pre.js
new file mode 100644
index 000000000..3bf484b2b
--- /dev/null
+++ b/packages/astro/test/fixtures/middleware space/integration-middleware-pre.js
@@ -0,0 +1,13 @@
+import { sequence, defineMiddleware } from 'astro:middleware';
+
+export const onRequest = defineMiddleware((context, next) => {
+ if(context.url.pathname === '/integration-pre') {
+ return new Response(JSON.stringify({ pre: 'works' }), {
+ headers: {
+ 'content-type': 'application/json'
+ }
+ });
+ }
+
+ return next();
+});
diff --git a/packages/astro/test/fixtures/middleware space/package.json b/packages/astro/test/fixtures/middleware space/package.json
index bc889aa63..91d4a344c 100644
--- a/packages/astro/test/fixtures/middleware space/package.json
+++ b/packages/astro/test/fixtures/middleware space/package.json
@@ -4,5 +4,8 @@
"private": true,
"dependencies": {
"astro": "workspace:*"
+ },
+ "exports": {
+ "./integration-middleware.js": "./integration-middleware.js"
}
}
diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js
index ac3054620..a9487950e 100644
--- a/packages/astro/test/middleware.test.js
+++ b/packages/astro/test/middleware.test.js
@@ -85,6 +85,20 @@ describe('Middleware in DEV mode', () => {
let headers = res.headers;
expect(headers.get('set-cookie')).to.not.equal(null);
});
+
+ describe('Integration hooks', () => {
+ it('Integration middleware marked as "pre" runs', async () => {
+ let res = await fixture.fetch('/integration-pre');
+ let json = await res.json();
+ expect(json.pre).to.equal('works');
+ });
+
+ it('Integration middleware marked as "post" runs', async () => {
+ let res = await fixture.fetch('/integration-post');
+ let json = await res.json();
+ expect(json.post).to.equal('works');
+ });
+ });
});
describe('Middleware in PROD mode, SSG', () => {