UNPKG

@swell/cli

Version:

Swell's command line interface/utility

330 lines (329 loc) 12.7 kB
import { input, select } from '@inquirer/prompts'; import { Args, Flags } from '@oclif/core'; import { CreateConfigCommand } from '../../create-config-command.js'; import { ConfigType } from '../../lib/apps/index.js'; import { getFunctionTemplate, } from '../../lib/create/function.js'; import { toFunctionFileName } from '../../lib/create/index.js'; const PERMITTED_TRIGGERS = ['cron', 'model', 'route']; const PERMITTED_MODES = [...PERMITTED_TRIGGERS, 'workflow']; export default class CreateFunction extends CreateConfigCommand { static args = { name: Args.string({ default: '', description: 'Filename with extension (e.g., my-func.ts)', }), trigger: Args.string({ default: '', description: 'cron | model | route | workflow', options: PERMITTED_MODES, }), }; static examples = [ '$ swell create function', '$ swell create function order-handler.ts model -e order.created,order.updated -y', '$ swell create function cleanup.ts cron -s "0 0 * * *" -y', '$ swell create function api-endpoint.ts route -r public -y', '$ swell create function run-import.ts --type workflow -y', '$ swell create function run-import.ts workflow -y', ]; static helpMeta = { usageDirect: '<name> [<trigger>|workflow] [...] -y', variantSection: { title: 'FUNCTION MODES', variants: [ { name: 'model', flag: '-e, --events=<value>', description: 'Model events (e.g., order.created,order.updated)', }, { name: 'cron', flag: '-s, --schedule=<value>', description: 'Cron expression (e.g., "0 0 * * *")', }, { name: 'route', flag: '-r, --route=<option>', description: 'Access: public (default) | private', }, { name: 'workflow', flag: '--type=workflow', description: 'Durable explicit-create workflow', }, ], }, variantFlags: ['events', 'schedule', 'route'], }; static flags = { description: Flags.string({ char: 'd', default: '', description: 'Description', }), events: Flags.string({ char: 'e', default: '', description: 'Model events (e.g., order.created,order.updated)', }), overwrite: Flags.boolean({ default: false, description: 'Overwrite existing file', }), route: Flags.string({ char: 'r', default: '', description: 'Access: public | private', options: ['public', 'private'], }), schedule: Flags.string({ char: 's', default: '', description: 'Cron expression (e.g., "0 0 * * *")', }), type: Flags.string({ description: 'Create a workflow function', options: ['workflow'], }), yes: Flags.boolean({ char: 'y', description: 'Skip prompts, require all arguments', }), }; static summary = 'Create a serverless function in JavaScript or TypeScript.'; createType = ConfigType.FUNCTION; async run() { const { args, flags } = await this.parse(CreateFunction); const confirmYes = Boolean(flags.yes); const trigger = this.resolveTrigger(args.trigger, flags.type); // NON-INTERACTIVE PATH if (confirmYes) { // Validate name argument if (!args.name) { this.error('Missing required argument for non-interactive mode: NAME\n\nExample: swell create function my-func.ts model -e order.created -y', { exit: 1 }); } // Validate trigger argument if (!trigger) { this.error('Missing required argument for non-interactive mode: TRIGGER or --type workflow\n\nValid modes: cron, model, route, workflow\n\nExample: swell create function my-func.ts model -e order.created -y\nExample: swell create function run-import.ts --type workflow -y', { exit: 1 }); } // Validate filename has extension const language = args.name.endsWith('.ts') ? 'ts' : args.name.endsWith('.js') ? 'js' : null; if (!language) { this.error('Function name must end with .ts or .js extension\n\nExample: swell create function my-func.ts model -e order.created -y', { exit: 1 }); } // Validate trigger-specific requirements if (trigger === 'model' && !flags.events) { this.error('Missing required flag for model trigger: --events\n\nExample: swell create function my-func.ts model -e order.created,order.updated -y', { exit: 1 }); } if (trigger === 'cron' && !flags.schedule) { this.error('Missing required flag for cron trigger: --schedule\n\nExample: swell create function my-func.ts cron -s "0 0 * * *" -y', { exit: 1 }); } this.validateWorkflowFlags(trigger, flags); // Build params with defaults const writeFunctionFileParams = { description: flags.description || '', events: flags.events || '', extension: undefined, functionName: args.name, language, route: flags.route || 'public', schedule: flags.schedule || '', trigger, }; // Write file without confirmation await this.writeFunctionFile(writeFunctionFileParams, flags.overwrite, /* shouldConfirm */ false); return; } // INTERACTIVE PATH const { name: functionName } = args; const { description, events, overwrite, route, schedule } = flags; let writeFunctionFileParams = { description, events, extension: undefined, functionName: functionName || '', language: (functionName?.endsWith('.ts') ? 'ts' : functionName?.endsWith('.js') ? 'js' : undefined), route, schedule, trigger: trigger, }; writeFunctionFileParams = { ...writeFunctionFileParams, ...(await this.gatherInput(writeFunctionFileParams)), }; this.validateWorkflowFlags(writeFunctionFileParams.trigger, flags); await this.writeFunctionFile(writeFunctionFileParams, overwrite, /* shouldConfirm */ true); } async gatherInput(params) { let { language } = params; if (!language) { language = await select({ choices: [ { name: 'TypeScript', value: 'ts', }, { name: 'JavaScript', value: 'js', }, ], message: 'What language are you using?', }); } let trigger = params.trigger; if (!trigger) { trigger = await select({ choices: [ { name: 'Function', value: 'function', }, { name: 'Workflow', value: 'workflow', }, ], message: 'What are you creating?', }); } if (trigger === 'function') { trigger = await select({ choices: [ { name: 'Event (model)', value: 'model', }, { name: 'Schedule (cron)', value: 'cron', }, { name: 'HTTP (route)', value: 'route', }, ], message: 'How will your function be triggered?', }); } let { extension } = params; if (trigger !== 'workflow') { const extensions = this.swellConfig.get('extensions') || []; const extensionsWithId = extensions.filter((ext) => ext.id); if (extensionsWithId.length > 0 && !extension) { const extensionChoices = [ { name: 'Not for an extension', value: '' }, ...extensionsWithId.map((ext) => ({ name: ext.id, value: ext.id, })), ]; extension = await select({ choices: extensionChoices, message: 'Is this function for an app extension?', }); } } let { events } = params; if (trigger === 'model' && !events) { events = await input({ default: 'product.created, etc...', message: 'Enter event types to trigger this function (comma separated)', }); if (events === 'product.created, etc...') { events = ''; } } let { schedule } = params; if (trigger === 'cron' && !schedule) { schedule = await input({ default: '0 0 * * *', message: 'Enter a crontab value', }); } let { route } = params; if (trigger === 'route' && !route) { route = await select({ choices: [ { name: 'Public', value: 'public' }, { name: 'Private', value: 'private' }, ], message: 'Should the HTTP route be public (using public_key) or private (using secret_key)?', }); } let { functionName } = params; if (!functionName) { functionName = await input({ default: functionName, message: 'Name your function', }); } let { description } = params; if (!description) { description = await input({ default: 'Does a thing when another other thing happens', message: 'Describe what the function does', }); } return { description, events: events || '', extension: extension || undefined, functionName, language, route: route || '', schedule: schedule || '', trigger: trigger, }; } resolveTrigger(positionalTrigger, typeFlag) { if (typeFlag === 'workflow') { if (positionalTrigger && PERMITTED_TRIGGERS.includes(positionalTrigger)) { this.error('--type workflow cannot be combined with a positional cron, model, or route trigger.', { exit: 1 }); } return 'workflow'; } return positionalTrigger ? positionalTrigger : undefined; } validateWorkflowFlags(trigger, flags) { if (trigger !== 'workflow') { return; } const invalidFlags = [ flags.events ? '--events' : '', flags.schedule ? '--schedule' : '', flags.route ? '--route' : '', ].filter(Boolean); if (invalidFlags.length > 0) { this.error(`Workflow functions do not support ordinary trigger flags: ${invalidFlags.join(', ')}.`, { exit: 1 }); } } async writeFunctionFile({ description, events, extension, functionName, language, route, schedule, trigger, }, overwrite, shouldConfirm = true) { const fileName = toFunctionFileName(functionName); await this.createFile({ extension: language, fileBody: getFunctionTemplate({ description, events, extension, functionName, language, route, schedule, trigger, }), fileName, }, overwrite, shouldConfirm); } }