├── .gitignore ├── README.md ├── app ├── entry.client.tsx ├── entry.server.tsx ├── events.server.ts ├── root.tsx └── routes │ ├── index.tsx │ └── sse.live-visitors.ts ├── package-lock.json ├── package.json ├── patches └── @remix-run+serve+0.0.0-nightly-a114c40-20220514.patch ├── remix.config.js ├── remix.env.d.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Development 6 | 7 | From your terminal: 8 | 9 | ```sh 10 | npm run dev 11 | ``` 12 | 13 | This starts your app in development mode, rebuilding assets on file changes. 14 | 15 | ## Deployment 16 | 17 | First, build your app for production: 18 | 19 | ```sh 20 | npm run build 21 | ``` 22 | 23 | Then run the app in production mode: 24 | 25 | ```sh 26 | npm start 27 | ``` 28 | 29 | Now you'll need to pick a host to deploy it to. 30 | 31 | ### DIY 32 | 33 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready. 34 | 35 | Make sure to deploy the output of `remix build` 36 | 37 | - `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/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from 'react-dom'; 2 | import { RemixBrowser } from '@remix-run/react'; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'react-dom/server'; 2 | import { RemixServer } from '@remix-run/react'; 3 | import type { EntryContext } from '@remix-run/node'; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | let markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set('Content-Type', 'text/html'); 16 | 17 | return new Response('' + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /app/events.server.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | declare global { 4 | var liveVisitors: number; 5 | var liveVisitorsEvents: EventEmitter; 6 | } 7 | 8 | global.liveVisitors = global.liveVisitors || 0; 9 | global.liveVisitorsEvents = global.liveVisitorsEvents || new EventEmitter(); 10 | 11 | export const events = global.liveVisitorsEvents; 12 | 13 | export function dispatchVisitorsChange(change: "dec" | "inc") { 14 | global.liveVisitors = global.liveVisitors || 0; 15 | global.liveVisitors += change === "dec" ? -1 : 1; 16 | global.liveVisitorsEvents.emit("visitorsChanged", liveVisitors); 17 | } 18 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | LiveReload, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from '@remix-run/react'; 9 | import type { MetaFunction } from '@remix-run/node'; 10 | 11 | export const meta: MetaFunction = () => { 12 | return { title: 'New Remix App' }; 13 | }; 14 | 15 | export default function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {process.env.NODE_ENV === 'development' && } 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export default function Index() { 4 | let [liveVisitors, setLiveVisitors] = useState(""); 5 | 6 | useEffect(() => { 7 | let eventSource = new EventSource("/sse/live-visitors"); 8 | eventSource.addEventListener("message", (event) => { 9 | setLiveVisitors(event.data || "unknown"); 10 | }); 11 | }, []); 12 | 13 | return ( 14 |
15 |

Welcome to Remix

16 |

Live visitors: {liveVisitors}

17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/routes/sse.live-visitors.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node"; 2 | 3 | import { dispatchVisitorsChange, events } from "~/events.server"; 4 | 5 | export let loader: LoaderFunction = ({ request }) => { 6 | if (!request.signal) return new Response(null, { status: 500 }); 7 | 8 | return new Response( 9 | new ReadableStream({ 10 | start(controller) { 11 | let encoder = new TextEncoder(); 12 | let handleVisitorsChanged = (liveVisitors: number) => { 13 | console.log({ liveVisitors }); 14 | controller.enqueue(encoder.encode("event: message\n")); 15 | controller.enqueue(encoder.encode(`data: ${liveVisitors}\n\n`)); 16 | }; 17 | 18 | let closed = false; 19 | let close = () => { 20 | if (closed) return; 21 | closed = true; 22 | 23 | events.removeListener("visitorsChanged", handleVisitorsChanged); 24 | request.signal.removeEventListener("abort", close); 25 | controller.close(); 26 | 27 | dispatchVisitorsChange("dec"); 28 | }; 29 | 30 | events.addListener("visitorsChanged", handleVisitorsChanged); 31 | request.signal.addEventListener("abort", close); 32 | if (request.signal.aborted) { 33 | close(); 34 | return; 35 | } 36 | 37 | dispatchVisitorsChange("inc"); 38 | }, 39 | }), 40 | { 41 | headers: { "Content-Type": "text/event-stream" }, 42 | } 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "remix-app-template", 4 | "description": "", 5 | "license": "", 6 | "scripts": { 7 | "postinstall": "patch-package", 8 | "build": "cross-env NODE_ENV=production remix build", 9 | "dev": "cross-env NODE_ENV=development remix dev", 10 | "start": "cross-env NODE_ENV=production remix-serve build" 11 | }, 12 | "dependencies": { 13 | "@remix-run/node": "0.0.0-nightly-a114c40-20220514", 14 | "@remix-run/react": "0.0.0-nightly-a114c40-20220514", 15 | "@remix-run/serve": "0.0.0-nightly-a114c40-20220514", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2" 18 | }, 19 | "devDependencies": { 20 | "@remix-run/dev": "0.0.0-nightly-a114c40-20220514", 21 | "@types/react": "^17.0.24", 22 | "@types/react-dom": "^17.0.9", 23 | "cross-env": "^7.0.3", 24 | "patch-package": "^6.4.7", 25 | "typescript": "^4.1.2" 26 | }, 27 | "engines": { 28 | "node": ">=14" 29 | }, 30 | "sideEffects": false 31 | } 32 | -------------------------------------------------------------------------------- /patches/@remix-run+serve+0.0.0-nightly-a114c40-20220514.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@remix-run/serve/index.js b/node_modules/@remix-run/serve/index.js 2 | index ff63316..d69b901 100644 3 | --- a/node_modules/@remix-run/serve/index.js 4 | +++ b/node_modules/@remix-run/serve/index.js 5 | @@ -26,7 +26,28 @@ var morgan__default = /*#__PURE__*/_interopDefaultLegacy(morgan); 6 | function createApp(buildPath, mode = "production") { 7 | let app = express__default["default"](); 8 | app.disable("x-powered-by"); 9 | - app.use(compression__default["default"]()); 10 | + app.use(compression__default["default"]({ 11 | + filter: (req, res) => { 12 | + let contentTypeHeader = res.getHeader("Content-Type"); 13 | + let contentType = ""; 14 | + if (contentTypeHeader) { 15 | + if (Array.isArray(contentTypeHeader)) { 16 | + contentType = contentTypeHeader.join(" "); 17 | + } else { 18 | + contentType = String(contentTypeHeader); 19 | + } 20 | + } 21 | + 22 | + if ( 23 | + contentType.includes("text/html") || 24 | + contentType.includes("text/event-stream") 25 | + ) { 26 | + return false; 27 | + } 28 | + 29 | + return true; 30 | + }, 31 | + })); 32 | app.use("/build", express__default["default"].static("public/build", { 33 | immutable: true, 34 | maxAge: "1y" 35 | -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev/config').AppConfig} 3 | */ 4 | module.exports = { 5 | appDirectory: "app", 6 | browserBuildDirectory: "public/build", 7 | publicPath: "/build/", 8 | serverBuildDirectory: "build", 9 | devServerPort: 8002 10 | }; 11 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | "noEmit": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "allowJs": true 19 | } 20 | } 21 | --------------------------------------------------------------------------------