diff --git a/templates/arc/.eslintrc.js b/templates/arc/.eslintrc.cjs similarity index 100% rename from templates/arc/.eslintrc.js rename to templates/arc/.eslintrc.cjs diff --git a/templates/arc/.gitignore b/templates/arc/.gitignore index 592d769e5d8..c8f4835d63e 100644 --- a/templates/arc/.gitignore +++ b/templates/arc/.gitignore @@ -1,7 +1,9 @@ +.DS_Store node_modules /.cache -/server/index.js +/server/index.mjs +/server/index.mjs.map /public/build preferences.arc sam.json diff --git a/templates/arc/README.md b/templates/arc/README.md index bf6f90a08d3..2c43f6611c6 100644 --- a/templates/arc/README.md +++ b/templates/arc/README.md @@ -4,6 +4,18 @@ ## Development +Create a `preferences.arc` file in the root with the following contents: + +``` +@sandbox +livereload false + +# NODE_ENV development is required when running the dev server +@env +testing + NODE_ENV development +``` + The following command will run two processes during development when using Architect as your server. - Your Architect server sandbox diff --git a/templates/arc/app.arc b/templates/arc/app.arc index 4e472fca56b..79428bd94fe 100644 --- a/templates/arc/app.arc +++ b/templates/arc/app.arc @@ -8,6 +8,10 @@ remix-architect-app @static +@plugins +plugin-remix + src plugin-remix.js + # @aws # profile default # region us-west-1 diff --git a/templates/arc/app/root.tsx b/templates/arc/app/root.tsx index 8cb74a167f8..f10a63cf481 100644 --- a/templates/arc/app/root.tsx +++ b/templates/arc/app/root.tsx @@ -19,6 +19,7 @@ export default function App() { + diff --git a/templates/arc/package.json b/templates/arc/package.json index e2d5ad3866b..90233f2cab3 100644 --- a/templates/arc/package.json +++ b/templates/arc/package.json @@ -1,11 +1,10 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "dev:remix": "remix watch", - "dev:arc": "cross-env NODE_ENV=development arc sandbox", - "dev": "npm-run-all build --parallel \"dev:*\"", + "dev": "remix dev -c \"arc sandbox -e testing\"", "start": "cross-env NODE_ENV=production arc sandbox", "typecheck": "tsc" }, @@ -20,13 +19,12 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@architect/architect": "^10.11.2", + "@architect/architect": "^10.12.1", "@remix-run/dev": "*", "@remix-run/eslint-config": "*", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "eslint": "^8.38.0", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4" }, "engines": { diff --git a/templates/arc/plugin-remix.js b/templates/arc/plugin-remix.js new file mode 100644 index 00000000000..4de7ebaf6df --- /dev/null +++ b/templates/arc/plugin-remix.js @@ -0,0 +1,37 @@ +// This should eventually be a npm package, but for now it lives here. +// It's job is to notify the remix dev server of the version of the running +// app to trigger HMR / HDR. + +import * as fs from "node:fs"; +import * as path from "node:path"; +import { logDevReady } from "@remix-run/node"; + +const buildPath = "server/index.mjs"; + +let lastTimeout; + +export default { + sandbox: { + async watcher() { + if (lastTimeout) { + clearTimeout(lastTimeout); + } + + lastTimeout = setTimeout(async () => { + const contents = fs.readFileSync( + path.resolve(process.cwd(), buildPath), + "utf8" + ); + const manifestMatches = contents.matchAll(/manifest-([A-f0-9]+)\.js/g); + const sent = new Set(); + for (const match of manifestMatches) { + const buildHash = match[1]; + if (!sent.has(buildHash)) { + sent.add(buildHash); + logDevReady({ assets: { version: buildHash } }); + } + } + }, 300); + }, + }, +}; diff --git a/templates/arc/remix.config.js b/templates/arc/remix.config.js index b76efda0f3a..c0345763aae 100644 --- a/templates/arc/remix.config.js +++ b/templates/arc/remix.config.js @@ -1,12 +1,14 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { +export default { ignoredRouteFiles: ["**/.*"], publicPath: "/_static/build/", - server: "./server.ts", - serverBuildPath: "server/index.js", + server: "server.ts", + serverBuildPath: "server/index.mjs", // appDirectory: "app", // assetsBuildDirectory: "public/build", + serverModuleFormat: "esm", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/arc/server/config.arc b/templates/arc/server/config.arc index 3d11ce0fc30..990ffe2fbe5 100644 --- a/templates/arc/server/config.arc +++ b/templates/arc/server/config.arc @@ -1,5 +1,5 @@ @aws -runtime nodejs14.x +runtime nodejs16.x # memory 1152 # timeout 30 # concurrency 1 diff --git a/templates/cloudflare-pages/.eslintrc.js b/templates/cloudflare-pages/.eslintrc.cjs similarity index 100% rename from templates/cloudflare-pages/.eslintrc.js rename to templates/cloudflare-pages/.eslintrc.cjs diff --git a/templates/cloudflare-pages/.gitignore b/templates/cloudflare-pages/.gitignore index 7c0736ebf5a..e542b26b368 100644 --- a/templates/cloudflare-pages/.gitignore +++ b/templates/cloudflare-pages/.gitignore @@ -1,3 +1,4 @@ +.DS_Store node_modules /.cache diff --git a/templates/cloudflare-pages/package.json b/templates/cloudflare-pages/package.json index 7e91782b468..982c8cd68ce 100644 --- a/templates/cloudflare-pages/package.json +++ b/templates/cloudflare-pages/package.json @@ -1,21 +1,18 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "dev:remix": "remix watch", - "dev:wrangler": "cross-env NODE_ENV=development npm run wrangler", - "dev": "npm-run-all build --parallel \"dev:*\"", - "start": "cross-env NODE_ENV=production npm run wrangler", - "typecheck": "tsc", - "wrangler": "wrangler pages dev ./public" + "dev": "remix dev --no-restart -c \"npm run start\"", + "start": "wrangler pages dev --compatibility-date=2023-06-21 ./public", + "typecheck": "tsc" }, "dependencies": { "@remix-run/cloudflare": "*", "@remix-run/cloudflare-pages": "*", "@remix-run/css-bundle": "*", "@remix-run/react": "*", - "cross-env": "^7.0.3", "isbot": "^3.6.8", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -27,9 +24,8 @@ "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "eslint": "^8.38.0", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4", - "wrangler": "^2.15.1" + "wrangler": "^3.1.1" }, "engines": { "node": ">=16.13.0" diff --git a/templates/cloudflare-pages/public/_headers b/templates/cloudflare-pages/public/_headers index dd69b93b392..c5129f35cd3 100644 --- a/templates/cloudflare-pages/public/_headers +++ b/templates/cloudflare-pages/public/_headers @@ -1,2 +1,4 @@ +/favicon.ico + Cache-Control: public, max-age=3600, s-maxage=3600 /build/* Cache-Control: public, max-age=31536000, immutable diff --git a/templates/cloudflare-pages/public/_routes.json b/templates/cloudflare-pages/public/_routes.json index 5826b059c08..4b57270dae9 100644 --- a/templates/cloudflare-pages/public/_routes.json +++ b/templates/cloudflare-pages/public/_routes.json @@ -1,5 +1,5 @@ { "version": 1, "include": ["/*"], - "exclude": ["/build/*"] + "exclude": ["/favicon.ico", "/build/*"] } diff --git a/templates/cloudflare-pages/remix.config.js b/templates/cloudflare-pages/remix.config.js index 6f5ffe0fffe..d31efea86fe 100644 --- a/templates/cloudflare-pages/remix.config.js +++ b/templates/cloudflare-pages/remix.config.js @@ -1,5 +1,5 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { +export default { devServerBroadcastDelay: 1000, ignoredRouteFiles: ["**/.*"], server: "./server.ts", @@ -14,6 +14,7 @@ module.exports = { // assetsBuildDirectory: "public/build", // publicPath: "/build/", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/cloudflare-pages/server.ts b/templates/cloudflare-pages/server.ts index b1681c47979..d8f4dcf1425 100644 --- a/templates/cloudflare-pages/server.ts +++ b/templates/cloudflare-pages/server.ts @@ -1,8 +1,13 @@ +import { logDevReady } from "@remix-run/cloudflare"; import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; import * as build from "@remix-run/dev/server-build"; +if (process.env.NODE_ENV === "development") { + logDevReady(build); +} + export const onRequest = createPagesFunctionHandler({ build, - getLoadContext: (context) => context.env, + getLoadContext: (context) => ({ env: context.env }), mode: process.env.NODE_ENV, }); diff --git a/templates/cloudflare-pages/wrangler.toml b/templates/cloudflare-pages/wrangler.toml deleted file mode 100644 index ecd695e5a65..00000000000 --- a/templates/cloudflare-pages/wrangler.toml +++ /dev/null @@ -1,2 +0,0 @@ -compatibility_date = "2022-04-05" -compatibility_flags = ["streams_enable_constructors"] diff --git a/templates/cloudflare-workers/.eslintrc.js b/templates/cloudflare-workers/.eslintrc.cjs similarity index 100% rename from templates/cloudflare-workers/.eslintrc.js rename to templates/cloudflare-workers/.eslintrc.cjs diff --git a/templates/cloudflare-workers/.gitignore b/templates/cloudflare-workers/.gitignore index f0421bd7025..7c29f45a232 100644 --- a/templates/cloudflare-workers/.gitignore +++ b/templates/cloudflare-workers/.gitignore @@ -1,3 +1,4 @@ +.DS_Store node_modules /.cache diff --git a/templates/cloudflare-workers/package.json b/templates/cloudflare-workers/package.json index 9ca72d4bfcb..7cca279a472 100644 --- a/templates/cloudflare-workers/package.json +++ b/templates/cloudflare-workers/package.json @@ -1,21 +1,18 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "deploy": "wrangler publish", - "dev:remix": "remix watch", - "dev:miniflare": "cross-env NODE_ENV=development miniflare ./build/index.js --watch", - "dev": "npm-run-all build --parallel \"dev:*\"", - "start": "cross-env NODE_ENV=production miniflare ./build/index.js", + "deploy": "remix build && wrangler publish", + "dev": "remix dev --no-restart -c \"npm start\"", + "start": "wrangler dev ./build/index.js", "typecheck": "tsc" }, "dependencies": { "@remix-run/cloudflare": "*", - "@remix-run/cloudflare-workers": "*", "@remix-run/css-bundle": "*", "@remix-run/react": "*", - "cross-env": "^7.0.3", "isbot": "^3.6.8", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -27,10 +24,8 @@ "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "eslint": "^8.38.0", - "miniflare": "^2.13.0", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4", - "wrangler": "^2.15.1" + "wrangler": "^3.1.1" }, "engines": { "node": ">=16.13.0" diff --git a/templates/cloudflare-workers/remix.config.js b/templates/cloudflare-workers/remix.config.js index c4210dd979a..b971b575863 100644 --- a/templates/cloudflare-workers/remix.config.js +++ b/templates/cloudflare-workers/remix.config.js @@ -1,10 +1,12 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - devServerBroadcastDelay: 1000, +export default { ignoredRouteFiles: ["**/.*"], server: "./server.ts", serverConditions: ["worker"], - serverDependenciesToBundle: "all", + serverDependenciesToBundle: [ + // bundle verything except the virtual module for the static content manifest provided by wrangler + /^(?!.*\b__STATIC_CONTENT_MANIFEST\b).*$/, + ], serverMainFields: ["browser", "module", "main"], serverMinify: true, serverModuleFormat: "esm", @@ -14,6 +16,7 @@ module.exports = { // serverBuildPath: "build/index.js", // publicPath: "/build/", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/cloudflare-workers/remix.env.d.ts b/templates/cloudflare-workers/remix.env.d.ts index 425870ae632..b5be9ba3bbe 100644 --- a/templates/cloudflare-workers/remix.env.d.ts +++ b/templates/cloudflare-workers/remix.env.d.ts @@ -1,3 +1,8 @@ /// /// /// + +declare module "__STATIC_CONTENT_MANIFEST" { + const manifest: string; + export default manifest; +} diff --git a/templates/cloudflare-workers/server.ts b/templates/cloudflare-workers/server.ts index 4f4b2ad0aff..d80fdf4cd2c 100644 --- a/templates/cloudflare-workers/server.ts +++ b/templates/cloudflare-workers/server.ts @@ -1,7 +1,53 @@ -import { createEventHandler } from "@remix-run/cloudflare-workers"; +import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; +import type { AppLoadContext } from "@remix-run/cloudflare"; +import { createRequestHandler, logDevReady } from "@remix-run/cloudflare"; import * as build from "@remix-run/dev/server-build"; +import __STATIC_CONTENT_MANIFEST from "__STATIC_CONTENT_MANIFEST"; -addEventListener( - "fetch", - createEventHandler({ build, mode: process.env.NODE_ENV }) -); +const MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST); +const handleRemixRequest = createRequestHandler(build, process.env.NODE_ENV); + +if (build.dev) { + logDevReady(build); +} + +export default { + async fetch( + request: Request, + env: { + __STATIC_CONTENT: Fetcher; + }, + ctx: ExecutionContext + ): Promise { + try { + const url = new URL(request.url); + const ttl = url.pathname.startsWith("/build/") + ? 60 * 60 * 24 * 365 // 1 year + : 60 * 5; // 5 minutes + return await getAssetFromKV( + { + request, + waitUntil: ctx.waitUntil.bind(ctx), + } as FetchEvent, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: MANIFEST, + cacheControl: { + browserTTL: ttl, + edgeTTL: ttl, + }, + } + ); + } catch (error) {} + + try { + const loadContext: AppLoadContext = { + env, + }; + return await handleRemixRequest(request, loadContext); + } catch (error) { + console.log(error); + return new Response("An unexpected error occurred", { status: 500 }); + } + }, +}; diff --git a/templates/cloudflare-workers/wrangler.toml b/templates/cloudflare-workers/wrangler.toml index 6dae0da58da..b4ddc4387f8 100644 --- a/templates/cloudflare-workers/wrangler.toml +++ b/templates/cloudflare-workers/wrangler.toml @@ -3,12 +3,7 @@ name = "remix-cloudflare-workers" workers_dev = true main = "./build/index.js" # https://developers.cloudflare.com/workers/platform/compatibility-dates -compatibility_date = "2022-04-05" -compatibility_flags = ["streams_enable_constructors"] +compatibility_date = "2023-04-20" [site] bucket = "./public" - -[build] - command = "npm run build" - diff --git a/templates/express/.eslintrc.js b/templates/express/.eslintrc.cjs similarity index 100% rename from templates/express/.eslintrc.js rename to templates/express/.eslintrc.cjs diff --git a/templates/express/package.json b/templates/express/package.json index e13c9097316..486f497a099 100644 --- a/templates/express/package.json +++ b/templates/express/package.json @@ -1,11 +1,10 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "dev": "npm-run-all build --parallel \"dev:*\"", - "dev:node": "cross-env NODE_ENV=development nodemon --require dotenv/config ./server.js --watch ./server.js", - "dev:remix": "remix watch", + "dev": "remix dev --no-restart -c \"node server.js\"", "start": "cross-env NODE_ENV=production node ./server.js", "typecheck": "tsc" }, @@ -30,10 +29,8 @@ "@types/morgan": "^1.9.4", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", - "dotenv": "^16.0.3", + "chokidar": "^3.5.3", "eslint": "^8.38.0", - "nodemon": "^2.0.22", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4" }, "engines": { diff --git a/templates/express/remix.config.js b/templates/express/remix.config.js index 29287a0d890..378d0fc051f 100644 --- a/templates/express/remix.config.js +++ b/templates/express/remix.config.js @@ -1,12 +1,13 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { +export default { ignoredRouteFiles: ["**/.*"], // appDirectory: "app", // assetsBuildDirectory: "public/build", // serverBuildPath: "build/index.js", // publicPath: "/build/", - serverModuleFormat: "cjs", + serverModuleFormat: "esm", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/express/server.js b/templates/express/server.js index 8d3d41a639d..10b59924ea4 100644 --- a/templates/express/server.js +++ b/templates/express/server.js @@ -1,14 +1,19 @@ -const path = require("path"); +import * as fs from "node:fs"; -const { createRequestHandler } = require("@remix-run/express"); -const { installGlobals } = require("@remix-run/node"); -const compression = require("compression"); -const express = require("express"); -const morgan = require("morgan"); +import { createRequestHandler } from "@remix-run/express"; +import { broadcastDevReady, installGlobals } from "@remix-run/node"; +import chokidar from "chokidar"; +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; installGlobals(); -const BUILD_DIR = path.join(process.cwd(), "build"); +const BUILD_PATH = "./build/index.js"; +/** + * @type { import('@remix-run/node').ServerBuild | Promise } + */ +let build = await import(BUILD_PATH); const app = express(); @@ -32,34 +37,42 @@ app.use(morgan("tiny")); app.all( "*", process.env.NODE_ENV === "development" - ? (req, res, next) => { - purgeRequireCache(); - - return createRequestHandler({ - build: require(BUILD_DIR), - mode: process.env.NODE_ENV, - })(req, res, next); - } + ? createDevRequestHandler() : createRequestHandler({ - build: require(BUILD_DIR), + build, mode: process.env.NODE_ENV, }) ); -const port = process.env.PORT || 3000; -app.listen(port, () => { +const port = process.env.PORT || 3000; +app.listen(port, async () => { console.log(`Express server listening on port ${port}`); + + if (process.env.NODE_ENV === "development") { + broadcastDevReady(build); + } }); -function purgeRequireCache() { - // purge require cache on requests for "server side HMR" this won't let - // you have in-memory objects between requests in development, - // alternatively you can set up nodemon/pm2-dev to restart the server on - // file changes, but then you'll have to reconnect to databases/etc on each - // change. We prefer the DX of this, so we've included it for you by default - for (const key in require.cache) { - if (key.startsWith(BUILD_DIR)) { - delete require.cache[key]; +function createDevRequestHandler() { + const watcher = chokidar.watch(BUILD_PATH, { ignoreInitial: true }); + + watcher.on("all", async () => { + // 1. purge require cache && load updated server build + const stat = fs.statSync(BUILD_PATH); + build = import(BUILD_PATH + "?t=" + stat.mtimeMs); + // 2. tell dev server that this app server is now ready + broadcastDevReady(await build); + }); + + return async (req, res, next) => { + try { + // + return createRequestHandler({ + build: await build, + mode: "development", + })(req, res, next); + } catch (error) { + next(error); } - } + }; } diff --git a/templates/fly/remix.config.js b/templates/fly/remix.config.js index cf359205c5e..60aa1cad1d6 100644 --- a/templates/fly/remix.config.js +++ b/templates/fly/remix.config.js @@ -5,7 +5,9 @@ module.exports = { // assetsBuildDirectory: "public/build", // serverBuildPath: "build/index.js", // publicPath: "/build/", + serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/netlify/netlify.toml b/templates/netlify/netlify.toml index 6e33122159d..2ec142b0b1f 100644 --- a/templates/netlify/netlify.toml +++ b/templates/netlify/netlify.toml @@ -2,10 +2,6 @@ command = "remix build" publish = "public" -[dev] - command = "remix watch" - port = 3000 - [[redirects]] from = "/*" to = "/.netlify/functions/server" diff --git a/templates/netlify/package.json b/templates/netlify/package.json index 904d242a7ea..16d1d91e585 100644 --- a/templates/netlify/package.json +++ b/templates/netlify/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "remix build", "dev": "remix dev", - "start": "cross-env NODE_ENV=production netlify dev", + "start": "netlify serve", "typecheck": "tsc" }, "dependencies": { diff --git a/templates/netlify/remix.config.js b/templates/netlify/remix.config.js index c510d3cc2d2..f54a5a8d198 100644 --- a/templates/netlify/remix.config.js +++ b/templates/netlify/remix.config.js @@ -9,7 +9,9 @@ module.exports = { // appDirectory: "app", // assetsBuildDirectory: "public/build", // publicPath: "/build/", + serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/remix/remix.config.js b/templates/remix/remix.config.js index 29287a0d890..60aa1cad1d6 100644 --- a/templates/remix/remix.config.js +++ b/templates/remix/remix.config.js @@ -7,6 +7,7 @@ module.exports = { // publicPath: "/build/", serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/vercel/remix.config.js b/templates/vercel/remix.config.js index 79e2b95fd7e..f688ad4857b 100644 --- a/templates/vercel/remix.config.js +++ b/templates/vercel/remix.config.js @@ -9,7 +9,9 @@ module.exports = { // appDirectory: "app", // assetsBuildDirectory: "public/build", // publicPath: "/build/", + serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true,