Skip to content

Commit 243a197

Browse files
committed
feat(cli): init cli package and add init command
1 parent b36c62f commit 243a197

16 files changed

+421
-6
lines changed

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
"dependencies": {
1010
"@koa/cors": "^3.3.0",
1111
"class-validator": "^0.13.2",
12+
"commander": "^9.4.0",
1213
"dayjs": "^1.11.2",
1314
"glob": "^8.0.1",
15+
"inquirer": "^8.0.0",
1416
"inversify": "^6.0.1",
1517
"joi": "^17.6.0",
1618
"js-yaml": "^4.1.0",
@@ -22,6 +24,7 @@
2224
"lodash": "^4.17.21",
2325
"nunjucks": "^3.2.3",
2426
"openapi3-ts": "^2.0.2",
27+
"ora": "^5.4.1",
2528
"reflect-metadata": "^0.1.13",
2629
"tslib": "^2.3.0",
2730
"tslog": "^3.3.3",
@@ -37,6 +40,7 @@
3740
"@nrwl/workspace": "14.0.3",
3841
"@types/from2": "^2.3.1",
3942
"@types/glob": "^7.2.0",
43+
"@types/inquirer": "^8.0.0",
4044
"@types/jest": "27.4.1",
4145
"@types/js-yaml": "^4.0.5",
4246
"@types/koa": "^2.13.4",

packages/cli/.eslintrc.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": ["../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7+
"rules": {}
8+
},
9+
{
10+
"files": ["*.ts", "*.tsx"],
11+
"rules": {}
12+
},
13+
{
14+
"files": ["*.js", "*.jsx"],
15+
"rules": {}
16+
}
17+
]
18+
}

packages/cli/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# cli
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
## Building
6+
7+
Run `nx build cli` to build the library.
8+
9+
## Running unit tests
10+
11+
Run `nx test cli` to execute the unit tests via [Jest](https://jestjs.io).
12+

packages/cli/jest.config.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
displayName: 'cli',
3+
preset: '../../jest.preset.ts',
4+
globals: {
5+
'ts-jest': {
6+
tsconfig: '<rootDir>/tsconfig.spec.json',
7+
},
8+
},
9+
transform: {
10+
'^.+\\.[tj]s$': 'ts-jest',
11+
},
12+
moduleFileExtensions: ['ts', 'js', 'html'],
13+
coverageDirectory: '../../coverage/packages/cli',
14+
};

packages/cli/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "@vulcan-sql/cli",
3+
"version": "0.1.0",
4+
"type": "commonjs"
5+
}

packages/cli/project.json

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"root": "packages/cli",
3+
"sourceRoot": "packages/cli/src",
4+
"targets": {
5+
"build": {
6+
"executor": "@nrwl/js:tsc",
7+
"outputs": ["{options.outputPath}"],
8+
"options": {
9+
"outputPath": "dist/packages/cli",
10+
"main": "packages/cli/src/index.ts",
11+
"tsConfig": "packages/cli/tsconfig.lib.json",
12+
"assets": ["packages/cli/*.md"]
13+
}
14+
},
15+
"publish": {
16+
"executor": "@nrwl/workspace:run-commands",
17+
"options": {
18+
"command": "node tools/scripts/publish.mjs cli {args.ver} {args.tag}",
19+
"cwd": "dist/packages/cli"
20+
},
21+
"dependsOn": [
22+
{
23+
"projects": "self",
24+
"target": "build"
25+
}
26+
]
27+
},
28+
"lint": {
29+
"executor": "@nrwl/linter:eslint",
30+
"outputs": ["{options.outputFile}"],
31+
"options": {
32+
"lintFilePatterns": ["packages/cli/**/*.ts"]
33+
}
34+
},
35+
"test": {
36+
"executor": "@nrwl/jest:jest",
37+
"outputs": ["coverage/packages/cli"],
38+
"options": {
39+
"jestConfig": "packages/cli/jest.config.ts",
40+
"passWithNoTests": true
41+
}
42+
},
43+
"run": {
44+
"executor": "@nrwl/workspace:run-commands",
45+
"options": {
46+
"command": "node src/index.js {args.cmd}",
47+
"cwd": "dist/packages/cli"
48+
},
49+
"dependsOn": [
50+
{
51+
"projects": "self",
52+
"target": "build"
53+
}
54+
]
55+
}
56+
},
57+
"tags": []
58+
}

packages/cli/src/commands/base.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Logger } from 'tslog';
2+
3+
export abstract class Command<O = any> {
4+
constructor(protected logger: Logger) {}
5+
6+
abstract handle(options: O): Promise<void>;
7+
}

packages/cli/src/commands/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './init';
2+
export * from './base';

