From 9cfab5ac70472f0134de0cdb0a9e3814d333ca83 Mon Sep 17 00:00:00 2001 From: Samual Norman Date: Thu, 3 Sep 2020 19:11:40 +0100 Subject: [PATCH] the tools are now built in fixed bug with config --- .vscode/settings.json | 7 +- index.ts | 386 ++++++++++++++++++++++++++++++++++++++++++ lib/hsm.ts | 20 ++- package-lock.json | 17 +- package.json | 6 +- 5 files changed, 410 insertions(+), 26 deletions(-) create mode 100644 index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 84b7f7b..a99dadd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,10 @@ "files.exclude": { "**/.*": true, "**/*.d.ts": true, - "**/*.js": { "when": "$(basename).ts" } + "**/*.js": { "when": "$(basename).ts" }, + "node_modules": true, + "package-lock.json": true, + "LICENSE": true }, "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..665790c --- /dev/null +++ b/index.ts @@ -0,0 +1,386 @@ +import { readdir as readDir, writeFile, unlink, mkdir as mkDir, readFile, copyFile } from "fs/promises" +import { watch as watchDir } from "chokidar" +import { minify } from "terser" +import { resolve as resolvePath, basename, extname } from "path" + +/** + * Copies target file or files in target folder to hackmud folder. + * + * @param target file or folder to be pushed + * @param hackmudPath hackmud directory + * @param user hackmud user to target + */ +export async function pushBuilt(target: string, hackmudPath: string, user: string) { + if (extname(target) == ".js") { + try { + copyFile(target, resolvePath(hackmudPath, user, "scripts", basename(target))) + + return { pushedCount: 1 } + } catch (error) { + if (error.code != "EISDIR") + throw error + } + } + + const files = await readDir(target) + let pushedCount = 0 + + for (const file of files) + if (extname(file) == ".js") { + copyFile(resolvePath(target, file), resolvePath(hackmudPath, user, "scripts", file)) + pushedCount++ + } + + return { pushedCount } +} + +/** + * Deletes target file or files in target folder and equivalent in hackmud folder. + * + * @param target file or folder to be cleared + * @param hackmudPath hackmud directory + * @param user hackmud user to target + */ +export async function clear(target: string, hackmudPath: string, user: string) { + let targetRemoved = 0 + let pushedRemoved = 0 + + for (const file of await readDir(target)) + if (extname(file) == ".js") { + unlink(resolvePath(target, file)) + targetRemoved++ + } + + for (const file of await readDir(resolvePath(hackmudPath, user, "scripts"))) + if (extname(file) == ".js") { + unlink(resolvePath(hackmudPath, user, "scripts", file)) + pushedRemoved++ + } + + return { targetRemoved, pushedRemoved } +} + +/** + * Builds target file or files in target folder and dumps them in specified directory. + * + * @param target file or folder to be built + * @param distPath folder to dump built files + */ +export async function build(target: string, distPath: string) { + const filesWrote: { name: string, minLength: number, oldLength: number }[] = [] + + for (const name of await readDir(target)) { + const code = await readFile(resolvePath(target, name), { encoding: "utf8" }) + const minCode = await hackmudMinify(code) + + try { + writeFile(resolvePath(distPath, name), addAutocomplete(code, minCode)) + } catch (error) { + if (error.code != "ENOENT") + throw error + + mkDir(distPath) + writeFile(resolvePath(distPath, name), addAutocomplete(code, minCode)) + } + + filesWrote.push({ name, minLength: hackmudLength(minCode), oldLength: hackmudLength(code) }) + } + + return filesWrote +} + +/** + * Watches target file or folder for updates and builds and pushes updated file. + * + * @param srcPath path to folder containing source files + * @param hackmudPath path to hackmud directory + * @param users users to push to (pushes to all if empty) + * @param scripts scripts to push from (pushes from all if empty) + * @param onUpdate function that's called after each script has been built and written + */ +export function watch(srcPath: string, hackmudPath: string, users: string[], scripts: string[], onUpdate?: (info: Info) => void) { + watchDir("", { depth: 1, cwd: srcPath, awaitWriteFinish: { stabilityThreshold: 100 } }).on("change", async path => { + if (extname(path) == ".js") { + const script = basename(path, ".js") + const parts = path.split("/") + + switch (parts.length) { + case 1: { + const file = path + + if (!scripts.length || scripts.includes(script)) { + const code = await readFile(resolvePath(srcPath, path), { encoding: "utf-8" }) + + const skips = new Map() + const promisesSkips: Promise[] = [] + + for (const dir of await readDir(srcPath, { withFileTypes: true })) { + if (dir.isDirectory()) { + promisesSkips.push(readDir(resolvePath(srcPath, dir.name), { withFileTypes: true }).then(files => { + for (const file of files) { + if (file.isFile() && extname(file.name) == ".js") { + const name = basename(file.name, ".js") + const skip = skips.get(name) + + if (skip) + skip.push(dir.name) + else + skips.set(name, [ dir.name ]) + } + } + })) + } + } + + await Promise.all(promisesSkips) + + const minCode = await hackmudMinify(code) + const info: Info = { script: path, users: [], srcLength: hackmudLength(code), minLength: hackmudLength(minCode) } + + const skip = skips.get(script) || [] + const promises: Promise[] = [] + + if (!users.length) + users = (await readDir(hackmudPath, { withFileTypes: true })) + .filter(a => a.isFile() && extname(a.name) == ".key") + .map(a => basename(a.name, ".key")) + + for (const user of users) { + if (!skip.includes(user)) { + info.users.push(user) + + promises.push(writeFile(resolvePath(hackmudPath, user, "scripts", file), minCode).catch(async error => { + if (error.code != "ENOENT") + throw error + + await mkDir(resolvePath(hackmudPath, user, "scripts"), { recursive: true }) + await writeFile(resolvePath(hackmudPath, user, "scripts", file), minCode) + })) + } + } + + if (onUpdate) { + await Promise.all(promises) + onUpdate(info) + } + } + + break + } + + case 2: { + const [ user, file ] = parts + + if ((!users.length || users.includes(user)) && (!scripts.length || scripts.includes(script))) { + const code = await readFile(resolvePath(srcPath, path), { encoding: "utf-8" }) + const minCode = await hackmudMinify(code) + const info: Info = { script: path, users: [ user ], srcLength: hackmudLength(code), minLength: hackmudLength(minCode) } + const promises: Promise[] = [] + + promises.push(writeFile(resolvePath(hackmudPath, user, "scripts", file), minCode).catch(async error => { + if (error.code != "ENOENT") + throw error + + await mkDir(resolvePath(hackmudPath, user, "scripts"), { recursive: true }) + await writeFile(resolvePath(hackmudPath, user, "scripts", file), minCode) + })) + + if (onUpdate) { + await Promise.all(promises) + onUpdate(info) + } + } + + break + } + } + } + }) +} + +interface Info { + script: string + users: string[] + srcLength: number + minLength: number +} + +/** + * Push a specific or all scripts to a specific or all users. + * In source directory, scripts in folders will override scripts with same name for user with folder name. + * + * e.g. foo/bar.js overrides other bar.js script just for user foo. + * + * @param srcPath path to folder containing source files + * @param hackmudPath path to hackmud directory + * @param users users to push to (pushes to all if empty) + * @param scripts scripts to push from (pushes from all if empty) + * @param callback function that's called after each script has been built and written + */ +export function push(srcPath: string, hackmudPath: string, users: string[], scripts: string[], callback?: (info: Info) => void) { + return new Promise(async resolve => { + const infoAll: Info[] = [] + const files = await readDir(srcPath, { withFileTypes: true }) + const skips = new Map() + + const promises: Promise[] = [] + + for (const dir of files) { + const user = dir.name + + if (dir.isDirectory() && (!users.length || users.includes(user))) { + promises.push(readDir(resolvePath(srcPath, user), { withFileTypes: true }).then(files => { + for (const file of files) { + const script = file.name + const name = basename(script, ".js") + + if (extname(script) == ".js" && file.isFile() && (!scripts.length || scripts.includes(name))) { + let skip = skips.get(name) + + if (skip) + skip.push(user) + else + skips.set(name, [ user ]) + + readFile(resolvePath(srcPath, user, script), { encoding: "utf-8" }).then(async code => { + const minCode = await hackmudMinify(code) + const info: Info = { script: `${user}/${script}`, users: [ user ], srcLength: hackmudLength(code), minLength: hackmudLength(minCode) } + + infoAll.push(info) + + await writeFile(resolvePath(hackmudPath, user, "scripts", script), minCode).catch(async error => { + if (error.code != "ENOENT") + throw error + + await mkDir(resolvePath(hackmudPath, user, "scripts"), { recursive: true }) + await writeFile(resolvePath(hackmudPath, user, "scripts", script), minCode) + }) + + callback?.(info) + }) + } + } + })) + } + } + + if (!users.length) + users = (await readDir(hackmudPath, { withFileTypes: true })) + .filter(a => a.isFile() && extname(a.name) == ".key") + .map(a => basename(a.name, ".key")) + + Promise.all(promises).then(() => { + const promises: Promise[] = [] + + for (const file of files) { + if (file.isFile()) { + const extension = extname(file.name) + + if (extension == ".js") { + const name = basename(file.name, extension) + + if (!scripts.length || scripts.includes(name)) { + promises.push(readFile(resolvePath(srcPath, file.name), { encoding: "utf-8" }).then(async code => { + const minCode = await hackmudMinify(code) + const info: Info = { script: file.name, users: [], srcLength: hackmudLength(code), minLength: hackmudLength(minCode) } + + infoAll.push(info) + + const skip = skips.get(name) || [] + + const promises: Promise[] = [] + + for (const user of users) + if (!skip.includes(user)) { + info.users.push(user) + + promises.push(writeFile(resolvePath(hackmudPath, user, "scripts", file.name), minCode).catch(async error => { + if (error.code != "ENOENT") + throw error + + await mkDir(resolvePath(hackmudPath, user, "scripts"), { recursive: true }) + await writeFile(resolvePath(hackmudPath, user, "scripts", file.name), minCode) + })) + } + + if (callback) { + await Promise.all(promises) + callback(info) + } + })) + } + } + } + } + + Promise.all(promises).then(() => { + resolve(infoAll) + }) + }) + }) +} + +/** + * Copies script from hackmud to local source folder. + * + * @param srcPath path to folder containing source files + * @param hackmudPath path to hackmud directory + * @param scriptName script to pull in `user.script` format + */ +export async function pull(srcPath: string, hackmudPath: string, scriptName: string) { + const [ user, script ] = scriptName.split(".") + + try { + await copyFile(resolvePath(hackmudPath, user, "scripts", `${script}.js`), resolvePath(srcPath, user, `${script}.js`)) + } catch (error) { + if (error.code != "ENOENT") + throw error + + await mkDir(resolvePath(srcPath, user)) + await copyFile(resolvePath(hackmudPath, user, "scripts", `${script}.js`), resolvePath(srcPath, user, `${script}.js`)) + } +} + +async function hackmudMinify(code: string) { + const anon_code = Date.now().toString(16) + + const minifiedCode = (await minify( + code.replace(/function(?: \w+| )?\(/, `function script_${anon_code}(`) + .replace(/#(?:(?:f|h|m|l|n|[0-4])?s|db|G|FMCL)/g, a => a.replace("#", `_hash_${anon_code}_`)), + { + compress: { + arrows: false, // hackmud does not like this + keep_fargs: false, + negate_iife: false, + booleans_as_integers: true, + unsafe_undefined: true, + unsafe_comps: true, + unsafe_proto: true, + passes: 2, + ecma: 2017 + } + } + )).code + + if (minifiedCode) + return minifiedCode + .replace(`script_${anon_code}`, "") + .replace(new RegExp(`_hash_${anon_code}_`, "g"), "#") + else + return "" +} + +function addAutocomplete(sourceCode: string, code: string) { + const autocompleteRegex = /^(?:\/\/ @autocomplete (.+)|function(?: \w+| )?\([^\)]*\)\s*{\s*\/\/(.+))\n/ + const match = sourceCode.match(autocompleteRegex) + + if (!match) + return code + + const autocomplete = (match[1] || match[2]).trim() + return code.replace(/function\s*\([^\)]*\){/, `$& // ${autocomplete}\n`) +} + +function hackmudLength(code: string) { + return code.replace(/\s/g, "").length +} diff --git a/lib/hsm.ts b/lib/hsm.ts index d91c444..b1b4dc1 100644 --- a/lib/hsm.ts +++ b/lib/hsm.ts @@ -1,7 +1,7 @@ import { readFile, mkdir as mkDir, writeFile, rmdir as rmDir } from "fs/promises" import { resolve as resolvePath } from "path" import { homedir as homeDir } from "os" -import { build, clear, pull, push, pushBuilt, watch } from "hackmud_env-tools" +import { build, clear, pull, push, pushBuilt, watch } from ".." import { redBright, yellowBright, greenBright, blueBright, cyanBright, magentaBright, bold, dim } from "ansi-colors" interface LooseObject { @@ -98,9 +98,9 @@ for (let arg of process.argv.slice(2)) { console.log(`cleared ${targetRemoved} file(s) from ${target} and ${pushedRemoved} file(s) from ${user}`) } else - console.log("set defaultUser in config first") + console.log("set defaultUser in config first\nhsm config set defaultUser ") } else - console.log("set hackmudPath in config first") + console.log("set hackmudPath in config first\nhsm config set hackmudPath ") break } @@ -223,7 +223,7 @@ for (let arg of process.argv.slice(2)) { break } - + case "config": switch (commands[1]) { case "get": @@ -239,7 +239,7 @@ for (let arg of process.argv.slice(2)) { console.log(config) } else help() - + break } @@ -250,7 +250,7 @@ for (let arg of process.argv.slice(2)) { if (keys.length) { let object = config - + for (let key of keys.slice(0, -1)) object = typeof object[key] == "object" ? object[key] : object[key] = {} @@ -262,14 +262,14 @@ for (let arg of process.argv.slice(2)) { console.log(config) } else help() - + break } default: if (commands[1]) console.log("unknown command") - + help() } @@ -285,7 +285,7 @@ for (let arg of process.argv.slice(2)) { default: if (commands[0]) console.log("unknown command") - + help() } @@ -319,6 +319,8 @@ async function getConfig() { try { config = JSON.parse(await readFile(configFile, { encoding: "utf-8" })) + } catch { + config = {} } finally { if (typeof config != "object") config = {} diff --git a/package-lock.json b/package-lock.json index 9cd8b41..c98c7bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hackmud-script-manager", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -84,15 +84,6 @@ "is-glob": "^4.0.1" } }, - "hackmud_env-tools": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/hackmud_env-tools/-/hackmud_env-tools-1.2.1.tgz", - "integrity": "sha512-uAkNfA2wP9/5EM8iRXJnXkExapUz2+id2f8UH66WLOuOojoDRNb0kO7QIDJfvLlKWSr4pfemDAJpJGu7hwJmRA==", - "requires": { - "chokidar": "^3.4.2", - "terser": "^5.2.1" - } - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -152,9 +143,9 @@ } }, "terser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.2.1.tgz", - "integrity": "sha512-/AOtjRtAMNGO0fIF6m8HfcvXTw/2AKpsOzDn36tA5RfhRdeXyb4RvHxJ5Pah7iL6dFkLk+gOnCaNHGwJPl6TrQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.0.tgz", + "integrity": "sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg==", "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", diff --git a/package.json b/package.json index 478bdfa..8b86ab4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "hackmud-script-manager", - "version": "0.1.1", + "version": "0.1.2", "description": "", + "main": "index.js", "files": [ "bin/hsm", "lib/hsm.js" @@ -22,7 +23,8 @@ "homepage": "https://github.com/samualtnorman/hackmud-script-manager#readme", "dependencies": { "ansi-colors": "^4.1.1", - "hackmud_env-tools": "^1.2.1" + "chokidar": "^3.4.2", + "terser": "^5.3.0" }, "devDependencies": { "@types/node": "^14.6.1",