hackmud-script-manager
Version:
Script manager for game hackmud, with minification, TypeScript support, and player script type definition generation.
221 lines (220 loc) • 7.94 kB
JavaScript
import { AutoMap } from "@samual/lib/AutoMap"
import { assert } from "@samual/lib/assert"
import { countHackmudCharacters } from "@samual/lib/countHackmudCharacters"
import { readDirectoryWithStats } from "@samual/lib/readDirectoryWithStats"
import { writeFilePersistent } from "@samual/lib/writeFilePersistent"
import { watch as watch$1 } from "chokidar"
import { stat, readFile, writeFile } from "fs/promises"
import { extname, basename, resolve } from "path"
import { supportedExtensions } from "./constants.js"
import { generateTypeDeclaration } from "./generateTypeDeclaration.js"
import { processScript } from "./processScript/index.js"
import "@babel/generator"
import "@babel/parser"
import "@babel/plugin-proposal-decorators"
import "@babel/plugin-proposal-destructuring-private"
import "@babel/plugin-proposal-explicit-resource-management"
import "@babel/plugin-transform-class-properties"
import "@babel/plugin-transform-class-static-block"
import "@babel/plugin-transform-exponentiation-operator"
import "@babel/plugin-transform-json-strings"
import "@babel/plugin-transform-logical-assignment-operators"
import "@babel/plugin-transform-nullish-coalescing-operator"
import "@babel/plugin-transform-numeric-separator"
import "@babel/plugin-transform-object-rest-spread"
import "@babel/plugin-transform-optional-catch-binding"
import "@babel/plugin-transform-optional-chaining"
import "@babel/plugin-transform-private-property-in-object"
import "@babel/plugin-transform-unicode-sets-regex"
import "@babel/traverse"
import "@babel/types"
import "@rollup/plugin-alias"
import "@rollup/plugin-babel"
import "@rollup/plugin-commonjs"
import "@rollup/plugin-json"
import "@rollup/plugin-node-resolve"
import "prettier"
import "rollup"
import "./processScript/minify.js"
import "@samual/lib/spliceString"
import "acorn"
import "terser"
import "./processScript/shared.js"
import "./processScript/postprocess.js"
import "./processScript/preprocess.js"
import "import-meta-resolve"
import "./processScript/transform.js"
import "@samual/lib/clearObject"
async function watch(
sourceDirectory,
hackmudDirectory,
{
scripts = ["*.*"],
onPush,
minify = !0,
mangleNames = !1,
typeDeclarationPath: typeDeclarationPath_,
onReady,
forceQuineCheats,
rootFolderPath
} = {}
) {
if (!scripts.length) throw Error("scripts option was an empty array")
if (!(await stat(sourceDirectory)).isDirectory()) throw Error("Target folder must be a folder")
const scriptNamesToUsers = new AutoMap(_scriptName => new Set()),
wildScriptUsers = new Set(),
wildUserScripts = new Set()
let pushEverything = !1
for (const fullScriptName of scripts) {
const [user, scriptName] = fullScriptName.split(".")
user && "*" != user
? scriptName && "*" != scriptName
? scriptNamesToUsers.get(scriptName).add(user)
: wildScriptUsers.add(user)
: scriptName && "*" != scriptName
? wildUserScripts.add(scriptName)
: (pushEverything = !0)
}
const watcher = watch$1(".", {
cwd: sourceDirectory,
awaitWriteFinish: { stabilityThreshold: 100 },
ignored: (path, stats) =>
!!stats?.isFile() && !(path.endsWith(".js") || (path.endsWith(".ts") && !path.endsWith(".d.ts")))
}).on("change", async path => {
if (path.endsWith(".d.ts")) return
const extension = extname(path)
if (!supportedExtensions.includes(extension)) return
const scriptName = basename(path, extension)
if (path == basename(path)) {
if (
!(
pushEverything ||
wildScriptUsers.size ||
wildUserScripts.has(scriptName) ||
scriptNamesToUsers.has(scriptName)
)
)
return
const scriptNamesToUsersToSkip = new AutoMap(_scriptName => [])
await Promise.all(
(await readDirectoryWithStats(sourceDirectory)).map(async ({ stats, name, path }) => {
if (stats.isDirectory())
for (const child of await readDirectoryWithStats(path))
if (child.stats.isFile()) {
const fileExtension = extname(child.name)
supportedExtensions.includes(fileExtension) &&
scriptNamesToUsersToSkip.get(basename(child.name, fileExtension)).push(name)
}
})
)
const usersToPushToSet = new Set()
if (pushEverything || wildUserScripts.has(scriptName)) {
for (const { stats, name } of await readDirectoryWithStats(sourceDirectory))
stats.isDirectory() && usersToPushToSet.add(name)
for (const { stats, name } of await readDirectoryWithStats(hackmudDirectory))
stats.isDirectory()
? usersToPushToSet.add(name)
: stats.isFile() && name.endsWith(".key") && usersToPushToSet.add(name.slice(0, -4))
for (const users of scriptNamesToUsers.values()) for (const user of users) usersToPushToSet.add(user)
}
for (const user of wildScriptUsers) usersToPushToSet.add(user)
for (const user of scriptNamesToUsers.get(scriptName)) usersToPushToSet.add(user)
const usersToPushTo = [...usersToPushToSet].filter(user => !scriptNamesToUsersToSkip.has(user))
if (!usersToPushTo.length) {
onPush?.({ path, users: [], characterCount: 0, error: Error("no users to push to"), warnings: [] })
return
}
const uniqueId = Math.floor(Math.random() * 2 ** 52)
.toString(36)
.padStart(11, "0"),
filePath = resolve(sourceDirectory, path)
let minifiedCode, warnings
try {
;({ script: minifiedCode, warnings } = await processScript(
await readFile(filePath, { encoding: "utf8" }),
{
minify,
scriptUser: !0,
scriptName,
uniqueId,
filePath,
mangleNames,
forceQuineCheats,
rootFolderPath
}
))
} catch (error) {
assert(error instanceof Error, "src/watch.ts:160:36")
onPush?.({ path, users: [], characterCount: 0, error, warnings: [] })
return
}
await Promise.all(
usersToPushTo.map(user =>
writeFilePersistent(
resolve(hackmudDirectory, user, `scripts/${scriptName}.js`),
minifiedCode
.replace(RegExp(`\\$${uniqueId}\\$SCRIPT_USER\\$`, "g"), user)
.replace(RegExp(`\\$${uniqueId}\\$FULL_SCRIPT_NAME\\$`, "g"), `${user}.${scriptName}`)
)
)
)
onPush?.({
path,
users: usersToPushTo,
characterCount: countHackmudCharacters(minifiedCode),
error: void 0,
warnings
})
return
}
const user = basename(resolve(path, ".."))
if (
!(
pushEverything ||
wildScriptUsers.size ||
wildUserScripts.has(scriptName) ||
scriptNamesToUsers.get(scriptName).has(user)
)
)
return
const sourceDirectoryResolved = resolve(sourceDirectory),
filePath = resolve(sourceDirectoryResolved, path),
sourceCode = await readFile(filePath, { encoding: "utf8" })
let script, warnings
try {
;({ script, warnings } = await processScript(sourceCode, {
minify,
scriptUser: user,
scriptName,
filePath,
mangleNames,
forceQuineCheats,
rootFolderPath
}))
} catch (error) {
assert(error instanceof Error, "src/watch.ts:207:35")
onPush?.({ path, users: [], characterCount: 0, error, warnings: [] })
return
}
await writeFilePersistent(resolve(hackmudDirectory, user, "scripts", scriptName + ".js"), script)
onPush?.({ path, users: [user], characterCount: countHackmudCharacters(script), error: void 0, warnings })
})
onReady && watcher.on("ready", onReady)
if (!typeDeclarationPath_) return
let typeDeclarationPath = typeDeclarationPath_
const writeTypeDeclaration = async () => {
const typeDeclaration = await generateTypeDeclaration(sourceDirectory, hackmudDirectory)
try {
await writeFile(typeDeclarationPath, typeDeclaration)
} catch (error) {
assert(error instanceof Error, "src/watch.ts:240:35")
if ("EISDIR" != error.code) throw error
typeDeclarationPath = resolve(typeDeclarationPath, "player.d.ts")
await writeFile(typeDeclarationPath, typeDeclaration)
}
}
await writeTypeDeclaration()
watcher.on("add", writeTypeDeclaration)
watcher.on("unlink", writeTypeDeclaration)
}
export { watch }