Skip to content

Commit b877f51

Browse files
committed
feat(tanstackstart-react): Auto-instrument request middleware
1 parent b0add63 commit b877f51

File tree

2 files changed

+283
-24
lines changed

2 files changed

+283
-24
lines changed

packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ type AutoInstrumentMiddlewareOptions = {
66
};
77

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

29-
// Only wrap requestMiddleware and functionMiddleware in createStart()
30-
// createStart() should always be in a file named start.ts
31-
if (!id.includes('start') || !code.includes('createStart(')) {
30+
// Detect file types that should be instrumented
31+
const isStartFile = id.includes('start') && code.includes('createStart(');
32+
const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code);
33+
34+
if (!isStartFile && !isRouteFile) {
3235
return null;
3336
}
3437

@@ -41,26 +44,53 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
4144
let needsImport = false;
4245
const skippedMiddlewares: string[] = [];
4346

44-
transformed = transformed.replace(
45-
/(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g,
46-
(match: string, key: string, contents: string) => {
47-
const objContents = arrayToObjectShorthand(contents);
48-
if (objContents) {
49-
needsImport = true;
50-
if (debug) {
51-
// eslint-disable-next-line no-console
52-
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
47+
// Transform global middleware arrays in createStart() files
48+
if (isStartFile) {
49+
transformed = transformed.replace(
50+
/(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g,
51+
(match: string, key: string, contents: string) => {
52+
const objContents = arrayToObjectShorthand(contents);
53+
if (objContents) {
54+
needsImport = true;
55+
if (debug) {
56+
// eslint-disable-next-line no-console
57+
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
58+
}
59+
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
60+
}
61+
// Track middlewares that couldn't be auto-wrapped
62+
// Skip if we matched whitespace only
63+
if (contents.trim()) {
64+
skippedMiddlewares.push(key);
5365
}
54-
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
55-
}
56-
// Track middlewares that couldn't be auto-wrapped
57-
// Skip if we matched whitespace only
58-
if (contents.trim()) {
59-
skippedMiddlewares.push(key);
60-
}
61-
return match;
62-
},
63-
);
66+
return match;
67+
},
68+
);
69+
}
70+
71+
// Transform route middleware arrays in createFileRoute() files
72+
if (isRouteFile) {
73+
transformed = transformed.replace(
74+
/(\s+)(middleware)\s*:\s*\[([^\]]*)\]/g,
75+
(match: string, whitespace: string, key: string, contents: string) => {
76+
const objContents = arrayToObjectShorthand(contents);
77+
if (objContents) {
78+
needsImport = true;
79+
if (debug) {
80+
// eslint-disable-next-line no-console
81+
console.log(`[Sentry] Auto-wrapping route ${key} in ${id}`);
82+
}
83+
return `${whitespace}${key}: wrapMiddlewaresWithSentry(${objContents})`;
84+
}
85+
// Track middlewares that couldn't be auto-wrapped
86+
// Skip if we matched whitespace only
87+
if (contents.trim()) {
88+
skippedMiddlewares.push(`route ${key}`);
89+
}
90+
return match;
91+
},
92+
);
93+
}
6494

6595
// Warn about middlewares that couldn't be auto-wrapped
6696
if (skippedMiddlewares.length > 0) {

packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,235 @@ createStart(() => ({
193193
});
194194
});
195195

196+
describe('route-level middleware auto-instrumentation', () => {
197+
const routeFileWithMiddleware = `
198+
import { createFileRoute } from '@tanstack/react-router';
199+
import { loggingMiddleware } from '../middleware';
200+
201+
export const Route = createFileRoute('/api/test')({
202+
server: {
203+
middleware: [loggingMiddleware],
204+
handlers: {
205+
GET: async () => ({ message: 'test' }),
206+
},
207+
},
208+
});
209+
`;
210+
211+
it('instruments route-level middleware arrays', () => {
212+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
213+
const result = plugin.transform(routeFileWithMiddleware, '/app/routes/api.test.ts');
214+
215+
expect(result).not.toBeNull();
216+
expect(result!.code).toContain("import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'");
217+
expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })');
218+
});
219+
220+
it('instruments multiple middlewares in route file', () => {
221+
const code = `
222+
import { createFileRoute } from '@tanstack/react-router';
223+
export const Route = createFileRoute('/foo')({
224+
server: {
225+
middleware: [authMiddleware, loggingMiddleware],
226+
handlers: { GET: () => ({}) },
227+
},
228+
});
229+
`;
230+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
231+
const result = plugin.transform(code, '/app/routes/foo.ts');
232+
233+
expect(result).not.toBeNull();
234+
expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ authMiddleware, loggingMiddleware })');
235+
});
236+
237+
it('does not instrument files without createFileRoute', () => {
238+
const code = `
239+
const middleware = [someMiddleware];
240+
export const foo = { middleware };
241+
`;
242+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
243+
const result = plugin.transform(code, '/app/utils.ts');
244+
245+
expect(result).toBeNull();
246+
});
247+
248+
it('does not instrument route files without middleware arrays', () => {
249+
const code = `
250+
import { createFileRoute } from '@tanstack/react-router';
251+
export const Route = createFileRoute('/client')({
252+
component: () => '<div>Client only</div>',
253+
});
254+
`;
255+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
256+
const result = plugin.transform(code, '/app/routes/client.tsx');
257+
258+
expect(result).toBeNull();
259+
});
260+
261+
it('does not instrument empty middleware arrays in route files', () => {
262+
const code = `
263+
import { createFileRoute } from '@tanstack/react-router';
264+
export const Route = createFileRoute('/foo')({
265+
server: {
266+
middleware: [],
267+
handlers: { GET: () => ({}) },
268+
},
269+
});
270+
`;
271+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
272+
const result = plugin.transform(code, '/app/routes/foo.ts');
273+
274+
expect(result).toBeNull();
275+
});
276+
});
277+
278+
describe('handler-specific middleware auto-instrumentation', () => {
279+
it('instruments handler-level middleware arrays', () => {
280+
const code = `
281+
import { createFileRoute } from '@tanstack/react-router';
282+
export const Route = createFileRoute('/foo')({
283+
server: {
284+
handlers: ({ createHandlers }) =>
285+
createHandlers({
286+
GET: {
287+
middleware: [loggingMiddleware],
288+
handler: () => ({ data: 'test' }),
289+
},
290+
}),
291+
},
292+
});
293+
`;
294+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
295+
const result = plugin.transform(code, '/app/routes/foo.ts');
296+
297+
expect(result).not.toBeNull();
298+
expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })');
299+
});
300+
301+
it('instruments multiple handler-level middleware arrays in same file', () => {
302+
const code = `
303+
import { createFileRoute } from '@tanstack/react-router';
304+
export const Route = createFileRoute('/foo')({
305+
server: {
306+
handlers: ({ createHandlers }) =>
307+
createHandlers({
308+
GET: {
309+
middleware: [readMiddleware],
310+
handler: () => ({}),
311+
},
312+
POST: {
313+
middleware: [writeMiddleware, authMiddleware],
314+
handler: () => ({}),
315+
},
316+
}),
317+
},
318+
});
319+
`;
320+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
321+
const result = plugin.transform(code, '/app/routes/foo.ts');
322+
323+
expect(result).not.toBeNull();
324+
expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ readMiddleware })');
325+
expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ writeMiddleware, authMiddleware })');
326+
});
327+
});
328+
329+
describe('route middleware edge cases', () => {
330+
it('does not wrap middleware containing function calls in route files', () => {
331+
const code = `
332+
import { createFileRoute } from '@tanstack/react-router';
333+
export const Route = createFileRoute('/foo')({
334+
server: {
335+
middleware: [createMiddleware()],
336+
handlers: { GET: () => ({}) },
337+
},
338+
});
339+
`;
340+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
341+
const result = plugin.transform(code, '/app/routes/foo.ts');
342+
343+
expect(result).toBeNull();
344+
});
345+
346+
it('warns about route middleware that cannot be auto-wrapped', () => {
347+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
348+
349+
const code = `
350+
import { createFileRoute } from '@tanstack/react-router';
351+
export const Route = createFileRoute('/foo')({
352+
server: {
353+
middleware: [getMiddleware()],
354+
handlers: { GET: () => ({}) },
355+
},
356+
});
357+
`;
358+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
359+
plugin.transform(code, '/app/routes/foo.ts');
360+
361+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Could not auto-instrument route middleware'));
362+
363+
consoleWarnSpy.mockRestore();
364+
});
365+
366+
it('handles route files with use server directive', () => {
367+
const code = `'use server';
368+
import { createFileRoute } from '@tanstack/react-router';
369+
export const Route = createFileRoute('/foo')({
370+
server: {
371+
middleware: [authMiddleware],
372+
handlers: { GET: () => ({}) },
373+
},
374+
});
375+
`;
376+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
377+
const result = plugin.transform(code, '/app/routes/foo.ts');
378+
379+
expect(result).not.toBeNull();
380+
expect(result!.code).toMatch(/^'use server';\s*\nimport \{ wrapMiddlewaresWithSentry \}/);
381+
});
382+
383+
it('does not instrument route files that already use wrapMiddlewaresWithSentry', () => {
384+
const code = `
385+
import { createFileRoute } from '@tanstack/react-router';
386+
import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';
387+
export const Route = createFileRoute('/foo')({
388+
server: {
389+
middleware: wrapMiddlewaresWithSentry({ authMiddleware }),
390+
handlers: { GET: () => ({}) },
391+
},
392+
});
393+
`;
394+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
395+
const result = plugin.transform(code, '/app/routes/foo.ts');
396+
397+
expect(result).toBeNull();
398+
});
399+
400+
it('handles both route-level and handler-level middleware in same file', () => {
401+
const code = `
402+
import { createFileRoute } from '@tanstack/react-router';
403+
export const Route = createFileRoute('/foo')({
404+
server: {
405+
middleware: [routeMiddleware],
406+
handlers: ({ createHandlers }) =>
407+
createHandlers({
408+
GET: {
409+
middleware: [getMiddleware],
410+
handler: () => ({}),
411+
},
412+
}),
413+
},
414+
});
415+
`;
416+
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
417+
const result = plugin.transform(code, '/app/routes/foo.ts');
418+
419+
expect(result).not.toBeNull();
420+
expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ routeMiddleware })');
421+
expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ getMiddleware })');
422+
});
423+
});
424+
196425
describe('arrayToObjectShorthand', () => {
197426
it('converts single identifier', () => {
198427
expect(arrayToObjectShorthand('foo')).toBe('{ foo }');

0 commit comments

Comments
 (0)