Skip to content

Commit

Permalink
refactor: Miscellaneous improvements.
Browse files Browse the repository at this point in the history
  • Loading branch information
darkobits committed Aug 14, 2022
1 parent 272a2c1 commit cd7014f
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 30 deletions.
5 changes: 3 additions & 2 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import os from 'os';

import cli from '@darkobits/saffron';

import { DEFAULT_UBUNTU_VERSION } from 'etc/constants';
import { DockerizeOptions } from 'etc/types';
import dockerize from 'lib/dockerize';
import log from 'lib/log';
Expand Down Expand Up @@ -44,7 +45,7 @@ cli.command<DockerizeArguments>({

command.option('ubuntu-version', {
group: 'Optional Arguments:',
description: 'Ubuntu version to use as a base image. [Default: 20.10]',
description: `Ubuntu version to use as a base image. [Default: ${DEFAULT_UBUNTU_VERSION}]`,
required: false,
type: 'string',
conflicts: ['dockerfile']
Expand All @@ -67,7 +68,7 @@ cli.command<DockerizeArguments>({

command.option('extra-args', {
group: 'Optional Arguments:',
description: 'Optional extra arguments to pass to "docker build". This is treated as a single string and should be quoted.',
description: 'Optional extra arguments to pass to "docker build".\nThis is treated as a single string and should be quoted.',
required: false,
type: 'string'
});
Expand Down
4 changes: 2 additions & 2 deletions src/etc/Dockerfile.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ RUN \
WORKDIR /home/app

# Copy manifests and build artifacts.
COPY package /home/app
COPY ./ /home/app

# Conditionally copy .npmrc.
<% if (hasNpmrc) { %>
COPY .npmrc .npmrc
<% } %>

# Install production dependencies.
RUN npm <%= hasLockfile ? 'ci' : 'install' %> --production --skip-optional --ignore-scripts
RUN npm <%= hasLockfile ? 'ci' : 'install' %> --omit dev --skip-optional --ignore-scripts

# Stage 2
FROM gcr.io/distroless/cc
Expand Down
2 changes: 2 additions & 0 deletions src/etc/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const DEFAULT_TINI_VERSION = '0.18.0';

export const DEFAULT_UBUNTU_VERSION = '22.04';

export const DOCKER_IMAGE_PATTERN = /^(?:([^/]+)\/)?(?:([^/]+)\/)?([^/:@]+)(?:[:@](.+))?$/;
2 changes: 1 addition & 1 deletion src/etc/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Gets the inner type from a type wrapped in Promise.
*/
export type ThenArg<T> = T extends Promise<infer U> ? U : T;
export type ThenArg<P> = P extends Promise<infer U> ? U : P;


/**
Expand Down
67 changes: 43 additions & 24 deletions src/lib/dockerize.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import path from 'path';

import chex from '@darkobits/chex';
import { dirname } from '@darkobits/fd-name';
import fs from 'fs-extra';
import emoji from 'node-emoji';
import ow from 'ow';
import tempy from 'tempy';

import { DEFAULT_TINI_VERSION } from 'etc/constants';
import {
DEFAULT_TINI_VERSION,
DEFAULT_UBUNTU_VERSION
} from 'etc/constants';
import { DockerizeOptions } from 'etc/types';
import log from 'lib/log';
import ow from 'lib/ow';
import {
computePackageEntry,
computeTag,
Expand All @@ -17,7 +21,7 @@ import {
ensureArray,
getImageSize,
getNodeLtsVersion,
packAndExtractPackage,
copyPackFiles,
parseLabels,
pkgInfo,
renderTemplate
Expand All @@ -28,7 +32,7 @@ export default async function dockerize(options: DockerizeOptions) {
const buildTime = log.createTimer();

// Ensure Docker and NPM are installed.
const [docker, npm] = await Promise.all([chex('docker'), chex('npm')]);
const docker = await chex('docker');


// ----- [1] Validate Options ------------------------------------------------
Expand Down Expand Up @@ -60,7 +64,7 @@ export default async function dockerize(options: DockerizeOptions) {
/**
* Ubuntu version to use as a base image.
*/
const ubuntuVersion = options.ubuntuVersion ?? '22.04';
const ubuntuVersion = options.ubuntuVersion ?? DEFAULT_UBUNTU_VERSION;

/**
* Tag that will be applied to the image.
Expand Down Expand Up @@ -131,7 +135,7 @@ export default async function dockerize(options: DockerizeOptions) {
// N.B. These two files are not included in `npm pack`, so we have to copy
// them explicitly.
copyNpmrc(options.npmrc, stagingDir),
copyPackageLockfile(pkg.root, path.join(stagingDir, 'package'))
copyPackageLockfile(pkg.root, stagingDir)
]);


Expand Down Expand Up @@ -173,8 +177,11 @@ export default async function dockerize(options: DockerizeOptions) {
// [7c] Otherwise, programmatically generate a Dockerfile and place it in the
// build context.
if (!finalDockerfileSourcePath) {
const ourDirectory = dirname();
if (!ourDirectory) throw new Error('Unable to compute path to the current directory.');

await renderTemplate({
template: path.join(__dirname, '..', 'etc', 'Dockerfile.ejs'),
template: path.join(ourDirectory, '..', 'etc', 'Dockerfile.ejs'),
dest: targetDockerfilePath,
data: {
entry,
Expand Down Expand Up @@ -208,49 +215,61 @@ export default async function dockerize(options: DockerizeOptions) {

log.info(`${emoji.get('whale')} Dockerizing package ${log.chalk.green(pkg.package.name)}.`);

log.verbose(`-> Package Root: ${log.chalk.green(pkg.root)}`);
log.verbose(`-> Staging Directory: ${log.chalk.green(stagingDir)}`);
log.verbose(`${log.chalk.gray.dim('├─')} Package Root: ${log.chalk.green(pkg.root)}`);
log.verbose(`${log.chalk.gray.dim('├─')} Staging Directory: ${log.chalk.green(stagingDir)}`);

if (extraArgs) {
log.verbose(`-> Extra Docker Args: ${extraArgs}`);
log.verbose(`${log.chalk.gray.dim('├─')}Extra Docker Args: ${extraArgs}`);
}

const dockerBuildCommand = `docker build ${options.cwd} ${dockerBuildArgs}`;
log.verbose(`-> Docker Command: ${log.chalk.dim(dockerBuildCommand)}`);
log.verbose(`${log.chalk.gray.dim('├─')} Docker Command: ${log.chalk.dim(dockerBuildCommand)}`);

if (finalDockerfileSourcePath) {
log.info(`-> Dockerfile: ${log.chalk.green(finalDockerfileSourcePath)}`);
log.info(`${log.chalk.gray.dim('├─')} Dockerfile: ${log.chalk.green(finalDockerfileSourcePath)}`);
}

log.info(`-> Entrypoint: ${log.chalk.green(entry)}`);
log.info(`-> Node Version: ${log.chalk.green(nodeVersion)}`);
log.info(`-> Lockfile: ${log.chalk[hasLockfile ? 'green' : 'yellow'](String(hasLockfile))}`);

if (envVars.length > 0) {
log.info('-> Environment Variables:');
log.info(`${log.chalk.gray.dim('├─')} ${log.chalk.gray('Environment Variables:')}`);

envVars.forEach(varExpression => {
envVars.forEach((varExpression, index) => {
const [key, value] = varExpression.split('=');
log.info(` - ${key}=${value}`);

if (index === envVars.length - 1) {
log.info(`${log.chalk.gray.dim('│ └─')} ${log.chalk.green(`${key}=${value}`)}`);
} else {
log.info(`${log.chalk.gray.dim('│ ├─')} ${log.chalk.green(`${key}=${value}`)}`);
}
});
}

if (options.labels) {
log.info('- Labels:');
log.info(`${log.chalk.gray.dim('├─')} ${log.chalk.gray('Labels:')}`);

ensureArray<string>(options.labels).forEach(labelExpression => {
const labelsArray = ensureArray<string>(options.labels);

labelsArray.forEach((labelExpression, index) => {
const [key, value] = labelExpression.split('=');
log.info(` - ${key}: ${value}`);

if (index === labelsArray.length - 1) {
log.info(`${log.chalk.gray.dim('│ └─')} ${log.chalk.green(`${key}=${value}`)}`);
} else {
log.info(`${log.chalk.gray.dim('│ ├─')} ${log.chalk.green(`${key}=${value}`)}`);
}
});
}

log.info(`${log.chalk.gray.dim('├─')} ${log.chalk.gray('Entrypoint:')} ${log.chalk.green(entry)}`);
log.info(`${log.chalk.gray.dim('├─')} ${log.chalk.gray('Node Version:')} ${log.chalk.green(nodeVersion)}`);
log.info(`${log.chalk.gray.dim('└─')} ${log.chalk.gray('Lockfile:')} ${log.chalk[hasLockfile ? 'green' : 'yellow'](String(hasLockfile))}`);


// ----- [10] Pack Package ---------------------------------------------------

log.info(`Building image ${log.chalk.cyan(tag)}...`);

// Copy production-relevant package files to the staging directory.
await packAndExtractPackage(npm, pkg.root, stagingDir);
await copyPackFiles(pkg.root, stagingDir);


// ----- [11] Build Image ----------------------------------------------------
Expand All @@ -268,7 +287,7 @@ export default async function dockerize(options: DockerizeOptions) {
}

if (buildProcess.stderr) {
buildProcess.stderr.pipe(log.createPipe('error'));
buildProcess.stderr.pipe(log.createPipe('silly'));
}

await buildProcess;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import LogFactory from '@darkobits/log';

// Disable logging by default so that we don't output anything when the Node
// API is used. The CLI will then set this appropriately.
const log = LogFactory({heading: 'dockerize', level: 'silent'});
const log = LogFactory({heading: 'dockerize', level: 'silent', stripIndent: false });

log.configure({ heading: log.chalk.cyan('dockerize') });

export default log;
31 changes: 31 additions & 0 deletions src/lib/ow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import _ow, { type Ow } from 'ow';


/**
* This is needed because while we transpile to ESM, we still transpile to CJS
* for testing in Jest, and certain modules will import different values based
* on these strategies, so we have to "find" the package's true default export
* at runtime in a way that works in both ESM and CJS.
*
* This is essentially a replacement for Babel's _interopRequireDefault helper
* which is not used / added to transpiled code when transpiling to ESM.
*
* TODO: Move to separate package.
*/
export function getDefaultExport<T extends object>(value: T) {
try {
let result = value;

while (Reflect.has(result, 'default')) {
result = Reflect.get(result, 'default');
}

return result;
} catch {
return value;
}
}

const ow: Ow = getDefaultExport(_ow);

export default ow;

0 comments on commit cd7014f

Please sign in to comment.