Headless CLI POC (#2258)

This commit is contained in:
Opender Singh 2020-06-12 11:44:12 +12:00 committed by GitHub
parent 208aec6ec8
commit f38bf20e13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 9302 additions and 0 deletions

View File

@ -11,6 +11,7 @@ screenshots/
**/webpack/
**/bin/
**/__fixtures__/
**/__snapshots__/
**/flow-typed/
**/dist/
**/.cache/

View File

@ -4,3 +4,4 @@
**/__fixtures__/*
**/flow-typed/*
*.md
**/__snapshots__/

View File

@ -14,6 +14,7 @@
"clean": "lerna clean --yes && rimraf node_modules",
"typecheck": "lerna run --parallel --stream typecheck",
"test": "npm run lint && npm run typecheck && lerna run --stream --parallel test",
"cli-start": "npm start --prefix packages/insomnia-cli",
"app-start": "npm start --prefix packages/insomnia-app",
"app-build": "npm run build --prefix packages/insomnia-app",
"app-package": "npm run package --prefix packages/insomnia-app",

View File

@ -0,0 +1,16 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "10"
}
}
],
"@babel/preset-flow"
],
"plugins": [
["@babel/plugin-proposal-optional-chaining"]
]
}

View File

@ -0,0 +1,14 @@
[ignore]
.*/node_modules/.*
./.cache
./dist
[include]
[libs]
./flow-typed
[options]
esproposal.optional_chaining=enable
[lints]

1
packages/insomnia-cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

View File

@ -0,0 +1,7 @@
# Ignore everything by default
# NOTE: NPM publish will never ignore package.json, package-lock.json, README, LICENSE, CHANGELOG
# https://npm.github.io/publishing-pkgs-docs/publishing/the-npmignore-file.html
*
# Don't ignore dist folder
!dist/*

View File

@ -0,0 +1,12 @@
# insomnia-cli
### Usage
```
npm install -g insomnia-cli
inso --version
```
### Development
- Bootstrap: `npm run bootstrap`
- Start the compiler in watch mode: `npm run watch`
- Run: `./bin/inso -v`

View File

@ -0,0 +1,7 @@
const mod = jest.requireActual('openapi-2-kong');
module.exports = {
...mod,
generate: jest.fn(),
generateFromString: jest.fn(),
};

3
packages/insomnia-cli/bin/inso Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../dist/cli').go();

View File

@ -0,0 +1,5 @@
// @flow
declare module 'commander' {
declare module.exports: *;
}

View File

@ -0,0 +1,5 @@
// @flow
declare module 'execa' {
declare module.exports: *;
}

View File

@ -0,0 +1,5 @@
// @flow
declare module 'get-bin-path' {
declare module.exports: *;
}

1186
packages/insomnia-cli/flow-typed/jest.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
// @flow
declare type ConversionResultType = 'kong-declarative-config' | 'kong-for-kubernetes';
declare type DeclarativeConfigResult = {
type: 'kong-declarative-config',
label: string,
documents: Object,
warnings: Array<{
severity: number,
message: string,
range: {
/* TODO */
},
}>,
};
declare type KongForKubernetesResult = {
type: 'kong-for-kubernetes',
label: string,
documents: Array<Object>,
warnings: Array<{
severity: number,
message: string,
range: {
/* TODO */
},
}>,
};
declare type ConversionResult = DeclarativeConfigResult | KongForKubernetesResult;
declare module 'openapi-2-kong' {
declare module.exports: {
generate: (specPath: string, type: ConversionResultType, tags?: Array<string>) => Promise<ConversionResult>,
generateFromString: (specStr: string, type: ConversionResultType, tags?: Array<string>) => Promise<ConversionResult>,
};
}

View File

@ -0,0 +1,9 @@
// @flow
declare module 'yaml' {
declare module.exports: {
stringify: Object => string,
parse: string => Object,
parseCST: string => Object,
};
}

