From 9c7e36c87bfbe8bdd727a513508accc89576fd0f Mon Sep 17 00:00:00 2001 From: Maksim Sinik Date: Wed, 4 Mar 2020 15:31:24 +0100 Subject: [PATCH] feat: implement cli for generate couchdb's design documents --- .eslintrc.js | 1 + bin/ddoc.js | 82 ++++++++++++++++++++++++++++++++++++ package.json | 12 +++++- src/bin/ddoc.ts | 93 +++++++++++++++++++++++++++++++++++++++++ src/bin/tsconfig.json | 14 +++++++ src/db/designs/index.ts | 1 - src/db/tsconfig.json | 7 ++-- 7 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 bin/ddoc.js create mode 100644 src/bin/ddoc.ts create mode 100644 src/bin/tsconfig.json delete mode 100644 src/db/designs/index.ts diff --git a/.eslintrc.js b/.eslintrc.js index 16866a65..e2422a7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + ignorePatterns: ['bin'], extends: [ 'plugin:@typescript-eslint/recommended', 'prettier', diff --git a/bin/ddoc.js b/bin/ddoc.js new file mode 100644 index 00000000..64cf8250 --- /dev/null +++ b/bin/ddoc.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const util_1 = require("util"); +const sade_1 = __importDefault(require("sade")); +const typescript_1 = __importDefault(require("typescript")); +const require_from_string_1 = __importDefault(require("require-from-string")); +const glob_1 = __importDefault(require("glob")); +const mkdirp_1 = __importDefault(require("mkdirp")); +const stat = util_1.promisify(fs_1.default.stat); +const readFile = util_1.promisify(fs_1.default.readFile); +const writeFile = util_1.promisify(fs_1.default.writeFile); +const glob = util_1.promisify(glob_1.default); +const prog = sade_1.default('ddoc'); +prog.version('0.1.0'); +prog + .command('build ', 'Build design document(s) from TypeScript soruce directory or file.', { + default: true, +}) + .option('-c, --config', 'Provide path to custom tsconfig.json', './tsconfig.json') + .example('build src/db/designs') + .example('build src/db/designs/patient.ts -c src/db/tsconfig.json') + .action(async (src, opts) => { + var _a; + try { + const cwd = process.cwd(); + const tsconfigPath = path_1.default.isAbsolute(opts.config) ? opts.config : path_1.default.join(cwd, opts.config); + console.log(`> using ${tsconfigPath} config`); + const tsconfig = require(tsconfigPath); + src = path_1.default.isAbsolute(src) ? path_1.default.normalize(src) : path_1.default.join(cwd, src); + const srcStats = await stat(src); + let dest = ((_a = tsconfig === null || tsconfig === void 0 ? void 0 : tsconfig.compilerOptions) === null || _a === void 0 ? void 0 : _a.outDir) ? path_1.default.join(path_1.default.dirname(tsconfigPath), tsconfig.compilerOptions.outDir) + : ''; + let ddocs; + if (srcStats.isDirectory()) { + dest = dest || src; + ddocs = await glob(path_1.default.join(src, '**/*.ts')); + } + else { + dest = dest || path_1.default.dirname(src); + ddocs = [src]; + } + console.log(`> src directory is ${src}`); + await mkdirp_1.default(dest); + console.log(`> destination directory is ${dest}`); + const errors = []; + await Promise.all(ddocs.map(async (srcPath) => { + try { + const sourceFile = (await readFile(srcPath)).toString(); + const output = typescript_1.default.transpileModule(sourceFile, tsconfig); + const ddoc = require_from_string_1.default(output.outputText); + const filename = path_1.default.basename(srcPath, '.ts'); + const stringifiedDesign = JSON.stringify(ddoc, (_, val) => { + if (typeof val === 'function') { + return val.toString(); + } + return val; + }, 1); + await writeFile(path_1.default.join(dest, `${filename}.json`), stringifiedDesign); + } + catch (err) { + errors.push(err); + } + })); + if (errors.length > 0) { + errors.forEach(err => { + console.error(err); + }); + throw new Error(`Compilation failed. Resolve errors in your code and try again.`); + } + } + catch (err) { + console.error(err); + process.exit(1); + } +}); +prog.parse(process.argv); diff --git a/package.json b/package.json index dc845aa6..a387fd57 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,11 @@ "url": "https://github.com/HospitalRun/hospitalrun-server.git" }, "scripts": { + "ddoc": "npm run build:bin && node bin/ddoc", "commit": "npx git-cz", "build": "npm run clean && tsc -p ./tsconfig.json && npm run build:designs", "build:designs": "tsc -p ./src/db/tsconfig.json", + "build:bin": "rimraf bin && tsc -p ./src/bin/tsconfig.json", "start": "node dist", "clean": "rimraf dist", "dev:db": "pouchdb-server --config ./db/config.json", @@ -30,7 +32,6 @@ "author": "Maksim Sinik ", "license": "MIT", "dependencies": { - "cross-env": "~7.0.0", "fastify": "~2.12.0", "fastify-autoload": "~1.2.2", "fastify-blipp": "~2.1.0", @@ -41,14 +42,18 @@ "fastify-plugin": "~1.6.1", "make-promises-safe": "~5.1.0", "nano": "~8.2.1", - "qs": "~6.9.1" + "qs": "~6.9.1", + "require-from-string": "~2.0.2" }, "devDependencies": { "@commitlint/cli": "8.3.5", "@commitlint/config-conventional": "8.3.4", "@commitlint/prompt": "8.3.5", + "@types/mkdirp": "~1.0.0", "@types/node": "~13.7.0", "@types/qs": "~6.9.0", + "@types/require-from-string": "~1.2.0", + "@types/sade": "~1.6.0", "@typescript-eslint/eslint-plugin": "~2.21.0", "@typescript-eslint/parser": "~2.21.0", "commitizen": "~4.0.3", @@ -61,14 +66,17 @@ "eslint-config-prettier": "~6.10.0", "eslint-plugin-import": "~2.20.0", "eslint-plugin-prettier": "~3.1.0", + "glob": "~7.1.6", "husky": "~4.2.0", "lint-staged": "~10.0.7", + "mkdirp": "~1.0.3", "nodemon": "~2.0.0", "npm-run-all": "~4.1.5", "pino-colada": "~1.5.0", "pouchdb-server": "~4.2.0", "prettier": "~1.19.1", "rimraf": "~3.0.0", + "sade": "~1.7.3", "source-map-support": "~0.5.16", "tap": "~14.10.5", "tap-mocha-reporter": "~5.0.0", diff --git a/src/bin/ddoc.ts b/src/bin/ddoc.ts new file mode 100644 index 00000000..d775d492 --- /dev/null +++ b/src/bin/ddoc.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import path from 'path' +import fs from 'fs' +import { promisify } from 'util' +import sade from 'sade' +import ts from 'typescript' +import requireFromString from 'require-from-string' +import originalGlob from 'glob' +import mkdirp from 'mkdirp' + +const stat = promisify(fs.stat) +const readFile = promisify(fs.readFile) +const writeFile = promisify(fs.writeFile) +const glob = promisify(originalGlob) + +const prog = sade('ddoc') + +prog.version('0.1.0') + +prog + .command('build ', 'Build design document(s) from TypeScript soruce directory or file.', { + default: true, + }) + // .describe('Build design documents from TypeScript soruce directory or file.') + .option('-c, --config', 'Provide path to custom tsconfig.json', './tsconfig.json') + .example('build src/db/designs') + .example('build src/db/designs/patient.ts -c src/db/tsconfig.json') + .action(async (src, opts) => { + try { + const cwd = process.cwd() + const tsconfigPath = path.isAbsolute(opts.config) ? opts.config : path.join(cwd, opts.config) + + console.log(`> using ${tsconfigPath} config`) + const tsconfig = require(tsconfigPath) // eslint-disable-line + + src = path.isAbsolute(src) ? path.normalize(src) : path.join(cwd, src) // eslint-disable-line + + const srcStats = await stat(src) + // use outDir if specified inside tsconfig, otherwise build json alongside ts files + let dest: string = tsconfig?.compilerOptions?.outDir + ? path.join(path.dirname(tsconfigPath), tsconfig.compilerOptions.outDir) + : '' + let ddocs: string[] + if (srcStats.isDirectory()) { + dest = dest || src + ddocs = await glob(path.join(src, '**/*.ts')) + } else { + dest = dest || path.dirname(src) + ddocs = [src] + } + + console.log(`> src directory is ${src}`) + await mkdirp(dest) + console.log(`> destination directory is ${dest}`) + + const errors: Error[] = [] + await Promise.all( + ddocs.map(async srcPath => { + try { + const sourceFile = (await readFile(srcPath)).toString() + const output = ts.transpileModule(sourceFile, tsconfig) + const ddoc = requireFromString(output.outputText) + const filename = path.basename(srcPath, '.ts') + const stringifiedDesign = JSON.stringify( + ddoc, + (_, val) => { + if (typeof val === 'function') { + return val.toString() + } + return val + }, + 1, + ) + await writeFile(path.join(dest, `${filename}.json`), stringifiedDesign) + } catch (err) { + errors.push(err) + } + }), + ) + if (errors.length > 0) { + errors.forEach(err => { + console.error(err) + }) + throw new Error(`Compilation failed. Resolve errors in your code and try again.`) + } + } catch (err) { + console.error(err) + process.exit(1) + } + }) + +prog.parse(process.argv) diff --git a/src/bin/tsconfig.json b/src/bin/tsconfig.json new file mode 100644 index 00000000..27269093 --- /dev/null +++ b/src/bin/tsconfig.json @@ -0,0 +1,14 @@ +// we need this custom tsconfig.json to handle CouchDB Javascript Context locally to this folder +{ + "extends": "../../tsconfig.json", + "include": [ + "." + ], + "exclude": [ + "node_modules", + ], + "compilerOptions": { + "outDir": "../../bin", + "sourceMap": false + } +} diff --git a/src/db/designs/index.ts b/src/db/designs/index.ts deleted file mode 100644 index 3a5910a3..00000000 --- a/src/db/designs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const designsRootFolder = __dirname diff --git a/src/db/tsconfig.json b/src/db/tsconfig.json index 45b022af..65c19345 100644 --- a/src/db/tsconfig.json +++ b/src/db/tsconfig.json @@ -1,17 +1,16 @@ -// we need this custom tsconfig.json to handle CouchDB Javascript Context locally to this folder { "extends": "../../tsconfig.json", "include": [ "." ], "exclude": [ - "node_modules", + "node_modules" ], "compilerOptions": { "typeRoots": [ - "../../node_modules/@types", "./design-functions-context.d.ts" ], - "outDir": "../../dist/db", + "outDir": "../../db/designs", + "sourceMap": false } }