From ccc5a58661916ca5640b9353a5a5ef7115c13785 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 10:43:49 -0600 Subject: [PATCH 1/7] fix(deno): handle prerendered pages --- .changeset/shaggy-moons-judge.md | 5 ++++ examples/deno/package.json | 2 +- examples/deno/src/pages/index.astro | 2 ++ packages/integrations/deno/package.json | 1 + .../integrations/deno/src/__deno_imports.ts | 9 +++++++ packages/integrations/deno/src/index.ts | 24 ++++++++++++++++++- packages/integrations/deno/src/server.ts | 13 ++++++---- 7 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .changeset/shaggy-moons-judge.md create mode 100644 packages/integrations/deno/src/__deno_imports.ts diff --git a/.changeset/shaggy-moons-judge.md b/.changeset/shaggy-moons-judge.md new file mode 100644 index 000000000000..f23e08334704 --- /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 8841cba7317e..9d9ac114be60 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 19e358c77ed3..0399a753441c 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 56804afec7c5..dc6b4ae46e43 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 000000000000..99c0b3a3eb0e --- /dev/null +++ b/packages/integrations/deno/src/__deno_imports.ts @@ -0,0 +1,9 @@ +// 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() {} diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts index 11f5adebfaca..1b2df573c6af 100644 --- a/packages/integrations/deno/src/index.ts +++ b/packages/integrations/deno/src/index.ts @@ -20,6 +20,13 @@ const SHIM = `globalThis.process = { env: Deno.env.toObject(), };`; +// 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@0.177.0/http/server.ts" +export { serveFile } from 'https://deno.land/std@0.177.0/http/file_server.ts';` + export function getAdapter(args?: Options): AstroAdapter { return { name: '@astrojs/deno', @@ -29,6 +36,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; @@ -61,10 +80,10 @@ export default function createIntegration(args?: Options): AstroIntegration { (vite.resolve.alias as Record)[alias.find] = alias.replacement; } } - vite.ssr = { noExternal: true, }; + vite.build.rollupOptions.external = [DENO_IMPORTS_SHIM] } }, 'astro:build:done': async () => { @@ -80,6 +99,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 e7a6c8747cd1..e463e706872d 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 } from '@astrojs/deno/__deno_imports.js'; interface Options { port?: number; @@ -40,7 +38,14 @@ 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, localPath.pathname); + + // Attempt to serve `index.html` if 404 + if (fileResp.status == 404) { + const fallback = new URL('./index.html', localPath).pathname; + fileResp = await serveFile(request, fallback); + } + // If the static file can't be found if (fileResp.status == 404) { From 03eb52fdb4a16f60c758407fd0c8eef60847b8b3 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 10:51:01 -0600 Subject: [PATCH 2/7] test(deno): add prerender test --- .../integrations/deno/test/basics.test.ts | 20 +++++++++++++++++++ .../fixtures/basics/src/pages/prerender.astro | 9 +++++++++ 2 files changed, 29 insertions(+) create mode 100644 packages/integrations/deno/test/fixtures/basics/src/pages/prerender.astro diff --git a/packages/integrations/deno/test/basics.test.ts b/packages/integrations/deno/test/basics.test.ts index d1f8907cb3c7..57ad8dc239fc 100644 --- a/packages/integrations/deno/test/basics.test.ts +++ b/packages/integrations/deno/test/basics.test.ts @@ -143,3 +143,23 @@ 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('perender', 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 000000000000..d19d3e6f9768 --- /dev/null +++ b/packages/integrations/deno/test/fixtures/basics/src/pages/prerender.astro @@ -0,0 +1,9 @@ +--- +export const prerender = true; +--- + + + +

test

