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 ( + + ) + } + `, + "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==