zapier-platform-cli
Version:
The CLI for managing integrations in Zapier Developer Platform.
226 lines (191 loc) • 7.71 kB
JavaScript
// @ts-check
const path = require('path');
const fs = require('fs');
const { Args, Flags } = require('@oclif/core');
const BaseCommand = require('../ZapierBaseCommand');
const { buildFlags } = require('../buildFlags');
const {
createScaffoldingContext,
plural,
updateEntryFile,
isValidEntryFileUpdate,
writeTemplateFile,
} = require('../../utils/scaffold');
const { splitFileFromPath } = require('../../utils/string');
const { isValidAppInstall } = require('../../utils/misc');
const { writeFile } = require('../../utils/files');
const { ISSUES_URL } = require('../../constants');
class ScaffoldCommand extends BaseCommand {
async perform() {
const { actionType, noun } = this.args;
const indexFileLocal = this.flags.entry ?? this.defaultIndexFileLocal();
const {
dest: actionDirLocal = this.defaultActionDirLocal(indexFileLocal),
'test-dest': testDirLocal = this.defaultTestDirLocal(indexFileLocal),
force,
} = this.flags;
const language = indexFileLocal.endsWith('.ts') ? 'ts' : 'js';
const context = createScaffoldingContext({
actionType,
noun,
language,
indexFileLocal,
actionDirLocal,
testDirLocal,
includeIntroComments: !this.flags['no-help'],
preventOverwrite: !force,
});
// TODO: read from config file?
this.startSpinner(`Creating new file: ${context.actionFileLocal}`);
await writeTemplateFile({
destinationPath: context.actionFileResolved,
templateType: context.actionType,
language: context.language,
preventOverwrite: context.preventOverwrite,
templateContext: context.templateContext,
});
this.stopSpinner();
this.startSpinner(`Creating new test file: ${context.testFileLocal}`);
await writeTemplateFile({
destinationPath: context.testFileResolved,
templateType: 'test',
language: context.language,
preventOverwrite: context.preventOverwrite,
templateContext: context.templateContext,
});
this.stopSpinner();
// * rewire the index.js to point to the new file
this.startSpinner(`Rewriting your ${context.indexFileLocal}`);
const originalContents = await updateEntryFile({
language: context.language,
indexFileResolved: context.indexFileResolved,
actionRelativeImportPath: context.actionRelativeImportPath,
actionImportName: context.templateContext.VARIABLE,
actionType: context.actionType,
});
if (isValidAppInstall().valid) {
const success = isValidEntryFileUpdate(
context.language,
context.indexFileResolved,
context.actionType,
context.templateContext.KEY,
);
this.stopSpinner({ success });
if (!success) {
const entryName = splitFileFromPath(context.indexFileResolved)[1];
this.startSpinner(
`Unable to successfully rewrite your ${entryName}. Rolling back...`,
);
await writeFile(context.indexFileResolved, originalContents);
this.stopSpinner();
this.error(
[
`\nPlease add the following lines to ${context.indexFileResolved}:`,
` * \`const ${context.templateContext.VARIABLE} = require('./${context.actionRelativeImportPath}');\` at the top-level`,
` * \`[${context.templateContext.VARIABLE}.key]: ${context.templateContext.VARIABLE}\` in the "${context.actionTypePlural}" object in your exported integration definition.`,
'',
`Also, please file an issue at ${ISSUES_URL} with the contents of your ${context.indexFileResolved}.`,
].join('\n'),
);
}
}
this.stopSpinner();
if (!this.flags.invokedFromAnotherCommand) {
this.log(`\nAll done! Your new ${context.actionType} is ready to use.`);
}
}
/**
* If `--entry` is not provided, this will determine the path to the
* root index file. Notably, we'll look for tsconfig.json and
* src/index.ts first, because even TS apps have a root level plain
* index.js that we should ignore.
*
* @returns {string}
*/
defaultIndexFileLocal() {
const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
const srcIndexTsPath = path.join(process.cwd(), 'src', 'index.ts');
if (fs.existsSync(tsConfigPath) && fs.existsSync(srcIndexTsPath)) {
this.log('Automatically detected TypeScript project');
return 'src/index.ts';
}
return 'index.js';
}
/**
* If `--dest` is not provided, this will determine the directory for
* the new action file to be created in.
*
* @param {string} indexFileLocal - The path to the index file
* @returns {string}
*/
defaultActionDirLocal(indexFileLocal) {
const parent = path.dirname(indexFileLocal);
return path.join(parent, plural(this.args.actionType));
}
/**
* If `--test-dest` is not provided, this will determine the directory
* for the new test file to be created in.
*
* @param {string} indexFileLocal - The path to the index file
* @returns {string}
*/
defaultTestDirLocal(indexFileLocal) {
const parent = path.dirname(indexFileLocal);
return path.join(parent, 'test', plural(this.args.actionType));
}
}
ScaffoldCommand.args = {
actionType: Args.string({
help: 'What type of step type are you creating?',
required: true,
options: ['trigger', 'search', 'create', 'resource'],
}),
noun: Args.string({
help: 'What sort of object this action acts on. For example, the name of the new thing to create',
required: true,
}),
};
ScaffoldCommand.flags = buildFlags({
commandFlags: {
dest: Flags.string({
char: 'd',
description:
"Specify the new file's directory. Use this flag when you want to create a different folder structure such as `src/triggers` instead of the default `triggers`. Defaults to `[triggers|searches|creates]/{noun}`.",
}),
'test-dest': Flags.string({
description:
"Specify the new test file's directory. Use this flag when you want to create a different folder structure such as `src/triggers` instead of the default `triggers`. Defaults to `test/[triggers|searches|creates]/{noun}`.",
}),
entry: Flags.string({
char: 'e',
description:
"Supply the path to your integration's entry point (`index.js` or `src/index.ts`). This will try to automatically detect the correct file if not provided.",
}),
force: Flags.boolean({
char: 'f',
description:
'Should we overwrite an existing trigger/search/create file?',
default: false,
}),
'no-help': Flags.boolean({
description:
"When scaffolding, should we skip adding helpful intro comments? Useful if this isn't your first rodeo.",
default: false,
}),
// TODO: typescript? jscodeshift supports it. We could tweak a template for it
},
});
ScaffoldCommand.examples = [
'zapier scaffold trigger contact',
'zapier scaffold search contact --dest=my_src/searches',
'zapier scaffold create contact --entry=src/index.js',
'zapier scaffold resource contact --force',
];
ScaffoldCommand.description = `Add a starting trigger, create, search, or resource to your integration.
The first argument should be one of \`trigger|search|create|resource\` followed by the noun that this will act on (something like "contact" or "deal").
The scaffold command does two general things:
* Creates a new file (such as \`triggers/contact.js\`)
* Imports and registers it inside your \`index.js\`
You can mix and match several options to customize the created scaffold for your project.`;
ScaffoldCommand.skipValidInstallCheck = true;
module.exports = ScaffoldCommand;