Skip to content

Commit a7bcba8

Browse files
committed
Return reply for Fastify adapter
1 parent 849e6fd commit a7bcba8

File tree

6 files changed

+91
-58
lines changed

6 files changed

+91
-58
lines changed
Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,33 @@
1-
# Welcome to Remix + Vite!
1+
# Welcome to Remix + Vite + Fastify + Sentry!
22

3-
📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features.
3+
📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite)
4+
for details on supported features.
45

56
## Development
67

78
Run the Express server with Vite dev middleware:
89

9-
```shellscript
10-
npm run dev
10+
```sh
11+
pnpm dev
1112
```
1213

1314
## Deployment
1415

1516
First, build your app for production:
1617

1718
```sh
18-
npm run build
19+
pnpm build
1920
```
2021

2122
Then run the app in production mode:
2223

2324
```sh
24-
npm start
25+
pnpm start
2526
```
2627

2728
Now you'll need to pick a host to deploy it to.
2829

29-
### DIY
30-
31-
If you're familiar with deploying Express applications you should be right at home. Just make sure to deploy the output of `npm run build`
30+
## Sentry Events Inspection
3231

33-
- `build/server`
34-
- `build/client`
32+
After you have launched the app with `pnpm dev:events`, visit the index page and open your browser's network tab. You
33+
will find there an event sent to `Sentry` and correlated server event will be written to `tests/events` directory.

dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,21 @@
1111
"start": "cross-env NODE_ENV=production node ./server.js",
1212
"typecheck": "tsc",
1313
"test:build": "pnpm install && npx playwright install && pnpm build",
14-
"test:assert": "pnpm playwright test"
14+
"test:assert": "FASTIFY_LOG_LEVEL=warn pnpm playwright test"
1515
},
1616
"dependencies": {
1717
"@remix-run/express": "^2.8.1",
1818
"@remix-run/node": "^2.8.1",
1919
"@remix-run/react": "^2.8.1",
2020
"@sentry/remix": "latest || *",
21-
"compression": "^1.7.4",
21+
"fastify": "^4.26.2",
22+
"@fastify/compress": "^7.0.0",
23+
"@fastify/middie": "^8.3.0",
24+
"@fastify/multipart": "^8.2.0",
25+
"@fastify/static": "^7.0.1",
26+
"@mcansh/remix-fastify": "^3.2.2",
2227
"dotenv": "^16.4.5",
23-
"express": "^4.18.2",
2428
"isbot": "^4.1.0",
25-
"morgan": "^1.10.0",
2629
"react": "^18.2.0",
2730
"react-dom": "^18.2.0"
2831
},
Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
import { createRequestHandler } from '@remix-run/express';
2-
import { wrapExpressCreateRequestHandler } from '@sentry/remix';
1+
import path from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
import { wrapFastifyCreateRequestHandler } from '@sentry/remix';
34
import { installGlobals } from '@remix-run/node';
4-
import compression from 'compression';
5-
import express from 'express';
6-
import morgan from 'morgan';
5+
import { createRequestHandler } from '@mcansh/remix-fastify';
6+
import fastify from 'fastify';
7+
import compression from '@fastify/compress';
8+
import middie from '@fastify/middie';
9+
import multipart from '@fastify/multipart';
10+
import serveStatic from '@fastify/static';
11+
12+
const __filename = fileURLToPath(import.meta.url);
13+
const __dirname = path.dirname(__filename);
14+
15+
const PORT = Number(process.env.PORT || '3000');
16+
const FASTIFY_LOG_LEVEL = process.env.FASTIFY_LOG_LEVEL || 'info';
717

818
installGlobals();
919

10-
const withSentryCreateRequestHandler = wrapExpressCreateRequestHandler(createRequestHandler);
20+
const withSentryCreateRequestHandler = wrapFastifyCreateRequestHandler(createRequestHandler);
1121

1222
const viteDevServer =
1323
process.env.NODE_ENV === 'production'
@@ -24,29 +34,48 @@ const remixHandler = withSentryCreateRequestHandler({
2434
: await import('./build/server/index.js'),
2535
});
2636

27-
const app = express();
28-
29-
app.use(compression());
30-
31-
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
32-
app.disable('x-powered-by');
37+
const app = fastify({ logger: { level: FASTIFY_LOG_LEVEL } });
38+
await app.register(middie);
39+
await app.register(multipart);
40+
await app.register(compression, { global: true });
3341

34-
// handle asset requests
3542
if (viteDevServer) {
3643
app.use(viteDevServer.middlewares);
3744
} else {
38-
// Vite fingerprints its assets so we can cache forever.
39-
app.use('/assets', express.static('build/client/assets', { immutable: true, maxAge: '1y' }));
45+
// options borrowed from:
46+
// https://github.com/mcansh/remix-fastify/blob/main/examples/vite/server.js
47+
// Remix fingerprints its assets so we can cache forever.
48+
await app.register(serveStatic, {
49+
root: path.join(__dirname, 'build', 'client', 'assets'),
50+
prefix: '/assets',
51+
wildcard: true,
52+
decorateReply: false,
53+
cacheControl: true,
54+
dotfiles: 'allow',
55+
etag: true,
56+
maxAge: '1y',
57+
immutable: true,
58+
serveDotFiles: true,
59+
lastModified: true,
60+
});
61+
await app.register(serveStatic, {
62+
root: path.join(__dirname, 'build', 'client'),
63+
prefix: '/',
64+
wildcard: false,
65+
cacheControl: true,
66+
dotfiles: 'allow',
67+
etag: true,
68+
maxAge: '1h',
69+
serveDotFiles: true,
70+
lastModified: true,
71+
});
4072
}
4173

42-
// Everything else (like favicon.ico) is cached for an hour. You may want to be
43-
// more aggressive with this caching.
44-
app.use(express.static('build/client', { maxAge: '1h' }));
45-
46-
app.use(morgan('tiny'));
47-
48-
// handle SSR requests
4974
app.all('*', remixHandler);
5075

51-
const port = process.env.PORT || 3000;
52-
app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`));
76+
app.listen({ port: PORT }, err => {
77+
if (err) {
78+
console.err(err);
79+
process.exit(1);
80+
}
81+
});

dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/tests/behaviour-server.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
5555
.toBe(200);
5656
}),
5757
);
58-
5958
expect(pageLoadTransactionEvent).not.toBeNull();
6059

6160
let serverEventIds = null;
@@ -69,6 +68,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
6968
}).toBe(true)
7069
expect(serverEventIds).not.toBeNull();
7170

71+
console.log(`Polling for server-side eventIds: ${JSON.stringify(serverEventIds)}`);
7272
// we could have read the event details from the file (since we are
7373
// dumping event without any mutations before sending it to Sentry),
7474
// but let's following the practice in other test applications and

packages/remix/src/utils/serverAdapters/shared.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ async function finishSentryProcessing(res: AugmentedResponse): Promise<void> {
114114

115115
function startRequestHandlerTransactionWithRoutes(
116116
this: unknown,
117-
handler: GenericRequestHandler,
117+
handler: GenericRequestHandler<SupportedResponse>,
118118
framework: SupportedFramework,
119119
routes: ServerRoute[],
120120
req: SupportedRequest,
@@ -144,22 +144,20 @@ function startRequestHandlerTransactionWithRoutes(
144144
}
145145

146146
export const wrapRequestHandler = <NextFn>(
147-
handler: GenericRequestHandler,
147+
handler: GenericRequestHandler<SupportedResponse>,
148148
framework: SupportedFramework,
149149
readyBuildOrGetBuildFn: ServerBuild | (() => Promise<ServerBuild> | ServerBuild),
150-
): GenericRequestHandler => {
150+
): GenericRequestHandler<SupportedResponse> => {
151151
let routes: ServerRoute[];
152152

153-
return async function (this: unknown, req: PolymorphicRequest, res: SupportedResponse, next: NextFn): Promise<void> {
153+
return async function (this: unknown, req: PolymorphicRequest, res: SupportedResponse, next: NextFn): Promise<SupportedResponse | null> {
154+
const isExpress = framework === SupportedFramework.Express;
155+
const isFastify = framework === SupportedFramework.Fastify;
154156
await withIsolationScope(async isolationScope => {
155-
if (framework === SupportedFramework.Express) {
156-
// eslint-disable-next-line @typescript-eslint/unbound-method
157-
(res as ExpressResponse).end = wrapEndMethod((res as ExpressResponse).end);
158-
} else if (framework === SupportedFramework.Fastify) {
159-
(res as FastifyReply).send = wrapEndMethod((res as FastifyReply).send);
160-
} else {
161-
throw new Error('Unreachable');
162-
}
157+
// eslint-disable-next-line @typescript-eslint/unbound-method
158+
if (isExpress) (res as ExpressResponse).end = wrapEndMethod((res as ExpressResponse).end)
159+
else if (isFastify) (res as FastifyReply).send = wrapEndMethod((res as FastifyReply).send)
160+
else throw new Error('Unreachable');
163161

164162
const request = extractRequestData(req);
165163

@@ -190,18 +188,22 @@ export const wrapRequestHandler = <NextFn>(
190188
routes = createRoutes(build.routes);
191189
return startRequestHandlerTransactionWithRoutes.call(this, handler, framework, routes, req, res, next, url);
192190
});
191+
// Fastify wants us to _return_ the "reply" instance
192+
// in case we are sending a response ourselves, which
193+
// we _are_ doing by means of`reply.send(data)`.
194+
return isFastify ? res : null;
193195
};
194196
};
195197

196198
export const prepareWrapCreateRequestHandler = (forFramework: SupportedFramework) =>
197199
function wrapCreateRequestHandler(
198-
createRequestHandler: CreateGenericRequestHandler,
199-
): (this: unknown, options: CreateRequestHandlerOptions) => GenericRequestHandler {
200-
return function (this: unknown, opts: CreateRequestHandlerOptions): GenericRequestHandler {
200+
createRequestHandler: CreateGenericRequestHandler<SupportedResponse>,
201+
): (this: unknown, options: CreateRequestHandlerOptions) => GenericRequestHandler<SupportedResponse> {
202+
return function (this: unknown, opts: CreateRequestHandlerOptions): GenericRequestHandler<SupportedResponse> {
201203
if (!opts.getLoadContext) opts['getLoadContext'] = () => ({});
202204
fill(opts, 'getLoadContext', wrapGetLoadContext(forFramework));
203205
const build = typeof opts.build === 'function' ? wrapBuildFn(opts.build) : instrumentBuild(opts.build, true);
204-
const handler: GenericRequestHandler = createRequestHandler.call(this, { ...opts, build });
206+
const handler: GenericRequestHandler<SupportedResponse> = createRequestHandler.call(this, { ...opts, build });
205207
return wrapRequestHandler(handler, forFramework, build);
206208
};
207209
};

packages/remix/src/utils/vendor/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,9 @@ export interface AssetsManifest {
237237
version: string;
238238
}
239239

240-
export type GenericRequestHandler = (req: any, res: any, next: any) => Promise<void>;
240+
export type GenericRequestHandler<R> = (req: any, res: R, next: any) => Promise<R | null>;
241241

242-
export type CreateGenericRequestHandler = (this: unknown, options: any) => GenericRequestHandler;
242+
export type CreateGenericRequestHandler<R> = (this: unknown, options: any) => GenericRequestHandler<R>;
243243

244244
export interface CreateRequestHandlerOptions {
245245
build: ServerBuild | (() => ServerBuild) | (() => Promise<ServerBuild>);

0 commit comments

Comments
 (0)