packages/cli/src/commands/init.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Command } from './base';
2+
import * as inquirer from 'inquirer';
3+
import { promises as fs } from 'fs';
4+
import * as path from 'path';
5+
import { version } from '../../package.json';
6+
import * as ora from 'ora';
7+
import { exec } from 'child_process';
8+
9+
const validators: Record<
10+
string,
11+
{
12+
regex: RegExp;
13+
errorMessage: string;
14+
}
15+
> = {
16+
projectName: {
17+
regex: /^[a-zA-Z0-9_-]+$/,
18+
errorMessage: `Project name should contain only letters, numbers, or dashes.`,
19+
},
20+
};
21+
22+
const validateAnswer = (name: string) => (input: string) => {
23+
const validator = validators[name];
24+
if (!validator.regex.test(input)) {
25+
throw new Error(validator.errorMessage);
26+
}
27+
return true;
28+
};
29+
30+
interface InitCommandOptions {
31+
projectName: string;
32+
version: string;
33+
}
34+
35+
export class InitCommand extends Command {
36+
public async handle(options: Partial<InitCommandOptions>): Promise<void> {
37+
const question = [];
38+
39+
if (!options.projectName) {
40+
question.push({
41+
type: 'input',
42+
name: 'projectName',
43+
message: 'Project name:',
44+
default: 'my-first-vulcan-project',
45+
validate: validateAnswer('projectName'),
46+
});
47+
} else {
48+
validateAnswer('projectName')(options.projectName);
49+
}
50+
51+
options = {
52+
...{ version },
53+
...options,
54+
...(await inquirer.prompt(question)),
55+
};
56+
57+
await this.createProject(options as InitCommandOptions);
58+
}
59+
60+
public async createProject(options: InitCommandOptions): Promise<void> {
61+
const projectPath = path.resolve(process.cwd(), options.projectName);
62+
await fs.mkdir(projectPath);
63+
const existedFiles = await fs.readdir(projectPath);
64+
if (existedFiles.length > 0)
65+
throw new Error(`Path ${projectPath} is not empty`);
66+
67+
const installSpinner = ora('Creating project...').start();
68+
try {
69+
await fs.writeFile(
70+
path.resolve(projectPath, 'package.json'),
71+
JSON.stringify(
72+
{
73+
name: options.projectName,
74+
dependencies: {
75+
'@vulcan-sql/core': options.version,
76+
},
77+
},
78+
null,
79+
2
80+
),
81+
'utf-8'
82+
);
83+
installSpinner.succeed('Project has been created.');
84+
installSpinner.start('Installing dependencies...');
85+
await this.execAndWait(`yarn --silent`, projectPath);
86+
installSpinner.succeed(`Dependencies have been installed.`);
87+
} catch (e) {
88+
installSpinner.fail();
89+
throw e;
90+
} finally {
91+
installSpinner.stop();
92+
}
93+
}
94+
95+
private async execAndWait(command: string, cwd: string) {
96+
return new Promise<void>((resolve, reject) => {
97+
exec(command, { cwd }, (error, _, stderr) => {
98+
if (error) {
99+
reject(stderr);
100+
}
101+
resolve();
102+
});
103+
});
104+
}
105+
}

packages/cli/src/index.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { program } from 'commander';
2+
import { InitCommand } from './commands';
3+
import { Logger } from 'tslog';
4+
5+
// We don't use createLogger helper from core package because CLI will be installed before all packages.
6+
const logger = new Logger({
7+
name: 'CLI',
8+
minLevel: 'info',
9+
exposeErrorCodeFrame: false,
10+
displayFilePath: 'hidden',
11+
displayFunctionName: false,
12+
});
13+
14+
const initCommand = new InitCommand(logger);
15+
16+
program.exitOverride();
17+
18+
program
19+
.command('init')
20+
.option('-p --project-name <project-name>')
21+
.action(async (options) => {
22+
await initCommand.handle(options);
23+
});
24+
25+
(async () => {
26+
try {
27+
await program.parseAsync();
28+
} catch (e: any) {
29+
logger.prettyError(e, true, false, false);
30+
process.exit(1);
31+
}
32+
})();

packages/cli/tsconfig.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"module": "commonjs",
5+
"forceConsistentCasingInFileNames": true,
6+
"strict": true,
7+
"noImplicitOverride": true,
8+
"noPropertyAccessFromIndexSignature": true,
9+
"noImplicitReturns": true,
10+
"noFallthroughCasesInSwitch": true,
11+
"resolveJsonModule": true
12+
},
13+
"files": [],
14+
"include": [],
15+
"references": [
16+
{
17+
"path": "./tsconfig.lib.json"
18+
},
19+
{
20+
"path": "./tsconfig.spec.json"
21+
}
22+
]
23+
}

packages/cli/tsconfig.lib.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../dist/out-tsc",
5+
"declaration": true,
6+
"types": ["node"]
7+
},
8+
"include": ["**/*.ts"],
9+
"exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"]
10+
}

packages/cli/tsconfig.spec.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../dist/out-tsc",
5+
"module": "commonjs",
6+
"types": ["jest", "node"]
7+
},
8+
"include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9+
}

tsconfig.base.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@vulcan-sql/build/schema-parser/*": [
3333
"packages/build/src/lib/schema-parser/*"
3434
],
35+
"@vulcan-sql/cli": ["packages/cli/src/index.ts"],
3536
"@vulcan-sql/core": ["packages/core/src/index"],
3637
"@vulcan-sql/core/artifact-builder": [
3738
"packages/core/src/lib/artifact-builder/index"

workspace.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"version": 2,
33
"projects": {
44
"build": "packages/build",
5+
"cli": "packages/cli",
56
"core": "packages/core",
67
"extension-dbt": "packages/extension-dbt",
78
"integration-testing": "packages/integration-testing",

0 commit comments

Comments
 (0)