diff --git a/integration/helpers/vite-deno-template/.gitignore b/integration/helpers/vite-deno-template/.gitignore
new file mode 100644
index 00000000000..25f125d1358
--- /dev/null
+++ b/integration/helpers/vite-deno-template/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/app/deno.d.ts
+/build
+/package.json
diff --git a/integration/helpers/vite-deno-template/app/root.tsx b/integration/helpers/vite-deno-template/app/root.tsx
new file mode 100644
index 00000000000..e31409ca31d
--- /dev/null
+++ b/integration/helpers/vite-deno-template/app/root.tsx
@@ -0,0 +1,25 @@
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/integration/helpers/vite-deno-template/app/routes/_index.tsx b/integration/helpers/vite-deno-template/app/routes/_index.tsx
new file mode 100644
index 00000000000..4b80a32a373
--- /dev/null
+++ b/integration/helpers/vite-deno-template/app/routes/_index.tsx
@@ -0,0 +1,41 @@
+import type { MetaFunction } from "@remix-run/react";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "New Remix App" },
+ { name: "description", content: "Welcome to Remix!" },
+ ];
+};
+
+export default function Index() {
+ return (
+
+ );
+}
diff --git a/integration/helpers/vite-deno-template/deno.jsonc b/integration/helpers/vite-deno-template/deno.jsonc
new file mode 100644
index 00000000000..0e9265e7307
--- /dev/null
+++ b/integration/helpers/vite-deno-template/deno.jsonc
@@ -0,0 +1,28 @@
+{
+ "tasks": {
+ "build": "deno run -A npm:@remix-run/dev@^2.11.2 vite:build",
+ "dev": "deno run -A npm:@remix-run/dev@^2.11.2 vite:dev",
+ "typecheck": "deno check '**/*' && deno run -A npm:typescript@^5.5.4/tsc",
+ "typegen": "deno types > ./app/deno.d.ts"
+ },
+ "exclude": ["app/", "build/"],
+ "nodeModulesDir": true,
+ "imports": {
+ "@remix-run/dev": "npm:@remix-run/dev@^2.11.2",
+ "@remix-run/express": "npm:@remix-run/express@^2.11.2",
+ "@remix-run/react": "npm:@remix-run/react@^2.11.2",
+ "@remix-run/server-runtime": "npm:@remix-run/server-runtime@^2.11.2",
+ "@std/http": "jsr:@std/http@^1.0.4",
+ "@std/path": "jsr:@std/path@^1.0.3",
+ "@types/node": "npm:@types/node@^22.5.1",
+ "@types/react": "npm:@types/react@^18.3.5",
+ "@types/react-dom": "npm:@types/react-dom@^18.3.0",
+ "isbot": "npm:isbot@^5.1.17",
+ "postcss": "npm:postcss@^8.4.41",
+ "react": "npm:react@^18.3.1",
+ "react-dom": "npm:react-dom@^18.3.1",
+ "typescript": "npm:typescript@^5.5.4",
+ "vite": "npm:vite@^5.4.2",
+ "vite-tsconfig-paths": "npm:vite-tsconfig-paths@^5.0.1"
+ }
+}
diff --git a/integration/helpers/vite-deno-template/public/favicon.ico b/integration/helpers/vite-deno-template/public/favicon.ico
new file mode 100644
index 00000000000..8830cf6821b
Binary files /dev/null and b/integration/helpers/vite-deno-template/public/favicon.ico differ
diff --git a/integration/helpers/vite-deno-template/tsconfig.json b/integration/helpers/vite-deno-template/tsconfig.json
new file mode 100644
index 00000000000..96c32abe645
--- /dev/null
+++ b/integration/helpers/vite-deno-template/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "include": [
+ "app/**/*.ts",
+ "app/**/*.tsx",
+ "app/**/.server/**/*.ts",
+ "app/**/.server/**/*.tsx",
+ "app/**/.client/**/*.ts",
+ "app/**/.client/**/*.tsx"
+ ],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["vite/client"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+
+ // Vite takes care of building everything, not tsc.
+ "noEmit": true
+ }
+}
diff --git a/integration/helpers/vite-deno-template/vite.config.mts b/integration/helpers/vite-deno-template/vite.config.mts
new file mode 100644
index 00000000000..54066fb7ad8
--- /dev/null
+++ b/integration/helpers/vite-deno-template/vite.config.mts
@@ -0,0 +1,16 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [
+ remix({
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ },
+ }),
+ tsconfigPaths(),
+ ],
+});
diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts
index 65c805288ff..1cb61291259 100644
--- a/integration/helpers/vite.ts
+++ b/integration/helpers/vite.ts
@@ -1,18 +1,19 @@
+import type { Page } from "@playwright/test";
+import { test as base, expect } from "@playwright/test";
+import dedent from "dedent";
+import fse from "fs-extra";
+import getPort from "get-port";
+import glob from "glob";
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
-import path from "node:path";
import fs from "node:fs/promises";
+import path from "node:path";
import type { Readable } from "node:stream";
import url from "node:url";
-import fse from "fs-extra";
+import shell from "shelljs";
import stripIndent from "strip-indent";
import waitOn from "wait-on";
-import getPort from "get-port";
-import shell from "shelljs";
-import glob from "glob";
-import dedent from "dedent";
-import type { Page } from "@playwright/test";
-import { test as base, expect } from "@playwright/test";
+const denoBin = "deno"; // assume deno is globally installed
const remixBin = "node_modules/@remix-run/dev/dist/cli.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const root = path.resolve(__dirname, "../..");
@@ -90,7 +91,10 @@ export const EXPRESS_SERVER = (args: {
app.listen(port, () => console.log('http://localhost:' + port));
`;
-type TemplateName = "vite-template" | "vite-cloudflare-template";
+type TemplateName =
+ | "vite-template"
+ | "vite-cloudflare-template"
+ | "vite-deno-template";
export async function createProject(
files: Record = {},
@@ -143,6 +147,23 @@ export const viteBuild = ({
});
};
+export const viteBuildDeno = ({
+ cwd,
+ env = {},
+}: {
+ cwd: string;
+ env?: Record;
+}) => {
+ return spawnSync(denoBin, ["run", "-A", remixBin, "vite:build"], {
+ cwd,
+ env: {
+ ...process.env,
+ ...colorEnv,
+ ...env,
+ },
+ });
+};
+
export const viteRemixServe = async ({
cwd,
port,
@@ -204,14 +225,16 @@ type ServerArgs = {
};
const createDev =
- (nodeArgs: string[]) =>
+ (args: string[], runtime = node) =>
async ({ cwd, port, env, basename }: ServerArgs): Promise<() => unknown> => {
- let proc = node(nodeArgs, { cwd, env });
+ let proc = runtime(args, { cwd, env });
await waitForServer(proc, { port, basename });
return () => proc.kill();
};
export const viteDev = createDev([remixBin, "vite:dev"]);
+export const viteDevDeno = createDev(["run", "-A", remixBin, "vite:dev"], deno);
+
export const customDev = createDev(["./server.mjs"]);
// Used for testing errors thrown on build when we don't want to start and
@@ -240,6 +263,13 @@ type Fixtures = {
port: number;
cwd: string;
}>;
+ viteDevDeno: (
+ files: Files,
+ templateName?: TemplateName
+ ) => Promise<{
+ port: number;
+ cwd: string;
+ }>;
customDev: (files: Files) => Promise<{
port: number;
cwd: string;
@@ -272,6 +302,17 @@ export const test = base.extend({
stop?.();
},
// eslint-disable-next-line no-empty-pattern
+ viteDevDeno: async ({}, use) => {
+ let stop: (() => unknown) | undefined;
+ await use(async (files, template) => {
+ let port = await getPort();
+ let cwd = await createProject(await files({ port }), template);
+ stop = await viteDevDeno({ cwd, port });
+ return { port, cwd };
+ });
+ stop?.();
+ },
+ // eslint-disable-next-line no-empty-pattern
customDev: async ({}, use) => {
let stop: (() => unknown) | undefined;
await use(async (files) => {
@@ -331,6 +372,22 @@ function node(
return proc;
}
+function deno(
+ args: string[],
+ options: { cwd: string; env?: Record }
+) {
+ let proc = spawn(denoBin, args, {
+ cwd: options.cwd,
+ env: {
+ ...process.env,
+ ...colorEnv,
+ ...options.env,
+ },
+ stdio: "pipe",
+ });
+ return proc;
+}
+
async function waitForServer(
proc: ChildProcess & { stdout: Readable; stderr: Readable },
args: { port: number; basename?: string }
diff --git a/integration/vite-deno-test.ts b/integration/vite-deno-test.ts
new file mode 100644
index 00000000000..694dbf5eae9
--- /dev/null
+++ b/integration/vite-deno-test.ts
@@ -0,0 +1,90 @@
+import { expect } from "@playwright/test";
+import dedent from "dedent";
+
+import type { Files } from "./helpers/vite.js";
+import { test, viteConfig } from "./helpers/vite.js";
+
+const files: Files = async ({ port }) => ({
+ "vite.config.ts": dedent`
+ export default {
+ ${await viteConfig.server({ port })}
+ plugins: [
+ remix(),
+ ],
+ }
+ `,
+ "app/routes/_index.tsx": `
+ import { Form, useLoaderData } from "@remix-run/react";
+ import {
+ json,
+ type ActionFunctionArgs,
+ type LoaderFunctionArgs,
+ } from "@remix-run/server-runtime";
+
+ const key = "__my-key__";
+
+ export async function loader({ context }: LoaderFunctionArgs) {
+ const MY_KV = await Deno.openKv("test");
+ const { value } = await MY_KV.get([key]);
+ return json({ value, extra: context.extra });
+ }
+
+ export async function action({ request }: ActionFunctionArgs) {
+ const MY_KV = await Deno.openKv("test");
+
+ if (request.method === "POST") {
+ const formData = await request.formData();
+ const value = formData.get("value") as string;
+ await MY_KV.set([key], value);
+ return null;
+ }
+
+ if (request.method === "DELETE") {
+ await MY_KV.delete([key]);
+ return null;
+ }
+
+ throw new Error(\`Method not supported: "\${request.method}"\`);
+ }
+
+ export default function Index() {
+ const { value } = useLoaderData();
+ return (
+
+
Welcome to Remix
+ {value ? (
+ <>
+
Value: {value}
+
+ >
+ ) : (
+ <>
+
No value
+
+ >
+ )}
+
+ );
+ }
+ `,
+});
+
+test("vite dev", async ({ page, viteDevDeno }) => {
+ let { port } = await viteDevDeno(files, "vite-deno-template");
+ await page.goto(`http://localhost:${port}/`, {
+ waitUntil: "networkidle",
+ });
+ await expect(page.locator("[data-text]")).toHaveText("No value");
+
+ await page.getByLabel("Set value:").fill("my-value");
+ await page.getByRole("button").click();
+ await expect(page.locator("[data-text]")).toHaveText("Value: my-value");
+ expect(page.errors).toEqual([]);
+});
diff --git a/templates/deno/README.md b/templates/deno/README.md
index 0452077eab8..f4832579d4c 100644
--- a/templates/deno/README.md
+++ b/templates/deno/README.md
@@ -40,9 +40,6 @@ deno task dev
This starts your app in development mode, rebuilding assets on file changes.
-The server used for development is located in `server/development.ts`. It is an
-express server running Vite in middleware mode.
-
## Production
First, build your app for production:
@@ -57,7 +54,7 @@ Then run the app in production mode:
deno task start
```
-The server used for production is located in `server/production.ts`. It is
+The server used for production is located in `server.production.ts`. It is
served by `deno serve` for maximum performance.
## Deployment
diff --git a/templates/deno/deno.jsonc b/templates/deno/deno.jsonc
index ce0d6f55ab1..ee0008938d2 100644
--- a/templates/deno/deno.jsonc
+++ b/templates/deno/deno.jsonc
@@ -2,7 +2,7 @@
"tasks": {
"build": "deno run -A npm:@remix-run/dev@^2.11.2 vite:build",
"deploy": "deployctl deploy --prod --include=deno.jsonc,deno.lock,build,server --project= ./server/production.ts",
- "dev": "deno run -A --watch --watch-exclude='./vite.config.*.timestamp-*.mjs' ./server/development.ts",
+ "dev": "deno run -A npm:@remix-run/dev@^2.11.2 vite:dev",
"lint": "deno lint && deno run -A npm:eslint@^8.57.0 --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "deno serve -A --parallel ./server/production.ts",
"typecheck": "deno check '**/*' && deno run -A npm:typescript@^5.5.4/tsc",
diff --git a/templates/deno/server/production.ts b/templates/deno/server.production.ts
similarity index 90%
rename from templates/deno/server/production.ts
rename to templates/deno/server.production.ts
index af5f70bf36d..131a28433d3 100644
--- a/templates/deno/server/production.ts
+++ b/templates/deno/server.production.ts
@@ -3,8 +3,8 @@ import { serveFile } from "@std/http/file-server";
import { join } from "@std/path/join";
const handleRequest = createRequestHandler(
- await import("../build/server/index.js"),
- "production",
+ await import("./build/server/index.js"),
+ "production"
);
export default {
@@ -24,7 +24,7 @@ export default {
if (pathname.startsWith("/assets/")) {
response.headers.set(
"cache-control",
- "public, max-age=31536000, immutable",
+ "public, max-age=31536000, immutable"
);
} else {
response.headers.set("cache-control", "public, max-age=600");
diff --git a/templates/deno/server/development.ts b/templates/deno/server/development.ts
deleted file mode 100644
index edbf82364da..00000000000
--- a/templates/deno/server/development.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { createRequestHandler } from "@remix-run/express";
-import type { ServerBuild } from "@remix-run/server-runtime";
-import express from "express";
-import { createServer } from "vite";
-
-const PORT = Number(Deno.env.get("PORT")) || 8000;
-
-const app = express();
-const viteDevServer = await createServer({
- server: { middlewareMode: true },
-});
-app.use(viteDevServer.middlewares);
-app.use(express.static("build/client", { maxAge: "1h" }));
-
-app.all(
- "*",
- createRequestHandler({
- build: () =>
- viteDevServer.ssrLoadModule(
- "virtual:remix/server-build",
- ) as Promise,
- mode: "development",
- }),
-);
-
-app.listen(PORT, () => console.log(`👉 http://localhost:${PORT}`));