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
JavaScript
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 }