7639
packages/insomnia-cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
{
"name": "insomnia-cli",
"version": "0.0.1",
"bin": {
"inso": "bin/inso"
},
"scripts": {
"typecheck": "",
"test": "jest",
"test:watch": "jest --watch",
"test:snapshots": "npm run build && jest -u",
"start": "npm run build:watch",
"build": "rimraf dist && babel src --out-dir dist --ignore \"src/**/*.test.js\",\"src/**/*.test.js.snap\"",
"build:watch": "npm run build -- --watch",
"bootstrap": "npm run build",
"prepublish": "npm run build"
},
"jest": {
"testMatch": [
"**/__tests__/**/*.test.js"
],
"verbose": false,
"resetMocks": true,
"resetModules": true
},
"devDependencies": {
"@babel/cli": "^7.10.1",
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/preset-env": "^7.10.2",
"@babel/preset-flow": "^7.10.1",
"execa": "^4.0.2",
"flow-bin": "^0.126.1",
"get-bin-path": "^5.1.0",
"husky": "^3.1.0",
"jest": "^26.0.1",
"rimraf": "^3.0.2"
},
"dependencies": {
"commander": "^5.1.0",
"openapi-2-kong": "^2.2.6",
"yaml": "^1.10.0"
}
}

View File

@ -0,0 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Snapshot for "inso --help" 1`] = `
"Usage: inso [options] [command]
A CLI for Insomnia!
Options:
-v, --version output the version number
-h, --help display help for command
Commands:
generate Code generation utilities
help [command] display help for command"
`;
exports[`Snapshot for "inso -h" 1`] = `
"Usage: inso [options] [command]
A CLI for Insomnia!
Options:
-v, --version output the version number
-h, --help display help for command
Commands:
generate Code generation utilities
help [command] display help for command"
`;
exports[`Snapshot for "inso generate -h" 1`] = `
"Usage: inso generate [options] [command]
Code generation utilities
Options:
-h, --help display help for command
Commands:
config [options] <filePath> Generate configuration from an api spec
help [command] display help for command"
`;
exports[`Snapshot for "inso generate config -h" 1`] = `
"Usage: inso generate config [options] <filePath>
Generate configuration from an api spec
Options:
-t, --type <value> the type of configuration to generate, options are
[kubernetes, declarative]
-o, --output <path> the output path
-h, --help display help for command"
`;
exports[`Snapshot for "inso help" 1`] = `
"Usage: inso [options] [command]
A CLI for Insomnia!
Options:
-v, --version output the version number
-h, --help display help for command
Commands:
generate Code generation utilities
help [command] display help for command"
`;

View File

@ -0,0 +1,54 @@
// @flow
import * as cli from '../cli';
import { generateConfig } from '../commands/generate';
jest.mock('../commands/generate');
const originalError = console.error;
const initInso = () => {
return (args: string): void => {
const cliArgs = `node test ${args}`
.split(' ')
.map(t => t.trim())
.filter(t => t);
// console.log('calling cli.go with: %o', cliArgs);
return cli.go(cliArgs, true);
};
};
describe('cli', () => {
let inso = initInso();
beforeEach(() => {
inso = initInso();
(console: any).error = jest.fn();
});
afterEach(() => {
(console: any).error = originalError;
});
it('should error when required --type option is missing', () =>
expect(() => inso('generate config file.yaml')).toThrowError());
it('should error when filePath is missing', () =>
expect(() => inso('generate config -t declarative')).toThrowError());
it('should call generateConfig with undefined output argument', () => {
inso('generate config -t declarative file.yaml');
expect(generateConfig).toHaveBeenCalledWith({
filePath: 'file.yaml',
type: 'declarative',
output: undefined,
});
});
it('should call generateConfig with all expected arguments', () => {
inso('generate config -t kubernetes -o output.yaml file.yaml');
expect(generateConfig).toHaveBeenCalledWith({
filePath: 'file.yaml',
type: 'kubernetes',
output: 'output.yaml',
});
});
});

View File

@ -0,0 +1,29 @@
// @flow
import execa from 'execa';
import { getBinPathSync } from 'get-bin-path';
import * as packageJson from '../../package.json';
// MAKE SURE YOU BUILD THE PROJECT BEFORE RUNNING THESE TESTS.
// These tests use the executable /bin/inso, which relies on /dist.
describe('Snapshot for', () => {
it.each(['-h', '--help', 'help', 'generate -h', 'generate config -h'])(
'"inso %s"',
async args => {
const { stdout } = await execa(getBinPathSync(), args.split(' '));
expect(stdout).toMatchSnapshot();
},
30000,
);
});
describe('Inso version', () => {
it.each(['-v', '--version'])(
'inso %s should print version from package.json',
async args => {
const { stdout } = await execa(getBinPathSync(), args.split(' '));
expect(stdout).toBe(packageJson.version);
},
30000,
);
});

View File

@ -0,0 +1,36 @@
// @flow
import { ConversionTypeMap, generateConfig } from './commands/generate';
import { getVersion, createCommand } from './util';
function makeGenerateCommand(exitOverride: boolean) {
// inso generate
const generate = createCommand(exitOverride, 'generate').description('Code generation utilities');
const conversionTypes = Object.keys(ConversionTypeMap).join(', ');
// inso generate config -t kubernetes config.yaml
generate
.command('config <filePath>')
.description('Generate configuration from an api spec')
.requiredOption(
'-t, --type <value>',
`the type of configuration to generate, options are [${conversionTypes}]`,
)
.option('-o, --output <path>', 'the output path')
.action((filePath, opts) => generateConfig({ filePath, ...opts }));
return generate;
}
export function go(args?: Array<string>, exitOverride?: boolean): void {
if (!args) {
args = process.argv;
}
// inso -v
createCommand(!!exitOverride)
.version(getVersion(), '-v, --version')
.description('A CLI for Insomnia!')
.addCommand(makeGenerateCommand(!!exitOverride))
.parse(args);
}

View File

@ -0,0 +1,54 @@
// @flow
import { ConversionTypeMap, generateConfig } from '../generate';
import type { GenerateConfigOptions } from '../generate';
import o2k from 'openapi-2-kong';
import fs from 'fs';
import path from 'path';
jest.mock('openapi-2-kong');
jest.mock('fs');
const base: GenerateConfigOptions = {
filePath: 'file.yaml',
type: 'kubernetes',
output: undefined,
};
describe('generateConfig()', () => {
// make flow happy
const mock = (mockFn: any) => mockFn;
afterEach(() => {
jest.restoreAllMocks();
});
it('should should not generate if type arg is invalid', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
await generateConfig({ ...base, type: 'invalid' });
expect(o2k.generate).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
'Config type "invalid" not unrecognized. Options are [kubernetes, declarative].',
);
});
it('should print conversion documents to console', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
mock(o2k.generate).mockResolvedValue({ documents: ['a', 'b'] });
await generateConfig(base);
expect(o2k.generate).toHaveBeenCalledWith(base.filePath, ConversionTypeMap[base.type]);
expect(consoleSpy).toHaveBeenCalledWith('a\n---\nb\n');
});
it('should write converted documents to file system', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
mock(o2k.generate).mockResolvedValue({ documents: ['a', 'b'] });
await generateConfig({ ...base, output: 'output.yaml' });
expect(o2k.generate).toHaveBeenCalledWith(base.filePath, ConversionTypeMap[base.type]);
expect(fs.writeFileSync).toHaveBeenCalledWith(path.resolve('output.yaml'), 'a\n---\nb\n');
expect(consoleSpy).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,48 @@
// @flow
import o2k from 'openapi-2-kong';
import YAML from 'yaml';
import path from 'path';
import fs from 'fs';
export const ConversionTypeMap: { [string]: ConversionResultType } = {
kubernetes: 'kong-for-kubernetes',
declarative: 'kong-declarative-config',
};
export type GenerateConfigOptions = {|
filePath: string,
type: $Keys<typeof ConversionTypeMap>,
output?: string,
|};
function validateOptions({ type }: GenerateConfigOptions): boolean {
if (!ConversionTypeMap[type]) {
const conversionTypes = Object.keys(ConversionTypeMap).join(', ');
console.log(`Config type "${type}" not unrecognized. Options are [${conversionTypes}].`);
return false;
}
return true;
}
export async function generateConfig(options: GenerateConfigOptions): Promise<void> {
if (!validateOptions(options)) {
return;
}
const { type, output, filePath } = options;
const result = await o2k.generate(filePath, ConversionTypeMap[type]);
const yamlDocs = result.documents.map(d => YAML.stringify(d));
// Join the YAML docs with "---" and strip any extra newlines surrounding them
const document = yamlDocs.join('\n---\n').replace(/\n+---\n+/g, '\n---\n');
if (output) {
const fullOutputPath = path.resolve(output);
fs.writeFileSync(fullOutputPath, document);
} else {
console.log(document);
}
}

View File

@ -0,0 +1,19 @@
// @flow
import commander from 'commander';
import * as packageJson from '../package.json';
export function createCommand(exitOverride: boolean, cmd?: string) {
const command = new commander.Command(cmd)
.storeOptionsAsProperties(false)
.passCommandToAction(false);
if (exitOverride) {
return command.exitOverride();
}
return command;
}
export function getVersion() {
return packageJson.version;
}