UNPKG

zapier-platform-cli

Version:

The CLI for managing integrations in Zapier Developer Platform.

254 lines (224 loc) 8.7 kB
// @ts-check // tools for modifying an AST const j = require('jscodeshift'); const ts = j.withParser('ts'); // simple helper functions used for searching for nodes // can't use j.identifier(name) because it has extra properties and we have to have no extras to find nodes // we can use them when creating nodes though! const typeHelpers = { identifier: (name) => ({ type: 'Identifier', name }), callExpression: (name) => ({ type: 'CallExpression', callee: { name }, }), memberExpression: (object, property) => ({ type: 'MemberExpression', object, property, }), }; /** * adds a `const verName = require(path)` to the root of a codeStr */ const importActionInJsApp = (codeStr, varName, path) => { if (codeStr.match(new RegExp(`${varName} ?= ?require`))) { // duplicate identifier, no need to re-add // this would fail if they used this variable name for something else; we'd keep going and double-declare that variable // TODO: throw error if it's a duplicate identifier but different require path? return codeStr; } const root = j(codeStr); // insert a new require statement after all other requires (that get put into variables, like might happen at the top-level) const reqStatements = root // searching for VariableDeclaration, like `const x = require('y')` // skips over `require` statements not saved to variables, since that's (probably) not a common case .find(j.VariableDeclaration, { declarations: [{ init: typeHelpers.callExpression('require') }], }) // filters for top-level require statements by filtering only for statements whose parents are type Program, the root .filter((path) => j.Program.check(path.parent.value)); const newRequireStatement = j.variableDeclaration('const', [ j.variableDeclarator( j.identifier(varName), j.callExpression(j.identifier('require'), [j.literal(path)]), ), ]); if (reqStatements.length) { reqStatements.at(-1).insertAfter(newRequireStatement); } else { // insert at top of program const body = root.find(j.Program).get().node.body; body.unshift(newRequireStatement); // retain leading comments body[0].comments = body[1].comments; delete body[1].comments; } return root.toSource(); }; const registerActionInJsApp = (codeStr, property, varName) => { // to play with this, use https://astexplorer.net/#/gist/cb4986b3f1c6eb975339608109a48e7d/0fbf2fabbcf27d0b6ebd8910f979bd5d97dd9404 const root = j(codeStr); // what we'll hopefully insert const newProperty = j.property.from({ kind: 'init', key: j.memberExpression(j.identifier(varName), j.identifier('key')), value: j.identifier(varName), computed: true, }); // we start by looking for what's on the right side of a `module.exports` call const exportAssignment = root.find(j.AssignmentExpression, { left: typeHelpers.memberExpression( typeHelpers.identifier('module'), typeHelpers.identifier('exports'), ), }); if (!exportAssignment.length) { throw new Error( 'Nothing is exported from this file; unable to find an object to modify', ); } let objToModify = exportAssignment.get().node.right; if (objToModify.type === 'Identifier') { // variable, need to find that const exportedVarDeclaration = root.find(j.VariableDeclaration, { declarations: [{ id: typeHelpers.identifier(objToModify.name) }], }); if ( !exportedVarDeclaration.length || // if the variable is just a different variable, we can't modify safely exportedVarDeclaration.get().node.declarations[0].init.type !== 'ObjectExpression' ) { throw new Error('Unable to find object definition for exported variable'); } objToModify = exportedVarDeclaration.get().node.declarations[0].init; } else if (objToModify.type !== 'ObjectExpression') { // If the exported value isn't an object or variable throw new Error(`Invalid export type: "${objToModify.type}"`); } // now we have an object to modify // check if this object already has the property at the top level const existingProp = objToModify.properties.find( (props) => props.key && props.key.name === property, ); if (existingProp) { const value = existingProp.value; if (value.type === 'Identifier') { // Handle shorthand syntax like `creates` instead of `creates: { ... }` // Transform it into an object with spread operator: `creates: { ...creates, [newAction.key]: newAction }` const spreadProperty = j.spreadElement(j.identifier(value.name)); existingProp.value = j.objectExpression([spreadProperty, newProperty]); existingProp.shorthand = false; // Disable shorthand since we're changing the value } else if (value.type === 'ObjectExpression') { value.properties.push(newProperty); } else { throw new Error( `Tried to edit the ${property} key, but the value wasn't an object`, ); } } else { objToModify.properties.push( j.property( 'init', j.identifier(property), j.objectExpression([newProperty]), ), ); } return root.toSource(); }; /** * Adds an import statement to the top of an index.ts file to import a * new action, such as `import some_trigger from './triggers/some_trigger';` * * @param {string} codeStr - The code of the index.ts file to modify. * @param {string} identifierName - The name of imported action used as a variable in the code. * @param {string} actionRelativeImportPath - The relative path to import the action from * @returns {string} */ const importActionInTsApp = ( codeStr, identifierName, actionRelativeImportPath, ) => { const root = ts(codeStr); const imports = root.find(ts.ImportDeclaration); const newImportStatement = j.importDeclaration( [j.importDefaultSpecifier(j.identifier(identifierName))], j.literal(actionRelativeImportPath), ); if (imports.length) { imports.at(-1).insertAfter(newImportStatement); } else { const body = root.find(ts.Program).get().node.body; body.unshift(newImportStatement); // Add newline after import? } return root.toSource({ quote: 'single' }); }; /** * * @param {string} codeStr * @param {'creates' | 'searches' | 'triggers'} actionTypePlural - The type of action to register within the app * @param {string} identifierName - Name of the action imported to be registered * @returns {string} */ const registerActionInTsApp = (codeStr, actionTypePlural, identifierName) => { const root = ts(codeStr); // the `[thing.key]: thing` entry we'd like to insert. const newProperty = ts.property.from({ kind: 'init', key: j.memberExpression(j.identifier(identifierName), j.identifier('key')), value: j.identifier(identifierName), computed: true, }); // Find the top level app Object; the one with the `platformVersion` // key. This is where we'll insert our new property. const appObjectCandidates = root .find(ts.ObjectExpression) .filter((path) => path.value.properties.some( (prop) => prop.key && prop.key.name === 'platformVersion', ), ); if (appObjectCandidates.length !== 1) { throw new Error('Unable to find the app definition to modify'); } const appObj = appObjectCandidates.get().node; // Now we have an app object to modify. // Check if this object already has the actionType group inside it. const existingProp = appObj.properties.find( (props) => props.key && props.key.name === actionTypePlural, ); if (existingProp) { const value = existingProp.value; if (value.type === 'Identifier') { // Handle shorthand syntax like `creates` instead of `creates: { ... }` // Transform it into an object with spread operator: `creates: { ...creates, [newAction.key]: newAction }` const spreadProperty = j.spreadElement(j.identifier(value.name)); existingProp.value = j.objectExpression([spreadProperty, newProperty]); existingProp.shorthand = false; // Disable shorthand since we're changing the value } else if (value.type === 'ObjectExpression') { value.properties.push(newProperty); } else { throw new Error( `Tried to edit the ${actionTypePlural} key, but the value wasn't an object`, ); } } else { appObj.properties.push( j.property( 'init', j.identifier(actionTypePlural), j.objectExpression([newProperty]), ), ); } return root.toSource({ quote: 'single' }); }; module.exports = { importActionInJsApp, registerActionInJsApp, importActionInTsApp, registerActionInTsApp, };