@sentry/wizard
Version:
Sentry wizard helping you to configure your project
411 lines • 20.9 kB
JavaScript
;
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