├── .eslintrc ├── .gitignore ├── README.md ├── app ├── components │ ├── lazy-context.ts │ └── lazy.tsx ├── entry.client.tsx ├── entry.server.tsx ├── root.tsx └── routes │ ├── index.tsx │ ├── lazy.tsx │ └── upgrading.tsx ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── server.js └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | /.cache 5 | /build 6 | /public/build 7 | .env 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Development 6 | 7 | You'll need to run two terminals (or bring in a process manager like concurrently/pm2-dev if you like): 8 | 9 | Start the Remix development asset server 10 | 11 | ```sh 12 | npm run dev 13 | ``` 14 | 15 | This starts your app in development mode, which will purge the server require cache when Remix rebuilds assets so you don't need a process manager restarting the express server. 16 | 17 | ## Deployment 18 | 19 | First, build your app for production: 20 | 21 | ```sh 22 | npm run build 23 | ``` 24 | 25 | Then run the app in production mode: 26 | 27 | ```sh 28 | npm start 29 | ``` 30 | 31 | Now you'll need to pick a host to deploy it to. 32 | 33 | ### DIY 34 | 35 | If you're familiar with deploying express applications you should be right at home just make sure to deploy the output of `remix build` 36 | 37 | - `server/build/` 38 | - `public/build/` 39 | 40 | ### Using a Template 41 | 42 | When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server. 43 | 44 | ```sh 45 | cd .. 46 | # create a new project, and pick a pre-configured host 47 | npx create-remix@latest 48 | cd my-new-remix-app 49 | # remove the new project's app (not the old one!) 50 | rm -rf app 51 | # copy your app over 52 | cp -R ../my-old-remix-app/app app 53 | ``` 54 | -------------------------------------------------------------------------------- /app/components/lazy-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export let Context = createContext<{ promise: Promise | null }>({ 4 | promise: null, 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/lazy.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { Context } from "./lazy-context"; 3 | 4 | export default function Lazy() { 5 | let { promise } = useContext(Context); 6 | if (promise) throw promise; 7 | 8 | return ( 9 | <> 10 |
11 | {/* Main */} 12 |
13 | {/* Preview */} 14 |
15 |

Preview

16 |

17 | Sed ultricies dolor non ante vulputate hendrerit. Vivamus sit amet 18 | suscipit sapien. Nulla iaculis eros a elit pharetra egestas. 19 |

20 |
21 |
22 | 29 | 36 | 37 |
38 |
39 | 48 |
49 |
50 |
51 | {/* ./ Preview */} 52 | {/* Typography*/} 53 |
54 |

Typography

55 |

56 | Aliquam lobortis vitae nibh nec rhoncus. Morbi mattis neque eget 57 | efficitur feugiat. Vivamus porta nunc a erat mattis, mattis 58 | feugiat turpis pretium. Quisque sed tristique felis. 59 |

60 | {/* Blockquote*/} 61 |
62 | "Maecenas vehicula metus tellus, vitae congue turpis hendrerit 63 | non. Nam at dui sit amet ipsum cursus ornare." 64 |
65 | - Phasellus eget lacinia 66 |
67 |
68 | {/* Lists*/} 69 |

Lists

70 |
    71 |
  • Aliquam lobortis lacus eu libero ornare facilisis.
  • 72 |
  • Nam et magna at libero scelerisque egestas.
  • 73 |
  • Suspendisse id nisl ut leo finibus vehicula quis eu ex.
  • 74 |
  • Proin ultricies turpis et volutpat vehicula.
  • 75 |
76 | {/* Inline text elements*/} 77 |

Inline text elements

78 |
79 |

80 | Primary link 81 |

82 |

83 | 84 | Secondary link 85 | 86 |

87 |

88 | 89 | Contrast link 90 | 91 |

92 |
93 |
94 |

95 | Bold 96 |

97 |

98 | Italic 99 |

100 |

101 | Underline 102 |

103 |
104 |
105 |

106 | Deleted 107 |

108 |

109 | Inserted 110 |

111 |

112 | Strikethrough 113 |

