UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

271 lines (245 loc) 7.95 kB
import * as fs from 'fs'; import * as recast from 'recast'; import x = recast.types; import t = x.namedTypes; const b = recast.types.builders; /** * Checks if a file where we don't know its concrete file type yet exists * and returns the full path to the file with the correct file type. */ export function findFile( filePath: string, fileTypes: string[] = ['.js', '.ts', '.mjs', '.cjs'], ): string | undefined { return fileTypes .map((type) => `${filePath}${type}`) .find((file) => fs.existsSync(file)); } /** * checks for require('@sentry/*') syntax */ export function hasSentryContent(program: t.Program): boolean { let foundSentry: boolean | undefined = false; recast.visit(program, { visitStringLiteral(path) { foundSentry = foundSentry || path.node.value.startsWith('@sentry/'); this.traverse(path); }, visitLiteral(path) { foundSentry = foundSentry || path.node.value?.toString().startsWith('@sentry/'); this.traverse(path); }, }); return !!foundSentry; } /** * Searches for a property of an ObjectExpression by name * * @param object the ObjectExpression to search in * @param name the name of the property to search for * * @returns the property if it exists */ export function getObjectProperty( object: t.ObjectExpression, name: string, ): t.Property | undefined { return object.properties.find((p): p is t.Property => { const isObjectProp = p.type === 'Property' || p.type === 'ObjectProperty'; if (!isObjectProp) { return false; } const hasMatchingLiteralKey = isObjectProp && (p.key.type === 'Literal' || p.key.type === 'StringLiteral') && p.key.value === name; if (hasMatchingLiteralKey) { return true; } // has matching identifier key return isObjectProp && p.key.type === 'Identifier' && p.key.name === name; }); } /** * Attempts to find a property of an ObjectExpression by name. If it doesn't exist, * the property will be added to the ObjectExpression with the provided default value. * * @param object the parent object expression to search in * @param name the name of the property to search for * @param defaultValue the default value to set if the property doesn't exist * * @returns the */ export function getOrSetObjectProperty( object: t.ObjectExpression, name: string, defaultValue: | t.Literal | t.BooleanLiteral | t.StringLiteral | t.ObjectExpression, ): t.Property { const existingProperty = getObjectProperty(object, name); if (existingProperty) { return existingProperty; } const newProperty = b.property.from({ kind: 'init', key: b.stringLiteral(name), value: defaultValue, }); object.properties.push(newProperty); return newProperty; } /** * Sets a property of an ObjectExpression if it exists, otherwise adds it * to the ObjectExpression. Optionally, a comment can be added to the * property. * * @param object the ObjectExpression to set the property on * @param name the name of the property to set * @param value the value of the property to set * @param comment (optional) a comment to add to the property */ export function setOrUpdateObjectProperty( object: t.ObjectExpression, name: string, value: t.Literal | t.BooleanLiteral | t.StringLiteral | t.ObjectExpression, comment?: string, ) { const newComments = comment && comment.split('\n').map((c) => b.commentLine(` ${c}`, true, false)); const existingProperty = getObjectProperty(object, name); if (existingProperty) { existingProperty.value = value; if (newComments) { existingProperty.comments = [ ...(existingProperty?.comments || []), ...newComments, ]; } } else { object.properties.push( b.objectProperty.from({ key: b.stringLiteral(name), value, ...(newComments && { comments: newComments, }), }), ); } } type JsonCParseResult = | { jsonObject: t.ObjectExpression; ast: t.Program; } | { jsonObject: undefined; ast: undefined; }; /** * Parses a JSON string with (potential) comments (JSON-C) and returns the JS AST * that can be walked and modified with recast like a normal JS AST. * * This is done by wrapping the JSON-C string in parentheses, thereby making it * a JS `Program` with an `ExpressionStatement` as its body. The expression is then * extracted from the AST and returned alongside the AST. * * To preserve as much original formatting as possible, the returned `ast` * property should be passed to {@link `printJsonC`} to get the JSON-C string back. * * If the input is not valid JSON-C, the result will be undefined. * * @see {@link JsonCParseResult} * * @param jsonString a JSON-C string * * @returns a {@link JsonCParseResult}, containing either the JSON-C object and the AST or undefined in both cases */ export function parseJsonC(jsonString: string): JsonCParseResult { try { const jsTsConfig = `(${jsonString})`; // no idea why recast returns any here, this is dumb :/ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const ast = recast.parse(jsTsConfig.toString()).program as t.Program; const jsonObject = (ast.body[0].type === 'ExpressionStatement' && ast.body[0].expression.type === 'ObjectExpression' && ast.body[0].expression) || undefined; if (jsonObject) { return { jsonObject, ast }; } } catch { /* empty */ } return { jsonObject: undefined, ast: undefined }; } /** * Takes the AST of a parsed JSON-C "program" and returns the JSON-C string without * any of the temporary JS wrapper code that was previously applied. * * Only use this in conjunction with {@link `parseJsonC`} * * @param ast the `ast` returned from {@link `parseJsonC`} * * @returns the JSON-C string */ export function printJsonC(ast: t.Program): string { const js = recast.print(ast).code; return js.substring(1, js.length - 1); } /** * Walks the program body and returns index of the last variable assignment initialized by require statement. * Only counts top level require statements. * * @returns index of the last `const foo = require('bar');` statement or -1 if none was found. */ export function getLastRequireIndex(program: t.Program): number { let lastRequireIdex = -1; program.body.forEach((s, i) => { if ( s.type === 'VariableDeclaration' && s.declarations[0].type === 'VariableDeclarator' && s.declarations[0].init !== null && typeof s.declarations[0].init !== 'undefined' && s.declarations[0].init.type === 'CallExpression' && s.declarations[0].init.callee.type === 'Identifier' && s.declarations[0].init.callee.name === 'require' ) { lastRequireIdex = i; } }); return lastRequireIdex; } /** * Walks the statements and removes require statements which first argument includes the predicate. * Only removes top level require statements like `const foo = require('bar');` * * @returns True if any require statement was removed. */ export function removeRequire(program: t.Program, predicate: string): boolean { let removedAtLeastOne = false; program.body = program.body.filter((s) => { if ( s.type === 'VariableDeclaration' && s.declarations[0].type === 'VariableDeclarator' && s.declarations[0].init !== null && typeof s.declarations[0].init !== 'undefined' && s.declarations[0].init.type === 'CallExpression' && s.declarations[0].init.callee.type === 'Identifier' && s.declarations[0].init.callee.name === 'require' && s.declarations[0].init.arguments[0].type === 'StringLiteral' && s.declarations[0].init.arguments[0].value.includes(predicate) ) { removedAtLeastOne = true; return false; } return true; }); return removedAtLeastOne; }