+ + From d6d8df01cdd26f9734fab60d9fc8fc0b6db77947 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 10:55:02 -0600 Subject: [PATCH 3/7] fix: defensively access vite.build.rollupOptions.external --- packages/integrations/deno/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts index 1b2df573c6af..a616ccd27f20 100644 --- a/packages/integrations/deno/src/index.ts +++ b/packages/integrations/deno/src/index.ts @@ -68,8 +68,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' }]; @@ -83,7 +86,8 @@ export default function createIntegration(args?: Options): AstroIntegration { vite.ssr = { noExternal: true, }; - vite.build.rollupOptions.external = [DENO_IMPORTS_SHIM] + + vite.build.rollupOptions.external.push(DENO_IMPORTS_SHIM); } }, 'astro:build:done': async () => { From 6a0f41a860773340347cddbaf9e3701c1ea2583f Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 11:03:49 -0600 Subject: [PATCH 4/7] fix(deno): support other formats of rollupOptions.external --- packages/integrations/deno/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts index a616ccd27f20..5c6b22cece73 100644 --- a/packages/integrations/deno/src/index.ts +++ b/packages/integrations/deno/src/index.ts @@ -87,7 +87,11 @@ export default function createIntegration(args?: Options): AstroIntegration { noExternal: true, }; - vite.build.rollupOptions.external.push(DENO_IMPORTS_SHIM); + 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 () => { From 926ba36a4ad3a9eadf12d39de1e53f91e4a3a1e3 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 11:48:19 -0600 Subject: [PATCH 5/7] fix(deno): crawl prerendered files for match --- packages/integrations/deno/src/server.ts | 24 +++++++++++++++++-- .../integrations/deno/test/basics.test.ts | 5 ++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index e463e706872d..dd273770929a 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -14,6 +14,16 @@ interface Options { let _server: Server | undefined = undefined; let _startPromise: Promise | undefined = undefined; +async function* getPrerenderedFiles(clientRoot: URL): AsyncGenerator { + 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; @@ -38,12 +48,22 @@ 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); + let fileResp = await serveFile(request, localPath.pathname); // Attempt to serve `index.html` if 404 if (fileResp.status == 404) { - const fallback = new URL('./index.html', localPath).pathname; - fileResp = await serveFile(request, fallback); + 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, fallback.pathname); + } } diff --git a/packages/integrations/deno/test/basics.test.ts b/packages/integrations/deno/test/basics.test.ts index 57ad8dc239fc..ea81042b2411 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(); }); }, @@ -144,12 +144,13 @@ Deno.test({ sanitizeOps: false, }); + Deno.test({ name: 'perendering', permissions: defaultTestPermissions, async fn() { await startApp(async (baseUrl: URL) => { - const resp = await fetch(new URL('perender', baseUrl)); + const resp = await fetch(new URL('/prerender', baseUrl)); assertEquals(resp.status, 200); const html = await resp.text(); From 27d4a9ab8a4114f9e6c680dd3f89bf5c6a48c565 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 11:49:44 -0600 Subject: [PATCH 6/7] fix(deno): ignore deno error in server file --- packages/integrations/deno/src/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index dd273770929a..41a9d30912e6 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -15,6 +15,7 @@ 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)) From 65791ea0742aaad95e943aafc7cf3acd61456efe Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 11:58:18 -0600 Subject: [PATCH 7/7] fix(deno): cross-platform serve file --- packages/integrations/deno/src/__deno_imports.ts | 1 + packages/integrations/deno/src/index.ts | 7 +++++-- packages/integrations/deno/src/server.ts | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/integrations/deno/src/__deno_imports.ts b/packages/integrations/deno/src/__deno_imports.ts index 99c0b3a3eb0e..2775f1a5b68b 100644 --- a/packages/integrations/deno/src/__deno_imports.ts +++ b/packages/integrations/deno/src/__deno_imports.ts @@ -7,3 +7,4 @@ export class Server { } export function serveFile() {} +export function fromFileUrl() {} diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts index 5c6b22cece73..fcfda5dc3a34 100644 --- a/packages/integrations/deno/src/index.ts +++ b/packages/integrations/deno/src/index.ts @@ -20,12 +20,15 @@ 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@0.177.0/http/server.ts" -export { serveFile } from 'https://deno.land/std@0.177.0/http/file_server.ts';` +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 { diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index 41a9d30912e6..8979a96d05a2 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -3,7 +3,7 @@ import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; // @ts-ignore -import { Server, serveFile } from '@astrojs/deno/__deno_imports.js'; +import { Server, serveFile, fromFileUrl } from '@astrojs/deno/__deno_imports.js'; interface Options { port?: number; @@ -50,7 +50,7 @@ export function start(manifest: SSRManifest, options: Options) { const url = new URL(request.url); const localPath = new URL('./' + app.removeBase(url.pathname), clientRoot); - let fileResp = await serveFile(request, localPath.pathname); + let fileResp = await serveFile(request, fromFileUrl(localPath)); // Attempt to serve `index.html` if 404 if (fileResp.status == 404) { @@ -63,7 +63,7 @@ export function start(manifest: SSRManifest, options: Options) { } } if (fallback) { - fileResp = await serveFile(request, fallback.pathname); + fileResp = await serveFile(request, fromFileUrl(fallback)); } }