Skip to content

Commit 6bf07b7

Browse files
authored
feat: ⚡ Remix app (#15)
1 parent c05f609 commit 6bf07b7

19 files changed

+16408
-56
lines changed

demo/remix-app/.eslintrc.cjs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* This is intended to be a basic starting point for linting in your app.
3+
* It relies on recommended configs out of the box for simplicity, but you can
4+
* and should modify this configuration to best suit your team's needs.
5+
*/
6+
7+
/** @type {import('eslint').Linter.Config} */
8+
module.exports = {
9+
root: true,
10+
parserOptions: {
11+
ecmaVersion: 'latest',
12+
sourceType: 'module',
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
env: {
18+
browser: true,
19+
commonjs: true,
20+
es6: true,
21+
},
22+
ignorePatterns: ['!**/.server', '!**/.client'],
23+
24+
// Base config
25+
extends: ['eslint:recommended'],
26+
27+
overrides: [
28+
// React
29+
{
30+
files: ['**/*.{js,jsx,ts,tsx}'],
31+
plugins: ['react', 'jsx-a11y'],
32+
extends: [
33+
'plugin:react/recommended',
34+
'plugin:react/jsx-runtime',
35+
'plugin:react-hooks/recommended',
36+
'plugin:jsx-a11y/recommended',
37+
],
38+
settings: {
39+
react: {
40+
version: 'detect',
41+
},
42+
formComponents: ['Form'],
43+
linkComponents: [
44+
{ name: 'Link', linkAttribute: 'to' },
45+
{ name: 'NavLink', linkAttribute: 'to' },
46+
],
47+
'import/resolver': {
48+
typescript: {},
49+
},
50+
},
51+
},
52+
53+
// Typescript
54+
{
55+
files: ['**/*.{ts,tsx}'],
56+
plugins: ['@typescript-eslint', 'import'],
57+
parser: '@typescript-eslint/parser',
58+
settings: {
59+
'import/internal-regex': '^~/',
60+
'import/resolver': {
61+
node: {
62+
extensions: ['.ts', '.tsx'],
63+
},
64+
typescript: {
65+
alwaysTryTypes: true,
66+
},
67+
},
68+
},
69+
extends: [
70+
'plugin:@typescript-eslint/recommended',
71+
'plugin:import/recommended',
72+
'plugin:import/typescript',
73+
],
74+
},
75+
76+
// Node
77+
{
78+
files: ['.eslintrc.cjs'],
79+
env: {
80+
node: true,
81+
},
82+
},
83+
],
84+
};

demo/remix-app/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
.env

demo/remix-app/app/entry.client.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* By default, Remix will handle hydrating your app on the client for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.client
5+
*/
6+
7+
import { RemixBrowser } from '@remix-run/react';
8+
import { startTransition, StrictMode } from 'react';
9+
import { hydrateRoot } from 'react-dom/client';
10+
11+
startTransition(() => {
12+
hydrateRoot(
13+
document,
14+
<StrictMode>
15+
<RemixBrowser />
16+
</StrictMode>,
17+
);
18+
});

demo/remix-app/app/entry.server.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* By default, Remix will handle generating the HTTP Response for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.server
5+
*/
6+
7+
import { PassThrough } from 'node:stream';
8+
9+
import type { AppLoadContext, EntryContext } from '@remix-run/node';
10+
import { createReadableStreamFromReadable } from '@remix-run/node';
11+
import { RemixServer } from '@remix-run/react';
12+
import { isbot } from 'isbot';
13+
import { renderToPipeableStream } from 'react-dom/server';
14+
15+
const ABORT_DELAY = 5_000;
16+
17+
export default function handleRequest(
18+
request: Request,
19+
responseStatusCode: number,
20+
responseHeaders: Headers,
21+
remixContext: EntryContext,
22+
// This is ignored so we can keep it in the template for visibility. Feel
23+
// free to delete this parameter in your app if you're not using it!
24+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25+
loadContext: AppLoadContext,
26+
) {
27+
return isbot(request.headers.get('user-agent') || '')
28+
? handleBotRequest(
29+
request,
30+
responseStatusCode,
31+
responseHeaders,
32+
remixContext,
33+
)
34+
: handleBrowserRequest(
35+
request,
36+
responseStatusCode,
37+
responseHeaders,
38+
remixContext,
39+
);
40+
}
41+
42+
function handleBotRequest(
43+
request: Request,
44+
responseStatusCode: number,
45+
responseHeaders: Headers,
46+
remixContext: EntryContext,
47+
) {
48+
return new Promise((resolve, reject) => {
49+
let shellRendered = false;
50+
const { pipe, abort } = renderToPipeableStream(
51+
<RemixServer
52+
context={remixContext}
53+
url={request.url}
54+
abortDelay={ABORT_DELAY}
55+
/>,
56+
{
57+
onAllReady() {
58+
shellRendered = true;
59+
const body = new PassThrough();
60+
const stream = createReadableStreamFromReadable(body);
61+
62+
responseHeaders.set('Content-Type', 'text/html');
63+
64+
resolve(
65+
new Response(stream, {
66+
headers: responseHeaders,
67+
status: responseStatusCode,
68+
}),
69+
);
70+
71+
pipe(body);
72+
},
73+
onShellError(error: unknown) {
74+
reject(error);
75+
},
76+
onError(error: unknown) {
77+
responseStatusCode = 500;
78+
// Log streaming rendering errors from inside the shell. Don't log
79+
// errors encountered during initial shell rendering since they'll
80+
// reject and get logged in handleDocumentRequest.
81+
if (shellRendered) {
82+
console.error(error);
83+
}
84+
},
85+
},
86+
);
87+
88+
setTimeout(abort, ABORT_DELAY);
89+
});
90+
}
91+
92+
function handleBrowserRequest(
93+
request: Request,
94+
responseStatusCode: number,
95+
responseHeaders: Headers,
96+
remixContext: EntryContext,
97+
) {
98+
return new Promise((resolve, reject) => {
99+
let shellRendered = false;
100+
const { pipe, abort } = renderToPipeableStream(
101+
<RemixServer
102+
context={remixContext}
103+
url={request.url}
104+
abortDelay={ABORT_DELAY}
105+
/>,
106+
{
107+
onShellReady() {
108+
shellRendered = true;
109+
const body = new PassThrough();
110+
const stream = createReadableStreamFromReadable(body);
111+
112+
responseHeaders.set('Content-Type', 'text/html');
113+
114+
resolve(
115+
new Response(stream, {
116+
headers: responseHeaders,
117+
status: responseStatusCode,
118+
}),
119+
);
120+
121+
pipe(body);
122+
},
123+
onShellError(error: unknown) {
124+
reject(error);
125+
},
126+
onError(error: unknown) {
127+
responseStatusCode = 500;
128+
// Log streaming rendering errors from inside the shell. Don't log
129+
// errors encountered during initial shell rendering since they'll
130+
// reject and get logged in handleDocumentRequest.
131+
if (shellRendered) {
132+
console.error(error);
133+
}
134+
},
135+
},
136+
);
137+
138+
setTimeout(abort, ABORT_DELAY);
139+
});
140+
}

demo/remix-app/app/index.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:root {
2+
color: rgba(255, 255, 255, 0.87);
3+
background-color: #242424;
4+
}

demo/remix-app/app/root.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
2+
3+
import './index.css';
4+
5+
export function Layout({ children }: { children: React.ReactNode }) {
6+
return (
7+
<html lang="en">
8+
<head>
9+
<meta charSet="utf-8" />
10+
<meta name="viewport" content="width=device-width, initial-scale=1" />
11+
<Meta />
12+
</head>
13+
<body>
14+
{children}
15+
<ScrollRestoration />
16+
<Scripts />
17+
</body>
18+
</html>
19+
);
20+
}
21+
22+
export default function App() {
23+
return <Outlet />;
24+
}

demo/remix-app/app/routes/_index.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { MetaFunction } from '@remix-run/node';
2+
3+
export const meta: MetaFunction = () => {
4+
return [
5+
{ title: 'New Remix App' },
6+
{ name: 'description', content: 'Welcome to Remix!' },
7+
];
8+
};
9+
10+
import './styles/styles.css';
11+
12+
export default function Index() {
13+
return (
14+
<div>
15+
<h1>Google</h1>
16+
<p className="poppins">Poppins</p>
17+
<p className="press-start">Press Start 2P</p>
18+
19+
<h1>Bunny</h1>
20+
<p className="bunny-aclonica">Aclonica</p>
21+
<p className="bunny-allan">Allan</p>
22+
23+
<h1>FontShare</h1>
24+
<p className="font-share-panchang">Panchang</p>
25+
26+
<h1>FontSource</h1>
27+
<p className="font-source-luckiest">Luckiest</p>
28+
29+
<h1>Local</h1>
30+
<p className="local">Local font</p>
31+
</div>
32+
);
33+
}
59.3 KB
Binary file not shown.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
p {
2+
font-size: x-large;
3+
}
4+
5+
.google-poppins {
6+
font-family: "Poppins", sans-serif;
7+
}
8+
9+
.google-press-start {
10+
font-family: "Press Start 2P", sans-serif;
11+
}
12+
13+
.bunny-aclonica {
14+
font-family: "Aclonica", sans-serif;
15+
}
16+
17+
.bunny-allan {
18+
font-family: "Allan", sans-serif;
19+
}
20+
21+
.font-share-panchang {
22+
font-family: "Panchang", sans-serif;
23+
}
24+
25+
.font-source-luckiest {
26+
font-family: "Luckiest Guy", sans-serif;
27+
}
28+
29+
@font-face {
30+
font-family: "Black Fox";
31+
src: url("./black-fox.ttf");
32+
}
33+
34+
.local {
35+
font-family: "Black Fox", sans-serif;
36+
}

0 commit comments

Comments
 (0)