Skip to content

Commit

Permalink
feat: add json output option
Browse files Browse the repository at this point in the history
  • Loading branch information
boneskull committed Aug 5, 2022
1 parent cabc49f commit 0b5b9e3
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 110 deletions.
214 changes: 127 additions & 87 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ async function main(args) {
hidden: true,
group: BEHAVIOR_GROUP,
},
json: {
type: 'boolean',
describe: 'Output JSON only',
group: BEHAVIOR_GROUP,
},
})
.check((argv) => {
if (
Expand All @@ -108,97 +113,123 @@ async function main(args) {
...(argv.scripts ?? []),
];

// squelch some output if `json` is true
argv.verbose = argv.json ? false : argv.verbose;

const smoker = new Smoker(scripts, argv);

const spinner = ora();
if (argv.json) {
/** @type {SmokerJsonOutput} */
let output;
/** @param {SmokerJsonOutput} result */
const setResult = (result) => {
smoker
.removeAllListeners(events.RUN_SCRIPTS_OK)
.removeAllListeners(events.RUN_SCRIPTS_FAILED);
output = result;
};
const writeResult = () => {
smoker
.removeAllListeners(events.SMOKE_OK)
.removeAllListeners(events.SMOKE_FAILED);
console.log(JSON.stringify(output, null, 2));
};
smoker
.once(events.RUN_SCRIPTS_OK, setResult)
.once(events.RUN_SCRIPTS_FAILED, setResult)
.once(events.SMOKE_FAILED, writeResult)
.once(events.SMOKE_OK, writeResult);
await smoker.smoke();
} else {
const spinner = ora();

smoker
.on(events.SMOKE_BEGIN, () => {
console.error(
`💨 ${blue('midnight-smoker')} ${white(`v${version}`)}`
);
})
.on(events.FIND_NPM_BEGIN, () => {
spinner.start('Looking for npm...');
})
.on(events.FIND_NPM_OK, (path) => {
spinner.succeed(`Found npm at ${path}`);
})
.on(events.FIND_NPM_FAILED, (err) => {
spinner.fail(`Could not find npm: ${err.message}`);
process.exitCode = 1;
})
.on(events.PACK_BEGIN, () => {
/** @type {string} */
let what;
if (argv.workspace?.length) {
what = pluralize('workspace', argv.workspace.length, true);
} else if (argv.all) {
what = 'all workspaces';
if (argv.includeRoot) {
what += ' (and the workspace root)';
smoker
.on(events.SMOKE_BEGIN, () => {
console.error(
`💨 ${blue('midnight-smoker')} ${white(`v${version}`)}`
);
})
.on(events.FIND_NPM_BEGIN, () => {
spinner.start('Looking for npm...');
})
.on(events.FIND_NPM_OK, (path) => {
spinner.succeed(`Found npm at ${path}`);
})
.on(events.FIND_NPM_FAILED, (err) => {
spinner.fail(`Could not find npm: ${err.message}`);
process.exitCode = 1;
})
.on(events.PACK_BEGIN, () => {
/** @type {string} */
let what;
if (argv.workspace?.length) {
what = pluralize('workspace', argv.workspace.length, true);
} else if (argv.all) {
what = 'all workspaces';
if (argv.includeRoot) {
what += ' (and the workspace root)';
}
} else {
what = 'current project';
}
} else {
what = 'current project';
}
spinner.start(`Packing ${what}...`);
})
.on(events.PACK_OK, (packItems) => {
spinner.succeed(
`Packed ${pluralize('package', packItems.length, true)}`
);
})
.on(events.PACK_FAILED, (err) => {
spinner.fail(err.message);
process.exitCode = 1;
})
.on(events.INSTALL_BEGIN, (packItems) => {
spinner.start(
`Installing from ${pluralize(
'tarball',
packItems.length,
true
)}...`
);
})
.on(events.INSTALL_FAILED, (err) => {
spinner.fail(err.message);
process.exitCode = 1;
})
.on(events.INSTALL_OK, (packItems) => {
spinner.succeed(
`Installed ${pluralize('package', packItems.length, true)}`
);
})
.on(events.RUN_SCRIPTS_BEGIN, ({total}) => {
spinner.start(`Running script 0/${total}...`);
})
.on(events.RUN_SCRIPT_BEGIN, ({current, total}) => {
spinner.text = `Running script ${current}/${total}...`;
})
.on(events.RUN_SCRIPT_FAILED, () => {
process.exitCode = 1;
})
.on(events.RUN_SCRIPTS_OK, ({total}) => {
spinner.succeed(
`Successfully ran ${pluralize('script', total, true)}`
);
})
.on(events.RUN_SCRIPTS_FAILED, ({total, executed, failures}) => {
spinner.fail(
`${failures} of ${total} ${pluralize('script', total)} failed`
);
process.exitCode = 1;
})
.on(events.SMOKE_FAILED, (err) => {
spinner.fail(err.message);
process.exitCode = 1;
})
.on(events.SMOKE_OK, () => {
spinner.succeed('Lovey-dovey! 💖');
});

await smoker.smoke();
spinner.start(`Packing ${what}...`);
})
.on(events.PACK_OK, (packItems) => {
spinner.succeed(
`Packed ${pluralize('package', packItems.length, true)}`
);
})
.on(events.PACK_FAILED, (err) => {
spinner.fail(err.message);
process.exitCode = 1;
})
.on(events.INSTALL_BEGIN, (packItems) => {
spinner.start(
`Installing from ${pluralize(
'tarball',
packItems.length,
true
)}...`
);
})
.on(events.INSTALL_FAILED, (err) => {
spinner.fail(err.message);
process.exitCode = 1;
})
.on(events.INSTALL_OK, (packItems) => {
spinner.succeed(
`Installed ${pluralize('package', packItems.length, true)}`
);
})
.on(events.RUN_SCRIPTS_BEGIN, ({total}) => {
spinner.start(`Running script 0/${total}...`);
})
.on(events.RUN_SCRIPT_BEGIN, ({current, total}) => {
spinner.text = `Running script ${current}/${total}...`;
})
.on(events.RUN_SCRIPT_FAILED, () => {
process.exitCode = 1;
})
.on(events.RUN_SCRIPTS_OK, ({total}) => {
spinner.succeed(
`Successfully ran ${pluralize('script', total, true)}`
);
})
.on(events.RUN_SCRIPTS_FAILED, ({total, executed, failures}) => {
spinner.fail(
`${failures} of ${total} ${pluralize('script', total)} failed`
);
process.exitCode = 1;
})
.on(events.SMOKE_FAILED, (err) => {
spinner.fail(err.message);
process.exitCode = 1;
})
.on(events.SMOKE_OK, () => {
spinner.succeed('Lovey-dovey! 💖');
});
await smoker.smoke();
}
}
)
.help()
Expand All @@ -207,3 +238,12 @@ async function main(args) {
}

main(process.argv.slice(2));

/**
* Output of the CLI script when `json` flag is `true`
* @typedef {Events['RunScriptsFailed']|Events['RunScriptsOk']} SmokerJsonOutput
*/

/**
* @typedef {import('./static').Events} Events
*/
28 changes: 17 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,19 @@ class Smoker extends createStrictEventEmitterClass() {
*/
scripts;

/**
* @type {Readonly<SmokerOptions>}
*/
opts;

/** @type {string|undefined} */
#npmPath;

/** @type {boolean} */
#force = false;

/**
* @type {Readonly<SmokerOptions>}
*/
opts;
/** @type {boolean} */
#linger = false;

/** @type {string|undefined} */
#cwd;
Expand Down Expand Up @@ -143,6 +146,7 @@ class Smoker extends createStrictEventEmitterClass() {
this.scripts = scripts.map((s) => s.trim());
opts = {...opts};

this.#linger = Boolean(opts.linger);
this.#force = Boolean(opts.force);
this.#clean = Boolean(opts.clean);
this.#verbose = Boolean(opts.verbose);
Expand Down Expand Up @@ -214,13 +218,14 @@ class Smoker extends createStrictEventEmitterClass() {
* @returns {Promise<void>}
*/
async #cleanWorkingDirectory(wd) {
// TODO EMIT
try {
await fs.rm(wd, {recursive: true});
} catch (e) {
const err = /** @type {NodeJS.ErrnoException} */ (e);
if (err.code !== 'ENOENT') {
throw new Error(`Failed to clean working directory ${wd}: ${e}`);
if (!this.#linger) {
try {
await fs.rm(wd, {recursive: true});
} catch (e) {
const err = /** @type {NodeJS.ErrnoException} */ (e);
if (err.code !== 'ENOENT') {
throw new Error(`Failed to clean working directory ${wd}: ${e}`);
}
}
}
}
Expand Down Expand Up @@ -580,6 +585,7 @@ class Smoker extends createStrictEventEmitterClass() {
);
if (failures) {
this.emit(RUN_SCRIPTS_FAILED, {
scripts,
total,
executed: results.length,
failures,
Expand Down
8 changes: 7 additions & 1 deletion src/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ export interface SmokerOptions {
* If `true`, halt at first failure
*/
bail?: boolean;

/**
* If `true`, output JSON instead of human-readable text
*/
json?: boolean;
}

export interface RunScriptResult extends ExecaReturnValue<string> {
Expand Down Expand Up @@ -126,7 +131,8 @@ export interface Events {
total: number;
executed: number;
failures: number;
results: ExecaReturnValue<string | ExecaError>[];
results: Array<ExecaReturnValue<string>|ExecaError<string>>;
scripts: string[];
};
RunScriptsOk: {
total: number;
Expand Down
39 changes: 28 additions & 11 deletions test/cli.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,44 @@ describe('midnight-smoker CLI', function () {
const actual = await run(['test:smoke', '--help']);
expect(actual, 'to equal snapshot', {
stdout:
"smoker <script> [scripts..]\n\nRun tests against a package as it would be published\n\nPositionals:\n script [string]\n scripts [string]\n\nBehavior:\n --workspace One or more npm workspaces to test [array]\n --all Test all workspaces [boolean]\n --include-root Include the workspace root; must provide '--all' [boolean]\n --install-args Extra arguments to pass to `npm install` [array]\n --dir Working directory to use [string] [default: new temp dir]\n --force Overwrite working directory if it exists [boolean]\n --clean Truncate working directory; must provide '--force' [boolean]\n --npm Path to `npm` executable [string] [default: `npm` in PATH]\n --verbose Print output from npm [boolean]\n --bail When running scripts, halt on first error [boolean]\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]",
"smoker <script> [scripts..]\n\nRun tests against a package as it would be published\n\nPositionals:\n script [string]\n scripts [string]\n\nBehavior:\n --workspace One or more npm workspaces to test [array]\n --all Test all workspaces [boolean]\n --include-root Include the workspace root; must provide '--all' [boolean]\n --install-args Extra arguments to pass to `npm install` [array]\n --dir Working directory to use [string] [default: new temp dir]\n --force Overwrite working directory if it exists [boolean]\n --clean Truncate working directory; must provide '--force' [boolean]\n --npm Path to `npm` executable [string] [default: `npm` in PATH]\n --verbose Print output from npm [boolean]\n --bail When running scripts, halt on first error [boolean]\n --json Output JSON only [boolean]\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]",
stderr: '',
exitCode: 0,
});
});

it('should smoke test this package', async function () {
it('should smoke test this and produce JSON output', async function () {
this.timeout('5s');

const {stdout, stderr, exitCode} = await execa(CLI_PATH, ['test:smoke'], {
cwd: CWD,
env: {
DEBUG: '',
},
});
const {stdout, stderr, exitCode} = await execa(
CLI_PATH,
['test:smoke', '--json'],
{
cwd: CWD,
env: {
DEBUG: '',
},
}
);
const actual = {stdout, stderr, exitCode};
actual.stdout = actual.stdout
// strip the path to npm from the `command` & `escapedCommand` since it could differ depending where this is run
.replace(
/(?<="(escaped)?[cC]ommand":\s*?")(.+?)(?=\/bin\/npm)/g,
'<path/to>'
)
// strip the versions since it could change
.replace(/midnight-smoker@\d+\.\d+\.\d+/, 'midnight-smoker@<version>')
.replace(/--version\\n\\n\d+\.\d+\.\d+/, '--version\\n\\n<version>')
// strip the path to `cli.js` since it differs per platform
.replace(/node(\.cmd)?\s+.+?cli\.js/, '<path/to/>cli.js');

expect(actual, 'to equal snapshot', {
stdout: '',
stderr:
'💨 midnight-smoker v1.0.1\n- Looking for npm...\n✔ Found npm at /Users/boneskull/.nvm/versions/node/v18.4.0/bin/npm\n- Packing current project...\n✔ Packed 1 package\n- Installing from 1 tarball...\n✔ Installed 1 package\n- Running script 0/1...\n✔ Successfully ran 1 script\n✔ Lovey-dovey! 💖',
stdout:
'{\n "scripts": [\n "test:smoke"\n ],\n "total": 1,\n "executed": 1,\n "failures": 0,\n "results": [\n {\n "pkgName": "midnight-smoker",\n "script": "test:smoke",\n "command": "<path/to>/bin/npm run-script test:smoke",\n "escapedCommand": "<path/to>/bin/npm\\" run-script \\"test:smoke\\"",\n "exitCode": 0,\n "stdout": "\\n> midnight-smoker@<version> test:smoke\\n> <path/to/>cli.js --version\\n\\n<version>",\n "stderr": "",\n "failed": false,\n "timedOut": false,\n "isCanceled": false,\n "killed": false\n }\n ]\n}',
stderr: '',
exitCode: 0,
});
expect(() => JSON.parse(actual.stdout), 'not to throw');
});
});

0 comments on commit 0b5b9e3

Please sign in to comment.