UNPKG

hackmud-script-manager

Version:

Script manager for game hackmud, with minification, TypeScript support, and player script type definition generation.

439 lines (438 loc) 16.9 kB
import babelGenerator from "@babel/generator" import babelTraverse from "@babel/traverse" import t from "@babel/types" import { assert } from "@samual/lib/assert" import { countHackmudCharacters } from "@samual/lib/countHackmudCharacters" import { spliceString } from "@samual/lib/spliceString" import { tokenizer, tokTypes } from "acorn" import * as terser from "terser" import { getReferencePathsToGlobal, includesIllegalString, replaceUnsafeStrings } from "./shared.js" const { default: generate } = babelGenerator, { default: traverse } = babelTraverse, minifyNumber = async number => /\$\((?<number>.+)\)/.exec((await terser.minify(`$(${number})`, { ecma: 2015 })).code).groups.number async function minify(file, { uniqueId = "00000000000", mangleNames = !1, forceQuineCheats, autocomplete } = {}) { assert(/^\w{11}$/.exec(uniqueId), "src/processScript/minify.ts:46:36") let program traverse(file, { Program(path) { program = path path.skip() } }) if (program.scope.hasGlobal("_START")) for (const referencePath of getReferencePathsToGlobal("_START", program)) referencePath.replaceWith(t.identifier("_ST")) if (program.scope.hasGlobal("_TIMEOUT")) for (const referencePath of getReferencePathsToGlobal("_TIMEOUT", program)) referencePath.replaceWith(t.identifier("_TO")) const mainFunctionPath = program.get("body.0") for (const parameter of [...mainFunctionPath.node.params].reverse()) { if ("Identifier" != parameter.type || mainFunctionPath.scope.getBinding(parameter.name).referenced) break mainFunctionPath.node.params.pop() } for (const global in program.scope.globals) { if ("arguments" == global || global.startsWith(`$${uniqueId}$`)) continue const referencePaths = getReferencePathsToGlobal(global, program) if (!(5 + global.length + referencePaths.length >= global.length * referencePaths.length)) { for (const path of referencePaths) path.replaceWith(t.identifier(`_${uniqueId}_GLOBAL_${global}_`)) mainFunctionPath.node.body.body.unshift( t.variableDeclaration("let", [ t.variableDeclarator(t.identifier(`_${uniqueId}_GLOBAL_${global}_`), t.identifier(global)) ]) ) } } const jsonValues = [] let scriptBeforeJSONValueReplacement, comment, undefinedIsReferenced = !1 if (1 != forceQuineCheats) { const fileBeforeJSONValueReplacement = t.cloneNode(file) traverse(fileBeforeJSONValueReplacement, { MemberExpression({ node: memberExpression }) { if (!memberExpression.computed) { assert("Identifier" == memberExpression.property.type, "src/processScript/minify.ts:115:60") if ("prototype" == memberExpression.property.name) { memberExpression.computed = !0 memberExpression.property = t.identifier(`_${uniqueId}_PROTOTYPE_PROPERTY_`) } else if ("__proto__" == memberExpression.property.name) { memberExpression.computed = !0 memberExpression.property = t.identifier(`_${uniqueId}_PROTO_PROPERTY_`) } else if (includesIllegalString(memberExpression.property.name)) { memberExpression.computed = !0 memberExpression.property = t.stringLiteral( replaceUnsafeStrings(uniqueId, memberExpression.property.name) ) } } }, ObjectProperty({ node: objectProperty }) { if ("Identifier" == objectProperty.key.type && includesIllegalString(objectProperty.key.name)) { objectProperty.key = t.stringLiteral(replaceUnsafeStrings(uniqueId, objectProperty.key.name)) objectProperty.shorthand = !1 } }, StringLiteral({ node }) { node.value = replaceUnsafeStrings(uniqueId, node.value) }, TemplateLiteral({ node }) { for (const templateElement of node.quasis) if (templateElement.value.cooked) { templateElement.value.cooked = replaceUnsafeStrings(uniqueId, templateElement.value.cooked) templateElement.value.raw = templateElement.value.cooked .replaceAll("\\", "\\\\") .replaceAll("`", "\\`") .replaceAll("${", "$\\{") } else templateElement.value.raw = replaceUnsafeStrings(uniqueId, templateElement.value.raw) }, RegExpLiteral(path) { path.node.pattern = replaceUnsafeStrings(uniqueId, path.node.pattern) delete path.node.extra } }) scriptBeforeJSONValueReplacement = ( await terser.minify(generate(fileBeforeJSONValueReplacement).code, { ecma: 2015, compress: { passes: 1 / 0, unsafe: !0, unsafe_arrows: !0, unsafe_comps: !0, unsafe_symbols: !0, unsafe_methods: !0, unsafe_proto: !0, unsafe_regexp: !0, unsafe_undefined: !0, sequences: !1 }, format: { semicolons: !1 }, keep_classnames: !mangleNames, keep_fnames: !mangleNames }) ).code .replace(RegExp(`_${uniqueId}_PROTOTYPE_PROPERTY_`, "g"), '"prototype"') .replace(RegExp(`_${uniqueId}_PROTO_PROPERTY_`, "g"), '"__proto__"') autocomplete && (scriptBeforeJSONValueReplacement = spliceString( scriptBeforeJSONValueReplacement, `//${autocomplete}\n`, getFunctionBodyStart(scriptBeforeJSONValueReplacement) + 1 )) if (0 == forceQuineCheats) return scriptBeforeJSONValueReplacement } let code, hasComment = !1 { const promises = [] traverse(file, { FunctionDeclaration(path) { const body = path.get("body") body.traverse({ Function(path) { "CallExpression" != path.parent.type && "callee" != path.parentKey && path.skip() }, Loop: path => path.skip(), ObjectExpression(path) { const o = {} parseObjectExpression(path.node, o) && path.replaceWith(t.identifier(`_${uniqueId}_JSON_VALUE_${jsonValues.push(o) - 1}_`)) }, ArrayExpression(path) { const o = [] parseArrayExpression(path.node, o) && path.replaceWith(t.identifier(`_${uniqueId}_JSON_VALUE_${jsonValues.push(o) - 1}_`)) } }) body.traverse({ TemplateLiteral(path) { if ("TaggedTemplateExpression" == path.parent.type) return const templateLiteral = path.node let replacement = t.stringLiteral(templateLiteral.quasis[0].value.cooked) for (let index = 0; index < templateLiteral.expressions.length; index++) { const expression = templateLiteral.expressions[index], templateElement = templateLiteral.quasis[index + 1] replacement = t.binaryExpression("+", replacement, expression) templateElement.value.cooked && (replacement = t.binaryExpression( "+", replacement, t.stringLiteral(templateElement.value.cooked) )) } path.replaceWith(replacement) }, MemberExpression({ node: memberExpression }) { if (!memberExpression.computed) { assert("Identifier" == memberExpression.property.type, "src/processScript/minify.ts:249:62") if (!(memberExpression.property.name.length < 3)) { memberExpression.computed = !0 memberExpression.property = t.stringLiteral(memberExpression.property.name) } } }, UnaryExpression(path) { if ("void" == path.node.operator) { if ("NumericLiteral" == path.node.argument.type && !path.node.argument.value) { path.replaceWith(t.identifier(`_${uniqueId}_UNDEFINED_`)) undefinedIsReferenced = !0 } } else if ("-" == path.node.operator && "NumericLiteral" == path.node.argument.type) { const value = -path.node.argument.value promises.push( (async () => { if ((await minifyNumber(value)).length <= 3) return "key" == path.parentKey && "ObjectProperty" == path.parent.type && (path.parent.computed = !0) let jsonValueIndex = jsonValues.indexOf(value) ;-1 == jsonValueIndex && (jsonValueIndex += jsonValues.push(value)) path.replaceWith(t.identifier(`_${uniqueId}_JSON_VALUE_${jsonValueIndex}_`)) })() ) path.skip() } }, NullLiteral(path) { let jsonValueIndex = jsonValues.indexOf(null) ;-1 == jsonValueIndex && (jsonValueIndex += jsonValues.push(null)) path.replaceWith(t.identifier(`_${uniqueId}_JSON_VALUE_${jsonValueIndex}_`)) }, NumericLiteral(path) { promises.push( (async () => { if ((await minifyNumber(path.node.value)).length <= 3) return "key" == path.parentKey && "ObjectProperty" == path.parent.type && (path.parent.computed = !0) let jsonValueIndex = jsonValues.indexOf(path.node.value) ;-1 == jsonValueIndex && (jsonValueIndex += jsonValues.push(path.node.value)) path.replaceWith(t.identifier(`_${uniqueId}_JSON_VALUE_${jsonValueIndex}_`)) })() ) }, StringLiteral(path) { if (JSON.stringify(path.node.value).includes("\\u00") || path.toString().length < 4) { path.node.value = replaceUnsafeStrings(uniqueId, path.node.value) return } "key" == path.parentKey && "ObjectProperty" == path.parent.type && (path.parent.computed = !0) let jsonValueIndex = jsonValues.indexOf(path.node.value) ;-1 == jsonValueIndex && (jsonValueIndex += jsonValues.push(path.node.value)) path.replaceWith(t.identifier(`_${uniqueId}_JSON_VALUE_${jsonValueIndex}_`)) }, ObjectProperty({ node }) { if (node.computed || "Identifier" != node.key.type || node.key.name.length < 4) return let jsonValueIndex = jsonValues.indexOf(node.key.name) ;-1 == jsonValueIndex && (jsonValueIndex += jsonValues.push(node.key.name)) node.computed = !0 node.key = t.identifier(`_${uniqueId}_JSON_VALUE_${jsonValueIndex}_`) }, RegExpLiteral(path) { path.node.pattern = replaceUnsafeStrings(uniqueId, path.node.pattern) delete path.node.extra } }) path.skip() } }) await Promise.all(promises) const functionDeclaration = file.program.body[0] assert("FunctionDeclaration" == functionDeclaration.type, "src/processScript/minify.ts:354:61") if (jsonValues.length) { hasComment = !0 if (1 == jsonValues.length) if ("string" != typeof jsonValues[0] || jsonValues[0].includes("\n") || jsonValues[0].includes("\t")) { const variableDeclaration = t.variableDeclaration("let", [ t.variableDeclarator( t.identifier(`_${uniqueId}_JSON_VALUE_0_`), t.callExpression(t.memberExpression(t.identifier("JSON"), t.identifier("parse")), [ t.memberExpression( t.taggedTemplateExpression( t.memberExpression( t.callExpression( t.identifier(`$${uniqueId}$4$SUBSCRIPT$scripts$quine$`), [] ), t.identifier("split") ), t.templateLiteral([t.templateElement({ raw: "\t", cooked: "\t" }, !0)], []) ), t.identifier(`$${uniqueId}$SPLIT_INDEX$`), !0 ) ]) ) ]) undefinedIsReferenced && variableDeclaration.declarations.push( t.variableDeclarator(t.identifier(`_${uniqueId}_UNDEFINED_`)) ) functionDeclaration.body.body.unshift(variableDeclaration) comment = JSON.stringify(jsonValues[0]) } else { const variableDeclaration = t.variableDeclaration("let", [ t.variableDeclarator( t.identifier(`_${uniqueId}_JSON_VALUE_0_`), t.memberExpression( t.taggedTemplateExpression( t.memberExpression( t.callExpression(t.identifier(`$${uniqueId}$4$SUBSCRIPT$scripts$quine$`), []), t.identifier("split") ), t.templateLiteral([t.templateElement({ raw: "\t", cooked: "\t" }, !0)], []) ), t.identifier(`$${uniqueId}$SPLIT_INDEX$`), !0 ) ) ]) undefinedIsReferenced && variableDeclaration.declarations.push( t.variableDeclarator(t.identifier(`_${uniqueId}_UNDEFINED_`)) ) functionDeclaration.body.body.unshift(variableDeclaration) comment = jsonValues[0] } else { const variableDeclaration = t.variableDeclaration("let", [ t.variableDeclarator( t.arrayPattern(jsonValues.map((_, index) => t.identifier(`_${uniqueId}_JSON_VALUE_${index}_`))), t.callExpression(t.memberExpression(t.identifier("JSON"), t.identifier("parse")), [ t.memberExpression( t.taggedTemplateExpression( t.memberExpression( t.callExpression(t.identifier(`$${uniqueId}$4$SUBSCRIPT$scripts$quine$`), []), t.identifier("split") ), t.templateLiteral([t.templateElement({ raw: "\t", cooked: "\t" }, !0)], []) ), t.identifier(`$${uniqueId}$SPLIT_INDEX$`), !0 ) ]) ) ]) undefinedIsReferenced && variableDeclaration.declarations.push(t.variableDeclarator(t.identifier(`_${uniqueId}_UNDEFINED_`))) functionDeclaration.body.body.unshift(variableDeclaration) comment = JSON.stringify(jsonValues) } } else undefinedIsReferenced && functionDeclaration.body.body.unshift( t.variableDeclaration("let", [t.variableDeclarator(t.identifier(`_${uniqueId}_UNDEFINED_`))]) ) code = generate(file).code } code = ( await terser.minify(code, { ecma: 2015, compress: { passes: 1 / 0, unsafe: !0, unsafe_arrows: !0, unsafe_comps: !0, unsafe_symbols: !0, unsafe_methods: !0, unsafe_proto: !0, unsafe_regexp: !0, unsafe_undefined: !0, sequences: !1 }, format: { semicolons: !1, wrap_func_args: !1 }, keep_classnames: !mangleNames, keep_fnames: !mangleNames }) ).code || "" if (null != comment) { code = spliceString( code, `${autocomplete ? `//${autocomplete}\n` : ""}\n//\t${comment}\t\n`, getFunctionBodyStart(code) + 1 ) code = code.replace( `$${uniqueId}$SPLIT_INDEX$`, await minifyNumber(code.split("\t").findIndex(part => part == comment)) ) } if (1 == forceQuineCheats) return code assert(scriptBeforeJSONValueReplacement, "src/processScript/minify.ts:485:43") return countHackmudCharacters(scriptBeforeJSONValueReplacement) <= countHackmudCharacters(code) + Number(hasComment) ? scriptBeforeJSONValueReplacement : code } function parseObjectExpression(node, o) { if (!node.properties.length) return !1 for (const property of node.properties) { if ("ObjectProperty" != property.type || property.computed) return !1 assert( "Identifier" == property.key.type || "NumericLiteral" == property.key.type || "StringLiteral" == property.key.type, "src/processScript/minify.ts:507:4" ) if ("ArrayExpression" == property.value.type) { const childArray = [] if (property.value.elements.length && !parseArrayExpression(property.value, childArray)) return !1 o["Identifier" == property.key.type ? property.key.name : property.key.value] = childArray } else if ("ObjectExpression" == property.value.type) { const childObject = {} if (property.value.properties.length && !parseObjectExpression(property.value, childObject)) return !1 o["Identifier" == property.key.type ? property.key.name : property.key.value] = childObject } else if ("NullLiteral" == property.value.type) o["Identifier" == property.key.type ? property.key.name : property.key.value] = null else if ( "BooleanLiteral" == property.value.type || "NumericLiteral" == property.value.type || "StringLiteral" == property.value.type ) o["Identifier" == property.key.type ? property.key.name : property.key.value] = property.value.value else { if ("TemplateLiteral" != property.value.type || property.value.expressions.length) return !1 o["Identifier" == property.key.type ? property.key.name : property.key.value] = property.value.quasis[0].value.cooked } } return !0 } function parseArrayExpression(node, o) { if (!node.elements.length) return !1 for (const element of node.elements) { if (!element) return !1 if ("ArrayExpression" == element.type) { const childArray = [] if (element.elements.length && !parseArrayExpression(element, childArray)) return !1 o.push(childArray) } else if ("ObjectExpression" == element.type) { const childObject = {} if (element.properties.length && !parseObjectExpression(element, childObject)) return !1 o.push(childObject) } else if ("NullLiteral" == element.type) o.push(null) else if ( "BooleanLiteral" == element.type || "NumericLiteral" == element.type || "StringLiteral" == element.type ) o.push(element.value) else { if ("TemplateLiteral" != element.type || element.expressions.length) return !1 o.push(element.quasis[0].value.cooked) } } return !0 } function getFunctionBodyStart(code) { const tokens = tokenizer(code, { ecmaVersion: 2015 }) tokens.getToken() tokens.getToken() tokens.getToken() let nests = 1 for (; nests; ) { const token = tokens.getToken() token.type == tokTypes.parenL ? nests++ : token.type == tokTypes.parenR && nests-- } return tokens.getToken().start } export { minify }