-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(remix): Add Vite dev-mode support to Express instrumentation. (#…
…10784) Resolves #10724 Related: #9500 Adds dev-mode support to Remix setups with Vite and Express. We currently accept Remix server `build` as an object to instrument. But Remix allows `build` as a synchronous or asynchronous function that returns the build object. Currently, it seems that functions are only used in development servers, and not in production. So, while this update slightly reduces `requestHandler` performance on dev servers, it does not on production builds. We need `build` in 2 places: 1- We instrument the loaders / actions on build, then we pass them down to the original implementations. 2- We use the `routes` inside them to create parameterised transactions. This update adds new internal wrappers around them to make sure that we don't miss out on the returned / resolved values in case `build` is a function, for both cases. This PR also adds a new E2E test application using the latest Remix version and Vite, and it runs the tests on `dev` mode. We also need a documentation update to reflect this, if it gets merged.
- Loading branch information
1 parent
811eaa4
commit 618b7d7
Showing
22 changed files
with
7,179 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/.eslintrc.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* This is intended to be a basic starting point for linting in your app. | ||
* It relies on recommended configs out of the box for simplicity, but you can | ||
* and should modify this configuration to best suit your team's needs. | ||
*/ | ||
|
||
/** @type {import('eslint').Linter.Config} */ | ||
module.exports = { | ||
root: true, | ||
parserOptions: { | ||
ecmaVersion: 'latest', | ||
sourceType: 'module', | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
env: { | ||
browser: true, | ||
commonjs: true, | ||
es6: true, | ||
}, | ||
|
||
// Base config | ||
extends: ['eslint:recommended'], | ||
|
||
overrides: [ | ||
// React | ||
{ | ||
files: ['**/*.{js,jsx,ts,tsx}'], | ||
plugins: ['react', 'jsx-a11y'], | ||
extends: [ | ||
'plugin:react/recommended', | ||
'plugin:react/jsx-runtime', | ||
'plugin:react-hooks/recommended', | ||
'plugin:jsx-a11y/recommended', | ||
], | ||
settings: { | ||
react: { | ||
version: 'detect', | ||
}, | ||
formComponents: ['Form'], | ||
linkComponents: [ | ||
{ name: 'Link', linkAttribute: 'to' }, | ||
{ name: 'NavLink', linkAttribute: 'to' }, | ||
], | ||
'import/resolver': { | ||
typescript: {}, | ||
}, | ||
}, | ||
}, | ||
|
||
// Typescript | ||
{ | ||
files: ['**/*.{ts,tsx}'], | ||
plugins: ['@typescript-eslint', 'import'], | ||
parser: '@typescript-eslint/parser', | ||
settings: { | ||
'import/internal-regex': '^~/', | ||
'import/resolver': { | ||
node: { | ||
extensions: ['.ts', '.tsx'], | ||
}, | ||
typescript: { | ||
alwaysTryTypes: true, | ||
}, | ||
}, | ||
}, | ||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript'], | ||
}, | ||
|
||
// Node | ||
{ | ||
files: ['.eslintrc.js', 'server.mjs'], | ||
env: { | ||
node: true, | ||
}, | ||
}, | ||
], | ||
}; |
5 changes: 5 additions & 0 deletions
5
dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/.gitignore
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
|
||
/.cache | ||
/build | ||
.env |
2 changes: 2 additions & 0 deletions
2
dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/.npmrc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@sentry:registry=http://127.0.0.1:4873 | ||
@sentry-internal:registry=http://127.0.0.1:4873 |
34 changes: 34 additions & 0 deletions
34
...ackages/e2e-tests/test-applications/create-remix-app-express-vite-dev/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Welcome to Remix + Vite! | ||
|
||
📖 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. | ||
|
||
## Development | ||
|
||
Run the Express server with Vite dev middleware: | ||
|
||
```shellscript | ||
npm run dev | ||
``` | ||
|
||
## Deployment | ||
|
||
First, build your app for production: | ||
|
||
```sh | ||
npm run build | ||
``` | ||
|
||
Then run the app in production mode: | ||
|
||
```sh | ||
npm start | ||
``` | ||
|
||
Now you'll need to pick a host to deploy it to. | ||
|
||
### DIY | ||
|
||
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` | ||
|
||
- `build/server` | ||
- `build/client` |
46 changes: 46 additions & 0 deletions
46
...ckages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; | ||
import * as Sentry from '@sentry/remix'; | ||
import { StrictMode, startTransition, useEffect } from 'react'; | ||
import { hydrateRoot } from 'react-dom/client'; | ||
|
||
Sentry.init({ | ||
environment: 'qa', // dynamic sampling bias to keep transactions | ||
dsn: window.ENV.SENTRY_DSN, | ||
integrations: [ | ||
Sentry.browserTracingIntegration({ | ||
useEffect, | ||
useLocation, | ||
useMatches, | ||
}), | ||
new Sentry.Replay(), | ||
], | ||
// Performance Monitoring | ||
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! | ||
// Session Replay | ||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. | ||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. | ||
}); | ||
|
||
Sentry.addEventProcessor(event => { | ||
if ( | ||
event.type === 'transaction' && | ||
(event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') | ||
) { | ||
const eventId = event.event_id; | ||
if (eventId) { | ||
window.recordedTransactions = window.recordedTransactions || []; | ||
window.recordedTransactions.push(eventId); | ||
} | ||
} | ||
|
||
return event; | ||
}); | ||
|
||
startTransition(() => { | ||
hydrateRoot( | ||
document, | ||
<StrictMode> | ||
<RemixBrowser /> | ||
</StrictMode>, | ||
); | ||
}); |
147 changes: 147 additions & 0 deletions
147
...ckages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.server.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { PassThrough } from 'node:stream'; | ||
|
||
import type { AppLoadContext, EntryContext } from '@remix-run/node'; | ||
import { createReadableStreamFromReadable } from '@remix-run/node'; | ||
import { installGlobals } from '@remix-run/node'; | ||
import { RemixServer } from '@remix-run/react'; | ||
import * as Sentry from '@sentry/remix'; | ||
import * as isbotModule from 'isbot'; | ||
import { renderToPipeableStream } from 'react-dom/server'; | ||
|
||
installGlobals(); | ||
|
||
const ABORT_DELAY = 5_000; | ||
|
||
Sentry.init({ | ||
environment: 'qa', // dynamic sampling bias to keep transactions | ||
dsn: process.env.E2E_TEST_DSN, | ||
// Performance Monitoring | ||
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! | ||
}); | ||
|
||
export const handleError = Sentry.wrapRemixHandleError; | ||
|
||
export default function handleRequest( | ||
request: Request, | ||
responseStatusCode: number, | ||
responseHeaders: Headers, | ||
remixContext: EntryContext, | ||
loadContext: AppLoadContext, | ||
) { | ||
return isBotRequest(request.headers.get('user-agent')) | ||
? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) | ||
: handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); | ||
} | ||
|
||
// We have some Remix apps in the wild already running with isbot@3 so we need | ||
// to maintain backwards compatibility even though we want new apps to use | ||
// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev. | ||
function isBotRequest(userAgent: string | null) { | ||
if (!userAgent) { | ||
return false; | ||
} | ||
|
||
// isbot >= 3.8.0, >4 | ||
if ('isbot' in isbotModule && typeof isbotModule.isbot === 'function') { | ||
return isbotModule.isbot(userAgent); | ||
} | ||
|
||
// isbot < 3.8.0 | ||
if ('default' in isbotModule && typeof isbotModule.default === 'function') { | ||
return isbotModule.default(userAgent); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function handleBotRequest( | ||
request: Request, | ||
responseStatusCode: number, | ||
responseHeaders: Headers, | ||
remixContext: EntryContext, | ||
) { | ||
return new Promise((resolve, reject) => { | ||
let shellRendered = false; | ||
const { pipe, abort } = renderToPipeableStream( | ||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />, | ||
{ | ||
onAllReady() { | ||
shellRendered = true; | ||
const body = new PassThrough(); | ||
const stream = createReadableStreamFromReadable(body); | ||
|
||
responseHeaders.set('Content-Type', 'text/html'); | ||
|
||
resolve( | ||
new Response(stream, { | ||
headers: responseHeaders, | ||
status: responseStatusCode, | ||
}), | ||
); | ||
|
||
pipe(body); | ||
}, | ||
onShellError(error: unknown) { | ||
reject(error); | ||
}, | ||
onError(error: unknown) { | ||
responseStatusCode = 500; | ||
// Log streaming rendering errors from inside the shell. Don't log | ||
// errors encountered during initial shell rendering since they'll | ||
// reject and get logged in handleDocumentRequest. | ||
if (shellRendered) { | ||
console.error(error); | ||
} | ||
}, | ||
}, | ||
); | ||
|
||
setTimeout(abort, ABORT_DELAY); | ||
}); | ||
} | ||
|
||
function handleBrowserRequest( | ||
request: Request, | ||
responseStatusCode: number, | ||
responseHeaders: Headers, | ||
remixContext: EntryContext, | ||
) { | ||
return new Promise((resolve, reject) => { | ||
let shellRendered = false; | ||
const { pipe, abort } = renderToPipeableStream( | ||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />, | ||
{ | ||
onShellReady() { | ||
shellRendered = true; | ||
const body = new PassThrough(); | ||
const stream = createReadableStreamFromReadable(body); | ||
|
||
responseHeaders.set('Content-Type', 'text/html'); | ||
|
||
resolve( | ||
new Response(stream, { | ||
headers: responseHeaders, | ||
status: responseStatusCode, | ||
}), | ||
); | ||
|
||
pipe(body); | ||
}, | ||
onShellError(error: unknown) { | ||
reject(error); | ||
}, | ||
onError(error: unknown) { | ||
responseStatusCode = 500; | ||
// Log streaming rendering errors from inside the shell. Don't log | ||
// errors encountered during initial shell rendering since they'll | ||
// reject and get logged in handleDocumentRequest. | ||
if (shellRendered) { | ||
console.error(error); | ||
} | ||
}, | ||
}, | ||
); | ||
|
||
setTimeout(abort, ABORT_DELAY); | ||
}); | ||
} |
Oops, something went wrong.