diff --git a/docs/posts/main/configuration/general.mdx b/docs/posts/main/configuration/general.mdx index eb7006e..1e07b2a 100644 --- a/docs/posts/main/configuration/general.mdx +++ b/docs/posts/main/configuration/general.mdx @@ -27,13 +27,18 @@ will be automatically imported and added to the dev tools. ## `includeInProd` - This option is used to set whether the plugin should be included in production builds or not. +This option is used to set whether the plugin should be included in production builds or not. - By default it is set to `undefined` and if you set this option to an object with the `client` and `server` properties set to `true` the plugin will be included in production builds. +By default it is set to `undefined` and if you set this option to an object with the `client`, `context` and `server` properties set to `true` the plugin will be included in production builds. The client part includes the dev tools with the plugin and the server part includes the info logs. You can granularly configure the exact behavior of both sides with client and server configs respectively. + +Each of these flags will include a part of the plugin in production, in order for any of these to work `react-router-devtools` need to be switched over to +a regular dependency and included in your project. If you only want to include the `devTools` helper in production, for example, you can +set `includeInProd` to `{ devTools: true }` and the `devTools` part will be included in production and available always. + <Warn title="Be careful!"> If you decide to deploy parts to production you should be very careful that you don't expose the dev tools to your clients or anybody who is not supposed to see them. Also the server part uses chalk which might not work in non-node environments! @@ -51,6 +56,7 @@ exact behavior of both sides with client and server configs respectively. includeInProd: { client: true, server: true, + devTools: true }, }), ], diff --git a/docs/posts/main/features/devtools.mdx b/docs/posts/main/features/devtools.mdx new file mode 100644 index 0000000..73320ec --- /dev/null +++ b/docs/posts/main/features/devtools.mdx @@ -0,0 +1,198 @@ +--- +title: "Devtools context" +alternateTitle: "Devtools context" +description: "Using the devtools context to trace events and send them to the network tab" +--- + +import Info from "./info.tsx"; +import Warn from "./warn.tsx"; + +## Devtools extended context + +The devtools context is a set of utilities that you can use in your data fetching functions to trace events +in the network tab of react-router-devtools. You can also include them in your production builds if you do not want +the hassle of having to optionally check if they are defined. + +The general usage of the devtools context is as follows: + +```ts +// The devTools object is available in all data fetching functions +export const loader = async ({ request, devTools }: LoaderFunctionArgs) => { + const tracing = devTools?.tracing; + // tracing is a set of utilities to be used in your data fetching functions to trace events + // in network tab of react-router-devtools + const startTime = tracing.start("my-event") + // do something here, eg DB call + tracing.end("my-event", startTime!) + return "data" +} +``` + +You can also use the devtools context in your action functions: + +```ts +export const action = async ({ request, devTools }: ActionFunctionArgs) => { + const tracing = devTools?.tracing; + // tracing is a set of utilities to be used in your data fetching functions to trace events + // in network tab of react-router-devtools + const startTime = tracing?.start("my-event") + // do something + tracing?.end("my-event", startTime!) + return "data" +} +``` + +The devtools context is also available in your client loader and client action functions: + +```ts +export const clientLoader = async ({ request, devTools }: ClientLoaderFunctionArgs) => { + const tracing = devTools?.tracing; + // tracing is a set of utilities to be used in your data fetching functions to trace events + // in network tab of react-router-devtools + const startTime = tracing?.start("my-event") + // do something + tracing?.end("my-event", startTime!) + return "data" +} +``` + +```ts +export const clientAction = async ({ request, devTools }: ClientActionFunctionArgs) => { + const tracing = devTools?.tracing; + // tracing is a set of utilities to be used in your data fetching functions to trace events + // in network tab of react-router-devtools + const startTime = tracing?.start("my-event") + // do something + tracing?.end("my-event", startTime!) + return "data" +} +``` + + +<Info> + If you want to make the devTools available always in your project, you can set `includeInProd` to `{ devTools: true }` in your vite config. + + In production the trace calls won't do anything, but the tracing will be more convinient to use. + + If you do so you can also override the types by adding the following to your project: + ```ts + import type { ExtendedContext } from "react-router-devtools/context"; + + declare module "react-router" { + interface LoaderFunctionArgs { + devTools: ExtendedContext + } + interface ActionFunctionArgs { + devTools: ExtendedContext + } + } + ``` +</Info> + +## RouteId + +The routeId is a string that is used to identify the route that is being processed. You can access it like so: +```ts +const loader = async ({ request, devTools }: LoaderFunctionArgs) => { + const routeId = devTools?.routeId; + // do something with the routeId + return "data" +} +``` + +## Tracing + +The tracing object contains all the utilities related to network tab tracing feature of react-router-devtools. + + +There are three functions you can use: +- trace +- start +- end + + + +### trace + +The `trace` function is a function that will trace the event given to it, pipe it to the network tab of react-router-devtools and show you analytics. + +This works by calling the provided function and analyzing the time it takes to execute it. + +```ts +const loader = async ({ request, devTools }: LoaderFunctionArgs) => { + const tracing = devTools?.tracing; + // this will be traced in the network tab of react-router-devtools + const user = tracing?.trace("my-event",() => getUser()) + + return { user } +} +``` + +#### Parameters + +- `name` - The name of the event +- `event` - The event to be traced + +#### Returns + +The result of the event + +### start + +The `start` function is a function that will start a trace for the name provided to it and return the start time. +This is used together with `end` to trace the time of the event. + +```ts +export const loader = async ({ request, devTools }: LoaderFunctionArgs) => { + const tracing = devTools?.tracing; + // this will be traced in the network tab of react-router-devtools + const startTime = tracing?.start("my-event") + // do something here, eg DB call + + // End the trace + tracing?.end("my-event", startTime!) + return "data" +} +``` + +<Warn title="Warning"> + This function relies on you using the `end` with the same name as the start event, otherwise +you will end up having a never ending loading bar in the network tab! +</Warn> + + +#### Parameters + +- `name` - The name of the event + +#### Returns + +The start time of the event + +### end + +The `end` function is a function that will end a trace for the name provided to it and return the end time. + +```ts +export const loader = async ({ request, devTools }: LoaderFunctionArgs) => { + const tracing = devTools?.tracing; + // this will be traced in the network tab of react-router-devtools + const startTime = tracing?.start("get user") + // do something here, eg DB call + const user = await getUser(); + // End the trace + tracing?.end("get user", startTime!, { user }) + return "data" + +} +``` + +#### Parameters + +- `name` - The name of the event +- `startTime` - The start time of the sendEvent +- `data` - The data to be sent with the event + +#### Returns + +The data provided in the last parameter diff --git a/docs/posts/main/metadata.json b/docs/posts/main/metadata.json index 74e933c..fdf8fa5 100644 --- a/docs/posts/main/metadata.json +++ b/docs/posts/main/metadata.json @@ -7,6 +7,7 @@ "server": "configuration/server", "shortcuts": "features/shortcuts", "active-page-tab": "features/active-page-tab", + "devtools": "features/devtools", "routes-tab": "features/routes-tab", "network-tab": "features/network-tab", "errors-tab": "features/errors-tab", @@ -72,6 +73,14 @@ "slug": "shortcuts", "spacer": true }, + "devtools": { + "title": "Development Tools context", + "alternateTitle": "Development Tools context", + "description": "Detailed overview of all the features offered on the Dev Tools context.", + "section": "Features", + "slug": "devtools", + "spacer": true + }, "active-page-tab": { "title": "Active Page Tab", "alternateTitle": "Active Page Tab", diff --git a/package.json b/package.json index d011c55..a9df9ee 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "react-router-devtools", - "description": "Devtools for React Router - debug and 10x your DX with React Router", + "description": "Devtools for React Router - debug, trace, find hydration errors, catch bugs and inspect server/client data with react-router-devtools", "author": "Alem Tuzlak", - "version": "2.0.0", + "version": "1.0.1", "license": "MIT", "keywords": [ "react-router", @@ -39,6 +39,14 @@ "types": "./dist/client.d.ts", "default": "./dist/client.js" }, + "./context": { + "import": { + "types": "./dist/context.d.ts", + "default": "./dist/context.js" + }, + "types": "./dist/context.d.ts", + "default": "./dist/context.js" + }, "./server": { "import": { "types": "./dist/server.d.ts", @@ -70,11 +78,13 @@ "runner": "npm-run-all -s build -p watch-all", "dev": "npm run runner react-router-vite", "build": "run-s tsup:* -- --clean", - "watch-all": "npm-run-all -p tsup:index:watch tsup:client:watch tsup:server:watch -- --watch", + "watch-all": "npm-run-all -p tsup:index:watch tsup:client:watch tsup:server:watch tsup:context:watch -- --watch", "tsup:index": "tsup", "tsup:index:watch": "tsup --watch", "tsup:client": "tsup --config tsup-client.config.ts", + "tsup:context": "tsup --config tsup-context.config.ts", "tsup:server": "tsup --config tsup-server.config.ts", + "tsup:context:watch": "npm run tsup:context -- --watch", "tsup:client:watch": "npm run tsup:client -- --watch", "tsup:server:watch": "npm run tsup:server -- --watch", "check": "biome check .", diff --git a/src/client/components/network-tracer/NetworkBar.tsx b/src/client/components/network-tracer/NetworkBar.tsx index adaed3d..db65f46 100644 --- a/src/client/components/network-tracer/NetworkBar.tsx +++ b/src/client/components/network-tracer/NetworkBar.tsx @@ -1,17 +1,17 @@ import { animate, motion, useMotionValue } from "framer-motion" import type React from "react" import { useEffect } from "react" -import type { NetworkRequest } from "./types" +import type { RequestEvent } from "../../../shared/request-event" interface NetworkBarProps { - request: NetworkRequest + request: RequestEvent index: number minTime: number pixelsPerMs: number barHeight: number barPadding: number now: number - onClick: (e: React.MouseEvent, request: NetworkRequest, order: number) => void + onClick: (e: React.MouseEvent, request: RequestEvent, order: number) => void isActive: boolean } @@ -20,6 +20,7 @@ const COLORS = { "client-loader": "#60a5fa", action: "#f59e0b", "client-action": "#ef4444", + "custom-event": "#ffffff", pending: "#94a3b8", error: "#dc2626", } @@ -90,7 +91,7 @@ export const NetworkBar: React.FC<NetworkBarProps> = ({ className="relative overflow-hidden group cursor-pointer hover:brightness-110" onClick={(e) => onClick(e, request, index)} > - {request.state === "pending" && isActive && ( + {isActive && ( <motion.div className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent opacity-20" animate={{ x: ["-100%", "100%"] }} diff --git a/src/client/components/network-tracer/NetworkWaterfall.tsx b/src/client/components/network-tracer/NetworkWaterfall.tsx index 0c75d71..c38bda2 100644 --- a/src/client/components/network-tracer/NetworkWaterfall.tsx +++ b/src/client/components/network-tracer/NetworkWaterfall.tsx @@ -3,14 +3,14 @@ import type React from "react" import { useEffect, useRef, useState } from "react" import { useHotkeys } from "react-hotkeys-hook" import { Tooltip } from "react-tooltip" +import type { RequestEvent } from "../../../shared/request-event" import { METHOD_COLORS } from "../../tabs/TimelineTab" import { Tag } from "../Tag" import { NetworkBar } from "./NetworkBar" import { REQUEST_BORDER_COLORS, RequestDetails } from "./RequestDetails" -import type { NetworkRequest, Position } from "./types" interface Props { - requests: NetworkRequest[] + requests: RequestEvent[] width: number } @@ -22,17 +22,19 @@ const MAX_SCALE = 10 const FUTURE_BUFFER = 1000 // 2 seconds ahead const INACTIVE_THRESHOLD = 100 // 1 seconds -export const TYPE_COLORS = { +const TYPE_COLORS = { loader: "bg-green-500", "client-loader": "bg-blue-500", action: "bg-yellow-500", "client-action": "bg-purple-500", + "custom-event": "bg-white", } const TYPE_TEXT_COLORS = { loader: "text-green-500", "client-loader": "text-blue-500", action: "text-yellow-500", "client-action": "text-purple-500", + "custom-event": "text-white", } const NetworkWaterfall: React.FC<Props> = ({ requests, width }) => { @@ -104,7 +106,7 @@ const NetworkWaterfall: React.FC<Props> = ({ requests, width }) => { // setScale((s) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s + delta))) // } - const handleBarClick = (e: React.MouseEvent, request: NetworkRequest, index: number) => { + const handleBarClick = (e: React.MouseEvent, request: RequestEvent, index: number) => { setSelectedRequest(index) } @@ -162,7 +164,7 @@ const NetworkWaterfall: React.FC<Props> = ({ requests, width }) => { <Tooltip place="top" id={`${request.id}${request.startTime}`} /> <div className="pr-4"> - <div>{request.id}</div> + <div className="whitespace-nowrap">{request.id}</div> </div> </button> <div className="flex items-center ml-auto"> diff --git a/src/client/components/network-tracer/RequestDetails.tsx b/src/client/components/network-tracer/RequestDetails.tsx index 54a2d85..57aa4e7 100644 --- a/src/client/components/network-tracer/RequestDetails.tsx +++ b/src/client/components/network-tracer/RequestDetails.tsx @@ -1,14 +1,12 @@ -import { motion } from "framer-motion" import type React from "react" +import type { RequestEvent } from "../../../shared/request-event" import { METHOD_COLORS } from "../../tabs/TimelineTab" import { Tag } from "../Tag" import { Icon } from "../icon/Icon" import { JsonRenderer } from "../jsonRenderer" -import { TYPE_COLORS } from "./NetworkWaterfall" -import type { NetworkRequest } from "./types" interface RequestDetailsProps { - request: NetworkRequest + request: RequestEvent onClose: () => void onChangeRequest: (index: number) => void total: number @@ -19,6 +17,7 @@ export const REQUEST_BORDER_COLORS = { "client-loader": "border-blue-500", action: "border-yellow-500", "client-action": "border-purple-500", + "custom-event": "border-white", error: "border-red-500", } export const RequestDetails: React.FC<RequestDetailsProps> = ({ request, onClose, total, index, onChangeRequest }) => { diff --git a/src/client/components/network-tracer/types.ts b/src/client/components/network-tracer/types.ts deleted file mode 100644 index d285a12..0000000 --- a/src/client/components/network-tracer/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface NetworkRequest { - id: string - url: string - method: string - status: number - startTime: number - endTime: number | null - size: number - type: "loader" | "action" | "client-loader" | "client-action" - data?: Record<string, unknown> - headers?: Record<string, string> - state: "pending" | "complete" | "error" - aborted?: boolean -} -export interface Position { - x: number - y: number -} diff --git a/src/client/context/RDTContext.tsx b/src/client/context/RDTContext.tsx index 63912a4..3d2e078 100644 --- a/src/client/context/RDTContext.tsx +++ b/src/client/context/RDTContext.tsx @@ -1,7 +1,6 @@ import type { Dispatch } from "react" import type React from "react" import { createContext, useEffect, useMemo, useReducer } from "react" -import type { NetworkRequest } from "../components/network-tracer/types.js" import { useRemoveBody } from "../hooks/detached/useRemoveBody.js" import { checkIsDetached, checkIsDetachedOwner, checkIsDetachedWindow } from "../utils/detached.js" import { tryParseJson } from "../utils/sanitize.js" diff --git a/src/client/context/requests/request-context.tsx b/src/client/context/requests/request-context.tsx index 0d4276a..00383e7 100644 --- a/src/client/context/requests/request-context.tsx +++ b/src/client/context/requests/request-context.tsx @@ -1,15 +1,15 @@ import { createContext, useCallback, useContext, useEffect, useState } from "react" -import type { NetworkRequest } from "../../components/network-tracer/types" +import type { RequestEvent } from "../../../shared/request-event" export const RequestContext = createContext<{ - requests: NetworkRequest[] + requests: RequestEvent[] removeAllRequests: () => void }>({ requests: [], removeAllRequests: () => {} }) -const requestMap = new Map<string, NetworkRequest>() +const requestMap = new Map<string, RequestEvent>() export const RequestProvider = ({ children }: any) => { - const [requests, setRequests] = useState<NetworkRequest[]>([]) + const [requests, setRequests] = useState<RequestEvent[]>([]) const setNewRequests = useCallback((payload: string) => { const requests = JSON.parse(payload) const newRequests = Array.isArray(requests) ? requests : [requests] diff --git a/src/client/context/useRDTContext.ts b/src/client/context/useRDTContext.ts index 588f92f..d4d1671 100644 --- a/src/client/context/useRDTContext.ts +++ b/src/client/context/useRDTContext.ts @@ -1,5 +1,4 @@ import { useCallback, useContext } from "react" -import type { NetworkRequest } from "../components/network-tracer/types.js" import { RDTContext } from "./RDTContext.js" import type { ReactRouterDevtoolsState } from "./rdtReducer.js" import type { TimelineEvent } from "./timeline/types.js" diff --git a/src/client/hof.ts b/src/client/hof.ts index c5eb43f..2375b1d 100644 --- a/src/client/hof.ts +++ b/src/client/hof.ts @@ -1,5 +1,5 @@ import type { ClientActionFunctionArgs, ClientLoaderFunctionArgs, LinksFunction } from "react-router" -import type { RequestEvent } from "../server/utils" +import type { RequestEvent } from "../shared/request-event" const sendEventToDevServer = (req: RequestEvent) => { import.meta.hot?.send("request-event", req) diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..4d15442 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,13 @@ +import { + withActionContextWrapper, + withClientActionContextWrapper, + withClientLoaderContextWrapper, + withLoaderContextWrapper, +} from "./context/extend-context" + +export { + withLoaderContextWrapper, + withActionContextWrapper, + withClientLoaderContextWrapper, + withClientActionContextWrapper, +} diff --git a/src/context/extend-context.ts b/src/context/extend-context.ts new file mode 100644 index 0000000..ce0f416 --- /dev/null +++ b/src/context/extend-context.ts @@ -0,0 +1,88 @@ +import type { + ActionFunctionArgs, + ClientActionFunctionArgs, + ClientLoaderFunctionArgs, + LoaderFunctionArgs, +} from "react-router" + +import type { AllDataFunctionArgs, NetworkRequestType } from "../shared/request-event" +import { traceEnd, traceEvent, traceStart } from "./tracing" + +const extendContextObject = (routeId: string, type: NetworkRequestType, args: AllDataFunctionArgs) => { + /** + * devTools is a set of utilities to be used in your data fetching functions. If you wish to include these + * tools in production, you need to install react-router-devtools as a regular dependency and include the + * context part in production! + */ + return { + // Current route ID + routeId, + /** + * Set of utilities to be used in your data fetching functions to trace events + * in network tab of react-router-devtools + */ + tracing: { + /** + * trace is a function that will trace the event given to it, pipe it to the network tab of react-router-devtools and show you analytics + * + * Warning: This function will only work in production if you install react-router-devtools as a regular dependency + * and include the context part in production! + * @param name - The name of the event + * @param event - The event to be traced + * @returns The result of the event + + */ + trace: traceEvent(type, args), + /** + * start is a function that will start a trace for the name provided to it and return the start time + * This is used together with traceEnd to trace the time of the event + * + * Warning: This function relies on you using the traceEnd with the same name as the start event, otherwise + * you will end up having a never ending loading bar in the network tab! + * + * @param name - The name of the event + * @returns The start time of the event + + */ + start: traceStart(type, args), + /** + * end is a function that will end a trace for the name provided to it and return the end time + * + * @param name - The name of the event + * @param startTime - The start time of the sendEvent + * @param data - The data to be sent with the event + * @returns The data provided in the last parameter + */ + end: traceEnd(type, args), + }, + } +} + +export type ExtendedContext = ReturnType<typeof extendContextObject> + +const extendContext = + (routeId: string, type: NetworkRequestType, loaderOrAction: <T>(args: any) => T) => + async (args: AllDataFunctionArgs) => { + const devTools = extendContextObject(routeId, type, args) + const res = await loaderOrAction({ + ...args, + devTools, + }) + return res + } + +export const withLoaderContextWrapper = (loader: <T>(args: LoaderFunctionArgs) => T, id: string) => { + return extendContext(id, "loader", loader) +} + +export const withActionContextWrapper = (action: <T>(args: ActionFunctionArgs) => T, id: string) => { + return extendContext(id, "action", action) +} + +export const withClientLoaderContextWrapper = (loader: <T>(args: ClientLoaderFunctionArgs) => T, id: string) => { + return extendContext(id, "client-loader", loader) +} + +export const withClientActionContextWrapper = (action: <T>(args: ClientActionFunctionArgs) => T, id: string) => { + return extendContext(id, "client-action", action) +} diff --git a/src/context/tracing.ts b/src/context/tracing.ts new file mode 100644 index 0000000..2ec4434 --- /dev/null +++ b/src/context/tracing.ts @@ -0,0 +1,68 @@ +import type { AllDataFunctionArgs, NetworkRequestType, RequestEvent } from "../shared/request-event" +import { sendEvent } from "../shared/send-event" + +export const traceEvent = + (type: NetworkRequestType, args: AllDataFunctionArgs) => + async <T>(name: string, event: (...args: any) => T) => { + const isServer = type === "action" || type === "loader" + const emitEventFunction = isServer + ? sendEvent + : (data: RequestEvent) => import.meta.hot?.send("request-event", data) + const startTime = Date.now() + emitEventFunction({ + type: "custom-event", + startTime, + url: args.request.url, + id: `${name}`, + headers: {}, + method: args.request.method, + }) + const data = await event() + emitEventFunction({ + type: "custom-event", + startTime, + endTime: Date.now(), + url: args.request.url, + id: `${name}`, + headers: {}, + method: args.request.method, + data, + }) + return data + } + +export const traceStart = (type: NetworkRequestType, args: AllDataFunctionArgs) => (name: string) => { + const isServer = type === "action" || type === "loader" + const emitEventFunction = isServer ? sendEvent : (data: RequestEvent) => import.meta.hot?.send("request-event", data) + const startTime = Date.now() + emitEventFunction({ + type: "custom-event", + startTime, + url: args.request.url, + id: `${name}`, + headers: {}, + method: args.request.method, + }) + return startTime +} + +export const traceEnd = + (type: NetworkRequestType, args: AllDataFunctionArgs) => + <T>(name: string, startTime: number, data?: T) => { + const isServer = type === "action" || type === "loader" + const emitEventFunction = isServer + ? sendEvent + : (data: RequestEvent) => import.meta.hot?.send("request-event", data) + + emitEventFunction({ + type: "custom-event", + startTime, + endTime: Date.now(), + url: args.request.url, + id: `${name}`, + headers: {}, + method: args.request.method, + data, + }) + return data + } diff --git a/src/index.ts b/src/index.ts index bc0c5e8..ad27d4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,19 @@ import type { Tab } from "./client/tabs/index.js" +import type { ExtendedContext } from "./context/extend-context.js" export { reactRouterDevTools, defineRdtConfig } from "./vite/plugin.js" // Type exports export type { EmbeddedDevToolsProps } from "./client/embedded-dev-tools.js" export type { ReactRouterDevtoolsProps as ReactRouterToolsProps } from "./client/react-router-dev-tools.js" +export type { ExtendedContext } from "./context/extend-context.js" + export type RdtPlugin = (...args: any) => Tab + +declare module "react-router" { + interface LoaderFunctionArgs { + devTools?: ExtendedContext + } + interface ActionFunctionArgs { + devTools?: ExtendedContext + } +} diff --git a/src/server/utils.ts b/src/server/utils.ts index 7d2961d..4470399 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -1,5 +1,6 @@ import chalk from "chalk" import type { ActionFunctionArgs, LoaderFunctionArgs, UNSAFE_DataWithResponseInit } from "react-router" +import { sendEvent } from "../shared/send-event.js" import { type DevToolsServerConfig, getConfig } from "./config.js" import { actionLog, errorLog, infoLog, loaderLog, redirectLog } from "./logger.js" import { diffInMs, secondsToHuman } from "./perf.js" @@ -236,41 +237,21 @@ const storeAndEmitActionOrLoaderInfo = async ( responseHeaders, }, } - const port = process.rdt_port - - if (port) { - fetch(`http://localhost:${port}/react-router-devtools-request`, { - method: "POST", - body: JSON.stringify(event), - }) - .then(() => {}) - .catch(() => {}) + if (typeof process === "undefined") { + return } -} - -export type RequestEvent = { - routine?: "request-event" - type: "action" | "loader" | "client-loader" | "client-action" - headers: Record<string, string> - id: string - startTime: number - endTime?: number | undefined - data?: any | undefined - method: string - status?: string - url: string - aborted?: boolean -} - -const sendEvent = (event: RequestEvent) => { const port = process.rdt_port if (port) { fetch(`http://localhost:${port}/react-router-devtools-request`, { method: "POST", - body: JSON.stringify({ routine: "request-event", ...event }), + body: JSON.stringify(event), }) - .then(() => {}) + .then(async (res) => { + if (res.ok) { + await res.text() + } + }) .catch(() => {}) } } @@ -310,8 +291,7 @@ export const analyzeLoaderOrAction = }) try { const res = await response - if (isDataFunctionResponse(res)) { - } + unAwaited(() => { const end = diffInMs(start) const endTime = Date.now() diff --git a/src/shared/request-event.ts b/src/shared/request-event.ts new file mode 100644 index 0000000..5a741d1 --- /dev/null +++ b/src/shared/request-event.ts @@ -0,0 +1,28 @@ +import type { + ActionFunctionArgs, + ClientActionFunctionArgs, + ClientLoaderFunctionArgs, + LoaderFunctionArgs, +} from "react-router" + +export type AllDataFunctionArgs = + | LoaderFunctionArgs + | ActionFunctionArgs + | ClientLoaderFunctionArgs + | ClientActionFunctionArgs +export type NetworkRequestType = "action" | "loader" | "client-action" | "client-loader" +type NetworkRequestTypeFull = "action" | "loader" | "client-action" | "client-loader" | "custom-event" + +export type RequestEvent = { + routine?: "request-event" + type: NetworkRequestTypeFull + headers: Record<string, string> + id: string + startTime: number + endTime?: number | undefined + data?: any | undefined + method: string + status?: string + url: string + aborted?: boolean +} diff --git a/src/shared/send-event.ts b/src/shared/send-event.ts new file mode 100644 index 0000000..f65ce59 --- /dev/null +++ b/src/shared/send-event.ts @@ -0,0 +1,22 @@ +import type { RequestEvent } from "./request-event" + +export const sendEvent = (event: RequestEvent) => { + if (typeof process === "undefined") { + return + } + const port = process.rdt_port + + if (port) { + fetch(`http://localhost:${port}/react-router-devtools-request`, { + method: "POST", + body: JSON.stringify({ routine: "request-event", ...event }), + }) + .then(async (res) => { + // avoid memory leaks + if (res.ok) { + await res.text() + } + }) + .catch(() => {}) + } +} diff --git a/src/vite/plugin.tsx b/src/vite/plugin.tsx index db3ec4f..69a48e1 100644 --- a/src/vite/plugin.tsx +++ b/src/vite/plugin.tsx @@ -5,12 +5,13 @@ import { cutArrayToLastN } from "../client/utils/common.js" import type { DevToolsServerConfig } from "../server/config.js" import type { ActionEvent, LoaderEvent } from "../server/event-queue.js" -import type { RequestEvent } from "../server/utils.js" +import type { RequestEvent } from "../shared/request-event.js" import { DEFAULT_EDITOR_CONFIG, type EditorConfig, type OpenSourceData, handleOpenSource } from "./editor.js" import { type WriteFileData, handleWriteFile } from "./file.js" import { handleDevToolsViteRequest, processPlugins } from "./utils.js" import { augmentDataFetchingFunctions } from "./utils/data-functions-augment.js" import { injectRdtClient } from "./utils/inject-client.js" +import { injectContext } from "./utils/inject-context.js" // this should mirror the types in server/config.ts as well as they are bundled separately. declare global { interface Window { @@ -34,6 +35,7 @@ type ReactRouterViteConfig = { includeInProd?: { client?: boolean server?: boolean + devTools?: boolean } /** The directory where the react router app is located. Defaults to the "./app" relative to where vite.config is being defined. */ appDir?: string @@ -50,11 +52,22 @@ export const reactRouterDevTools: (args?: ReactRouterViteConfig) => Plugin[] = ( } const includeClient = args?.includeInProd?.client ?? false const includeServer = args?.includeInProd?.server ?? false + const includeDevtools = args?.includeInProd?.devTools ?? false const appDir = args?.appDir || "./app" const shouldInject = (mode: string | undefined, include: boolean) => mode === "development" || include - + const isTransformable = (id: string) => { + const extensions = [".tsx", ".jsx", ".ts", ".js"] + if (!extensions.some((ext) => id.endsWith(ext))) { + return + } + if (id.includes("node_modules") || id.includes("dist") || id.includes("build") || id.includes("?")) { + return + } + const routeId = id.replace(normalizePath(process.cwd()), "").replace("/app/", "").replace(".tsx", "") + return routeId + } // Set the server config on the process object so that it can be accessed by the plugin if (typeof process !== "undefined") { process.rdt_config = serverConfig @@ -86,19 +99,29 @@ export const reactRouterDevTools: (args?: ReactRouterViteConfig) => Plugin[] = ( }, }, { - name: "react-router-devtools-data-function-augment", + name: "react-router-devtools-inject-context", apply(config) { - return shouldInject(config.mode, includeServer) + return shouldInject(config.mode, includeDevtools) }, transform(code, id) { - const extensions = [".tsx", ".jsx", ".ts", ".js"] - if (!extensions.some((ext) => id.endsWith(ext))) { + const routeId = isTransformable(id) + if (!routeId) { return } - if (id.includes("node_modules") || id.includes("dist") || id.includes("build") || id.includes("?")) { + const finalCode = injectContext(code, routeId) + return finalCode + }, + }, + { + name: "react-router-devtools-data-function-augment", + apply(config) { + return shouldInject(config.mode, includeServer) + }, + transform(code, id) { + const routeId = isTransformable(id) + if (!routeId) { return } - const routeId = id.replace(normalizePath(process.cwd()), "").replace("/app/", "").replace(".tsx", "") const finalCode = augmentDataFetchingFunctions(code, routeId) return finalCode }, diff --git a/src/vite/utils/inject-context.test.ts b/src/vite/utils/inject-context.test.ts new file mode 100644 index 0000000..32700c6 --- /dev/null +++ b/src/vite/utils/inject-context.test.ts @@ -0,0 +1,361 @@ +import { injectContext } from "./inject-context" + +const removeWhitespace = (str: string) => str.replace(/\s/g, "") + +describe("transform", () => { + it("should transform the loader export when it's a function", () => { + const result = injectContext( + ` + export function loader() {} + `, + "test" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export const loader = _withLoaderContextWrapper(function loader() {}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the loader export when it's a const variable", () => { + const result = injectContext( + ` + export const loader = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export const loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the loader export when it's a let variable", () => { + const result = injectContext( + ` + export let loader = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export let loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the loader export when it's a var variable", () => { + const result = injectContext( + ` + export var loader = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export var loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the loader export when it's re-exported from another file", () => { + const result = injectContext( + ` + export { loader } from "./loader.js"; + `, + "test" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export { loader as _loader } from "./loader.js"; + export const loader = _withLoaderContextWrapper(_loader, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the loader export when it's imported from another file and exported", () => { + const result = injectContext( + ` + import { loader } from "./loader.js"; + export { loader }; + `, + "test" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + import { loader } from "./loader.js"; + export { loader as _loader }; + export const loader = _withLoaderContextWrapper(_loader, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's a function", () => { + const result = injectContext( + ` + export function clientLoader() {} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; + export const clientLoader = _withClientLoaderContextWrapper(function clientLoader() {}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's a const variable", () => { + const result = injectContext( + ` + export const clientLoader = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; + export const clientLoader = _withClientLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's a let variable", () => { + const result = injectContext( + ` + export let clientLoader = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; + export let clientLoader = _withClientLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's a var variable", () => { + const result = injectContext( + ` + export var clientLoader = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; + export var clientLoader = _withClientLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's re-exported from another file", () => { + const result = injectContext( + ` + import { clientLoader } from "./client-loader.js"; + export { clientLoader }; + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; + import { clientLoader } from "./client-loader.js"; + export { clientLoader as _clientLoader }; + export const clientLoader = _withClientLoaderContextWrapper(_clientLoader, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's imported from another file and exported", () => { + const result = injectContext( + ` + import { clientLoader } from "./client-loader.js"; + export { clientLoader }; + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; + import { clientLoader } from "./client-loader.js"; + export { clientLoader as _clientLoader }; + export const clientLoader = _withClientLoaderContextWrapper(_clientLoader, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the action export when it's a function", () => { + const result = injectContext( + ` + export function action() {} + `, + "test" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; + export const action = _withActionContextWrapper(function action() {}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the action export when it's a const variable", () => { + const result = injectContext( + ` + export const action = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; + export const action = _withActionContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the action export when it's a let variable", () => { + const result = injectContext( + ` + export let action = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; + export let action = _withActionContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the action export when it's a var variable", () => { + const result = injectContext( + ` + export var action = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; + export var action = _withActionContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the action export when it's re-exported from another file", () => { + const result = injectContext( + ` + export { action } from "./action.js"; + `, + "test" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; + export { action as _action } from "./action.js"; + export const action = _withActionContextWrapper(_action, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should wrap the action export when it's imported from another file and exported", () => { + const result = injectContext( + ` + import { action } from "./action.js"; + export { action }; + `, + "test" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; + import { action } from "./action.js"; + export { action as _action }; + export const action = _withActionContextWrapper(_action, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the client action export when it's a function", () => { + const result = injectContext( + ` + export function clientAction() {} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; + export const clientAction = _withClientActionContextWrapper(function clientAction() {}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the client action export when it's a const variable", () => { + const result = injectContext( + ` + export const clientAction = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; + export const clientAction = _withClientActionContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the client action export when it's a let variable", () => { + const result = injectContext( + ` + export let clientAction = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; + export let clientAction = _withClientActionContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the client action export when it's a var variable", () => { + const result = injectContext( + ` + export var clientAction = async ({ request }) => { return {};} + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; + export var clientAction = _withClientActionContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the client action export when it's re-exported from another file", () => { + const result = injectContext( + ` + import { clientAction } from "./client-action.js"; + export { clientAction }; + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; + import { clientAction } from "./client-action.js"; + export { clientAction as _clientAction }; + export const clientAction = _withClientActionContextWrapper(_clientAction, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) + + it("should transform the client action export when it's imported from another file and exported", () => { + const result = injectContext( + ` + import { clientAction } from "./client-action.js"; + export { clientAction }; + `, + "test" + ) + const expected = removeWhitespace(` + import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; + import { clientAction } from "./client-action.js"; + export { clientAction as _clientAction }; + export const clientAction = _withClientActionContextWrapper(_clientAction, "test"); + `) + expect(removeWhitespace(result)).toStrictEqual(expected) + }) +}) diff --git a/src/vite/utils/inject-context.ts b/src/vite/utils/inject-context.ts new file mode 100644 index 0000000..666ef8c --- /dev/null +++ b/src/vite/utils/inject-context.ts @@ -0,0 +1,134 @@ +import type { types as Babel } from "@babel/core" +import type { ParseResult } from "@babel/parser" +import type { NodePath } from "@babel/traverse" +import { gen, parse, t, trav } from "./babel" + +const SERVER_COMPONENT_EXPORTS = ["loader", "action"] +const CLIENT_COMPONENT_EXPORTS = ["clientLoader", "clientAction"] +const ALL_EXPORTS = [...SERVER_COMPONENT_EXPORTS, ...CLIENT_COMPONENT_EXPORTS] + +const transform = (ast: ParseResult<Babel.File>, routeId: string) => { + const hocs: Array<[string, Babel.Identifier]> = [] + function getHocId(path: NodePath, hocName: string) { + const uid = path.scope.generateUidIdentifier(hocName) + const hasHoc = hocs.find(([name]) => name === hocName) + if (hasHoc) { + return uid + } + hocs.push([hocName, uid]) + return uid + } + + function uppercaseFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1) + } + const transformations: Array<() => void> = [] + trav(ast, { + ExportDeclaration(path) { + if (path.isExportNamedDeclaration()) { + const decl = path.get("declaration") + if (decl.isVariableDeclaration()) { + for (const varDeclarator of decl.get("declarations")) { + const id = varDeclarator.get("id") + const init = varDeclarator.get("init") + const expr = init.node + if (!expr) return + if (!id.isIdentifier()) return + const { name } = id.node + + if (!ALL_EXPORTS.includes(name)) return + + const uid = getHocId(path, `with${uppercaseFirstLetter(name)}ContextWrapper`) + init.replaceWith(t.callExpression(uid, [expr, t.stringLiteral(routeId)])) + } + + return + } + + if (decl.isFunctionDeclaration()) { + const { id } = decl.node + if (!id) return + const { name } = id + if (!ALL_EXPORTS.includes(name)) return + + const uid = getHocId(path, `with${uppercaseFirstLetter(name)}ContextWrapper`) + decl.replaceWith( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [toFunctionExpression(decl.node), t.stringLiteral(routeId)]) + ), + ]) + ) + } + } + }, + ExportNamedDeclaration(path) { + const specifiers = path.node.specifiers + for (const specifier of specifiers) { + if (!(t.isExportSpecifier(specifier) && t.isIdentifier(specifier.exported))) { + return + } + const name = specifier.exported.name + if (!ALL_EXPORTS.includes(name)) { + return + } + const uid = getHocId(path, `with${uppercaseFirstLetter(name)}ContextWrapper`) + transformations.push(() => { + const uniqueName = path.scope.generateUidIdentifier(name).name + path.replaceWith( + t.exportNamedDeclaration( + null, + [t.exportSpecifier(t.identifier(name), t.identifier(uniqueName))], + path.node.source + ) + ) + + // Insert the wrapped export after the modified export statement + path.insertAfter( + t.exportNamedDeclaration( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [t.identifier(uniqueName), t.stringLiteral(routeId)]) + ), + ]), + [] + ) + ) + }) + } + }, + }) + for (const transformation of transformations) { + transformation() + } + if (hocs.length > 0) { + ast.program.body.unshift( + t.importDeclaration( + hocs.map(([name, identifier]) => t.importSpecifier(identifier, t.identifier(name))), + t.stringLiteral("react-router-devtools/context") + ) + ) + } + + const didTransform = hocs.length > 0 + return didTransform +} + +function toFunctionExpression(decl: Babel.FunctionDeclaration) { + return t.functionExpression(decl.id, decl.params, decl.body, decl.generator, decl.async) +} + +export function injectContext(code: string, routeId: string) { + try { + const ast = parse(code, { sourceType: "module" }) + const didTransform = transform(ast, routeId) + if (!didTransform) { + return code + } + return gen(ast).code + } catch (e) { + return code + } +} diff --git a/test-apps/react-router-vite/app/root.tsx b/test-apps/react-router-vite/app/root.tsx index 3dfb223..7a4394c 100644 --- a/test-apps/react-router-vite/app/root.tsx +++ b/test-apps/react-router-vite/app/root.tsx @@ -1,7 +1,9 @@ import { + ActionFunctionArgs, data, Form, Links, + LoaderFunctionArgs, Meta, Outlet, Scripts, @@ -12,7 +14,7 @@ import { userSomething } from "./modules/user.server"; export const links = () => []; -export const loader = () => { +export const loader = ({context, devTools }: LoaderFunctionArgs) => { userSomething(); const mainPromise = new Promise((resolve, reject) => { setTimeout(() => { @@ -24,15 +26,19 @@ export const loader = () => { resolve({ test: "test", subPromise}); }, 2000); }); + const start =devTools?.tracing.start("test")!; + devTools?.tracing.end("test", start); return data({ message: "Hello World", mainPromise }, { headers: { "Cache-Control": "max-age=3600, private" } }); } -export const action =async () => { +export const action =async ({devTools}: ActionFunctionArgs) => { + const start = devTools?.tracing.start("action submission") await new Promise((resolve, reject) => { setTimeout(() => { resolve("test"); }, 2000); }); + devTools?.tracing.end("action submission", start!) return ({ message: "Hello World" }); } diff --git a/test-apps/react-router-vite/app/routes/_index.tsx b/test-apps/react-router-vite/app/routes/_index.tsx index 7c37d56..b17ef2d 100644 --- a/test-apps/react-router-vite/app/routes/_index.tsx +++ b/test-apps/react-router-vite/app/routes/_index.tsx @@ -12,7 +12,21 @@ export const meta: MetaFunction = () => { }; -export const loader = async ({ request, }: LoaderFunctionArgs) => { +export const loader = async ({ request, context,devTools }: LoaderFunctionArgs) => { + + const trace = devTools?.tracing.trace + const data = await trace?.("Loader call - GET users", async () => { + + const also = await new Promise((resolve, reject) => { + setTimeout(() => { + resolve("test"); + }, 2000); + }); + return { + custom: "data", + also + } + }) const test = await new Promise((resolve, reject) => { setTimeout(() => { resolve("test"); @@ -23,12 +37,25 @@ export const loader = async ({ request, }: LoaderFunctionArgs) => { resolve("test1"); }, 3500); }); - return { message: "Hello World!", test, test1, }; + return { message: "Hello World!", test, test1, data }; }; -export const clientLoader = async ({ request, serverLoader }: ClientLoaderFunctionArgs) => { +export const clientLoader = async ({ request, serverLoader, devTools }: ClientLoaderFunctionArgs) => { const headers = Object.fromEntries(request.headers.entries()); const serverLoaderResults = await serverLoader(); + + const trace = devTools?.tracing.trace + const data = await trace?.("CLIENT LOADER API call",async () => { + const also = await new Promise((resolve, reject) => { + setTimeout(() => { + resolve("test"); + }, 1000); + }); + return { + custom: "data", + also + } + }) const promise =await new Promise((resolve, reject) => { setTimeout(() => { resolve("test"); diff --git a/tsup-context.config.ts b/tsup-context.config.ts new file mode 100644 index 0000000..0943875 --- /dev/null +++ b/tsup-context.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: ["src/context.ts"], + splitting: false, + sourcemap: false, + clean: false, + dts: true, + format: ["esm"], +})