UNPKG

optc

Version:

An easy way to write TypeScript cli script application.

566 lines (549 loc) 19.2 kB
'use strict'; 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;