Skip to content

Commit

Permalink
feat: add experimental deno-server preset (#592)
Browse files Browse the repository at this point in the history
Co-authored-by: Pooya Parsa <pooya@pi0.io>
  • Loading branch information
danielroe and pi0 authored Jun 20, 2023
1 parent 461b8a4 commit 58cde15
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 20 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ jobs:
if: ${{ matrix.os != 'windows-latest' }}
with:
bun-version: latest
- uses: denoland/setup-deno@v1
if: ${{ matrix.os != 'windows-latest' }}
with:
deno-version: v1.x
- run: pnpm install
- run: pnpm test:types
- run: pnpm build
Expand Down
28 changes: 23 additions & 5 deletions docs/content/2.deploy/providers/deno.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Deploy Nitro apps to [Deno Deploy](https://deno.com/deploy).

## Deno Deploy

**Preset:** `deno` ([switch to this preset](/deploy/#changing-the-deployment-preset))
**Preset:** `deno-deploy` ([switch to this preset](/deploy/#changing-the-deployment-preset))

::alert{type="warning"}
Deno support is experimental.
Deno deploy preset is experimental.
::

### Deploy with the CLI
Expand All @@ -17,8 +17,8 @@ You can use [deployctl](https://deno.com/deploy/docs/deployctl) to deploy your a
Login to [Deno Deploy](https://dash.deno.com/account#access-tokens) to obtain a `DENO_DEPLOY_TOKEN` access token, and set it as an environment variable.

```bash
# Build with the deno NITRO preset
NITRO_PRESET=deno npm run build
# Build with the deno-deploy NITRO preset
NITRO_PRESET=deno-deploy npm run build

# Make sure to run the deployctl command from the output directory
cd .output
Expand Down Expand Up @@ -56,11 +56,29 @@ jobs:
- run: pnpm install
- run: pnpm build
env:
NITRO_PRESET: deno
NITRO_PRESET: deno-deploy
- name: Deploy to Deno Deploy
uses: denoland/deployctl@v1
with:
project: my-project
entrypoint: server/index.ts
root: .output
```
## Deno Server (Runtime)
**Preset:** `deno-server` ([switch to this preset](/deploy/#changing-the-deployment-preset))

::alert{type="warning"}
Deno runtime preset is experimental.
::

You can build your Nitro server using Node.js to run within [Deno Runtime](https://deno.com/runtime) in a custom server.

```bash
# Build with the deno NITRO preset
NITRO_PRESET=deno-server npm run build
# Start production server
deno run --unstable --allow-net --allow-read --allow-env .output/server/index.ts
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"klona": "^2.0.6",
"knitwork": "^1.0.0",
"listhen": "^1.0.4",
"magic-string": "^0.30.0",
"mime": "^3.0.0",
"mlly": "^1.3.0",
"mri": "^1.2.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions src/presets/deno.ts → src/presets/deno-deploy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineNitroPreset } from "../preset";

export const deno = defineNitroPreset({
entry: "#internal/nitro/entries/deno",
export const denoDeploy = defineNitroPreset({
entry: "#internal/nitro/entries/deno-deploy",
node: false,
noExternals: true,
serveStatic: "deno",
Expand All @@ -23,3 +23,5 @@ export const deno = defineNitroPreset({
},
},
});

export const deno = denoDeploy;
129 changes: 129 additions & 0 deletions src/presets/deno-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { builtinModules } from "node:module";
import { isAbsolute, resolve } from "pathe";
import MagicString from "magic-string";
import { findStaticImports } from "mlly";
import inject from "@rollup/plugin-inject";
import { defineNitroPreset } from "../preset";
import { writeFile } from "../utils";
import { ImportMetaRe } from "../rollup/plugins/import-meta";

export const denoServer = defineNitroPreset({
extends: "node-server",
entry: "#internal/nitro/entries/deno-server",
commands: {
preview: "deno task --config ./deno.json start",
},
rollupConfig: {
output: {
hoistTransitiveImports: false,
},
plugins: [
inject({
modules: {
process: "process",
global: "global",
Buffer: ["buffer", "Buffer"],
setTimeout: ["timers", "setTimeout"],
clearTimeout: ["timers", "clearTimeout"],
setInterval: ["timers", "setInterval"],
clearInterval: ["timers", "clearInterval"],
setImmediate: ["timers", "setImmediate"],
clearImmediate: ["timers", "clearImmediate"],
},
}),
{
name: "rollup-plugin-node-deno",
resolveId(id) {
id = id.replace("node:", "");
if (builtinModules.includes(id)) {
return {
id: `node:${id}`,
moduleSideEffects: false,
external: true,
};
}
if (isHTTPImport(id)) {
return {
id,
external: true,
};
}
},
renderChunk(code) {
const s = new MagicString(code);
const imports = findStaticImports(code);
for (const i of imports) {
if (
!i.specifier.startsWith(".") &&
!isAbsolute(i.specifier) &&
!isHTTPImport(i.specifier) &&
!i.specifier.startsWith("npm:")
) {
const specifier = i.specifier.replace("node:", "");
s.replace(
i.code,
i.code.replace(
new RegExp(`(?<quote>['"])${i.specifier}\\k<quote>`),
JSON.stringify(
builtinModules.includes(specifier)
? "node:" + specifier
: "npm:" + specifier
)
)
);
}
}
if (s.hasChanged()) {
return {
code: s.toString(),
map: s.generateMap({ includeContent: true }),
};
}
},
},
{
name: "inject-process",
renderChunk: {
order: "post",
handler(code, chunk) {
if (
!chunk.isEntry &&
(!ImportMetaRe.test(code) || code.includes("ROLLUP_NO_REPLACE"))
) {
return;
}

const s = new MagicString(code);
s.prepend("import process from 'node:process';");

return {
code: s.toString(),
map: s.generateMap({ includeContent: true }),
};
},
},
},
],
},
hooks: {
async compiled(nitro) {
// https://deno.com/manual@v1.34.3/getting_started/configuration_file
const denoJSON = {
tasks: {
start:
"deno run --unstable --allow-net --allow-read --allow-env ./server/index.mjs",
},
};
await writeFile(
resolve(nitro.options.output.dir, "deno.json"),
JSON.stringify(denoJSON, null, 2)
);
},
},
});

const HTTP_IMPORT_RE = /^(https?:)?\/\//;

function isHTTPImport(id: string) {
return HTTP_IMPORT_RE.test(id);
}
3 changes: 2 additions & 1 deletion src/presets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export * from "./bun";
export * from "./cloudflare-module";
export * from "./cloudflare-pages";
export * from "./cloudflare";
export * from "./deno";
export * from "./deno-deploy";
export * from "./deno-server";
export * from "./digital-ocean";
export * from "./firebase";
export * from "./heroku";
Expand Down
4 changes: 2 additions & 2 deletions src/rollup/plugins/import-meta.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Plugin } from "rollup";
import { Nitro } from "../../types";

export function importMeta(nitro: Nitro): Plugin {
const ImportMetaRe = /import\.meta|globalThis._importMeta_/;
export const ImportMetaRe = /import\.meta|globalThis._importMeta_/;

export function importMeta(nitro: Nitro): Plugin {
return {
name: "import-meta",
renderChunk(code, chunk) {
Expand Down
File renamed without changes.
82 changes: 82 additions & 0 deletions src/runtime/entries/deno-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import "#internal/nitro/virtual/polyfill";
import destr from "destr";
import { nitroApp } from "../app";
import { useRuntimeConfig } from "#internal/nitro";

// @ts-expect-error unknown global Deno
if (Deno.env.get("DEBUG")) {
addEventListener("unhandledrejection", (event) =>
console.error("[nitro] [dev] [unhandledRejection]", event.reason)
);
addEventListener("error", (event) =>
console.error("[nitro] [dev] [uncaughtException]", event.error)
);
} else {
addEventListener("unhandledrejection", (err) =>
console.error("[nitro] [production] [unhandledRejection] " + err)
);
addEventListener("error", (event) =>
console.error("[nitro] [production] [uncaughtException] " + event.error)
);
}

// @ts-expect-error unknown global Deno
// https://deno.land/api@v1.34.3?s=Deno.serve&unstable=
Deno.serve(
{
// @ts-expect-error unknown global Deno
key: Deno.env.get("NITRO_SSL_KEY"),
// @ts-expect-error unknown global Deno
cert: Deno.env.get("NITRO_SSL_CERT"),
// @ts-expect-error unknown global Deno
port: destr(Deno.env.get("NITRO_PORT") || Deno.env.get("PORT")) || 3000,
// @ts-expect-error unknown global Deno
hostname: Deno.env.get("NITRO_HOST") || Deno.env.get("HOST"),
onListen: (opts) => {
const baseURL = (useRuntimeConfig().app.baseURL || "").replace(/\/$/, "");
const url = `${opts.hostname}:${opts.port}${baseURL}`;
console.log(`Listening ${url}`);
},
},
handler
);

async function handler(request: Request) {
const url = new URL(request.url);

// https://deno.land/api?s=Body
let body;
if (request.body) {
body = await request.arrayBuffer();
}

const r = await nitroApp.localCall({
url: url.pathname + url.search,
host: url.hostname,
protocol: url.protocol,
headers: Object.fromEntries(request.headers.entries()),
method: request.method,
redirect: request.redirect,
body,
});

// TODO: fix in runtime/static
const responseBody = r.status === 304 ? null : r.body;
return new Response(responseBody, {
// @ts-ignore TODO: Should be HeadersInit instead of string[][]
headers: normalizeOutgoingHeaders(r.headers),
status: r.status,
statusText: r.statusText,
});
}

function normalizeOutgoingHeaders(
headers: Record<string, string | string[] | undefined>
) {
return Object.entries(headers).map(([k, v]) => [
k,
Array.isArray(v) ? v.join(",") : v,
]);
}

export default {};
35 changes: 35 additions & 0 deletions test/presets/deno-server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { resolve } from "pathe";
import { describe, it, expect } from "vitest";
import { execa, execaCommandSync } from "execa";
import { getRandomPort, waitForPort } from "get-port-please";
import { setupTest, testNitro } from "../tests";

const hasDeno =
execaCommandSync("deno --version", { stdio: "ignore", reject: false })
.exitCode === 0;

describe.runIf(hasDeno)("nitro:preset:deno-server", async () => {
const ctx = await setupTest("deno-server");
testNitro(ctx, async () => {
const port = await getRandomPort();
const p = execa(
"deno",
["task", "--config", resolve(ctx.outDir, "deno.json"), "start"],
{
stdio: "inherit",
env: {
PORT: String(port),
},
}
);
ctx.server = {
url: `http://127.0.0.1:${port}`,
close: () => p.kill(),
} as any;
await waitForPort(port, { delay: 1000, retries: 20 });
return async ({ url, ...opts }) => {
const res = await ctx.fetch(url, opts);
return res;
};
});
});
Loading

0 comments on commit 58cde15

Please sign in to comment.