Skip to content

Commit

Permalink
feat: add deno package (remix-run#1397)
Browse files Browse the repository at this point in the history
fix: added history as a dep to @remix-run/react
  • Loading branch information
jacob-ebey authored Jan 26, 2022
1 parent ff43654 commit bd823e9
Show file tree
Hide file tree
Showing 19 changed files with 486 additions and 1 deletion.
13 changes: 13 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ module.exports = {
testEnvironment: "node",
testMatch: ["<rootDir>/packages/create-remix/**/*-test.[jt]s?(x)"]
},
{
displayName: "deno",
testEnvironment: "jsdom",
testMatch: ["<rootDir>/packages/remix-deno/**/*-test.[jt]s?(x)"],
moduleNameMapper: {
"https://deno.land/std/path/mod.ts":
"<rootDir>/packages/remix-deno/__tests__/pathmock.js"
},
setupFiles: [
"<rootDir>/jest/setupNodeGlobals.ts",
"<rootDir>/packages/remix-deno/__tests__/setupJest.js"
]
},
{
displayName: "dev",
testEnvironment: "node",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"packages/remix-architect",
"packages/remix-cloudflare-pages",
"packages/remix-cloudflare-workers",
"packages/remix-deno",
"packages/remix-dev",
"packages/remix-eslint-config",
"packages/remix-express",
Expand Down
13 changes: 13 additions & 0 deletions packages/remix-deno/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Welcome to Remix!

[Remix](https://remix.run) is a web framework that helps you build better websites with React.

To get started, open a new shell and run:

```sh
$ npx create-remix@latest
```

Then follow the prompts you see in your terminal.

For more information about Remix, [visit remix.run](https://remix.run)!
1 change: 1 addition & 0 deletions packages/remix-deno/__tests__/pathmock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { join } from "path";
193 changes: 193 additions & 0 deletions packages/remix-deno/__tests__/server-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime";

import {
createRequestHandler,
createRequestHandlerWithStaticFiles,
serveStaticFiles
} from "../server";

jest.mock("@remix-run/server-runtime/server");
let mockedCreateRequestHandler =
createRemixRequestHandler as jest.MockedFunction<
typeof createRemixRequestHandler
>;

describe("deno request handler", () => {
let consoleSpy: jest.SpyInstance = jest
.spyOn(global.console, "error")
.mockImplementation(() => {}) as any;

afterEach(() => {
consoleSpy.mockReset();
mockedCreateRequestHandler.mockReset();
});

afterAll(() => {
consoleSpy.mockRestore();
jest.restoreAllMocks();
});

it("handles requests", async () => {
mockedCreateRequestHandler.mockImplementation(() => async req => {
return new Response(`URL: ${new URL(req.url).pathname}`);
});
let handler = createRequestHandler({
build: undefined
});

let response = await handler(new Request("http://test.com/foo/bar"));
expect(response.status).toBe(200);
expect(await response.text()).toBe("URL: /foo/bar");
});

it("handles errors", async () => {
let error = new Error("test error");
mockedCreateRequestHandler.mockImplementation(() => async req => {
throw error;
});
let handler = createRequestHandler({
build: undefined
});

let response = await handler(new Request("http://test.com/foo/bar"));
expect(response.status).toBe(500);
expect(await response.text()).toBe("Internal Error");

expect(consoleSpy).toHaveBeenCalledWith(error);
});

it("passes request context", async () => {
let loadContext;
mockedCreateRequestHandler.mockImplementation(() => async (req, ctx) => {
loadContext = ctx;
return new Response(`URL: ${new URL(req.url).pathname}`);
});
let context = {};
let handler = createRequestHandler({
build: undefined,
getLoadContext: async () => context
});

let response = await handler(new Request("http://test.com/foo/bar"));
expect(loadContext).toBe(context);
expect(response.status).toBe(200);
expect(await response.text()).toBe("URL: /foo/bar");
});
});

describe("deno static files", () => {
let options = {
publicDir: "./public",
assetsPublicPath: "/build/"
};

it("throws when no file", async () => {
let error = new Error("test error");
global.Deno.readFile.mockImplementation(() => {
throw error;
});

let request = new Request("http://test.com/foo/bar");
await expect(() => serveStaticFiles(request, options)).rejects.toThrow(
error
);
});

it("reads file and applies long cache control", async () => {
global.Deno.readFile.mockImplementation(() => {
return "file content";
});

let request = new Request("http://test.com/build/bar.css");
let response = await serveStaticFiles(request, options);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/css");
expect(response.headers.get("Cache-Control")).toBe(
"public, max-age=31536000, immutable"
);
expect(await response.text()).toBe("file content");
});

it("reads file and applies short cache control", async () => {
global.Deno.readFile.mockImplementation(() => {
return "file content";
});

let request = new Request("http://test.com/test/bar.css");
let response = await serveStaticFiles(request, options);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/css");
expect(response.headers.get("Cache-Control")).toBe("public, max-age=600");
expect(await response.text()).toBe("file content");
});

it("reads file and applies custom cache control string", async () => {
global.Deno.readFile.mockImplementation(() => {
return "file content";
});

let request = new Request("http://test.com/test/bar.css");
let response = await serveStaticFiles(request, {
cacheControl: "public, max-age=1"
});
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/css");
expect(response.headers.get("Cache-Control")).toBe("public, max-age=1");
expect(await response.text()).toBe("file content");
});

it("reads file and applies custom cache control function", async () => {
global.Deno.readFile.mockImplementation(() => {
return "file content";
});

let request = new Request("http://test.com/test/bar.css");
let response = await serveStaticFiles(request, {
cacheControl: () => "public, max-age=2"
});
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/css");
expect(response.headers.get("Cache-Control")).toBe("public, max-age=2");
expect(await response.text()).toBe("file content");
});
});

describe("deno combined request handler", () => {
it("handles requests", async () => {
global.Deno.readFile.mockImplementation(() => {
let error = new Error("test error");
(error as any).code = "ENOENT";
throw error;
});

mockedCreateRequestHandler.mockImplementation(() => async req => {
return new Response(`URL: ${new URL(req.url).pathname}`);
});
let handler = createRequestHandlerWithStaticFiles({
build: undefined
});

let response = await handler(new Request("http://test.com/foo/bar"));
expect(response.status).toBe(200);
expect(await response.text()).toBe("URL: /foo/bar");
});

it("handles static assets", async () => {
global.Deno.readFile.mockImplementation(() => {
return "file content";
});

mockedCreateRequestHandler.mockImplementation(() => async req => {
return new Response(`URL: ${new URL(req.url).pathname}`);
});
let handler = createRequestHandlerWithStaticFiles({
build: undefined
});

let response = await handler(new Request("http://test.com/foo/bar.css"));
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/css");
expect(response.headers.get("Cache-Control")).toBe("public, max-age=600");
expect(await response.text()).toBe("file content");
});
});
5 changes: 5 additions & 0 deletions packages/remix-deno/__tests__/setupJest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
global.Deno = {
readFile: jest.fn()
};

global.window = {};
52 changes: 52 additions & 0 deletions packages/remix-deno/cookieSigning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const encoder = new TextEncoder();

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

let data = encoder.encode(value);
let signature = await crypto.subtle.sign("HMAC", key, data);
let hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(
/=+$/,
""
);

return value + "." + hash;
}

export async function unsign(
cookie: string,
secret: string
): Promise<string | false> {
let key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);

let value = cookie.slice(0, cookie.lastIndexOf("."));
let hash = cookie.slice(cookie.lastIndexOf(".") + 1);

let data = encoder.encode(value);
let signature = byteStringToUint8Array(atob(hash));
let valid = await crypto.subtle.verify("HMAC", key, signature, data);

return valid ? value : false;
}

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

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

return array;
}
6 changes: 6 additions & 0 deletions packages/remix-deno/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { sign, unsign } from "./cookieSigning";

export function installGlobals() {
window.sign = sign;
window.unsign = unsign;
}
9 changes: 9 additions & 0 deletions packages/remix-deno/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { installGlobals } from "./globals";

export {
createRequestHandler,
createRequestHandlerWithStaticFiles,
serveStaticFiles
} from "./server";

installGlobals();
21 changes: 21 additions & 0 deletions packages/remix-deno/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@remix-run/deno",
"description": "Deno platform abstractions for Remix",
"version": "1.1.3",
"license": "MIT",
"module": "./index.js",
"types": "./index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/remix-run/remix",
"directory": "packages/remix-deno"
},
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
"dependencies": {
"@remix-run/server-runtime": "1.1.3",
"mime": "^3.0.0"
},
"sideEffects": false
}
Loading

0 comments on commit bd823e9

Please sign in to comment.