├── pnpm-workspace.yaml ├── test ├── fixtures │ ├── basics │ │ ├── src │ │ │ ├── pages │ │ │ │ ├── 404.astro │ │ │ │ ├── mdx.mdx │ │ │ │ ├── markdown.md │ │ │ │ ├── prerender.astro │ │ │ │ ├── login.astro │ │ │ │ ├── admin.astro │ │ │ │ ├── nodecompat.astro │ │ │ │ └── index.astro │ │ │ ├── components │ │ │ │ └── React.jsx │ │ │ └── util │ │ │ │ └── data.ts │ │ ├── astro.config.mjs │ │ └── package.json │ └── dynimport │ │ ├── src │ │ ├── components │ │ │ └── Thing.astro │ │ └── pages │ │ │ └── index.astro │ │ ├── package.json │ │ └── astro.config.mjs ├── dynamic-import.test.ts ├── helpers.ts └── basics.test.ts ├── .gitignore ├── src ├── __deno_imports.ts ├── types.ts ├── server.ts └── index.ts ├── tsconfig.json ├── tsconfig.base.json ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── package.json ├── README.md └── deno.lock /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | - test/fixtures/* 4 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/404.astro: -------------------------------------------------------------------------------- 1 |

Custom 404 Page

2 | -------------------------------------------------------------------------------- /test/fixtures/dynimport/src/components/Thing.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 |
testing
5 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/mdx.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Title 3 | description: Description 4 | --- 5 | 6 | # Heading from MDX 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | test/**/fixtures/**/env.d.ts 4 | test/**/fixtures/**/.astro/ 5 | test/**/fixtures/**/deno.lock 6 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Title 3 | description: Description 4 | --- 5 | 6 | # Heading from Markdown 7 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/components/React.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return
testing
; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/prerender.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export const prerender = true; 3 | --- 4 | 5 | 6 | 7 |

test

8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/login.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 5 | Testing 6 | 7 | 8 |

