hackmud-script-manager
Version:
Script manager for game hackmud, with minification, TypeScript support, and player script type definition generation.
338 lines (337 loc) • 13.2 kB
JavaScript
import babelGenerator from "@babel/generator"
import { parse } from "@babel/parser"
import babelPluginProposalDecorators from "@babel/plugin-proposal-decorators"
import babelPluginProposalDestructuringPrivate from "@babel/plugin-proposal-destructuring-private"
import babelPluginProposalExplicitResourceManagement from "@babel/plugin-proposal-explicit-resource-management"
import babelPluginTransformClassProperties from "@babel/plugin-transform-class-properties"
import babelPluginTransformClassStaticBlock from "@babel/plugin-transform-class-static-block"
import babelPluginTransformExponentiationOperator from "@babel/plugin-transform-exponentiation-operator"
import babelPluginTransformJsonStrings from "@babel/plugin-transform-json-strings"
import babelPluginTransformLogicalAssignmentOperators from "@babel/plugin-transform-logical-assignment-operators"
import babelPluginTransformNullishCoalescingOperator from "@babel/plugin-transform-nullish-coalescing-operator"
import babelPluginTransformNumericSeparator from "@babel/plugin-transform-numeric-separator"
import babelPluginTransformObjectRestSpread from "@babel/plugin-transform-object-rest-spread"
import babelPluginTransformOptionalCatchBinding from "@babel/plugin-transform-optional-catch-binding"
import babelPluginTransformOptionalChaining from "@babel/plugin-transform-optional-chaining"
import babelPluginTransformPrivatePropertyInObject from "@babel/plugin-transform-private-property-in-object"
import babelPluginTransformUnicodeSetsRegex from "@babel/plugin-transform-unicode-sets-regex"
import babelTraverse from "@babel/traverse"
import t from "@babel/types"
import rollupPluginAlias from "@rollup/plugin-alias"
import { babel } from "@rollup/plugin-babel"
import rollupPluginCommonJS from "@rollup/plugin-commonjs"
import rollupPluginJSON from "@rollup/plugin-json"
import rollupPluginNodeResolve from "@rollup/plugin-node-resolve"
import { assert } from "@samual/lib/assert"
import { relative, isAbsolute, sep } from "path"
import prettier from "prettier"
import { rollup } from "rollup"
import { supportedExtensions } from "../constants.js"
import { minify } from "./minify.js"
import { postprocess } from "./postprocess.js"
import { preprocess } from "./preprocess.js"
import { getReferencePathsToGlobal, includesIllegalString, replaceUnsafeStrings } from "./shared.js"
import { transform } from "./transform.js"
import "@samual/lib/countHackmudCharacters"
import "@samual/lib/spliceString"
import "acorn"
import "terser"
import "import-meta-resolve"
import "@samual/lib/clearObject"
const { format } = prettier,
{ default: generate } = babelGenerator,
{ default: traverse } = babelTraverse
async function processScript(
code,
{
minify: shouldMinify = !0,
uniqueId = Math.floor(Math.random() * 2 ** 52)
.toString(36)
.padStart(11, "0"),
scriptUser,
scriptName,
filePath,
mangleNames = !1,
forceQuineCheats,
rootFolderPath
}
) {
assert(/^\w{11}$/.exec(uniqueId), "src/processScript/index.ts:81:36")
const sourceCode = code
let autocomplete, statedSeclevel
const autocompleteMatch = /^function\s*\(.+\/\/(?<autocomplete>.+)/.exec(code)
if (autocompleteMatch) {
code = "export default " + code
;({ autocomplete } = autocompleteMatch.groups)
} else
for (const line of code.split("\n")) {
const comment = /^\s*\/\/(?<commentContent>.+)/.exec(line)
if (!comment) break
const commentContent = comment.groups.commentContent.trim()
if (commentContent.startsWith("@autocomplete ")) autocomplete = commentContent.slice(14).trimStart()
else if (commentContent.startsWith("@seclevel ")) {
const seclevelString = commentContent.slice(10).trimStart().toLowerCase()
switch (seclevelString) {
case "fullsec":
case "full":
case "fs":
case "4s":
case "f":
case "4":
statedSeclevel = 4
break
case "highsec":
case "high":
case "hs":
case "3s":
case "h":
case "3":
statedSeclevel = 3
break
case "midsec":
case "mid":
case "ms":
case "2s":
case "m":
case "2":
statedSeclevel = 2
break
case "lowsec":
case "low":
case "ls":
case "1s":
case "l":
case "1":
statedSeclevel = 1
break
case "nullsec":
case "null":
case "ns":
case "0s":
case "n":
case "0":
statedSeclevel = 0
break
default:
throw Error(`unrecognised seclevel "${seclevelString}"`)
}
}
}
assert(/^\w{11}$/.exec(uniqueId), "src/processScript/index.ts:162:36")
const plugins = [
[babelPluginProposalDecorators.default, { decoratorsBeforeExport: !0 }],
[babelPluginTransformClassProperties.default],
[babelPluginTransformClassStaticBlock.default],
[babelPluginTransformPrivatePropertyInObject.default],
[babelPluginTransformLogicalAssignmentOperators.default],
[babelPluginTransformNumericSeparator.default],
[babelPluginTransformNullishCoalescingOperator.default],
[babelPluginTransformOptionalChaining.default],
[babelPluginTransformOptionalCatchBinding.default],
[babelPluginTransformJsonStrings.default],
[babelPluginTransformObjectRestSpread.default],
[babelPluginTransformExponentiationOperator.default],
[babelPluginTransformUnicodeSetsRegex.default],
[babelPluginProposalDestructuringPrivate.default],
[babelPluginProposalExplicitResourceManagement.default]
]
let filePathResolved
if (filePath) {
filePathResolved = relative(".", filePath)
if (filePath.endsWith(".ts"))
plugins.push([
(await import("@babel/plugin-transform-typescript")).default,
{ allowDeclareFields: !0, optimizeConstEnums: !0 }
])
else {
const [
babelPluginProposalDoExpressions,
babelPluginProposalFunctionBind,
babelPluginProposalFunctionSent,
babelPluginProposalPartialApplication,
babelPluginProposalPipelineOperator,
babelPluginProposalThrowExpressions,
babelPluginProposalRecordAndTuple
] = await Promise.all([
import("@babel/plugin-proposal-do-expressions"),
import("@babel/plugin-proposal-function-bind"),
import("@babel/plugin-proposal-function-sent"),
import("@babel/plugin-proposal-partial-application"),
import("@babel/plugin-proposal-pipeline-operator"),
import("@babel/plugin-proposal-throw-expressions"),
import("@babel/plugin-proposal-record-and-tuple")
])
plugins.push(
[babelPluginProposalDoExpressions.default],
[babelPluginProposalFunctionBind.default],
[babelPluginProposalFunctionSent.default],
[babelPluginProposalPartialApplication.default],
[babelPluginProposalPipelineOperator.default, { proposal: "hack", topicToken: "%" }],
[babelPluginProposalThrowExpressions.default],
[babelPluginProposalRecordAndTuple.default, { syntaxType: "hash", importPolyfill: !0 }]
)
}
} else {
filePathResolved = uniqueId + ".ts"
const [
babelPluginTransformTypescript,
babelPluginProposalDoExpressions,
babelPluginProposalFunctionBind,
babelPluginProposalFunctionSent,
babelPluginProposalPartialApplication,
babelPluginProposalPipelineOperator,
babelPluginProposalThrowExpressions,
babelPluginProposalRecordAndTuple
] = await Promise.all([
import("@babel/plugin-transform-typescript"),
import("@babel/plugin-proposal-do-expressions"),
import("@babel/plugin-proposal-function-bind"),
import("@babel/plugin-proposal-function-sent"),
import("@babel/plugin-proposal-partial-application"),
import("@babel/plugin-proposal-pipeline-operator"),
import("@babel/plugin-proposal-throw-expressions"),
import("@babel/plugin-proposal-record-and-tuple")
])
plugins.push(
[babelPluginTransformTypescript.default, { allowDeclareFields: !0, optimizeConstEnums: !0 }],
[babelPluginProposalDoExpressions.default],
[babelPluginProposalFunctionBind.default],
[babelPluginProposalFunctionSent.default],
[babelPluginProposalPartialApplication.default],
[babelPluginProposalPipelineOperator.default, { proposal: "hack", topicToken: "%" }],
[babelPluginProposalThrowExpressions.default],
[babelPluginProposalRecordAndTuple.default, { syntaxType: "hash", importPolyfill: !0 }]
)
}
const bundle = await rollup({
input: filePathResolved,
plugins: [
rollupPluginJSON({ preferConst: !0 }),
{
name: "hackmud-script-manager",
async transform(code, id) {
if (isAbsolute(id) && !id.includes(`${sep}node_modules${sep}`))
return (await preprocess(code, { uniqueId })).code
let program
traverse(parse(code, { sourceType: "module" }), {
Program(path) {
program = path
path.skip()
}
})
for (const referencePath of getReferencePathsToGlobal("JSON", program))
"MemberExpression" == referencePath.parentPath.node.type &&
"Identifier" == referencePath.parentPath.node.property.type &&
("parse" == referencePath.parentPath.node.property.name
? (referencePath.parentPath.node.property.name = "oparse")
: "stringify" == referencePath.parentPath.node.property.name &&
(referencePath.parentPath.node.property.name = "ostringify"))
return generate(program.node).code
}
},
babel({ babelHelpers: "bundled", plugins, configFile: !1, extensions: supportedExtensions }),
rollupPluginCommonJS(),
rollupPluginNodeResolve({ extensions: supportedExtensions }),
!!rootFolderPath && rollupPluginAlias({ entries: [{ find: /^\//, replacement: rootFolderPath + "/" }] })
],
treeshake: { moduleSideEffects: !1 }
}),
seclevelNames = ["NULLSEC", "LOWSEC", "MIDSEC", "HIGHSEC", "FULLSEC"]
code = (await bundle.generate({})).output[0].code
const { file, seclevel, warnings } = transform(parse(code, { sourceType: "module" }), sourceCode, {
uniqueId,
scriptUser,
scriptName
})
if (null != statedSeclevel && seclevel < statedSeclevel)
throw Error(
`detected seclevel ${seclevelNames[seclevel]} is lower than stated seclevel ${seclevelNames[statedSeclevel]}`
)
code = generate(file).code
if (shouldMinify) code = await minify(file, { uniqueId, mangleNames, forceQuineCheats, autocomplete })
else {
traverse(file, {
MemberExpression({ node: memberExpression }) {
if (!memberExpression.computed) {
assert("Identifier" == memberExpression.property.type, "src/processScript/index.ts:326:60")
if ("prototype" == memberExpression.property.name) {
memberExpression.computed = !0
memberExpression.property = t.stringLiteral("prototype")
} else if ("__proto__" == memberExpression.property.name) {
memberExpression.computed = !0
memberExpression.property = t.stringLiteral("__proto__")
} else if (includesIllegalString(memberExpression.property.name)) {
memberExpression.computed = !0
memberExpression.property = t.stringLiteral(
replaceUnsafeStrings(uniqueId, memberExpression.property.name)
)
}
}
},
VariableDeclarator(path) {
const renameVariables = lValue => {
switch (lValue.type) {
case "Identifier":
includesIllegalString(lValue.name) &&
path.scope.rename(
lValue.name,
"$" +
Math.floor(Math.random() * 2 ** 52)
.toString(36)
.padStart(11, "0")
)
break
case "ObjectPattern":
for (const property of lValue.properties) {
assert("ObjectProperty" == property.type, "src/processScript/index.ts:356:51")
renameVariables(property.value)
}
break
case "ArrayPattern":
for (const element of lValue.elements) element && renameVariables(element)
break
default:
throw Error(`unknown lValue type "${lValue.type}"`)
}
}
renameVariables(path.node.id)
},
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
}
})
code = await format(generate(file, { comments: !1 }).code, {
parser: "babel",
arrowParens: "avoid",
semi: !1,
trailingComma: "none"
})
}
code = postprocess(code, uniqueId)
if (includesIllegalString(code))
throw Error(
'you found a weird edge case where I wasn\'t able to replace illegal strings like "SC$", please report thx'
)
return { script: code, warnings }
}
export { minify, postprocess, preprocess, processScript, transform }