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"
])
})())
})