├── 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 |
--------------------------------------------------------------------------------