Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose locals to render api and from requests in dev mode #7385

Merged
merged 18 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -244,15 +244,15 @@ export class App {
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 @@ -599,8 +599,8 @@ async function generatePath(
mod,
renderContext,
env,
apiContext,
isCompressHTML: settings.config.compressHTML,
cookies: apiContext.cookies,
});
}
);
Expand All @@ -609,8 +609,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', {
matthewp marked this conversation as resolved.
Show resolved Hide resolved
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