114 |
115 |
116 |

117 | Small 118 |

119 |

120 | Text Sub 121 |

122 |

123 | Text Sup 124 |

125 |
126 |
127 |

128 | 129 | Abbr. 130 | 131 |

132 |

133 | Kbd 134 |

135 |

136 | Highlighted 137 |

138 |
139 | {/* Headings*/} 140 |

Heading 3

141 |

142 | Integer bibendum malesuada libero vel eleifend. Fusce iaculis 143 | turpis ipsum, at efficitur sem scelerisque vel. Aliquam auctor 144 | diam ut purus cursus fringilla. Class aptent taciti sociosqu ad 145 | litora torquent per conubia nostra, per inceptos himenaeos. 146 |

147 |

Heading 4

148 |

149 | Cras fermentum velit vitae auctor aliquet. Nunc non congue urna, 150 | at blandit nibh. Donec ac fermentum felis. Vivamus tincidunt arcu 151 | ut lacus hendrerit, eget mattis dui finibus. 152 |

153 |
Heading 5
154 |

155 | Donec nec egestas nulla. Sed varius placerat felis eu suscipit. 156 | Mauris maximus ante in consequat luctus. Morbi euismod sagittis 157 | efficitur. Aenean non eros orci. Vivamus ut diam sem. 158 |

159 |
Heading 6
160 |

161 | Ut sed quam non mauris placerat consequat vitae id risus. 162 | Vestibulum tincidunt nulla ut tortor posuere, vitae malesuada 163 | tortor molestie. Sed nec interdum dolor. Vestibulum id auctor 164 | nisi, a efficitur sem. Aliquam sollicitudin efficitur turpis, 165 | sollicitudin hendrerit ligula semper id. Nunc risus felis, egestas 166 | eu tristique eget, convallis in velit. 167 |

168 | {/* Medias*/} 169 |
170 | Minimal landscape 174 |
175 | Image from unsplash.com 176 |
177 |
178 |
179 | {/* ./ Typography*/} 180 | {/* Buttons*/} 181 |
182 |

Buttons

183 |
184 | 185 | 186 | 187 |
188 |
189 | 190 | 191 | 192 |
193 |
194 | {/* ./ Buttons */} 195 | {/* Form elements*/} 196 |
197 |
198 |

Form elements

199 | {/* Search */} 200 | 201 | 207 | {/* Text */} 208 | 209 | 210 | Curabitur consequat lacus at lacus porta finibus. 211 | {/* Select */} 212 | 213 | 217 | {/* File browser */} 218 | 222 | {/* Range slider control */} 223 | 234 | {/* States */} 235 |
236 | 246 | 256 | 266 |
267 |
268 | {/* Date*/} 269 | 273 | {/* Time*/} 274 | 278 | {/* Color*/} 279 | 288 |
289 |
290 | {/* Checkboxes */} 291 |
292 | 293 | Checkboxes 294 | 295 | 304 | 308 |
309 | {/* Radio buttons */} 310 |
311 | 312 | Radio buttons 313 | 314 | 324 | 333 |
334 | {/* Switch */} 335 |
336 | 337 | Switches 338 | 339 | 349 | 358 |
359 |
360 | {/* Buttons */} 361 | 362 | 363 |
364 |
365 | {/* ./ Form elements*/} 366 | {/* Tables */} 367 |
368 |

Tables

369 |
370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 |
#HeadingHeadingHeadingHeadingHeadingHeadingHeading
1CellCellCellCellCellCellCell
2CellCellCellCellCellCellCell
3CellCellCellCellCellCellCell
416 |
417 |
418 | {/* ./ Tables */} 419 | {/* Modal */} 420 | 426 | {/* ./ Modal */} 427 | {/* Accordions */} 428 |
429 |

Accordions

430 |
431 | Accordion 1 432 |

