feat: expose locals to render api and from requests in dev mode (#7385)
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: wrapperup <wrapperup4@gmail.com>
This commit is contained in:
parent
61d6e45cef
commit
8e2923cc62
22 changed files with 254 additions and 87 deletions
6
.changeset/smooth-jokes-watch.md
Normal file
6
.changeset/smooth-jokes-watch.md
Normal file
|
@ -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.
|
|
@ -115,7 +115,7 @@ export class App {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async render(request: Request, routeData?: RouteData): Promise<Response> {
|
async render(request: Request, routeData?: RouteData, locals?: object): Promise<Response> {
|
||||||
let defaultStatus = 200;
|
let defaultStatus = 200;
|
||||||
if (!routeData) {
|
if (!routeData) {
|
||||||
routeData = this.match(request);
|
routeData = this.match(request);
|
||||||
|
@ -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
|
// Use the 404 status code for 404.astro components
|
||||||
if (routeData.route === '/404') {
|
if (routeData.route === '/404') {
|
||||||
|
@ -243,7 +243,7 @@ export class App {
|
||||||
page.onRequest as MiddlewareResponseHandler,
|
page.onRequest as MiddlewareResponseHandler,
|
||||||
apiContext,
|
apiContext,
|
||||||
() => {
|
() => {
|
||||||
return renderPage({ mod, renderContext, env: this.#env, apiContext });
|
return renderPage({ mod, renderContext, env: this.#env, cookies: apiContext.cookies });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -251,7 +251,7 @@ export class App {
|
||||||
mod,
|
mod,
|
||||||
renderContext,
|
renderContext,
|
||||||
env: this.#env,
|
env: this.#env,
|
||||||
apiContext,
|
cookies: apiContext.cookies,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reflect.set(request, responseSentSymbol, true);
|
Reflect.set(request, responseSentSymbol, true);
|
||||||
|
|
|
@ -41,11 +41,12 @@ export class NodeApp extends App {
|
||||||
match(req: NodeIncomingMessage | Request, opts: MatchOptions = {}) {
|
match(req: NodeIncomingMessage | Request, opts: MatchOptions = {}) {
|
||||||
return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req), opts);
|
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) {
|
if (typeof req.body === 'string' && req.body.length > 0) {
|
||||||
return super.render(
|
return super.render(
|
||||||
req instanceof Request ? req : createRequestFromNodeRequest(req, Buffer.from(req.body)),
|
req instanceof Request ? req : createRequestFromNodeRequest(req, Buffer.from(req.body)),
|
||||||
routeData
|
routeData,
|
||||||
|
locals
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +55,8 @@ export class NodeApp extends App {
|
||||||
req instanceof Request
|
req instanceof Request
|
||||||
? req
|
? req
|
||||||
: createRequestFromNodeRequest(req, Buffer.from(JSON.stringify(req.body))),
|
: createRequestFromNodeRequest(req, Buffer.from(JSON.stringify(req.body))),
|
||||||
routeData
|
routeData,
|
||||||
|
locals
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,13 +77,15 @@ export class NodeApp extends App {
|
||||||
return reqBodyComplete.then(() => {
|
return reqBodyComplete.then(() => {
|
||||||
return super.render(
|
return super.render(
|
||||||
req instanceof Request ? req : createRequestFromNodeRequest(req, body),
|
req instanceof Request ? req : createRequestFromNodeRequest(req, body),
|
||||||
routeData
|
routeData,
|
||||||
|
locals
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return super.render(
|
return super.render(
|
||||||
req instanceof Request ? req : createRequestFromNodeRequest(req),
|
req instanceof Request ? req : createRequestFromNodeRequest(req),
|
||||||
routeData
|
routeData,
|
||||||
|
locals
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -603,8 +603,8 @@ async function generatePath(
|
||||||
mod,
|
mod,
|
||||||
renderContext,
|
renderContext,
|
||||||
env,
|
env,
|
||||||
apiContext,
|
|
||||||
isCompressHTML: settings.config.compressHTML,
|
isCompressHTML: settings.config.compressHTML,
|
||||||
|
cookies: apiContext.cookies,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -613,8 +613,8 @@ async function generatePath(
|
||||||
mod,
|
mod,
|
||||||
renderContext,
|
renderContext,
|
||||||
env,
|
env,
|
||||||
apiContext,
|
|
||||||
isCompressHTML: settings.config.compressHTML,
|
isCompressHTML: settings.config.compressHTML,
|
||||||
|
cookies: apiContext.cookies,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -78,6 +78,7 @@ export function createAPIContext({
|
||||||
|
|
||||||
// We define a custom property, so we can check the value passed to locals
|
// We define a custom property, so we can check the value passed to locals
|
||||||
Object.defineProperty(context, 'locals', {
|
Object.defineProperty(context, 'locals', {
|
||||||
|
enumerable: true,
|
||||||
get() {
|
get() {
|
||||||
return Reflect.get(request, clientLocalsSymbol);
|
return Reflect.get(request, clientLocalsSymbol);
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,9 +7,12 @@ import type {
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { getParamsAndPropsOrThrow } from './core.js';
|
import { getParamsAndPropsOrThrow } from './core.js';
|
||||||
import type { Environment } from './environment';
|
import type { Environment } from './environment';
|
||||||
|
|
||||||
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The RenderContext represents the parts of rendering that are specific to one request.
|
* The RenderContext represents the parts of rendering that are specific to one request.
|
||||||
*/
|
*/
|
||||||
|
@ -27,6 +30,7 @@ export interface RenderContext {
|
||||||
cookies?: AstroCookies;
|
cookies?: AstroCookies;
|
||||||
params: Params;
|
params: Params;
|
||||||
props: Props;
|
props: Props;
|
||||||
|
locals?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateRenderContextArgs = Partial<RenderContext> & {
|
export type CreateRenderContextArgs = Partial<RenderContext> & {
|
||||||
|
@ -51,7 +55,8 @@ export async function createRenderContext(
|
||||||
logging: options.env.logging,
|
logging: options.env.logging,
|
||||||
ssr: options.env.ssr,
|
ssr: options.env.ssr,
|
||||||
});
|
});
|
||||||
return {
|
|
||||||
|
let context = {
|
||||||
...options,
|
...options,
|
||||||
origin,
|
origin,
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -59,4 +64,21 @@ export async function createRenderContext(
|
||||||
params,
|
params,
|
||||||
props,
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { APIContext, ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
|
import type { AstroCookies, ComponentInstance, Params, Props, RouteData } from '../../@types/astro';
|
||||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
import { render, renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
||||||
import { attachToResponse } from '../cookies/index.js';
|
import { attachToResponse } from '../cookies/index.js';
|
||||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import type { LogOptions } from '../logger/core.js';
|
import type { LogOptions } from '../logger/core.js';
|
||||||
|
@ -108,15 +108,15 @@ export type RenderPage = {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
renderContext: RenderContext;
|
renderContext: RenderContext;
|
||||||
env: Environment;
|
env: Environment;
|
||||||
apiContext?: APIContext;
|
|
||||||
isCompressHTML?: boolean;
|
isCompressHTML?: boolean;
|
||||||
|
cookies: AstroCookies;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function renderPage({
|
export async function renderPage({
|
||||||
mod,
|
mod,
|
||||||
renderContext,
|
renderContext,
|
||||||
env,
|
env,
|
||||||
apiContext,
|
cookies,
|
||||||
isCompressHTML = false,
|
isCompressHTML = false,
|
||||||
}: RenderPage) {
|
}: RenderPage) {
|
||||||
if (routeIsRedirect(renderContext.route)) {
|
if (routeIsRedirect(renderContext.route)) {
|
||||||
|
@ -133,8 +133,6 @@ export async function renderPage({
|
||||||
if (!Component)
|
if (!Component)
|
||||||
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||||
|
|
||||||
let locals = apiContext?.locals ?? {};
|
|
||||||
|
|
||||||
const result = createResult({
|
const result = createResult({
|
||||||
adapterName: env.adapterName,
|
adapterName: env.adapterName,
|
||||||
links: renderContext.links,
|
links: renderContext.links,
|
||||||
|
@ -155,8 +153,8 @@ export async function renderPage({
|
||||||
scripts: renderContext.scripts,
|
scripts: renderContext.scripts,
|
||||||
ssr: env.ssr,
|
ssr: env.ssr,
|
||||||
status: renderContext.status ?? 200,
|
status: renderContext.status ?? 200,
|
||||||
cookies: apiContext?.cookies,
|
cookies,
|
||||||
locals,
|
locals: renderContext.locals ?? {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Support `export const components` for `MDX` pages
|
// Support `export const components` for `MDX` pages
|
||||||
|
|
|
@ -180,22 +180,31 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
mod,
|
mod,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
|
const apiContext = createAPIContext({
|
||||||
|
request: options.request,
|
||||||
|
params: renderContext.params,
|
||||||
|
props: renderContext.props,
|
||||||
|
adapterName: options.env.adapterName,
|
||||||
|
});
|
||||||
if (options.middleware) {
|
if (options.middleware) {
|
||||||
if (options.middleware && options.middleware.onRequest) {
|
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 onRequest = options.middleware.onRequest as MiddlewareResponseHandler;
|
||||||
const response = await callMiddleware<Response>(env.logging, onRequest, apiContext, () => {
|
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 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export interface CreateRequestOptions {
|
||||||
body?: RequestBody | undefined;
|
body?: RequestBody | undefined;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
ssr: boolean;
|
ssr: boolean;
|
||||||
|
locals?: object | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
@ -26,6 +27,7 @@ export function createRequest({
|
||||||
body = undefined,
|
body = undefined,
|
||||||
logging,
|
logging,
|
||||||
ssr,
|
ssr,
|
||||||
|
locals,
|
||||||
}: CreateRequestOptions): Request {
|
}: CreateRequestOptions): Request {
|
||||||
let headersObj =
|
let headersObj =
|
||||||
headers instanceof Headers
|
headers instanceof Headers
|
||||||
|
@ -66,7 +68,7 @@ export function createRequest({
|
||||||
Reflect.set(request, clientAddressSymbol, clientAddress);
|
Reflect.set(request, clientAddressSymbol, clientAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
Reflect.set(request, clientLocalsSymbol, {});
|
Reflect.set(request, clientLocalsSymbol, locals ?? {});
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import { isServerLikeOutput } from '../prerender/utils.js';
|
||||||
import { log404 } from './common.js';
|
import { log404 } from './common.js';
|
||||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||||
|
|
||||||
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
||||||
...args: any
|
...args: any
|
||||||
) => Promise<infer R>
|
) => Promise<infer R>
|
||||||
|
@ -153,6 +155,7 @@ export async function handleRoute(
|
||||||
logging,
|
logging,
|
||||||
ssr: buildingToSSR,
|
ssr: buildingToSSR,
|
||||||
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
|
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.
|
// Set user specified headers to response object.
|
||||||
|
|
8
packages/astro/test/fixtures/ssr-locals/package.json
vendored
Normal file
8
packages/astro/test/fixtures/ssr-locals/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/ssr-locals",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
10
packages/astro/test/fixtures/ssr-locals/src/pages/api.js
vendored
Normal file
10
packages/astro/test/fixtures/ssr-locals/src/pages/api.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
export async function get({ locals }) {
|
||||||
|
let out = { ...locals };
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(out), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
4
packages/astro/test/fixtures/ssr-locals/src/pages/foo.astro
vendored
Normal file
4
packages/astro/test/fixtures/ssr-locals/src/pages/foo.astro
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
const { foo } = Astro.locals;
|
||||||
|
---
|
||||||
|
<h1 id="foo">{ foo }</h1>
|
40
packages/astro/test/ssr-locals.test.js
Normal file
40
packages/astro/test/ssr-locals.test.js
Normal file
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -34,7 +34,7 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
|
||||||
this.#manifest = manifest;
|
this.#manifest = manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(request, routeData) {
|
async render(request, routeData, locals) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
if(this.#manifest.assets.has(url.pathname)) {
|
if(this.#manifest.assets.has(url.pathname)) {
|
||||||
const filePath = new URL('../client/' + this.removeBase(url.pathname), import.meta.url);
|
const filePath = new URL('../client/' + this.removeBase(url.pathname), import.meta.url);
|
||||||
|
@ -42,9 +42,8 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd
|
||||||
return new Response(data);
|
return new Response(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Reflect.set(request, Symbol.for('astro.locals'), {});
|
|
||||||
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
|
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
|
||||||
return super.render(request, routeData);
|
return super.render(request, routeData, locals);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,6 +122,25 @@ app.use(ssrHandler);
|
||||||
app.listen({ port: 8080 });
|
app.listen({ port: 8080 });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Additionally, you can also pass in an object to be accessed with `Astro.locals` or in Astro middleware:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import express from 'express';
|
||||||
|
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.static('dist/client/'))
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const locals = {
|
||||||
|
title: 'New title'
|
||||||
|
};
|
||||||
|
|
||||||
|
ssrHandler(req, res, next, locals);
|
||||||
|
);
|
||||||
|
|
||||||
|
app.listen(8080);
|
||||||
|
```
|
||||||
|
|
||||||
Note that middleware mode does not do file serving. You'll need to configure your HTTP framework to do that for you. By default the client assets are written to `./dist/client/`.
|
Note that middleware mode does not do file serving. You'll need to configure your HTTP framework to do that for you. By default the client assets are written to `./dist/client/`.
|
||||||
|
|
||||||
### Standalone
|
### Standalone
|
||||||
|
|
|
@ -9,14 +9,15 @@ export default function (app: NodeApp, mode: Options['mode']) {
|
||||||
return async function (
|
return async function (
|
||||||
req: IncomingMessage,
|
req: IncomingMessage,
|
||||||
res: ServerResponse,
|
res: ServerResponse,
|
||||||
next?: (err?: unknown) => void
|
next?: (err?: unknown) => void,
|
||||||
|
locals?: object
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const route =
|
const route =
|
||||||
mode === 'standalone' ? app.match(req, { matchNotFound: true }) : app.match(req);
|
mode === 'standalone' ? app.match(req, { matchNotFound: true }) : app.match(req);
|
||||||
if (route) {
|
if (route) {
|
||||||
try {
|
try {
|
||||||
const response = await app.render(req);
|
const response = await app.render(req, route, locals);
|
||||||
await writeWebResponse(app, res, response);
|
await writeWebResponse(app, res, response);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (next) {
|
if (next) {
|
||||||
|
|
9
packages/integrations/node/test/fixtures/locals/package.json
vendored
Normal file
9
packages/integrations/node/test/fixtures/locals/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@test/locals",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*",
|
||||||
|
"@astrojs/node": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
10
packages/integrations/node/test/fixtures/locals/src/pages/api.js
vendored
Normal file
10
packages/integrations/node/test/fixtures/locals/src/pages/api.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
export async function post({ locals }) {
|
||||||
|
let out = { ...locals };
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(out), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
4
packages/integrations/node/test/fixtures/locals/src/pages/foo.astro
vendored
Normal file
4
packages/integrations/node/test/fixtures/locals/src/pages/foo.astro
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
const { foo } = Astro.locals;
|
||||||
|
---
|
||||||
|
<h1>{foo}</h1>
|
53
packages/integrations/node/test/locals.test.js
Normal file
53
packages/integrations/node/test/locals.test.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import nodejs from '../dist/index.js';
|
||||||
|
import { loadFixture, createRequestAndResponse } from './test-utils.js';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('API routes', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/locals/',
|
||||||
|
output: 'server',
|
||||||
|
adapter: nodejs({ mode: 'middleware' }),
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can render locals in page', async () => {
|
||||||
|
const { handler } = await import('./fixtures/locals/dist/server/entry.mjs');
|
||||||
|
let { req, res, text } = createRequestAndResponse({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/foo',
|
||||||
|
});
|
||||||
|
|
||||||
|
let locals = { foo: 'bar' };
|
||||||
|
|
||||||
|
handler(req, res, () => {}, locals);
|
||||||
|
req.send();
|
||||||
|
|
||||||
|
let html = await text();
|
||||||
|
|
||||||
|
expect(html).to.contain('<h1>bar</h1>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can access locals in API', async () => {
|
||||||
|
const { handler } = await import('./fixtures/locals/dist/server/entry.mjs');
|
||||||
|
let { req, res, done } = createRequestAndResponse({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api',
|
||||||
|
});
|
||||||
|
|
||||||
|
let locals = { foo: 'bar' };
|
||||||
|
|
||||||
|
handler(req, res, () => {}, locals);
|
||||||
|
req.send();
|
||||||
|
|
||||||
|
let [buffer] = await done;
|
||||||
|
|
||||||
|
let json = JSON.parse(buffer.toString('utf-8'));
|
||||||
|
|
||||||
|
expect(json.foo).to.equal('bar');
|
||||||
|
});
|
||||||
|
});
|
|
@ -3252,6 +3252,12 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../..
|
version: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/ssr-locals:
|
||||||
|
dependencies:
|
||||||
|
astro:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/ssr-manifest:
|
packages/astro/test/fixtures/ssr-manifest:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro:
|
astro:
|
||||||
|
@ -4415,7 +4421,7 @@ importers:
|
||||||
version: 9.2.2
|
version: 9.2.2
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
version: 4.3.1(@types/node@14.18.21)
|
version: 4.3.1(@types/node@18.16.3)(sass@1.52.2)
|
||||||
|
|
||||||
packages/integrations/netlify/test/edge-functions/fixtures/dynimport:
|
packages/integrations/netlify/test/edge-functions/fixtures/dynimport:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4550,6 +4556,15 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../../../astro
|
version: link:../../../../../astro
|
||||||
|
|
||||||
|
packages/integrations/node/test/fixtures/locals:
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/node':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../..
|
||||||
|
astro:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../../astro
|
||||||
|
|
||||||
packages/integrations/node/test/fixtures/node-middleware:
|
packages/integrations/node/test/fixtures/node-middleware:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/node':
|
'@astrojs/node':
|
||||||
|
@ -4930,7 +4945,7 @@ importers:
|
||||||
version: 3.0.0(vite@4.3.1)(vue@3.2.47)
|
version: 3.0.0(vite@4.3.1)(vue@3.2.47)
|
||||||
'@vue/babel-plugin-jsx':
|
'@vue/babel-plugin-jsx':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1(@babel/core@7.21.8)
|
||||||
'@vue/compiler-sfc':
|
'@vue/compiler-sfc':
|
||||||
specifier: ^3.2.39
|
specifier: ^3.2.39
|
||||||
version: 3.2.39
|
version: 3.2.39
|
||||||
|
@ -9317,23 +9332,6 @@ packages:
|
||||||
resolution: {integrity: sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==}
|
resolution: {integrity: sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@vue/babel-plugin-jsx@1.1.1:
|
|
||||||
resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==}
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-module-imports': 7.21.4
|
|
||||||
'@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.18.2)
|
|
||||||
'@babel/template': 7.20.7
|
|
||||||
'@babel/traverse': 7.18.2
|
|
||||||
'@babel/types': 7.21.5
|
|
||||||
'@vue/babel-helper-vue-transform-on': 1.0.2
|
|
||||||
camelcase: 6.3.0
|
|
||||||
html-tags: 3.3.1
|
|
||||||
svg-tags: 1.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
- supports-color
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@vue/babel-plugin-jsx@1.1.1(@babel/core@7.21.8):
|
/@vue/babel-plugin-jsx@1.1.1(@babel/core@7.21.8):
|
||||||
resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==}
|
resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -17665,39 +17663,6 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/vite@4.3.1(@types/node@14.18.21):
|
|
||||||
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
'@types/node': '>= 14'
|
|
||||||
less: '*'
|
|
||||||
sass: '*'
|
|
||||||
stylus: '*'
|
|
||||||
sugarss: '*'
|
|
||||||
terser: ^5.4.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/node':
|
|
||||||
optional: true
|
|
||||||
less:
|
|
||||||
optional: true
|
|
||||||
sass:
|
|
||||||
optional: true
|
|
||||||
stylus:
|
|
||||||
optional: true
|
|
||||||
sugarss:
|
|
||||||
optional: true
|
|
||||||
terser:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 14.18.21
|
|
||||||
esbuild: 0.17.18
|
|
||||||
postcss: 8.4.23
|
|
||||||
rollup: 3.21.8
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents: 2.3.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite@4.3.1(@types/node@18.16.3)(sass@1.52.2):
|
/vite@4.3.1(@types/node@18.16.3)(sass@1.52.2):
|
||||||
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
Loading…
Reference in a new issue