├── .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 |
--------------------------------------------------------------------------------