433 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 434 | Pellentesque urna diam, tincidunt nec porta sed, auctor id 435 | velit. Etiam venenatis nisl ut orci consequat, vitae tempus quam 436 | commodo. Nulla non mauris ipsum. Aliquam eu posuere orci. Nulla 437 | convallis lectus rutrum quam hendrerit, in facilisis elit 438 | sollicitudin. Mauris pulvinar pulvinar mi, dictum tristique elit 439 | auctor quis. Maecenas ac ipsum ultrices, porta turpis sit amet, 440 | congue turpis. 441 |

442 |
443 |
444 | Accordion 2 445 |
    446 |
  • Vestibulum id elit quis massa interdum sodales.
  • 447 |
  • 448 | Nunc quis eros vel odio pretium tincidunt nec quis neque. 449 |
  • 450 |
  • Quisque sed eros non eros ornare elementum.
  • 451 |
  • 452 | Cras sed libero aliquet, porta dolor quis, dapibus ipsum. 453 |
  • 454 |
455 |
456 |
457 | {/* ./ Accordions */} 458 | {/* Article*/} 459 |
460 |

Article

461 |

462 | Nullam dui arcu, malesuada et sodales eu, efficitur vitae dolor. 463 | Sed ultricies dolor non ante vulputate hendrerit. Vivamus sit amet 464 | suscipit sapien. Nulla iaculis eros a elit pharetra egestas. Nunc 465 | placerat facilisis cursus. Sed vestibulum metus eget dolor 466 | pharetra rutrum. 467 |

468 |
469 | 470 | Duis nec elit placerat, suscipit nibh quis, finibus neque. 471 | 472 |
473 |
474 | {/* ./ Article*/} 475 | {/* Progress */} 476 |
477 |

Progress bar

478 | 479 | 480 |
481 | {/* ./ Progress */} 482 | {/* Loading */} 483 |
484 |

Loading

