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;
|
serverEntrypoint: URL;
|
||||||
host: string | undefined;
|
host: string | undefined;
|
||||||
port: number;
|
port: number;
|
||||||
|
base: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreatePreviewServer = (
|
export type CreatePreviewServer = (
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js
|
||||||
import { call as callEndpoint } from '../endpoint/index.js';
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { consoleLogDestination } from '../logger/console.js';
|
import { consoleLogDestination } from '../logger/console.js';
|
||||||
import { error } from '../logger/core.js';
|
import { error } from '../logger/core.js';
|
||||||
import { joinPaths, prependForwardSlash } from '../path.js';
|
import { joinPaths, prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
|
||||||
import {
|
import {
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
createRenderContext,
|
createRenderContext,
|
||||||
|
@ -45,6 +45,8 @@ export class App {
|
||||||
dest: consoleLogDestination,
|
dest: consoleLogDestination,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
};
|
};
|
||||||
|
#base: string;
|
||||||
|
#baseWithoutTrailingSlash: string;
|
||||||
|
|
||||||
constructor(manifest: Manifest, streaming = true) {
|
constructor(manifest: Manifest, streaming = true) {
|
||||||
this.#manifest = manifest;
|
this.#manifest = manifest;
|
||||||
|
@ -78,6 +80,15 @@ export class App {
|
||||||
ssr: true,
|
ssr: true,
|
||||||
streaming,
|
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 {
|
match(request: Request, { matchNotFound = false }: MatchOptions = {}): RouteData | undefined {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
@ -85,7 +96,8 @@ export class App {
|
||||||
if (this.#manifest.assets.has(url.pathname)) {
|
if (this.#manifest.assets.has(url.pathname)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let routeData = matchRoute(url.pathname, this.#manifestData);
|
let pathname = '/' + this.removeBase(url.pathname);
|
||||||
|
let routeData = matchRoute(pathname, this.#manifestData);
|
||||||
|
|
||||||
if (routeData) {
|
if (routeData) {
|
||||||
return routeData;
|
return routeData;
|
||||||
|
@ -157,7 +169,6 @@ export class App {
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const manifest = this.#manifest;
|
const manifest = this.#manifest;
|
||||||
const renderers = manifest.renderers;
|
|
||||||
const info = this.#routeDataToRouteInfo.get(routeData!)!;
|
const info = this.#routeDataToRouteInfo.get(routeData!)!;
|
||||||
const links = createLinkStylesheetElementSet(info.links, manifest.site);
|
const links = createLinkStylesheetElementSet(info.links, manifest.site);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { pagesVirtualModuleId } from '../app/index.js';
|
||||||
import { serializeRouteData } from '../routing/index.js';
|
import { serializeRouteData } from '../routing/index.js';
|
||||||
import { addRollupInput } from './add-rollup-input.js';
|
import { addRollupInput } from './add-rollup-input.js';
|
||||||
import { eachPageData, sortedCSS } from './internal.js';
|
import { eachPageData, sortedCSS } from './internal.js';
|
||||||
|
import { removeLeadingForwardSlash, removeTrailingForwardSlash } from '../path.js';
|
||||||
|
|
||||||
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
@ -141,9 +142,12 @@ function buildManifest(
|
||||||
scripts.push({ type: 'external', value: entryModules[PAGE_SCRIPT_ID] });
|
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({
|
routes.push({
|
||||||
file: '',
|
file: '',
|
||||||
links: sortedCSS(pageData),
|
links,
|
||||||
scripts: [
|
scripts: [
|
||||||
...scripts,
|
...scripts,
|
||||||
...settings.scripts
|
...settings.scripts
|
||||||
|
@ -172,7 +176,7 @@ function buildManifest(
|
||||||
pageMap: null as any,
|
pageMap: null as any,
|
||||||
renderers: [],
|
renderers: [],
|
||||||
entryModules,
|
entryModules,
|
||||||
assets: staticFiles.map((s) => '/' + s),
|
assets: staticFiles.map((s) => settings.config.base + s),
|
||||||
};
|
};
|
||||||
|
|
||||||
return ssrManifest;
|
return ssrManifest;
|
||||||
|
|
|
@ -54,6 +54,7 @@ export default async function preview(
|
||||||
serverEntrypoint: new URL(settings.config.build.serverEntry, settings.config.build.server),
|
serverEntrypoint: new URL(settings.config.build.serverEntry, settings.config.build.server),
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
base: settings.config.base,
|
||||||
});
|
});
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
|
|
|
@ -3,7 +3,14 @@ const origin = Astro.url.origin;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head><title>Testing</title></head>
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: orangered;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 id="origin">{origin}</h1>
|
<h1 id="origin">{origin}</h1>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -12,14 +12,16 @@ describe('Using Astro.request in SSR', () => {
|
||||||
root: './fixtures/ssr-request/',
|
root: './fixtures/ssr-request/',
|
||||||
adapter: testAdapter(),
|
adapter: testAdapter(),
|
||||||
output: 'server',
|
output: 'server',
|
||||||
|
base: '/subpath/'
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Gets the request pased in', async () => {
|
it('Gets the request passed in', async () => {
|
||||||
const app = await fixture.loadTestAdapterApp();
|
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);
|
const response = await app.render(request);
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerioLoad(html);
|
const $ = cheerioLoad(html);
|
||||||
expect($('#origin').text()).to.equal('http://example.com');
|
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');
|
const json = await fixture.readFile('/client/cars.json');
|
||||||
expect(json).to.not.be.undefined;
|
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') {
|
if (id === '@my-ssr') {
|
||||||
return `
|
return `
|
||||||
import { App } from 'astro/app';
|
import { App } from 'astro/app';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
class MyApp extends App {
|
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';` : ''}
|
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
|
||||||
return super.render(request, routeData);
|
return super.render(request, routeData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.14.42"
|
"esbuild": "^0.14.42"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"astro": "^1.6.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*",
|
||||||
"astro-scripts": "workspace:*",
|
"astro-scripts": "workspace:*",
|
||||||
|
|
|
@ -16,7 +16,7 @@ export function createExports(manifest: SSRManifest) {
|
||||||
|
|
||||||
// static assets
|
// static assets
|
||||||
if (manifest.assets.has(pathname)) {
|
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);
|
return env.ASSETS.fetch(assetRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ export function createExports(manifest: SSRManifest) {
|
||||||
const { origin, pathname } = new URL(request.url);
|
const { origin, pathname } = new URL(request.url);
|
||||||
// static assets
|
// static assets
|
||||||
if (manifest.assets.has(pathname)) {
|
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);
|
return next(assetRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.14.43"
|
"esbuild": "^0.14.43"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"astro": "^1.6.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*",
|
||||||
"astro-scripts": "workspace:*"
|
"astro-scripts": "workspace:*"
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function start(manifest: SSRManifest, options: Options) {
|
||||||
// If the request path wasn't found in astro,
|
// If the request path wasn't found in astro,
|
||||||
// try to fetch a static file instead
|
// try to fetch a static file instead
|
||||||
const url = new URL(request.url);
|
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());
|
const fileResp = await fetch(localPath.toString());
|
||||||
|
|
||||||
// If the static file can't be found
|
// If the static file can't be found
|
||||||
|
|
|
@ -33,6 +33,9 @@
|
||||||
"@astrojs/webapi": "^1.1.1",
|
"@astrojs/webapi": "^1.1.1",
|
||||||
"send": "^0.18.0"
|
"send": "^0.18.0"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"astro": "^1.6.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/send": "^0.17.1",
|
"@types/send": "^0.17.1",
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*",
|
||||||
|
|
|
@ -8,15 +8,17 @@ interface CreateServerOptions {
|
||||||
client: URL;
|
client: URL;
|
||||||
port: number;
|
port: number;
|
||||||
host: string | undefined;
|
host: string | undefined;
|
||||||
|
removeBase: (pathname: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createServer(
|
export function createServer(
|
||||||
{ client, port, host }: CreateServerOptions,
|
{ client, port, host, removeBase }: CreateServerOptions,
|
||||||
handler: http.RequestListener
|
handler: http.RequestListener
|
||||||
) {
|
) {
|
||||||
const listener: http.RequestListener = (req, res) => {
|
const listener: http.RequestListener = (req, res) => {
|
||||||
if (req.url) {
|
if (req.url) {
|
||||||
const stream = send(req, encodeURI(req.url), {
|
const pathname = '/' + removeBase(req.url);
|
||||||
|
const stream = send(req, encodeURI(pathname), {
|
||||||
root: fileURLToPath(client),
|
root: fileURLToPath(client),
|
||||||
dotfiles: 'deny',
|
dotfiles: 'deny',
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
||||||
import { createServer } from './http-server.js';
|
import { createServer } from './http-server.js';
|
||||||
import type { createExports } from './server';
|
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 ServerModule = ReturnType<typeof createExports>;
|
||||||
type MaybeServerModule = Partial<ServerModule>;
|
type MaybeServerModule = Partial<ServerModule>;
|
||||||
let ssrHandler: ServerModule['handler'];
|
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(
|
const server = createServer(
|
||||||
{
|
{
|
||||||
client,
|
client,
|
||||||
port,
|
port,
|
||||||
host,
|
host,
|
||||||
|
removeBase
|
||||||
},
|
},
|
||||||
handler
|
handler
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,6 +45,7 @@ export default function startServer(app: NodeApp, options: Options) {
|
||||||
client,
|
client,
|
||||||
port,
|
port,
|
||||||
host,
|
host,
|
||||||
|
removeBase: app.removeBase.bind(app),
|
||||||
},
|
},
|
||||||
handler
|
handler
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue