From b2b291d29143703cece0d12c8e74b2e1151d2061 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 7 Nov 2022 10:05:12 -0500 Subject: [PATCH] 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 * Update packages/integrations/cloudflare/src/server.advanced.ts Co-authored-by: Bjorn Lu * Include the subpath for links Co-authored-by: Bjorn Lu --- .changeset/blue-parrots-jam.md | 12 +++++++ packages/astro/src/@types/astro.ts | 1 + packages/astro/src/core/app/index.ts | 17 ++++++++-- .../astro/src/core/build/vite-plugin-ssr.ts | 8 +++-- packages/astro/src/core/preview/index.ts | 1 + .../ssr-request/src/pages/request.astro | 9 ++++- packages/astro/test/ssr-request.test.js | 34 +++++++++++++++++-- packages/astro/test/test-adapter.js | 16 ++++++++- packages/integrations/cloudflare/package.json | 3 ++ .../cloudflare/src/server.advanced.ts | 2 +- .../cloudflare/src/server.directory.ts | 2 +- packages/integrations/deno/package.json | 3 ++ packages/integrations/deno/src/server.ts | 2 +- packages/integrations/node/package.json | 3 ++ packages/integrations/node/src/http-server.ts | 6 ++-- packages/integrations/node/src/preview.ts | 11 +++++- packages/integrations/node/src/standalone.ts | 1 + 17 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 .changeset/blue-parrots-jam.md diff --git a/.changeset/blue-parrots-jam.md b/.changeset/blue-parrots-jam.md new file mode 100644 index 000000000000..91e38b2f845f --- /dev/null +++ b/.changeset/blue-parrots-jam.md @@ -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. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 86278b0b16fe..bf91d6e4d367 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1426,6 +1426,7 @@ export interface PreviewServerParams { serverEntrypoint: URL; host: string | undefined; port: number; + base: string; } export type CreatePreviewServer = ( diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index e3d2bbe1125f..7b7d6852d8b2 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -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 { 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); diff --git a/packages/astro/src/core/build/vite-plugin-ssr.ts b/packages/astro/src/core/build/vite-plugin-ssr.ts index 3e7fa7b62ce1..6f331a72d491 100644 --- a/packages/astro/src/core/build/vite-plugin-ssr.ts +++ b/packages/astro/src/core/build/vite-plugin-ssr.ts @@ -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; diff --git a/packages/astro/src/core/preview/index.ts b/packages/astro/src/core/preview/index.ts index 9bdd302b480f..818ac90fdee0 100644 --- a/packages/astro/src/core/preview/index.ts +++ b/packages/astro/src/core/preview/index.ts @@ -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; diff --git a/packages/astro/test/fixtures/ssr-request/src/pages/request.astro b/packages/astro/test/fixtures/ssr-request/src/pages/request.astro index 63da970274a6..0b0422dd515f 100644 --- a/packages/astro/test/fixtures/ssr-request/src/pages/request.astro +++ b/packages/astro/test/fixtures/ssr-request/src/pages/request.astro @@ -3,7 +3,14 @@ const origin = Astro.url.origin; --- -Testing + + Testing + +

{origin}

diff --git a/packages/astro/test/ssr-request.test.js b/packages/astro/test/ssr-request.test.js index 629753371f62..15c7d276526f 100644 --- a/packages/astro/test/ssr-request.test.js +++ b/packages/astro/test/ssr-request.test.js @@ -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'); + }); }); diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index 2bfc0bff45b3..64bf853365f5 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -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); } diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index f94fb76ef468..5a8142fffee4 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -36,6 +36,9 @@ "dependencies": { "esbuild": "^0.14.42" }, + "peerDependencies": { + "astro": "^1.6.3" + }, "devDependencies": { "astro": "workspace:*", "astro-scripts": "workspace:*", diff --git a/packages/integrations/cloudflare/src/server.advanced.ts b/packages/integrations/cloudflare/src/server.advanced.ts index 502700e0be97..ac040c9b2dcc 100644 --- a/packages/integrations/cloudflare/src/server.advanced.ts +++ b/packages/integrations/cloudflare/src/server.advanced.ts @@ -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); } diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts index d31e2189fde6..e7463b84ccb5 100644 --- a/packages/integrations/cloudflare/src/server.directory.ts +++ b/packages/integrations/cloudflare/src/server.directory.ts @@ -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); } diff --git a/packages/integrations/deno/package.json b/packages/integrations/deno/package.json index cedf5f813ce2..24dd0086241e 100644 --- a/packages/integrations/deno/package.json +++ b/packages/integrations/deno/package.json @@ -31,6 +31,9 @@ "dependencies": { "esbuild": "^0.14.43" }, + "peerDependencies": { + "astro": "^1.6.3" + }, "devDependencies": { "astro": "workspace:*", "astro-scripts": "workspace:*" diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index 453d73c40130..9dd00088903d 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -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 diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index 3ed4567f30c1..6ad2b01a4f00 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -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:*", diff --git a/packages/integrations/node/src/http-server.ts b/packages/integrations/node/src/http-server.ts index 4992ec0b9117..bdd628c29d16 100644 --- a/packages/integrations/node/src/http-server.ts +++ b/packages/integrations/node/src/http-server.ts @@ -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', }); diff --git a/packages/integrations/node/src/preview.ts b/packages/integrations/node/src/preview.ts index 443befa199ce..3b896cc72557 100644 --- a/packages/integrations/node/src/preview.ts +++ b/packages/integrations/node/src/preview.ts @@ -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; type MaybeServerModule = Partial; 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 ); diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts index 18cf8a2a5cb9..5ec2455eeb7a 100644 --- a/packages/integrations/node/src/standalone.ts +++ b/packages/integrations/node/src/standalone.ts @@ -45,6 +45,7 @@ export default function startServer(app: NodeApp, options: Options) { client, port, host, + removeBase: app.removeBase.bind(app), }, handler );