Skip to content

Commit 32b402a

Browse files
committed
Tries to implement early hints but apparently not a good fit with express due to lack of http2 support
1 parent 2588500 commit 32b402a

File tree

6 files changed

+134
-1
lines changed

6 files changed

+134
-1
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,7 @@ playwright/.cache/
8080
# Because seed scripts uses lots of media which generates multiple versions. Reduce git bloat
8181
apps/*/media/assets/
8282
scratch/
83+
84+
# for running http2 locally
85+
*.key
86+
*.cert

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,19 @@
2727

2828

2929
# Add cspotsourcemap
30+
31+
32+
# Preload css and preconnect (we don't have any) using early hints
33+
34+
- [github remix run remix discussions](https://github.com/remix-run/remix/discussions/5378)
35+
- defer this as it might get implemented in remix
36+
37+
# Visual regression testing
38+
39+
- [playwright docs test snapshots](https://playwright.dev/docs/test-snapshots)
40+
41+
# Client hints
42+
43+
- Using kent dodds approach
44+
- See if you can prevent reload
45+
- defer as library might get published

apps/web/cert/.keep

Whitespace-only changes.

apps/web/src/app/entry.server.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
import { createReadableStreamFromReadable } from '@remix-run/node'
77
import { RemixServer } from '@remix-run/react'
88
import isbot from 'isbot'
9-
import { renderToPipeableStream } from 'react-dom/server'
109
import { PassThrough } from 'node:stream'
10+
import { renderToPipeableStream } from 'react-dom/server'
1111

1212
import { IsBotProvider } from './utils/isBotProvider'
1313

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* The idea here is to emit "early hints" in the response based on the CSS files or other assets needed by the route
3+
*
4+
* Eg for a route with 3 css files we want to emit at the top of the response:
5+
*
6+
* HTTP/1.1 103 Early Hints
7+
* Link: </build/_assets/app-XX4AKNF6.css>; rel=preload; as=style;
8+
* Link: </build/_assets/react-notion-x-5KGZZXWT.css>; rel=preload; as=style;
9+
* Link: </build/_assets/prism-coy-3Y5Y6EPU.css>; rel=preload; as=style;
10+
*/
11+
12+
import { type LinkDescriptor, type ServerBuild } from '@remix-run/node'
13+
import { matchServerRoutes } from '@remix-run/server-runtime/dist/routeMatching'
14+
import { createRoutes } from '@remix-run/server-runtime/dist/routes'
15+
import { type Response } from 'express'
16+
import { type ExpressMiddleware } from '~/types/middlewareType'
17+
18+
type EarlyHintAs = 'document' | 'script' | 'image' | 'style' | 'font'
19+
type EarlyHintCORS = 'anonymous' | 'use-credentials' | 'crossorigin'
20+
type EarlyHintRel =
21+
| 'dns-prefetch'
22+
| 'preconnect'
23+
| 'prefetch'
24+
| 'preload'
25+
| 'prerender'
26+
27+
export interface EarlyHintItem {
28+
href: string
29+
rel: EarlyHintRel
30+
cors?: boolean | EarlyHintCORS
31+
as?: EarlyHintAs
32+
}
33+
34+
export const remixEarlyHints = (build: ServerBuild): ExpressMiddleware => {
35+
const routes = createRoutes(build.routes)
36+
37+
return (req, res, next) => {
38+
// Find the routes that match
39+
const matches = matchServerRoutes(routes, req.path)
40+
41+
// For those routes, run their links() functions and find their links
42+
const matchesLinks: LinkDescriptor[] = (matches ?? []).flatMap((match) => {
43+
const { links } = match.route.module
44+
return links ? links() : []
45+
})
46+
47+
// Filter down to stylesheets and emit css resources
48+
const cssResources = matchesLinks.flatMap((link) => {
49+
if ('href' in link && link.rel === 'stylesheet' && link.href) {
50+
return [
51+
{
52+
rel: 'preload',
53+
as: 'style',
54+
href: link.href,
55+
},
56+
] satisfies EarlyHintItem[]
57+
}
58+
return []
59+
})
60+
61+
// you can add custom hints like preconnect to hosts
62+
writeEarlyHintsLinks(cssResources, res)
63+
64+
next()
65+
}
66+
}
67+
68+
function writeEarlyHintsLinks(earlyHints: EarlyHintItem[], res: Response) {
69+
const hints = earlyHints.map((earlyHint) => formatEntry(earlyHint))
70+
71+
res.writeEarlyHints({ link: hints })
72+
}
73+
74+
const formatEntry = (earlyHint: EarlyHintItem, nodeApi = true) => {
75+
const { href, rel, as = '', cors = false } = earlyHint
76+
77+
let _cors
78+
switch (cors) {
79+
case true:
80+
case 'crossorigin':
81+
_cors = 'crossorigin'
82+
break
83+
case 'anonymous':
84+
_cors = 'crossorigin=anonymous'
85+
break
86+
case 'use-credentials':
87+
_cors = 'crossorigin=use-credentials'
88+
break
89+
case false:
90+
_cors = ''
91+
break
92+
default:
93+
_cors = ''
94+
break
95+
}
96+
97+
if (nodeApi) {
98+
return `<${href}>; rel=${rel}${as.length !== 0 ? '; as=' : ''}${as}${
99+
_cors.length !== 0 ? '; ' : ''
100+
}${_cors}`
101+
}
102+
103+
return `Link: <${href}>; rel=${rel}${as.length !== 0 ? '; as=' : ''}${as}${
104+
_cors.length !== 0 ? '; ' : ''
105+
}${_cors}`
106+
}

run

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ function types:gen { ## Generates payload types
9494
pnpm -F web dev:types
9595
}
9696

97+
function ssl { ## Generate ssl certificate
98+
openssl req -nodes -new -x509 \
99+
-keyout ./apps/web/cert/server.key \
100+
-out ./apps/web/cert/server.cert \
101+
-subj "/C=US/ST=State/L=City/O=company/OU=Com/CN=www.testserver.local"
102+
}
103+
97104
function dc:run { ## Docker: Runs [SERVICE] for one-off commands; Does not use ports specified in the service config preventing port collisions;
98105
if [ $# -eq 0 ]; then echo 1>&2 "Usage: $0 $FUNCNAME run [SERVICE] [OPTIONAL COMMANDS]
99106
$0 $FUNCNAME run api pnpm add -D express

0 commit comments

Comments
 (0)