optc
Version:
An easy way to write TypeScript cli script application.
566 lines (549 loc) • 19.2 kB
JavaScript
;
const node_fs = require('node:fs');
const node_child_process = require('node:child_process');
const fs = require('fs-extra');
const path = require('node:path');
const crypto = require('node:crypto');
const node_url = require('node:url');
const BabelTsPlugin = require('@babel/plugin-transform-typescript');
const breadc = require('breadc');
const kolorist = require('kolorist');
const path$1 = require('path');
const axios = require('axios');
const globby = require('globby');
const os = require('node:os');
const findUp = require('find-up');
const createDebug = require('debug');
const scule = require('scule');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
const crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
const BabelTsPlugin__default = /*#__PURE__*/_interopDefaultCompat(BabelTsPlugin);
const path__default$1 = /*#__PURE__*/_interopDefaultCompat(path$1);
const axios__default = /*#__PURE__*/_interopDefaultCompat(axios);
const os__default = /*#__PURE__*/_interopDefaultCompat(os);
const createDebug__default = /*#__PURE__*/_interopDefaultCompat(createDebug);
const version = "0.6.4";
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
function Process(pieces, args, { cwd = process.cwd(), verbose = true, shell = true } = {}) {
const parseCmd = () => {
const escape = (arg) => {
if (typeof arg === "number") {
return "" + arg;
} else if (typeof arg === "boolean") {
return arg ? "true" : "false";
} else if (typeof arg === "string") {
if (arg === "" || /^[a-z0-9/_.-]+$/i.test(arg)) {
return arg;
} else {
return `"${arg.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\f/g, "\\f").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\v/g, "\\v").replace(/\0/g, "\\0")}"`;
}
} else if (arg instanceof ProcessResult) {
throw new Error("Optc: Unimplement branch");
} else {
throw new Error("Optc: Unreachable branch");
}
};
const cmd = [pieces[0]];
let i = 0;
while (i < args.length) {
if (Array.isArray(args[i])) {
cmd.push(args[i].map(escape).join(" "));
} else {
cmd.push(escape(args[i]));
}
cmd.push(pieces[++i]);
}
return cmd.join("");
};
return new Promise((res) => {
const cmd = parseCmd();
setTimeout(() => {
if (verbose) {
console.log(`$ ${cmd}`);
}
const child = node_child_process.spawn(cmd, {
cwd,
shell,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true
});
let stdout = "", stderr = "", combined = "";
const onStdout = (data) => {
if (verbose) {
process.stdout.write(data);
}
stdout += data;
combined += data;
};
const onStderr = (data) => {
if (verbose) {
process.stderr.write(data);
}
stderr += data;
combined += data;
};
child.stdout.on("data", onStdout);
child.stderr.on("data", onStderr);
child.on("close", (code, signal) => {
const result = new ProcessResult({ code, signal, stdout, stderr, combined });
res(result);
});
}, 0);
});
}
class ProcessResult {
constructor({
code,
signal,
stdout,
stderr,
combined
}) {
__publicField$1(this, "code");
__publicField$1(this, "signal");
__publicField$1(this, "stdout");
__publicField$1(this, "stderr");
__publicField$1(this, "combined");
this.code = code;
this.signal = signal;
this.stdout = stdout;
this.stderr = stderr;
this.combined = combined;
}
}
function logWarn(msg) {
console.warn(`${kolorist.lightYellow("Warn")} ${msg}`);
}
async function importJiti() {
const jiti = await import('jiti');
return jiti.default ? jiti.default : jiti;
}
const OPTC_CACHE = process.env.OPTC_CACHE === "false" ? false : true;
const OPTC_ROOT = process.env.OPTC_ROOT ? path__default.resolve(process.env.OPTC_ROOT) : path__default.join(os__default.homedir(), ".optc");
const NODE_MODULES = findUp.findUpSync("node_modules", { type: "directory" });
const CACHE_ROOT = NODE_MODULES ? path__default.join(NODE_MODULES, ".cache/optc") : path__default.join(OPTC_ROOT, ".cache");
async function ensureSpace() {
await fs__default.ensureDir(OPTC_ROOT);
const dep = path__default.join(OPTC_ROOT, "dep.ts");
const pkg = path__default.join(OPTC_ROOT, "package.json");
const optcDts = path__default.join(OPTC_ROOT, "optc.d.ts");
const globalDts = path__default.join(OPTC_ROOT, "globals.d.ts");
if (!fs__default.existsSync(pkg)) {
await fs__default.writeFile(
pkg,
JSON.stringify(
{
name: "optc-workspace",
private: true,
dependencies: {},
optc: {
version
}
},
null,
2
),
"utf-8"
);
}
if (!fs__default.existsSync(dep)) {
const body = `export default function(global: any) {
}`;
await fs__default.writeFile(dep, body, "utf-8");
}
if (!fs__default.existsSync(globalDts)) {
const dts = path__default.join(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('shared/optc.81056db6.cjs', document.baseURI).href))), "../../../globals.d.ts");
const body = `/// <reference path="${dts}" />`;
await fs__default.writeFile(globalDts, body, "utf-8");
}
if (!fs__default.existsSync(optcDts)) {
const body = `/// <reference path="./optc.d.ts" />`;
await fs__default.writeFile(optcDts, body, "utf-8");
}
}
async function loadDep(name = "dep.ts") {
const filepath = path__default.join(OPTC_ROOT, name);
if (!fs__default.existsSync(filepath))
return;
const jiti = (await importJiti())(filepath, { cache: true, sourceMaps: false });
const module = await jiti(filepath);
if (module.default && typeof module.default === "function") {
await module.default(global);
}
}
$.prompt = "$";
$.shell = true;
$.verbose = true;
function $(pieces, ...args) {
return Process(pieces, args, { cwd: process.cwd(), verbose: $.verbose, shell: $.shell });
}
function cd(dir) {
print(`cd ${dir}`, { prompt: true });
process.chdir(dir);
}
function pwd() {
print(`pwd`, { prompt: true });
print(process.cwd());
return process.cwd();
}
function ls(dir) {
return node_fs.readdirSync(dir ?? process.cwd());
}
function readTextFile(filename, encode = "utf-8") {
return node_fs.readFileSync(filename, encode);
}
function writeTextFile(filename, content, encode = "utf-8") {
return node_fs.writeFileSync(filename, content, encode);
}
function sleep(ms) {
return new Promise((res) => setTimeout(() => res(), ms));
}
function print(msg, option) {
if ($.verbose) {
if (!!option?.prompt) {
const prompt = typeof option?.prompt === "string" ? option.prompt : $.prompt;
console.log(`${prompt} ${msg}`);
} else {
console.log(msg);
}
}
}
async function registerGlobal(preset) {
global.$ = $;
global.cd = cd;
global.pwd = pwd;
global.ls = ls;
global.path = path__default$1;
global.fs = fs__default;
global.readTextFile = readTextFile;
global.writeTextFile = writeTextFile;
global.glob = globby.globby;
global.globby = globby.globby;
global.sleep = sleep;
global.http = axios__default;
global.axios = axios__default;
for (const key of [
"copy",
"mkdirp",
"move",
"remove",
"outputFile",
"readJson",
"writeJson",
"outputJson",
"emptyDir",
"ensureFile",
"ensureDir"
]) {
global[key] = fs__default[key];
}
await loadDep(preset);
}
var ValueType = /* @__PURE__ */ ((ValueType2) => {
ValueType2["String"] = "string";
ValueType2["Number"] = "number";
ValueType2["Boolean"] = "boolean";
ValueType2["Array"] = "string[]";
return ValueType2;
})(ValueType || {});
const debug = createDebug__default("optc:reflection");
function ReflectionPlugin(_ctx, option) {
debug("Create Reflection Plugin");
const optionMap = /* @__PURE__ */ new Map();
const mainCode = option.code.toString();
const isSkip = (code) => code !== mainCode;
return {
name: "optc-reflection",
pre(file) {
optionMap.clear();
},
visitor: {
ExportDeclaration(exportPath, state) {
if (isSkip(state.file.code))
return;
const isDefault = exportPath.node.type === "ExportDefaultDeclaration";
exportPath.traverse({
FunctionDeclaration(path) {
debug(isDefault ? "Export a default function" : "Export a function");
const options = [];
const parameters = path.node.params.map((param, pidx) => {
if (param.type === "Identifier") {
let type = param.typeAnnotation?.type === "TSTypeAnnotation" ? parseType(param.typeAnnotation) : void 0;
if (type && type === ValueType.Boolean) {
type = ValueType.String;
logWarn(
`Unsupport parameter type annotation at function ${path.node.id?.name ?? "default"}`
);
}
if (!type && pidx + 1 === path.node.params.length) {
if (param.typeAnnotation?.type === "TSTypeAnnotation") {
if (param.typeAnnotation.typeAnnotation.type === "TSTypeReference") {
if (param.typeAnnotation.typeAnnotation.typeName.type === "Identifier") {
const name = param.typeAnnotation.typeAnnotation.typeName.name;
if (optionMap.has(name)) {
options.push(...optionMap.get(name));
} else {
optionMap.set(name, options);
}
}
} else if (param.typeAnnotation.typeAnnotation.type === "TSTypeLiteral") {
options.push(...parseOptions(param.typeAnnotation.typeAnnotation.members));
}
}
return void 0;
} else if (!type) {
logWarn(
`Unsupport parameter type annotation at function ${path.node.id?.name ?? "default"}`
);
}
return {
name: param.name,
type: type ?? ValueType.String,
required: !param.optional
};
} else if (param.type === "ObjectPattern") ; else {
return void 0;
}
});
const command = {
name: path.node.id?.name ?? "",
default: isDefault,
options,
parameters: parameters.filter(Boolean),
description: parseComment(exportPath.node.leadingComments)
};
option.commands.push(command);
path.stop();
}
});
},
TSInterfaceDeclaration(path, state) {
if (isSkip(state.file.code))
return;
debug("Declare Interface");
const options = parseOptions(path.node.body.body);
if (optionMap.has(path.node.id.name)) {
optionMap.get(path.node.id.name).push(...options);
} else {
optionMap.set(path.node.id.name, options);
}
}
}
};
}
function parseComment(comments) {
if (!Array.isArray(comments))
return "";
comments = comments.filter((c) => !(c.type === "CommentLine") || !c.value.startsWith("/"));
if (comments.length === 0)
return "";
if (comments[comments.length - 1].type === "CommentLine") {
return comments[comments.length - 1].value.trim();
} else {
const text = comments[comments.length - 1].value;
if (text.startsWith("*\n")) {
const lines = text.split("\n").map((t) => t.trim()).map((t) => t.startsWith("*") ? t.slice(1) : t).map((t) => t.trim()).filter(Boolean);
return lines.length > 0 ? lines[0] : "";
} else {
return text.trim();
}
}
}
function parseType(typeAnnotation) {
if (!typeAnnotation)
return ValueType.String;
const type = typeAnnotation.typeAnnotation.type;
if (type === "TSStringKeyword") {
return ValueType.String;
} else if (type === "TSNumberKeyword") {
return ValueType.Number;
} else if (type === "TSBooleanKeyword") {
return ValueType.Boolean;
} else if (type === "TSArrayType") {
const elType = typeAnnotation.typeAnnotation.elementType.type;
if (elType === "TSStringKeyword") {
return ValueType.Array;
} else {
return void 0;
}
} else {
return void 0;
}
}
function parseOptions(body) {
const sigs = body.filter((t) => t.type === "TSPropertySignature");
return sigs.map((sig) => {
if (sig.key.type === "Identifier" || sig.key.type === "StringLiteral") {
const name = sig.key.type === "Identifier" ? sig.key.name : sig.key.value;
if (name === "--") {
if (parseType(sig.typeAnnotation) !== ValueType.Array) ;
return void 0;
}
let type = parseType(sig.typeAnnotation);
return {
name: scule.kebabCase(name),
type: type ?? ValueType.String,
required: !sig.optional,
description: parseComment(sig.leadingComments)
};
} else {
return void 0;
}
}).filter(Boolean);
}
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class Optc {
constructor(scriptPath, option) {
__publicField(this, "scriptPath");
__publicField(this, "breadc");
__publicField(this, "commands", []);
this.scriptPath = scriptPath;
this.breadc = breadc.breadc(option.name, { version: option.version, description: option.description });
}
getRawCommands() {
return this.commands;
}
setupCommands(module, commands) {
this.commands.push(...commands);
for (const command of commands) {
const name = [
command.name,
...command.parameters.map(
(arg) => arg.type === ValueType.Array ? `[...${arg.name}]` : arg.required ? `<${arg.name}>` : `[${arg.name}]`
)
];
if (command.default) {
name.splice(0, 1);
}
const fn = command.default ? module.default : module[command.name];
if (!fn || typeof fn !== "function") {
if (command.default) {
logWarn(`Can not find default function`);
} else {
logWarn(`Can not find function ${command.name}`);
}
}
const cmd = command.options.reduce(
(cmd2, option) => {
let text = `--${option.name}`;
if (option.type === ValueType.String || option.type === ValueType.Number) {
if (option.required) {
text += " <text>";
} else {
text += " <text>";
}
} else if (option.type === ValueType.Array) {
text += " [...text]";
}
return cmd2.option(text, option.description);
},
this.breadc.command(name.join(" "), command.description)
);
if (command.default && command.name) {
cmd.alias(command.name);
}
cmd.action(fn);
}
}
async run(args) {
await registerGlobal();
return this.breadc.run(args);
}
}
async function makeOptc(script) {
await fs__default.ensureDir(CACHE_ROOT);
const scriptName = path__default.parse(path__default.basename(script)).name;
const content = await fs__default.readFile(script);
const hash = crypto__default.createHash("sha256").update(content).digest("hex");
const cachedScriptPath = path__default.join(CACHE_ROOT, scriptName + "_" + hash + ".js");
const cachedReflPath = path__default.join(CACHE_ROOT, scriptName + "_" + hash + ".json");
const initOptc = async () => {
const commands = [];
const jiti = (await importJiti())(node_url.pathToFileURL(script).href, {
cache: OPTC_CACHE,
sourceMaps: false,
transformOptions: {
babel: {
plugins: [
[ReflectionPlugin, { code: content, commands }],
[BabelTsPlugin__default, {}]
]
}
}
});
if (!OPTC_CACHE || !fs__default.existsSync(cachedScriptPath) || !fs__default.existsSync(cachedReflPath)) {
await fs__default.writeFile(cachedScriptPath, content);
const module = await jiti(cachedScriptPath);
const loadField = (field, defaultValue) => {
const value = module[field];
if (value === void 0 || value === null)
return defaultValue;
if (typeof value === "string")
return value;
if (typeof value === "function")
return value();
return defaultValue;
};
const cliName = loadField("name", scriptName);
const cliVersion = loadField("version", "unknown");
const cliDescription = loadField("description", "");
const refl = {
name: cliName,
description: cliDescription,
version: cliVersion,
commands,
optc: {
version: version
}
};
await fs__default.writeFile(cachedReflPath, JSON.stringify(refl, null, 2), "utf-8");
const cli = new Optc(cachedScriptPath, {
name: cliName,
version: cliVersion
});
cli.setupCommands(module, commands);
return cli;
} else {
const refl = JSON.parse(await fs__default.readFile(cachedReflPath, "utf-8"));
if (refl?.optc?.version !== version) {
await fs__default.unlink(cachedReflPath);
return initOptc();
}
const cli = new Optc(cachedScriptPath, refl);
const module = await jiti(cachedScriptPath);
cli.setupCommands(module, refl.commands);
return cli;
}
};
return await initOptc();
}
async function bootstrap(script, ...args) {
return await (await makeOptc(script)).run(args);
}
exports.$ = $;
exports.OPTC_ROOT = OPTC_ROOT;
exports.Process = Process;
exports.ProcessResult = ProcessResult;
exports.bootstrap = bootstrap;
exports.cd = cd;
exports.ensureSpace = ensureSpace;
exports.ls = ls;
exports.makeOptc = makeOptc;
exports.pwd = pwd;
exports.readTextFile = readTextFile;
exports.sleep = sleep;
exports.version = version;
exports.writeTextFile = writeTextFile;