Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Netlify Edge Functions #2866

Merged
merged 13 commits into from
Apr 19, 2022
13 changes: 11 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
"typescript.tsdk": "node_modules/typescript/lib",
"deno.enablePaths": [
"./templates/deno/",
"./templates/netlify/remix.init/edge-server.js",
"./packages/remix-netlify-edge/mod.ts",
"./packages/remix-netlify-edge/server.ts",
"./packages/remix-netlify-edge/remix-deno/"
],
"deno.unstable": true,
"deno.importMap": "deno-import-map.json"
}
9 changes: 9 additions & 0 deletions deno-import-map.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"imports": {
"netlify:edge": "https://edge-bootstrap.netlify.app/v1/index.ts",
"@remix-run/netlify-edge": "./packages/remix-netlify-edge/mod.ts",
"@remix-run/netlify-edge/deno": "./packages/remix-netlify-edge/remix-deno/mod.ts",
"@remix-run/dev/server-build": "https://esm.sh/@remix-run/dev/server-build",
"@remix-run/server-runtime": "https://esm.sh/@remix-run/server-runtime@1.4.1?pin=v77"
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"packages/remix-eslint-config",
"packages/remix-express",
"packages/remix-netlify",
"packages/remix-netlify-edge",
"packages/remix-node",
"packages/remix-react",
"packages/remix-serve",
Expand Down
57 changes: 56 additions & 1 deletion packages/remix-dev/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,10 @@ async function createBrowserBuild(
NodeModulesPolyfillPlugin(),
];

if (config.serverBuildTarget === "deno") {
if (
config.serverBuildTarget === "deno" ||
config.serverBuildTarget === "netlify-edge"
Comment on lines +357 to +358
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified to

    ["deno", "netlify-edge"].includes(config.serverBuildTarget]

) {
// @ts-expect-error
let { cache } = await import("esbuild-plugin-cache");
plugins.unshift(
Expand Down Expand Up @@ -431,6 +434,58 @@ function createServerBuild(
plugins.unshift(NodeModulesPolyfillPlugin());
}

if (config.serverBuildTarget === "netlify-edge") {
let edgeManifest = {
functions: [{ function: "server", path: "/*" }],
import_map: "../remix-import-map.json",
version: 1,
};
let edgeDir = path.dirname(config.serverBuildPath);

fse.ensureDirSync(edgeDir);
fse.writeJSONSync(path.join(edgeDir, "manifest.json"), edgeManifest);

// This generated import map is processed by the netlify CLI and combined with the internal map
let importMapPath = path.join(
config.rootDirectory,
".netlify",
"remix-import-map.json"
);

let runtimePath: string | undefined;

try {
// If the user has it locally-installed, use that
runtimePath = `file://${require.resolve(
"@remix-run/netlify-edge/mod.ts",
{
paths: [config.rootDirectory],
}
)}`;
} catch {
// ...otherwise, load it from the package URL. The env var is so we can override in dev.
runtimePath =
process.env.NETLIFY_EDGE_RUNTIME_PATH ??
"https://unpkg.com/@remix-run/netlify-edge@experimental-netlify-edge/mod.ts";
}

let buildPath = require.resolve("./server-build.d.ts");

if (runtimePath) {
let importMap = {
imports: {
"@remix-run/netlify-edge": runtimePath,
"@remix-run/dev/server-build": `file://${buildPath}`,
"@remix-run/server-runtime":
"https://esm.sh/@remix-run/server-runtime@1.4.1?pin=v77",
},
};

fse.ensureDirSync(path.dirname(importMapPath));
fse.writeJSONSync(importMapPath, importMap);
}
}

return esbuild
.build({
absWorkingDir: config.rootDirectory,
Expand Down
17 changes: 16 additions & 1 deletion packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import {
} from "../virtualModules";
import { createMatchPath } from "../utils/tsconfig";

// Modules that have Deno import mappings
const denoModules = new Set([
"@remix-run/netlify-edge",
"@remix-run/server-runtime",
]);

/**
* A plugin responsible for resolving bare module ids based on server target.
* This includes externalizing for node based plaforms, and bundling for single file
Expand Down Expand Up @@ -80,10 +86,19 @@ export function serverBareModulesPlugin(
}

switch (remixConfig.serverBuildTarget) {
// Always bundle everything for cloudflare.
// Always bundle everything for Cloudflare
case "cloudflare-pages":
case "cloudflare-workers":
return undefined;
case "netlify-edge":
// Bundle everything except URL imports and aliased modules for Netlify Edge
if (
!path.startsWith("https:") &&
!path.startsWith("file:") &&
!denoModules.has(path)
) {
return undefined;
}
}

for (let pattern of remixConfig.serverDependenciesToBundle) {
Expand Down
5 changes: 5 additions & 0 deletions packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ServerBuildTarget =
| "node-cjs"
| "arc"
| "netlify"
| "netlify-edge"
| "vercel"
| "cloudflare-pages"
| "cloudflare-workers"
Expand Down Expand Up @@ -290,6 +291,7 @@ export async function readConfig(
case "cloudflare-pages":
case "cloudflare-workers":
case "deno":
case "netlify-edge":
serverModuleFormat = "esm";
serverPlatform = "neutral";
break;
Expand Down Expand Up @@ -328,6 +330,9 @@ export async function readConfig(
case "netlify":
serverBuildPath = ".netlify/functions-internal/server.js";
break;
case "netlify-edge":
serverBuildPath = ".netlify/edge-functions/server.js";
break;
case "vercel":
serverBuildPath = "api/index.js";
break;
Expand Down
13 changes: 13 additions & 0 deletions packages/remix-netlify-edge/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)!
33 changes: 33 additions & 0 deletions packages/remix-netlify-edge/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type {
CreateCookieFunction,
CreateCookieSessionStorageFunction,
CreateSessionStorageFunction,
CreateMemorySessionStorageFunction,
ServerBuild,
} from "@remix-run/server-runtime";

interface BaseContext {
next: (options?: { sendConditionalRequest?: boolean }) => Promise<Response>;
}
export declare function createRequestHandler<
Context extends BaseContext = BaseContext
>({
build,
mode,
getLoadContext,
}: {
build: ServerBuild;
mode?: string;
getLoadContext?: (
request: Request,
context?: Context
) => Promise<Context> | Context;
}): (request: Request, context: Context) => Promise<Response | void>;
export {};

export * from "@remix-run/server-runtime";

export const createCookie: CreateCookieFunction;
export const createCookieSessionStorage: CreateCookieSessionStorageFunction;
export const createSessionStorage: CreateSessionStorageFunction;
export const createMemorySessionStorage: CreateMemorySessionStorageFunction;
12 changes: 12 additions & 0 deletions packages/remix-netlify-edge/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @remix-run/netlify-edge
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

// This package is meant to be imported by Deno, but includes types for Node
48 changes: 48 additions & 0 deletions packages/remix-netlify-edge/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export { createRequestHandler } from "./server.ts";
export type {
ActionFunction,
AppData,
AppLoadContext,
CreateRequestHandlerFunction,
Cookie,
CookieOptions,
CookieParseOptions,
CookieSerializeOptions,
CookieSignatureOptions,
DataFunctionArgs,
EntryContext,
ErrorBoundaryComponent,
HandleDataRequestFunction,
HandleDocumentRequestFunction,
HeadersFunction,
HtmlLinkDescriptor,
HtmlMetaDescriptor,
LinkDescriptor,
LinksFunction,
LoaderFunction,
MetaDescriptor,
MetaFunction,
PageLinkDescriptor,
RequestHandler,
RouteComponent,
RouteHandle,
ServerBuild,
ServerEntryModule,
Session,
SessionData,
SessionIdStorageStrategy,
SessionStorage,
} from "https://esm.sh/@remix-run/server-runtime@1.4.1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep this or change it to something like we have in the remix-deno package?

Suggested change
} from "https://esm.sh/@remix-run/server-runtime@1.4.1";
} from "https://esm.sh/@remix-run/server-runtime?pin=v77";

export {
createSession,
isCookie,
isSession,
json,
redirect,
} from "https://esm.sh/@remix-run/server-runtime@1.4.1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} from "https://esm.sh/@remix-run/server-runtime@1.4.1";
} from "https://esm.sh/@remix-run/server-runtime?pin=v77";

export {
createCookie,
createCookieSessionStorage,
createMemorySessionStorage,
createSessionStorage,
} from "./remix-deno/implementations.ts";
29 changes: 29 additions & 0 deletions packages/remix-netlify-edge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@remix-run/netlify-edge",
"description": "Netlify Edge platform abstractions for Remix",
"version": "1.0.0",
"license": "MIT",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
".": "./index.js",
"./mod.ts": "./mod.ts",
"./deno": "./remix-deno/mod.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/remix-run/remix",
"directory": "packages/remix-netlify-edge"
},
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
"files": [
"**/*.ts",
"index.js"
],
"peerDependencies": {
"@remix-run/server-runtime": "*"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably go into dependencies I think? 🤔

}
}
4 changes: 4 additions & 0 deletions packages/remix-netlify-edge/remix-deno/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# @remix-run/deno

`@remix-run/deno` package is temporarily inlined within this directory while Deno support is experimental.
In the future, this directory would be removed and Remix + Deno apps would import `@remix-run/deno` from some URL.
54 changes: 54 additions & 0 deletions packages/remix-netlify-edge/remix-deno/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {
SignFunction,
UnsignFunction,
} from "https://esm.sh/@remix-run/server-runtime?pin=v77";

const encoder = new TextEncoder();

export const sign: SignFunction = async (value, secret) => {
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 const unsign: UnsignFunction = async (cookie, secret) => {
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;
}
10 changes: 10 additions & 0 deletions packages/remix-netlify-edge/remix-deno/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export {};
declare global {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test";
}
interface Process {
env: ProcessEnv;
}
let process: Process;
}
15 changes: 15 additions & 0 deletions packages/remix-netlify-edge/remix-deno/implementations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
createCookieFactory,
createCookieSessionStorageFactory,
createMemorySessionStorageFactory,
createSessionStorageFactory,
} from "https://esm.sh/@remix-run/server-runtime?pin=v77";

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);
Loading