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:
parent
293f82484a
commit
990f44b655
10 changed files with 128 additions and 18 deletions
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
10
packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js
vendored
Normal file
10
packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
export function get() {
|
||||
return {
|
||||
body: JSON.stringify([
|
||||
{ name: 'lettuce' },
|
||||
{ name: 'broccoli' },
|
||||
{ name: 'pizza' }
|
||||
])
|
||||
};
|
||||
}
|
6
packages/astro/test/fixtures/ssr-api-route/src/pages/index.astro
vendored
Normal file
6
packages/astro/test/fixtures/ssr-api-route/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
<html>
|
||||
<head><title>Testing</title></head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
</body>
|
||||
</html>
|
39
packages/astro/test/ssr-api-route.test.js
Normal file
39
packages/astro/test/ssr-api-route.test.js
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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) }; }`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue