diff --git a/.eslintrc.js b/.eslintrc.js index 2061cd2..0ef9764 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,4 @@ /** @type {import('eslint').Linter.Config} */ -module.exports = { +export default { extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], }; diff --git a/app/entry.client.tsx b/app/entry.client.tsx index f1fed31..999c0a1 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -2,21 +2,11 @@ import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; -function hydrate() { - startTransition(() => { - hydrateRoot( - document, - - - - ); - }); -} - -if (window.requestIdleCallback) { - window.requestIdleCallback(hydrate); -} else { - // Safari doesn't support requestIdleCallback - // https://caniuse.com/requestidlecallback - window.setTimeout(hydrate, 1); -} +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/app/entry.server.tsx b/app/entry.server.tsx index fb8ea87..beacf24 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,21 +1 @@ -import type { EntryContext } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -import { renderToString } from "react-dom/server"; - -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, { - headers: responseHeaders, - status: responseStatusCode, - }); -} +export { handleRequest as default } from "@netlify/remix-adapter"; diff --git a/package.json b/package.json index 9b8ed36..225ba5a 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,14 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "predev": "rimraf ./public/_redirects", "dev": "remix dev", "start": "netlify serve", "typecheck": "tsc -b" }, "dependencies": { - "@netlify/functions": "^1.3.0", "@remix-run/node": "*", "@remix-run/react": "*", "cross-env": "^7.0.3", @@ -20,11 +19,10 @@ "@remix-run/dev": "*", "@remix-run/eslint-config": "*", "@remix-run/serve": "*", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", "eslint": "^8.27.0", - "rimraf": "^4.1.4", - "typescript": "^5.1.0" + "typescript": "^5.2.2" }, "engines": { "node": ">=18" diff --git a/remix.config.js b/remix.config.js index 77dbc31..bbe7ebd 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,22 +1,10 @@ -const baseConfig = - process.env.NODE_ENV === "production" - ? // when running the Netify CLI or building on Netlify, we want to use - { - server: "./server.js", - serverBuildPath: ".netlify/functions-internal/server.js", - } - : // otherwise support running remix dev, i.e. no custom server - undefined; +import { config } from "@netlify/remix-adapter"; /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - ...baseConfig, - ignoredRouteFiles: ["**/.*"], - // See https://remix.run/docs/en/main/file-conventions/route-files-v2 - future: { - v2_routeConvention: true, - } +export default { + ...(process.env.NODE_ENV === "production" ? config : undefined), + // This works out of the box with the Netlify adapter, but you can // add your own custom config here if you want to. // - // See https://remix.run/docs/en/v1/file-conventions/remix-config + // See https://remix.run/file-conventions/remix-config }; diff --git a/remix.init/README-edge.md b/remix.init/README-edge.md index 010d272..3c06738 100644 --- a/remix.init/README-edge.md +++ b/remix.init/README-edge.md @@ -40,26 +40,26 @@ npm install Run ```sh -netlify dev +npm run dev ``` Open up [http://localhost:8888](http://localhost:8888), and you're ready to go! ### Serve your site locally -Run +To serve your site locally in a production-like environment, run ```sh -npm netlify serve +npm run start ``` -to serve your site locally at [http://localhost:8888](http://localhost:8888). +Your site will be available at [http://localhost:8888](http://localhost:8888). Note that it will not auto-reload when you make changes. ## Excluding routes You can exclude routes for non-Remix code such as custom Netlify Functions or Edge Functions. To do this, add an additional entry in the array like in the example below: -````diff +```diff export const config = { cache: "manual", path: "/*", @@ -70,6 +70,7 @@ export const config = { - excluded_patterns: ["/_assets/*"], + excluded_patterns: ["/_assets/*", "/api/*"], }; +``` ## Deployment @@ -81,4 +82,4 @@ netlify deploy --build # production deployment netlify deploy --build --prod -```` +``` diff --git a/remix.init/README.md b/remix.init/README.md index 7e2979b..52a6a52 100644 --- a/remix.init/README.md +++ b/remix.init/README.md @@ -43,21 +43,21 @@ Run netlify dev ``` -Open up [http://localhost:8888](http://localhost:8888), and you're ready to go! +Open up [http://localhost:3000](http://localhost:3000), and you're ready to go! ### Adding Redirects and Rewrites -To add redirects and rewrites, add them to the `netlify.toml` file or to the [\_app_redirects](_app_redirects) file. For more information about redirects and rewrites, see the [Netlify docs](https://docs.netlify.com/routing/redirects/). +To add redirects and rewrites, add them to the `netlify.toml` file. For more information about redirects and rewrites, see the [Netlify docs](https://docs.netlify.com/routing/redirects/). ### Serve your site locally -Run +To serve your site locally in a production-like environment, run ```sh npm run start ``` -to serve your site locally at [http://localhost:8888](http://localhost:8888). +Your site will be available at [http://localhost:8888](http://localhost:8888). Note that it will not auto-reload when you make changes. ## Deployment diff --git a/remix.init/_app_redirects b/remix.init/_app_redirects deleted file mode 100644 index cf91099..0000000 --- a/remix.init/_app_redirects +++ /dev/null @@ -1,7 +0,0 @@ -# This template uses this file instead of the typicial Netlify _redirects file. -# For more information about redirects and rewrites, see https://docs.netlify.com/routing/redirects/. - -# Do not remove the line below. This is required to serve the site when deployed. -/* /.netlify/functions/server 200 - -# Add other redirects and rewrites here and/or in your netlify.toml diff --git a/remix.init/entry.server.tsx b/remix.init/entry.server.tsx index d6d0f0e..0ebf826 100644 --- a/remix.init/entry.server.tsx +++ b/remix.init/entry.server.tsx @@ -1,22 +1 @@ -import type { EntryContext } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -// Looking to use renderReadableStream? See https://github.com/netlify/remix-template/discussions/100 -import { renderToString } from "react-dom/server"; - -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, { - headers: responseHeaders, - status: responseStatusCode, - }); -} +export { handleRequest as default } from "@netlify/remix-edge-adapter"; diff --git a/remix.init/index.js b/remix.init/index.js index 0a62acc..838aa22 100644 --- a/remix.init/index.js +++ b/remix.init/index.js @@ -3,15 +3,15 @@ const fs = require("fs/promises"); const { join } = require("path"); const PackageJson = require("@npmcli/package-json"); const execa = require("execa"); -const { Command } = require('commander'); +const { Command } = require("commander"); -const foldersToExclude = [".github", ".git"]; +const foldersToExclude = [".github"]; // Netlify Edge Functions template file changes const edgeFilesToCopy = [ ["README-edge.md", "README.md"], ["netlify-edge-toml", "netlify.toml"], - ["server.js"], + ["server.ts"], ["remix.config.js"], ["entry.server.tsx", "app/entry.server.tsx"], ["root.tsx", "app/root.tsx"], @@ -22,10 +22,9 @@ const edgeFilesToCopy = [ const filesToCopy = [ ["README.md"], ["netlify-toml", "netlify.toml"], - ["_app_redirects"], + ["redirects", ".redirects"], ]; - async function copyTemplateFiles({ files, rootDirectory }) { for (const [file, target] of files) { let sourceFile = file; @@ -41,10 +40,7 @@ async function copyTemplateFiles({ files, rootDirectory }) { async function updatePackageJsonForEdge(directory) { const packageJson = await PackageJson.load(directory); const { - dependencies: { - "@remix-run/node": _node, - ...dependencies - }, + dependencies: { "@remix-run/node": _node, ...dependencies }, scripts, ...restOfPackageJson } = packageJson.content; @@ -53,13 +49,14 @@ async function updatePackageJsonForEdge(directory) { // dev script is the same as the start script for Netlify Edge, "cross-env NODE_ENV=production netlify dev" scripts: { ...scripts, - predev: "rimraf ./.netlify/edge-functions/", + dev: 'remix dev --manual -c "ntl dev --framework=#static"', }, ...restOfPackageJson, dependencies: { ...dependencies, "@netlify/edge-functions": "^2.0.0", - "@netlify/remix-edge-adapter": "1.2.0", + "@netlify/remix-edge-adapter": "^3.0.0", + "@netlify/remix-runtime": "^2.0.0", }, }); @@ -69,19 +66,25 @@ async function updatePackageJsonForEdge(directory) { async function updatePackageJsonForFunctions(directory) { const packageJson = await PackageJson.load(directory); const { - dependencies: { - "@remix-run/node": _node, - ...dependencies - }, + dependencies: { "@remix-run/node": _node, ...dependencies }, scripts, ...restOfPackageJson } = packageJson.content; packageJson.update({ ...restOfPackageJson, + scripts: { + ...scripts, + build: "npm run redirects:enable && remix build", + dev: "npm run redirects:disable && remix dev", + "redirects:enable": "shx cp .redirects public/_redirects", + "redirects:disable": "shx rm -f public/_redirects", + }, dependencies: { ...dependencies, - "@netlify/remix-adapter": "^1.0.0", + "@netlify/functions": "^2.0.0", + "@netlify/remix-adapter": "^2.0.0", + shx: "^0.3.4", }, }); @@ -104,7 +107,24 @@ async function removeNonTemplateFiles({ rootDirectory, folders }) { } } -async function main({ rootDirectory }) { +async function installAdditionalDependencies({ + rootDirectory, + packageManager, +}) { + try { + console.log(`Installing additional dependencies with ${packageManager}.`); + const npmInstall = await execa(packageManager, ["install"], { + cwd: rootDirectory, + stdio: "inherit", + }); + } catch (e) { + console.log( + `Unable to install additional packages. Run ${packageManager} install in the root of the new project, "${rootDirectory}".` + ); + } +} + +async function main({ rootDirectory, packageManager }) { await removeNonTemplateFiles({ rootDirectory, folders: foldersToExclude, @@ -116,6 +136,7 @@ async function main({ rootDirectory }) { rootDirectory, }); await updatePackageJsonForFunctions(rootDirectory); + await installAdditionalDependencies({ rootDirectory, packageManager }); return; } @@ -126,30 +147,28 @@ async function main({ rootDirectory }) { await updatePackageJsonForEdge(rootDirectory); - // The Netlify Edge Functions template has different and additional dependencies to install. - try { - console.log("installing additional npm packages..."); - const npmInstall = await execa("npm", ["install"], { cwd: rootDirectory }); - console.log(npmInstall.stdout); - } catch (e) { - console.log( - `Unable to install additional packages. Run npm install in the root of the new project, "${rootDirectory}".` - ); - } + await installAdditionalDependencies({ rootDirectory, packageManager }); } async function shouldUseEdge() { - // parse the top level command args to see if edge was passed in const program = new Command(); program - .option('--netlify-edge', 'explicitly use Netlify Edge Functions to serve this Remix site.', undefined) - .option('--no-netlify-edge', 'explicitly do NOT use Netlify Edge Functions to serve this Remix site - use Serverless Functions instead.', undefined) + .option( + "--netlify-edge", + "explicitly use Netlify Edge Functions to serve this Remix site.", + undefined + ) + .option( + "--no-netlify-edge", + "explicitly do NOT use Netlify Edge Functions to serve this Remix site - use Serverless Functions instead.", + undefined + ); program.allowUnknownOption().parse(); const passedEdgeOption = program.opts().netlifyEdge; - if(passedEdgeOption !== true && passedEdgeOption !== false){ + if (passedEdgeOption !== true && passedEdgeOption !== false) { const { edge } = await inquirer.prompt([ { name: "edge", diff --git a/remix.init/netlify-edge-toml b/remix.init/netlify-edge-toml index 13021f6..3525653 100644 --- a/remix.init/netlify-edge-toml +++ b/remix.init/netlify-edge-toml @@ -2,6 +2,11 @@ command = "npm run build" publish = "public" -[dev] -command = "npm run dev" -targetPort = 3000 +# Set immutable caching for static files, because they have fingerprinted filenames + +[[headers]] +for = "/build/*" + +[headers.values] + +"Cache-Control" = "public, max-age=31560000, immutable" diff --git a/remix.init/netlify-toml b/remix.init/netlify-toml index 630bab8..e27db6b 100644 --- a/remix.init/netlify-toml +++ b/remix.init/netlify-toml @@ -1,14 +1,16 @@ [build] -command = "remix build && cp _app_redirects public/_redirects" +command = "npm run build" publish = "public" [dev] command = "npm run dev" targetPort = 3000 +# Set immutable caching for static files, because they have fingerprinted filenames + [[headers]] for = "/build/*" [headers.values] -# Set to 60 seconds as an example. You can also add cache headers via Remix. See the documentation on [headers](https://remix.run/docs/en/v1/route/headers) in Remix. -"Cache-Control" = "public, max-age=60, s-maxage=60" + +"Cache-Control" = "public, max-age=31560000, immutable" diff --git a/remix.init/redirects b/remix.init/redirects new file mode 100644 index 0000000..eb6d445 --- /dev/null +++ b/remix.init/redirects @@ -0,0 +1,4 @@ +# This file is copied into the public folder during the build step and removed during dev. +# If you need to add your own redirects, add them in netlify.toml or they will be overwritten. +# Do not remove the line below. This is required to serve the site when deployed. +/* /.netlify/functions/server 200 \ No newline at end of file diff --git a/remix.init/remix.config.js b/remix.init/remix.config.js index 031471b..6113a42 100644 --- a/remix.init/remix.config.js +++ b/remix.init/remix.config.js @@ -1,16 +1,10 @@ -const { config } = require("@netlify/remix-edge-adapter"); -const baseConfig = - process.env.NODE_ENV === "production" - ? config - : { ignoredRouteFiles: ["**/.*"], future: config.future }; +import { config } from '@netlify/remix-edge-adapter' -/** - * @type {import('@remix-run/dev').AppConfig} - */ -module.exports = { - ...baseConfig, +/** @type {import('@remix-run/dev').AppConfig} */ +export default { + ...config, // This works out of the box with the Netlify adapter, but you can // add your own custom config here if you want to. // - // See https://remix.run/docs/en/v1/file-conventions/remix-config -}; + // See https://remix.run/file-conventions/remix-config +} diff --git a/remix.init/root.tsx b/remix.init/root.tsx index 95772f0..d360df5 100644 --- a/remix.init/root.tsx +++ b/remix.init/root.tsx @@ -1,4 +1,5 @@ -import type { MetaFunction } from "@netlify/remix-runtime"; +import { cssBundleHref } from "@remix-run/css-bundle"; +import type { LinksFunction } from "@netlify/remix-runtime"; import { Links, LiveReload, @@ -8,18 +9,16 @@ import { ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => [ - { - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", - } +export const links: LinksFunction = () => [ + ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; export default function App() { return ( + + diff --git a/remix.init/server.js b/remix.init/server.ts similarity index 69% rename from remix.init/server.js rename to remix.init/server.ts index 18dc1b5..f73b344 100644 --- a/remix.init/server.js +++ b/remix.init/server.ts @@ -1,6 +1,6 @@ -// Import path interpreted by the Remix compiler import * as build from "@remix-run/dev/server-build"; import { createRequestHandler } from "@netlify/remix-edge-adapter"; +import { broadcastDevReady } from "@netlify/remix-runtime"; export default createRequestHandler({ build, @@ -8,6 +8,11 @@ export default createRequestHandler({ mode: process.env.NODE_ENV, }); +if (process.env.NODE_ENV === "development") { + // Tell remix dev that the server is ready + broadcastDevReady(build); +} + export const config = { cache: "manual", path: "/*", @@ -15,5 +20,5 @@ export const config = { // // Add other exclusions here, e.g. "^/api/*$" for custom Netlify functions or // custom Netlify Edge Functions - excluded_patterns: ["^/_assets/*$"], + excludedPath: ["/build/*", "/favicon.ico"], }; diff --git a/server.js b/server.ts similarity index 71% rename from server.js rename to server.ts index 6997ce3..d1d6d95 100644 --- a/server.js +++ b/server.ts @@ -1,7 +1,9 @@ -import { createRequestHandler } from "@netlify/remix-adapter"; import * as build from "@remix-run/dev/server-build"; +import { createRequestHandler } from "@netlify/remix-adapter"; -export const handler = createRequestHandler({ +const handler = createRequestHandler({ build, mode: process.env.NODE_ENV, }); + +export default handler; diff --git a/tsconfig.json b/tsconfig.json index d8ee23a..2846d2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,13 @@ "remix.env.d.ts", "**/*.ts", "**/*.tsx", - "server.js", - "remix.init/server.js" ], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2019"], + "lib": [ + "DOM", + "DOM.Iterable", + "ES2019" + ], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", @@ -20,10 +22,11 @@ "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { - "~/*": ["./app/*"] + "~/*": [ + "./app/*" + ] }, - // Remix takes care of building everything in `remix build`. "noEmit": true } -} +} \ No newline at end of file