diff --git a/packages/remix/package.json b/packages/remix/package.json index 4e214c8962b5..52ff6e499983 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -78,12 +78,14 @@ "devDependencies": { "@remix-run/node": "^2.15.2", "@remix-run/react": "^2.15.2", + "@remix-run/server-runtime": "2.15.2", "@types/express": "^4.17.14", "vite": "^5.4.11" }, "peerDependencies": { "@remix-run/node": "2.x", "@remix-run/react": "2.x", + "@remix-run/server-runtime": "2.x", "react": "18.x" }, "scripts": { diff --git a/packages/remix/src/server/errors.ts b/packages/remix/src/server/errors.ts index 32e76a9db260..90359212300d 100644 --- a/packages/remix/src/server/errors.ts +++ b/packages/remix/src/server/errors.ts @@ -1,9 +1,12 @@ import type { + ActionFunction, ActionFunctionArgs, EntryContext, HandleDocumentRequestFunction, + LoaderFunction, LoaderFunctionArgs, } from '@remix-run/node'; +import { isRouteErrorResponse } from '@remix-run/router'; import type { RequestEventData, Span } from '@sentry/core'; import { addExceptionMechanism, @@ -17,8 +20,9 @@ import { import { DEBUG_BUILD } from '../utils/debug-build'; import type { RemixOptions } from '../utils/remixOptions'; import { storeFormDataKeys } from '../utils/utils'; -import { extractData, isResponse, isRouteErrorResponse } from '../utils/vendor/response'; -import type { DataFunction, RemixRequest } from '../utils/vendor/types'; +import { extractData, isResponse } from '../utils/vendor/response'; + +type DataFunction = LoaderFunction | ActionFunction; /** * Captures an exception happened in the Remix server. @@ -87,7 +91,7 @@ export function errorHandleDocumentRequestFunction( this: unknown, origDocumentRequestFunction: HandleDocumentRequestFunction, requestContext: { - request: RemixRequest; + request: Request; responseStatusCode: number; responseHeaders: Headers; context: EntryContext; diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index 8cd54e989530..3417188cc7d5 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -1,4 +1,18 @@ /* eslint-disable max-lines */ +import type { AgnosticRouteObject } from '@remix-run/router'; +import { isDeferredData, isRouteErrorResponse } from '@remix-run/router'; +import type { + ActionFunction, + ActionFunctionArgs, + AppLoadContext, + CreateRequestHandlerFunction, + EntryContext, + HandleDocumentRequestFunction, + LoaderFunction, + LoaderFunctionArgs, + RequestHandler, + ServerBuild, +} from '@remix-run/server-runtime'; import type { RequestEventData, Span, TransactionSource, WrappedFunction } from '@sentry/core'; import { continueTrace, @@ -22,23 +36,15 @@ import { } from '@sentry/core'; import { DEBUG_BUILD } from '../utils/debug-build'; import { createRoutes, getTransactionName } from '../utils/utils'; -import { extractData, isDeferredData, isResponse, isRouteErrorResponse, json } from '../utils/vendor/response'; -import type { - AppData, - AppLoadContext, - CreateRequestHandlerFunction, - DataFunction, - DataFunctionArgs, - EntryContext, - HandleDocumentRequestFunction, - RemixRequest, - RequestHandler, - ServerBuild, - ServerRoute, - ServerRouteManifest, -} from '../utils/vendor/types'; +import { extractData, isResponse, json } from '../utils/vendor/response'; import { captureRemixServerException, errorHandleDataFunction, errorHandleDocumentRequestFunction } from './errors'; +type AppData = unknown; +type RemixRequest = Parameters[0]; +type ServerRouteManifest = ServerBuild['routes']; +type DataFunction = LoaderFunction | ActionFunction; +type DataFunctionArgs = LoaderFunctionArgs | ActionFunctionArgs; + const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); function isRedirectResponse(response: Response): boolean { return redirectStatusCodes.has(response.status); @@ -261,7 +267,7 @@ function wrapRequestHandler( return origRequestHandler.call(this, request, loadContext); } - let resolvedRoutes: ServerRoute[] | undefined; + let resolvedRoutes: AgnosticRouteObject[] | undefined; if (options?.instrumentTracing) { if (typeof build === 'function') { @@ -428,7 +434,7 @@ export const makeWrappedCreateRequestHandler = (options?: { instrumentTracing?: function (origCreateRequestHandler: CreateRequestHandlerFunction): CreateRequestHandlerFunction { return function ( this: unknown, - build: ServerBuild | (() => Promise), + build: ServerBuild | (() => ServerBuild | Promise), ...args: unknown[] ): RequestHandler { const newBuild = instrumentBuild(build, options); diff --git a/packages/remix/src/utils/utils.ts b/packages/remix/src/utils/utils.ts index 57b26e07ce56..a1d878ac1314 100644 --- a/packages/remix/src/utils/utils.ts +++ b/packages/remix/src/utils/utils.ts @@ -1,9 +1,11 @@ -import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node'; +import type { ActionFunctionArgs, LoaderFunctionArgs, ServerBuild } from '@remix-run/node'; +import type { AgnosticRouteObject } from '@remix-run/router'; import type { Span, TransactionSource } from '@sentry/core'; import { logger } from '@sentry/core'; import { DEBUG_BUILD } from './debug-build'; import { getRequestMatch, matchServerRoutes } from './vendor/response'; -import type { ServerRoute, ServerRouteManifest } from './vendor/types'; + +type ServerRouteManifest = ServerBuild['routes']; /** * @@ -29,7 +31,7 @@ export async function storeFormDataKeys(args: LoaderFunctionArgs | ActionFunctio /** * Get transaction name from routes and url */ -export function getTransactionName(routes: ServerRoute[], url: URL): [string, TransactionSource] { +export function getTransactionName(routes: AgnosticRouteObject[], url: URL): [string, TransactionSource] { const matches = matchServerRoutes(routes, url.pathname); const match = matches && getRequestMatch(url, matches); return match === null ? [url.pathname, 'url'] : [match.route.id || 'no-route-id', 'route']; @@ -41,11 +43,11 @@ export function getTransactionName(routes: ServerRoute[], url: URL): [string, Tr * @param manifest * @param parentId */ -export function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] { +export function createRoutes(manifest: ServerRouteManifest, parentId?: string): AgnosticRouteObject[] { return Object.entries(manifest) .filter(([, route]) => route.parentId === parentId) .map(([id, route]) => ({ ...route, children: createRoutes(manifest, id), - })); + })) as AgnosticRouteObject[]; } diff --git a/packages/remix/src/utils/vendor/response.ts b/packages/remix/src/utils/vendor/response.ts index 4b7197f65982..dcdf70348967 100644 --- a/packages/remix/src/utils/vendor/response.ts +++ b/packages/remix/src/utils/vendor/response.ts @@ -8,8 +8,6 @@ import type { AgnosticRouteMatch, AgnosticRouteObject } from '@remix-run/router'; import { matchRoutes } from '@remix-run/router'; -import type { DeferredData, ErrorResponse, ServerRoute } from './types'; - /** * Based on Remix Implementation * @@ -76,7 +74,7 @@ export const json: JsonFunction = (data, init = {}) => { * Changed so that `matchRoutes` function is passed in. */ export function matchServerRoutes( - routes: ServerRoute[], + routes: AgnosticRouteObject[], pathname: string, ): AgnosticRouteMatch[] | null { const matches = matchRoutes(routes, pathname); @@ -126,38 +124,3 @@ export function getRequestMatch( return match; } - -/** - * https://github.com/remix-run/remix/blob/3e589152bc717d04e2054c31bea5a1056080d4b9/packages/remix-server-runtime/responses.ts#L75-L85 - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isDeferredData(value: any): value is DeferredData { - const deferred: DeferredData = value; - return ( - deferred && - typeof deferred === 'object' && - typeof deferred.data === 'object' && - typeof deferred.subscribe === 'function' && - typeof deferred.cancel === 'function' && - typeof deferred.resolveData === 'function' - ); -} - -/** - * https://github.com/remix-run/react-router/blob/f9b3dbd9cbf513366c456b33d95227f42f36da63/packages/router/utils.ts#L1574 - * - * Check if the given error is an ErrorResponse generated from a 4xx/5xx - * Response thrown from an action/loader - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isRouteErrorResponse(value: any): value is ErrorResponse { - const error: ErrorResponse = value; - - return ( - error != null && - typeof error.status === 'number' && - typeof error.statusText === 'string' && - typeof error.internal === 'boolean' && - 'data' in error - ); -} diff --git a/packages/remix/src/utils/vendor/types.ts b/packages/remix/src/utils/vendor/types.ts deleted file mode 100644 index 015207bd94a2..000000000000 --- a/packages/remix/src/utils/vendor/types.ts +++ /dev/null @@ -1,235 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-types */ -// Types vendored from @remix-run/server-runtime@1.6.0: -// https://github.com/remix-run/remix/blob/f3691d51027b93caa3fd2cdfe146d7b62a6eb8f2/packages/remix-server-runtime/server.ts -// Copyright 2021 Remix Software Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import type * as Express from 'express'; -import type { Agent } from 'https'; -import type { ComponentType } from 'react'; - -type Dev = { - command?: string; - scheme?: string; - host?: string; - port?: number; - restart?: boolean; - tlsKey?: string; - tlsCert?: string; -}; - -export interface FutureConfig { - unstable_dev: boolean | Dev; - /** @deprecated Use the `postcss` config option instead */ - unstable_postcss: boolean; - /** @deprecated Use the `tailwind` config option instead */ - unstable_tailwind: boolean; - v2_errorBoundary: boolean; - v2_headers: boolean; - v2_meta: boolean; - v2_normalizeFormMethod: boolean; - v2_routeConvention: boolean; -} - -export interface RemixConfig { - [key: string]: any; - future: FutureConfig; -} - -export interface ErrorResponse { - status: number; - statusText: string; - data: any; - error?: Error; - internal: boolean; -} - -export type RemixRequestState = { - method: string; - redirect: RequestRedirect; - headers: Headers; - parsedURL: URL; - signal: AbortSignal | null; - size: number | null; -}; - -export type RemixRequest = Request & - Record & { - agent?: Agent | ((parsedURL: URL) => Agent) | undefined; - }; - -export type AppLoadContext = Record & { __sentry_express_wrapped__?: boolean }; -export type AppData = any; -export type RequestHandler = (request: RemixRequest, loadContext?: AppLoadContext) => Promise; -export type CreateRequestHandlerFunction = (this: unknown, build: ServerBuild, ...args: any[]) => RequestHandler; -export type ServerRouteManifest = RouteManifest>; -export type Params = { - readonly [key in Key]: string | undefined; -}; - -export type ExpressRequest = Express.Request; -export type ExpressResponse = Express.Response; -export type ExpressNextFunction = Express.NextFunction; - -export interface Route { - index: false | undefined; - caseSensitive?: boolean; - id: string; - parentId?: string; - path?: string; -} - -export interface EntryRoute extends Route { - hasAction: boolean; - hasLoader: boolean; - hasCatchBoundary: boolean; - hasErrorBoundary: boolean; - imports?: string[]; - module: string; -} - -export interface RouteData { - [routeId: string]: AppData; -} - -export type DeferredData = { - data: Record; - init?: ResponseInit; - deferredKeys: string[]; - subscribe(fn: (aborted: boolean, settledKey?: string) => void): () => boolean; - cancel(): void; - resolveData(signal: AbortSignal): Promise; -}; - -export interface MetaFunction { - (args: { data: AppData; parentsData: RouteData; params: Params; location: Location }): HtmlMetaDescriptor; -} - -export interface HtmlMetaDescriptor { - [name: string]: null | string | undefined | Record | Array | string>; - charset?: 'utf-8'; - charSet?: 'utf-8'; - title?: string; -} - -export type CatchBoundaryComponent = ComponentType<{}>; -export type RouteComponent = ComponentType<{}>; -export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; -export type RouteHandle = any; -export interface LinksFunction { - (): any[]; -} -export interface EntryRouteModule { - CatchBoundary?: CatchBoundaryComponent; - ErrorBoundary?: ErrorBoundaryComponent; - default: RouteComponent; - handle?: RouteHandle; - links?: LinksFunction; - meta?: MetaFunction | HtmlMetaDescriptor; -} - -export interface ActionFunction { - (args: DataFunctionArgs): Promise | Response | Promise | AppData; -} - -export interface LoaderFunction { - (args: DataFunctionArgs): Promise | Response | Promise | AppData; -} - -export interface HeadersFunction { - (args: { loaderHeaders: Headers; parentHeaders: Headers; actionHeaders: Headers }): Headers | HeadersInit; -} - -export interface ServerRouteModule extends EntryRouteModule { - action?: ActionFunction; - headers?: HeadersFunction | { [name: string]: string }; - loader?: LoaderFunction; -} - -export interface ServerRoute extends Route { - children: ServerRoute[]; - module: ServerRouteModule; -} - -export interface RouteManifest { - [routeId: string]: Route; -} - -export interface ServerBuild { - entry: { - module: ServerEntryModule; - }; - routes: ServerRouteManifest; - assets: AssetsManifest; - publicPath?: string; - assetsBuildDirectory?: string; - future?: FutureConfig; -} - -export interface HandleDocumentRequestFunction { - ( - request: RemixRequest, - responseStatusCode: number, - responseHeaders: Headers, - context: EntryContext, - loadContext?: AppLoadContext, - ): Promise | Response; -} - -export interface HandleDataRequestFunction { - (response: Response, args: DataFunctionArgs): Promise | Response; -} - -interface ServerEntryModule { - default: HandleDocumentRequestFunction; - handleDataRequest?: HandleDataRequestFunction; -} - -export interface DataFunctionArgs { - request: RemixRequest; - context: AppLoadContext; - params: Params; -} - -export interface DataFunction { - (args: DataFunctionArgs): Promise | Response | Promise | AppData; -} - -// Taken from Remix Implementation -// https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/routeMatching.ts#L6-L10 -export interface RouteMatch { - params: Params; - pathname: string; - route: Route; -} - -export interface EntryContext { - [name: string]: any; -} - -export interface AssetsManifest { - entry: { - imports: string[]; - module: string; - }; - routes: RouteManifest; - url: string; - version: string; -} - -export type ExpressRequestHandler = (req: any, res: any, next: any) => Promise; - -export type ExpressCreateRequestHandler = (this: unknown, options: any) => ExpressRequestHandler; - -export interface ExpressCreateRequestHandlerOptions { - build: ServerBuild; - getLoadContext?: GetLoadContextFunction; - mode?: string; -} - -export type GetLoadContextFunction = (req: any, res: any) => AppLoadContext;