Testing

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/admin.astro: -------------------------------------------------------------------------------- 1 | --- 2 | Astro.cookies.set('logged-in', false, { 3 | maxAge: 60 * 60 * 24 * 900, 4 | path: '/' 5 | }); 6 | 7 | return Astro.redirect('/login'); 8 | --- 9 | -------------------------------------------------------------------------------- /test/fixtures/dynimport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@test/deno-astro-dynimport", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "astro": "^5", 7 | "@deno/astro-adapter": "workspace:*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/dynimport/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { default: Thing } = await import('../components/Thing.astro'); 3 | --- 4 | 5 | 6 | testing 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/dynimport/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import deno from "@deno/astro-adapter"; 3 | 4 | export default defineConfig({ 5 | adapter: deno(), 6 | output: "server", 7 | build: { 8 | client: "./client2", 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/__deno_imports.ts: -------------------------------------------------------------------------------- 1 | // This file is a shim for any Deno-specific imports! 2 | // It will be replaced in the final Deno build. 3 | // 4 | // This allows us to prerender pages in Node. 5 | export class Server { 6 | listenAndServe() {} 7 | } 8 | 9 | export function serveFile() {} 10 | export function fromFileUrl() {} 11 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | port?: number; 3 | hostname?: string; 4 | start?: boolean; 5 | } 6 | 7 | export interface InternalOptions extends Options { 8 | relativeClientPath: string; 9 | } 10 | 11 | export interface BuildConfig { 12 | server: URL; 13 | serverEntry: string; 14 | assets: string; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/basics/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import deno from "@deno/astro-adapter"; 3 | import react from "@astrojs/react"; 4 | import mdx from "@astrojs/mdx"; 5 | 6 | export default defineConfig({ 7 | adapter: deno(), 8 | integrations: [react(), mdx()], 9 | output: "server", 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "ES2022", 6 | "outDir": "./dist", 7 | // TODO: Due to the shim for Deno imports in `server.ts`, we can't use moduleResolution: 'node16' or the types get very weird. 8 | "moduleResolution": "Node" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/basics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@test/deno-astro-basic", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "astro": "^5", 7 | "@deno/astro-adapter": "workspace:*", 8 | "@astrojs/react": "^4", 9 | "@astrojs/mdx": "^4", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/util/data.ts: -------------------------------------------------------------------------------- 1 | export interface Data { 2 | foo: string; 3 | } 4 | 5 | export async function getData(): Promise { 6 | return new Promise((resolve, _reject) => { 7 | setTimeout(() => { 8 | resolve({ foo: "bar" }); 9 | }, 100); 10 | }); 11 | } 12 | 13 | // Testing top-level await, a feature supported in esnext 14 | export const someData = await getData(); 15 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "strict": true, 7 | "moduleResolution": "Node16", 8 | "target": "ES2022", 9 | "module": "Node16", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "verbatimModuleSyntax": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/nodecompat.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // unprefixed node built-in module 3 | import path from 'path' 4 | 5 | // prefixed node built-in module 6 | import os from 'node:os' 7 | --- 8 | 9 | Go to my file 10 |
11 | CPU Architecture 12 | {os.arch()} 13 |
14 |

Everything went fine.

15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/basics/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { someData } from '../util/data'; 3 | import ReactComponent from '../components/React.jsx'; 4 | const envValue = import.meta.env.SOME_VARIABLE; 5 | --- 6 | 7 | 8 | Basic App on Deno 9 | 10 | 11 | 12 |

Basic App on Deno

13 |

{envValue}

14 |

{someData.foo}

15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Clone repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Deno 2.x 18 | uses: denoland/setup-deno@v2 19 | with: 20 | deno-version: v2.x 21 | 22 | - name: Setup Node 18 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 18 26 | registry-url: https://registry.npmjs.org/ 27 | 28 | - name: Setup PNPM 29 | uses: pnpm/action-setup@v2 30 | with: 31 | version: 8 32 | 33 | - name: Check fmt 34 | run: deno fmt --check 35 | 36 | - name: Install dependencies 37 | run: pnpm i 38 | 39 | - name: Test 40 | run: pnpm test 41 | -------------------------------------------------------------------------------- /test/dynamic-import.test.ts: -------------------------------------------------------------------------------- 1 | /* Deno types consider DOM elements nullable */ 2 | /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ 3 | import { DOMParser } from "https://deno.land/x/deno_dom@v0.1.35-alpha/deno-dom-wasm.ts"; 4 | import { 5 | assert, 6 | assertEquals, 7 | } from "https://deno.land/std@0.158.0/testing/asserts.ts"; 8 | import { runBuildAndStartAppFromSubprocess } from "./helpers.ts"; 9 | 10 | Deno.test({ 11 | name: "Dynamic import", 12 | async fn(t) { 13 | const app = await runBuildAndStartAppFromSubprocess( 14 | "./fixtures/dynimport/", 15 | ); 16 | 17 | await t.step("Works", async () => { 18 | const resp = await fetch(app.url); 19 | assertEquals(resp.status, 200); 20 | const html = await resp.text(); 21 | assert(html); 22 | const doc = new DOMParser().parseFromString(html, `text/html`); 23 | const div = doc!.querySelector("#thing"); 24 | assert(div, "div exists"); 25 | }); 26 | 27 | await app.stop(); 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deno/astro-adapter", 3 | "description": "Deploy your Astro site to a Deno server", 4 | "version": "0.3.2", 5 | "type": "module", 6 | "author": "denoland", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/denoland/deno-astro-adapter.git" 11 | }, 12 | "keywords": [ 13 | "withastro", 14 | "astro-adapter" 15 | ], 16 | "bugs": "https://github.com/denoland/deno-astro-adapter/issues", 17 | "homepage": "https://github.com/denoland/deno-astro-adapter/", 18 | "exports": { 19 | ".": "./src/index.ts", 20 | "./server.ts": "./src/server.ts", 21 | "./__deno_imports.ts": "./src/__deno_imports.ts", 22 | "./package.json": "./package.json" 23 | }, 24 | "files": [ 25 | "src" 26 | ], 27 | "scripts": { 28 | "test": "deno test --allow-run --allow-env --allow-read --allow-net ./test/", 29 | "fmt": "deno fmt" 30 | }, 31 | "dependencies": {}, 32 | "peerDependencies": { 33 | "@opentelemetry/api": "^1", 34 | "astro": "^5.0.1" 35 | }, 36 | "peerDependenciesMeta": { 37 | "@opentelemetry/api": { 38 | "optional": true 39 | } 40 | }, 41 | "devDependencies": { 42 | "@opentelemetry/api": "^1", 43 | "@types/node": "^20.11.5", 44 | "astro": "^5.4.2", 45 | "typescript": "^5.3.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { fromFileUrl } from "jsr:@std/path@1.0"; 2 | import { assert } from "jsr:@std/assert@1.0"; 3 | 4 | const dir = new URL("./", import.meta.url); 5 | const defaultURL = new URL("http://localhost:8085/"); 6 | 7 | export const defaultTestPermissions: Deno.PermissionOptions = { 8 | read: true, 9 | net: true, 10 | run: true, 11 | env: true, 12 | }; 13 | 14 | declare type ExitCallback = () => Promise; 15 | 16 | export async function runBuild(fixturePath: string) { 17 | const command = new Deno.Command("node_modules/.bin/astro", { 18 | args: ["build", "--silent"], 19 | cwd: fromFileUrl(new URL(fixturePath, dir)), 20 | }); 21 | const process = command.spawn(); 22 | try { 23 | const status = await process.status; 24 | assert(status.success); 25 | } finally { 26 | safeKill(process); 27 | } 28 | } 29 | 30 | export async function startModFromImport(baseUrl: URL): Promise { 31 | const entryUrl = new URL("./dist/server/entry.mjs", baseUrl); 32 | const mod = await import(entryUrl.toString()); 33 | 34 | if (!mod.running()) { 35 | mod.start(); 36 | } 37 | 38 | return () => mod.stop(); 39 | } 40 | 41 | export async function startModFromSubprocess( 42 | baseUrl: URL, 43 | ): Promise { 44 | const entryUrl = new URL("./dist/server/entry.mjs", baseUrl); 45 | const command = new Deno.Command("deno", { 46 | args: ["run", "--allow-env", "--allow-net", fromFileUrl(entryUrl)], 47 | cwd: fromFileUrl(baseUrl), 48 | stderr: "piped", 49 | }); 50 | const process = command.spawn(); 51 | await waitForServer(process); 52 | return async () => { 53 | safeKill(process); 54 | await process.status; 55 | }; 56 | } 57 | 58 | async function waitForServer(process: Deno.ChildProcess) { 59 | const reader = process.stderr.getReader(); 60 | const dec = new TextDecoder(); 61 | 62 | while (true) { 63 | const { value } = await reader.read(); 64 | if (!value) { 65 | throw new Error("Server did not start"); 66 | } 67 | const msg = dec.decode(value); 68 | if (msg.includes("Server running")) { 69 | break; 70 | } 71 | } 72 | reader.cancel(); 73 | } 74 | 75 | function safeKill(process: Deno.ChildProcess) { 76 | try { 77 | process.kill("SIGKILL"); 78 | } catch { 79 | // ignore 80 | } 81 | } 82 | 83 | export async function runBuildAndStartApp(fixturePath: string) { 84 | const url = new URL(fixturePath, dir); 85 | 86 | await runBuild(fixturePath); 87 | const stop = await startModFromImport(url); 88 | 89 | return { url: defaultURL, stop }; 90 | } 91 | 92 | export async function runBuildAndStartAppFromSubprocess(fixturePath: string) { 93 | const url = new URL(fixturePath, dir); 94 | 95 | await runBuild(fixturePath); 96 | const stop = await startModFromSubprocess(url); 97 | 98 | return { url: defaultURL, stop }; 99 | } 100 | -------------------------------------------------------------------------------- /test/basics.test.ts: -------------------------------------------------------------------------------- 1 | /* Deno types consider DOM elements nullable */ 2 | /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ 3 | import { DOMParser } from "https://deno.land/x/deno_dom@v0.1.35-alpha/deno-dom-wasm.ts"; 4 | import { 5 | assert, 6 | assertEquals, 7 | } from "https://deno.land/std@0.158.0/testing/asserts.ts"; 8 | import { defaultTestPermissions, runBuildAndStartApp } from "./helpers.ts"; 9 | 10 | // this needs to be here and not in the specific test case, because 11 | // the variables are loaded in the global scope of the built server 12 | // module, which is only executed once upon the first load 13 | const varContent = "this is a value stored in env variable"; 14 | Deno.env.set("SOME_VARIABLE", varContent); 15 | 16 | Deno.test({ 17 | name: "Basics", 18 | permissions: defaultTestPermissions, 19 | sanitizeResources: false, 20 | sanitizeOps: false, 21 | async fn(t) { 22 | const app = await runBuildAndStartApp("./fixtures/basics/"); 23 | 24 | await t.step("Works", async () => { 25 | const resp = await fetch(app.url); 26 | assertEquals(resp.status, 200); 27 | 28 | const html = await resp.text(); 29 | assert(html); 30 | 31 | const doc = new DOMParser().parseFromString(html, `text/html`); 32 | const div = doc!.querySelector("#react"); 33 | 34 | assert(div, "div exists"); 35 | }); 36 | 37 | await t.step("Custom 404", async () => { 38 | const resp = await fetch(new URL("this-does-not-exist", app.url)); 39 | assertEquals(resp.status, 404); 40 | 41 | const html = await resp.text(); 42 | assert(html); 43 | 44 | const doc = new DOMParser().parseFromString(html, `text/html`); 45 | const header = doc!.querySelector("#custom-404"); 46 | assert(header, "displays custom 404"); 47 | }); 48 | 49 | await t.step("Loads style assets", async () => { 50 | let resp = await fetch(app.url); 51 | const html = await resp.text(); 52 | 53 | const doc = new DOMParser().parseFromString(html, `text/html`); 54 | const style = doc!.querySelector("style"); 55 | 56 | assert(style?.textContent?.includes("Courier New")); 57 | }); 58 | 59 | await t.step("Correctly loads run-time env variables", async () => { 60 | const resp = await fetch(app.url); 61 | const html = await resp.text(); 62 | 63 | const doc = new DOMParser().parseFromString(html, `text/html`); 64 | const p = doc!.querySelector("p#env-value"); 65 | assertEquals(p!.innerText, varContent); 66 | }); 67 | 68 | await t.step("Can use a module with top-level await", async () => { 69 | const resp = await fetch(app.url); 70 | const html = await resp.text(); 71 | 72 | const doc = new DOMParser().parseFromString(html, `text/html`); 73 | const p = doc!.querySelector("p#module-value"); 74 | assertEquals(p!.innerText, "bar"); 75 | }); 76 | 77 | await t.step("Works with Markdown", async () => { 78 | const resp = await fetch(new URL("markdown", app.url)); 79 | const html = await resp.text(); 80 | 81 | const doc = new DOMParser().parseFromString(html, `text/html`); 82 | const h1 = doc!.querySelector("h1"); 83 | assertEquals(h1!.innerText, "Heading from Markdown"); 84 | }); 85 | 86 | await t.step("Works with MDX", async () => { 87 | const resp = await fetch(new URL("mdx", app.url)); 88 | const html = await resp.text(); 89 | 90 | const doc = new DOMParser().parseFromString(html, `text/html`); 91 | const h1 = doc!.querySelector("h1"); 92 | assertEquals(h1!.innerText, "Heading from MDX"); 93 | }); 94 | 95 | await t.step("Astro.cookies", async () => { 96 | const url = new URL("/admin", app.url); 97 | const resp = await fetch(url, { redirect: "manual" }); 98 | assertEquals(resp.status, 302); 99 | 100 | const headers = resp.headers; 101 | assertEquals( 102 | headers.get("set-cookie"), 103 | "logged-in=false; Max-Age=77760000; Path=/", 104 | ); 105 | }); 106 | 107 | await t.step("perendering", async () => { 108 | const resp = await fetch(new URL("/prerender", app.url)); 109 | assertEquals(resp.status, 200); 110 | 111 | const html = await resp.text(); 112 | assert(html); 113 | 114 | const doc = new DOMParser().parseFromString(html, `text/html`); 115 | const h1 = doc!.querySelector("h1"); 116 | assertEquals(h1!.innerText, "test"); 117 | }); 118 | 119 | await t.step("node compatibility", async () => { 120 | const resp = await fetch(new URL("/nodecompat", app.url)); 121 | assertEquals(resp.status, 200); 122 | await resp.text(); 123 | }); 124 | 125 | app.stop(); 126 | }, 127 | }); 128 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | // Normal Imports 2 | import type { SSRManifest } from "astro"; 3 | import { App } from "astro/app"; 4 | import { setGetEnv } from "astro/env/setup"; 5 | import type { InternalOptions } from "./types"; 6 | setGetEnv((key) => Deno.env.get(key)); 7 | 8 | // @ts-expect-error 9 | import { fromFileUrl, serveFile } from "@deno/astro-adapter/__deno_imports.ts"; 10 | 11 | let _server: Deno.Server | undefined = undefined; 12 | let _startPromise: Promise | undefined = undefined; 13 | 14 | async function* getPrerenderedFiles(clientRoot: URL): AsyncGenerator { 15 | // @ts-expect-error 16 | for await (const ent of Deno.readDir(clientRoot)) { 17 | if (ent.isDirectory) { 18 | yield* getPrerenderedFiles(new URL(`./${ent.name}/`, clientRoot)); 19 | } else if (ent.name.endsWith(".html")) { 20 | yield new URL(`./${ent.name}`, clientRoot); 21 | } 22 | } 23 | } 24 | 25 | function removeTrailingForwardSlash(path: string) { 26 | return path.endsWith("/") ? path.slice(0, path.length - 1) : path; 27 | } 28 | 29 | export function start(manifest: SSRManifest, options: InternalOptions) { 30 | if (options.start === false) { 31 | return; 32 | } 33 | 34 | // undefined = not yet loaded, null = not installed 35 | let trace: import("@opentelemetry/api").TraceAPI | null | undefined; 36 | 37 | const clientRoot = new URL(options.relativeClientPath, import.meta.url); 38 | const app = new App(manifest); 39 | const handler = async (request: Request, handlerInfo: any) => { 40 | if (trace === undefined) { 41 | try { 42 | trace = (await import("@opentelemetry/api")).trace; 43 | } catch { 44 | trace = null; 45 | // @open-telemetry/api is not installed 46 | } 47 | } 48 | const routeData = app.match(request); 49 | if (routeData) { 50 | const span = trace?.getActiveSpan(); 51 | span?.updateName(`${request.method} ${routeData.route}`); 52 | span?.setAttribute("http.route", routeData.route); 53 | span?.setAttribute("astro.prerendered", routeData.prerender); 54 | span?.setAttribute("astro.type", routeData.type); 55 | const hostname = handlerInfo.remoteAddr?.hostname; 56 | Reflect.set(request, Symbol.for("astro.clientAddress"), hostname); 57 | const response = await app.render(request, { routeData }); 58 | if (app.setCookieHeaders) { 59 | for (const setCookieHeader of app.setCookieHeaders(response)) { 60 | response.headers.append("Set-Cookie", setCookieHeader); 61 | } 62 | } 63 | return response; 64 | } 65 | 66 | // If the request path wasn't found in astro, 67 | // try to fetch a static file instead 68 | const url = new URL(request.url); 69 | const localPath = new URL("./" + app.removeBase(url.pathname), clientRoot); 70 | 71 | let fileResp = await serveFile(request, fromFileUrl(localPath)); 72 | 73 | // Attempt to serve `index.html` if 404 74 | if (fileResp.status == 404) { 75 | let fallback; 76 | for await (const file of getPrerenderedFiles(clientRoot)) { 77 | const pathname = file.pathname.replace(/\/(index)?\.html$/, ""); 78 | if (removeTrailingForwardSlash(localPath.pathname).endsWith(pathname)) { 79 | fallback = file; 80 | break; 81 | } 82 | } 83 | if (fallback) { 84 | fileResp = await serveFile(request, fromFileUrl(fallback)); 85 | } 86 | } 87 | 88 | // If the static file can't be found 89 | if (fileResp.status == 404) { 90 | // Render the astro custom 404 page 91 | const response = await app.render(request); 92 | 93 | if (app.setCookieHeaders) { 94 | for (const setCookieHeader of app.setCookieHeaders(response)) { 95 | response.headers.append("Set-Cookie", setCookieHeader); 96 | } 97 | } 98 | return response; 99 | 100 | // If the static file is found 101 | } else { 102 | return fileResp; 103 | } 104 | }; 105 | 106 | const port = options.port ?? 8085; 107 | const hostname = options.hostname ?? "0.0.0.0"; 108 | _server = Deno.serve({ port, hostname }, handler); 109 | _startPromise = _server.finished; 110 | console.error(`Server running on port ${port}`); 111 | } 112 | 113 | export function createExports(manifest: SSRManifest, options: InternalOptions) { 114 | const app = new App(manifest); 115 | return { 116 | async stop() { 117 | if (_server) { 118 | _server.shutdown(); 119 | _server = undefined; 120 | } 121 | await Promise.resolve(_startPromise); 122 | }, 123 | running() { 124 | return _server !== undefined; 125 | }, 126 | async start() { 127 | return start(manifest, options); 128 | }, 129 | async handle(request: Request) { 130 | return app.render(request); 131 | }, 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @deno/astro-adapter 2 | 3 | This adapter allows Astro to deploy your SSR site to Deno targets. 4 | 5 | Learn how to deploy your Astro site in our 6 | [Deno Deploy deployment guide](https://docs.astro.build/en/guides/deploy/deno/). 7 | 8 | - [Why Astro Deno](#why-astro-deno) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [Configuration](#configuration) 12 | - [Examples](#examples) 13 | - [Contributing](#contributing) 14 | 15 | ## Why Astro Deno 16 | 17 | If you're using Astro as a static site builder—its behavior out of the box—you 18 | don't need an adapter. 19 | 20 | If you wish to 21 | [use server-side rendering (SSR)](https://docs.astro.build/en/guides/server-side-rendering/), 22 | Astro requires an adapter that matches your deployment runtime. 23 | 24 | You also need an adapter or server if you wish to deploy your site to 25 | [Deno Deploy](https://deno.com/deploy). 26 | 27 | [Deno](https://deno.com/) is a runtime similar to Node, but with an API that's 28 | more similar to the browser's API. This adapter provides access to Deno's API 29 | and creates a script to run your project on a Deno server. 30 | 31 | ## Installation 32 | 33 | Add the Deno adapter to enable SSR in your Astro project with the following 34 | steps: 35 | 36 | 1. Install the Deno adapter to your project’s dependencies using your preferred 37 | package manager. If you’re using npm or aren’t sure, run this in the 38 | terminal: 39 | 40 | ```bash 41 | npm install @deno/astro-adapter 42 | ``` 43 | 44 | 1. Update your `astro.config.mjs` project configuration file with the changes 45 | below. 46 | 47 | ```js ins={3,6-7} 48 | // astro.config.mjs 49 | import { defineConfig } from "astro/config"; 50 | import deno from "@deno/astro-adapter"; 51 | 52 | export default defineConfig({ 53 | output: "server", 54 | adapter: deno(), 55 | }); 56 | ``` 57 | 58 | Next, update your `preview` script in `package.json` to run `deno`: 59 | 60 | ```json ins={8} 61 | // package.json 62 | { 63 | // ... 64 | "scripts": { 65 | "dev": "astro dev", 66 | "start": "astro dev", 67 | "build": "astro build", 68 | "preview": "deno run --allow-net --allow-read --allow-env ./dist/server/entry.mjs" 69 | } 70 | } 71 | ``` 72 | 73 | You can now use this command to preview your production Astro site locally with 74 | Deno. 75 | 76 | ```bash 77 | npm run preview 78 | ``` 79 | 80 | ## Usage 81 | 82 | After 83 | [performing a build](https://docs.astro.build/en/guides/deploy/#building-your-site-locally) 84 | there will be a `dist/server/entry.mjs` module. You can start a server by 85 | importing this module in your Deno app: 86 | 87 | ```js 88 | import "./dist/server/entry.mjs"; 89 | ``` 90 | 91 | See the `start` option below for how you can have more control over starting the 92 | Astro server. 93 | 94 | You can also run the script directly using deno: 95 | 96 | ```sh 97 | deno run --allow-net --allow-read --allow-env ./dist/server/entry.mjs 98 | ``` 99 | 100 | ## Configuration 101 | 102 | To configure this adapter, pass an object to the `deno()` function call in 103 | `astro.config.mjs`. 104 | 105 | ```js 106 | // astro.config.mjs 107 | import { defineConfig } from "astro/config"; 108 | import deno from "@deno/astro-adapter"; 109 | 110 | export default defineConfig({ 111 | output: "server", 112 | adapter: deno({ 113 | //options go here 114 | }), 115 | }); 116 | ``` 117 | 118 | ### start 119 | 120 | This adapter automatically starts a server when it is imported. You can turn 121 | this off with the `start` option: 122 | 123 | ```js 124 | import { defineConfig } from "astro/config"; 125 | import deno from "@deno/astro-adapter"; 126 | 127 | export default defineConfig({ 128 | output: "server", 129 | adapter: deno({ 130 | start: false, 131 | }), 132 | }); 133 | ``` 134 | 135 | If you disable this, you need to write your own Deno web server. Import and call 136 | `handle` from the generated entry script to render requests: 137 | 138 | ```ts 139 | import { handle } from "./dist/server/entry.mjs"; 140 | 141 | Deno.serve((req: Request) => { 142 | // Check the request, maybe do static file handling here. 143 | 144 | return handle(req); 145 | }); 146 | ``` 147 | 148 | ### port and hostname 149 | 150 | You can set the port (default: `8085`) and hostname (default: `0.0.0.0`) for the 151 | deno server to use. If `start` is false, this has no effect; your own server 152 | must configure the port and hostname. 153 | 154 | ```js 155 | import { defineConfig } from "astro/config"; 156 | import deno from "@deno/astro-adapter"; 157 | 158 | export default defineConfig({ 159 | output: "server", 160 | adapter: deno({ 161 | port: 8081, 162 | hostname: "myhost", 163 | }), 164 | }); 165 | ``` 166 | 167 | ## Examples 168 | 169 | The [Deno + Astro Template](https://github.com/denoland/deno-astro-template) 170 | includes a `preview` command that runs the entry script directly. Run 171 | `npm run build` then `npm run preview` to run the production deno server. 172 | 173 | ## Contributing 174 | 175 | To configure your development environment, clone the repository and install 176 | [`pnpm`](https://pnpm.io/). `pnpm` is a package manager that emphasizes disk 177 | space efficiency and is used for managing the dependencies of this project. Once 178 | installed, run `pnpm i` to install the dependencies. 179 | 180 | ```sh 181 | git clone 182 | cd astro-adapter 183 | pnpm i 184 | ``` 185 | 186 | The Deno Astro Adapter is currently built and tested with Deno 2.x. To test your 187 | changes make sure you have Deno 2.x installed 188 | 189 | ```sh 190 | pnpm run test 191 | ``` 192 | 193 | Finally, you can check your code formatting with: `pnpm run fmt`. 194 | 195 | This package is maintained by Deno's Core team. You're welcome to submit an 196 | issue or PR! 197 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { AstroAdapter, AstroConfig, AstroIntegration } from "astro"; 2 | import * as fs from "node:fs"; 3 | import { fileURLToPath } from "node:url"; 4 | import type { BuildConfig, Options } from "./types"; 5 | import { join, relative } from "node:path"; 6 | 7 | const STD_VERSION = `1.0`; 8 | // REF: https://github.com/denoland/deno/tree/main/ext/node/polyfills 9 | const COMPATIBLE_NODE_MODULES = [ 10 | "assert", 11 | "assert/strict", 12 | "async_hooks", 13 | "buffer", 14 | "child_process", 15 | "cluster", 16 | "console", 17 | "constants", 18 | "crypto", 19 | "dgram", 20 | "diagnostics_channel", 21 | "dns", 22 | "events", 23 | "fs", 24 | "fs/promises", 25 | "http", 26 | "http2", 27 | "https", 28 | "inspector", 29 | "module", 30 | "net", 31 | "os", 32 | "path", 33 | "path/posix", 34 | "path/win32", 35 | "perf_hooks", 36 | "process", 37 | "punycode", 38 | "querystring", 39 | "readline", 40 | "repl", 41 | "stream", 42 | "stream/promises", 43 | "stream/web", 44 | "string_decoder", 45 | "sys", 46 | "timers", 47 | "timers/promises", 48 | "tls", 49 | "trace_events", 50 | "tty", 51 | "url", 52 | "util", 53 | "util/types", 54 | "v8", 55 | "vm", 56 | "wasi", 57 | // 'webcrypto', 58 | "worker_threads", 59 | "zlib", 60 | ]; 61 | 62 | // We shim deno-specific imports so we can run the code in Node 63 | // to prerender pages. In the final Deno build, this import is 64 | // replaced with the Deno-specific contents listed below. 65 | const DENO_SHIM_PATH = `@deno/astro-adapter/__deno_imports.ts`; 66 | const DENO_IMPORTS = 67 | `import { serveFile } from "jsr:@std/http@${STD_VERSION}/file-server"; 68 | import { fromFileUrl } from "jsr:@std/path@${STD_VERSION}";`; 69 | const DENO_SHIM_BASE = `import { serveFile, fromFileUrl } from`; 70 | const DENO_IMPORTS_SHIM = `${DENO_SHIM_BASE} "${DENO_SHIM_PATH}";`; 71 | const DENO_IMPORTS_SHIM_LEGACY = `${DENO_SHIM_BASE} '${DENO_SHIM_PATH}';`; 72 | 73 | export function getAdapter( 74 | args: Options | undefined, 75 | config: AstroConfig, 76 | ): AstroAdapter { 77 | const clientPath = join(fileURLToPath(config.build.client)); 78 | const serverPath = join( 79 | fileURLToPath(config.build.server), 80 | config.build.serverEntry, 81 | ); 82 | const relativeClientPath = relative(serverPath, clientPath) + "/"; 83 | const realArgs = { ...args, relativeClientPath }; 84 | return { 85 | name: "@deno/astro-adapter", 86 | serverEntrypoint: "@deno/astro-adapter/server.ts", 87 | args: realArgs, 88 | exports: ["stop", "handle", "start", "running"], 89 | supportedAstroFeatures: { 90 | hybridOutput: "stable", 91 | staticOutput: "stable", 92 | serverOutput: "stable", 93 | sharpImageService: "stable", 94 | }, 95 | adapterFeatures: { 96 | envGetSecret: "stable", 97 | }, 98 | }; 99 | } 100 | 101 | export default function createIntegration(args?: Options): AstroIntegration { 102 | let _buildConfig: BuildConfig; 103 | return { 104 | name: "@deno/astro-adapter", 105 | hooks: { 106 | "astro:config:done": ({ setAdapter, config }) => { 107 | setAdapter(getAdapter(args, config)); 108 | _buildConfig = config.build; 109 | }, 110 | "astro:build:setup": ({ vite, target }) => { 111 | if (target === "server") { 112 | vite.resolve = vite.resolve ?? {}; 113 | vite.resolve.alias = vite.resolve.alias ?? {}; 114 | vite.build = vite.build ?? {}; 115 | vite.build.rollupOptions = vite.build.rollupOptions ?? {}; 116 | vite.build.rollupOptions.external = 117 | vite.build.rollupOptions.external ?? []; 118 | 119 | const aliases = [ 120 | { 121 | find: "react-dom/server", 122 | replacement: "react-dom/server.browser", 123 | }, 124 | ...COMPATIBLE_NODE_MODULES.map((mod) => ({ 125 | find: `${mod}`, 126 | replacement: `node:${mod}`, 127 | })), 128 | ]; 129 | 130 | if (Array.isArray(vite.resolve.alias)) { 131 | vite.resolve.alias = [...vite.resolve.alias, ...aliases]; 132 | } else { 133 | for (const alias of aliases) { 134 | (vite.resolve.alias as Record)[alias.find] = 135 | alias.replacement; 136 | } 137 | } 138 | 139 | if (Array.isArray(vite.build.rollupOptions.external)) { 140 | vite.build.rollupOptions.external.push(DENO_SHIM_PATH); 141 | } else if (typeof vite.build.rollupOptions.external !== "function") { 142 | vite.build.rollupOptions.external = [ 143 | vite.build.rollupOptions.external, 144 | DENO_SHIM_PATH, 145 | ]; 146 | } 147 | } 148 | }, 149 | "astro:build:done": async () => { 150 | // Replace `import { serveFile, fromFileUrl } from '@deno/astro-adapter/__deno_imports.ts';` in one of the chunks/ files with the actual imports. 151 | const chunksDirUrl = new URL("./chunks/", _buildConfig.server); 152 | for (const file of fs.readdirSync(chunksDirUrl)) { 153 | if (!file.endsWith(".mjs")) continue; 154 | const pth = fileURLToPath(new URL(file, chunksDirUrl)); 155 | const contents = fs.readFileSync(pth, "utf-8"); 156 | if ( 157 | !contents.includes(DENO_IMPORTS_SHIM_LEGACY) && 158 | !contents.includes(DENO_IMPORTS_SHIM) 159 | ) continue; 160 | fs.writeFileSync( 161 | pth, 162 | contents.replace( 163 | DENO_IMPORTS_SHIM_LEGACY, 164 | DENO_IMPORTS, 165 | ).replace( 166 | DENO_IMPORTS_SHIM, 167 | DENO_IMPORTS, 168 | ), 169 | ); 170 | } 171 | }, 172 | }, 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@std/assert@1.0": "1.0.6", 5 | "jsr:@std/cli@^1.0.6": "1.0.6", 6 | "jsr:@std/encoding@^1.0.5": "1.0.5", 7 | "jsr:@std/fmt@^1.0.2": "1.0.2", 8 | "jsr:@std/http@1.0": "1.0.8", 9 | "jsr:@std/internal@^1.0.4": "1.0.4", 10 | "jsr:@std/media-types@^1.0.3": "1.0.3", 11 | "jsr:@std/net@^1.0.4": "1.0.4", 12 | "jsr:@std/path@1.0": "1.0.6", 13 | "jsr:@std/path@^1.0.6": "1.0.6", 14 | "jsr:@std/streams@^1.0.7": "1.0.7", 15 | "npm:@types/node@^20.11.5": "20.14.5", 16 | "npm:typescript@^5.3.3": "5.4.5" 17 | }, 18 | "jsr": { 19 | "@std/assert@1.0.6": { 20 | "integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207", 21 | "dependencies": [ 22 | "jsr:@std/internal" 23 | ] 24 | }, 25 | "@std/cli@1.0.6": { 26 | "integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d" 27 | }, 28 | "@std/encoding@1.0.5": { 29 | "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" 30 | }, 31 | "@std/fmt@1.0.2": { 32 | "integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7" 33 | }, 34 | "@std/http@1.0.8": { 35 | "integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd", 36 | "dependencies": [ 37 | "jsr:@std/cli", 38 | "jsr:@std/encoding", 39 | "jsr:@std/fmt", 40 | "jsr:@std/media-types", 41 | "jsr:@std/net", 42 | "jsr:@std/path@^1.0.6", 43 | "jsr:@std/streams" 44 | ] 45 | }, 46 | "@std/internal@1.0.4": { 47 | "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" 48 | }, 49 | "@std/media-types@1.0.3": { 50 | "integrity": "b12d30a7852f7578f4d210622df713bbfd1cbdd9b4ec2eaf5c1845ab70bab159" 51 | }, 52 | "@std/net@1.0.4": { 53 | "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" 54 | }, 55 | "@std/path@1.0.6": { 56 | "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" 57 | }, 58 | "@std/streams@1.0.7": { 59 | "integrity": "1a93917ca0c58c01b2bfb93647189229b1702677f169b6fb61ad6241cd2e499b" 60 | } 61 | }, 62 | "npm": { 63 | "@types/node@20.14.5": { 64 | "integrity": "sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==", 65 | "dependencies": [ 66 | "undici-types" 67 | ] 68 | }, 69 | "typescript@5.4.5": { 70 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==" 71 | }, 72 | "undici-types@5.26.5": { 73 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 74 | } 75 | }, 76 | "remote": { 77 | "https://deno.land/std@0.158.0/fmt/colors.ts": "ff7dc9c9f33a72bd48bc24b21bbc1b4545d8494a431f17894dbc5fe92a938fc4", 78 | "https://deno.land/std@0.158.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c", 79 | "https://deno.land/std@0.158.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", 80 | "https://deno.land/std@0.158.0/testing/asserts.ts": "8696c488bc98d8d175e74dc652a0ffbc7fca93858da01edc57ed33c1148345da", 81 | "https://deno.land/x/deno_dom@v0.1.35-alpha/build/deno-wasm/deno-wasm.js": "3fa41dba4813e6d4b024a53a146b76e1afcbdf218fc02063442378c61239ed14", 82 | "https://deno.land/x/deno_dom@v0.1.35-alpha/deno-dom-wasm.ts": "bfd999a493a6974e9fca4d331bee03bfb68cfc600c662cd0b48b21d67a2a8ba0", 83 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/api.ts": "0ff5790f0a3eeecb4e00b7d8fbfa319b165962cf6d0182a65ba90f158d74f7d7", 84 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/constructor-lock.ts": "59714df7e0571ec7bd338903b1f396202771a6d4d7f55a452936bd0de9deb186", 85 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/deserialize.ts": "f4d34514ca00473ca428b69ad437ba345925744b5d791cb9552e2d7a0e7b0439", 86 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/document-fragment.ts": "a40c6e18dd0efcf749a31552c1c9a6f7fa614452245e86ee38fc92ba0235e5ae", 87 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/document.ts": "bcb96378097106d82e0d1a356496baea1b73f92dd7d492e6ed655016025665df", 88 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/dom-parser.ts": "609097b426f8c2358f3e5d2bca55ed026cf26cdf86562e94130dfdb0f2537f92", 89 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/element.ts": "312ae401081e6ce11cf62a854c0f78388e4be46579c1fdd9c1d118bc9c79db38", 90 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/elements/html-template-element.ts": "19ad97c55222115e8daaca2788b9c98cc31a7f9d2547ed5bca0c56a4a12bfec8", 91 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/html-collection.ts": "ae90197f5270c32074926ad6cf30ee07d274d44596c7e413c354880cebce8565", 92 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/node-list.ts": "4c6e4b4585301d4147addaccd90cb5f5a80e8d6290a1ba7058c5e3dfea16e15d", 93 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/node.ts": "3069e6fc93ac4111a136ed68199d76673339842b9751610ba06f111ba7dc10a7", 94 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/selectors/custom-api.ts": "852696bd58e534bc41bd3be9e2250b60b67cd95fd28ed16b1deff1d548531a71", 95 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/selectors/nwsapi-types.ts": "c43b36c36acc5d32caabaa54fda8c9d239b2b0fcbce9a28efb93c84aa1021698", 96 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/selectors/nwsapi.js": "985d7d8fc1eabbb88946b47a1c44c1b2d4aa79ff23c21424219f1528fa27a2ff", 97 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/selectors/selectors.ts": "83eab57be2290fb48e3130533448c93c6c61239f2a2f3b85f1917f80ca0fdc75", 98 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/selectors/sizzle-types.ts": "78149e2502409989ce861ed636b813b059e16bc267bb543e7c2b26ef43e4798b", 99 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/selectors/sizzle.js": "c3aed60c1045a106d8e546ac2f85cc82e65f62d9af2f8f515210b9212286682a", 100 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/utils-types.ts": "96db30e3e4a75b194201bb9fa30988215da7f91b380fca6a5143e51ece2a8436", 101 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/dom/utils.ts": "ecd889ba74f3ce282620d8ca1d4d5e0365e6cc86101d2352f3bbf936ae496e2c", 102 | "https://deno.land/x/deno_dom@v0.1.35-alpha/src/parser.ts": "b65eb7e673fa7ca611de871de109655f0aa9fa35ddc1de73df1a5fc2baafc332" 103 | }, 104 | "workspace": { 105 | "packageJson": { 106 | "dependencies": [ 107 | "npm:@opentelemetry/api@1", 108 | "npm:@types/node@^20.11.5", 109 | "npm:astro@^5.4.2", 110 | "npm:typescript@^5.3.3" 111 | ] 112 | } 113 | } 114 | } 115 | --------------------------------------------------------------------------------