UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

411 lines 20.9 kB
"use strict"; 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 { clackMocks } = vitest_1.vi.hoisted(() => { const info = vitest_1.vi.fn(); const warn = vitest_1.vi.fn(); const error = vitest_1.vi.fn(); const success = vitest_1.vi.fn(); const outro = vitest_1.vi.fn(); const confirm = vitest_1.vi.fn(() => Promise.resolve(false)); // default to false for tests return { clackMocks: { info, warn, error, success, outro, confirm, }, }; }); vitest_1.vi.mock('@clack/prompts', () => { return { __esModule: true, default: { log: { info: clackMocks.info, warn: clackMocks.warn, error: clackMocks.error, success: clackMocks.success, }, outro: clackMocks.outro, confirm: clackMocks.confirm, }, }; }); const { existsSyncMock, readFileSyncMock, writeFileSyncMock } = vitest_1.vi.hoisted(() => { return { existsSyncMock: vitest_1.vi.fn(), readFileSyncMock: vitest_1.vi.fn(), writeFileSyncMock: vitest_1.vi.fn(), }; }); const { getPackageDotJsonMock, getPackageVersionMock } = vitest_1.vi.hoisted(() => ({ getPackageDotJsonMock: vitest_1.vi.fn(), getPackageVersionMock: vitest_1.vi.fn(), })); vitest_1.vi.mock('../../src/utils/package-json', () => ({ getPackageDotJson: getPackageDotJsonMock, getPackageVersion: getPackageVersionMock, })); vitest_1.vi.mock('fs', async () => { return { ...(await vitest_1.vi.importActual('fs')), existsSync: existsSyncMock, readFileSync: readFileSyncMock, writeFileSync: writeFileSyncMock, promises: { writeFile: vitest_1.vi.fn(), }, }; }); // module-level mock for child_process.execSync vitest_1.vi.mock('child_process', () => ({ __esModule: true, execSync: vitest_1.vi.fn(), })); // mock showCopyPasteInstructions and makeCodeSnippet used by templates vitest_1.vi.mock('../../src/utils/clack', () => { return { __esModule: true, showCopyPasteInstructions: vitest_1.vi.fn(() => Promise.resolve()), makeCodeSnippet: vitest_1.vi.fn((colors, callback) => { // Mock implementation that just calls the callback with simple string functions const unchanged = (str) => str; const plus = (str) => `+ ${str}`; const minus = (str) => `- ${str}`; return callback(unchanged, plus, minus); }), getPackageDotJson: getPackageDotJsonMock, }; }); const sdk_setup_1 = require("../../src/react-router/sdk-setup"); const childProcess = __importStar(require("child_process")); const templates_1 = require("../../src/react-router/templates"); (0, vitest_1.describe)('React Router SDK Setup', () => { (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); vitest_1.vi.resetAllMocks(); getPackageVersionMock.mockImplementation((packageName, packageJson) => { if (packageJson.dependencies?.[packageName]) { return packageJson.dependencies[packageName]; } if (packageJson.devDependencies?.[packageName]) { return packageJson.devDependencies[packageName]; } return null; }); }); (0, vitest_1.describe)('isReactRouterV7', () => { (0, vitest_1.it)('should return true for React Router v7+ in dependencies or devDependencies', () => { (0, vitest_1.expect)((0, sdk_setup_1.isReactRouterV7)({ dependencies: { '@react-router/dev': '7.0.0' } })).toBe(true); (0, vitest_1.expect)((0, sdk_setup_1.isReactRouterV7)({ dependencies: { '@react-router/dev': '^7.1.0' } })).toBe(true); (0, vitest_1.expect)((0, sdk_setup_1.isReactRouterV7)({ devDependencies: { '@react-router/dev': '7.1.0' }, })).toBe(true); }); (0, vitest_1.it)('should return false for React Router v6 or missing dependency', () => { (0, vitest_1.expect)((0, sdk_setup_1.isReactRouterV7)({ dependencies: { '@react-router/dev': '6.28.0' } })).toBe(false); (0, vitest_1.expect)((0, sdk_setup_1.isReactRouterV7)({ dependencies: { react: '^18.0.0' } })).toBe(false); (0, vitest_1.expect)((0, sdk_setup_1.isReactRouterV7)({})).toBe(false); }); }); (0, vitest_1.describe)('generateServerInstrumentation', () => { (0, vitest_1.it)('should generate server instrumentation file with all features enabled', () => { const dsn = 'https://sentry.io/123'; const enableTracing = true; const enableProfiling = false; const enableLogs = true; const result = (0, templates_1.getSentryInstrumentationServerContent)(dsn, enableTracing, enableProfiling, enableLogs); (0, vitest_1.expect)(result).toContain('dsn: "https://sentry.io/123"'); (0, vitest_1.expect)(result).toContain('tracesSampleRate: 1'); (0, vitest_1.expect)(result).toContain('enableLogs: true'); }); (0, vitest_1.it)('should generate server instrumentation file when performance is disabled', () => { const dsn = 'https://sentry.io/123'; const enableTracing = false; const enableProfiling = false; const enableLogs = false; const result = (0, templates_1.getSentryInstrumentationServerContent)(dsn, enableTracing, enableProfiling, enableLogs); (0, vitest_1.expect)(result).toContain('dsn: "https://sentry.io/123"'); (0, vitest_1.expect)(result).toContain('tracesSampleRate: 0'); (0, vitest_1.expect)(result).not.toContain('enableLogs: true'); }); }); }); (0, vitest_1.describe)('runReactRouterReveal', () => { (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); vitest_1.vi.resetAllMocks(); }); (0, vitest_1.it)('runs the reveal CLI when entry files are missing', () => { existsSyncMock.mockReturnValue(false); childProcess.execSync.mockImplementation(() => 'ok'); (0, sdk_setup_1.runReactRouterReveal)(false); (0, vitest_1.expect)(childProcess.execSync).toHaveBeenCalledWith('npx react-router reveal', { encoding: 'utf8', stdio: 'pipe', }); }); (0, vitest_1.it)('does not run the reveal CLI when entry files already exist', () => { existsSyncMock.mockReturnValue(true); childProcess.execSync.mockReset(); (0, sdk_setup_1.runReactRouterReveal)(false); (0, vitest_1.expect)(childProcess.execSync).not.toHaveBeenCalled(); }); }); (0, vitest_1.describe)('server instrumentation helpers', () => { (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); vitest_1.vi.resetAllMocks(); }); (0, vitest_1.it)('createServerInstrumentationFile writes instrumentation file and returns path', () => { writeFileSyncMock.mockImplementation(() => undefined); const path = (0, sdk_setup_1.createServerInstrumentationFile)('https://sentry.io/123', { performance: true, replay: false, logs: true, profiling: false, }); (0, vitest_1.expect)(path).toContain('instrument.server.mjs'); (0, vitest_1.expect)(writeFileSyncMock).toHaveBeenCalled(); const writtenCall = writeFileSyncMock.mock.calls[0]; (0, vitest_1.expect)(writtenCall[0]).toEqual(vitest_1.expect.stringContaining('instrument.server.mjs')); (0, vitest_1.expect)(writtenCall[1]).toEqual(vitest_1.expect.stringContaining('dsn: "https://sentry.io/123"')); (0, vitest_1.expect)(writtenCall[1]).toEqual(vitest_1.expect.stringContaining('tracesSampleRate: 1')); }); }); (0, vitest_1.describe)('tryRevealAndGetManualInstructions', () => { (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); vitest_1.vi.resetAllMocks(); }); (0, vitest_1.it)('should return true when user confirms and reveal command succeeds', async () => { const missingFilename = 'entry.client.tsx'; const filePath = '/app/entry.client.tsx'; // Mock user confirming the reveal operation clackMocks.confirm.mockResolvedValueOnce(true); // Mock execSync succeeding childProcess.execSync.mockReturnValueOnce('Successfully generated entry files'); // Mock file existing after reveal existsSyncMock.mockReturnValueOnce(true); const result = await (0, sdk_setup_1.tryRevealAndGetManualInstructions)(missingFilename, filePath); (0, vitest_1.expect)(result).toBe(true); (0, vitest_1.expect)(clackMocks.confirm).toHaveBeenCalledWith({ message: vitest_1.expect.stringContaining('Would you like to try running'), initialValue: true, }); (0, vitest_1.expect)(clackMocks.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Running')); (0, vitest_1.expect)(childProcess.execSync).toHaveBeenCalledWith('npx react-router reveal', { encoding: 'utf8', stdio: 'pipe', }); (0, vitest_1.expect)(clackMocks.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Found entry.client.tsx after running reveal')); }); (0, vitest_1.it)('should return false when user declines reveal operation', async () => { const missingFilename = 'entry.server.tsx'; const filePath = '/app/entry.server.tsx'; // Mock user declining the reveal operation clackMocks.confirm.mockResolvedValueOnce(false); const result = await (0, sdk_setup_1.tryRevealAndGetManualInstructions)(missingFilename, filePath); (0, vitest_1.expect)(result).toBe(false); (0, vitest_1.expect)(clackMocks.confirm).toHaveBeenCalled(); (0, vitest_1.expect)(childProcess.execSync).not.toHaveBeenCalled(); (0, vitest_1.expect)(clackMocks.info).not.toHaveBeenCalled(); }); (0, vitest_1.it)('should return false when reveal command succeeds but file still does not exist', async () => { const missingFilename = 'entry.client.jsx'; const filePath = '/app/entry.client.jsx'; // Mock user confirming the reveal operation clackMocks.confirm.mockResolvedValueOnce(true); // Mock execSync succeeding childProcess.execSync.mockReturnValueOnce('Command output'); // Mock file NOT existing after reveal existsSyncMock.mockReturnValueOnce(false); const result = await (0, sdk_setup_1.tryRevealAndGetManualInstructions)(missingFilename, filePath); (0, vitest_1.expect)(result).toBe(false); (0, vitest_1.expect)(childProcess.execSync).toHaveBeenCalled(); (0, vitest_1.expect)(clackMocks.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('entry.client.jsx still not found after running reveal')); }); (0, vitest_1.it)('should return false when reveal command throws an error', async () => { const missingFilename = 'entry.server.jsx'; const filePath = '/app/entry.server.jsx'; // Mock user confirming the reveal operation clackMocks.confirm.mockResolvedValueOnce(true); // Mock execSync throwing an error const mockError = new Error('Command failed'); childProcess.execSync.mockImplementationOnce(() => { throw mockError; }); const result = await (0, sdk_setup_1.tryRevealAndGetManualInstructions)(missingFilename, filePath); (0, vitest_1.expect)(result).toBe(false); (0, vitest_1.expect)(childProcess.execSync).toHaveBeenCalled(); (0, vitest_1.expect)(clackMocks.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to run npx react-router reveal')); }); (0, vitest_1.it)('should log command output when reveal succeeds', async () => { const missingFilename = 'entry.client.tsx'; const filePath = '/app/entry.client.tsx'; const commandOutput = 'Generated entry files successfully'; // Mock user confirming the reveal operation clackMocks.confirm.mockResolvedValueOnce(true); // Mock execSync succeeding with output childProcess.execSync.mockReturnValueOnce(commandOutput); // Mock file existing after reveal existsSyncMock.mockReturnValueOnce(true); await (0, sdk_setup_1.tryRevealAndGetManualInstructions)(missingFilename, filePath); (0, vitest_1.expect)(clackMocks.info).toHaveBeenCalledWith(commandOutput); }); (0, vitest_1.it)('should handle reveal command with proper parameters', async () => { const missingFilename = 'entry.client.tsx'; const filePath = '/app/entry.client.tsx'; // Mock user confirming clackMocks.confirm.mockResolvedValueOnce(true); // Mock execSync succeeding childProcess.execSync.mockReturnValueOnce('ok'); // Mock file existing existsSyncMock.mockReturnValueOnce(true); await (0, sdk_setup_1.tryRevealAndGetManualInstructions)(missingFilename, filePath); (0, vitest_1.expect)(childProcess.execSync).toHaveBeenCalledWith('npx react-router reveal', { encoding: 'utf8', stdio: 'pipe', }); }); }); (0, vitest_1.describe)('updatePackageJsonScripts', () => { (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); vitest_1.vi.resetAllMocks(); }); (0, vitest_1.it)('should set NODE_ENV=production for both dev and start scripts (workaround for React Router v7 + React 19 issue)', async () => { const mockPackageJson = { scripts: { dev: 'react-router dev', start: 'react-router serve', build: 'react-router build', }, }; // Mock getPackageDotJson to return our test package.json getPackageDotJsonMock.mockResolvedValue(mockPackageJson); // Mock fs.promises.writeFile const fsPromises = await import('fs'); const writeFileMock = vitest_1.vi .spyOn(fsPromises.promises, 'writeFile') .mockResolvedValue(); await (0, sdk_setup_1.updatePackageJsonScripts)(); // Verify writeFile was called (0, vitest_1.expect)(writeFileMock).toHaveBeenCalled(); // Check the written package.json content const writtenContent = JSON.parse(writeFileMock.mock.calls[0]?.[1]); // Both dev and start scripts should use the correct filenames and commands according to documentation (0, vitest_1.expect)(writtenContent.scripts.dev).toBe("NODE_OPTIONS='--import ./instrument.server.mjs' react-router dev"); // The start script should use react-router-serve with build path according to documentation (0, vitest_1.expect)(writtenContent.scripts.start).toBe("NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js"); // The build script should remain unchanged (0, vitest_1.expect)(writtenContent.scripts.build).toBe('react-router build'); }); (0, vitest_1.it)('should handle package.json with only start script', async () => { const mockPackageJson = { scripts: { start: 'react-router serve', }, }; // Mock getPackageDotJson to return our test package.json getPackageDotJsonMock.mockResolvedValue(mockPackageJson); // Mock fs.promises.writeFile const fsPromises = await import('fs'); const writeFileMock = vitest_1.vi .spyOn(fsPromises.promises, 'writeFile') .mockResolvedValue(); await (0, sdk_setup_1.updatePackageJsonScripts)(); // Verify only start script is modified when dev doesn't exist const writtenContent = JSON.parse(writeFileMock.mock.calls[0]?.[1]); (0, vitest_1.expect)(writtenContent.scripts.start).toBe("NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js"); (0, vitest_1.expect)(writtenContent.scripts.dev).toBeUndefined(); }); (0, vitest_1.it)('should throw error when no start script exists', async () => { const mockPackageJson = { scripts: { build: 'react-router build', }, }; // Mock getPackageDotJson to return package.json without start script getPackageDotJsonMock.mockResolvedValue(mockPackageJson); await (0, vitest_1.expect)((0, sdk_setup_1.updatePackageJsonScripts)()).rejects.toThrow('Could not find a `start` script in your package.json. Please add: "start": "react-router-serve ./build/server/index.js" and re-run the wizard.'); }); (0, vitest_1.it)('should handle unquoted NODE_OPTIONS in dev script', async () => { const mockPackageJson = { scripts: { dev: 'NODE_OPTIONS=--loader ts-node/register react-router dev', start: 'react-router serve', }, }; getPackageDotJsonMock.mockResolvedValue(mockPackageJson); const fsPromises = await import('fs'); const writeFileMock = vitest_1.vi .spyOn(fsPromises.promises, 'writeFile') .mockResolvedValue(); await (0, sdk_setup_1.updatePackageJsonScripts)(); const writtenContent = JSON.parse(writeFileMock.mock.calls[0]?.[1]); // Should merge unquoted NODE_OPTIONS and wrap result in single quotes (0, vitest_1.expect)(writtenContent.scripts.dev).toBe("NODE_OPTIONS='--loader ts-node/register --import ./instrument.server.mjs' react-router dev"); }); (0, vitest_1.it)('should handle unquoted NODE_OPTIONS in start script', async () => { const mockPackageJson = { scripts: { start: 'NODE_OPTIONS=--require ./dotenv-config.js react-router-serve ./build/server/index.js', }, }; getPackageDotJsonMock.mockResolvedValue(mockPackageJson); const fsPromises = await import('fs'); const writeFileMock = vitest_1.vi .spyOn(fsPromises.promises, 'writeFile') .mockResolvedValue(); await (0, sdk_setup_1.updatePackageJsonScripts)(); const writtenContent = JSON.parse(writeFileMock.mock.calls[0]?.[1]); // Should merge unquoted NODE_OPTIONS and wrap result in single quotes (0, vitest_1.expect)(writtenContent.scripts.start).toBe("NODE_OPTIONS='--require ./dotenv-config.js --import ./instrument.server.mjs' react-router-serve ./build/server/index.js"); }); (0, vitest_1.it)('should handle quoted NODE_OPTIONS and standardize to single quotes', async () => { const mockPackageJson = { scripts: { dev: 'NODE_OPTIONS="--max-old-space-size=4096" react-router dev', start: "NODE_OPTIONS='--enable-source-maps' react-router serve", }, }; getPackageDotJsonMock.mockResolvedValue(mockPackageJson); const fsPromises = await import('fs'); const writeFileMock = vitest_1.vi .spyOn(fsPromises.promises, 'writeFile') .mockResolvedValue(); await (0, sdk_setup_1.updatePackageJsonScripts)(); const writtenContent = JSON.parse(writeFileMock.mock.calls[0]?.[1]); // Should standardize to single quotes (0, vitest_1.expect)(writtenContent.scripts.dev).toBe("NODE_OPTIONS='--max-old-space-size=4096 --import ./instrument.server.mjs' react-router dev"); (0, vitest_1.expect)(writtenContent.scripts.start).toBe("NODE_OPTIONS='--enable-source-maps --import ./instrument.server.mjs' react-router serve"); }); }); //# sourceMappingURL=sdk-setup.test.js.map