UNPKG

boostr

Version:
306 lines (299 loc) 12.8 kB
import fsExtra from 'fs-extra'; import { join, resolve } from 'path'; import { temporaryFileTask } from 'tempy'; import { Subservice } from './sub.js'; import { check } from '../checker.js'; import { build } from '../builder.js'; import { ProcessController } from '../processes/index.js'; import { AWSFunctionResource, domainNameToLambdaFunctionName } from '../resources/aws/function.js'; const BOOTSTRAP_TEMPLATE_LOCAL = `export {default} from '{{entryPoint}}';`; const BOOTSTRAP_TEMPLATE_AWS_LAMBDA = ` import {createAWSLambdaHandler, createAWSLambdaExecutionQueueSender} from '@layr/aws-integration'; import {ExecutionQueue} from '@layr/execution-queue'; import AWS from 'aws-sdk'; import componentGetter from '{{entryPoint}}'; export const handler = createAWSLambdaHandler(async function () { const rootComponent = await componentGetter(); const lambdaClient = new AWS.Lambda({apiVersion: '2015-03-31'}); const executionQueueSender = createAWSLambdaExecutionQueueSender({ lambdaClient, functionName: '{{lambdaFunctionName}}' }); const executionQueue = new ExecutionQueue(executionQueueSender); executionQueue.registerRootComponent(rootComponent); return rootComponent; }); `; export class BackendService extends Subservice { async check() { await super.check(); const serviceDirectory = this.getDirectory(); const serviceName = this.getName(); await check({ serviceDirectory, serviceName }); } async build({ watch = false, forceLocal = false } = {}) { await super.build(); const serviceDirectory = this.getDirectory(); const serviceName = this.getName(); const stage = this.getStage(); const { environment, platform, rootComponent, build: buildConfig = {}, hooks } = this.getConfig(); if (!rootComponent) { this.throwError(`A 'rootComponent' property is required in the configuration (directory: '${serviceDirectory}')`); } const buildDirectory = join(serviceDirectory, 'build', stage); fsExtra.emptyDirSync(buildDirectory); const isLocal = platform === 'local' || forceLocal; let bundleFileNameWithoutExtension; let bootstrapTemplate; let bootstrapVariables; let builtInExternal; if (isLocal) { bundleFileNameWithoutExtension = 'bundle'; bootstrapTemplate = BOOTSTRAP_TEMPLATE_LOCAL; bootstrapVariables = {}; builtInExternal = undefined; } else if (platform === 'aws') { bundleFileNameWithoutExtension = 'handler'; bootstrapTemplate = BOOTSTRAP_TEMPLATE_AWS_LAMBDA; const { hostname } = this.parseConfigURL(); bootstrapVariables = { lambdaFunctionName: domainNameToLambdaFunctionName(hostname) }; builtInExternal = ['aws-sdk']; } else { this.throwError(`Couldn't create a build configuration for the '${platform}' platform`); } const { jsBundleFile } = await build({ serviceDirectory, entryPoint: rootComponent, buildDirectory, bundleFileNameWithoutExtension, bootstrapTemplate, bootstrapVariables, serviceName, environment, external: buildConfig.external, builtInExternal, sourceMap: buildConfig.sourceMap ?? isLocal, minify: buildConfig.minify ?? !isLocal, installExternalDependencies: !isLocal, watch, esbuildOptions: { target: 'node16', platform: 'node', mainFields: ['module', 'main'] } }); if (hooks?.afterBuild !== undefined) { // TODO: Handle watch mode await hooks.afterBuild({ serviceDirectory, serviceName, stage, platform, buildDirectory, jsBundleFile }); } return { buildDirectory, jsBundleFile }; } async start() { await super.start(); const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); if (config.platform !== 'local') { return; } const { port } = this.parseConfigURL(); let processController; const { jsBundleFile } = await this.build({ watch: { afterRebuild() { processController.restart(); } } }); processController = new ProcessController('start-backend', ['--componentGetterFile', jsBundleFile, '--port', String(port)], { currentDirectory: directory, environment: config.environment, serviceName }); await processController.start(); } async migrateDatabase(databaseURL) { const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); const { jsBundleFile } = await this.build({ forceLocal: true }); const processController = new ProcessController('migrate-database', ['--componentGetterFile', jsBundleFile, '--databaseURL', databaseURL], { currentDirectory: directory, environment: config.environment, serviceName }); await processController.run(); } async importDatabase(databaseURL, inputFile) { const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); const { jsBundleFile } = await this.build({ forceLocal: true }); const processController = new ProcessController('import-database', [ '--componentGetterFile', jsBundleFile, '--databaseURL', databaseURL, '--inputFile', inputFile ], { currentDirectory: directory, environment: config.environment, serviceName }); await processController.run(); } async exportDatabase(databaseURL, outputFile) { const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); const { jsBundleFile } = await this.build({ forceLocal: true }); const processController = new ProcessController('export-database', [ '--componentGetterFile', jsBundleFile, '--databaseURL', databaseURL, '--outputFile', outputFile ], { currentDirectory: directory, environment: config.environment, serviceName }); await processController.run(); } async deploy({ skipServiceNames = [] } = {}) { await super.deploy({ skipServiceNames }); const serviceName = this.getName(); if (skipServiceNames.includes(serviceName)) { return; } const config = this.getConfig(); if (!config.url) { this.logMessage(`The 'url' property is not specified in the configuration. Skipping deployment...`); return; } await this.check(); const backgroundMethods = await this.findBackgroundMethods(); const { hostname } = this.parseConfigURL(); const { buildDirectory } = await this.build(); const resource = new AWSFunctionResource({ domainName: hostname, region: config.aws?.region, profile: config.aws?.profile, accessKeyId: config.aws?.accessKeyId, secretAccessKey: config.aws?.secretAccessKey, directory: buildDirectory, environment: config.environment, backgroundMethods, lambda: { runtime: config.aws?.lambda?.runtime, executionRole: config.aws?.lambda?.executionRole, memorySize: config.aws?.lambda?.memorySize, timeout: config.aws?.lambda?.timeout, reservedConcurrentExecutions: config.aws?.lambda?.reservedConcurrentExecutions } }, { serviceName }); await resource.initialize(); await resource.deploy(); } async findBackgroundMethods() { this.logMessage('Searching for background methods...'); const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); await this.startDependencies(); const { jsBundleFile } = await this.build({ forceLocal: true }); const backgroundMethods = await temporaryFileTask(async (outputFile) => { const processController = new ProcessController('find-backend-background-methods', [ '--componentGetterFile', jsBundleFile, '--serviceName', serviceName, '--outputFile', outputFile ], { currentDirectory: directory, environment: config.environment, serviceName, nodeArguments: ['--experimental-repl-await'] }); await processController.run(); return fsExtra.readJSONSync(outputFile); }); await this.stopDependencies(); this.logMessage(`${backgroundMethods.length} background method(s) found`); return backgroundMethods; } async introspect(outputFile) { outputFile = resolve(process.cwd(), outputFile); const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); const { jsBundleFile } = await this.build({ forceLocal: true }); const processController = new ProcessController('introspect-backend', ['--componentGetterFile', jsBundleFile, '--outputFile', outputFile], { currentDirectory: directory, environment: config.environment, serviceName }); await processController.run(); } async eval(code) { const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); await this.startDependencies(); const { jsBundleFile } = await this.build({ forceLocal: true }); const processController = new ProcessController('eval-backend', ['--componentGetterFile', jsBundleFile, '--serviceName', serviceName, '--code', code], { currentDirectory: directory, environment: config.environment, serviceName, nodeArguments: ['--experimental-repl-await'] }); await processController.run(); await this.stopDependencies(); } async startREPL() { const directory = this.getDirectory(); const config = this.getConfig(); const serviceName = this.getName(); await this.startDependencies(); const { jsBundleFile } = await this.build({ forceLocal: true }); const processController = new ProcessController('start-backend-repl', ['--componentGetterFile', jsBundleFile, '--serviceName', serviceName], { currentDirectory: directory, environment: config.environment, serviceName, nodeArguments: ['--experimental-repl-await'], decorateOutput: false }); await processController.run(); await this.stopDependencies(); } } BackendService.type = 'backend'; BackendService.description = 'A backend service implementing the data model and the business logic of your app.'; BackendService.examples = [ 'boostr {{serviceName}} start', 'boostr {{serviceName}} deploy --production', 'boostr {{serviceName}} exec -- npm install lodash' ]; // === Commands === BackendService.commands = { ...Subservice.commands, introspect: { ...Subservice.commands.introspect, description: 'Introspects your backend root component and writes the result to a JSON file.', examples: ['boostr {{serviceName}} introspect introspection.json'], arguments: ['outputFile'], async handler([outputFile]) { await this.introspect(outputFile); } }, eval: { ...Subservice.commands.eval, description: 'Evaluates the specified JavaScript code with your backend root component exposed globally.', examples: ['boostr {{serviceName}} eval "Application.isHealthy()"'], arguments: ['codeToEval'], async handler([code]) { await this.eval(code); } }, repl: { ...Subservice.commands.repl, description: 'Starts a Node.js REPL with your backend root component exposed globally.', examples: ['boostr {{serviceName}} repl'], async handler() { await this.startREPL(); } } }; //# sourceMappingURL=backend.js.map