UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

415 lines 21.8 kB
"use strict"; /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const recast = __importStar(require("recast")); const server_entry_1 = require("../../../src/react-router/codemods/server-entry"); // @ts-expect-error - magicast is ESM and TS complains about that. It works though const magicast_1 = require("magicast"); vitest_1.vi.mock('@clack/prompts', () => { const mock = { log: { warn: vitest_1.vi.fn(), info: vitest_1.vi.fn(), success: vitest_1.vi.fn(), }, }; return { default: mock, ...mock, }; }); vitest_1.vi.mock('../../../src/utils/debug', () => ({ debug: vitest_1.vi.fn(), })); (0, vitest_1.describe)('instrumentServerEntry', () => { const fixturesDir = path.join(__dirname, 'fixtures', 'server-entry'); let tmpDir; let tmpFile; (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); // Create unique tmp directory for each test tmpDir = path.join(__dirname, 'fixtures', 'tmp', `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); tmpFile = path.join(tmpDir, 'entry.server.tsx'); // Ensure tmp directory exists fs.mkdirSync(tmpDir, { recursive: true }); }); (0, vitest_1.afterEach)(() => { // Clean up tmp directory if (fs.existsSync(tmpDir)) { fs.rmSync(tmpDir, { recursive: true }); } }); (0, vitest_1.it)('should add Sentry import and wrap handleRequest function', async () => { const basicContent = fs.readFileSync(path.join(fixturesDir, 'basic.tsx'), 'utf8'); fs.writeFileSync(tmpFile, basicContent); await (0, server_entry_1.instrumentServerEntry)(tmpFile); const modifiedContent = fs.readFileSync(tmpFile, 'utf8'); // Should add Sentry import (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";'); // Should wrap the existing handleRequest function (0, vitest_1.expect)(modifiedContent).toContain('export default Sentry.wrapSentryHandleRequest(handleRequest);'); // Should add the Sentry import at the top of the file (after existing imports) const lines = modifiedContent.split('\n'); const sentryImportLine = lines.findIndex((line) => line.includes('import * as Sentry from "@sentry/react-router";')); (0, vitest_1.expect)(sentryImportLine).toBeGreaterThanOrEqual(0); // Should create default handleError since none exists (0, vitest_1.expect)(modifiedContent).toContain('export const handleError = Sentry.createSentryHandleError({'); (0, vitest_1.expect)(modifiedContent).toContain('logErrors: false'); }); (0, vitest_1.it)('should handle already instrumented server entry without duplication', async () => { const alreadyInstrumentedContent = fs.readFileSync(path.join(fixturesDir, 'already-instrumented.tsx'), 'utf8'); fs.writeFileSync(tmpFile, alreadyInstrumentedContent); await (0, server_entry_1.instrumentServerEntry)(tmpFile); const modifiedContent = fs.readFileSync(tmpFile, 'utf8'); // Should not add duplicate imports or wrapping since already instrumented (0, vitest_1.expect)(modifiedContent).toContain("import * as Sentry from '@sentry/react-router';"); (0, vitest_1.expect)(modifiedContent).toContain('export default Sentry.wrapSentryHandleRequest(handleRequest);'); // Should NOT add a new createSentryHandleError export since handleError already has captureException (0, vitest_1.expect)(modifiedContent).not.toContain('export const handleError = Sentry.createSentryHandleError({'); // Should preserve the existing handleError function with captureException (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);'); (0, vitest_1.expect)(modifiedContent).toContain('export async function handleError'); }); (0, vitest_1.it)('should handle variable export pattern with existing export', async () => { const variableExportContent = fs.readFileSync(path.join(fixturesDir, 'variable-export.tsx'), 'utf8'); fs.writeFileSync(tmpFile, variableExportContent); await (0, server_entry_1.instrumentServerEntry)(tmpFile); const modifiedContent = fs.readFileSync(tmpFile, 'utf8'); // Should add Sentry import and wrap handleRequest (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";'); (0, vitest_1.expect)(modifiedContent).toContain('export default Sentry.wrapSentryHandleRequest(handleRequest);'); // Should instrument the existing handleError variable with captureException (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);'); // Should preserve the variable export pattern (0, vitest_1.expect)(modifiedContent).toContain('export const handleError'); }); }); (0, vitest_1.describe)('instrumentHandleRequest', () => { let tmpDir; (0, vitest_1.beforeEach)(() => { tmpDir = path.join(__dirname, 'fixtures', 'tmp', `handle-request-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); fs.mkdirSync(tmpDir, { recursive: true }); }); (0, vitest_1.afterEach)(() => { if (fs.existsSync(tmpDir)) { fs.rmSync(tmpDir, { recursive: true }); } }); (0, vitest_1.it)('should add required imports when creating new handleRequest', async () => { const content = `// Empty server entry file`; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); (0, server_entry_1.instrumentHandleRequest)(mod); // Check if required imports were added const imports = mod.imports.$items; const hasServerRouter = imports.some((item) => item.imported === 'ServerRouter' && item.from === 'react-router'); const hasRenderToPipeableStream = imports.some((item) => item.imported === 'renderToPipeableStream' && item.from === 'react-dom/server'); (0, vitest_1.expect)(hasServerRouter).toBe(true); (0, vitest_1.expect)(hasRenderToPipeableStream).toBe(true); }); (0, vitest_1.it)('should not duplicate imports if they already exist', async () => { const content = ` import { ServerRouter } from 'react-router'; import { renderToPipeableStream } from 'react-dom/server'; import { createReadableStreamFromReadable } from '@react-router/node'; `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); const originalImportsCount = mod.imports.$items.length; (0, server_entry_1.instrumentHandleRequest)(mod); // Should not add duplicate imports (0, vitest_1.expect)(mod.imports.$items.length).toBe(originalImportsCount); }); }); (0, vitest_1.describe)('instrumentHandleError', () => { let tmpDir; (0, vitest_1.beforeEach)(() => { tmpDir = path.join(__dirname, 'fixtures', 'tmp', `handle-error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); fs.mkdirSync(tmpDir, { recursive: true }); }); (0, vitest_1.afterEach)(() => { if (fs.existsSync(tmpDir)) { fs.rmSync(tmpDir, { recursive: true }); } }); (0, vitest_1.it)('should not modify existing handleError with captureException', async () => { const content = ` export function handleError(error: unknown) { Sentry.captureException(error); console.error(error); } `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); const originalBodyLength = mod.$ast.body.length; (0, server_entry_1.instrumentHandleError)(mod); // Should not modify since captureException already exists (0, vitest_1.expect)(mod.$ast.body.length).toBe(originalBodyLength); }); (0, vitest_1.it)('should not modify existing handleError with createSentryHandleError', async () => { const content = ` export const handleError = Sentry.createSentryHandleError({ logErrors: false }); `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); // eslint-disable-next-line @typescript-eslint/no-explicit-any const originalBodyLength = mod.$ast.body.length; (0, server_entry_1.instrumentHandleError)(mod); // Should not modify since createSentryHandleError already exists // eslint-disable-next-line @typescript-eslint/no-explicit-any (0, vitest_1.expect)(mod.$ast.body.length).toBe(originalBodyLength); }); (0, vitest_1.it)('should add captureException to existing handleError function declaration without breaking AST', async () => { const content = ` export function handleError(error: unknown) { console.error('Custom error handling:', error); // some other logic here } `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); // This should not throw an error due to broken AST manipulation (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow(); // Verify the function was modified correctly const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code; (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)'); (0, vitest_1.expect)(modifiedCode).toContain("console.error('Custom error handling:', error)"); }); (0, vitest_1.it)('should add captureException to existing handleError variable declaration without breaking AST', async () => { const content = ` export const handleError = (error: unknown, { request }: { request: Request }) => { console.log('Handling error:', error.message); return new Response('Error occurred', { status: 500 }); }; `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); // This should not throw an error due to broken AST manipulation (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow(); // Verify the function was modified correctly const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code; (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)'); (0, vitest_1.expect)(modifiedCode).toContain("console.log('Handling error:', error.message)"); }); (0, vitest_1.it)('should handle existing handleError with only error parameter and add request parameter', async () => { const content = ` export const handleError = (error: unknown) => { console.error('Simple error handler:', error); }; `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); // This should not throw an error due to broken AST manipulation (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow(); // Verify the function signature was updated correctly const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code; (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)'); (0, vitest_1.expect)(modifiedCode).toContain('if (!request.signal.aborted)'); // Should add request parameter (0, vitest_1.expect)(modifiedCode).toMatch(/handleError.*=.*\(\s*error.*,\s*\{\s*request\s*\}/); }); }); (0, vitest_1.describe)('instrumentHandleError AST manipulation edge cases', () => { let tmpDir; (0, vitest_1.beforeEach)(() => { tmpDir = path.join(__dirname, 'fixtures', 'tmp', `ast-edge-cases-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); fs.mkdirSync(tmpDir, { recursive: true }); }); (0, vitest_1.afterEach)(() => { if (fs.existsSync(tmpDir)) { fs.rmSync(tmpDir, { recursive: true }); } }); (0, vitest_1.it)('should handle function declaration with existing try-catch block', async () => { const content = ` export function handleError(error: unknown, { request }: { request: Request }) { try { console.error('Error occurred:', error); logToExternalService(error); } catch (loggingError) { console.warn('Failed to log error:', loggingError); } } `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); // This test will expose the broken AST logic (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow(); const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code; (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)'); (0, vitest_1.expect)(modifiedCode).toContain('if (!request.signal.aborted)'); // Should preserve existing try-catch (0, vitest_1.expect)(modifiedCode).toContain('try {'); (0, vitest_1.expect)(modifiedCode).toContain('} catch (loggingError) {'); }); (0, vitest_1.it)('should handle arrow function with block body', async () => { const content = ` export const handleError = (error: unknown, context: any) => { const { request } = context; console.error('Error in route:', error); return new Response('Internal Server Error', { status: 500 }); }; `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); // This test will expose the broken AST logic (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow(); const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code; (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)'); (0, vitest_1.expect)(modifiedCode).toContain('if (!request.signal.aborted)'); }); (0, vitest_1.it)('should demonstrate that the AST bug is now fixed - no longer throws TypeError', async () => { const content = ` export function handleError(error: unknown) { console.error('Error occurred:', error); } `; const tempFile = path.join(tmpDir, 'entry.server.tsx'); fs.writeFileSync(tempFile, content); const mod = await (0, magicast_1.loadFile)(tempFile); // This test specifically targets the broken AST logic at lines 279-284 in server-entry.ts // The bug is in this code: // implementation.declarations[0].init.arguments[0].body.body.unshift(...) // Where 'implementation' is an IfStatement, not a VariableDeclaration let thrownError = null; try { (0, server_entry_1.instrumentHandleError)(mod); } catch (error) { thrownError = error; } // The bug is fixed - no error should be thrown (0, vitest_1.expect)(thrownError).toBeNull(); // And the code should be successfully modified const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code; (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)'); // The error occurs because recast.parse() creates an IfStatement: // { type: 'IfStatement', test: ..., consequent: ... } // But the code tries to access .declarations[0] as if it were a VariableDeclaration }); (0, vitest_1.it)('should demonstrate the specific line that breaks - recast.parse creates IfStatement not VariableDeclaration', () => { // This test shows exactly what the problematic line 278 in server-entry.ts creates const problematicCode = `if (!request.signal.aborted) { Sentry.captureException(error); }`; // This is what line 278 does: recast.parse(problematicCode).program.body[0] const implementation = recast.parse(problematicCode).program.body[0]; // The implementation is an IfStatement, not a VariableDeclaration (0, vitest_1.expect)(implementation.type).toBe('IfStatement'); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unnecessary-type-assertion (0, vitest_1.expect)(implementation.declarations).toBeUndefined(); // But lines 279-284 try to access implementation.declarations[0].init.arguments[0].body.body // This will throw "Cannot read properties of undefined (reading '0')" (0, vitest_1.expect)(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-assertion const declarations = implementation.declarations; // eslint-disable-next-line @typescript-eslint/no-unsafe-return return declarations[0]; // This line will throw the error }).toThrow('Cannot read properties of undefined'); }); }); // Test for Bug #1: Array access vulnerability (0, vitest_1.describe)('Array access vulnerability bugs', () => { let tmpDir; let tmpFile; (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); // Create unique tmp directory for each test tmpDir = path.join(__dirname, 'fixtures', 'tmp', `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); tmpFile = path.join(tmpDir, 'entry.server.tsx'); // Ensure tmp directory exists fs.mkdirSync(tmpDir, { recursive: true }); }); (0, vitest_1.afterEach)(() => { // Clean up tmp directory if (fs.existsSync(tmpDir)) { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); (0, vitest_1.it)('should safely handle VariableDeclaration with empty declarations array', () => { // This test verifies that the bug fix works correctly // Previously this would crash, but now it handles empty arrays safely // The implementation now includes proper safety checks, so we test that // it can handle edge cases without crashing // Test the actual safe implementation behavior const testResult = () => { // Simulate the safe check logic from the actual implementation const declarations = []; // Empty array if (!declarations || declarations.length === 0) { return false; // Safe early return } // This code would never be reached due to the safe check return declarations[0].id.name === 'handleError'; }; // Should return false safely without throwing (0, vitest_1.expect)(testResult()).toBe(false); }); (0, vitest_1.it)('should safely handle VariableDeclaration with empty declarations array after fix', async () => { // This test will pass after we fix the bug fs.writeFileSync(tmpFile, 'export const handleError = () => {};'); const mod = await (0, magicast_1.loadFile)(tmpFile); // Create a problematic AST structure const problematicNode = { type: 'ExportNamedDeclaration', declaration: { type: 'VariableDeclaration', kind: 'const', declarations: [], // Empty declarations array }, }; // Add the problematic node to the AST // @ts-expect-error - We need to access body for this test even though it's typed as any mod.$ast.body.push(problematicNode); // After the fix, this should NOT throw an error let thrownError = null; try { (0, server_entry_1.instrumentHandleError)(mod); } catch (error) { thrownError = error; } // After the fix, no error should be thrown (0, vitest_1.expect)(thrownError).toBeNull(); }); }); //# sourceMappingURL=server-entry.test.js.map