diff --git a/.changeset/shaggy-moons-judge.md b/.changeset/shaggy-moons-judge.md new file mode 100644 index 000000000..f23e08334 --- /dev/null +++ b/.changeset/shaggy-moons-judge.md @@ -0,0 +1,5 @@ +--- +'@astrojs/deno': patch +--- + +Fix prerendered page behavior diff --git a/examples/deno/package.json b/examples/deno/package.json index 8841cba73..9d9ac114b 100644 --- a/examples/deno/package.json +++ b/examples/deno/package.json @@ -6,7 +6,7 @@ "dev": "astro dev", "start": "astro dev", "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" }, "dependencies": { diff --git a/examples/deno/src/pages/index.astro b/examples/deno/src/pages/index.astro index 19e358c77..0399a7534 100644 --- a/examples/deno/src/pages/index.astro +++ b/examples/deno/src/pages/index.astro @@ -1,5 +1,7 @@ --- import Layout from '../components/Layout.astro'; + +export const prerender = true; --- diff --git a/packages/integrations/deno/package.json b/packages/integrations/deno/package.json index 56804afec..dc6b4ae46 100644 --- a/packages/integrations/deno/package.json +++ b/packages/integrations/deno/package.json @@ -20,6 +20,7 @@ "exports": { ".": "./dist/index.js", "./server.js": "./dist/server.js", + "./__deno_imports.js": "./dist/__deno_imports.js", "./package.json": "./package.json" }, "scripts": { diff --git a/packages/integrations/deno/src/__deno_imports.ts b/packages/integrations/deno/src/__deno_imports.ts new file mode 100644 index 000000000..2775f1a5b --- /dev/null +++ b/packages/integrations/deno/src/__deno_imports.ts @@ -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() {} diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts index 11f5adebf..fcfda5dc3 100644 --- a/packages/integrations/deno/src/index.ts +++ b/packages/integrations/deno/src/index.ts @@ -20,6 +20,16 @@ const SHIM = `globalThis.process = { 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 { return { 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 { let _buildConfig: BuildConfig; let _vite: any; @@ -49,8 +71,11 @@ export default function createIntegration(args?: Options): AstroIntegration { 'astro:build:setup': ({ vite, target }) => { if (target === 'server') { _vite = vite; - vite.resolve = vite.resolve || {}; - vite.resolve.alias = vite.resolve.alias || {}; + vite.resolve = vite.resolve ?? {}; + 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' }]; @@ -61,10 +86,15 @@ export default function createIntegration(args?: Options): AstroIntegration { (vite.resolve.alias as Record)[alias.find] = alias.replacement; } } - vite.ssr = { 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 () => { @@ -80,6 +110,9 @@ export default function createIntegration(args?: Options): AstroIntegration { format: 'esm', bundle: true, external: ['@astrojs/markdown-remark'], + plugins: [ + denoImportsShimPlugin + ], banner: { js: SHIM, }, diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index e7a6c8747..8979a96d0 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -3,9 +3,7 @@ import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; // @ts-ignore -import { Server } from 'https://deno.land/std@0.167.0/http/server.ts'; -// @ts-ignore -import { fetch } from 'https://deno.land/x/file_fetch/mod.ts'; +import { Server, serveFile, fromFileUrl } from '@astrojs/deno/__deno_imports.js'; interface Options { port?: number; @@ -16,6 +14,17 @@ interface Options { let _server: Server | undefined = undefined; let _startPromise: Promise | undefined = undefined; +async function* getPrerenderedFiles(clientRoot: URL): AsyncGenerator { + // @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) { if (options.start === false) { return; @@ -40,7 +49,24 @@ export function start(manifest: SSRManifest, options: Options) { // try to fetch a static file instead const url = new URL(request.url); 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 (fileResp.status == 404) { diff --git a/packages/integrations/deno/test/basics.test.ts b/packages/integrations/deno/test/basics.test.ts index d1f8907cb..ea81042b2 100644 --- a/packages/integrations/deno/test/basics.test.ts +++ b/packages/integrations/deno/test/basics.test.ts @@ -68,7 +68,7 @@ Deno.test({ resp = await fetch(new URL(href!, baseUrl)); assertEquals(resp.status, 200); const ct = resp.headers.get('content-type'); - assertEquals(ct, 'text/css'); + assertEquals(ct, 'text/css; charset=UTF-8'); await resp.body!.cancel(); }); }, @@ -143,3 +143,24 @@ Deno.test({ sanitizeResources: 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, +}); diff --git a/packages/integrations/deno/test/fixtures/basics/src/pages/prerender.astro b/packages/integrations/deno/test/fixtures/basics/src/pages/prerender.astro new file mode 100644 index 000000000..d19d3e6f9 --- /dev/null +++ b/packages/integrations/deno/test/fixtures/basics/src/pages/prerender.astro @@ -0,0 +1,9 @@ +--- +export const prerender = true; +--- + + + +

test

+ +