Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async (
return next();
});

// Server route request middleware
const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
// Server route request middleware - exported unwrapped for auto-instrumentation via Vite plugin
export const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
console.log('Server route request middleware executed');
return next();
});
Expand All @@ -40,14 +40,9 @@ const errorMiddleware = createMiddleware({ type: 'function' }).server(async () =
});

// Manually wrap middlewares with Sentry (for middlewares that won't be auto-instrumented)
export const [
wrappedServerFnMiddleware,
wrappedServerRouteRequestMiddleware,
wrappedEarlyReturnMiddleware,
wrappedErrorMiddleware,
] = wrapMiddlewaresWithSentry({
serverFnMiddleware,
serverRouteRequestMiddleware,
earlyReturnMiddleware,
errorMiddleware,
});
export const [wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware] =
wrapMiddlewaresWithSentry({
serverFnMiddleware,
earlyReturnMiddleware,
errorMiddleware,
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createFileRoute } from '@tanstack/react-router';
import { wrappedServerRouteRequestMiddleware } from '../middleware';
import { serverRouteRequestMiddleware } from '../middleware';

export const Route = createFileRoute('/api/test-middleware')({
server: {
middleware: [wrappedServerRouteRequestMiddleware],
middleware: [serverRouteRequestMiddleware],
handlers: {
GET: async () => {
return { message: 'Server route middleware test' };
Expand Down
136 changes: 100 additions & 36 deletions packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,58 @@ type AutoInstrumentMiddlewareOptions = {
debug?: boolean;
};

type WrapResult = {
code: string;
didWrap: boolean;
skipped: string[];
};

/**
* Core function that wraps middleware arrays matching the given regex.
*/
function wrapMiddlewareArrays(code: string, id: string, debug: boolean, regex: RegExp): WrapResult {
const skipped: string[] = [];
let didWrap = false;

const transformed = code.replace(regex, (match: string, key: string, contents: string) => {
const objContents = arrayToObjectShorthand(contents);
if (objContents) {
didWrap = true;
if (debug) {
// eslint-disable-next-line no-console
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
}
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
}
// Track middlewares that couldn't be auto-wrapped
// Skip if we matched whitespace only
if (contents.trim()) {
skipped.push(key);
}
return match;
});

return { code: transformed, didWrap, skipped };
}

/**
* Wraps global middleware arrays (requestMiddleware, functionMiddleware) in createStart() files.
*/
export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): WrapResult {
return wrapMiddlewareArrays(code, id, debug, /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g);
}

/**
* Wraps route middleware arrays in createFileRoute() files.
*/
export function wrapRouteMiddleware(code: string, id: string, debug: boolean): WrapResult {
return wrapMiddlewareArrays(code, id, debug, /(middleware)\s*:\s*\[([^\]]*)\]/g);
}

/**
* A Vite plugin that automatically instruments TanStack Start middlewares
* by wrapping `requestMiddleware` and `functionMiddleware` arrays in `createStart()`.
* A Vite plugin that automatically instruments TanStack Start middlewares:
* - `requestMiddleware` and `functionMiddleware` arrays in `createStart()`
* - `middleware` arrays in `createFileRoute()` route definitions
*/
export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddlewareOptions = {}): Plugin {
const { enabled = true, debug = false } = options;
Expand All @@ -26,9 +75,11 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
return null;
}

// Only wrap requestMiddleware and functionMiddleware in createStart()
// createStart() should always be in a file named start.ts
if (!id.includes('start') || !code.includes('createStart(')) {
// Detect file types that should be instrumented
const isStartFile = id.includes('start') && code.includes('createStart(');
const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code);

if (!isStartFile && !isRouteFile) {
return null;
}

Expand All @@ -41,26 +92,26 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
let needsImport = false;
const skippedMiddlewares: string[] = [];

transformed = transformed.replace(
/(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g,
(match: string, key: string, contents: string) => {
const objContents = arrayToObjectShorthand(contents);
if (objContents) {
needsImport = true;
if (debug) {
// eslint-disable-next-line no-console
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
}
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
}
// Track middlewares that couldn't be auto-wrapped
// Skip if we matched whitespace only
if (contents.trim()) {
skippedMiddlewares.push(key);
}
return match;
},
);
switch (true) {
// global middleware
case isStartFile: {
const result = wrapGlobalMiddleware(transformed, id, debug);
transformed = result.code;
needsImport = needsImport || result.didWrap;
skippedMiddlewares.push(...result.skipped);
break;
}
// route middleware
case isRouteFile: {
const result = wrapRouteMiddleware(transformed, id, debug);
transformed = result.code;
needsImport = needsImport || result.didWrap;
skippedMiddlewares.push(...result.skipped);
break;
}
default:
break;
}

// Warn about middlewares that couldn't be auto-wrapped
if (skippedMiddlewares.length > 0) {
Expand All @@ -76,17 +127,7 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
return null;
}

const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n";

// Check for 'use server' or 'use client' directives, these need to be before any imports
const directiveMatch = transformed.match(/^(['"])use (client|server)\1;?\s*\n?/);
if (directiveMatch) {
// Insert import after the directive
const directive = directiveMatch[0];
transformed = directive + sentryImport + transformed.slice(directive.length);
} else {
transformed = sentryImport + transformed;
}
transformed = addSentryImport(transformed);

return { code: transformed, map: null };
},
Expand Down Expand Up @@ -117,3 +158,26 @@ export function arrayToObjectShorthand(contents: string): string | null {

return `{ ${uniqueItems.join(', ')} }`;
}

/**
* Adds the wrapMiddlewaresWithSentry import to the code.
* Handles 'use client' and 'use server' directives by inserting the import after them.
*/
export function addSentryImport(code: string): string {
const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n";

// Don't add the import if it already exists
if (code.includes(sentryImport.trimEnd())) {
return code;
}

// Check for 'use server' or 'use client' directives, these need to be before any imports
const directiveMatch = code.match(/^(['"])use (client|server)\1;?\s*\n?/);

if (!directiveMatch) {
return sentryImport + code;
}

const directive = directiveMatch[0];
return directive + sentryImport + code.slice(directive.length);
}
Loading
Loading