Skip to content

Commit

Permalink
feat(@remix-run/deno): add server runtime package for deno
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori committed May 10, 2022
1 parent 27fa69f commit 869ddb0
Show file tree
Hide file tree
Showing 19 changed files with 431 additions and 4 deletions.
5 changes: 4 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
**/tests/__snapshots/
**/node_modules/
!.eslintrc.js
templates/deno
.tmp
/playground
**/__tests__/fixtures

# deno
packages/remix-deno
templates/deno
13 changes: 13 additions & 0 deletions .vscode/deno_resolve_npm_imports.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"// Resolve NPM imports for `packages/remix-deno`.": "",

"// This import map is used solely for the denoland.vscode-deno extension.": "",
"// Remix does not support import maps.": "",
"// Dependency management is done through `npm` and `node_modules/` instead.": "",
"// Deno-only dependencies may be imported via URL imports (without using import maps).": "",

"imports": {
"mime": "https://esm.sh/mime@3.0.0",
"@remix-run/server-runtime": "https://esm.sh/@remix-run/server-runtime@1.4.3"
}
}
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"deno.enablePaths": [
"./packages/remix-deno/",
],
"deno.importMap": "./.vscode/deno_resolve_npm_imports.json"
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"packages/remix-cloudflare",
"packages/remix-cloudflare-pages",
"packages/remix-cloudflare-workers",
"packages/remix-deno",
"packages/remix-dev",
"packages/remix-eslint-config",
"packages/remix-express",
Expand Down
6 changes: 6 additions & 0 deletions packages/remix-deno/.empty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
Intentionally left empty as a dummy input for Rollup.
This package should not be bundled by Rollup as its source
code is a Deno module, not an NPM package.
*/
2 changes: 2 additions & 0 deletions packages/remix-deno/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This file is needed for the Rollup copy plugin to ignore local node_modules/
node_modules
1 change: 1 addition & 0 deletions packages/remix-deno/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @remix-run/deno
52 changes: 52 additions & 0 deletions packages/remix-deno/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { SignFunction, UnsignFunction } from "@remix-run/server-runtime";

const encoder = new TextEncoder();

export const sign: SignFunction = async (value, secret) => {
const data = encoder.encode(value);
const key = await createKey(secret, ["sign"]);
const signature = await crypto.subtle.sign("HMAC", key, data);
const hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(
/=+$/,
"",
);

return value + "." + hash;
};

export const unsign: UnsignFunction = async (cookie, secret) => {
const value = cookie.slice(0, cookie.lastIndexOf("."));
const hash = cookie.slice(cookie.lastIndexOf(".") + 1);

const data = encoder.encode(value);
const key = await createKey(secret, ["verify"]);
const signature = byteStringToUint8Array(atob(hash));
const valid = await crypto.subtle.verify("HMAC", key, signature, data);

return valid ? value : false;
};

async function createKey(
secret: string,
usages: CryptoKey["usages"],
): Promise<CryptoKey> {
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
usages,
);

return key;
}

function byteStringToUint8Array(byteString: string): Uint8Array {
const array = new Uint8Array(byteString.length);

for (let i = 0; i < byteString.length; i++) {
array[i] = byteString.charCodeAt(i);
}

return array;
}
12 changes: 12 additions & 0 deletions packages/remix-deno/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Remix provides `process.env.NODE_ENV` at compile time.
Declare types for `process` here so that they are available in Deno.
*/

interface ProcessEnv {
NODE_ENV: "development" | "production" | "test";
}
interface Process {
env: ProcessEnv;
}
var process: Process;
17 changes: 17 additions & 0 deletions packages/remix-deno/implementations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
createCookieFactory,
createCookieSessionStorageFactory,
createMemorySessionStorageFactory,
createSessionStorageFactory,
} from "@remix-run/server-runtime";

import { sign, unsign } from "./crypto.ts";

export const createCookie = createCookieFactory({ sign, unsign });
export const createCookieSessionStorage = createCookieSessionStorageFactory(
createCookie,
);
export const createSessionStorage = createSessionStorageFactory(createCookie);
export const createMemorySessionStorage = createMemorySessionStorageFactory(
createSessionStorage,
);
57 changes: 57 additions & 0 deletions packages/remix-deno/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import "./globals.ts";
export { createFileSessionStorage } from "./sessions/fileStorage.ts";
export {
createRequestHandler,
createRequestHandlerWithStaticFiles,
serveStaticFiles,
} from "./server.ts";

