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 type { SSRManifest as Manifest, RouteInfo } from './types';
|
||||||
|
|
||||||
import { defaultLogOptions } from '../logger.js';
|
import { defaultLogOptions } from '../logger.js';
|
||||||
export { deserializeManifest } from './common.js';
|
export { deserializeManifest } from './common.js';
|
||||||
import { matchRoute } from '../routing/match.js';
|
import { matchRoute } from '../routing/match.js';
|
||||||
import { render } from '../render/core.js';
|
import { render } from '../render/core.js';
|
||||||
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||||
import { prependForwardSlash } from '../path.js';
|
import { prependForwardSlash } from '../path.js';
|
||||||
|
|
||||||
|
const supportedFileNameToMimeTypes = new Map<string, string>([
|
||||||
|
['json', 'application/json']
|
||||||
|
]);
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
#manifest: Manifest;
|
#manifest: Manifest;
|
||||||
#manifestData: ManifestData;
|
#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 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 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 links = createLinkStylesheetElementSet(info.links, manifest.site);
|
||||||
const scripts = createModuleScriptElementWithSrcSet(info.scripts, 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 ssr = !!opts.astroConfig._ctx.adapter?.serverEntrypoint;
|
||||||
const outFolder = ssr ? getServerRoot(opts.astroConfig) : getOutRoot(opts.astroConfig);
|
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());
|
const ssrEntry = await import(ssrEntryURL.toString());
|
||||||
|
|
||||||
for(const pageData of eachPageData(internals)) {
|
for(const pageData of eachPageData(internals)) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { vitePluginPages } from './vite-plugin-pages.js';
|
||||||
import { generatePages } from './generate.js';
|
import { generatePages } from './generate.js';
|
||||||
import { trackPageData } from './internal.js';
|
import { trackPageData } from './internal.js';
|
||||||
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
|
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
|
||||||
|
import { isBuildingToSSR } from '../util.js';
|
||||||
|
|
||||||
export async function staticBuild(opts: StaticBuildOptions) {
|
export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
const { allPages, astroConfig } = opts;
|
const { allPages, astroConfig } = opts;
|
||||||
|
@ -111,11 +112,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
||||||
outDir: fileURLToPath(out),
|
outDir: fileURLToPath(out),
|
||||||
ssr: true,
|
ssr: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: [],// TODO can we remove this? Array.from(input),
|
input: [],
|
||||||
output: {
|
output: {
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
entryFileNames: 'astro-entry.mjs',
|
entryFileNames: 'entry.mjs',
|
||||||
//chunkFileNames: 'chunks/[name].[hash].mjs',
|
|
||||||
assetFileNames: 'assets/[name].[hash][extname]',
|
assetFileNames: 'assets/[name].[hash][extname]',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -134,8 +134,8 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
||||||
}),
|
}),
|
||||||
...(viteConfig.plugins || []),
|
...(viteConfig.plugins || []),
|
||||||
// SSR needs to be last
|
// SSR needs to be last
|
||||||
opts.astroConfig._ctx.adapter?.serverEntrypoint &&
|
isBuildingToSSR(opts.astroConfig) &&
|
||||||
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter),
|
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
|
||||||
],
|
],
|
||||||
publicDir: ssr ? false : viteConfig.publicDir,
|
publicDir: ssr ? false : viteConfig.publicDir,
|
||||||
root: viteConfig.root,
|
root: viteConfig.root,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { RouteData, SerializedRouteData } from '../../../@types/astro';
|
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 {
|
return {
|
||||||
type: 'page',
|
type,
|
||||||
pattern,
|
pattern,
|
||||||
params,
|
params,
|
||||||
component,
|
component,
|
||||||
|
@ -20,7 +20,7 @@ export function serializeRouteData(routeData: RouteData): SerializedRouteData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeRouteData(rawRouteData: SerializedRouteData) {
|
export function deserializeRouteData(rawRouteData: SerializedRouteData) {
|
||||||
const { component, params, pathname } = rawRouteData;
|
const { component, params, pathname, type } = rawRouteData;
|
||||||
const pattern = new RegExp(rawRouteData.pattern);
|
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 { load as cheerioLoad } from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
import testAdapter from './test-adapter.js';
|
import testAdapter from './test-adapter.js';
|
||||||
import { App } from '../dist/core/app/index.js';
|
|
||||||
|
|
||||||
// Asset bundling
|
// Asset bundling
|
||||||
describe('Dynamic pages in SSR', () => {
|
describe('Dynamic pages in SSR', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -20,8 +20,7 @@ describe('Dynamic pages in SSR', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Do not have to implement getStaticPaths', async () => {
|
it('Do not have to implement getStaticPaths', async () => {
|
||||||
const {createApp} = await import('./fixtures/ssr-dynamic/dist/server/entry.mjs');
|
const app = await fixture.loadTestAdapterApp();
|
||||||
const app = createApp(new URL('./fixtures/ssr-dynamic/dist/server/', import.meta.url));
|
|
||||||
const request = new Request('http://example.com/123');
|
const request = new Request('http://example.com/123');
|
||||||
const response = await app.render(request);
|
const response = await app.render(request);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default function() {
|
||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if(id === '@my-ssr') {
|
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/core/dev/index').DevServer} DevServer
|
||||||
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
|
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
|
||||||
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
||||||
|
* @typedef {import('../src/core/app/index').App} App
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @typedef {Object} Fixture
|
* @typedef {Object} Fixture
|
||||||
|
@ -30,6 +31,7 @@ polyfill(globalThis, {
|
||||||
* @property {() => Promise<DevServer>} startDevServer
|
* @property {() => Promise<DevServer>} startDevServer
|
||||||
* @property {() => Promise<PreviewServer>} preview
|
* @property {() => Promise<PreviewServer>} preview
|
||||||
* @property {() => Promise<void>} clean
|
* @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'),
|
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
|
||||||
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
||||||
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
|
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