@sentry/wizard
Version:
Sentry wizard helping you to configure your project
250 lines • 13.4 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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.configureReactRouterConfig = exports.configureReactRouterVitePlugin = exports.instrumentSentryOnEntryServer = exports.updatePackageJsonScripts = exports.createServerInstrumentationFile = exports.instrumentRootRoute = exports.initializeSentryOnEntryClient = exports.isReactRouterV7 = exports.runReactRouterReveal = exports.tryRevealAndGetManualInstructions = exports.getRouteFilePath = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const childProcess = __importStar(require("child_process"));
// @ts-expect-error - clack is ESM and TS complains about that. It works though
const prompts_1 = __importDefault(require("@clack/prompts"));
const chalk_1 = __importDefault(require("chalk"));
const semver_1 = require("semver");
const package_json_1 = require("../utils/package-json");
const debug_1 = require("../utils/debug");
const templates_1 = require("./templates");
const root_1 = require("./codemods/root");
const server_entry_1 = require("./codemods/server-entry");
const clack_1 = require("../utils/clack");
const client_entry_1 = require("./codemods/client.entry");
const vite_1 = require("./codemods/vite");
const react_router_config_1 = require("./codemods/react-router-config");
const REACT_ROUTER_REVEAL_COMMAND = 'npx react-router reveal';
const INSTRUMENTATION_FILE = 'instrument.server.mjs';
const APP_DIRECTORY = 'app';
const ROUTES_DIRECTORY = 'routes';
function _formatConfigErrorMessage(filename, errorMessage, fallbackHint) {
return (`Could not automatically configure ${filename}. ${errorMessage}\n` +
`This may happen if your config has an unusual format. ` +
`${fallbackHint}`);
}
function getAppFilePath(filename, isTS, isPage = true) {
const ext = isPage ? (isTS ? 'tsx' : 'jsx') : isTS ? 'ts' : 'js';
return path.join(process.cwd(), APP_DIRECTORY, `${filename}.${ext}`);
}
function getRouteFilePath(filename, isTS) {
const ext = isTS ? 'tsx' : 'jsx';
return path.join(process.cwd(), APP_DIRECTORY, ROUTES_DIRECTORY, `${filename}.${ext}`);
}
exports.getRouteFilePath = getRouteFilePath;
async function tryRevealAndGetManualInstructions(missingFilename, filePath) {
const shouldTryReveal = await prompts_1.default.confirm({
message: `Would you like to try running ${chalk_1.default.cyan(REACT_ROUTER_REVEAL_COMMAND)} to generate entry files?`,
initialValue: true,
});
if (shouldTryReveal) {
try {
prompts_1.default.log.info(`Running ${chalk_1.default.cyan(REACT_ROUTER_REVEAL_COMMAND)}...`);
const output = childProcess.execSync(REACT_ROUTER_REVEAL_COMMAND, {
encoding: 'utf8',
stdio: 'pipe',
});
prompts_1.default.log.info(output);
if (fs.existsSync(filePath)) {
prompts_1.default.log.success(`Found ${chalk_1.default.cyan(missingFilename)} after running reveal.`);
return true;
}
else {
prompts_1.default.log.warn(`${chalk_1.default.cyan(missingFilename)} still not found after running reveal.`);
}
}
catch (e) {
(0, debug_1.debug)('Failed to run React Router reveal command:', e);
prompts_1.default.log.warn(`Failed to run ${chalk_1.default.cyan(REACT_ROUTER_REVEAL_COMMAND)}. This command generates entry files for React Router v7.`);
}
}
return false; // File still doesn't exist, manual intervention needed
}
exports.tryRevealAndGetManualInstructions = tryRevealAndGetManualInstructions;
async function ensureEntryFileExists(filename, filePath) {
if (fs.existsSync(filePath)) {
return; // File exists, nothing to do
}
prompts_1.default.log.warn(`Could not find ${chalk_1.default.cyan(filename)}.`);
const fileExists = await tryRevealAndGetManualInstructions(filename, filePath);
if (!fileExists) {
throw new Error(`Failed to create or find ${filename}. Please create this file manually or ensure your React Router v7 project structure is correct.`);
}
}
function runReactRouterReveal(force = false) {
if (force ||
(!fs.existsSync(path.join(process.cwd(), 'app/entry.client.tsx')) &&
!fs.existsSync(path.join(process.cwd(), 'app/entry.client.jsx')))) {
try {
childProcess.execSync(REACT_ROUTER_REVEAL_COMMAND, {
encoding: 'utf8',
stdio: 'pipe',
});
}
catch (e) {
(0, debug_1.debug)('Failed to run React Router reveal command:', e);
throw e;
}
}
}
exports.runReactRouterReveal = runReactRouterReveal;
function isReactRouterV7(packageJson) {
const reactRouterVersion = (0, package_json_1.getPackageVersion)('@react-router/dev', packageJson);
if (!reactRouterVersion) {
return false;
}
const minVer = (0, semver_1.minVersion)(reactRouterVersion);
if (!minVer) {
return false;
}
return (0, semver_1.gte)(minVer, '7.0.0');
}
exports.isReactRouterV7 = isReactRouterV7;
async function initializeSentryOnEntryClient(dsn, enableTracing, enableReplay, enableLogs, isTS) {
const clientEntryPath = getAppFilePath('entry.client', isTS);
const clientEntryFilename = path.basename(clientEntryPath);
await ensureEntryFileExists(clientEntryFilename, clientEntryPath);
await (0, client_entry_1.instrumentClientEntry)(clientEntryPath, dsn, enableTracing, enableReplay, enableLogs);
prompts_1.default.log.success(`Updated ${chalk_1.default.cyan(clientEntryFilename)} with Sentry initialization.`);
}
exports.initializeSentryOnEntryClient = initializeSentryOnEntryClient;
async function instrumentRootRoute(isTS) {
const rootPath = getAppFilePath('root', isTS);
const rootFilename = path.basename(rootPath);
if (!fs.existsSync(rootPath)) {
throw new Error(`${rootFilename} not found in app directory. Please ensure your React Router v7 app has a root.tsx/jsx file in the app folder.`);
}
await (0, root_1.instrumentRoot)(rootFilename);
prompts_1.default.log.success(`Updated ${chalk_1.default.cyan(rootFilename)} with ErrorBoundary.`);
}
exports.instrumentRootRoute = instrumentRootRoute;
function createServerInstrumentationFile(dsn, selectedFeatures) {
const instrumentationPath = path.join(process.cwd(), INSTRUMENTATION_FILE);
const content = (0, templates_1.getSentryInstrumentationServerContent)(dsn, selectedFeatures.performance, selectedFeatures.profiling, selectedFeatures.logs);
fs.writeFileSync(instrumentationPath, content);
prompts_1.default.log.success(`Created ${chalk_1.default.cyan(INSTRUMENTATION_FILE)}.`);
return instrumentationPath;
}
exports.createServerInstrumentationFile = createServerInstrumentationFile;
async function updatePackageJsonScripts() {
const packageJson = await (0, clack_1.getPackageDotJson)();
if (!packageJson?.scripts) {
throw new Error('Could not find a `scripts` section in your package.json file. Please add scripts manually or ensure your package.json is valid.');
}
if (!packageJson.scripts.start) {
throw new Error('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.');
}
function mergeNodeOptions(scriptCommand, instrumentPath = './instrument.server.mjs') {
if (scriptCommand.includes(instrumentPath)) {
return scriptCommand;
}
const quotedMatch = scriptCommand.match(/NODE_OPTIONS=(['"])([^'"]*)\1/);
if (quotedMatch) {
const existingOptions = quotedMatch[2];
const mergedOptions = `${existingOptions} --import ${instrumentPath}`.trim();
return scriptCommand.replace(/NODE_OPTIONS=(['"])([^'"]*)\1/, `NODE_OPTIONS='${mergedOptions}'`);
}
const unquotedMatch = scriptCommand.match(/NODE_OPTIONS=([^\s]+(?:\s+[^\s]+)*?)(\s+(?:react-router-serve|react-router|node|npx|tsx))/);
if (unquotedMatch) {
const existingOptions = unquotedMatch[1];
const commandPart = unquotedMatch[2];
const mergedOptions = `${existingOptions} --import ${instrumentPath}`.trim();
return scriptCommand.replace(/NODE_OPTIONS=([^\s]+(?:\s+[^\s]+)*?)(\s+(?:react-router-serve|react-router|node|npx|tsx))/, `NODE_OPTIONS='${mergedOptions}'${commandPart}`);
}
return `NODE_OPTIONS='--import ${instrumentPath}' ${scriptCommand}`;
}
if (packageJson.scripts.dev) {
packageJson.scripts.dev = mergeNodeOptions(packageJson.scripts.dev);
}
const startScript = packageJson.scripts.start;
if (!startScript.includes(INSTRUMENTATION_FILE) &&
!startScript.includes('NODE_OPTIONS')) {
packageJson.scripts.start = `NODE_OPTIONS='--import ./${INSTRUMENTATION_FILE}' react-router-serve ./build/server/index.js`;
}
else {
packageJson.scripts.start = mergeNodeOptions(startScript);
}
await fs.promises.writeFile('package.json', JSON.stringify(packageJson, null, 2));
}
exports.updatePackageJsonScripts = updatePackageJsonScripts;
async function instrumentSentryOnEntryServer(isTS) {
const serverEntryPath = getAppFilePath('entry.server', isTS);
const serverEntryFilename = path.basename(serverEntryPath);
await ensureEntryFileExists(serverEntryFilename, serverEntryPath);
await (0, server_entry_1.instrumentServerEntry)(serverEntryPath);
prompts_1.default.log.success(`Updated ${chalk_1.default.cyan(serverEntryFilename)} with Sentry error handling.`);
}
exports.instrumentSentryOnEntryServer = instrumentSentryOnEntryServer;
async function configureReactRouterVitePlugin(orgSlug, projectSlug) {
const configPath = fs.existsSync(path.join(process.cwd(), 'vite.config.ts'))
? path.join(process.cwd(), 'vite.config.ts')
: path.join(process.cwd(), 'vite.config.js');
const filename = chalk_1.default.cyan(path.basename(configPath));
try {
const { wasConverted } = await (0, vite_1.instrumentViteConfig)(orgSlug, projectSlug);
prompts_1.default.log.success(`Updated ${filename} with sentryReactRouter plugin.`);
if (wasConverted) {
prompts_1.default.log.info(`Converted your Vite config to function form ${chalk_1.default.dim('(defineConfig(config => ({ ... })))')} to support the Sentry React Router plugin.`);
}
}
catch (e) {
(0, debug_1.debug)('Failed to modify vite config:', e);
const errorMessage = e instanceof Error ? e.message : String(e);
throw new Error(_formatConfigErrorMessage(filename, errorMessage, 'You may need to add the plugin manually.'));
}
}
exports.configureReactRouterVitePlugin = configureReactRouterVitePlugin;
async function configureReactRouterConfig(isTS) {
const configFilename = `react-router.config.${isTS ? 'ts' : 'js'}`;
const configPath = path.join(process.cwd(), configFilename);
const filename = chalk_1.default.cyan(configFilename);
try {
const fileExistedBefore = fs.existsSync(configPath);
const { ssrWasChanged } = await (0, react_router_config_1.instrumentReactRouterConfig)(isTS);
if (fileExistedBefore) {
prompts_1.default.log.success(`Updated ${filename} with Sentry buildEnd hook.`);
}
else {
prompts_1.default.log.success(`Created ${filename} with Sentry buildEnd hook.`);
}
if (ssrWasChanged) {
prompts_1.default.log.warn(`${chalk_1.default.yellow('Note:')} SSR has been enabled in your React Router config (${chalk_1.default.cyan('ssr: true')}). This is required for Sentry sourcemap uploads to work correctly.`);
}
}
catch (e) {
(0, debug_1.debug)('Failed to modify react-router.config:', e);
const errorMessage = e instanceof Error ? e.message : String(e);
throw new Error(_formatConfigErrorMessage(filename, errorMessage, 'You may need to add the buildEnd hook manually.'));
}
}
exports.configureReactRouterConfig = configureReactRouterConfig;
//# sourceMappingURL=sdk-setup.js.map