0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

Fix API route support

This commit is contained in:
Matthew Phillips 2022-03-24 08:06:41 -04:00
parent 293f82484a
commit 990f44b655
10 changed files with 128 additions and 18 deletions

View file

@ -1,14 +1,19 @@
import type { ComponentInstance, ManifestData, RouteData, SSRLoadedRenderer } from '../../@types/astro';
import type { ComponentInstance, EndpointHandler, ManifestData, RouteData } from '../../@types/astro';
import type { SSRManifest as Manifest, RouteInfo } from './types';
import { defaultLogOptions } from '../logger.js';
export { deserializeManifest } from './common.js';
import { matchRoute } from '../routing/match.js';
import { render } from '../render/core.js';
import { call as callEndpoint } from '../endpoint/index.js';
import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { prependForwardSlash } from '../path.js';
const supportedFileNameToMimeTypes = new Map<string, string>([
['json', 'application/json']
]);
export class App {
#manifest: Manifest;
#manifestData: ManifestData;
@ -40,12 +45,22 @@ export class App {
}
}
const manifest = this.#manifest;
const info = this.#routeDataToRouteInfo.get(routeData!)!;
const mod = this.#manifest.pageMap.get(routeData.component)!;
const renderers = this.#manifest.renderers;
if(routeData.type === 'page') {
return this.#renderPage(request, routeData, mod);
} else if(routeData.type === 'endpoint') {
return this.#callEndpoint(request, routeData, mod);
} else {
throw new Error(`Unsupported route type [${routeData.type}].`);
}
}
async #renderPage(request: Request, routeData: RouteData, mod: ComponentInstance): 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);
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
@ -88,4 +103,38 @@ export class App {
}
});
}
async #callEndpoint(request: Request, routeData: RouteData, mod: ComponentInstance): Promise<Response> {
const url = new URL(request.url);
const handler = mod as unknown as EndpointHandler;
const result = await callEndpoint(handler, {
headers: request.headers,
logging: defaultLogOptions,
method: request.method,
origin: url.origin,
pathname: url.pathname,
routeCache: this.#routeCache,
ssr: true,
});
if(result.type === 'response') {
return result.response;
} else {
const body = result.body;
const ext = /\.([a-z]+)/.exec(url.pathname);
const headers = new Headers();
if(ext) {
const mime = supportedFileNameToMimeTypes.get(ext[1]);
if(mime) {
headers.set('Content-Type', mime);
}
}
const bytes = this.#encoder.encode(body);
headers.set('Content-Length', bytes.byteLength.toString());
return new Response(bytes, {
status: 200,
headers
});
}
}
}

View file

@ -90,7 +90,7 @@ export async function generatePages(result: RollupOutput, opts: StaticBuildOptio
const ssr = !!opts.astroConfig._ctx.adapter?.serverEntrypoint;
const outFolder = ssr ? getServerRoot(opts.astroConfig) : getOutRoot(opts.astroConfig);
const ssrEntryURL = new URL('./astro-entry.mjs', outFolder);
const ssrEntryURL = new URL('./entry.mjs', outFolder);
const ssrEntry = await import(ssrEntryURL.toString());
for(const pageData of eachPageData(internals)) {

View file

@ -19,6 +19,7 @@ import { vitePluginPages } from './vite-plugin-pages.js';
import { generatePages } from './generate.js';
import { trackPageData } from './internal.js';
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
import { isBuildingToSSR } from '../util.js';
export async function staticBuild(opts: StaticBuildOptions) {
const { allPages, astroConfig } = opts;
@ -111,11 +112,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
outDir: fileURLToPath(out),
ssr: true,
rollupOptions: {
input: [],// TODO can we remove this? Array.from(input),
input: [],
output: {
format: 'esm',
entryFileNames: 'astro-entry.mjs',
//chunkFileNames: 'chunks/[name].[hash].mjs',
entryFileNames: 'entry.mjs',
assetFileNames: 'assets/[name].[hash][extname]',
},
},
@ -134,8 +134,8 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
}),
...(viteConfig.plugins || []),
// SSR needs to be last
opts.astroConfig._ctx.adapter?.serverEntrypoint &&
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter),
isBuildingToSSR(opts.astroConfig) &&
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
],
publicDir: ssr ? false : viteConfig.publicDir,
root: viteConfig.root,

View file

@ -1,8 +1,8 @@
import type { RouteData, SerializedRouteData } from '../../../@types/astro';
function createRouteData(pattern: RegExp, params: string[], component: string, pathname: string | undefined): RouteData {
function createRouteData(pattern: RegExp, params: string[], component: string, pathname: string | undefined, type: 'page' | 'endpoint'): RouteData {
return {
type: 'page',
type,
pattern,
params,
component,
@ -20,7 +20,7 @@ export function serializeRouteData(routeData: RouteData): SerializedRouteData {
}
export function deserializeRouteData(rawRouteData: SerializedRouteData) {
const { component, params, pathname } = rawRouteData;
const { component, params, pathname, type } = rawRouteData;
const pattern = new RegExp(rawRouteData.pattern);
return createRouteData(pattern, params, component, pathname);
return createRouteData(pattern, params, component, pathname, type);
}

View file

@ -0,0 +1,10 @@
export function get() {
return {
body: JSON.stringify([
{ name: 'lettuce' },
{ name: 'broccoli' },
{ name: 'pizza' }
])
};
}

View file

@ -0,0 +1,6 @@
<html>
<head><title>Testing</title></head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -0,0 +1,39 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';
// Asset bundling
describe('API routes in SSR', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/ssr-api-route/',
buildOptions: {
experimentalSsr: true,
},
adapter: testAdapter()
});
await fixture.build();
});
it('Basic pages work', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
const html = await response.text();
expect(html).to.not.be.empty;
});
it('Can load the API route too', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/food.json');
const response = await app.render(request);
expect(response.status).to.equal(200);
expect(response.headers.get('Content-Type')).to.equal('application/json');
expect(response.headers.get('Content-Length')).to.not.be.empty;
const body = await response.json();
expect(body.length).to.equal(3);
});
});

View file

@ -2,10 +2,10 @@ import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';
import { App } from '../dist/core/app/index.js';
// Asset bundling
describe('Dynamic pages in SSR', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
@ -20,8 +20,7 @@ describe('Dynamic pages in SSR', () => {
});
it('Do not have to implement getStaticPaths', async () => {
const {createApp} = await import('./fixtures/ssr-dynamic/dist/server/entry.mjs');
const app = createApp(new URL('./fixtures/ssr-dynamic/dist/server/', import.meta.url));
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/123');
const response = await app.render(request);
const html = await response.text();

View file

@ -23,7 +23,7 @@ export default function() {
},
load(id) {
if(id === '@my-ssr') {
return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: (root) => new App(manifest, root) }; }`;
return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: () => new App(manifest) }; }`;
}
}
}

View file

@ -20,6 +20,7 @@ polyfill(globalThis, {
* @typedef {import('../src/core/dev/index').DevServer} DevServer
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
* @typedef {import('../src/core/app/index').App} App
*
*
* @typedef {Object} Fixture
@ -30,6 +31,7 @@ polyfill(globalThis, {
* @property {() => Promise<DevServer>} startDevServer
* @property {() => Promise<PreviewServer>} preview
* @property {() => Promise<void>} clean
* @property {() => Promise<App>} loadTestAdapterApp
*/
/**
@ -85,6 +87,11 @@ export async function loadFixture(inlineConfig) {
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
loadTestAdapterApp: async () => {
const url = new URL('./server/entry.mjs', config.dist);
const {createApp} = await import(url);
return createApp();
}
};
}