UNPKG

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