UNPKG

@udraft/cli

Version:

uDraft is a language and stack agnostic code-generation tool that simplifies full-stack development by converting a single YAML file into code for rapid development.

380 lines (346 loc) 11.4 kB
#! /usr/bin/env node const { cwd } = require("process"); const { UDraft } = require("@udraft/core"); const { default: TSClassRenderer, } = require("@udraft/core/dist/builtin/ts-class-renderer"); const {} = require("@udraft/core/dist"); const { default: TSClassValidatorRenderer, } = require("@udraft/core/dist/builtin/ts-class-validator-renderer"); const { default: TSMongooseSchemaRenderer, } = require("@udraft/core/dist/builtin/ts-mongoose-schema-renderer"); const { default: TSApiClientRenderer, } = require("@udraft/core/dist/builtin/ts-api-client-renderer"); const { default: DartClassRenderer, } = require("@udraft/core/dist/builtin/dart-class-renderer"); const { default: DartApiClientRenderer, } = require("@udraft/core/dist/builtin/dart-api-client-renderer"); const { default: TSDraftRenderer, } = require("@udraft/core/dist/builtin/ts-draft-renderer"); const requireFromUrl = require("require-from-url/sync"); const commandLineArgs = require("command-line-args"); const commandLineUsage = require("command-line-usage"); const term = require("terminal-kit").terminal; const fs = require("fs"); const { parseDocument, stringify } = require("yaml"); const path = require("path"); const gitCloner = require("git-cloner"); const { exec } = require("child_process"); const loadFile = (file) => { if (!file) { if (fs.existsSync("./udraft.yml")) file = "./udraft.yml"; else if (fs.existsSync("./udraft.yaml")) file = "./udraft.yaml"; else { term.red(`[uDraft] No YAML file found in current directory!\n`); return; } } if (!fs.existsSync(file)) { term.red(`[uDraft] YAML file not found: ${file}\n`); return; } let rawYaml = fs.readFileSync(file, "utf-8"); const yamlData = parseDocument(rawYaml).toJSON(); return { raw: rawYaml, data: yamlData, file }; }; const loadRenderer = (importUrl) => { let renderer = null; try { const httpRgx = /^(https*):\/\/.+$/; const npmRgx = /^(.+)\@((?:\d\.\d\.\d)|(?:latest))$/; const npmMatch = importUrl.match(npmRgx); const httpMatch = importUrl.match(httpRgx); if (httpMatch) renderer = requireFromUrl(importUrl).default; else if (npmMatch) renderer = requireFromUrl( `https://unpkg.com/${npmMatch[1]}@${npmMatch[2]}/dist/index.js` ).default; else renderer = require(path.join(cwd(), importUrl)).default; } catch (err) {} return renderer; }; const createRenderer = async (file, rendererName) => { rendererName = rendererName.trim(); const outputPath = path.join(cwd(), "udraft/renderers", rendererName); if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath, { recursive: true }); term.blue(`[uDraft] Creating renderer...\n`); gitCloner( [ { source: "https://github.com/lucas-portela/udraft-renderer-boilerplate.git", path: outputPath, }, ], { urlType: "http", showOutput: false, }, (err, data) => { if (err) { term.red(`[uDraft] Error creating renderer:\n`); term.red(err); return; } const packageJsonPath = path.join(outputPath, "package.json"); const indexTsPath = path.join(outputPath, "src/index.ts"); let packageJson = fs.readFileSync(packageJsonPath, "utf-8"); let indexTs = fs.readFileSync(indexTsPath, "utf-8"); packageJson = packageJson.replace( "renderer-name", rendererName.replace("@", "-") ); indexTs = indexTs.replace("language@renderer-name", rendererName); fs.writeFileSync(packageJsonPath, packageJson, "utf-8"); fs.writeFileSync(indexTsPath, indexTs, "utf-8"); term.blue(`[uDraft] Installing dependencies...\n`); exec(`cd "${outputPath}" && npm i && npm run build`, (error) => { if (error) { term.red(`[uDraft] Error installing dependencies:\n`); term.red(err); return; } installRenderer(file, path.relative(cwd(), outputPath), true); term .green(`[uDraft] Renderer `) .green.bold(rendererName) .green(` created and installed succesfully!\n`); term .white( `[uDraft] To edit your custom renderer you should:\n\t- Edit: ` ) .gray.bold(`${path.relative(cwd(), outputPath)}/src/index.ts`) .white(`\n\t- execute: `) .grey.bold("npm run build\n\n"); }); } ); }; const installRenderer = (file, importUrl, silent = false) => { const fileResult = loadFile(file); if (!fileResult) return; const yamlData = fileResult.data; yamlData.renderers = yamlData.renderers || []; const rendererNames = yamlData.renderers .map((rurl) => ({ url: rurl, renderer: loadRenderer(rurl), name: "", })) .map((r) => ({ ...r, name: new r.renderer().$name(), })); const renderer = loadRenderer(importUrl); let rendererName = ""; try { if (renderer) rendererName = new renderer().$name(); } catch (err) { term.red(`[uDraft] Invalid renderer: ${importUrl}\n`); return; } if (!renderer) { term.red(`[uDraft] Renderer not found: ${importUrl}\n`); return; } let duplicated = rendererNames.filter((r) => r.name == rendererName); if (duplicated.length > 0) { yamlData.renderers = yamlData.renderers.filter( (rurl) => !duplicated.some((r) => r.url == rurl) ); } yamlData.renderers.push(importUrl); fs.writeFileSync( fileResult.file, stringify(yamlData).replace(/\: null\n/g, ":\n"), { encoding: "utf-8" } ); if (!silent) term .green( `[uDraft] ${duplicated.length > 0 ? "Updated" : "Installed"} renderer` ) .bold.green(` ${rendererName}\n`); }; const run = async (file, pipeline) => { const fileResult = loadFile(file); if (!fileResult) return; let rawYaml = fileResult.raw; const yamlData = fileResult.data; const resolveImports = (yD, dir = "./") => { Object.keys(yD).forEach((k) => { const f = (k.match(/^\$import\((.+)\)$/) || [])[1]; if (f) { let nestedFile = path.join(dir, f); let ext = "yml"; if (!fs.existsSync(nestedFile + "." + ext)) { ext = "yaml"; if (!fs.existsSync(nestedFile + "." + ext)) return; } nestedFile += "." + ext; const nestedDir = path.dirname(nestedFile); const nestedRaw = fs.readFileSync(nestedFile, "utf-8"); const nestedData = parseDocument(nestedRaw).toJSON() ?? {}; resolveImports(nestedData, nestedDir); Object.keys(nestedData).forEach((nestedKey) => { yD[nestedKey] = nestedData[nestedKey]; }); delete yD[k]; } else if (yD[k] && typeof yD[k] === "object") { resolveImports(yD[k], dir); } }); }; resolveImports(yamlData); rawYaml = JSON.stringify(yamlData); const draft = UDraft.yaml(rawYaml); if (!draft) return; const pipelines = yamlData.pipelines; if (!pipelines) { term.red(`[uDraft] No pipelines found in YAML: ${file}\n`); return; } const renderers = { "ts@classes": TSClassRenderer, "ts@validators": TSClassValidatorRenderer, "ts@mongoose": TSMongooseSchemaRenderer, "ts@client-api": TSApiClientRenderer, "dart@classes": DartClassRenderer, "dart@client-api": DartApiClientRenderer, "ts@draft": TSDraftRenderer, }; (yamlData.renderers || []).forEach((importUrl) => { let renderer = null; const httpRgx = /^(https*):\/\/.+$/; const npmRgx = /^(.+)\@((?:\d\.\d\.\d)|(?:latest))$/; const npmMatch = importUrl.match(npmRgx); const httpMatch = importUrl.match(httpRgx); if (httpMatch) renderer = requireFromUrl(importUrl).default; else if (npmMatch) renderer = requireFromUrl( `https://unpkg.com/${npmMatch[1]}@${npmMatch[2]}/dist/index.js` ).default; else renderer = require(path.join(cwd(), importUrl)).default; if (renderer) { const name = new renderer().$name(); renderers[name] = renderer; } }); console.log(); const executePipeline = async (name, pipelineData) => { term.green(`[uDraft] Pipeline: ${name}\n`); let pipe = draft.begin("./"); for (let cmd of pipelineData) { if (cmd.cd) pipe = pipe.goTo(cmd.cd); else if (cmd.clear != undefined) pipe = pipe.clear(); else { const rendererName = Object.keys(cmd)[0]; if (!rendererName) continue; const rendererArgs = cmd[rendererName]; const renderer = renderers[rendererName]; if (!renderer) { term.red(`[uDraft] Renderer not found: ${rendererName}\n`); return false; } Object.keys(rendererArgs || {}).forEach((arg) => { if (arg[0] == "$") { rendererArgs[arg.slice(1)] = eval(rendererArgs[arg]); delete rendererArgs[arg]; } }); try { pipe = pipe.pipeline([new renderer(rendererArgs)]); } catch (err) { term.red(`[uDraft] Error adding ${rendererName} to pipeline: `, err); term("\n"); return false; } } } pipe = pipe.clear(); await pipe.exec(); return true; }; if (pipeline) await executePipeline(pipeline, pipelines[pipeline]); else { const names = Object.keys(pipelines); for (let name of names) { if (!(await executePipeline(name, pipelines[name]))) return; } } }; const optionDefinitions = [ { name: "help", alias: "h", type: Boolean, description: "Display this usage guide.", }, { name: "install", alias: "i", type: String, description: "Install a new renderer in the uDraft YAML.", typeLabel: "<path|npm package|url>", }, { name: "create-renderer", alias: "c", type: String, description: 'Create a new Renderer and install it in the uDraft YAML. The name should be in the form: "language-type@what-is-rendered" (Eg.: ts@classes for a TypeScript class renderer)', typeLabel: "<renderer name>", }, { name: "file", alias: "f", type: String, description: "The path to the uDraft YAML file. If not set, the tool will try to use 'udraft.yml' or 'udraft.yaml'.", typeLabel: "<path>", defaultOption: true, }, { name: "pipeline", alias: "p", type: String, description: "Which pipeline to execte. If not set, all the pipelines will be executed.", typeLabel: "<name>", }, ]; const options = commandLineArgs(optionDefinitions); if (options.help) { const usage = commandLineUsage([ { header: "uDraft CLI Tool", content: "uDraft is a language and stack agnostic code-generation tool that simplifies full-stack development by converting a single YAML file into code for rapid development.", }, { header: "usage", content: "udraft [--help] [--pipeline=<name>] [file]", }, { header: "Options", optionList: optionDefinitions, }, { content: "Project home: {underline https://www.npmjs.com/package/@udraft/core}", }, ]); console.log(usage); } else if (options.install) { installRenderer(options.file, options.install); } else if (options["create-renderer"]) { createRenderer(options.file, options["create-renderer"]); } else { run(options.file, options.pipeline); }