diff --git a/.eslintignore b/.eslintignore
index dc039c8c090..d11c2a620fb 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,7 +2,10 @@
**/tests/__snapshots/
**/node_modules/
!.eslintrc.js
-templates/deno
.tmp
/playground
**/__tests__/fixtures
+
+# deno
+packages/remix-deno
+templates/deno
diff --git a/.vscode/deno_resolve_npm_imports.json b/.vscode/deno_resolve_npm_imports.json
new file mode 100644
index 00000000000..e6490a679e1
--- /dev/null
+++ b/.vscode/deno_resolve_npm_imports.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 25fa6215fdd..402c21f23d7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -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"
}
diff --git a/docs/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md b/docs/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md
new file mode 100644
index 00000000000..8213d5a6b0c
--- /dev/null
+++ b/docs/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md
@@ -0,0 +1,110 @@
+# Use `npm` to manage NPM dependencies for Deno projects
+
+Date: 2022-05-10
+
+Status: accepted
+
+## Context
+
+Deno has three ways to manage dependencies:
+
+1. Inlined URL imports: `import {...} from "https://deno.land/x/blah"`
+2. [deps.ts](https://deno.land/manual/examples/manage_dependencies)
+3. [Import maps](https://deno.land/manual/linking_to_external_code/import_maps)
+
+Additionally, NPM packages can be accessed as Deno modules via [Deno-friendly CDNs](https://deno.land/manual/node/cdns#deno-friendly-cdns) like https://esm.sh .
+
+Remix has some requirements around dependencies:
+- Remix treeshakes dependencies that are free of side-effects.
+- Remix sets the environment (dev/prod/test) across all code, including dependencies, at runtime via the `NODE_ENV` environment variable.
+- Remix depends on some NPM packages that should be specified as peer dependencies (notably, `react` and `react-dom`).
+
+### Treeshaking
+
+To optimize bundle size, Remix [treeshakes](https://esbuild.github.io/api/#tree-shaking) your app's code and dependencies.
+This also helps to separate browser code and server code.
+
+Under the hood, the Remix compiler uses [esbuild](https://esbuild.github.io).
+Like other bundlers, `esbuild` uses [`sideEffects` in `package.json` to determine when it is safe to eliminate unused imports](https://esbuild.github.io/api/#conditionally-injecting-a-file).
+
+Unfortunately, URL imports do not have a standard mechanism for marking packages as side-effect free.
+
+### Setting dev/prod/test environment
+
+Deno-friendly CDNs set the environment via a query parameter (e.g. `?dev`), not via an environment variable.
+That means changing environment requires changing the URL import in the source code.
+While you could use multiple import maps (`dev.json`, `prod.json`, etc...) to workaround this, import maps have other limitations:
+
+- standard tooling for managing import maps is not available
+- import maps are not composeable, so any dependencies that use import maps must be manually accounted for
+
+### Specifying peer dependencies
+
+Even if import maps were perfected, CDNs compile each dependency in isolation.
+That means that specifying peer dependencies becomes tedious and error-prone as the user needs to:
+
+- determine which dependencies themselves depend on `react` (or other similar peer dependency), even if indirectly.
+- manually figure out which `react` version works across _all_ of these dependencies
+- set that version for `react` as a query parameter in _all_ or the URLs for the identified dependencies
+
+If any dependencies change (added, removed, version change),
+the user must repeat all of these steps again.
+
+## Decision
+
+### Use `npm` to manage NPM dependencies for Deno
+
+Do not use Deno-friendly CDNs for NPM dependencies in Remix projects using Deno.
+
+Use `npm` and `node_modules/` to manage NPM dependencies like `react` for Remix projects, even when using Deno with Remix.
+
+Deno module dependencies (e.g. from `https://deno.land`) can still be managed via URL imports.
+
+### Allow URL imports
+
+Remix will preserve any URL imports in the built bundles as external dependencies,
+letting your browser runtime and server runtime handle them accordingly.
+That means that you may:
+
+- use URL imports for the browser
+- use URL imports for the server, if your server runtime supports it
+
+For example, Node will throw errors for URL imports, while Deno will resolve URL imports as normal.
+
+### Do not support import maps
+
+Remix will not yet support import maps.
+
+## Consequences
+
+- URL imports will not be treeshaken
+- Users can specify environment via the `NODE_ENV` environment variable at runtime.
+- Users won't have to do error-prone, manual dependency resolution.
+
+### VS Code type hints
+
+Users may configure an import map for the [Deno extension for VS Code](denoland.vscode-deno) to enable type hints for NPM-managed dependencies within their Deno editor:
+
+`.vscode/resolve_npm_imports_in_deno.json`
+```json
+{
+ "// 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": {
+ "react": "https://esm.sh/react@18.0.0",
+ "react-dom": "https://esm.sh/react-dom@18.0.0",
+ "react-dom/server": "https://esm.sh/react-dom@18.0.0/server"
+ }
+}
+```
+
+`.vscode/settings.json`
+```json
+{
+ "deno.enable": true,
+ "deno.importMap": "./.vscode/resolve_npm_imports_in_deno.json"
+}
+```
\ No newline at end of file
diff --git a/integration/deno-compiler-test.ts b/integration/deno-compiler-test.ts
new file mode 100644
index 00000000000..80283133895
--- /dev/null
+++ b/integration/deno-compiler-test.ts
@@ -0,0 +1,230 @@
+import { test, expect } from "@playwright/test";
+import * as fse from "fs-extra";
+import path from "path";
+import shell from "shelljs";
+import glob from "glob";
+
+import { createFixtureProject, js, json } from "./helpers/create-fixture";
+
+let projectDir: string;
+
+const findBrowserBundle = (projectDir: string): string =>
+ path.resolve(projectDir, "public", "build");
+
+const findServerBundle = (projectDir: string): string =>
+ path.resolve(projectDir, "build", "index.js");
+
+const importPattern = (importSpecifier: string) =>
+ new RegExp(
+ String.raw`import\s*{.*}\s*from\s*"` + importSpecifier + String.raw`"`
+ );
+
+const findCodeFiles = async (directory: string) =>
+ glob.sync("**/*.@(js|jsx|ts|tsx)", {
+ cwd: directory,
+ absolute: true,
+ });
+const searchFiles = async (pattern: string | RegExp, files: string[]) => {
+ let result = shell.grep("-l", pattern, files);
+ return result.stdout
+ .trim()
+ .split("\n")
+ .filter((line) => line.length > 0);
+};
+
+test.beforeAll(async () => {
+ projectDir = await createFixtureProject({
+ template: "deno-template",
+ files: {
+ "package.json": json({
+ private: true,
+ sideEffects: false,
+ dependencies: {
+ "@remix-run/deno": "0.0.0-local-version",
+ "@remix-run/react": "0.0.0-local-version",
+ react: "0.0.0-local-version",
+ "react-dom": "0.0.0-local-version",
+ component: "0.0.0-local-version",
+ "deno-pkg": "0.0.0-local-version",
+ },
+ devDependencies: {
+ "@remix-run/dev": "0.0.0-local-version",
+ },
+ }),
+ "app/routes/index.jsx": js`
+ import fake from "deno-pkg";
+ import { urlComponent } from "https://deno.land/x/component.ts";
+ import { urlUtil } from "https://deno.land/x/util.ts";
+ import { urlServerOnly } from "https://deno.land/x/server-only.ts";
+
+ import { npmComponent } from "npm-component";
+ import { npmUtil } from "npm-util";
+ import { npmServerOnly } from "npm-server-only";
+
+ import { useLoaderData } from "@remix-run/react";
+
+ export const loader = () => {
+ return json({
+ a: urlUtil(),
+ b: urlServerOnly(),
+ c: npmUtil(),
+ d: npmServerOnly(),
+ });
+ }
+
+ export default function Index() {
+ const data = useLoaderData();
+ return (
+
+ - {fake}
+
+ - {urlComponent}
+ - {urlUtil()}
+ - {data.a}
+ - {data.b}
+
+ - {npmComponent}
+ - {npmUtil()}
+ - {data.c}
+ - {data.d}
+
+ )
+ }
+ `,
+ "node_modules/npm-component/package.json": json({
+ name: "npm-component",
+ version: "1.0.0",
+ sideEffects: false,
+ }),
+ "node_modules/npm-component/index.js": js`
+ module.exports = { npmComponent: () => "NPM_COMPONENT" };
+ `,
+ "node_modules/npm-util/package.json": json({
+ name: "npm-util",
+ version: "1.0.0",
+ sideEffects: false,
+ }),
+ "node_modules/npm-util/index.js": js`
+ module.exports = { npmUtil: () => "NPM_UTIL" };
+ `,
+ "node_modules/npm-server-only/package.json": json({
+ name: "npm-server-only",
+ version: "1.0.0",
+ sideEffects: false,
+ }),
+ "node_modules/npm-server-only/index.js": js`
+ module.exports = { npmServerOnly: () => "NPM_SERVER_ONLY" };
+ `,
+ "node_modules/deno-pkg/package.json": json({
+ name: "deno-pkg",
+ version: "1.0.0",
+ type: "module",
+ main: "./default.js",
+ exports: {
+ deno: "./deno.js",
+ worker: "./worker.js",
+ default: "./default.js",
+ },
+ sideEffects: false,
+ }),
+ "node_modules/deno-pkg/deno.js": js`
+ export default "DENO_EXPORTS";
+ `,
+ "node_modules/deno-pkg/worker.js": js`
+ export default "WORKER_EXPORTS";
+ `,
+ "node_modules/deno-pkg/default.js": js`
+ export default "DEFAULT_EXPORTS";
+ `,
+ },
+ });
+});
+
+test("compiler does not bundle url imports for server", async () => {
+ let serverBundle = await fse.readFile(findServerBundle(projectDir), "utf8");
+ expect(serverBundle).toMatch(importPattern("https://deno.land/x/util.ts"));
+ expect(serverBundle).toMatch(
+ importPattern("https://deno.land/x/server-only.ts")
+ );
+
+ // server-side rendering
+ expect(serverBundle).toMatch(
+ importPattern("https://deno.land/x/component.ts")
+ );
+});
+
+test("compiler does not bundle url imports for browser", async () => {
+ let browserBundle = findBrowserBundle(projectDir);
+ let browserCodeFiles = await findCodeFiles(browserBundle);
+
+ let utilFiles = await searchFiles(
+ importPattern("https://deno.land/x/util.ts"),
+ browserCodeFiles
+ );
+ expect(utilFiles.length).toBeGreaterThanOrEqual(1);
+
+ let componentFiles = await searchFiles(
+ importPattern("https://deno.land/x/component.ts"),
+ browserCodeFiles
+ );
+ expect(componentFiles.length).toBeGreaterThanOrEqual(1);
+
+ /*
+ Url imports _could_ have side effects, but the vast majority do not.
+ Currently Remix marks all URL imports as side-effect free.
+ */
+ let serverOnlyUtilFiles = await searchFiles(
+ importPattern("https://deno.land/x/server-only.ts"),
+ browserCodeFiles
+ );
+ expect(serverOnlyUtilFiles.length).toBe(0);
+});
+
+test("compiler bundles npm imports for server", async () => {
+ let serverBundle = await fse.readFile(findServerBundle(projectDir), "utf8");
+
+ expect(serverBundle).not.toMatch(importPattern("npm-component"));
+ expect(serverBundle).toContain("NPM_COMPONENT");
+
+ expect(serverBundle).not.toMatch(importPattern("npm-util"));
+ expect(serverBundle).toContain("NPM_UTIL");
+
+ expect(serverBundle).not.toMatch(importPattern("npm-server-only"));
+ expect(serverBundle).toContain("NPM_SERVER_ONLY");
+});
+
+test("compiler bundles npm imports for browser", async () => {
+ let browserBundle = findBrowserBundle(projectDir);
+ let browserCodeFiles = await findCodeFiles(browserBundle);
+
+ let utilImports = await searchFiles(
+ importPattern("npm-util"),
+ browserCodeFiles
+ );
+ expect(utilImports.length).toBe(0);
+ let utilFiles = await searchFiles("NPM_UTIL", browserCodeFiles);
+ expect(utilFiles.length).toBeGreaterThanOrEqual(1);
+
+ let componentImports = await searchFiles(
+ importPattern("npm-component"),
+ browserCodeFiles
+ );
+ expect(componentImports.length).toBe(0);
+ let componentFiles = await searchFiles("NPM_COMPONENT", browserCodeFiles);
+ expect(componentFiles.length).toBeGreaterThanOrEqual(1);
+
+ let serverOnlyImports = await searchFiles(
+ importPattern("npm-server-only"),
+ browserCodeFiles
+ );
+ expect(serverOnlyImports.length).toBe(0);
+ let serverOnlyFiles = await searchFiles("NPM_SERVER_ONLY", browserCodeFiles);
+ expect(serverOnlyFiles.length).toBe(0);
+});
+
+test("compiler bundles deno export of 3rd party package", async () => {
+ let serverBundle = await fse.readFile(findServerBundle(projectDir), "utf8");
+
+ expect(serverBundle).toMatch("DENO_EXPORTS");
+ expect(serverBundle).not.toMatch("DEFAULT_EXPORTS");
+});
diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts
index f7add1aeabf..94af6ab020c 100644
--- a/integration/helpers/create-fixture.ts
+++ b/integration/helpers/create-fixture.ts
@@ -18,7 +18,7 @@ interface FixtureInit {
buildStdio?: Writable;
sourcemap?: boolean;
files: { [filename: string]: string };
- template?: "cf-template" | "node-template";
+ template?: "cf-template" | "deno-template" | "node-template";
setup?: "node" | "cloudflare";
}
diff --git a/integration/helpers/deno-template/.gitignore b/integration/helpers/deno-template/.gitignore
new file mode 100644
index 00000000000..2b5c2c32959
--- /dev/null
+++ b/integration/helpers/deno-template/.gitignore
@@ -0,0 +1,5 @@
+/node_modules/
+
+/.cache
+/build
+/public/build
\ No newline at end of file
diff --git a/integration/helpers/deno-template/app/entry.client.tsx b/integration/helpers/deno-template/app/entry.client.tsx
new file mode 100644
index 00000000000..62a6a81634d
--- /dev/null
+++ b/integration/helpers/deno-template/app/entry.client.tsx
@@ -0,0 +1,5 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import { RemixBrowser } from "@remix-run/react";
+
+ReactDOM.hydrate(, document);
diff --git a/integration/helpers/deno-template/app/entry.server.tsx b/integration/helpers/deno-template/app/entry.server.tsx
new file mode 100644
index 00000000000..5aff7ec014f
--- /dev/null
+++ b/integration/helpers/deno-template/app/entry.server.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import { renderToString } from "react-dom/server";
+import { RemixServer } from "@remix-run/react";
+import type { EntryContext } from "@remix-run/deno";
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+) {
+ const markup = renderToString(
+ ,
+ );
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ return new Response("" + markup, {
+ status: responseStatusCode,
+ headers: responseHeaders,
+ });
+}
diff --git a/integration/helpers/deno-template/app/root.tsx b/integration/helpers/deno-template/app/root.tsx
new file mode 100644
index 00000000000..a74cc0195c0
--- /dev/null
+++ b/integration/helpers/deno-template/app/root.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+import {
+ Links,
+ LiveReload,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+import type { MetaFunction } from "@remix-run/deno";
+
+export const meta: MetaFunction = () => ({
+ charset: "utf-8",
+ title: "New Remix App",
+ viewport: "width=device-width,initial-scale=1",
+});
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/integration/helpers/deno-template/package.json b/integration/helpers/deno-template/package.json
new file mode 100644
index 00000000000..1fec877f2bf
--- /dev/null
+++ b/integration/helpers/deno-template/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "remix-template-deno",
+ "private": true,
+ "sideEffects": false,
+ "dependencies": {
+ "@remix-run/deno": "*",
+ "@remix-run/react": "*",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "*"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+}
diff --git a/integration/helpers/deno-template/public/favicon.ico b/integration/helpers/deno-template/public/favicon.ico
new file mode 100644
index 00000000000..8830cf6821b
Binary files /dev/null and b/integration/helpers/deno-template/public/favicon.ico differ
diff --git a/integration/helpers/deno-template/remix.config.js b/integration/helpers/deno-template/remix.config.js
new file mode 100644
index 00000000000..625a152107a
--- /dev/null
+++ b/integration/helpers/deno-template/remix.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ serverBuildTarget: "deno",
+ server: "./server.ts",
+
+ /*
+ If live reload causes page to re-render without changes (live reload is too fast),
+ increase the dev server broadcast delay.
+
+ If live reload seems slow, try to decrease the dev server broadcast delay.
+ */
+ devServerBroadcastDelay: 300,
+};
diff --git a/integration/helpers/deno-template/server.ts b/integration/helpers/deno-template/server.ts
new file mode 100644
index 00000000000..87e95bf7b18
--- /dev/null
+++ b/integration/helpers/deno-template/server.ts
@@ -0,0 +1,14 @@
+import { serve } from "https://deno.land/std@0.128.0/http/server.ts";
+import { createRequestHandlerWithStaticFiles } from "@remix-run/deno";
+// Import path interpreted by the Remix compiler
+import * as build from "@remix-run/dev/server-build";
+
+const remixHandler = createRequestHandlerWithStaticFiles({
+ build,
+ mode: process.env.NODE_ENV,
+ getLoadContext: () => ({}),
+});
+
+const port = Number(Deno.env.get("PORT")) || 8000;
+console.log(`Listening on http://localhost:${port}`);
+serve(remixHandler, { port });
diff --git a/package.json b/package.json
index df9d711fe20..18a186ee2ec 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/packages/remix-deno/.empty.js b/packages/remix-deno/.empty.js
new file mode 100644
index 00000000000..1f74377ffc1
--- /dev/null
+++ b/packages/remix-deno/.empty.js
@@ -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.
+*/
\ No newline at end of file
diff --git a/packages/remix-deno/.gitignore b/packages/remix-deno/.gitignore
new file mode 100644
index 00000000000..43b0a725e54
--- /dev/null
+++ b/packages/remix-deno/.gitignore
@@ -0,0 +1,2 @@
+# This file is needed for the Rollup copy plugin to ignore local node_modules/
+node_modules
\ No newline at end of file
diff --git a/packages/remix-deno/README.md b/packages/remix-deno/README.md
new file mode 100644
index 00000000000..91fea4a8312
--- /dev/null
+++ b/packages/remix-deno/README.md
@@ -0,0 +1,14 @@
+# @remix-run/deno
+
+This package contains a Deno module that provides abstractions for Remix.
+
+For more, see the [Remix docs](https://remix.run/docs/).
+
+## Install
+
+Installation is done via `npm`, but the code itself is Deno source code.
+Read more about [why we use `npm` to manage dependencies for Deno projects](https://github.com/remix-run/remix/blob/main/docs/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md) in Remix.
+
+```sh
+npm install @remix-run/deno
+```
\ No newline at end of file
diff --git a/packages/remix-deno/crypto.ts b/packages/remix-deno/crypto.ts
new file mode 100644
index 00000000000..65511ac5c76
--- /dev/null
+++ b/packages/remix-deno/crypto.ts
@@ -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 {
+ 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;
+}
diff --git a/packages/remix-deno/globals.ts b/packages/remix-deno/globals.ts
new file mode 100644
index 00000000000..cbfa8c5dc0d
--- /dev/null
+++ b/packages/remix-deno/globals.ts
@@ -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;
diff --git a/templates/deno/remix-deno/implementations.ts b/packages/remix-deno/implementations.ts
similarity index 57%
rename from templates/deno/remix-deno/implementations.ts
rename to packages/remix-deno/implementations.ts
index a915cc5dc7a..c86e93993f1 100644
--- a/templates/deno/remix-deno/implementations.ts
+++ b/packages/remix-deno/implementations.ts
@@ -3,13 +3,15 @@ import {
createCookieSessionStorageFactory,
createMemorySessionStorageFactory,
createSessionStorageFactory,
-} from "./deps/@remix-run/server-runtime.ts";
+} from "@remix-run/server-runtime";
import { sign, unsign } from "./crypto.ts";
export const createCookie = createCookieFactory({ sign, unsign });
-export const createCookieSessionStorage =
- createCookieSessionStorageFactory(createCookie);
+export const createCookieSessionStorage = createCookieSessionStorageFactory(
+ createCookie,
+);
export const createSessionStorage = createSessionStorageFactory(createCookie);
-export const createMemorySessionStorage =
- createMemorySessionStorageFactory(createSessionStorage);
+export const createMemorySessionStorage = createMemorySessionStorageFactory(
+ createSessionStorage,
+);
diff --git a/templates/deno/remix-deno/index.ts b/packages/remix-deno/index.ts
similarity index 89%
rename from templates/deno/remix-deno/index.ts
rename to packages/remix-deno/index.ts
index 05e54ae13ff..e422c53858a 100644
--- a/templates/deno/remix-deno/index.ts
+++ b/packages/remix-deno/index.ts
@@ -1,3 +1,4 @@
+import "./globals.ts";
export { createFileSessionStorage } from "./sessions/fileStorage.ts";
export {
createRequestHandler,
@@ -18,18 +19,18 @@ export {
isSession,
json,
redirect,
-} from "https://esm.sh/@remix-run/server-runtime?pin=v68";
+} from "@remix-run/server-runtime";
export type {
ActionFunction,
AppData,
AppLoadContext,
- CreateRequestHandlerFunction,
Cookie,
CookieOptions,
CookieParseOptions,
CookieSerializeOptions,
CookieSignatureOptions,
+ CreateRequestHandlerFunction,
DataFunctionArgs,
EntryContext,
ErrorBoundaryComponent,
@@ -53,4 +54,4 @@ export type {
SessionData,
SessionIdStorageStrategy,
SessionStorage,
-} from "https://esm.sh/@remix-run/server-runtime?pin=v68";
+} from "@remix-run/server-runtime";
diff --git a/packages/remix-deno/package.json b/packages/remix-deno/package.json
new file mode 100644
index 00000000000..0ea3be90c14
--- /dev/null
+++ b/packages/remix-deno/package.json
@@ -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"
+ }
+}
diff --git a/templates/deno/remix-deno/server.ts b/packages/remix-deno/server.ts
similarity index 79%
rename from templates/deno/remix-deno/server.ts
rename to packages/remix-deno/server.ts
index e458ca5fc40..18a246f3535 100644
--- a/templates/deno/remix-deno/server.ts
+++ b/packages/remix-deno/server.ts
@@ -1,8 +1,7 @@
import * as path from "https://deno.land/std@0.128.0/path/mod.ts";
-import mime from "https://esm.sh/mime";
-
-import { createRequestHandler as createRemixRequestHandler } from "./deps/@remix-run/server-runtime.ts";
-import type { ServerBuild } from "./deps/@remix-run/server-runtime.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)) {
@@ -37,6 +36,12 @@ export function createRequestHandler({
};
}
+class FileNotFoundError extends Error {
+ constructor(filePath: string) {
+ super(`No such file or directory: ${filePath}`);
+ }
+}
+
export async function serveStaticFiles(
request: Request,
{
@@ -47,7 +52,7 @@ export async function serveStaticFiles(
cacheControl?: string | ((url: URL) => string);
publicDir?: string;
assetsPublicPath?: string;
- }
+ },
) {
const url = new URL(request.url);
@@ -65,9 +70,16 @@ export async function serveStaticFiles(
headers.set("Cache-Control", defaultCacheControl(url, assetsPublicPath));
}
- const file = await Deno.readFile(path.join(publicDir, url.pathname));
-
- return new Response(file, { headers });
+ const filePath = path.join(publicDir, url.pathname);
+ try {
+ const file = await Deno.readFile(filePath);
+ return new Response(file, { headers });
+ } catch (error) {
+ if (error.code === "EISDIR" || error.code === "ENOENT") {
+ throw new FileNotFoundError(filePath);
+ }
+ throw error;
+ }
}
export function createRequestHandlerWithStaticFiles({
@@ -94,7 +106,7 @@ export function createRequestHandlerWithStaticFiles({
try {
return await serveStaticFiles(request, staticFiles);
} catch (error) {
- if (error.code !== "EISDIR" && error.code !== "ENOENT") {
+ if (!(error instanceof FileNotFoundError)) {
throw error;
}
}
diff --git a/templates/deno/remix-deno/sessions/fileStorage.ts b/packages/remix-deno/sessions/fileStorage.ts
similarity index 94%
rename from templates/deno/remix-deno/sessions/fileStorage.ts
rename to packages/remix-deno/sessions/fileStorage.ts
index e34909e5fba..dd91c3fdce8 100644
--- a/templates/deno/remix-deno/sessions/fileStorage.ts
+++ b/packages/remix-deno/sessions/fileStorage.ts
@@ -1,9 +1,9 @@
import * as path from "https://deno.land/std@0.128.0/path/mod.ts";
import type {
- SessionStorage,
SessionIdStorageStrategy,
-} from "../deps/@remix-run/server-runtime.ts";
+ SessionStorage,
+} from "@remix-run/server-runtime";
import { createSessionStorage } from "../implementations.ts";
interface FileSessionStorageOptions {
@@ -54,7 +54,7 @@ export function createFileSessionStorage({
if (exists) continue;
await Deno.mkdir(path.dirname(file), { recursive: true }).catch(
- () => {}
+ () => {},
);
await Deno.writeFile(file, new TextEncoder().encode(content));
@@ -69,10 +69,9 @@ export function createFileSessionStorage({
const file = getFile(dir, id);
const content = JSON.parse(await Deno.readTextFile(file));
const data = content.data;
- const expires =
- typeof content.expires === "string"
- ? new Date(content.expires)
- : null;
+ const expires = typeof content.expires === "string"
+ ? new Date(content.expires)
+ : null;
if (!expires || expires > new Date()) {
return data;
diff --git a/packages/remix-dev/__tests__/replace-remix-imports-test.ts b/packages/remix-dev/__tests__/replace-remix-imports-test.ts
index 2ef2a3a10a7..9f259202d94 100644
--- a/packages/remix-dev/__tests__/replace-remix-imports-test.ts
+++ b/packages/remix-dev/__tests__/replace-remix-imports-test.ts
@@ -4,8 +4,10 @@ import os from "os";
import stripAnsi from "strip-ansi";
import type { PackageJson } from "type-fest";
import shell from "shelljs";
+import glob from "fast-glob";
import { run } from "../cli/run";
+import { readConfig } from "../config";
let output: string;
const ORIGINAL_IO = {
@@ -72,6 +74,7 @@ const replaceRemixImports = async (projectDir: string) => {
describe("`replace-remix-imports` migration", () => {
it("runs successfully", async () => {
let projectDir = makeApp();
+ let config = await readConfig(projectDir);
await replaceRemixImports(projectDir);
expect(output).toContain("detected `@remix-run/node`");
@@ -96,9 +99,15 @@ describe("`replace-remix-imports` migration", () => {
expect(packageJson.scripts).not.toContain("postinstall");
expect(output).toContain("✅ Your Remix imports look good!");
- let { code } = shell.grep("-nri", 'from "remix"', projectDir);
- // `grep` exits with status code `1` when no matches are found
- expect(code).toBe(1);
+
+ let files = glob.sync("**/*.@(ts|tsx|js|jsx)", {
+ cwd: config.appDirectory,
+ absolute: true,
+ });
+ let result = shell.grep("-l", 'from "remix"', files);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toBeNull();
+ expect(result.code).toBe(0);
expect(output).toContain("successfully migrated");
expect(output).toContain("npm install");
diff --git a/packages/remix-dev/cli/create.ts b/packages/remix-dev/cli/create.ts
index c75a8f27659..d5637a106dd 100644
--- a/packages/remix-dev/cli/create.ts
+++ b/packages/remix-dev/cli/create.ts
@@ -452,14 +452,15 @@ function isRemixStack(input: string) {
function isRemixTemplate(input: string) {
return [
- "remix",
- "express",
"arc",
+ "cloudflare-pages",
+ "cloudflare-workers",
+ "deno",
+ "express",
"fly",
"netlify",
+ "remix",
"vercel",
- "cloudflare-pages",
- "cloudflare-workers",
].includes(input);
}
diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts
index ac165fe9b7a..1ce772e6013 100644
--- a/packages/remix-dev/compiler.ts
+++ b/packages/remix-dev/compiler.ts
@@ -23,6 +23,7 @@ import { serverBareModulesPlugin } from "./compiler/plugins/serverBareModulesPlu
import { serverEntryModulePlugin } from "./compiler/plugins/serverEntryModulePlugin";
import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin";
import { writeFileSafe } from "./compiler/utils/fs";
+import { urlImportsPlugin } from "./compiler/plugins/urlImportsPlugin";
// When we build Remix, this shim file is copied directly into the output
// directory in the same place relative to this file. It is eventually injected
@@ -347,23 +348,13 @@ async function createBrowserBuild(
}
let plugins = [
+ urlImportsPlugin(),
mdxPlugin(config),
browserRouteModulesPlugin(config, /\?browser$/),
emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/),
NodeModulesPolyfillPlugin(),
];
- if (config.serverBuildTarget === "deno") {
- // @ts-expect-error
- let { cache } = await import("esbuild-plugin-cache");
- plugins.unshift(
- cache({
- importmap: {},
- directory: path.join(config.cacheDirectory, "http-import-cache"),
- })
- );
- }
-
return esbuild.build({
entryPoints,
outdir: config.assetsBuildDirectory,
@@ -418,7 +409,10 @@ function createServerBuild(
let isCloudflareRuntime = ["cloudflare-pages", "cloudflare-workers"].includes(
config.serverBuildTarget ?? ""
);
+ let isDenoRuntime = config.serverBuildTarget === "deno";
+
let plugins: esbuild.Plugin[] = [
+ urlImportsPlugin(),
mdxPlugin(config),
emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/),
serverRouteModulesPlugin(config),
@@ -438,7 +432,11 @@ function createServerBuild(
entryPoints,
outfile: config.serverBuildPath,
write: false,
- conditions: isCloudflareRuntime ? ["worker"] : undefined,
+ conditions: isCloudflareRuntime
+ ? ["worker"]
+ : isDenoRuntime
+ ? ["deno", "worker"]
+ : undefined,
platform: config.serverPlatform,
format: config.serverModuleFormat,
treeShaking: true,
diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
index eea09649103..09c20fdc908 100644
--- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
+++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
@@ -21,8 +21,10 @@ export function serverBareModulesPlugin(
dependencies: Record,
onWarning?: (warning: string, key: string) => void
): Plugin {
- let matchPath = createMatchPath();
+ let isDenoRuntime = remixConfig.serverBuildTarget === "deno";
+
// Resolve paths according to tsconfig paths property
+ let matchPath = isDenoRuntime ? undefined : createMatchPath();
function resolvePath(id: string) {
if (!matchPath) {
return id;
@@ -83,6 +85,7 @@ export function serverBareModulesPlugin(
// Always bundle everything for cloudflare.
case "cloudflare-pages":
case "cloudflare-workers":
+ case "deno":
return undefined;
}
diff --git a/packages/remix-dev/compiler/plugins/urlImportsPlugin.ts b/packages/remix-dev/compiler/plugins/urlImportsPlugin.ts
new file mode 100644
index 00000000000..6732f02a397
--- /dev/null
+++ b/packages/remix-dev/compiler/plugins/urlImportsPlugin.ts
@@ -0,0 +1,21 @@
+import type { Plugin } from "esbuild";
+
+/**
+ * Mark all URL imports as external so that each URL import is preserved in the build output.
+ */
+export const urlImportsPlugin = (): Plugin => {
+ return {
+ name: "url-imports",
+ setup(build) {
+ build.onResolve({ filter: /^https?:\/\// }, ({ path }) => {
+ /*
+ The vast majority of packages are side-effect free,
+ and URL imports don't have a mechanism for specifying that they are side-effect free.
+
+ Mark all url imports as side-effect free so that they can be treeshaken by esbuild.
+ */
+ return { path, external: true, sideEffects: false };
+ });
+ },
+ };
+};
diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json
index dfc7ab8884b..bd83fd07621 100644
--- a/packages/remix-dev/package.json
+++ b/packages/remix-dev/package.json
@@ -27,7 +27,6 @@
"chokidar": "^3.5.1",
"dotenv": "^16.0.0",
"esbuild": "0.14.22",
- "esbuild-plugin-cache": "^0.2.9",
"exit-hook": "2.2.1",
"express": "4.17.3",
"fast-glob": "3.2.11",
diff --git a/rollup.config.js b/rollup.config.js
index 75e569896ee..43da5834d5d 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -518,6 +518,28 @@ function remixCloudflare() {
];
}
+/** @returns {import("rollup").RollupOptions[]} */
+function remixDeno() {
+ let sourceDir = "packages/remix-deno";
+ let outputDir = getOutputDir("@remix-run/deno");
+
+ return [
+ {
+ input: `${sourceDir}/.empty.js`,
+ plugins: [
+ copy({
+ targets: [
+ { src: `LICENSE.md`, dest: outputDir },
+ { src: `${sourceDir}/**/*`, dest: outputDir },
+ ],
+ gitignore: true,
+ }),
+ copyToPlaygrounds(),
+ ],
+ },
+ ];
+}
+
/** @returns {import("rollup").RollupOptions[]} */
function remixCloudflareWorkers() {
let sourceDir = "packages/remix-cloudflare-workers";
@@ -858,6 +880,7 @@ export default function rollup(options) {
...remixServerRuntime(options),
...remixNode(options),
...remixCloudflare(options),
+ ...remixDeno(options),
...remixCloudflarePages(options),
...remixCloudflareWorkers(options),
...remixServerAdapters(options),
diff --git a/scripts/publish-private.js b/scripts/publish-private.js
index 1525401bd47..89b8d978319 100644
--- a/scripts/publish-private.js
+++ b/scripts/publish-private.js
@@ -32,6 +32,7 @@ async function run() {
"cloudflare",
"cloudflare-pages",
"cloudflare-workers",
+ "deno",
"node", // publish node before node servers
"architect",
"express", // publish express before serve
diff --git a/scripts/publish.js b/scripts/publish.js
index c49887c59bb..32d893f04c2 100644
--- a/scripts/publish.js
+++ b/scripts/publish.js
@@ -45,6 +45,7 @@ async function run() {
"cloudflare",
"cloudflare-pages",
"cloudflare-workers",
+ "deno",
"node", // publish node before node servers
"architect",
"express", // publish express before serve
diff --git a/scripts/utils.js b/scripts/utils.js
index 30f326530b8..6c58735ad90 100644
--- a/scripts/utils.js
+++ b/scripts/utils.js
@@ -17,7 +17,7 @@ let remixPackages = {
"netlify",
"vercel",
],
- runtimes: ["cloudflare", "node"],
+ runtimes: ["cloudflare", "deno", "node"],
core: ["dev", "server-runtime", "react", "eslint-config"],
get all() {
return [...this.adapters, ...this.runtimes, ...this.core, "serve"];
diff --git a/templates/deno/.eslintignore b/templates/deno/.eslintignore
new file mode 100644
index 00000000000..f59ec20aabf
--- /dev/null
+++ b/templates/deno/.eslintignore
@@ -0,0 +1 @@
+*
\ No newline at end of file
diff --git a/templates/deno/.vscode/resolve_npm_imports.json b/templates/deno/.vscode/resolve_npm_imports.json
new file mode 100644
index 00000000000..2a7dcfdd408
--- /dev/null
+++ b/templates/deno/.vscode/resolve_npm_imports.json
@@ -0,0 +1,15 @@
+{
+ "// 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": {
+ "react": "https://esm.sh/react@18.0.0",
+ "react-dom": "https://esm.sh/react-dom@18.0.0",
+ "react-dom/server": "https://esm.sh/react-dom@18.0.0/server",
+ "@remix-run/dev/server-build": "https://esm.sh/@remix-run/dev@1.4.3/server-build",
+ "@remix-run/deno": "https://esm.sh/@remix-run/deno@1.4.3",
+ "@remix-run/react": "https://esm.sh/@remix-run/react@1.4.3"
+ }
+}
diff --git a/templates/deno/.vscode/settings.json b/templates/deno/.vscode/settings.json
index 3dfe427e466..c58c9450882 100644
--- a/templates/deno/.vscode/settings.json
+++ b/templates/deno/.vscode/settings.json
@@ -1,4 +1,5 @@
{
"deno.enable": true,
- "deno.lint": true
-}
\ No newline at end of file
+ "deno.lint": true,
+ "deno.importMap": "./.vscode/resolve_npm_imports.json"
+}
diff --git a/templates/deno/README.md b/templates/deno/README.md
index 2647b6e964c..27676ec9e9a 100644
--- a/templates/deno/README.md
+++ b/templates/deno/README.md
@@ -1,69 +1,62 @@
-# Remix Deno Template
+# Remix + Deno
-⚠️ EXPERIMENTAL ⚠️
+Welcome to the Deno template for Remix! 🦕
+
+For more, check out the [Remix docs](https://remix.run/docs).
## Install
```sh
-npx create-remix@latest --template
+npx create-remix@latest --template deno
```
-## Scripts
+## Managing dependencies
-```sh
-npm run build
-```
+Read about [how we recommend to manage dependencies for Remix projects using Deno](https://github.com/remix-run/remix/blob/main/docs/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md
+).
-```sh
-npm run start
-```
+- ✅ You should use `npm` to install NPM packages
+ ```sh
+ npm install react
+ ```
+ ```ts
+ import { useState } from "react";
+ ```
+- ✅ You may use inlined URL imports or [deps.ts](https://deno.land/manual/examples/manage_dependencies#managing-dependencies) for Deno modules.
+ ```ts
+ import { copy } from "https://deno.land/std@0.138.0/streams/conversion.ts";
+ ```
+- ❌ Do not use [import maps](https://deno.land/manual/linking_to_external_code/import_maps).
+
+## Development
+
+From your terminal:
```sh
npm run dev
```
-## 🚧 Under construction 🚧
-
-This section details temporary scaffolding for the Remix Deno template.
-All of this scaffolding is planned for removal at a later date.
-
-### `package.json`
-
-A `package.json` is included for now so that `remix` CLI (from `@remix-run/dev`) can run the Remix compiler.
-
-In the future, we could provide a stand-alone executable for the Remix compiler OR `npx remix`, and we would remove the `package.json` file from this Deno template.
+This starts your app in development mode, rebuilding assets on file changes.
-### Local `remix-deno` package
+## Production
-For now, we are inlining Remix's `@remix-run/deno` package into `remix-deno` to enable faster experimentation and development.
+First, build your app for production:
-In the future, this template would not include the `remix-deno` directory at all.
-Instead, users could import from `@remix-run/deno` (exact URL TBD).
-
-## 🐞 Known issues 🐞
-
-### `dev` does not live reload
-
-Deno server is not currently configured to live reload when `--watch` detects changes, requiring a manual refresh in the browser for non-server changes (e.g. changing JSX content).
-
-To enable live reload, `@remix-run/react` must be built with `NODE_ENV=development`.
-To do so with `esm.sh`, `?dev` must be added to all imports that depend on React.
-However, bundling the React development build for `esm.sh` (`https://esm.sh/react@17.0.2?dev`) runs into an [esbuild bug](https://github.com/evanw/esbuild/issues/2099).
-
-Need a better way to switch from development to production mode than adding/removing `?dev` for all React-dependent imports.
-Also, need esbuild bug to be resolved.
-
-### Pinned React imports
+```sh
+npm run build
+```
-For all React-related imports (including `@remix-run/*` imports), we append `?pin=v68` to the URL.
-This is the only reliable way we were able to guarantee that only one copy of React would be present in the browser.
+Then run the app in production mode:
-No plans on how to address this yet.
+```sh
+npm start
+```
-### @remix-run/dev/server-build
+## Deployment
-The `@remix-run/dev/server-build` import within `server.ts` (`import * as build from '@remix-run/dev/server-build'`) is a special import for the Remix compiler that points to the built server entry point (typically within `./build`).
+Building the Deno app (`npm run build`) results in two outputs:
-The `vscode_deno` plugin complains about this import with a red squiggly underline as Deno cannot resolve this special import.
+- `build/` (server bundle)
+- `public/build/` (browser bundle)
-No plans on how to address this yet.
+You can deploy these bundles to a host of your choice, just make sure it runs Deno!
diff --git a/templates/deno/app/deps/@remix-run/react.ts b/templates/deno/app/deps/@remix-run/react.ts
deleted file mode 100644
index 209ac677660..00000000000
--- a/templates/deno/app/deps/@remix-run/react.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export {
- Links,
- LiveReload,
- Meta,
- Outlet,
- RemixBrowser,
- RemixServer,
- Scripts,
- ScrollRestoration,
-} from "https://esm.sh/@remix-run/react@1.4.0?pin=v77&dev";
diff --git a/templates/deno/app/deps/README.md b/templates/deno/app/deps/README.md
deleted file mode 100644
index fc0f00c0819..00000000000
--- a/templates/deno/app/deps/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-Splitting up client and server dependencies so that `react-dom` is only bundled on the client and `react-dom-server` is only bundled on the server.
-
-## React and other singletons
-
-Singletons like `react` will cause errors if more than one copy is present.
-
-To ensure only one copy of each singleton is loaded, use [`deps` files](https://deno.land/manual@v1.19.3/examples/manage_dependencies) to centrally defined a single version of each singleton.
\ No newline at end of file
diff --git a/templates/deno/app/deps/react-dom-server.ts b/templates/deno/app/deps/react-dom-server.ts
deleted file mode 100644
index 6edfec42340..00000000000
--- a/templates/deno/app/deps/react-dom-server.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export {
- default,
- renderToString,
-} from "https://esm.sh/react-dom@18.0.0/server?pin=v77&dev";
diff --git a/templates/deno/app/deps/react-dom.ts b/templates/deno/app/deps/react-dom.ts
deleted file mode 100644
index 0fc6d10f56e..00000000000
--- a/templates/deno/app/deps/react-dom.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "https://esm.sh/react-dom@18.0.0?pin=v77&dev";
diff --git a/templates/deno/app/deps/react.ts b/templates/deno/app/deps/react.ts
deleted file mode 100644
index 1bf522b9381..00000000000
--- a/templates/deno/app/deps/react.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "https://esm.sh/react@18.0.0?pin=v77&dev";
diff --git a/templates/deno/app/entry.client.tsx b/templates/deno/app/entry.client.tsx
index 8d7f2b5aa9d..62a6a81634d 100644
--- a/templates/deno/app/entry.client.tsx
+++ b/templates/deno/app/entry.client.tsx
@@ -1,5 +1,5 @@
-import React from "./deps/react.ts";
-import ReactDOM from "./deps/react-dom.ts";
-import { RemixBrowser } from "./deps/@remix-run/react.ts";
+import React from "react";
+import ReactDOM from "react-dom";
+import { RemixBrowser } from "@remix-run/react";
ReactDOM.hydrate(, document);
diff --git a/templates/deno/app/entry.server.tsx b/templates/deno/app/entry.server.tsx
index 044cc6ad9df..5aff7ec014f 100644
--- a/templates/deno/app/entry.server.tsx
+++ b/templates/deno/app/entry.server.tsx
@@ -1,16 +1,16 @@
-import React from "./deps/react.ts";
-import { renderToString } from "./deps/react-dom-server.ts";
-import { RemixServer } from "./deps/@remix-run/react.ts";
-import type { EntryContext } from "../remix-deno/index.ts";
+import React from "react";
+import { renderToString } from "react-dom/server";
+import { RemixServer } from "@remix-run/react";
+import type { EntryContext } from "@remix-run/deno";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
- remixContext: EntryContext
+ remixContext: EntryContext,
) {
const markup = renderToString(
-
+ ,
);
responseHeaders.set("Content-Type", "text/html");
diff --git a/templates/deno/app/root.tsx b/templates/deno/app/root.tsx
index e1e633406bc..a74cc0195c0 100644
--- a/templates/deno/app/root.tsx
+++ b/templates/deno/app/root.tsx
@@ -1,4 +1,4 @@
-import React from "./deps/react.ts";
+import React from "react";
import {
Links,
LiveReload,
@@ -6,8 +6,8 @@ import {
Outlet,
Scripts,
ScrollRestoration,
-} from "./deps/@remix-run/react.ts";
-import type { MetaFunction } from "../remix-deno/index.ts";
+} from "@remix-run/react";
+import type { MetaFunction } from "@remix-run/deno";
export const meta: MetaFunction = () => ({
charset: "utf-8",
diff --git a/templates/deno/app/routes/index.tsx b/templates/deno/app/routes/index.tsx
index dd83272f72a..f2280e0a045 100644
--- a/templates/deno/app/routes/index.tsx
+++ b/templates/deno/app/routes/index.tsx
@@ -1,4 +1,4 @@
-import React from "../deps/react.ts";
+import React from "react";
export default function Index() {
return (
diff --git a/templates/deno/package.json b/templates/deno/package.json
index 0e9f358ac74..d1e2304d5d0 100644
--- a/templates/deno/package.json
+++ b/templates/deno/package.json
@@ -7,7 +7,9 @@
"dev": "remix build && run-p dev:*",
"dev:remix": "remix watch",
"dev:deno": "cross-env NODE_ENV=development deno run --unstable --watch --allow-net --allow-read --allow-env ./build/index.js",
- "start": "cross-env NODE_ENV=production deno run --unstable --allow-net --allow-read --allow-env ./build/index.js"
+ "start": "cross-env NODE_ENV=production deno run --unstable --allow-net --allow-read --allow-env ./build/index.js",
+ "lint": "deno lint --ignore=node_modules",
+ "format": "deno fmt --ignore=node_modules"
},
"devDependencies": {
"@remix-run/dev": "*",
@@ -16,5 +18,11 @@
},
"engines": {
"node": ">=14"
+ },
+ "dependencies": {
+ "@remix-run/deno": "*",
+ "@remix-run/react": "*",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
}
}
diff --git a/templates/deno/remix-deno/README.md b/templates/deno/remix-deno/README.md
deleted file mode 100644
index a591d709a98..00000000000
--- a/templates/deno/remix-deno/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# @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.
\ No newline at end of file
diff --git a/templates/deno/remix-deno/crypto.ts b/templates/deno/remix-deno/crypto.ts
deleted file mode 100644
index afe93093e36..00000000000
--- a/templates/deno/remix-deno/crypto.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import type {
- SignFunction,
- UnsignFunction,
-} from "./deps/@remix-run/server-runtime.ts";
-
-const encoder = new TextEncoder();
-
-export const sign: SignFunction = async (value, secret) => {
- let data = encoder.encode(value);
- let key = await createKey(secret, ["sign"]);
- 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 value = cookie.slice(0, cookie.lastIndexOf("."));
- let hash = cookie.slice(cookie.lastIndexOf(".") + 1);
-
- let data = encoder.encode(value);
- let key = await createKey(secret, ["verify"]);
- let signature = byteStringToUint8Array(atob(hash));
- let valid = await crypto.subtle.verify("HMAC", key, signature, data);
-
- return valid ? value : false;
-};
-
-async function createKey(
- secret: string,
- usages: CryptoKey["usages"]
-): Promise {
- let key = await crypto.subtle.importKey(
- "raw",
- encoder.encode(secret),
- { name: "HMAC", hash: "SHA-256" },
- false,
- usages
- );
-
- return key;
-}
-
-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;
-}
diff --git a/templates/deno/remix-deno/deps/@remix-run/server-runtime.ts b/templates/deno/remix-deno/deps/@remix-run/server-runtime.ts
deleted file mode 100644
index 1ca2c9ba9e2..00000000000
--- a/templates/deno/remix-deno/deps/@remix-run/server-runtime.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export type {
- ServerBuild,
- SessionIdStorageStrategy,
- SessionStorage,
- SignFunction,
- UnsignFunction,
-} from "https://esm.sh/@remix-run/server-runtime@1.4.0?pin=v77";
-export {
- createCookieFactory,
- createCookieSessionStorageFactory,
- createMemorySessionStorageFactory,
- createSessionStorageFactory,
- createRequestHandler,
-} from "https://esm.sh/@remix-run/server-runtime@1.4.0?pin=v77";
diff --git a/templates/deno/remix.config.js b/templates/deno/remix.config.js
index 165a13b8001..625a152107a 100644
--- a/templates/deno/remix.config.js
+++ b/templates/deno/remix.config.js
@@ -1,4 +1,12 @@
module.exports = {
serverBuildTarget: "deno",
server: "./server.ts",
+
+ /*
+ If live reload causes page to re-render without changes (live reload is too fast),
+ increase the dev server broadcast delay.
+
+ If live reload seems slow, try to decrease the dev server broadcast delay.
+ */
+ devServerBroadcastDelay: 300,
};
diff --git a/templates/deno/server.ts b/templates/deno/server.ts
index 187e6cd27d1..87e95bf7b18 100644
--- a/templates/deno/server.ts
+++ b/templates/deno/server.ts
@@ -1,12 +1,10 @@
import { serve } from "https://deno.land/std@0.128.0/http/server.ts";
-// Temporary: in the future, import from `@remix-run/deno` at some URL
-import { createRequestHandlerWithStaticFiles } from "./remix-deno/index.ts";
+import { createRequestHandlerWithStaticFiles } from "@remix-run/deno";
// Import path interpreted by the Remix compiler
import * as build from "@remix-run/dev/server-build";
const remixHandler = createRequestHandlerWithStaticFiles({
build,
- // process.env.NODE_ENV is provided by Remix at compile time
mode: process.env.NODE_ENV,
getLoadContext: () => ({}),
});
diff --git a/yarn.lock b/yarn.lock
index 699fa43911b..1fdb0a6bab3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2135,7 +2135,7 @@
resolved "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz"
integrity sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==
-"@types/node@^14.11.8", "@types/node@^14.14.11", "@types/node@^14.14.31":
+"@types/node@^14.14.31":
version "14.18.16"
resolved "https://registry.npmjs.org/@types/node/-/node-14.18.16.tgz"
integrity sha512-X3bUMdK/VmvrWdoTkz+VCn6nwKwrKCFTHtqwBIaQJNx4RUIBBUFXM00bqPz/DsDd+Icjmzm6/tyYZzeGVqb6/Q==
@@ -4158,22 +4158,6 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
-deno-cache@^0.2.12:
- version "0.2.12"
- resolved "https://registry.npmjs.org/deno-cache/-/deno-cache-0.2.12.tgz"
- integrity sha512-Jv8utRPQhsm+kx9ky0OdUnTWBLKGlFcBoLjQqrpuDd9zhuciCLPmklbz1YYfdaeM0dgp1nwRoqlHu5sH3vmJGQ==
- dependencies:
- "@types/node" "^14.11.8"
- "@types/node-fetch" "^2.5.7"
- node-fetch "^2.6.1"
-
-deno-importmap@^0.1.6:
- version "0.1.6"
- resolved "https://registry.npmjs.org/deno-importmap/-/deno-importmap-0.1.6.tgz"
- integrity sha512-nZ5ZA8qW5F0Yzq1VhRp1wARpWSfD0FQvI1IUHXbE3oROO6tcYomTIWSAZGzO4LGQl1hTG6UmhPNTP3d4uMXzMg==
- dependencies:
- "@types/node" "^14.14.11"
-
depd@^1.1.0, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz"
@@ -4486,14 +4470,6 @@ esbuild-openbsd-64@0.14.22:
resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.22.tgz"
integrity sha512-vK912As725haT313ANZZZN+0EysEEQXWC/+YE4rQvOQzLuxAQc2tjbzlAFREx3C8+uMuZj/q7E5gyVB7TzpcTA==
-esbuild-plugin-cache@^0.2.9:
- version "0.2.9"
- resolved "https://registry.npmjs.org/esbuild-plugin-cache/-/esbuild-plugin-cache-0.2.9.tgz"
- integrity sha512-wSYkiLbaZWuFPdepopZ16/hjIDx+fow4/KbtuD8UQPlswFssI5Efgqmzg6hhqJk87fpOk6OYn5pEPftQ5i0mkg==
- dependencies:
- deno-cache "^0.2.12"
- deno-importmap "^0.1.6"
-
esbuild-register@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.3.2.tgz"
@@ -7872,7 +7848,7 @@ mime@1.6.0, mime@^1.3.4:
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-mime@3.0.0:
+mime@3.0.0, mime@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
@@ -8100,7 +8076,7 @@ node-dir@^0.1.17:
dependencies:
minimatch "^3.0.2"
-node-fetch@^2.6.1, node-fetch@^2.6.7:
+node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==