diff options
Diffstat (limited to 'packages/astro')
-rw-r--r-- | packages/astro/src/@types/astro.ts | 25 | ||||
-rw-r--r-- | packages/astro/src/core/build/index.ts | 22 | ||||
-rw-r--r-- | packages/astro/src/core/build/internal.ts | 1 | ||||
-rw-r--r-- | packages/astro/src/core/build/plugins/index.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/core/build/plugins/plugin-middleware.ts | 33 | ||||
-rw-r--r-- | packages/astro/src/core/build/plugins/plugin-pages.ts | 11 | ||||
-rw-r--r-- | packages/astro/src/core/build/plugins/plugin-ssr.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/core/build/static-build.ts | 3 | ||||
-rw-r--r-- | packages/astro/src/core/config/schema.ts | 9 | ||||
-rw-r--r-- | packages/astro/src/core/endpoint/index.ts | 21 | ||||
-rw-r--r-- | packages/astro/src/core/middleware/index.ts | 102 | ||||
-rw-r--r-- | packages/astro/src/integrations/index.ts | 33 | ||||
-rw-r--r-- | packages/astro/test/middleware.test.js | 49 | ||||
-rw-r--r-- | packages/astro/test/ssr-split-manifest.test.js | 4 | ||||
-rw-r--r-- | packages/astro/test/test-adapter.js | 3 |
15 files changed, 281 insertions, 39 deletions
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 185401a89..12f309f1a 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -849,6 +849,27 @@ export interface AstroUserConfig { * ``` */ split?: boolean; + + /** + * @docs + * @name build.excludeMiddleware + * @type {boolean} + * @default {false} + * @version 2.8.0 + * @description + * Defines whether or not any SSR middleware code will be bundled when built. + * + * When enabled, middleware code is not bundled and imported by all pages during the build. To instead execute and import middleware code manually, set `build.excludeMiddleware: true`: + * + * ```js + * { + * build: { + * excludeMiddleware: true + * } + * } + * ``` + */ + excludeMiddleware?: boolean; }; /** @@ -1842,6 +1863,10 @@ export interface AstroIntegration { * the physical file you should import. */ entryPoints: Map<RouteData, URL>; + /** + * File path of the emitted middleware + */ + middlewareEntryPoint: URL | undefined; }) => void | Promise<void>; 'astro:build:start'?: () => void | Promise<void>; 'astro:build:setup'?: (options: { diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index 037c462fd..11e2b1fa9 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -1,5 +1,4 @@ import type { AstroConfig, AstroSettings, ManifestData, RuntimeMode } from '../../@types/astro'; - import fs from 'fs'; import * as colors from 'kleur/colors'; import { performance } from 'perf_hooks'; @@ -12,7 +11,7 @@ import { runHookConfigSetup, } from '../../integrations/index.js'; import { createVite } from '../create-vite.js'; -import { debug, info, levels, timerMessage, type LogOptions } from '../logger/core.js'; +import { debug, info, warn, levels, timerMessage, type LogOptions } from '../logger/core.js'; import { printHelp } from '../messages.js'; import { apply as applyPolyfill } from '../polyfill.js'; import { RouteCache } from '../render/route-cache.js'; @@ -211,6 +210,25 @@ class AstroBuilder { `the outDir cannot be the root folder. Please build to a folder such as dist.` ); } + + if (config.build.split === true) { + if (config.output === 'static') { + warn( + this.logging, + 'configuration', + 'The option `build.split` won\'t take effect, because `output` is not `"server"` or `"hybrid"`.' + ); + } + } + if (config.build.excludeMiddleware === true) { + if (config.output === 'static') { + warn( + this.logging, + 'configuration', + 'The option `build.excludeMiddleware` won\'t take effect, because `output` is not `"server"` or `"hybrid"`.' + ); + } + } } /** Stats */ diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 28d15d874..5dff6f3dd 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -88,6 +88,7 @@ export interface BuildInternals { entryPoints: Map<RouteData, URL>; ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>; componentMetadata: SSRResult['componentMetadata']; + middlewareEntryPoint?: URL; } /** diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts index 160e18fdd..3a44824d6 100644 --- a/packages/astro/src/core/build/plugins/index.ts +++ b/packages/astro/src/core/build/plugins/index.ts @@ -19,7 +19,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP register(pluginAnalyzer(internals)); register(pluginInternals(internals)); register(pluginRenderers(options)); - register(pluginMiddleware(options)); + register(pluginMiddleware(options, internals)); register(pluginPages(options, internals)); register(pluginCSS(options, internals)); register(astroHeadBuildPlugin(internals)); diff --git a/packages/astro/src/core/build/plugins/plugin-middleware.ts b/packages/astro/src/core/build/plugins/plugin-middleware.ts index dee73d2f8..6db39733e 100644 --- a/packages/astro/src/core/build/plugins/plugin-middleware.ts +++ b/packages/astro/src/core/build/plugins/plugin-middleware.ts @@ -3,12 +3,17 @@ import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js'; import { addRollupInput } from '../add-rollup-input.js'; import type { AstroBuildPlugin } from '../plugin'; import type { StaticBuildOptions } from '../types'; +import type { BuildInternals } from '../internal'; export const MIDDLEWARE_MODULE_ID = '@astro-middleware'; const EMPTY_MIDDLEWARE = '\0empty-middleware'; -export function vitePluginMiddleware(opts: StaticBuildOptions): VitePlugin { +export function vitePluginMiddleware( + opts: StaticBuildOptions, + internals: BuildInternals +): VitePlugin { + let resolvedMiddlewareId: string; return { name: '@astro/plugin-middleware', @@ -22,6 +27,7 @@ export function vitePluginMiddleware(opts: StaticBuildOptions): VitePlugin { `${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}` ); if (middlewareId) { + resolvedMiddlewareId = middlewareId.id; return middlewareId.id; } else { return EMPTY_MIDDLEWARE; @@ -35,18 +41,39 @@ export function vitePluginMiddleware(opts: StaticBuildOptions): VitePlugin { 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') { + internals.middlewareEntryPoint = new URL(chunkName, opts.settings.config.build.server); + } } }, }; } -export function pluginMiddleware(opts: StaticBuildOptions): AstroBuildPlugin { +export function pluginMiddleware( + opts: StaticBuildOptions, + internals: BuildInternals +): AstroBuildPlugin { return { build: 'ssr', hooks: { 'build:before': () => { return { - vitePlugin: vitePluginMiddleware(opts), + vitePlugin: vitePluginMiddleware(opts, internals), }; }, }, diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index cf078f0b5..2ee438a6a 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -73,10 +73,13 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V imports.push(`import { renderers } from "${RENDERERS_MODULE_ID}";`); exports.push(`export { renderers };`); - const middlewareModule = await this.resolve(MIDDLEWARE_MODULE_ID); - if (middlewareModule) { - imports.push(`import { onRequest } from "${middlewareModule.id}";`); - exports.push(`export { onRequest };`); + // The middleware should not be imported by the pages + if (!opts.settings.config.build.excludeMiddleware) { + const middlewareModule = await this.resolve(MIDDLEWARE_MODULE_ID); + if (middlewareModule) { + imports.push(`import { onRequest } from "${middlewareModule.id}";`); + exports.push(`export { onRequest };`); + } } return `${imports.join('\n')}${exports.join('\n')}`; diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 41f38a8b2..514fe2409 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -138,6 +138,7 @@ export function pluginSSR( manifest, logging: options.logging, entryPoints: internals.entryPoints, + middlewareEntryPoint: internals.middlewareEntryPoint, }); const code = injectManifest(manifest, internals.ssrEntryChunk); mutate(internals.ssrEntryChunk, 'server', code); @@ -260,6 +261,7 @@ export function pluginSSRSplit( manifest, logging: options.logging, entryPoints: internals.entryPoints, + middlewareEntryPoint: internals.middlewareEntryPoint, }); for (const [, chunk] of internals.ssrSplitEntryChunks) { const code = injectManifest(manifest, chunk); diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 59a42db34..9bef0d681 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -26,7 +26,6 @@ import { generatePages } from './generate.js'; import { trackPageData } from './internal.js'; import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.js'; import { registerAllPlugins } from './plugins/index.js'; -import { MIDDLEWARE_MODULE_ID } from './plugins/plugin-middleware.js'; import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js'; import { RESOLVED_SPLIT_MODULE_ID, SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js'; @@ -183,8 +182,6 @@ async function ssrBuild( ); } else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) { return makeSplitEntryPointFileName(chunkInfo.facadeModuleId, routes); - } else if (chunkInfo.facadeModuleId === MIDDLEWARE_MODULE_ID) { - return 'middleware.mjs'; } else if (chunkInfo.facadeModuleId === SSR_VIRTUAL_MODULE_ID) { return opts.settings.config.build.serverEntry; } else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) { diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 7410df470..ae681a543 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -25,6 +25,7 @@ const ASTRO_CONFIG_DEFAULTS = { redirects: true, inlineStylesheets: 'never', split: false, + excludeMiddleware: false, }, compressHTML: false, server: { @@ -122,6 +123,10 @@ export const AstroConfigSchema = z.object({ .default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets), split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split), + excludeMiddleware: z + .boolean() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware), }) .optional() .default({}), @@ -283,6 +288,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) { .default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets), split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split), + excludeMiddleware: z + .boolean() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware), }) .optional() .default({}), diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index dde07cd9c..33cb113a2 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -31,19 +31,26 @@ type EndpointCallResult = response: Response; }; +type CreateAPIContext = { + request: Request; + params: Params; + site?: string; + props: Record<string, any>; + adapterName?: string; +}; + +/** + * Creates a context that holds all the information needed to handle an Astro endpoint. + * + * @param {CreateAPIContext} payload + */ export function createAPIContext({ request, params, site, props, adapterName, -}: { - request: Request; - params: Params; - site?: string; - props: Record<string, any>; - adapterName?: string; -}): APIContext { +}: CreateAPIContext): APIContext { const context = { cookies: new AstroCookies(request), request, diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index f9fb07bd4..47127c674 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,9 +1,107 @@ -import type { MiddlewareResponseHandler } from '../../@types/astro'; +import type { MiddlewareResponseHandler, Params } from '../../@types/astro'; import { sequence } from './sequence.js'; +import { createAPIContext } from '../endpoint/index.js'; function defineMiddleware(fn: MiddlewareResponseHandler) { return fn; } +/** + * Payload for creating a context to be passed to Astro middleware + */ +export type CreateContext = { + /** + * The incoming request + */ + request: Request; + /** + * Optional parameters + */ + params?: Params; +}; + +/** + * Creates a context to be passed to Astro middleware `onRequest` function. + */ +function createContext({ request, params }: CreateContext) { + return createAPIContext({ + request, + params: params ?? {}, + props: {}, + site: undefined, + }); +} + +/** + * Checks whether the passed `value` is serializable. + * + * A serializable value contains plain values. For example, `Proxy`, `Set`, `Map`, functions, etc. + * are not accepted because they can't be serialized. + */ +function isLocalsSerializable(value: unknown): boolean { + let type = typeof value; + let plainObject = true; + if (type === 'object' && isPlainObject(value)) { + for (const [, nestedValue] of Object.entries(value)) { + if (!isLocalsSerializable(nestedValue)) { + plainObject = false; + break; + } + } + } else { + plainObject = false; + } + let result = + value === null || + type === 'string' || + type === 'number' || + type === 'boolean' || + Array.isArray(value) || + plainObject; + + return result; +} + +/** + * + * From [redux-toolkit](https://github.com/reduxjs/redux-toolkit/blob/master/packages/toolkit/src/isPlainObject.ts) + * + * Returns true if the passed value is "plain" object, i.e. an object whose + * prototype is the root `Object.prototype`. This includes objects created + * using object literals, but not for instance for class instances. + */ +function isPlainObject(value: unknown): value is object { + if (typeof value !== 'object' || value === null) return false; + + let proto = Object.getPrototypeOf(value); + if (proto === null) return true; + + let baseProto = proto; + while (Object.getPrototypeOf(baseProto) !== null) { + baseProto = Object.getPrototypeOf(baseProto); + } + + return proto === baseProto; +} + +/** + * It attempts to serialize `value` and return it as a string. + * + * ## Errors + * If the `value` is not serializable if the function will throw a runtime error. + * + * Something is **not serializable** when it contains properties/values like functions, `Map`, `Set`, `Date`, + * and other types that can't be made a string. + * + * @param value + */ +function trySerializeLocals(value: unknown) { + if (isLocalsSerializable(value)) { + return JSON.stringify(value); + } else { + throw new Error("The passed value can't be serialized."); + } +} + // NOTE: this export must export only the functions that will be exposed to user-land as officials APIs -export { sequence, defineMiddleware }; +export { sequence, defineMiddleware, createContext, trySerializeLocals }; diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index eaf4b21d1..b243ba979 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -298,22 +298,30 @@ export async function runHookBuildSetup({ return updatedConfig; } +type RunHookBuildSsr = { + config: AstroConfig; + manifest: SerializedSSRManifest; + logging: LogOptions; + entryPoints: Map<RouteData, URL>; + middlewareEntryPoint: URL | undefined; +}; + export async function runHookBuildSsr({ config, manifest, logging, entryPoints, -}: { - config: AstroConfig; - manifest: SerializedSSRManifest; - logging: LogOptions; - entryPoints: Map<RouteData, URL>; -}) { + middlewareEntryPoint, +}: RunHookBuildSsr) { for (const integration of config.integrations) { if (integration?.hooks?.['astro:build:ssr']) { await withTakingALongTimeMsg({ name: integration.name, - hookResult: integration.hooks['astro:build:ssr']({ manifest, entryPoints }), + hookResult: integration.hooks['astro:build:ssr']({ + manifest, + entryPoints, + middlewareEntryPoint, + }), logging, }); } @@ -340,17 +348,14 @@ export async function runHookBuildGenerated({ } } -export async function runHookBuildDone({ - config, - pages, - routes, - logging, -}: { +type RunHookBuildDone = { config: AstroConfig; pages: string[]; routes: RouteData[]; logging: LogOptions; -}) { +}; + +export async function runHookBuildDone({ config, pages, routes, logging }: RunHookBuildDone) { const dir = isServerLikeOutput(config) ? config.build.client : config.outDir; await fs.promises.mkdir(dir, { recursive: true }); diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index e2c57bafb..9e2213146 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -2,6 +2,8 @@ import { loadFixture } from './test-utils.js'; import { expect } from 'chai'; import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; +import { fileURLToPath } from 'node:url'; +import { readFileSync, existsSync } from 'node:fs'; describe('Middleware in DEV mode', () => { /** @type {import('./test-utils').Fixture} */ @@ -104,12 +106,19 @@ describe('Middleware in PROD mode, SSG', () => { describe('Middleware API in PROD mode, SSR', () => { /** @type {import('./test-utils').Fixture} */ let fixture; + let middlewarePath; before(async () => { fixture = await loadFixture({ root: './fixtures/middleware-dev/', output: 'server', - adapter: testAdapter({}), + adapter: testAdapter({ + setEntryPoints(entryPointsOrMiddleware) { + if (entryPointsOrMiddleware instanceof URL) { + middlewarePath = entryPointsOrMiddleware; + } + }, + }), }); await fixture.build(); }); @@ -201,6 +210,18 @@ describe('Middleware API in PROD mode, SSR', () => { const text = await response.text(); expect(text.includes('REDACTED')).to.be.true; }); + + it('the integration should receive the path to the middleware', async () => { + expect(middlewarePath).to.not.be.undefined; + try { + const path = fileURLToPath(middlewarePath); + expect(existsSync(path)).to.be.true; + const content = readFileSync(fileURLToPath(middlewarePath), 'utf-8'); + expect(content.length).to.be.greaterThan(0); + } catch (e) { + throw e; + } + }); }); describe('Middleware with tailwind', () => { @@ -224,3 +245,29 @@ describe('Middleware with tailwind', () => { expect(bundledCSS.includes('--tw-content')).to.be.true; }); }); + +describe('Middleware, split middleware option', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/middleware-dev/', + output: 'server', + build: { + excludeMiddleware: true, + }, + adapter: testAdapter({}), + }); + await fixture.build(); + }); + + it('should not render locals data because the page does not export it', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerio.load(html); + expect($('p').html()).to.not.equal('bar'); + }); +}); diff --git a/packages/astro/test/ssr-split-manifest.test.js b/packages/astro/test/ssr-split-manifest.test.js index 9e8a0981e..394740395 100644 --- a/packages/astro/test/ssr-split-manifest.test.js +++ b/packages/astro/test/ssr-split-manifest.test.js @@ -18,7 +18,9 @@ describe('astro:ssr-manifest, split', () => { output: 'server', adapter: testAdapter({ setEntryPoints(entries) { - entryPoints = entries; + if (entries) { + entryPoints = entries; + } }, setRoutes(routes) { currentRoutes = routes; diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index af5a7777b..ed79e5f21 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -74,9 +74,10 @@ export default function ( ...extendAdapter, }); }, - 'astro:build:ssr': ({ entryPoints }) => { + 'astro:build:ssr': ({ entryPoints, middlewareEntryPoint }) => { if (setEntryPoints) { setEntryPoints(entryPoints); + setEntryPoints(middlewareEntryPoint); } }, 'astro:build:done': ({ routes }) => { |