Skip to content

Commit

Permalink
feat: add base CLI tool and parsing options
Browse files Browse the repository at this point in the history
  • Loading branch information
favna committed Jun 30, 2021
1 parent 6f579f0 commit 785ac0c
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env node

import { cliRootDir, indent, packageCwd } from '#lib/constants';
import { logVerboseError, logVerboseInfo } from '#lib/logVerbose';
import { parseOptionsFile } from '#lib/optionsParser';
import { existsAsync } from '#lib/promisified';
import { cyan } from 'colorette';
import { Command } from 'commander';
import { readFile } from 'fs/promises';
import { join } from 'path';
import { URL } from 'url';

const packageFile = new URL('package.json', cliRootDir);
const packageJson = JSON.parse(await readFile(packageFile, 'utf-8'));

const command = new Command()
.version(packageJson.version)
.argument('<dist-directory>')
.usage(`${cyan('<dist-directory>')}`)
.option('-v, --verbose', 'Print verbose information', false)
.option(
'-e, --external [external...]',
`Repeatable, each will be treated as a new entry. Library or libraries to treat as external in Rollup (see: ${cyan(
'https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency'
)})`,
(value: string, previous: string[]) => previous.concat([value]),
[]
);

const program = command.parse(process.argv);
const options = await parseOptionsFile(program.opts());

const packageJsonPath = join(packageCwd, 'package.json');
const packageJsonExistsInCwd = await existsAsync(packageJsonPath);

if (!packageJsonExistsInCwd) {
logVerboseError({
text: ['Could not find a file named "package.json" in the current working directory. Are you sure this is a NodeJS repository?'],
verbose: options.verbose,
verboseText: ['I detected this current working directory: ', process.cwd()],
exitAfterLog: true
});
}

logVerboseInfo([
'Resolved options: ',
`${indent}verbose: ${JSON.stringify(options.verbose)}`,
`${indent}external: ${JSON.stringify(options.external)}`
]);
23 changes: 23 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { join } from 'path';
import { URL } from 'url';

/** The root directory of the CLI tool */
export const cliRootDir = new URL('../../', import.meta.url);

/** Current working directory from which the script is called */
export const packageCwd = process.cwd();

/** Path to the config file in proprietary format */
export const rollupTypeBundlerRcPath = join(packageCwd, '.rollup-type-bundlerrc');

/** Path to the config file in .json format */
export const rollupTypeBundlerRcJsonPath = join(rollupTypeBundlerRcPath, '.json');

/** Path to the config file in .yml format */
export const rollupTypeBundlerRcYmlPath = join(rollupTypeBundlerRcPath, '.yml');

/** Path to the config file in .yaml format */
export const rollupTypeBundlerRcYamlPath = join(rollupTypeBundlerRcPath, '.yaml');

/** 4 spaces indent for logging */
export const indent = ' '.repeat(4);
4 changes: 4 additions & 0 deletions src/lib/interfaces.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Options {
verbose?: boolean;
external?: string[];
}
28 changes: 28 additions & 0 deletions src/lib/logVerbose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cyan, red } from 'colorette';

export function logVerboseError({ text, verbose = false, verboseText = [], exitAfterLog: exitOnLog = false }: LogVerboseErrorOptions) {
let combinedText = text;

if (verbose) {
combinedText = combinedText.concat(verboseText);
}

console.error(red(combinedText.join('\n')));

if (exitOnLog) {
process.exit(1);
}
}

export function logVerboseInfo(text: string[], verbose = false) {
if (verbose) {
console.info(cyan(text.join('\n')));
}
}

interface LogVerboseErrorOptions {
text: string[];
verbose?: boolean;
verboseText?: string[];
exitAfterLog?: boolean;
}
61 changes: 61 additions & 0 deletions src/lib/optionsParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { rollupTypeBundlerRcJsonPath, rollupTypeBundlerRcPath, rollupTypeBundlerRcYamlPath, rollupTypeBundlerRcYmlPath } from '#lib/constants';
import type { Options } from '#lib/interfaces';
import { logVerboseError } from '#lib/logVerbose';
import { existsAsync } from '#lib/promisified';
import { load } from 'js-yaml';
import type { PathLike } from 'fs';

import { readFile } from 'fs/promises';

export async function parseOptionsFile(cliOptions: Options) {
const rollupTypeBundlerRcExists = await existsAsync(rollupTypeBundlerRcPath);
const rollupTypeBundlerRcJsonExists = await existsAsync(rollupTypeBundlerRcJsonPath);
const rollupTypeBundlerRcYmlExists = await existsAsync(rollupTypeBundlerRcYmlPath);
const rollupTypeBundlerRcYamlExists = await existsAsync(rollupTypeBundlerRcYamlPath);

let options = cliOptions;

if (rollupTypeBundlerRcYamlExists || rollupTypeBundlerRcYmlExists) {
try {
options = await readYaml(rollupTypeBundlerRcYamlExists ? rollupTypeBundlerRcYamlPath : rollupTypeBundlerRcYmlPath);
} catch (err) {
logVerboseError({
text: ['Failed to read yaml config file'],
verbose: options.verbose,
verboseText: [
'Attempted to read options file:',
rollupTypeBundlerRcYamlExists ? rollupTypeBundlerRcYamlPath : rollupTypeBundlerRcYmlPath,
'',
'Full error: ',
err
]
});
}
} else if (rollupTypeBundlerRcExists || rollupTypeBundlerRcJsonExists) {
try {
options = await readJson(rollupTypeBundlerRcExists ? rollupTypeBundlerRcPath : rollupTypeBundlerRcJsonPath);
} catch (err) {
logVerboseError({
text: ['Failed to read json config file'],
verbose: options.verbose,
verboseText: [
'Attempted to read options file:',
rollupTypeBundlerRcExists ? rollupTypeBundlerRcPath : rollupTypeBundlerRcJsonPath,
'',
'Full error: ',
err
]
});
}
}

return options;
}

async function readYaml<T>(pathLike: PathLike) {
return load(await readFile(pathLike, { encoding: 'utf-8' })) as unknown as T;
}

async function readJson<T>(pathLike: PathLike) {
return JSON.parse(await readFile(pathLike, { encoding: 'utf-8' })) as T;
}
10 changes: 10 additions & 0 deletions src/lib/promisified.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { exec } from 'child_process';
import type { PathLike } from 'fs';
import { lstat } from 'fs/promises';
import { promisify } from 'util';

export const execAsync = promisify(exec);
export const existsAsync = (path: PathLike) =>
lstat(path)
.then(() => true)
.catch(() => false);
15 changes: 15 additions & 0 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./",
"outDir": "../dist",
"composite": true,
"preserveConstEnums": true,
"baseUrl": ".",
"paths": {
"#lib/*": ["./lib/*"],
"#root/*": ["*"]
}
},
"include": ["."]
}

0 comments on commit 785ac0c

Please sign in to comment.