From eea8c83bff0bc135bf7e5fb8d881aea5671462b8 Mon Sep 17 00:00:00 2001 From: Pagan Gazzard Date: Wed, 8 Jul 2020 18:03:10 +0100 Subject: [PATCH] Enforce and improve lazy loading of resin-cli-form Change-type: patch --- lib/actions-oclif/device/os-update.ts | 5 ++--- lib/actions-oclif/device/rename.ts | 5 ++--- lib/actions-oclif/login.ts | 5 ++--- lib/actions-oclif/os/configure.ts | 5 ++--- lib/actions/config.js | 5 ++--- lib/actions/local/common.js | 5 ++--- lib/actions/local/flash.ts | 10 +++++++--- lib/actions/os.js | 11 ++++------- lib/actions/preload.js | 11 ++++------- lib/utils/lazy.ts | 5 +++++ lib/utils/patterns.ts | 27 ++++++++++++--------------- lib/utils/promote.ts | 21 ++++++--------------- tslint.json | 2 +- typings/resin-cli-form/index.d.ts | 22 +++++++++------------- 14 files changed, 60 insertions(+), 79 deletions(-) diff --git a/lib/actions-oclif/device/os-update.ts b/lib/actions-oclif/device/os-update.ts index e7f9b4c5..46f5da4d 100644 --- a/lib/actions-oclif/device/os-update.ts +++ b/lib/actions-oclif/device/os-update.ts @@ -19,7 +19,7 @@ import { flags } from '@oclif/command'; import type { IArg } from '@oclif/parser/lib/args'; import Command from '../../command'; import * as cf from '../../utils/common-flags'; -import { getBalenaSdk, stripIndent } from '../../utils/lazy'; +import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy'; import { tryAsInteger } from '../../utils/validation'; import type { Device } from 'balena-sdk'; import { ExpectedError } from '../../errors'; @@ -78,7 +78,6 @@ export default class DeviceOsUpdateCmd extends Command { const sdk = getBalenaSdk(); const patterns = await import('../../utils/patterns'); - const form = await import('resin-cli-form'); // Get device info const { @@ -121,7 +120,7 @@ export default class DeviceOsUpdateCmd extends Command { ); } } else { - targetOsVersion = await form.ask({ + targetOsVersion = await getCliForm().ask({ message: 'Target OS version', type: 'list', choices: hupVersionInfo.versions.map((version) => ({ diff --git a/lib/actions-oclif/device/rename.ts b/lib/actions-oclif/device/rename.ts index ce0c5db3..9a9da565 100644 --- a/lib/actions-oclif/device/rename.ts +++ b/lib/actions-oclif/device/rename.ts @@ -19,7 +19,7 @@ import { flags } from '@oclif/command'; import type { IArg } from '@oclif/parser/lib/args'; import Command from '../../command'; import * as cf from '../../utils/common-flags'; -import { getBalenaSdk, stripIndent } from '../../utils/lazy'; +import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy'; import { tryAsInteger } from '../../utils/validation'; interface FlagsDef { @@ -69,11 +69,10 @@ export default class DeviceRenameCmd extends Command { const { args: params } = this.parse(DeviceRenameCmd); const balena = getBalenaSdk(); - const form = await import('resin-cli-form'); const newName = params.newName || - (await form.ask({ + (await getCliForm().ask({ message: 'How do you want to name this device?', type: 'input', })) || diff --git a/lib/actions-oclif/login.ts b/lib/actions-oclif/login.ts index 0de1c465..ce2039af 100644 --- a/lib/actions-oclif/login.ts +++ b/lib/actions-oclif/login.ts @@ -18,7 +18,7 @@ import { flags } from '@oclif/command'; import Command from '../command'; import * as cf from '../utils/common-flags'; -import { getBalenaSdk, stripIndent } from '../utils/lazy'; +import { getBalenaSdk, stripIndent, getCliForm } from '../utils/lazy'; import { ExpectedError } from '../errors'; interface FlagsDef { @@ -147,8 +147,7 @@ ${messages.reachingOut}`); // Token if (loginOptions.token) { if (!token) { - const form = await import('resin-cli-form'); - token = await form.ask({ + token = await getCliForm().ask({ message: 'Session token or API key from the preferences page', name: 'token', type: 'input', diff --git a/lib/actions-oclif/os/configure.ts b/lib/actions-oclif/os/configure.ts index 6da3662c..54b43d2d 100644 --- a/lib/actions-oclif/os/configure.ts +++ b/lib/actions-oclif/os/configure.ts @@ -23,7 +23,7 @@ import Command from '../../command'; import { ExpectedError } from '../../errors'; import * as cf from '../../utils/common-flags'; -import { getBalenaSdk, stripIndent } from '../../utils/lazy'; +import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy'; import { CommandHelp } from '../../utils/oclif-utils'; const BOOT_PARTITION = 1; @@ -396,7 +396,6 @@ async function askQuestionsForDeviceType( options: FlagsDef, configJson?: import('../../utils/config').ImgConfig, ): Promise { - const form = await import('resin-cli-form'); const helpers = await import('../../utils/helpers'); const answerSources: any[] = [camelifyConfigOptions(options)]; const defaultAnswers: Partial = {}; @@ -436,7 +435,7 @@ async function askQuestionsForDeviceType( extraOpts = { override: defaultAnswers }; } - return form.run(questions, extraOpts); + return getCliForm().run(questions, extraOpts); } /** diff --git a/lib/actions/config.js b/lib/actions/config.js index da327804..3bb5bd33 100644 --- a/lib/actions/config.js +++ b/lib/actions/config.js @@ -17,7 +17,7 @@ limitations under the License. import * as commandOptions from './command-options'; import { normalizeUuidProp } from '../utils/normalization'; -import { getBalenaSdk, getVisuals } from '../utils/lazy'; +import { getBalenaSdk, getVisuals, getCliForm } from '../utils/lazy'; import * as _ from 'lodash'; const getUmountAsync = async () => { @@ -303,7 +303,6 @@ Examples: normalizeUuidProp(options, 'device'); const Bluebird = require('bluebird'); const balena = getBalenaSdk(); - const form = require('resin-cli-form'); const prettyjson = require('prettyjson'); const { @@ -378,7 +377,7 @@ See the help page for examples: formOptions, // Pass params as an override: if there is any param with exactly the same name as a ) => // required option, that value is used (and the corresponding question is not asked) - form.run(formOptions, { override: options }), + getCliForm().run(formOptions, { override: options }), ) .then(function (answers) { answers.version = options.version; diff --git a/lib/actions/local/common.js b/lib/actions/local/common.js index 7bf3d50c..f299d7e8 100644 --- a/lib/actions/local/common.js +++ b/lib/actions/local/common.js @@ -2,7 +2,7 @@ import * as Bluebird from 'bluebird'; import * as _ from 'lodash'; import * as dockerUtils from '../../utils/docker'; import { exitWithExpectedError } from '../../errors'; -import { getChalk } from '../../utils/lazy'; +import { getChalk, getCliForm } from '../../utils/lazy'; export const dockerPort = 2375; export const dockerTimeout = 2000; @@ -26,7 +26,6 @@ export const selectContainerFromDevice = Bluebird.method(function ( if (filterSupervisor == null) { filterSupervisor = false; } - const form = require('resin-cli-form'); const docker = dockerUtils.createClient({ host: deviceIp, port: dockerPort, @@ -45,7 +44,7 @@ export const selectContainerFromDevice = Bluebird.method(function ( exitWithExpectedError(`No containers found in ${deviceIp}`); } - return form.ask({ + return getCliForm().ask({ message: 'Select a container', type: 'list', choices: _.map(containers, function (container) { diff --git a/lib/actions/local/flash.ts b/lib/actions/local/flash.ts index 33831852..390f515f 100644 --- a/lib/actions/local/flash.ts +++ b/lib/actions/local/flash.ts @@ -17,7 +17,12 @@ limitations under the License. import type { CommandDefinition } from 'capitano'; import type * as SDK from 'etcher-sdk'; -import { getChalk, getVisuals, stripIndent } from '../../utils/lazy'; +import { + getChalk, + getVisuals, + stripIndent, + getCliForm, +} from '../../utils/lazy'; import { ExpectedError } from '../../errors'; async function getDrive(options: { @@ -71,14 +76,13 @@ export const flash: CommandDefinition< }, ], async action(params, options) { - const form = await import('resin-cli-form'); const { sourceDestination, multiWrite } = await import('etcher-sdk'); const drive = await getDrive(options); const yes = options.yes || - (await form.ask({ + (await getCliForm().ask({ message: 'This will erase the selected drive. Are you sure?', type: 'confirm', name: 'yes', diff --git a/lib/actions/os.js b/lib/actions/os.js index 253ce7af..86f89789 100644 --- a/lib/actions/os.js +++ b/lib/actions/os.js @@ -17,7 +17,7 @@ limitations under the License. import * as commandOptions from './command-options'; import * as _ from 'lodash'; -import { getBalenaSdk, getVisuals } from '../utils/lazy'; +import { getBalenaSdk, getVisuals, getCliForm } from '../utils/lazy'; const formatVersion = function (v, isRecommended) { let result = `v${v}`; @@ -35,7 +35,6 @@ const resolveVersion = function (deviceType, version) { return Promise.resolve(version); } - const form = require('resin-cli-form'); const balena = getBalenaSdk(); return balena.models.os @@ -46,7 +45,7 @@ const resolveVersion = function (deviceType, version) { name: formatVersion(v, v === recommended), })); - return form.ask({ + return getCliForm().ask({ message: 'Select the OS version:', type: 'list', choices, @@ -185,7 +184,6 @@ const buildConfigForDeviceType = function (deviceType, advanced) { if (advanced == null) { advanced = false; } - const form = require('resin-cli-form'); const helpers = require('../utils/helpers'); let override; @@ -201,7 +199,7 @@ const buildConfigForDeviceType = function (deviceType, advanced) { } } - return form.run(questions, { override }); + return getCliForm().run(questions, { override }); }; const $buildConfig = function (image, deviceTypeSlug, advanced) { @@ -287,7 +285,6 @@ Examples: action(params, options) { const Bluebird = require('bluebird'); const umountAsync = Bluebird.promisify(require('umount').umount); - const form = require('resin-cli-form'); const patterns = require('../utils/patterns'); const helpers = require('../utils/helpers'); @@ -298,7 +295,7 @@ ${INIT_WARNING_MESSAGE}\ `); return Bluebird.resolve(helpers.getManifest(params.image, options.type)) .then((manifest) => - form.run(manifest.initialization?.options, { + getCliForm().run(manifest.initialization?.options, { override: { drive: options.drive, }, diff --git a/lib/actions/preload.js b/lib/actions/preload.js index 1fd9b0ee..a890db93 100644 --- a/lib/actions/preload.js +++ b/lib/actions/preload.js @@ -15,7 +15,7 @@ limitations under the License. */ import * as _ from 'lodash'; -import { getBalenaSdk, getVisuals } from '../utils/lazy'; +import { getBalenaSdk, getVisuals, getCliForm } from '../utils/lazy'; import * as dockerUtils from '../utils/docker'; const isCurrent = (commit) => commit === 'latest' || commit === 'current'; @@ -83,7 +83,6 @@ const getApplicationsWithSuccessfulBuilds = function (deviceType) { const selectApplication = function (deviceType) { const visuals = getVisuals(); - const form = require('resin-cli-form'); const { exitWithExpectedError } = require('../errors'); const applicationInfoSpinner = new visuals.Spinner( @@ -100,7 +99,7 @@ const selectApplication = function (deviceType) { `You have no apps with successful releases for a '${deviceType}' device type.`, ); } - return form.ask({ + return getCliForm().ask({ message: 'Select an application', type: 'list', choices: applications.map((app) => ({ @@ -112,7 +111,6 @@ const selectApplication = function (deviceType) { }; const selectApplicationCommit = function (releases) { - const form = require('resin-cli-form'); const { exitWithExpectedError } = require('../errors'); if (releases.length === 0) { @@ -125,7 +123,7 @@ const selectApplicationCommit = function (releases) { value: release.commit, })), ); - return form.ask({ + return getCliForm().ask({ message: 'Select a release', type: 'list', default: 'current', @@ -140,7 +138,6 @@ const offerToDisableAutomaticUpdates = function ( ) { const Bluebird = require('bluebird'); const balena = getBalenaSdk(); - const form = require('resin-cli-form'); if ( isCurrent(commit) || @@ -162,7 +159,7 @@ see https://balena.io/docs/reference/api/resources/device/#set-device-to-release Alternatively you can pass the --pin-device-to-release flag to pin only this device to the selected release.\ `; - return form + return getCliForm() .ask({ message, type: 'confirm', diff --git a/lib/utils/lazy.ts b/lib/utils/lazy.ts index 33c01164..975ae3ee 100644 --- a/lib/utils/lazy.ts +++ b/lib/utils/lazy.ts @@ -19,6 +19,7 @@ limitations under the License. import type * as BalenaSdk from 'balena-sdk'; import type { Chalk } from 'chalk'; import type * as visuals from 'resin-cli-visuals'; +import type * as CliForm from 'resin-cli-form'; import type { stripIndent as StripIndent } from 'common-tags'; // Equivalent of _.once but avoiding the need to import lodash for lazy deps @@ -52,6 +53,10 @@ export const getVisuals = once( export const getChalk = once(() => require('chalk') as Chalk); +export const getCliForm = once( + () => require('resin-cli-form') as typeof CliForm, +); + // Directly export stripIndent as we always use it immediately, but importing just `stripIndent` reduces startup time // tslint:disable-next-line:no-var-requires export const stripIndent = require('common-tags/lib/stripIndent') as typeof StripIndent; diff --git a/lib/utils/patterns.ts b/lib/utils/patterns.ts index cb6b0a87..824d0b2d 100644 --- a/lib/utils/patterns.ts +++ b/lib/utils/patterns.ts @@ -16,7 +16,6 @@ limitations under the License. import { BalenaApplicationNotFound } from 'balena-errors'; import type * as BalenaSdk from 'balena-sdk'; import _ = require('lodash'); -import _form = require('resin-cli-form'); import { exitWithExpectedError, @@ -24,15 +23,13 @@ import { NotLoggedInError, ExpectedError, } from '../errors'; -import { getBalenaSdk, getVisuals, stripIndent } from './lazy'; +import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy'; import validation = require('./validation'); import { delay } from './helpers'; -const getForm = _.once((): typeof _form => require('resin-cli-form')); - export function authenticate(options: {}): Promise { const balena = getBalenaSdk(); - return getForm() + return getCliForm() .run( [ { @@ -56,7 +53,7 @@ export function authenticate(options: {}): Promise { return; } - return getForm() + return getCliForm() .ask({ message: 'Two factor auth challenge:', name: 'code', @@ -91,7 +88,7 @@ export async function checkLoggedIn(): Promise { } export function askLoginType() { - return getForm().ask<'web' | 'credentials' | 'token' | 'register'>({ + return getCliForm().ask<'web' | 'credentials' | 'token' | 'register'>({ message: 'How would you like to login?', name: 'loginType', type: 'list', @@ -123,7 +120,7 @@ export function selectDeviceType() { deviceTypes = _.sortBy(deviceTypes, 'name').filter( (dt) => dt.state !== 'DISCONTINUED', ); - return getForm().ask({ + return getCliForm().ask({ message: 'Device Type', type: 'list', choices: _.map(deviceTypes, ({ slug: value, name }) => ({ @@ -147,7 +144,7 @@ export async function confirm( return; } - const confirmed = await getForm().ask({ + const confirmed = await getCliForm().ask({ message, type: 'confirm', default: false, @@ -177,7 +174,7 @@ export function selectApplication( }) .filter(filter || _.constant(true)) .then((applications) => { - return getForm().ask({ + return getCliForm().ask({ message: 'Select an application', type: 'list', choices: _.map(applications, (application) => ({ @@ -212,7 +209,7 @@ export function selectOrCreateApplication() { value: null, }); - return getForm().ask({ + return getCliForm().ask({ message: 'Select an application', type: 'list', choices: appOptions, @@ -224,7 +221,7 @@ export function selectOrCreateApplication() { return application; } - return getForm().ask({ + return getCliForm().ask({ message: 'Choose a Name for your new application', type: 'input', validate: validation.validateApplicationName, @@ -324,7 +321,7 @@ export function inferOrSelectDevice(preferredUuid: string) { ? preferredUuid : onlineDevices[0].uuid; - return getForm().ask({ + return getCliForm().ask({ message: 'Select a device', type: 'list', default: defaultUuid, @@ -381,7 +378,7 @@ export async function getOnlineTargetUuid( throw new ExpectedError('No accessible devices are online'); } - return await getForm().ask({ + return await getCliForm().ask({ message: 'Select a device', type: 'list', default: devices[0].uuid, @@ -416,7 +413,7 @@ export function selectFromList( message: string, choices: Array, ): Promise { - return getForm().ask({ + return getCliForm().ask({ message, type: 'list', choices: _.map(choices, (s) => ({ diff --git a/lib/utils/promote.ts b/lib/utils/promote.ts index 27c894bb..7e92e048 100644 --- a/lib/utils/promote.ts +++ b/lib/utils/promote.ts @@ -17,7 +17,7 @@ import type * as BalenaSdk from 'balena-sdk'; import { ExpectedError, printErrorMessage } from '../errors'; -import { getVisuals, stripIndent } from './lazy'; +import { getVisuals, stripIndent, getCliForm } from './lazy'; import Logger = require('./logger'); import { exec, execBuffered, getDeviceOsRelease } from './ssh'; @@ -206,7 +206,6 @@ async function getOrSelectApplication( appName?: string, ): Promise { const _ = await import('lodash'); - const form = await import('resin-cli-form'); const allDeviceTypes = await sdk.models.config.getDeviceTypes(); const deviceTypeManifest = _.find(allDeviceTypes, { slug: deviceType }); @@ -227,12 +226,7 @@ async function getOrSelectApplication( .value(); if (!appName) { - return createOrSelectAppOrExit( - form, - sdk, - compatibleDeviceTypes, - deviceType, - ); + return createOrSelectAppOrExit(sdk, compatibleDeviceTypes, deviceType); } const options: BalenaSdk.PineOptionsFor = {}; @@ -254,7 +248,7 @@ async function getOrSelectApplication( const applications = await sdk.models.application.getAll(options); if (applications.length === 0) { - const shouldCreateApp = await form.ask({ + const shouldCreateApp = await getCliForm().ask({ message: `No application found with name "${appName}".\n` + 'Would you like to create it now?', @@ -288,7 +282,6 @@ async function getOrSelectApplication( // `getOrSelectApplication` above in order to satisfy some resin-lint v3 // rules, but it looks like there's a fair amount of duplicate logic. async function createOrSelectAppOrExit( - form: any, sdk: BalenaSdk.BalenaSDK, compatibleDeviceTypes: string[], deviceType: string, @@ -301,7 +294,7 @@ async function createOrSelectAppOrExit( const applications = await sdk.models.application.getAll(options); if (applications.length === 0) { - const shouldCreateApp = await form.ask({ + const shouldCreateApp = await getCliForm().ask({ message: 'You have no applications this device can join.\n' + 'Would you like to create one now?', @@ -322,7 +315,6 @@ async function createApplication( deviceType: string, name?: string, ): Promise { - const form = await import('resin-cli-form'); const validation = await import('./validation'); let username = await sdk.auth.whoami(); @@ -334,7 +326,7 @@ async function createApplication( const applicationName = await new Promise(async (resolve, reject) => { while (true) { try { - const appName = await form.ask({ + const appName = await getCliForm().ask({ message: 'Enter a name for your new application:', type: 'input', default: name, @@ -376,7 +368,6 @@ async function generateApplicationConfig( app: BalenaSdk.Application, options: { version: string }, ) { - const form = await import('resin-cli-form'); const { generateApplicationConfig: configGen } = await import('./config'); const manifest = await sdk.models.device.getManifestBySlug(app.device_type); @@ -384,7 +375,7 @@ async function generateApplicationConfig( manifest.options && manifest.options.filter((opt) => opt.name !== 'network'); const values = { - ...(opts ? await form.run(opts) : {}), + ...(opts ? await getCliForm().run(opts) : {}), ...options, }; diff --git a/tslint.json b/tslint.json index 497c672c..775e7d83 100644 --- a/tslint.json +++ b/tslint.json @@ -2,6 +2,6 @@ "extends": "./node_modules/@balena/lint/config/tslint-prettier.json", "rules": { "ignoreDefinitionFiles": false, - "import-blacklist": [true, "resin-cli-visuals", "chalk", "common-tags"] + "import-blacklist": [true, "resin-cli-visuals", "chalk", "common-tags", "resin-cli-form"] } } diff --git a/typings/resin-cli-form/index.d.ts b/typings/resin-cli-form/index.d.ts index 7f3856ad..625e4cb8 100644 --- a/typings/resin-cli-form/index.d.ts +++ b/typings/resin-cli-form/index.d.ts @@ -18,13 +18,13 @@ declare module 'resin-cli-form' { import Bluebird = require('bluebird'); - type TypeOrPromiseLike = T | PromiseLike; + export type TypeOrPromiseLike = T | PromiseLike; - type Validate = ( + export type Validate = ( input: any, ) => TypeOrPromiseLike; - interface AskOptions { + export interface AskOptions { message: string; type?: string; name?: string; @@ -36,20 +36,16 @@ declare module 'resin-cli-form' { validate?: Validate; } - interface RunQuestion { + export interface RunQuestion { message: string; name: string; type?: string; validate?: Validate; } - const form: { - ask: (options: AskOptions) => Bluebird; - run: ( - questions?: RunQuestion[], - extraOptions?: { override?: object }, - ) => Bluebird; - }; - - export = form; + export const ask: (options: AskOptions) => Bluebird; + export const run: ( + questions?: RunQuestion[], + extraOptions?: { override?: object }, + ) => Bluebird; }