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:
parent
ed92730925
commit
61113dd731
9 changed files with 116 additions and 9 deletions
5
.changeset/shaggy-moons-judge.md
Normal file
5
.changeset/shaggy-moons-judge.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@astrojs/deno': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix prerendered page behavior
|
|
@ -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": {
|
||||||
|
|
|
@ -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).">
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
10
packages/integrations/deno/src/__deno_imports.ts
Normal file
10
packages/integrations/deno/src/__deno_imports.ts
Normal 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() {}
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
|
9
packages/integrations/deno/test/fixtures/basics/src/pages/prerender.astro
vendored
Normal file
9
packages/integrations/deno/test/fixtures/basics/src/pages/prerender.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
export const prerender = true;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>test</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue