Fix prerendered page handling on Deno (#6284)

* fix(deno): handle prerendered pages

* test(deno): add prerender test

* fix: defensively access vite.build.rollupOptions.external

* fix(deno): support other formats of rollupOptions.external

* fix(deno): crawl prerendered files for match

* fix(deno): ignore deno error in server file

* fix(deno): cross-platform serve file
This commit is contained in:
Nate Moore 2023-02-17 12:19:19 -06:00 committed by GitHub
parent ed92730925
commit 61113dd731
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 116 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/deno': patch
---
Fix prerendered page behavior

View file

@ -6,7 +6,7 @@
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "start": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "deno run --allow-net --allow-read ./dist/server/entry.mjs", "preview": "deno run --allow-net --allow-read --allow-env ./dist/server/entry.mjs",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {

View file

@ -1,5 +1,7 @@
--- ---
import Layout from '../components/Layout.astro'; import Layout from '../components/Layout.astro';
export const prerender = true;
--- ---
<Layout title="Welcome to Astro (on Deno)."> <Layout title="Welcome to Astro (on Deno).">

View file

@ -20,6 +20,7 @@
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
"./server.js": "./dist/server.js", "./server.js": "./dist/server.js",
"./__deno_imports.js": "./dist/__deno_imports.js",
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"scripts": { "scripts": {

View file

@ -0,0 +1,10 @@
// This file is a shim for any Deno-specific imports!
// It will be replaced in the final Deno build.
//
// This allows us to prerender pages in Node.
export class Server {
listenAndServe() {}
}
export function serveFile() {}
export function fromFileUrl() {}

View file

@ -20,6 +20,16 @@ const SHIM = `globalThis.process = {
env: Deno.env.toObject(), env: Deno.env.toObject(),
};`; };`;
const DENO_VERSION = `0.177.0`
// We shim deno-specific imports so we can run the code in Node
// to prerender pages. In the final Deno build, this import is
// replaced with the Deno-specific contents listed below.
const DENO_IMPORTS_SHIM = `@astrojs/deno/__deno_imports.js`;
const DENO_IMPORTS = `export { Server } from "https://deno.land/std@${DENO_VERSION}/http/server.ts"
export { serveFile } from 'https://deno.land/std@${DENO_VERSION}/http/file_server.ts';
export { fromFileUrl } from "https://deno.land/std@${DENO_VERSION}/path/mod.ts";`
export function getAdapter(args?: Options): AstroAdapter { export function getAdapter(args?: Options): AstroAdapter {
return { return {
name: '@astrojs/deno', name: '@astrojs/deno',
@ -29,6 +39,18 @@ export function getAdapter(args?: Options): AstroAdapter {
}; };
} }
const denoImportsShimPlugin = {
name: '@astrojs/deno:shim',
setup(build: esbuild.PluginBuild) {
build.onLoad({ filter: /__deno_imports\.js$/ }, async (args) => {
return {
contents: DENO_IMPORTS,
loader: 'js',
}
})
},
}
export default function createIntegration(args?: Options): AstroIntegration { export default function createIntegration(args?: Options): AstroIntegration {
let _buildConfig: BuildConfig; let _buildConfig: BuildConfig;
let _vite: any; let _vite: any;
@ -49,8 +71,11 @@ export default function createIntegration(args?: Options): AstroIntegration {
'astro:build:setup': ({ vite, target }) => { 'astro:build:setup': ({ vite, target }) => {
if (target === 'server') { if (target === 'server') {
_vite = vite; _vite = vite;
vite.resolve = vite.resolve || {}; vite.resolve = vite.resolve ?? {};
vite.resolve.alias = vite.resolve.alias || {}; vite.resolve.alias = vite.resolve.alias ?? {};
vite.build = vite.build ?? {};
vite.build.rollupOptions = vite.build.rollupOptions ?? {};
vite.build.rollupOptions.external = vite.build.rollupOptions.external ?? [];
const aliases = [{ find: 'react-dom/server', replacement: 'react-dom/server.browser' }]; const aliases = [{ find: 'react-dom/server', replacement: 'react-dom/server.browser' }];
@ -61,10 +86,15 @@ export default function createIntegration(args?: Options): AstroIntegration {
(vite.resolve.alias as Record<string, string>)[alias.find] = alias.replacement; (vite.resolve.alias as Record<string, string>)[alias.find] = alias.replacement;
} }
} }
vite.ssr = { vite.ssr = {
noExternal: true, noExternal: true,
}; };
if (Array.isArray(vite.build.rollupOptions.external)) {
vite.build.rollupOptions.external.push(DENO_IMPORTS_SHIM);
} else if (typeof vite.build.rollupOptions.external !== 'function') {
vite.build.rollupOptions.external = [vite.build.rollupOptions.external, DENO_IMPORTS_SHIM]
}
} }
}, },
'astro:build:done': async () => { 'astro:build:done': async () => {
@ -80,6 +110,9 @@ export default function createIntegration(args?: Options): AstroIntegration {
format: 'esm', format: 'esm',
bundle: true, bundle: true,
external: ['@astrojs/markdown-remark'], external: ['@astrojs/markdown-remark'],
plugins: [
denoImportsShimPlugin
],
banner: { banner: {
js: SHIM, js: SHIM,
}, },

View file

@ -3,9 +3,7 @@ import type { SSRManifest } from 'astro';
import { App } from 'astro/app'; import { App } from 'astro/app';
// @ts-ignore // @ts-ignore
import { Server } from 'https://deno.land/std@0.167.0/http/server.ts'; import { Server, serveFile, fromFileUrl } from '@astrojs/deno/__deno_imports.js';
// @ts-ignore
import { fetch } from 'https://deno.land/x/file_fetch/mod.ts';
interface Options { interface Options {
port?: number; port?: number;
@ -16,6 +14,17 @@ interface Options {
let _server: Server | undefined = undefined; let _server: Server | undefined = undefined;
let _startPromise: Promise<void> | undefined = undefined; let _startPromise: Promise<void> | undefined = undefined;
async function* getPrerenderedFiles(clientRoot: URL): AsyncGenerator<URL> {
// @ts-ignore
for await (const ent of Deno.readDir(clientRoot)) {
if (ent.isDirectory) {
yield* getPrerenderedFiles(new URL(`./${ent.name}/`, clientRoot))
} else if (ent.name.endsWith('.html')) {
yield new URL(`./${ent.name}`, clientRoot)
}
}
}
export function start(manifest: SSRManifest, options: Options) { export function start(manifest: SSRManifest, options: Options) {
if (options.start === false) { if (options.start === false) {
return; return;
@ -40,7 +49,24 @@ export function start(manifest: SSRManifest, options: Options) {
// 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('./' + app.removeBase(url.pathname), clientRoot); const localPath = new URL('./' + app.removeBase(url.pathname), clientRoot);
const fileResp = await fetch(localPath.toString());
let fileResp = await serveFile(request, fromFileUrl(localPath));
// Attempt to serve `index.html` if 404
if (fileResp.status == 404) {
let fallback;
for await (const file of getPrerenderedFiles(clientRoot)) {
const pathname = file.pathname.replace(/\/(index)?\.html$/, '');
if (localPath.pathname.endsWith(pathname)) {
fallback = file;
break;
}
}
if (fallback) {
fileResp = await serveFile(request, fromFileUrl(fallback));
}
}
// If the static file can't be found // If the static file can't be found
if (fileResp.status == 404) { if (fileResp.status == 404) {

View file

@ -68,7 +68,7 @@ Deno.test({
resp = await fetch(new URL(href!, baseUrl)); resp = await fetch(new URL(href!, baseUrl));
assertEquals(resp.status, 200); assertEquals(resp.status, 200);
const ct = resp.headers.get('content-type'); const ct = resp.headers.get('content-type');
assertEquals(ct, 'text/css'); assertEquals(ct, 'text/css; charset=UTF-8');
await resp.body!.cancel(); await resp.body!.cancel();
}); });
}, },
@ -143,3 +143,24 @@ Deno.test({
sanitizeResources: false, sanitizeResources: false,
sanitizeOps: false, sanitizeOps: false,
}); });
Deno.test({
name: 'perendering',
permissions: defaultTestPermissions,
async fn() {
await startApp(async (baseUrl: URL) => {
const resp = await fetch(new URL('/prerender', baseUrl));
assertEquals(resp.status, 200);
const html = await resp.text();
assert(html);
const doc = new DOMParser().parseFromString(html, `text/html`);
const h1 = doc!.querySelector('h1');
assertEquals(h1!.innerText, 'test');
});
},
sanitizeResources: false,
sanitizeOps: false,
});

View file

@ -0,0 +1,9 @@
---
export const prerender = true;
---
<html>
<body>
<h1>test</h1>
</body>
</html>