@sentry/wizard
Version:
Sentry wizard helping you to configure your project
527 lines • 18.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 });
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