@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
JavaScript
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);
}