Skip to content

Commit

Permalink
feat: expose locals to render api and from requests in dev mode (#7385)
Browse files Browse the repository at this point in the history
Co-authored-by: Emanuele Stoppa <[email protected]>
Co-authored-by: wrapperup <[email protected]>
  • Loading branch information
ematipico and wrapperup authored Jun 21, 2023
1 parent 61d6e45 commit 8e2923c
Show file tree
Hide file tree
Showing 22 changed files with 254 additions and 87 deletions.
6 changes: 6 additions & 0 deletions .changeset/smooth-jokes-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'astro': minor
'@astrojs/node': minor
---

`Astro.locals` is now exposed to the adapter API. Node Adapter can now pass in a `locals` object in the SSR handler middleware.
8 changes: 4 additions & 4 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class App {
return undefined;
}
}
async render(request: Request, routeData?: RouteData): Promise<Response> {
async render(request: Request, routeData?: RouteData, locals?: object): Promise<Response> {
let defaultStatus = 200;
if (!routeData) {
routeData = this.match(request);
Expand All @@ -131,7 +131,7 @@ export class App {
}
}

Reflect.set(request, clientLocalsSymbol, {});
Reflect.set(request, clientLocalsSymbol, locals ?? {});

// Use the 404 status code for 404.astro components
if (routeData.route === '/404') {
Expand Down Expand Up @@ -243,15 +243,15 @@ export class App {
page.onRequest as MiddlewareResponseHandler,
apiContext,
() => {
return renderPage({ mod, renderContext, env: this.#env, apiContext });
return renderPage({ mod, renderContext, env: this.#env, cookies: apiContext.cookies });
}
);
} else {
response = await renderPage({
mod,
renderContext,
env: this.#env,
apiContext,
cookies: apiContext.cookies,
});
}
Reflect.set(request, responseSentSymbol, true);
Expand Down
14 changes: 9 additions & 5 deletions packages/astro/src/core/app/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ export class NodeApp extends App {
match(req: NodeIncomingMessage | Request, opts: MatchOptions = {}) {
return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req), opts);
}
render(req: NodeIncomingMessage | Request, routeData?: RouteData) {
render(req: NodeIncomingMessage | Request, routeData?: RouteData, locals?: object) {
if (typeof req.body === 'string' && req.body.length > 0) {
return super.render(
req instanceof Request ? req : createRequestFromNodeRequest(req, Buffer.from(req.body)),
routeData
routeData,
locals
);
}

Expand All @@ -54,7 +55,8 @@ export class NodeApp extends App {
req instanceof Request
? req
: createRequestFromNodeRequest(req, Buffer.from(JSON.stringify(req.body))),
routeData
routeData,
locals
);
}

Expand All @@ -75,13 +77,15 @@ export class NodeApp extends App {
return reqBodyComplete.then(() => {
return super.render(
req instanceof Request ? req : createRequestFromNodeRequest(req, body),
routeData
routeData,
locals
);
});
}
return super.render(
req instanceof Request ? req : createRequestFromNodeRequest(req),
routeData
routeData,
locals
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,8 @@ async function generatePath(
mod,
renderContext,
env,
apiContext,
isCompressHTML: settings.config.compressHTML,
cookies: apiContext.cookies,
});
}
);
Expand All @@ -613,8 +613,8 @@ async function generatePath(
mod,
renderContext,
env,
apiContext,
isCompressHTML: settings.config.compressHTML,
cookies: apiContext.cookies,
});
}
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export function createAPIContext({

// We define a custom property, so we can check the value passed to locals
Object.defineProperty(context, 'locals', {
enumerable: true,
get() {
return Reflect.get(request, clientLocalsSymbol);
},
Expand Down
24 changes: 23 additions & 1 deletion packages/astro/src/core/render/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import type {
SSRElement,
SSRResult,
} from '../../@types/astro';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { getParamsAndPropsOrThrow } from './core.js';
import type { Environment } from './environment';

const clientLocalsSymbol = Symbol.for('astro.locals');

/**
* The RenderContext represents the parts of rendering that are specific to one request.
*/
Expand All @@ -27,6 +30,7 @@ export interface RenderContext {
cookies?: AstroCookies;
params: Params;
props: Props;
locals?: object;
}

export type CreateRenderContextArgs = Partial<RenderContext> & {
Expand All @@ -51,12 +55,30 @@ export async function createRenderContext(
logging: options.env.logging,
ssr: options.env.ssr,
});
return {

let context = {
...options,
origin,
pathname,
url,
params,
props,
};

// We define a custom property, so we can check the value passed to locals
Object.defineProperty(context, 'locals', {
enumerable: true,
get() {
return Reflect.get(request, clientLocalsSymbol);
},
set(val) {
if (typeof val !== 'object') {
throw new AstroError(AstroErrorData.LocalsNotAnObject);
} else {
Reflect.set(request, clientLocalsSymbol, val);
}
},
});

return context;
}
14 changes: 6 additions & 8 deletions packages/astro/src/core/render/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { APIContext, ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
import type { AstroCookies, ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
import { render, renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
import { attachToResponse } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import type { LogOptions } from '../logger/core.js';
Expand Down Expand Up @@ -108,15 +108,15 @@ export type RenderPage = {
mod: ComponentInstance;
renderContext: RenderContext;
env: Environment;
apiContext?: APIContext;
isCompressHTML?: boolean;
cookies: AstroCookies;
};

export async function renderPage({
mod,
renderContext,
env,
apiContext,
cookies,
isCompressHTML = false,
}: RenderPage) {
if (routeIsRedirect(renderContext.route)) {
Expand All @@ -133,8 +133,6 @@ export async function renderPage({
if (!Component)
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);

let locals = apiContext?.locals ?? {};

const result = createResult({
adapterName: env.adapterName,
links: renderContext.links,
Expand All @@ -155,8 +153,8 @@ export async function renderPage({
scripts: renderContext.scripts,
ssr: env.ssr,
status: renderContext.status ?? 200,
cookies: apiContext?.cookies,
locals,
cookies,
locals: renderContext.locals ?? {},
});

// Support `export const components` for `MDX` pages
Expand Down
27 changes: 18 additions & 9 deletions packages/astro/src/core/render/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,22 +180,31 @@ export async function renderPage(options: SSROptions): Promise<Response> {
mod,
env,
});
const apiContext = createAPIContext({
request: options.request,
params: renderContext.params,
props: renderContext.props,
adapterName: options.env.adapterName,
});
if (options.middleware) {
if (options.middleware && options.middleware.onRequest) {
const apiContext = createAPIContext({
request: options.request,
params: renderContext.params,
props: renderContext.props,
adapterName: options.env.adapterName,
});

const onRequest = options.middleware.onRequest as MiddlewareResponseHandler;
const response = await callMiddleware<Response>(env.logging, onRequest, apiContext, () => {
return coreRenderPage({ mod, renderContext, env: options.env, apiContext });
return coreRenderPage({
mod,
renderContext,
env: options.env,
cookies: apiContext.cookies,
});
});

return response;
}
}
return await coreRenderPage({ mod, renderContext, env: options.env }); // NOTE: without "await", errors won’t get caught below
return await coreRenderPage({
mod,
renderContext,
env: options.env,
cookies: apiContext.cookies,
}); // NOTE: without "await", errors won’t get caught below
}
4 changes: 3 additions & 1 deletion packages/astro/src/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface CreateRequestOptions {
body?: RequestBody | undefined;
logging: LogOptions;
ssr: boolean;
locals?: object | undefined;
}

const clientAddressSymbol = Symbol.for('astro.clientAddress');
Expand All @@ -26,6 +27,7 @@ export function createRequest({
body = undefined,
logging,
ssr,
locals,
}: CreateRequestOptions): Request {
let headersObj =
headers instanceof Headers
Expand Down Expand Up @@ -66,7 +68,7 @@ export function createRequest({
Reflect.set(request, clientAddressSymbol, clientAddress);
}

Reflect.set(request, clientLocalsSymbol, {});
Reflect.set(request, clientLocalsSymbol, locals ?? {});

return request;
}
3 changes: 3 additions & 0 deletions packages/astro/src/vite-plugin-astro-server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { isServerLikeOutput } from '../prerender/utils.js';
import { log404 } from './common.js';
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';

const clientLocalsSymbol = Symbol.for('astro.locals');

type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
...args: any
) => Promise<infer R>
Expand Down Expand Up @@ -153,6 +155,7 @@ export async function handleRoute(
logging,
ssr: buildingToSSR,
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
locals: Reflect.get(req, clientLocalsSymbol), // Allows adapters to pass in locals in dev mode.
});

// Set user specified headers to response object.
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/ssr-locals/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/ssr-locals",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
10 changes: 10 additions & 0 deletions packages/astro/test/fixtures/ssr-locals/src/pages/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

export async function get({ locals }) {
let out = { ...locals };

return new Response(JSON.stringify(out), {
headers: {
'Content-Type': 'application/json'
}
});
}
4 changes: 4 additions & 0 deletions packages/astro/test/fixtures/ssr-locals/src/pages/foo.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
const { foo } = Astro.locals;
---
<h1 id="foo">{ foo }</h1>
40 changes: 40 additions & 0 deletions packages/astro/test/ssr-locals.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';

describe('SSR Astro.locals from server', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-locals/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
});

it('Can access Astro.locals in page', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/foo');
const locals = { foo: 'bar' };
const response = await app.render(request, undefined, locals);
const html = await response.text();

const $ = cheerio.load(html);
expect($('#foo').text()).to.equal('bar');
});

it('Can access Astro.locals in api context', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/api');
const locals = { foo: 'bar' };
const response = await app.render(request, undefined, locals);
expect(response.status).to.equal(200);
const body = await response.json();

expect(body.foo).to.equal('bar');
});
});
5 changes: 2 additions & 3 deletions packages/astro/test/test-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,16 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
this.#manifest = manifest;
}
async render(request, routeData) {
async render(request, routeData, locals) {
const url = new URL(request.url);
if(this.#manifest.assets.has(url.pathname)) {
const filePath = new URL('../client/' + this.removeBase(url.pathname), import.meta.url);
const data = await fs.promises.readFile(filePath);
return new Response(data);
}
Reflect.set(request, Symbol.for('astro.locals'), {});
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
return super.render(request, routeData);
return super.render(request, routeData, locals);
}
}
Expand Down
Loading

0 comments on commit 8e2923c

Please sign in to comment.