Handle base in adapters (#5290)

* Handle `base` in adapters

* Use removeBase in the test adapter

* Update packages/integrations/node/src/preview.ts

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>

* Update packages/integrations/cloudflare/src/server.advanced.ts

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>

* Include the subpath for links

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
This commit is contained in:
Matthew Phillips 2022-11-07 10:05:12 -05:00 committed by GitHub
parent 97e2b6ad7a
commit b2b291d291
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 116 additions and 15 deletions

View file

@ -0,0 +1,12 @@
---
'@astrojs/cloudflare': major
'@astrojs/deno': major
'@astrojs/node': major
'astro': patch
---
Handle base configuration in adapters
This allows adapters to correctly handle `base` configuration. Internally Astro now matches routes when the URL includes the `base`.
Adapters now also have access to the `removeBase` method which will remove the `base` from a pathname. This is useful to look up files for static assets.

View file

@ -1426,6 +1426,7 @@ export interface PreviewServerParams {
serverEntrypoint: URL;
host: string | undefined;
port: number;
base: string;
}
export type CreatePreviewServer = (

View file

@ -13,7 +13,7 @@ import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js
import { call as callEndpoint } from '../endpoint/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { error } from '../logger/core.js';
import { joinPaths, prependForwardSlash } from '../path.js';
import { joinPaths, prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
import {
createEnvironment,
createRenderContext,
@ -45,6 +45,8 @@ export class App {
dest: consoleLogDestination,
level: 'info',
};
#base: string;
#baseWithoutTrailingSlash: string;
constructor(manifest: Manifest, streaming = true) {
this.#manifest = manifest;
@ -78,6 +80,15 @@ export class App {
ssr: true,
streaming,
});
this.#base = this.#manifest.base || '/';
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#base);
}
removeBase(pathname: string) {
if(pathname.startsWith(this.#base)) {
return pathname.slice(this.#baseWithoutTrailingSlash.length + 1);
}
return pathname;
}
match(request: Request, { matchNotFound = false }: MatchOptions = {}): RouteData | undefined {
const url = new URL(request.url);
@ -85,7 +96,8 @@ export class App {
if (this.#manifest.assets.has(url.pathname)) {
return undefined;
}
let routeData = matchRoute(url.pathname, this.#manifestData);
let pathname = '/' + this.removeBase(url.pathname);
let routeData = matchRoute(pathname, this.#manifestData);
if (routeData) {
return routeData;
@ -157,7 +169,6 @@ export class App {
): Promise<Response> {
const url = new URL(request.url);
const manifest = this.#manifest;
const renderers = manifest.renderers;
const info = this.#routeDataToRouteInfo.get(routeData!)!;
const links = createLinkStylesheetElementSet(info.links, manifest.site);

View file

@ -13,6 +13,7 @@ import { pagesVirtualModuleId } from '../app/index.js';
import { serializeRouteData } from '../routing/index.js';
import { addRollupInput } from './add-rollup-input.js';
import { eachPageData, sortedCSS } from './internal.js';
import { removeLeadingForwardSlash, removeTrailingForwardSlash } from '../path.js';
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
@ -141,9 +142,12 @@ function buildManifest(
scripts.push({ type: 'external', value: entryModules[PAGE_SCRIPT_ID] });
}
const bareBase = removeTrailingForwardSlash(removeLeadingForwardSlash(settings.config.base));
const links = sortedCSS(pageData).map(pth => bareBase ? bareBase + '/' + pth : pth);
routes.push({
file: '',
links: sortedCSS(pageData),
links,
scripts: [
...scripts,
...settings.scripts
@ -172,7 +176,7 @@ function buildManifest(
pageMap: null as any,
renderers: [],
entryModules,
assets: staticFiles.map((s) => '/' + s),
assets: staticFiles.map((s) => settings.config.base + s),
};
return ssrManifest;

View file

@ -54,6 +54,7 @@ export default async function preview(
serverEntrypoint: new URL(settings.config.build.serverEntry, settings.config.build.server),
host,
port,
base: settings.config.base,
});
return server;

View file

@ -3,7 +3,14 @@ const origin = Astro.url.origin;
---
<html>
<head><title>Testing</title></head>
<head>
<title>Testing</title>
<style>
body {
background: orangered;
}
</style>
</head>
<body>
<h1 id="origin">{origin}</h1>
</body>

View file

@ -12,14 +12,16 @@ describe('Using Astro.request in SSR', () => {
root: './fixtures/ssr-request/',
adapter: testAdapter(),
output: 'server',
base: '/subpath/'
});
await fixture.build();
});
it('Gets the request pased in', async () => {
it('Gets the request passed in', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/request');
const request = new Request('http://example.com/subpath/request');
const response = await app.render(request);
expect(response.status).to.equal(200);
const html = await response.text();
const $ = cheerioLoad(html);
expect($('#origin').text()).to.equal('http://example.com');
@ -29,4 +31,32 @@ describe('Using Astro.request in SSR', () => {
const json = await fixture.readFile('/client/cars.json');
expect(json).to.not.be.undefined;
});
it('CSS assets have their base prefix', async () => {
const app = await fixture.loadTestAdapterApp();
let request = new Request('http://example.com/subpath/request');
let response = await app.render(request);
expect(response.status).to.equal(200);
const html = await response.text();
const $ = cheerioLoad(html);
const linkHref = $('link').attr('href');
expect(linkHref.startsWith('/subpath/')).to.equal(true);
request = new Request('http://example.com' + linkHref);
response = await app.render(request);
expect(response.status).to.equal(200);
const css = await response.text();
expect(css).to.not.be.an('undefined');
});
it('assets can be fetched', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/subpath/cars.json');
const response = await app.render(request);
expect(response.status).to.equal(200);
const data = await response.json();
expect(data).to.be.an('array');
});
});

View file

@ -25,9 +25,23 @@ export default function ({ provideAddress } = { provideAddress: true }) {
if (id === '@my-ssr') {
return `
import { App } from 'astro/app';
import fs from 'fs';
class MyApp extends App {
render(request, routeData) {
#manifest = null;
constructor(manifest, streaming) {
super(manifest, streaming);
this.#manifest = manifest;
}
async render(request, routeData) {
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);
}
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
return super.render(request, routeData);
}

View file

@ -36,6 +36,9 @@
"dependencies": {
"esbuild": "^0.14.42"
},
"peerDependencies": {
"astro": "^1.6.3"
},
"devDependencies": {
"astro": "workspace:*",
"astro-scripts": "workspace:*",

View file

@ -16,7 +16,7 @@ export function createExports(manifest: SSRManifest) {
// static assets
if (manifest.assets.has(pathname)) {
const assetRequest = new Request(`${origin}/static${pathname}`, request);
const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
return env.ASSETS.fetch(assetRequest);
}

View file

@ -17,7 +17,7 @@ export function createExports(manifest: SSRManifest) {
const { origin, pathname } = new URL(request.url);
// static assets
if (manifest.assets.has(pathname)) {
const assetRequest = new Request(`${origin}/static${pathname}`, request);
const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
return next(assetRequest);
}

View file

@ -31,6 +31,9 @@
"dependencies": {
"esbuild": "^0.14.43"
},
"peerDependencies": {
"astro": "^1.6.3"
},
"devDependencies": {
"astro": "workspace:*",
"astro-scripts": "workspace:*"

View file

@ -38,7 +38,7 @@ export function start(manifest: SSRManifest, options: Options) {
// If the request path wasn't found in astro,
// try to fetch a static file instead
const url = new URL(request.url);
const localPath = new URL('.' + url.pathname, clientRoot);
const localPath = new URL('./' + app.removeBase(url.pathname), clientRoot);
const fileResp = await fetch(localPath.toString());
// If the static file can't be found

View file

@ -33,6 +33,9 @@
"@astrojs/webapi": "^1.1.1",
"send": "^0.18.0"
},
"peerDependencies": {
"astro": "^1.6.3"
},
"devDependencies": {
"@types/send": "^0.17.1",
"astro": "workspace:*",

View file

@ -8,15 +8,17 @@ interface CreateServerOptions {
client: URL;
port: number;
host: string | undefined;
removeBase: (pathname: string) => string;
}
export function createServer(
{ client, port, host }: CreateServerOptions,
{ client, port, host, removeBase }: CreateServerOptions,
handler: http.RequestListener
) {
const listener: http.RequestListener = (req, res) => {
if (req.url) {
const stream = send(req, encodeURI(req.url), {
const pathname = '/' + removeBase(req.url);
const stream = send(req, encodeURI(pathname), {
root: fileURLToPath(client),
dotfiles: 'deny',
});

View file

@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
import { createServer } from './http-server.js';
import type { createExports } from './server';
const preview: CreatePreviewServer = async function ({ client, serverEntrypoint, host, port }) {
const preview: CreatePreviewServer = async function ({ client, serverEntrypoint, host, port, base }) {
type ServerModule = ReturnType<typeof createExports>;
type MaybeServerModule = Partial<ServerModule>;
let ssrHandler: ServerModule['handler'];
@ -36,11 +36,20 @@ const preview: CreatePreviewServer = async function ({ client, serverEntrypoint,
});
};
const baseWithoutTrailingSlash: string = base.endsWith('/') ? base.slice(0, base.length - 1) : base;
function removeBase(pathname: string): string {
if(pathname.startsWith(base)) {
return pathname.slice(baseWithoutTrailingSlash.length);
}
return pathname;
}
const server = createServer(
{
client,
port,
host,
removeBase
},
handler
);

View file

@ -45,6 +45,7 @@ export default function startServer(app: NodeApp, options: Options) {
client,
port,
host,
removeBase: app.removeBase.bind(app),
},
handler
);