Skip to content

Commit

Permalink
Integration defined middleware (#8869)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
4 people authored Nov 8, 2023
1 parent b093794 commit f5bdfa2
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 75 deletions.
48 changes: 48 additions & 0 deletions .changeset/khaki-glasses-raise.md
Original file line number Diff line number Diff line change
@@ -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'
});
}
}
};
}
```
7 changes: 7 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
68 changes: 3 additions & 65 deletions packages/astro/src/core/build/plugins/plugin-middleware.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -75,7 +13,7 @@ export function pluginMiddleware(
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginMiddleware(opts, internals),
vitePlugin: vitePluginMiddlewareBuild(opts, internals),
};
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
renderers: [],
scripts: [],
clientDirectives: getDefaultClientDirectives(),
middlewares: { pre: [], post: [] },
watchFiles: [],
devOverlayPlugins: [],
timer: new AstroTimer(),
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }),
Expand Down
8 changes: 2 additions & 6 deletions packages/astro/src/core/middleware/loadMiddleware.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions packages/astro/src/core/middleware/sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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`.
Expand Down
124 changes: 124 additions & 0 deletions packages/astro/src/core/middleware/vite-plugin.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
},
};
}
11 changes: 11 additions & 0 deletions packages/astro/src/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/vite-plugin-astro-server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit f5bdfa2

Please sign in to comment.