diff --git a/lib/utils/compose-types.d.ts b/lib/utils/compose-types.d.ts index 5b27abb3..68119ce8 100644 --- a/lib/utils/compose-types.d.ts +++ b/lib/utils/compose-types.d.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import type { ImageModel, ReleaseModel } from 'balena-release/build/models'; import type { Composition, ImageDescriptor } from 'resin-compose-parse'; import type { Pack } from 'tar-stream'; @@ -52,7 +53,7 @@ export interface ComposeOpts { inlineLogs?: boolean; multiDockerignore: boolean; noParentCheck: boolean; - projectName: string; + projectName?: string; projectPath: string; isLocal?: boolean; } @@ -79,7 +80,7 @@ export interface ComposeProject { export interface Release { client: ReturnType; release: Pick< - import('balena-release/build/models').ReleaseModel, + ReleaseModel, | 'id' | 'status' | 'commit' @@ -91,7 +92,9 @@ export interface Release { | 'start_timestamp' | 'end_timestamp' >; - serviceImages: Partial; + serviceImages: Dictionary< + Omit + >; } interface TarDirectoryOptions { diff --git a/lib/utils/compose.js b/lib/utils/compose.ts similarity index 64% rename from lib/utils/compose.js rename to lib/utils/compose.ts index fecd67c8..f07e25fd 100644 --- a/lib/utils/compose.js +++ b/lib/utils/compose.ts @@ -15,14 +15,32 @@ * limitations under the License. */ +import type { Renderer } from './compose_ts'; +import type * as SDK from 'balena-sdk'; +import type Dockerode = require('dockerode'); import * as path from 'path'; +import type { Composition, ImageDescriptor } from 'resin-compose-parse'; +import type { + BuiltImage, + ComposeOpts, + ComposeProject, + Release, + TaggedImage, +} from './compose-types'; import { getChalk } from './lazy'; +import Logger = require('./logger'); +import { ProgressCallback } from 'docker-progress'; -/** - * @returns Promise<{import('./compose-types').ComposeOpts}> - */ -export function generateOpts(options) { - const { promises: fs } = require('fs'); +export function generateOpts(options: { + source?: string; + projectName?: string; + nologs: boolean; + 'noconvert-eol': boolean; + dockerfile?: string; + 'multi-dockerignore': boolean; + 'noparent-check': boolean; +}): Promise { + const { promises: fs } = require('fs') as typeof import('fs'); return fs.realpath(options.source || '.').then((projectPath) => ({ projectName: options.projectName, projectPath, @@ -34,24 +52,19 @@ export function generateOpts(options) { })); } -// Parse the given composition and return a structure with info. Input is: -// - composePath: the *absolute* path to the directory containing the compose file -// - composeStr: the contents of the compose file, as a string -/** - * @param {string} composePath - * @param {string} composeStr - * @param {string | undefined} projectName The --projectName flag (build, deploy) - * @param {string | undefined} imageTag The --tag flag (build, deploy) - * @returns {import('./compose-types').ComposeProject} +/** Parse the given composition and return a structure with info. Input is: + * - composePath: the *absolute* path to the directory containing the compose file + * - composeStr: the contents of the compose file, as a string */ export function createProject( - composePath, - composeStr, + composePath: string, + composeStr: string, projectName = '', imageTag = '', -) { - const yml = require('js-yaml'); - const compose = require('resin-compose-parse'); +): ComposeProject { + const yml = require('js-yaml') as typeof import('js-yaml'); + const compose = + require('resin-compose-parse') as typeof import('resin-compose-parse'); // both methods below may throw. const rawComposition = yml.load(composeStr); @@ -67,7 +80,8 @@ export function createProject( descr.image.context != null && descr.image.tag == null ) { - const { makeImageName } = require('./compose_ts'); + const { makeImageName } = + require('./compose_ts') as typeof import('./compose_ts'); descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag); } return descr; @@ -80,30 +94,20 @@ export function createProject( }; } -/** - * @param {string} apiEndpoint - * @param {string} auth - * @param {number} userId - * @param {number} appId - * @param {import('resin-compose-parse').Composition} composition - * @param {boolean} draft - * @param {string|undefined} semver - * @param {string|undefined} contract - * @returns {Promise} - */ export const createRelease = async function ( - apiEndpoint, - auth, - userId, - appId, - composition, - draft, - semver, - contract, -) { - const _ = require('lodash'); - const crypto = require('crypto'); - const releaseMod = require('balena-release'); + apiEndpoint: string, + auth: string, + userId: number, + appId: number, + composition: Composition, + draft: boolean, + semver?: string, + contract?: string, +): Promise { + const _ = require('lodash') as typeof import('lodash'); + const crypto = require('crypto') as typeof import('crypto'); + const releaseMod = + require('balena-release') as typeof import('balena-release'); const client = releaseMod.createClient({ apiEndpoint, auth }); @@ -133,24 +137,26 @@ export const createRelease = async function ( 'start_timestamp', 'end_timestamp', ]), - serviceImages: _.mapValues(serviceImages, (serviceImage) => - _.omit(serviceImage, [ - 'created_at', - 'is_a_build_of__service', - '__metadata', - ]), + serviceImages: _.mapValues( + serviceImages, + (serviceImage) => + _.omit(serviceImage, [ + 'created_at', + 'is_a_build_of__service', + '__metadata', + ]) as Omit< + typeof serviceImage, + 'created_at' | 'is_a_build_of__service' | '__metadata' + >, ), }; }; -/** - * - * @param {import('dockerode')} docker - * @param {Array} images - * @param {Partial} serviceImages - * @returns {Promise>} - */ -export const tagServiceImages = (docker, images, serviceImages) => +export const tagServiceImages = ( + docker: Dockerode, + images: BuiltImage[], + serviceImages: Release['serviceImages'], +): Promise => Promise.all( images.map(function (d) { const serviceImage = serviceImages[d.serviceName]; @@ -177,25 +183,24 @@ export const tagServiceImages = (docker, images, serviceImages) => }), ); -/** - * @param {*} sdk - * @param {import('./logger')} logger - * @param {number} appID - * @returns {Promise} - */ -export const getPreviousRepos = (sdk, logger, appID) => +export const getPreviousRepos = ( + sdk: SDK.BalenaSDK, + logger: Logger, + appID: number, +): Promise => sdk.pine - .get({ + .get({ resource: 'release', options: { + $select: 'id', $filter: { belongs_to__application: appID, status: 'success', }, - $select: ['id'], $expand: { contains__image: { - $expand: 'image', + $select: 'image', + $expand: { image: { $select: 'is_stored_at__image_location' } }, }, }, $orderby: 'id desc', @@ -205,8 +210,11 @@ export const getPreviousRepos = (sdk, logger, appID) => .then(function (release) { // grab all images from the latest release, return all image locations in the registry if (release.length > 0) { - const images = release[0].contains__image; - const { getRegistryAndName } = require('resin-multibuild'); + const images = release[0].contains__image as Array<{ + image: [SDK.Image]; + }>; + const { getRegistryAndName } = + require('resin-multibuild') as typeof import('resin-multibuild'); return Promise.all( images.map(function (d) { const imageName = d.image[0].is_stored_at__image_location || ''; @@ -226,21 +234,13 @@ export const getPreviousRepos = (sdk, logger, appID) => return []; }); -/** - * @param {*} sdk - * @param {string} tokenAuthEndpoint - * @param {string} registry - * @param {string[]} images - * @param {string[]} previousRepos - * @returns {Promise} - */ export const authorizePush = function ( - sdk, - tokenAuthEndpoint, - registry, - images, - previousRepos, -) { + sdk: SDK.BalenaSDK, + tokenAuthEndpoint: string, + registry: string, + images: string[], + previousRepos: string[], +): Promise { if (!Array.isArray(images)) { images = [images]; } @@ -261,17 +261,20 @@ export const authorizePush = function ( // utilities -const renderProgressBar = function (percentage, stepCount) { - const _ = require('lodash'); +const renderProgressBar = function (percentage: number, stepCount: number) { + const _ = require('lodash') as typeof import('lodash'); percentage = _.clamp(percentage, 0, 100); const barCount = Math.floor((stepCount * percentage) / 100); const spaceCount = stepCount - barCount; const bar = `[${_.repeat('=', barCount)}>${_.repeat(' ', spaceCount)}]`; - return `${bar} ${_.padStart(percentage, 3)}%`; + return `${bar} ${_.padStart(`${percentage}`, 3)}%`; }; -export const pushProgressRenderer = function (tty, prefix) { - const fn = function (e) { +export const pushProgressRenderer = function ( + tty: ReturnType, + prefix: string, +): ProgressCallback & { end: () => void } { + const fn: ProgressCallback & { end: () => void } = function (e) { const { error, percentage } = e; if (error != null) { throw new Error(error); @@ -285,14 +288,39 @@ export const pushProgressRenderer = function (tty, prefix) { return fn; }; -export class BuildProgressUI { - constructor(tty, descriptors) { +export class BuildProgressUI implements Renderer { + public streams; + private _prefix; + private _prefixWidth; + private _tty; + private _services; + private _startTime: undefined | number; + private _ended; + private _serviceToDataMap: Dictionary<{ + status?: string; + progress?: number; + error?: Error; + }> = {}; + private _cancelled; + private _spinner; + private _runloop: + | undefined + | ReturnType; + + // these are to handle window wrapping + private _maxLineWidth: undefined | number; + private _lineWidths: number[] = []; + + constructor( + tty: ReturnType, + descriptors: ImageDescriptor[], + ) { this._handleEvent = this._handleEvent.bind(this); this.start = this.start.bind(this); this.end = this.end.bind(this); this._display = this._display.bind(this); - const _ = require('lodash'); - const through = require('through2'); + const _ = require('lodash') as typeof import('lodash'); + const through = require('through2') as typeof import('through2'); const eventHandler = this._handleEvent; const services = _.map(descriptors, 'serviceName'); @@ -310,7 +338,6 @@ export class BuildProgressUI { .value(); this._tty = tty; - this._serviceToDataMap = {}; this._services = services; // Logger magically prefixes the log line with [Build] etc., but it doesn't @@ -320,22 +347,22 @@ export class BuildProgressUI { const offset = 10; // account for escape sequences inserted for colouring this._prefixWidth = - offset + prefix.length + _.max(_.map(services, 'length')); + offset + prefix.length + _.max(_.map(services, (s) => s.length))!; this._prefix = prefix; - // these are to handle window wrapping - this._maxLineWidth = null; - this._lineWidths = []; - - this._startTime = null; this._ended = false; this._cancelled = false; - this._spinner = require('./compose_ts').createSpinner(); + this._spinner = ( + require('./compose_ts') as typeof import('./compose_ts') + ).createSpinner(); this.streams = streams; } - _handleEvent(service, event) { + _handleEvent( + service: string, + event: { status?: string; progress?: number; error?: Error }, + ) { this._serviceToDataMap[service] = event; } @@ -344,20 +371,19 @@ export class BuildProgressUI { this._services.forEach((service) => { this.streams[service].write({ status: 'Preparing...' }); }); - this._runloop = require('./compose_ts').createRunLoop(this._display); + this._runloop = ( + require('./compose_ts') as typeof import('./compose_ts') + ).createRunLoop(this._display); this._startTime = Date.now(); } - /** - * @param {Dictionary | undefined} summary - */ - end(summary) { + end(summary?: Dictionary) { if (this._ended) { return; } this._ended = true; this._runloop?.end(); - this._runloop = null; + this._runloop = undefined; this._clear(); this._renderStatus(true); @@ -378,7 +404,7 @@ export class BuildProgressUI { } _getServiceSummary() { - const _ = require('lodash'); + const _ = require('lodash') as typeof import('lodash'); const services = this._services; const serviceToDataMap = this._serviceToDataMap; @@ -405,11 +431,11 @@ export class BuildProgressUI { .value(); } - _renderStatus(end) { - end ??= false; - - const moment = require('moment'); - require('moment-duration-format')(moment); + _renderStatus(end = false) { + const moment = require('moment') as typeof import('moment'); + ( + require('moment-duration-format') as typeof import('moment-duration-format') + )(moment); this._tty.clearLine(); this._tty.write(this._prefix); @@ -434,11 +460,11 @@ export class BuildProgressUI { } } - _renderSummary(serviceToStrMap) { - const _ = require('lodash'); + _renderSummary(serviceToStrMap: Dictionary) { + const _ = require('lodash') as typeof import('lodash'); const chalk = getChalk(); - const truncate = require('cli-truncate'); - const strlen = require('string-width'); + const truncate = require('cli-truncate') as typeof import('cli-truncate'); + const strlen = require('string-width') as typeof import('string-width'); this._services.forEach((service, index) => { let str = _.padEnd(this._prefix + chalk.bold(service), this._prefixWidth); @@ -454,13 +480,23 @@ export class BuildProgressUI { } } -export class BuildProgressInline { - constructor(outStream, descriptors) { +export class BuildProgressInline implements Renderer { + public streams; + private _prefixWidth; + private _outStream; + private _services; + private _startTime: number | undefined; + private _ended; + + constructor( + outStream: NodeJS.ReadWriteStream, + descriptors: Array<{ serviceName: string }>, + ) { this.start = this.start.bind(this); this.end = this.end.bind(this); this._renderEvent = this._renderEvent.bind(this); - const _ = require('lodash'); - const through = require('through2'); + const _ = require('lodash') as typeof import('lodash'); + const through = require('through2') as typeof import('through2'); const services = _.map(descriptors, 'serviceName'); const eventHandler = this._renderEvent; @@ -477,10 +513,9 @@ export class BuildProgressInline { .value(); const offset = 10; // account for escape sequences inserted for colouring - this._prefixWidth = offset + _.max(_.map(services, 'length')); + this._prefixWidth = offset + _.max(_.map(services, (s) => s.length))!; this._outStream = outStream; this._services = services; - this._startTime = null; this._ended = false; this.streams = streams; @@ -494,12 +529,11 @@ export class BuildProgressInline { this._startTime = Date.now(); } - /** - * @param {Dictionary | undefined} summary - */ - end(summary) { - const moment = require('moment'); - require('moment-duration-format')(moment); + end(summary?: Dictionary) { + const moment = require('moment') as typeof import('moment'); + ( + require('moment-duration-format') as typeof import('moment-duration-format') + )(moment); if (this._ended) { return; @@ -527,8 +561,8 @@ export class BuildProgressInline { this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`); } - _renderEvent(service, event) { - const _ = require('lodash'); + _renderEvent(service: string, event: { status?: string; error?: Error }) { + const _ = require('lodash') as typeof import('lodash'); const str = (function () { const { status, error } = event; diff --git a/lib/utils/compose_ts.ts b/lib/utils/compose_ts.ts index 3d374afc..fda9bec9 100644 --- a/lib/utils/compose_ts.ts +++ b/lib/utils/compose_ts.ts @@ -235,7 +235,7 @@ interface BuildTaskPlus extends MultiBuild.BuildTask { logBuffer?: string[]; } -interface Renderer { +export interface Renderer { start: () => void; end: (buildSummaryByService?: Dictionary) => void; streams: Dictionary;