From 9e4039e4592b2ccc2ed84490021f1922f42f51b4 Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:59:31 +0100 Subject: [PATCH] Merge JS + enable minification + Lighthouse --- assets/analyzer.js | 489 ------------------------------------------- assets/script.js | 505 ++++++++++++++++++++++++++++++++++++++++++++- assets/style.css | 6 +- eslint.config.js | 4 +- index.html | 1 - minify.js | 50 +++++ package-lock.json | 14 ++ package.json | 6 + serviceworker.js | 3 +- 9 files changed, 574 insertions(+), 504 deletions(-) delete mode 100644 assets/analyzer.js create mode 100644 minify.js diff --git a/assets/analyzer.js b/assets/analyzer.js deleted file mode 100644 index 7ac5171..0000000 --- a/assets/analyzer.js +++ /dev/null @@ -1,489 +0,0 @@ -let interval -let files = 0 -let done = 0 -let error = 0 -let selected = null -let rpMode = false - -let filetypes = {} -let filetypesOther = {} -let packFiles = [] -let packImages = [] -let commands = {} -let cmdsBehindExecute = {} -let cmdsBehindMacros = {} -let cmdsBehindReturn = {} -let comments = 0 -let empty = 0 -let emptyFiles = [] -let dpExclusive = { - folders: { - advancements: 0, - loot_tables: 0, - recipes: 0, - predicates: 0, - dimension: 0, - dimension_type: 0, - worldgen: 0 - }, - tags: { - banner_pattern: 0, - blocks: 0, - cat_variant: 0, - entity_types: 0, - fluids: 0, - functions: 0, - game_events: 0, - instrument: 0, - items: 0, - painting_variant: 0, - point_of_interest_type: 0, - worldgen: 0 - }, - scoreboards: 0, - selectors: { - a: 0, - e: 0, - p: 0, - r: 0, - s: 0 - }, - functions: ["#minecraft:load", "#minecraft:tick"], - functionCalls: [{target: "#minecraft:load"}, {target: "#minecraft:tick"}] -} -let rpExclusive = { - atlases: 0, - blockstates: 0, - font: 0, - lang: 0, - models: 0, - particles: 0, - shaders: 0, - sounds: 0, - texts: 0, - textures: 0 -} - -async function processEntries(entries) { - for await (const entry of entries) { - const filePath = entry.webkitRelativePath || entry.name - if (filePath.includes("/.git/") || filePath.includes("/.svn/")) continue - if (entry.kind == "directory") { - processEntries(entry) - continue - } - - const ext = entry.name.split(".").pop() - if ( - ext == "mcmeta" || ext == "json" || - (!rpMode && (ext == "mcfunction" || ext == "nbt")) || - (rpMode && (ext == "png" || ext == "icns" || ext == "txt" || ext == "ogg" || ext == "fsh" || ext == "vsh" || ext == "glsl" || ext == "lang" || ext == "properties" || ext == "inc" || ext == "xcf")) - ) { - if (filetypes[ext]) filetypes[ext]++ - else filetypes[ext] = 1 - } else { - if (filetypesOther[(entry.name.includes(".") ? "." : "") + ext]) filetypesOther[(entry.name.includes(".") ? "." : "") + ext]++ - else filetypesOther[(entry.name.includes(".") ? "." : "") + ext] = 1 - } - - if ( - ext == "mcfunction" || ext == "mcmeta" || (!rpMode && ext == "json" && (filePath.includes("/advancements/") || filePath.includes("/tags/functions/"))) || - ext == "fsh" || ext == "vsh" || ext == "glsl" || entry.name.endsWith("pack.png") - ) { - files++ - - const processFile = result => { - done++ - if (result.trim() == "") return emptyFiles.push(filePath) - - if (!rpMode && ext == "mcfunction") { - const fileLocation = /data\/([-a-z0-9_.]+)\/functions\/([-a-z0-9_./]+)\.mcfunction/i.exec(filePath) - if (fileLocation && !dpExclusive.functions.includes(fileLocation[1] + ":" + fileLocation[2])) dpExclusive.functions.push(fileLocation[1] + ":" + fileLocation[2]) - - const lines = result.split("\n") - for (let line of lines) { - line = line.trim() - if (line.startsWith("#")) comments++ - if (line == "") empty++ - if (line.startsWith("#") || line == "") continue - const splitted = line.split(" ") - - let cmd = splitted[0] - if (cmd.startsWith("$")) { - cmd = cmd.slice(1) - if (cmdsBehindMacros[cmd]) cmdsBehindMacros[cmd]++ - else cmdsBehindMacros[cmd] = 1 - } - - if (commands[cmd]) commands[cmd]++ - else commands[cmd] = 1 - - if (cmd == "execute") { - const matches = / run ([a-z_:]{2,})/g.exec(line) - if (matches) matches.forEach(match => { - const cmdBehind = match.replace("run ", "").trim() - - if (cmdsBehindExecute[cmdBehind]) cmdsBehindExecute[cmdBehind]++ - else cmdsBehindExecute[cmdBehind] = 1 - if (commands[cmdBehind]) commands[cmdBehind]++ - else commands[cmdBehind] = 1 - - if (cmdBehind == "return") { - const returnCmd = / run return run ([a-z_:]{2,})/g.exec(line) - if (returnCmd && returnCmd[1]) { - if (cmdsBehindReturn[returnCmd[1]]) cmdsBehindReturn[returnCmd[1]]++ - else cmdsBehindReturn[returnCmd[1]] = 1 - } - } - }) - } else if (cmd == "return") { - const returnCmd = / run return run ([a-z_:]{2,})/g.exec(line) - if (returnCmd && returnCmd[1]) { - if (cmdsBehindReturn[returnCmd[1]]) cmdsBehindReturn[returnCmd[1]]++ - else cmdsBehindReturn[returnCmd[1]] = 1 - } - } - if (fileLocation && (cmd == "function" || line.includes(" function ") || line.includes("/function "))) { - const func = /function ((#?[-a-z0-9_.]+):)?([-a-z0-9_./]+)/i.exec(line) - if (func && func[3]) dpExclusive.functionCalls.push({ - source: fileLocation[1] + ":" + fileLocation[2], - target: (func[2] || "minecraft") + ":" + func[3] - }) - } - - if (/scoreboard objectives add \w+ \w+( .+)?$/.test(line)) dpExclusive.scoreboards++ - - splitted.forEach(arg => { - if (arg.startsWith("@")) { - arg = arg.slice(1) - if (arg.startsWith("a")) dpExclusive.selectors.a++ - else if (arg.startsWith("e")) dpExclusive.selectors.e++ - else if (arg.startsWith("p")) dpExclusive.selectors.p++ - else if (arg.startsWith("r")) dpExclusive.selectors.r++ - else if (arg.startsWith("s")) dpExclusive.selectors.s++ - } - }) - } - } else if (ext == "mcmeta") { - if (entry.name == "pack.mcmeta") { - try { - packFiles.push(JSON.parse(result)) - } catch (e) { - console.warn("Could not parse pack.mcmeta: " + filePath, e) - error++ - } - } - } else if (entry.name.endsWith("pack.png") && !result.includes(">")) packImages.push(result) - else if (rpMode && (ext == "fsh" || ext == "vsh" || ext == "glsl")) { - const lines = result.split("\n") - for (let line of lines) { - line = line.trim() - if (line.startsWith("//") || line.startsWith("/*")) comments++ - if (line == "") empty++ - if (line.startsWith("//") || line.startsWith("/*") || line == "") continue - - const cmd = line.match(/^[a-z_#0-9]+/i)?.[0] - if (cmd && cmd != "{" && cmd != "}") { - if (commands[cmd]) commands[cmd]++ - else commands[cmd] = 1 - } - } - } else if (!rpMode && ext == "json") { - if (filePath.includes("/advancements/")) { - const fileLocation = /data\/([-a-z0-9_.]+)\/advancements\/([-a-z0-9_./]+)\.json/i.exec(filePath) - - try { - const parsed = JSON.parse(result) - if (parsed.rewards && parsed.rewards.function) dpExclusive.functionCalls.push({ - source: "(Advancement) " + fileLocation[1] + ":" + fileLocation[2], - target: parsed.rewards.function.includes(":") ? parsed.rewards.function : "minecraft:" + parsed.rewards.function - }) - } catch (e) { - console.warn("Unable to analyze advancement: " + filePath, e) - } - } else if (filePath.includes("/tags/functions/")) { - const fileLocation = /data\/([-a-z0-9_.]+)\/tags\/functions\/([-a-z0-9_./]+)\.json/i.exec(filePath) - if (fileLocation && !dpExclusive.functions.includes("#" + fileLocation[1] + ":" + fileLocation[2])) dpExclusive.functions.push("#" + fileLocation[1] + ":" + fileLocation[2]) - - try { - const parsed = JSON.parse(result) - if (parsed.values) parsed.values.forEach(func => { - if (typeof func == "object") { - if (func.required === false) return - func = func.id - } - - dpExclusive.functionCalls.push({ - source: "#" + fileLocation[1] + ":" + fileLocation[2], - target: func.includes(":") ? func : "minecraft:" + func - }) - }) - } catch (e) { - console.warn("Unable to analyze function tag: " + filePath, e) - } - } - } - } - - if ("content" in entry) processFile(entry.content) - else { - const reader = new FileReader() - if (ext == "png") reader.readAsDataURL(entry) - else entry.text().then(processFile) - - reader.onload = () => { - processFile(reader.result) - } - reader.onerror = e => { - console.warn("Could not read file: " + filePath, e) - error++ - } - } - } - if (!rpMode && ext == "json") { - Object.keys(dpExclusive.folders).forEach(type => { - if (filePath.includes("/" + type + "/")) dpExclusive.folders[type]++ - }) - Object.keys(dpExclusive.tags).forEach(type => { - if (filePath.includes("/tags/" + type + "/")) dpExclusive.tags[type]++ - }) - } else if (rpMode) - Object.keys(rpExclusive).forEach(type => { - if (filePath.includes("/" + type + "/")) rpExclusive[type]++ - }) - } -} - -async function mainScan(hasData = false) { - if (interval) clearInterval(interval) - document.getElementById("result").innerHTML = "" - - if (!hasData) { - files = 0 - done = 0 - error = 0 - rpMode = document.getElementById("radiorp").checked - - filetypes = {} - filetypesOther = {} - packFiles = [] - packImages = [] - commands = {} - cmdsBehindExecute = {} - cmdsBehindMacros = {} - cmdsBehindReturn = {} - comments = 0 - empty = 0 - emptyFiles = [] - dpExclusive = { - folders: { - advancements: 0, - loot_tables: 0, - recipes: 0, - predicates: 0, - dimension: 0, - dimension_type: 0, - worldgen: 0 - }, - tags: { - banner_pattern: 0, - blocks: 0, - cat_variant: 0, - entity_types: 0, - fluids: 0, - functions: 0, - game_events: 0, - instrument: 0, - items: 0, - painting_variant: 0, - point_of_interest_type: 0, - worldgen: 0 - }, - scoreboards: 0, - selectors: { - a: 0, - e: 0, - p: 0, - r: 0, - s: 0 - }, - functions: ["#minecraft:load", "#minecraft:tick"], - functionCalls: [{target: "#minecraft:load"}, {target: "#minecraft:tick"}] - } - rpExclusive = { - atlases: 0, - blockstates: 0, - font: 0, - lang: 0, - models: 0, - particles: 0, - shaders: 0, - sounds: 0, - texts: 0, - textures: 0 - } - - processEntries(selected) - } - - interval = setInterval(() => { - document.getElementById("progress").innerText = Math.round(done / files * 100) + "% scanned" + (error > 0 ? " - " + error + " errors" : "") - if (done + error == files || hasData) { - clearInterval(interval) - if (files == 0) return document.getElementById("progress").innerText = "No files found!" - document.getElementById("resultButtons").hidden = false - if (error == 0) document.getElementById("progress").innerText = "" - if (Object.values(filetypes).reduce((a, b) => a + b) == 0) document.getElementById("progress").innerHTML = "No " + (rpMode ? "resource" : "data") + "pack files found!" - - const uncalledFunctions = dpExclusive.functions.filter(funcName => !dpExclusive.functionCalls.some(func => func.target == funcName)) - const missingFunctions = [...new Set(dpExclusive.functionCalls.filter(func => !dpExclusive.functions.includes(func.target)).map(func => func.target))] - - let html = - (packImages.length > 0 ? "
" + packImages.map(img => "") + "
" : "") + - (packFiles.length > 0 ? "" + (rpMode ? "Resource" : "Data") + "pack" + (packFiles.length == 1 ? "" : "s") + " found:
" + - packFiles.map(pack => { - let oldestFormat = pack.pack.pack_format - let newestFormat = pack.pack.pack_format - if (pack.pack.supported_formats && typeof pack.pack.supported_formats == "object") { - if (Array.isArray(pack.pack.supported_formats)) { - oldestFormat = pack.pack.supported_formats[0] - newestFormat = pack.pack.supported_formats[1] - } else { - oldestFormat = pack.pack.supported_formats.min_inclusive - newestFormat = pack.pack.supported_formats.max_inclusive - } - } - - let description = "" - if (pack.pack && pack.pack.description) { - if (typeof pack.pack.description == "object") { - const desc = Array.isArray(pack.pack.description) ? pack.pack.description : [pack.pack.description] - desc.forEach(d => { - if (d.text || d.translation) description += d.text || d.translation - }) - } else description = pack.pack.description - } else description = "No description" - - return "" + description.replace(/§[0-9a-flmnor]/gi, "") + - (window.versions.some(ver => (rpMode ? ver.resourcepack_version : ver.datapack_version) == pack.pack.pack_format) ? - "
Supported versions: " + - (window.versions.findLast(ver => (rpMode ? ver.resourcepack_version : ver.datapack_version) == oldestFormat)?.name || "?") + - " - " + - (window.versions.find(ver => (rpMode ? ver.resourcepack_version : ver.datapack_version) == newestFormat)?.name || "?") + - "" - : "") + - "
" + - (pack.features?.enabled?.length > 0 ? - "
Selected internal features: " + - pack.features.enabled.map(feature => "" + feature + "").join(", ") + "" - : "") + - (pack.filter?.block?.length > 0 ? "
Pack filters:
" + pack.filter.block.map(filter => - "" + - (filter.namespace ? "Namespace: " + filter.namespace + "" : "") + - (filter.namespace && filter.path ? ", " : "") + - (filter.path ? "Path: " + filter.path + "" : "") + - "" - ).join("
") + "
" : "") - }).join("
") + "
" - : "") + - (packFiles.length == 0 && (filetypes.fsh || filetypes.vsh || filetypes.xcf || filetypes.glsl) ? "Shader found
" : "") + - - (Object.keys(commands).length > 0 ? - "Total amount of commands: " + localize(Object.keys(commands).reduce((a, b) => a + commands[b], 0)) + "
" + - "Unique command names: " + localize(Object.keys(commands).length) + "
" - : "") + - (comments > 0 ? "Comments: " + localize(comments) + "
" : "") + - (empty > 0 ? "Empty lines: " + localize(empty) + "
" : "") + - "Pack file types found:
" + - Object.keys(filetypes).sort((a, b) => filetypes[b] - filetypes[a]).map(type => "." + type + ": " + localize(filetypes[type]) + "
").join("") + - (Object.keys(filetypesOther).length > 0 ? - "
" + - "Non-pack file types found:" + - Object.keys(filetypesOther).sort((a, b) => filetypesOther[b] - filetypesOther[a]).map(type => "" + type + ": " + localize(filetypesOther[type]) + "
").join("") + - "

" - : "") + - (uncalledFunctions.length > 0 ? - "Uncalled functions:
" + - uncalledFunctions.map(func => "" + func + "
").join("") + - "
" - : "") + - (missingFunctions.length > 0 ? - "Missing functions:
" + - missingFunctions.map(func => "" + func + "
").join("") + - "
" - : "") + - (emptyFiles.length > 0 ? - "Empty files:
" + - emptyFiles.map(func => "" + func + "
").join("") + - "
" - : "") + - - (dpExclusive.scoreboards > 0 ? "Scoreboards created: " + localize(dpExclusive.scoreboards) + "
" : "") + - (!rpMode && Object.values(dpExclusive.selectors).reduce((a, b) => a + b) != 0 ? "Selectors used:
" : "") + - Object.keys(dpExclusive.selectors).filter(i => dpExclusive.selectors[i] > 0).sort((a, b) => dpExclusive.selectors[b] - dpExclusive.selectors[a]) - .map(type => "@" + type + ": " + localize(dpExclusive.selectors[type]) + "
").join("") + - (!rpMode && Object.values(dpExclusive.folders).reduce((a, b) => a + b) != 0 ? "Data pack features used:
" : "") + - Object.keys(dpExclusive.folders).filter(i => dpExclusive.folders[i] > 0).sort((a, b) => dpExclusive.folders[b] - dpExclusive.folders[a]) - .map(type => "" + type + ": " + localize(dpExclusive.folders[type]) + "
").join("") + - (!rpMode && Object.values(dpExclusive.tags).reduce((a, b) => a + b) != 0 ? "Tags used:
" : "") + - Object.keys(dpExclusive.tags).filter(i => dpExclusive.tags[i] > 0).sort((a, b) => dpExclusive.tags[b] - dpExclusive.tags[a]) - .map(type => "" + type + ": " + localize(dpExclusive.tags[type]) + "
").join("") + - - (rpMode && Object.values(rpExclusive).reduce((a, b) => a + b) != 0 ? "
Resource pack features used:
" : "") + - Object.keys(rpExclusive).filter(i => rpExclusive[i] > 0).sort((a, b) => rpExclusive[b] - rpExclusive[a]) - .map(type => "" + type + ": " + localize(rpExclusive[type]) + "
").join("") - - html += "
" - commands = Object.fromEntries(Object.entries(commands).sort(([, a], [, b]) => b - a)) - Object.keys(commands).forEach(cmd => { - html += cmd + ": " + localize(commands[cmd]) + "
" - if (cmdsBehindExecute[cmd]) html += "Behind execute: " + localize(cmdsBehindExecute[cmd]) + - (cmd == "execute" ? "⚠️ (... run execute ... equals ... ...)" : "") + "
" - if (cmdsBehindMacros[cmd]) html += "Behind macro: " + localize(cmdsBehindMacros[cmd]) + "
" - if (cmdsBehindReturn[cmd]) html += "Behind return: " + localize(cmdsBehindReturn[cmd]) + "
" - }) - document.getElementById("result").innerHTML = html - } - }, 100) -} - -async function selectFolder() { - if (interval) clearInterval(interval) - selected = null - - const input = document.createElement("input") - input.type = "file" - input.webkitdirectory = true - input.onchange = e => { - selected = e.target.files - mainScan() - } - if ("showPicker" in HTMLInputElement.prototype) input.showPicker() - else input.click() -} - -async function selectZip() { - if (interval) clearInterval(interval) - - const input = document.createElement("input") - input.type = "file" - input.accept = ".zip" - input.onchange = e => handleZip(e.target.files[0]) - - if ("showPicker" in HTMLInputElement.prototype) input.showPicker() - else input.click() -} - -function handleZip(file) { - selected = [] - - new JSZip().loadAsync(file).then(async zip => { - for await (const zipFile of Object.values(zip.files)) { - selected.push({ - name: zipFile.name, - content: await zipFile.async("text") - }) - } - mainScan() - }) -} diff --git a/assets/script.js b/assets/script.js index 566f3de..372765b 100644 --- a/assets/script.js +++ b/assets/script.js @@ -25,10 +25,76 @@ const requestVersions = async () => { } requestVersions() +let interval +let files = 0 +let done = 0 +let error = 0 +let selected = null +let rpMode = false + +let filetypes = {} +let filetypesOther = {} +let packFiles = [] +let packImages = [] +let commands = {} +let cmdsBehindExecute = {} +let cmdsBehindMacros = {} +let cmdsBehindReturn = {} +let comments = 0 +let empty = 0 +let emptyFiles = [] +let dpExclusive = { + folders: { + advancements: 0, + loot_tables: 0, + recipes: 0, + predicates: 0, + dimension: 0, + dimension_type: 0, + worldgen: 0 + }, + tags: { + banner_pattern: 0, + blocks: 0, + cat_variant: 0, + entity_types: 0, + fluids: 0, + functions: 0, + game_events: 0, + instrument: 0, + items: 0, + painting_variant: 0, + point_of_interest_type: 0, + worldgen: 0 + }, + scoreboards: 0, + selectors: { + a: 0, + e: 0, + p: 0, + r: 0, + s: 0 + }, + functions: ["#minecraft:load", "#minecraft:tick"], + functionCalls: [{target: "#minecraft:load"}, {target: "#minecraft:tick"}] +} +let rpExclusive = { + atlases: 0, + blockstates: 0, + font: 0, + lang: 0, + models: 0, + particles: 0, + shaders: 0, + sounds: 0, + texts: 0, + textures: 0 +} + window.addEventListener("load", () => { - if (getCookie("theme") == "light") document.body.classList.add("light-theme") + if (getCookie("theme") == "light") document.body.classList.add("light") else if (window.matchMedia("(prefers-color-scheme: light)").matches) { - document.body.classList.add("light-theme") + document.body.classList.add("light") setCookie("theme", "light", 365) } @@ -37,7 +103,7 @@ window.addEventListener("load", () => { const parsed = JSON.parse(params.get("data")) files = parsed.files done = parsed.done - errors = parsed.errors + error = parsed.error rpMode = parsed.rpMode document.getElementById("radiorp").checked = rpMode @@ -117,7 +183,7 @@ function openDialog(dialog) { } const toggleTheme = () => { - const toggled = document.body.classList.toggle("light-theme") + const toggled = document.body.classList.toggle("light") setCookie("theme", toggled ? "light" : "dark", 365) } @@ -152,16 +218,17 @@ async function share(type) { dpExclusive, rpExclusive }, null, type == "json" ? "\t" : void 0) + if (type == "link") { - const name = Math.random().toString(36).slice(7) - const date = Date.now() + 1000 * 60 * 60 * 24 * 7 + const name = Math.random().toString(36).slice(8) + const date = Date.now() + 1000 * 60 * 60 * 24 * 30 const res = await fetch("https://sh0rt.zip", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", - "User-Agent": "TomatoCake / pack-analyzer" + "User-Agent": "TomatoCake Pack-Analyzer" }, body: JSON.stringify({ name, @@ -265,3 +332,427 @@ function createImage() { }) } } + +async function processEntries(entries) { + for await (const entry of entries) { + const filePath = entry.webkitRelativePath || entry.name + if (filePath.includes("/.git/") || filePath.includes("/.svn/")) continue + if (entry.kind == "directory") { + processEntries(entry) + continue + } + + const ext = entry.name.split(".").pop() + if ( + ext == "mcmeta" || ext == "json" || + (!rpMode && (ext == "mcfunction" || ext == "nbt")) || + (rpMode && (ext == "png" || ext == "icns" || ext == "txt" || ext == "ogg" || ext == "fsh" || ext == "vsh" || ext == "glsl" || ext == "lang" || ext == "properties" || ext == "inc" || ext == "xcf")) + ) { + if (filetypes[ext]) filetypes[ext]++ + else filetypes[ext] = 1 + } else { + if (filetypesOther[(entry.name.includes(".") ? "." : "") + ext]) filetypesOther[(entry.name.includes(".") ? "." : "") + ext]++ + else filetypesOther[(entry.name.includes(".") ? "." : "") + ext] = 1 + } + + if ( + ext == "mcfunction" || ext == "mcmeta" || (!rpMode && ext == "json" && (filePath.includes("/advancements/") || filePath.includes("/tags/functions/"))) || + ext == "fsh" || ext == "vsh" || ext == "glsl" || entry.name.endsWith("pack.png") + ) { + files++ + + const processFile = result => { + done++ + if (result.trim() == "") return emptyFiles.push(filePath) + + if (!rpMode && ext == "mcfunction") { + const fileLocation = /data\/([-a-z0-9_.]+)\/functions\/([-a-z0-9_./]+)\.mcfunction/i.exec(filePath) + if (fileLocation && !dpExclusive.functions.includes(fileLocation[1] + ":" + fileLocation[2])) dpExclusive.functions.push(fileLocation[1] + ":" + fileLocation[2]) + + const lines = result.split("\n") + for (let line of lines) { + line = line.trim() + if (line.startsWith("#")) comments++ + if (line == "") empty++ + if (line.startsWith("#") || line == "") continue + const splitted = line.split(" ") + + let cmd = splitted[0] + if (cmd.startsWith("$")) { + cmd = cmd.slice(1) + if (cmdsBehindMacros[cmd]) cmdsBehindMacros[cmd]++ + else cmdsBehindMacros[cmd] = 1 + } + + if (commands[cmd]) commands[cmd]++ + else commands[cmd] = 1 + + if (cmd == "execute") { + const matches = / run ([a-z_:]{2,})/g.exec(line) + if (matches) matches.forEach(match => { + const cmdBehind = match.replace("run ", "").trim() + + if (cmdsBehindExecute[cmdBehind]) cmdsBehindExecute[cmdBehind]++ + else cmdsBehindExecute[cmdBehind] = 1 + if (commands[cmdBehind]) commands[cmdBehind]++ + else commands[cmdBehind] = 1 + + if (cmdBehind == "return") { + const returnCmd = / run return run ([a-z_:]{2,})/g.exec(line) + if (returnCmd && returnCmd[1]) { + if (cmdsBehindReturn[returnCmd[1]]) cmdsBehindReturn[returnCmd[1]]++ + else cmdsBehindReturn[returnCmd[1]] = 1 + } + } + }) + } else if (cmd == "return") { + const returnCmd = / run return run ([a-z_:]{2,})/g.exec(line) + if (returnCmd && returnCmd[1]) { + if (cmdsBehindReturn[returnCmd[1]]) cmdsBehindReturn[returnCmd[1]]++ + else cmdsBehindReturn[returnCmd[1]] = 1 + } + } + if (fileLocation && (cmd == "function" || line.includes(" function ") || line.includes("/function "))) { + const func = /function ((#?[-a-z0-9_.]+):)?([-a-z0-9_./]+)/i.exec(line) + if (func && func[3]) dpExclusive.functionCalls.push({ + source: fileLocation[1] + ":" + fileLocation[2], + target: (func[2] || "minecraft") + ":" + func[3] + }) + } + + if (/scoreboard objectives add \w+ \w+( .+)?$/.test(line)) dpExclusive.scoreboards++ + + splitted.forEach(arg => { + if (arg.startsWith("@")) { + arg = arg.slice(1) + if (arg.startsWith("a")) dpExclusive.selectors.a++ + else if (arg.startsWith("e")) dpExclusive.selectors.e++ + else if (arg.startsWith("p")) dpExclusive.selectors.p++ + else if (arg.startsWith("r")) dpExclusive.selectors.r++ + else if (arg.startsWith("s")) dpExclusive.selectors.s++ + } + }) + } + } else if (ext == "mcmeta") { + if (entry.name == "pack.mcmeta") { + try { + packFiles.push(JSON.parse(result)) + } catch (e) { + console.warn("Could not parse pack.mcmeta: " + filePath, e) + error++ + } + } + } else if (entry.name.endsWith("pack.png") && !result.includes(">")) packImages.push(result) + else if (rpMode && (ext == "fsh" || ext == "vsh" || ext == "glsl")) { + const lines = result.split("\n") + for (let line of lines) { + line = line.trim() + if (line.startsWith("//") || line.startsWith("/*")) comments++ + if (line == "") empty++ + if (line.startsWith("//") || line.startsWith("/*") || line == "") continue + + const cmd = line.match(/^[a-z_#0-9]+/i)?.[0] + if (cmd && cmd != "{" && cmd != "}") { + if (commands[cmd]) commands[cmd]++ + else commands[cmd] = 1 + } + } + } else if (!rpMode && ext == "json") { + if (filePath.includes("/advancements/")) { + const fileLocation = /data\/([-a-z0-9_.]+)\/advancements\/([-a-z0-9_./]+)\.json/i.exec(filePath) + + try { + const parsed = JSON.parse(result) + if (parsed.rewards && parsed.rewards.function) dpExclusive.functionCalls.push({ + source: "(Advancement) " + fileLocation[1] + ":" + fileLocation[2], + target: parsed.rewards.function.includes(":") ? parsed.rewards.function : "minecraft:" + parsed.rewards.function + }) + } catch (e) { + console.warn("Unable to analyze advancement: " + filePath, e) + } + } else if (filePath.includes("/tags/functions/")) { + const fileLocation = /data\/([-a-z0-9_.]+)\/tags\/functions\/([-a-z0-9_./]+)\.json/i.exec(filePath) + if (fileLocation && !dpExclusive.functions.includes("#" + fileLocation[1] + ":" + fileLocation[2])) dpExclusive.functions.push("#" + fileLocation[1] + ":" + fileLocation[2]) + + try { + const parsed = JSON.parse(result) + if (parsed.values) parsed.values.forEach(func => { + if (typeof func == "object") { + if (func.required === false) return + func = func.id + } + + dpExclusive.functionCalls.push({ + source: "#" + fileLocation[1] + ":" + fileLocation[2], + target: func.includes(":") ? func : "minecraft:" + func + }) + }) + } catch (e) { + console.warn("Unable to analyze function tag: " + filePath, e) + } + } + } + } + + if ("content" in entry) processFile(entry.content) + else { + const reader = new FileReader() + if (ext == "png") reader.readAsDataURL(entry) + else entry.text().then(processFile) + + reader.onload = () => { + processFile(reader.result) + } + reader.onerror = e => { + console.warn("Could not read file: " + filePath, e) + error++ + } + } + } + if (!rpMode && ext == "json") { + Object.keys(dpExclusive.folders).forEach(type => { + if (filePath.includes("/" + type + "/")) dpExclusive.folders[type]++ + }) + Object.keys(dpExclusive.tags).forEach(type => { + if (filePath.includes("/tags/" + type + "/")) dpExclusive.tags[type]++ + }) + } else if (rpMode) + Object.keys(rpExclusive).forEach(type => { + if (filePath.includes("/" + type + "/")) rpExclusive[type]++ + }) + } +} + +async function mainScan(hasData = false) { + if (interval) clearInterval(interval) + document.getElementById("result").innerHTML = "" + + if (!hasData) { + files = 0 + done = 0 + error = 0 + rpMode = document.getElementById("radiorp").checked + + filetypes = {} + filetypesOther = {} + packFiles = [] + packImages = [] + commands = {} + cmdsBehindExecute = {} + cmdsBehindMacros = {} + cmdsBehindReturn = {} + comments = 0 + empty = 0 + emptyFiles = [] + dpExclusive = { + folders: { + advancements: 0, + loot_tables: 0, + recipes: 0, + predicates: 0, + dimension: 0, + dimension_type: 0, + worldgen: 0 + }, + tags: { + banner_pattern: 0, + blocks: 0, + cat_variant: 0, + entity_types: 0, + fluids: 0, + functions: 0, + game_events: 0, + instrument: 0, + items: 0, + painting_variant: 0, + point_of_interest_type: 0, + worldgen: 0 + }, + scoreboards: 0, + selectors: { + a: 0, + e: 0, + p: 0, + r: 0, + s: 0 + }, + functions: ["#minecraft:load", "#minecraft:tick"], + functionCalls: [{target: "#minecraft:load"}, {target: "#minecraft:tick"}] + } + rpExclusive = { + atlases: 0, + blockstates: 0, + font: 0, + lang: 0, + models: 0, + particles: 0, + shaders: 0, + sounds: 0, + texts: 0, + textures: 0 + } + + processEntries(selected) + } + + interval = setInterval(() => { + document.getElementById("progress").innerText = Math.round(done / files * 100) + "% scanned" + (error > 0 ? " - " + error + " errors" : "") + if (done + error == files || hasData) { + clearInterval(interval) + if (files == 0) return document.getElementById("progress").innerText = "No files found!" + document.getElementById("resultButtons").hidden = false + if (error == 0) document.getElementById("progress").innerText = "" + if (Object.values(filetypes).reduce((a, b) => a + b) == 0) document.getElementById("progress").innerHTML = "No " + (rpMode ? "resource" : "data") + "pack files found!" + + const uncalledFunctions = dpExclusive.functions.filter(funcName => !dpExclusive.functionCalls.some(func => func.target == funcName)) + const missingFunctions = [...new Set(dpExclusive.functionCalls.filter(func => !dpExclusive.functions.includes(func.target)).map(func => func.target))] + + let html = + (packImages.length > 0 ? "
" + packImages.map(img => "") + "
" : "") + + (packFiles.length > 0 ? "" + (rpMode ? "Resource" : "Data") + "pack" + (packFiles.length == 1 ? "" : "s") + " found:
" + + packFiles.map(pack => { + let oldestFormat = pack.pack.pack_format + let newestFormat = pack.pack.pack_format + if (pack.pack.supported_formats && typeof pack.pack.supported_formats == "object") { + if (Array.isArray(pack.pack.supported_formats)) { + oldestFormat = pack.pack.supported_formats[0] + newestFormat = pack.pack.supported_formats[1] + } else { + oldestFormat = pack.pack.supported_formats.min_inclusive + newestFormat = pack.pack.supported_formats.max_inclusive + } + } + + let description = "" + if (pack.pack && pack.pack.description) { + if (typeof pack.pack.description == "object") { + const desc = Array.isArray(pack.pack.description) ? pack.pack.description : [pack.pack.description] + desc.forEach(d => { + if (d.text || d.translation) description += d.text || d.translation + }) + } else description = pack.pack.description + } else description = "No description" + + return "" + description.replace(/§[0-9a-flmnor]/gi, "") + + (window.versions.some(ver => (rpMode ? ver.resourcepack_version : ver.datapack_version) == pack.pack.pack_format) ? + "
Supported versions: " + + (window.versions.findLast(ver => (rpMode ? ver.resourcepack_version : ver.datapack_version) == oldestFormat)?.name || "?") + + " - " + + (window.versions.find(ver => (rpMode ? ver.resourcepack_version : ver.datapack_version) == newestFormat)?.name || "?") + + "" + : "") + + "
" + + (pack.features?.enabled?.length > 0 ? + "
Selected internal features: " + + pack.features.enabled.map(feature => "" + feature + "").join(", ") + "" + : "") + + (pack.filter?.block?.length > 0 ? "
Pack filters:
" + pack.filter.block.map(filter => + "" + + (filter.namespace ? "Namespace: " + filter.namespace + "" : "") + + (filter.namespace && filter.path ? ", " : "") + + (filter.path ? "Path: " + filter.path + "" : "") + + "" + ).join("
") + "
" : "") + }).join("
") + "
" + : "") + + (packFiles.length == 0 && (filetypes.fsh || filetypes.vsh || filetypes.xcf || filetypes.glsl) ? "Shader found
" : "") + + + (Object.keys(commands).length > 0 ? + "Total amount of commands: " + localize(Object.keys(commands).reduce((a, b) => a + commands[b], 0)) + "
" + + "Unique command names: " + localize(Object.keys(commands).length) + "
" + : "") + + (comments > 0 ? "Comments: " + localize(comments) + "
" : "") + + (empty > 0 ? "Empty lines: " + localize(empty) + "
" : "") + + "Pack file types found:
" + + Object.keys(filetypes).sort((a, b) => filetypes[b] - filetypes[a]).map(type => "." + type + ": " + localize(filetypes[type]) + "
").join("") + + (Object.keys(filetypesOther).length > 0 ? + "
" + + "Non-pack file types found:" + + Object.keys(filetypesOther).sort((a, b) => filetypesOther[b] - filetypesOther[a]).map(type => "" + type + ": " + localize(filetypesOther[type]) + "
").join("") + + "

" + : "") + + (uncalledFunctions.length > 0 ? + "Uncalled functions:
" + + uncalledFunctions.map(func => "" + func + "
").join("") + + "
" + : "") + + (missingFunctions.length > 0 ? + "Missing functions:
" + + missingFunctions.map(func => "" + func + "
").join("") + + "
" + : "") + + (emptyFiles.length > 0 ? + "Empty files:
" + + emptyFiles.map(func => "" + func + "
").join("") + + "
" + : "") + + + (dpExclusive.scoreboards > 0 ? "Scoreboards created: " + localize(dpExclusive.scoreboards) + "
" : "") + + (!rpMode && Object.values(dpExclusive.selectors).reduce((a, b) => a + b) != 0 ? "Selectors used:
" : "") + + Object.keys(dpExclusive.selectors).filter(i => dpExclusive.selectors[i] > 0).sort((a, b) => dpExclusive.selectors[b] - dpExclusive.selectors[a]) + .map(type => "@" + type + ": " + localize(dpExclusive.selectors[type]) + "
").join("") + + (!rpMode && Object.values(dpExclusive.folders).reduce((a, b) => a + b) != 0 ? "Data pack features used:
" : "") + + Object.keys(dpExclusive.folders).filter(i => dpExclusive.folders[i] > 0).sort((a, b) => dpExclusive.folders[b] - dpExclusive.folders[a]) + .map(type => "" + type + ": " + localize(dpExclusive.folders[type]) + "
").join("") + + (!rpMode && Object.values(dpExclusive.tags).reduce((a, b) => a + b) != 0 ? "Tags used:
" : "") + + Object.keys(dpExclusive.tags).filter(i => dpExclusive.tags[i] > 0).sort((a, b) => dpExclusive.tags[b] - dpExclusive.tags[a]) + .map(type => "" + type + ": " + localize(dpExclusive.tags[type]) + "
").join("") + + + (rpMode && Object.values(rpExclusive).reduce((a, b) => a + b) != 0 ? "
Resource pack features used:
" : "") + + Object.keys(rpExclusive).filter(i => rpExclusive[i] > 0).sort((a, b) => rpExclusive[b] - rpExclusive[a]) + .map(type => "" + type + ": " + localize(rpExclusive[type]) + "
").join("") + + html += "
" + commands = Object.fromEntries(Object.entries(commands).sort(([, a], [, b]) => b - a)) + Object.keys(commands).forEach(cmd => { + html += cmd + ": " + localize(commands[cmd]) + "
" + if (cmdsBehindExecute[cmd]) html += "Behind execute: " + localize(cmdsBehindExecute[cmd]) + + (cmd == "execute" ? "⚠️ (... run execute ... equals ... ...)" : "") + "
" + if (cmdsBehindMacros[cmd]) html += "Behind macro: " + localize(cmdsBehindMacros[cmd]) + "
" + if (cmdsBehindReturn[cmd]) html += "Behind return: " + localize(cmdsBehindReturn[cmd]) + "
" + }) + document.getElementById("result").innerHTML = html + } + }, 100) +} + +async function selectFolder() { + if (interval) clearInterval(interval) + selected = null + + const input = document.createElement("input") + input.type = "file" + input.webkitdirectory = true + input.onchange = e => { + selected = e.target.files + mainScan() + } + if ("showPicker" in HTMLInputElement.prototype) input.showPicker() + else input.click() +} + +async function selectZip() { + if (interval) clearInterval(interval) + + const input = document.createElement("input") + input.type = "file" + input.accept = ".zip" + input.onchange = e => handleZip(e.target.files[0]) + + if ("showPicker" in HTMLInputElement.prototype) input.showPicker() + else input.click() +} + +function handleZip(file) { + selected = [] + + new JSZip().loadAsync(file).then(async zip => { + for await (const zipFile of Object.values(zip.files)) { + selected.push({ + name: zipFile.name, + content: await zipFile.async("text") + }) + } + mainScan() + }) +} diff --git a/assets/style.css b/assets/style.css index bfadbc9..92ff7d3 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,4 +1,4 @@ -.light-theme { +.light { --background: #FFF; --header-bg: #8be0c0; --dialog-bg: #EEE; @@ -35,8 +35,8 @@ body { } button { - padding: 5px 8px; - font-size: 16px; + padding: 6px 8px; + font-size: 16.5px; border: none; border-radius: 7px; cursor: pointer; diff --git a/eslint.config.js b/eslint.config.js index 105698f..76be1a8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -225,7 +225,7 @@ module.exports = [ globals: global }, files: ["**/*.js"], - ignores: ["eslint.config.js", "assets/jszip.min.js"], + ignores: ["eslint.config.js", "minify.js", "assets/jszip.min.js"], plugins: { unicorn, sonarjs, @@ -240,7 +240,7 @@ module.exports = [ ...globals.node } }, - files: ["eslint.config.js"], + files: ["eslint.config.js", "minify.js"], plugins: { unicorn, sonarjs, diff --git a/index.html b/index.html index 14af9d9..af62fd6 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,6 @@ - diff --git a/minify.js b/minify.js new file mode 100644 index 0000000..ba52ac6 --- /dev/null +++ b/minify.js @@ -0,0 +1,50 @@ +const fsPromises = require("node:fs").promises +const UglifyJS = require("uglify-js") + +const nameCache = {} +const defaultOptions = { + compress: { + passes: 2, + unsafe: true, + unsafe_Function: true, + unsafe_math: true, + unsafe_proto: true, + unsafe_regexp: true + } +} + +const minifyFile = async (path, options = {}) => { + const filename = path.split("/").pop() + const result = UglifyJS.minify({ + [path]: await fsPromises.readFile(path, "utf8") + }, { + sourceMap: { + root: "https://pack-analyzer.pages.dev/assets/", + filename, + url: filename + ".map" + }, + warnings: "verbose", + parse: { + shebang: false + }, + nameCache, + mangle: true, + ...defaultOptions, + ...options + }) + + if (result.error) throw result.error + if (result.warnings && result.warnings.length > defaultOptions.compress.passes) console.log(path, result.warnings) + + if (process.env.MINIFY_ENABLED) { + await fsPromises.writeFile(path, result.code) + await fsPromises.writeFile(path + ".map", result.map) + } +} + +async function main() { + await minifyFile("./assets/script.js", { + module: false + }) +} +main() diff --git a/package-lock.json b/package-lock.json index 874761b..4791d75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "pack-analyzer", "version": "2.0.0", "license": "CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0", + "dependencies": { + "uglify-js": "^3.17.4" + }, "devDependencies": { "@html-eslint/eslint-plugin": "^0.23.1", "@html-eslint/parser": "^0.23.0", @@ -2082,6 +2085,17 @@ "node": ">=8" } }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/package.json b/package.json index ca890c7..72776eb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,12 @@ "type": "git", "url": "https://github.com/DEVTomatoCake/Pack-Analyzer.git" }, + "scripts": { + "minify": "node minify.js" + }, + "dependencies": { + "uglify-js": "^3.17.4" + }, "devDependencies": { "@html-eslint/eslint-plugin": "^0.23.1", "@html-eslint/parser": "^0.23.0", diff --git a/serviceworker.js b/serviceworker.js index 52a8026..3438890 100644 --- a/serviceworker.js +++ b/serviceworker.js @@ -18,8 +18,7 @@ self.addEventListener("install", event => { const fallbackCache = await caches.open("fallback" + version) fallbackCache.addAll([ "/assets/style.css", - "/assets/script.js", - "/assets/analyzer.js" + "/assets/script.js" ]) })()) })