export {
createCookie,
createCookieSessionStorage,
createMemorySessionStorage,
createSessionStorage,
} from "./implementations.ts";

export {
createSession,
isCookie,
isSession,
json,
redirect,
} from "@remix-run/server-runtime";

export type {
ActionFunction,
AppData,
AppLoadContext,
Cookie,
CookieOptions,
CookieParseOptions,
CookieSerializeOptions,
CookieSignatureOptions,
CreateRequestHandlerFunction,
DataFunctionArgs,
EntryContext,
ErrorBoundaryComponent,
HandleDataRequestFunction,
HandleDocumentRequestFunction,
HeadersFunction,
HtmlLinkDescriptor,
HtmlMetaDescriptor,
LinkDescriptor,
LinksFunction,
LoaderFunction,
MetaDescriptor,
MetaFunction,
PageLinkDescriptor,
RequestHandler,
RouteComponent,
RouteHandle,
ServerBuild,
ServerEntryModule,
Session,
SessionData,
SessionIdStorageStrategy,
SessionStorage,
} from "@remix-run/server-runtime";
20 changes: 20 additions & 0 deletions packages/remix-deno/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@remix-run/deno",
"version": "1.4.3",
"description": "Deno platform abstractions for Remix",
"homepage": "https://remix.run",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/remix-run/remix",
"directory": "packages/remix-deno"
},
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
"sideEffects": false,
"dependencies": {
"@remix-run/server-runtime": "*",
"mime": "^3.0.0"
}
}
103 changes: 103 additions & 0 deletions packages/remix-deno/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as path from "https://deno.land/std@0.128.0/path/mod.ts";
import mime from "mime";
import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";
import type { ServerBuild } from "@remix-run/server-runtime";

function defaultCacheControl(url: URL, assetsPublicPath = "/build/") {
if (url.pathname.startsWith(assetsPublicPath)) {
return "public, max-age=31536000, immutable";
} else {
return "public, max-age=600";
}
}

export function createRequestHandler<Context = unknown>({
build,
mode,
getLoadContext,
}: {
build: ServerBuild;
mode?: string;
getLoadContext?: (request: Request) => Promise<Context> | Context;
}) {
const remixHandler = createRemixRequestHandler(build, mode);
return async (request: Request) => {
try {
const loadContext = getLoadContext
? await getLoadContext(request)
: undefined;

return await remixHandler(request, loadContext);
} catch (e) {
console.error(e);

return new Response("Internal Error", { status: 500 });
}
};
}

export async function serveStaticFiles(
request: Request,
{
cacheControl,
publicDir = "./public",
assetsPublicPath = "/build/",
}: {
cacheControl?: string | ((url: URL) => string);
publicDir?: string;
assetsPublicPath?: string;
},
) {
const url = new URL(request.url);

const headers = new Headers();
const contentType = mime.getType(url.pathname);
if (contentType) {
headers.set("Content-Type", contentType);
}

if (typeof cacheControl === "function") {
headers.set("Cache-Control", cacheControl(url));
} else if (cacheControl) {
headers.set("Cache-Control", cacheControl);
} else {
headers.set("Cache-Control", defaultCacheControl(url, assetsPublicPath));
}

const file = await Deno.readFile(path.join(publicDir, url.pathname));

return new Response(file, { headers });
}

export function createRequestHandlerWithStaticFiles<Context = unknown>({
build,
mode,
getLoadContext,
staticFiles = {
publicDir: "./public",
assetsPublicPath: "/build/",
},
}: {
build: ServerBuild;
mode?: string;
getLoadContext?: (request: Request) => Promise<Context> | Context;
staticFiles?: {
cacheControl?: string | ((url: URL) => string);
publicDir?: string;
assetsPublicPath?: string;
};
}) {
const remixHandler = createRequestHandler({ build, mode, getLoadContext });

return async (request: Request) => {
try {
return await serveStaticFiles(request, staticFiles);
} catch (error) {
if (error.code !== "EISDIR" && error.code !== "ENOENT") {
throw error;
}
}

return remixHandler(request);
};
}
Loading

0 comments on commit 869ddb0

Please sign in to comment.