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:
parent
97e2b6ad7a
commit
b2b291d291
17 changed files with 116 additions and 15 deletions
12
.changeset/blue-parrots-jam.md
Normal file
12
.changeset/blue-parrots-jam.md
Normal 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.
|
|
@ -1426,6 +1426,7 @@ export interface PreviewServerParams {
|
|||
serverEntrypoint: URL;
|
||||
host: string | undefined;
|
||||
port: number;
|
||||
base: string;
|
||||
}
|
||||
|
||||
export type CreatePreviewServer = (
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
"dependencies": {
|
||||
"esbuild": "^0.14.42"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^1.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
"dependencies": {
|
||||
"esbuild": "^0.14.43"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^1.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -45,6 +45,7 @@ export default function startServer(app: NodeApp, options: Options) {
|
|||
client,
|
||||
port,
|
||||
host,
|
||||
removeBase: app.removeBase.bind(app),
|
||||
},
|
||||
handler
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue