UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

250 lines 13.4 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; }; 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