UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

527 lines 18.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 }); exports.checkIfRunsOnProdMode = exports.checkIfRunsOnDevMode = exports.checkIfExpoBundles = exports.checkIfReactNativeBundles = exports.checkIfFlutterBuilds = exports.checkIfBuilds = exports.checkSentryProperties = exports.checkEnvBuildPlugin = exports.checkSentryCliRc = exports.checkPackageJson = exports.checkFileExists = exports.checkFileContents = exports.checkFileDoesNotContain = exports.modifyFile = exports.createFile = exports.startWizardInstance = exports.getWizardCommand = exports.revertLocalChanges = exports.cleanupGit = exports.initGit = exports.WizardTestEnv = exports.log = exports.TEST_ARGS = exports.KEYS = void 0; const fs = __importStar(require("node:fs")); const path = __importStar(require("node:path")); const node_child_process_1 = require("node:child_process"); const Logging_1 = require("../../lib/Helper/Logging"); const vitest_1 = require("vitest"); exports.KEYS = { UP: '\u001b[A', DOWN: '\u001b[B', LEFT: '\u001b[D', RIGHT: '\u001b[C', ENTER: '\r', SPACE: ' ', }; exports.TEST_ARGS = { AUTH_TOKEN: process.env.SENTRY_TEST_AUTH_TOKEN || 'TEST_AUTH_TOKEN', PROJECT_DSN: process.env.SENTRY_TEST_DSN || 'https://public@dsn.ingest.sentry.io/1337', ORG_SLUG: process.env.SENTRY_TEST_ORG || 'TEST_ORG_SLUG', PROJECT_SLUG: process.env.SENTRY_TEST_PROJECT || 'TEST_PROJECT_SLUG', }; exports.log = { success: (message) => { (0, Logging_1.green)(`[SUCCESS] ${message}`); }, info: (message) => { (0, Logging_1.dim)(`[INFO] ${message}`); }, error: (message) => { function formatMessage(message, depth) { if (depth > 3) { return '...'; } if (message instanceof Error) { return JSON.stringify({ name: message.name, message: message.message, stack: message.stack, ...(message.cause ? { cause: formatMessage(message.cause, depth + 1), } : {}), }, null, 2); } return String(message); } (0, Logging_1.red)(`[ERROR] ${formatMessage(message, 0)}`); }, }; class WizardTestEnv { taskHandle; constructor(cmd, args, opts) { this.taskHandle = (0, node_child_process_1.spawn)(cmd, args, { cwd: opts?.cwd, stdio: 'pipe' }); if (opts?.debug) { this.taskHandle.stdout?.pipe(process.stdout); this.taskHandle.stderr?.pipe(process.stderr); } } sendStdin(input) { this.taskHandle.stdin?.write(input); } /** * Sends the input and waits for the output. * @returns a promise that resolves when the output was found * @throws an error when the output was not found within the timeout */ sendStdinAndWaitForOutput(input, output, options) { const outputPromise = this.waitForOutput(output, options); if (Array.isArray(input)) { for (const i of input) { this.sendStdin(i); } } else { this.sendStdin(input); } return outputPromise; } /** * Waits for the task to exit with a given `statusCode`. * * @returns a promise that resolves to `true` if the run ends with the status * code, or it rejects when the `timeout` was reached. */ waitForStatusCode(statusCode, options = {}) { const { timeout } = { timeout: 60000, ...options, }; return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.kill(); reject(new Error(`Timeout waiting for status code: ${statusCode ?? 'null'}`)); }, timeout); this.taskHandle.on('error', (err) => { clearTimeout(timeoutId); reject(err); }); this.taskHandle.on('exit', (code) => { clearTimeout(timeoutId); resolve(code === statusCode); }); }); } /** * Waits for the provided output with `.includes()` logic. * * @returns a promise that resolves to `true` if the output was found, `false` if the output was not found within the * timeout and `optional: true` is set, or it rejects when the timeout was reached with `optional: false` */ waitForOutput(output, options = {}) { const { timeout, optional } = { timeout: 60000, optional: false, ...options, }; return new Promise((resolve, reject) => { let outputBuffer = ''; const timeoutId = setTimeout(() => { this.taskHandle.off('error', errorListener); this.taskHandle.stdout?.off('data', dataListener); this.kill(); if (optional) { // The output is not found but it's optional so we can resolve the promise with false resolve(false); } else { reject(new Error(`Timeout waiting for output: ${output}. Got the following instead: ${outputBuffer}`)); } }, timeout); const dataListener = (data) => { outputBuffer += data; if (outputBuffer.includes(output)) { clearTimeout(timeoutId); this.taskHandle.off('error', errorListener); this.taskHandle.stdout?.off('data', dataListener); // The output is found so we can resolve the promise with true resolve(true); } }; const errorListener = (err) => { this.taskHandle.off('error', errorListener); this.taskHandle.stdout?.off('data', dataListener); clearTimeout(timeoutId); reject(err); }; this.taskHandle.on('error', errorListener); this.taskHandle.stdout?.on('data', dataListener); }); } kill() { this.taskHandle.stdin?.destroy(); this.taskHandle.stderr?.destroy(); this.taskHandle.stdout?.destroy(); this.taskHandle.kill('SIGINT'); this.taskHandle.unref(); } } exports.WizardTestEnv = WizardTestEnv; /** * Initialize a git repository in the given directory * @param projectDir */ function initGit(projectDir) { try { (0, node_child_process_1.execSync)('git init', { cwd: projectDir }); // Add all files to the git repo (0, node_child_process_1.execSync)('git add -A', { cwd: projectDir }); // Add author info to avoid git commit error (0, node_child_process_1.execSync)('git config user.email test@test.sentry.io', { cwd: projectDir }); (0, node_child_process_1.execSync)('git config user.name Test', { cwd: projectDir }); (0, node_child_process_1.execSync)('git commit -m init', { cwd: projectDir }); } catch (e) { exports.log.error('Error initializing git'); exports.log.error(e); } } exports.initGit = initGit; /** * Cleanup the git repository in the given directory * * Caution! Make sure `projectDir` is a test project directory, * if in doubt, please commit your local non-test changes first! * @param projectDir */ function cleanupGit(projectDir) { try { // Remove the .git directory (0, node_child_process_1.execSync)(`rm -rf ${projectDir}/.git`); } catch (e) { exports.log.error('Error cleaning up git'); exports.log.error(e); } } exports.cleanupGit = cleanupGit; /** * Revert local changes in the given directory * * Caution! Make sure `projectDir` is a test project directory, * if in doubt, please commit your local non-test changes first! * * @param projectDir */ function revertLocalChanges(projectDir) { try { // Revert tracked files (0, node_child_process_1.execSync)('git restore .', { cwd: projectDir }); // Revert untracked files (0, node_child_process_1.execSync)('git clean -fd .', { cwd: projectDir }); // Remove node_modules and dist (.gitignore'd and therefore not removed via git clean) (0, node_child_process_1.execSync)('rm -rf node_modules', { cwd: projectDir }); (0, node_child_process_1.execSync)('rm -rf dist', { cwd: projectDir }); } catch (e) { exports.log.error('Error reverting local changes'); exports.log.error(e); } } exports.revertLocalChanges = revertLocalChanges; function getWizardCommand(integration) { const binName = process.env.SENTRY_WIZARD_E2E_TEST_BIN ? ['dist-bin', `sentry-wizard-${process.platform}-${process.arch}`] : ['dist', 'bin.js']; const binPath = path.join(__dirname, '..', '..', ...binName); const args = [ '--debug', '-i', integration, '--preSelectedProject.authToken', exports.TEST_ARGS.AUTH_TOKEN, '--preSelectedProject.dsn', exports.TEST_ARGS.PROJECT_DSN, '--preSelectedProject.orgSlug', exports.TEST_ARGS.ORG_SLUG, '--preSelectedProject.projectSlug', exports.TEST_ARGS.PROJECT_SLUG, '--disable-telemetry', ]; return `${binPath} ${args.join(' ')}`; } exports.getWizardCommand = getWizardCommand; /** * Start the wizard instance with the given integration and project directory * @param integration * @param projectDir * * @returns WizardTestEnv */ function startWizardInstance(integration, projectDir, debug = false) { const binName = process.env.SENTRY_WIZARD_E2E_TEST_BIN ? ['dist-bin', `sentry-wizard-${process.platform}-${process.arch}`] : ['dist', 'bin.js']; const binPath = path.join(__dirname, '..', '..', ...binName); revertLocalChanges(projectDir); cleanupGit(projectDir); initGit(projectDir); return new WizardTestEnv(binPath, [ '--debug', '-i', integration, '--preSelectedProject.authToken', exports.TEST_ARGS.AUTH_TOKEN, '--preSelectedProject.dsn', exports.TEST_ARGS.PROJECT_DSN, '--preSelectedProject.orgSlug', exports.TEST_ARGS.ORG_SLUG, '--preSelectedProject.projectSlug', exports.TEST_ARGS.PROJECT_SLUG, '--disable-telemetry', ], { cwd: projectDir, debug }); } exports.startWizardInstance = startWizardInstance; /** * Create a file with the given content * * @param filePath * @param content */ function createFile(filePath, content) { return fs.writeFileSync(filePath, content || ''); } exports.createFile = createFile; /** * Modify the file with the new content * * @param filePath * @param oldContent * @param newContent */ function modifyFile(filePath, replaceMap) { const fileContent = fs.readFileSync(filePath, 'utf-8'); let newFileContent = fileContent; for (const [oldContent, newContent] of Object.entries(replaceMap)) { newFileContent = newFileContent.replace(oldContent, newContent); } fs.writeFileSync(filePath, newFileContent); } exports.modifyFile = modifyFile; /** * Read the file contents and check if it does not contain the given content * * @param {string} filePath * @param {(string | string[])} content */ function checkFileDoesNotContain(filePath, content) { const fileContent = fs.readFileSync(filePath, 'utf-8'); const contentArray = Array.isArray(content) ? content : [content]; for (const c of contentArray) { (0, vitest_1.expect)(fileContent).not.toContain(c); } } exports.checkFileDoesNotContain = checkFileDoesNotContain; /** * Read the file contents and check if it contains the given content * * @param {string} filePath * @param {(string | string[])} content */ function checkFileContents(filePath, content) { const fileContent = fs.readFileSync(filePath, 'utf-8'); const contentArray = Array.isArray(content) ? content : [content]; for (const c of contentArray) { (0, vitest_1.expect)(fileContent).toContain(c); } } exports.checkFileContents = checkFileContents; /** * Check if the file exists * * @param filePath */ function checkFileExists(filePath) { (0, vitest_1.expect)(fs.existsSync(filePath)).toBe(true); } exports.checkFileExists = checkFileExists; /** * Check if the package.json contains the given integration * * @param projectDir * @param integration */ function checkPackageJson(projectDir, integration) { checkFileContents(`${projectDir}/package.json`, `@sentry/${integration}`); } exports.checkPackageJson = checkPackageJson; /** * Check if the .sentryclirc contains the auth token * * @param projectDir */ function checkSentryCliRc(projectDir) { checkFileContents(`${projectDir}/.sentryclirc`, `token=${exports.TEST_ARGS.AUTH_TOKEN}`); } exports.checkSentryCliRc = checkSentryCliRc; /** * Check if the .env.sentry-build-plugin contains the auth token * @param projectDir */ function checkEnvBuildPlugin(projectDir) { checkFileContents(`${projectDir}/.env.sentry-build-plugin`, `SENTRY_AUTH_TOKEN=${exports.TEST_ARGS.AUTH_TOKEN}`); } exports.checkEnvBuildPlugin = checkEnvBuildPlugin; /** * Check if the sentry.properties contains the auth token * @param projectDir */ function checkSentryProperties(projectDir) { checkFileContents(`${projectDir}/sentry.properties`, `auth_token=${exports.TEST_ARGS.AUTH_TOKEN}`); } exports.checkSentryProperties = checkSentryProperties; /** * Check if the project builds * Check if the project builds and ends with status code 0. * @param projectDir */ async function checkIfBuilds(projectDir) { const testEnv = new WizardTestEnv('npm', ['run', 'build'], { cwd: projectDir, }); const builtSuccessfully = await testEnv.waitForStatusCode(0, { timeout: 120000, }); (0, vitest_1.expect)(builtSuccessfully).toBe(true); } exports.checkIfBuilds = checkIfBuilds; /** * Check if the flutter project builds * @param projectDir */ async function checkIfFlutterBuilds(projectDir, expectedOutput, debug = false) { const testEnv = new WizardTestEnv('flutter', ['build', 'web'], { cwd: projectDir, debug: debug, }); const outputReceived = await testEnv.waitForOutput(expectedOutput, { timeout: 120000, }); (0, vitest_1.expect)(outputReceived).toBe(true); } exports.checkIfFlutterBuilds = checkIfFlutterBuilds; /** * Check if the React Native project bundles successfully for the specified platform. * Returns a boolean indicating if the process exits with status code 0. * @param projectDir The root directory of the React Native project. * @param platform The platform to bundle for ('ios' or 'android'). * @param debug runs the command in debug mode if true */ async function checkIfReactNativeBundles(projectDir, platform, debug = false) { const entryFile = 'index.js'; const dev = 'false'; // Test a production-like bundle let bundleOutput; let assetsDest; if (platform === 'ios') { bundleOutput = './ios/main.jsbundle'; assetsDest = './ios'; } else { // android bundleOutput = './android/app/src/main/assets/index.android.bundle'; assetsDest = './android/app/src/main/res'; } const bundleCommandArgs = [ 'react-native', 'bundle', '--entry-file', entryFile, '--platform', platform, '--dev', dev, '--bundle-output', bundleOutput, '--assets-dest', assetsDest, ]; const testEnv = new WizardTestEnv('npx', bundleCommandArgs, { cwd: projectDir, debug: debug, }); const builtSuccessfully = await testEnv.waitForStatusCode(0, { timeout: 300000, }); testEnv.kill(); return builtSuccessfully; } exports.checkIfReactNativeBundles = checkIfReactNativeBundles; /** * Check if the Expo project exports successfully for the specified platform. * Returns a boolean indicating if the process exits with status code 0. * @param projectDir The root directory of the Expo project. * @param platform The platform to export for ('ios', 'android', or 'web'). * @param debug runs the command in debug mode if true */ async function checkIfExpoBundles(projectDir, platform, debug = false) { const exportCommandArgs = [ 'expo', 'export', '--platform', platform, ]; const testEnv = new WizardTestEnv('npx', exportCommandArgs, { cwd: projectDir, debug: debug, }); const builtSuccessfully = await testEnv.waitForStatusCode(0, { timeout: 300000, }); testEnv.kill(); return builtSuccessfully; } exports.checkIfExpoBundles = checkIfExpoBundles; /** * Check if the project runs on dev mode * @param projectDir * @param expectedOutput */ async function checkIfRunsOnDevMode(projectDir, expectedOutput) { const testEnv = new WizardTestEnv('npm', ['run', 'dev'], { cwd: projectDir }); (0, vitest_1.expect)(await testEnv.waitForOutput(expectedOutput, { timeout: 120000, })).toBe(true); testEnv.kill(); } exports.checkIfRunsOnDevMode = checkIfRunsOnDevMode; /** * Check if the project runs on prod mode * @param projectDir * @param expectedOutput */ async function checkIfRunsOnProdMode(projectDir, expectedOutput, startCommand = 'start') { const testEnv = new WizardTestEnv('npm', ['run', startCommand], { cwd: projectDir, }); (0, vitest_1.expect)(await testEnv.waitForOutput(expectedOutput, { timeout: 120000, })).toBe(true); testEnv.kill(); } exports.checkIfRunsOnProdMode = checkIfRunsOnProdMode; //# sourceMappingURL=index.js.map