485 |
486 | 487 |
488 | {/* ./ Loading */} 489 |
490 | {/* ./ Main */} 491 | {/* Footer */} 492 | 500 | {/* ./ Footer */} 501 | {/* Modal example */} 502 | 503 | 530 | 531 | {/* ./ Modal example */} 532 | {/* Minimal theme switcher */} 533 | {/* Modal */} 534 |
535 | 536 | ); 537 | } 538 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrateRoot } from "react-dom/client"; 2 | import { RemixBrowser } from "@remix-run/react"; 3 | 4 | hydrateRoot(document, ); 5 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { PassThrough } from "stream"; 2 | import { renderToPipeableStream } from "react-dom/server"; 3 | import { RemixServer } from "@remix-run/react"; 4 | import type { EntryContext } from "@remix-run/node"; 5 | import { Response, Headers } from "@remix-run/node"; 6 | import isbot from "isbot"; 7 | 8 | const ABORT_DELAY = 5000; 9 | 10 | export default function handleRequest( 11 | request: Request, 12 | responseStatusCode: number, 13 | responseHeaders: Headers, 14 | remixContext: EntryContext 15 | ) { 16 | const callbackName = isbot(request.headers.get("user-agent")) 17 | ? "onAllReady" 18 | : "onShellReady"; 19 | 20 | return new Promise((resolve, reject) => { 21 | let didError = false; 22 | 23 | const { pipe, abort } = renderToPipeableStream( 24 | , 25 | { 26 | [callbackName]() { 27 | let body = new PassThrough(); 28 | 29 | responseHeaders.set("Content-Type", "text/html"); 30 | responseHeaders.set("Transfer-Encoding", "chunked"); 31 | responseHeaders.set("Connection", "keep-alive"); 32 | 33 | resolve( 34 | new Response(body, { 35 | status: didError ? 500 : responseStatusCode, 36 | headers: responseHeaders, 37 | }) 38 | ); 39 | pipe(body); 40 | }, 41 | onShellError(err) { 42 | reject(err); 43 | }, 44 | onError(error) { 45 | didError = true; 46 | console.error(error); 47 | }, 48 | } 49 | ); 50 | setTimeout(abort, ABORT_DELAY); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { LinksFunction, MetaFunction } from "@remix-run/node"; 2 | import { 3 | Link, 4 | Links, 5 | LiveReload, 6 | Meta, 7 | Outlet, 8 | Scripts, 9 | ScrollRestoration, 10 | } from "@remix-run/react"; 11 | 12 | import picoStyleHref from "@picocss/pico/css/pico.min.css"; 13 | 14 | export const links: LinksFunction = () => [ 15 | { 16 | rel: "stylesheet", 17 | href: picoStyleHref, 18 | }, 19 | ]; 20 | 21 | export const meta: MetaFunction = () => ({ 22 | charset: "utf-8", 23 | title: "New Remix App", 24 | viewport: "width=device-width,initial-scale=1", 25 | }); 26 | 27 | export default function App() { 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 49 | 50 | 51 | 52 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | } 79 | 80 | function RemixLogo() { 81 | return ( 82 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | 3 | export let loader = async () => { 4 | await new Promise((resolve) => setTimeout(resolve, 1000)); 5 | return null; 6 | }; 7 | 8 | export default function Index() { 9 | return ( 10 |
11 |

Remix + React 18

12 |

Ready from day one with minimal to no changes to your codebase.

13 | 14 |
    15 |
  • 16 | Upgrading 17 |
  • 18 |
  • 19 | React.lazy 20 |
  • 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/routes/lazy.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { lazy, Suspense } from "react"; 3 | 4 | import { Context } from "~/components/lazy-context"; 5 | let LazyComponent = lazy(() => import("~/components/lazy")); 6 | 7 | export default function Lazy() { 8 | let delay = useMemo<{ promise: Promise | null }>( 9 | () => ({ 10 | promise: new Promise((resolve) => setTimeout(resolve, 1000)).then( 11 | () => { 12 | delay.promise = null; 13 | } 14 | ), 15 | }), 16 | [] 17 | ); 18 | 19 | return ( 20 | <> 21 |
22 |

React.lazy

23 |

24 | Everything below here is lazy loaded with an artificial delay but 25 | still SSR'd and streamed down. 26 |

27 |
28 |
29 | 30 | 33 |

Loading...

34 | 35 | } 36 | > 37 | 38 |
39 |
40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/routes/upgrading.tsx: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node"; 2 | import type { LinksFunction, LoaderFunction } from "@remix-run/node"; 3 | import { Link, useLoaderData } from "@remix-run/react"; 4 | 5 | import Prism from "prismjs"; 6 | import darkThemeStylesHref from "prismjs/themes/prism-tomorrow.min.css"; 7 | 8 | export const links: LinksFunction = () => [ 9 | { 10 | rel: "stylesheet", 11 | href: darkThemeStylesHref, 12 | }, 13 | ]; 14 | 15 | export let loader: LoaderFunction = () => { 16 | return json({ 17 | installReact: Prism.highlight( 18 | `npm install react react-dom`, 19 | Prism.languages.plain, 20 | "plain" 21 | ), 22 | entryClient: Prism.highlight( 23 | `import { hydrateRoot } from "react-dom/client"; 24 | import { RemixBrowser } from "@remix-run/react"; 25 | 26 | hydrateRoot(document, );`, 27 | Prism.languages.js, 28 | "js" 29 | ), 30 | entryServer: Prism.highlight( 31 | `import { PassThrough } from "stream"; 32 | import { renderToPipeableStream } from "react-dom/server"; 33 | import { RemixServer } from "@remix-run/react"; 34 | import type { EntryContext } from "@remix-run/node"; 35 | import { Response, Headers } from "@remix-run/node"; 36 | import isbot from "isbot"; 37 | 38 | const ABORT_DELAY = 5000; 39 | 40 | export default function handleRequest( 41 | request: Request, 42 | responseStatusCode: number, 43 | responseHeaders: Headers, 44 | remixContext: EntryContext 45 | ) { 46 | const callbackName = isbot(request.headers.get("user-agent")) 47 | ? "onAllReady" 48 | : "onShellReady"; 49 | 50 | return new Promise((resolve, reject) => { 51 | let didError = false; 52 | 53 | const { pipe, abort } = renderToPipeableStream( 54 | , 55 | { 56 | [callbackName]() { 57 | let body = new PassThrough(); 58 | 59 | responseHeaders.set("Content-Type", "text/html"); 60 | 61 | resolve( 62 | new Response(body, { 63 | status: didError ? 500 : responseStatusCode, 64 | headers: responseHeaders, 65 | }) 66 | ); 67 | pipe(body); 68 | }, 69 | onShellError(err) { 70 | reject(err); 71 | }, 72 | onError(error) { 73 | didError = true; 74 | console.error(error); 75 | }, 76 | } 77 | ); 78 | setTimeout(abort, ABORT_DELAY); 79 | }); 80 | }`, 81 | Prism.languages.js, 82 | "js" 83 | ), 84 | }); 85 | }; 86 | 87 | export default function Upgrading() { 88 | let { installReact, entryClient, entryServer } = useLoaderData(); 89 | console.log(installReact); 90 | return ( 91 |
92 |

Upgrading to React 18

93 |

To start, all you have to do is install React 18:

94 | 95 |
 96 |         
 97 |       
98 | 99 |
100 | 101 |

102 | This will get you running right away, but you will be missing out on 103 | some of React 18's best features until you update your{" "} 104 | entry.client 105 | and entry.server. 106 |

107 | 108 |

109 | entry.client will need to be updated to use the new{" "} 110 | hydrateRoot API: 111 |

112 | 113 |
114 |         
115 |       
116 | 117 |
118 | 119 |

120 | entry.server will need to be updated to use the new{" "} 121 | renderToPipeableStream API. This is a bit more involved 122 | than renderToString but isn't terrible: 123 |

124 | 125 |
126 |         
127 |       
128 | 129 |
130 | 131 |

132 | That's it! You are now streaming your HTML responses and are ready to 133 | use new features available on the server such as{" "} 134 | 135 | React.lazy 136 | 137 |

138 |
139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-template-express", 3 | "private": true, 4 | "description": "", 5 | "license": "", 6 | "sideEffects": false, 7 | "scripts": { 8 | "build": "remix build", 9 | "dev": "remix build && run-p dev:*", 10 | "dev:node": "cross-env NODE_ENV=development nodemon ./server.js --watch ./server.js", 11 | "dev:remix": "remix watch", 12 | "postinstall": "remix setup node", 13 | "start": "cross-env NODE_ENV=production node ./server.js" 14 | }, 15 | "dependencies": { 16 | "@picocss/pico": "^1.5.0", 17 | "@remix-run/express": "^1.6.1", 18 | "@remix-run/react": "^1.6.1", 19 | "compression": "^1.7.4", 20 | "cross-env": "^7.0.3", 21 | "express": "^4.17.1", 22 | "isbot": "^3.4.5", 23 | "morgan": "^1.10.0", 24 | "prismjs": "^1.27.0", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-simple-chatbot": "^0.6.1" 28 | }, 29 | "devDependencies": { 30 | "@remix-run/dev": "^1.6.1", 31 | "@types/prismjs": "^1.26.0", 32 | "@types/react": "^18.0.14", 33 | "@types/react-dom": "^18.0.5", 34 | "eslint": "^8.11.0", 35 | "nodemon": "^2.0.15", 36 | "npm-run-all": "^4.1.5", 37 | "typescript": "^4.5.5" 38 | }, 39 | "engines": { 40 | "node": ">=14" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-react-18-streaming/ff9ceb42e30d3ac82e2ccf3bbb115d4315b94e22/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev').AppConfig} 3 | */ 4 | module.exports = { 5 | ignoredRouteFiles: [".*"], 6 | serverDependenciesToBundle: [/react-syntax-highlighter/], 7 | // appDirectory: "app", 8 | // assetsBuildDirectory: "public/build", 9 | // serverBuildPath: "build/index.js", 10 | // publicPath: "/build/", 11 | }; 12 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const compression = require("compression"); 3 | const morgan = require("morgan"); 4 | const { createRequestHandler } = require("@remix-run/express"); 5 | const { createRoutes } = require("@remix-run/server-runtime/routes"); 6 | const { 7 | matchServerRoutes, 8 | } = require("@remix-run/server-runtime/routeMatching"); 9 | 10 | const buildPath = "./build"; 11 | 12 | const app = express(); 13 | 14 | const noCompressContentTypes = [ 15 | /text\/html/, 16 | /text\/remix-deferred/, 17 | /text\/event-stream/, 18 | ]; 19 | 20 | // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header 21 | app.disable("x-powered-by"); 22 | 23 | app.use( 24 | compression({ 25 | filter: (req, res) => { 26 | let contentTypeHeader = res.getHeader("Content-Type"); 27 | let contentType = ""; 28 | if (typeof contentTypeHeader === "string") { 29 | contentType = contentTypeHeader; 30 | } else if (typeof contentTypeHeader === "number") { 31 | contentType = String(contentTypeHeader); 32 | } else if (contentTypeHeader) { 33 | contentType = contentTypeHeader.join("; "); 34 | } 35 | 36 | if ( 37 | noCompressContentTypes && 38 | noCompressContentTypes.some((regex) => regex.test(contentType)) 39 | ) { 40 | return false; 41 | } 42 | 43 | return true; 44 | }, 45 | }) 46 | ); 47 | 48 | // Remix fingerprints its assets so we can cache forever. 49 | app.use( 50 | "/build", 51 | express.static("public/build", { immutable: true, maxAge: "1y" }) 52 | ); 53 | 54 | // Everything else (like favicon.ico) is cached for an hour. You may want to be 55 | // more aggressive with this caching. 56 | app.use(express.static("public", { maxAge: "1h" })); 57 | 58 | app.use(morgan("tiny")); 59 | 60 | app.all( 61 | "*", 62 | ...(process.env.NODE_ENV === "development" 63 | ? [ 64 | (req, res, next) => { 65 | purgeRequireCache(buildPath); 66 | 67 | remixEarlyHints(require(buildPath))(req, res); 68 | return createRequestHandler({ 69 | build: require(buildPath), 70 | mode: process.env.NODE_ENV, 71 | })(req, res, next); 72 | }, 73 | ] 74 | : [ 75 | remixEarlyHints(require(buildPath)), 76 | createRequestHandler({ 77 | build: require(buildPath), 78 | mode: process.env.NODE_ENV, 79 | }), 80 | ]) 81 | ); 82 | const port = process.env.PORT || 3000; 83 | 84 | app.listen(port, () => { 85 | console.log(`Express server listening on port ${port}`); 86 | }); 87 | 88 | function purgeRequireCache(path) { 89 | delete require.cache[require.resolve(path)]; 90 | } 91 | 92 | function remixEarlyHints(build) { 93 | function getRel(resource) { 94 | if (resource.endsWith(".js")) { 95 | return "modulepreload"; 96 | } 97 | return "preload"; 98 | } 99 | 100 | const routes = createRoutes(build.routes); 101 | 102 | /** 103 | * 104 | * @param {*} req 105 | * @param {import("express").Response} res 106 | * @param {*} next 107 | */ 108 | return (req, res, next) => { 109 | const matches = matchServerRoutes(routes, req.path); 110 | 111 | let resources = 112 | matches && 113 | matches.flatMap((match) => [ 114 | build.assets.routes[match.route.id].module, 115 | ...(build.assets.routes[match.route.id].imports || []), 116 | ]); 117 | 118 | if (resources && resources.length > 0) { 119 | res.connection.write("HTTP/1.1 103\r\n"); 120 | for (const resource of resources) { 121 | res.connection.write( 122 | `Link: <${resource}>; rel=${getRel(resource)}\r\n` 123 | ); 124 | } 125 | res.connection.write("\r\n"); 126 | } 127 | 128 | if (next) next(); 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2020"], 5 | "module": "ES2020", 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-jsx", 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "target": "ES2020", 12 | "strict": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "~/*": ["./app/*"] 16 | }, 17 | "noEmit": true, 18 | "allowJs": true, 19 | "forceConsistentCasingInFileNames": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------