/* The Bundle.java utility prepends a file manifest to this script before execution is started. This script runs within the context of an async function. */ /////////////////////////////////////////////////////////////////////////////// // Bundle // /////////////////////////////////////////////////////////////////////////////// // Bundled file manager let Bundle = globalThis.Bundle = new class Bundle extends Array { constructor() { super(); // Configure instance fields this.debug = location.protocol != "file:" && location.hash == "#debug"; this.decoder = new TextDecoder(); this.encoder = new TextEncoder(); this.moduleCall = (... a)=>this.module (... a); this.resourceCall = (... a)=>this.resource(... a); // Generate the CRC32 lookup table this.crcLookup = new Uint32Array(256); for (let x = 0; x <= 255; x++) { let l = x; for (let j = 7; j >= 0; j--) l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); this.crcLookup[x] = l; } } ///////////////////////////// Public Methods ////////////////////////////// // Add a file to the bundle add(name, data) { this.push(this[name] = new BundledFile(name, data)); } // Export all bundled resources to a .ZIP file save() { let centrals = new Array(this.length); let locals = new Array(this.length); let offset = 0; let size = 0; // Encode file and directory entries for (let x = 0; x < this.length; x++) { let file = this[x]; let sum = this.crc32(file.data); locals [x] = file.toZipHeader(sum); centrals[x] = file.toZipHeader(sum, offset); offset += locals [x].length; size += centrals[x].length; } // Encode end of central directory let end = []; this.writeInt(end, 4, 0x06054B50); // Signature this.writeInt(end, 2, 0); // Disk number this.writeInt(end, 2, 0); // Central dir start disk this.writeInt(end, 2, this.length); // # central dir this disk this.writeInt(end, 2, this.length); // # central dir total this.writeInt(end, 4, size); // Size of central dir this.writeInt(end, 4, offset); // Offset of central dir this.writeInt(end, 2, 0); // .ZIP comment length // Prompt the user to save the resulting file let a = document.createElement("a"); a.download = bundleName + ".zip"; a.href = URL.createObjectURL(new Blob( locals.concat(centrals).concat([Uint8Array.from(end)]), { type: "application/zip" } )); a.style.visibility = "hidden"; document.body.appendChild(a); a.click(); a.remove(); } ///////////////////////////// Package Methods ///////////////////////////// // Write a byte array into an output buffer writeBytes(data, bytes) { //data.push(... bytes); for (let b of bytes) data.push(b); } // Write an integer into an output buffer writeInt(data, size, value) { for (; size > 0; size--, value >>= 8) data.push(value & 0xFF); } // Write a string of text as bytes into an output buffer writeString(data, text) { data.push(... this.encoder.encode(text)); } ///////////////////////////// Private Methods ///////////////////////////// // Calculate the CRC32 checksum for a byte array crc32(data) { let c = 0xFFFFFFFF; for (let x = 0; x < data.length; x++) c = ((c >>> 8) ^ this.crcLookup[(c ^ data[x]) & 0xFF]); return ~c & 0xFFFFFFFF; } }(); /////////////////////////////////////////////////////////////////////////////// // BundledFile // /////////////////////////////////////////////////////////////////////////////// // Individual bundled file class BundledFile { constructor(name, data) { // Configure instance fields this.data = data; this.name = name; // Resolve the MIME type this.mime = name.endsWith(".css" ) ? "text/css;charset=UTF-8" : name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" : name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" : name.endsWith(".json" ) ? "application/json;charset=UTF-8" : name.endsWith(".png" ) ? "image/png" : name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" : name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" : name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" : name.endsWith(".wasm" ) ? "application/wasm" : name.endsWith(".woff2") ? "font/woff2" : "application/octet-stream" ; } ///////////////////////////// Public Methods ////////////////////////////// // Install a font from a bundled resource async installFont(name) { if (name === undefined) { name = "/" + this.name; name = name.substring(name.lastIndexOf("/") + 1); } let ret = new FontFace(name, "url('"+ (Bundle.debug ? this.name : this.toDataURL()) + "'"); await ret.load(); document.fonts.add(ret); return ret; } // Install an image as a CSS icon from a bundled resource installImage(name, filename) { document.documentElement.style.setProperty("--" + name || this.name.replaceAll(/\/\./, "_"), "url('" + (Bundle.debug ? filename || this.name : this.toDataURL()) + "')"); return name; } // Install a stylesheet from a bundled resource installStylesheet(enabled) { let ret = document.createElement("link"); ret.href = Bundle.debug ? this.name : this.toDataURL(); ret.rel = "stylesheet"; ret.type = "text/css"; ret.setEnabled = enabled=>{ if (enabled) ret.removeAttribute("disabled"); else ret.setAttribute("disabled", ""); }; ret.setEnabled(!!enabled); document.head.appendChild(ret); return ret; } // Encode the file data as a data URL toDataURL() { return "data:" + this.mime + ";base64," + btoa(String.fromCharCode(...this.data)); } // Interpret the file's contents as bundled script source data URL toScript() { // Process all URL strings prefixed with /**/ let parts = this.toString().split("/**/"); let src = parts.shift(); for (let part of parts) { let quote = part.indexOf("\"", 1); // Begin with the path of the current file let path = this.name.split("/"); path.pop(); // Navigate to the path of the target file let file = part.substring(1, quote).split("/"); while (file.length > 0) { let sub = file.shift(); switch (sub) { case "..": path.pop(); // Fallthrough case "." : break; default : path.push(sub); } } // Append the file as a data URL file = Bundle[path.join("/")]; src += "\"" + file[ file.mime.startsWith("text/javascript") ? "toScript" : "toDataURL" ]() + "\"" + part.substring(quote + 1); } // Encode the transformed source as a data URL return "data:" + this.mime + ";base64," + btoa(src); } // Decode the file data as a UTF-8 string toString() { return Bundle.decoder.decode(this.data); } ///////////////////////////// Package Methods ///////////////////////////// // Produce a .ZIP header for export toZipHeader(crc32, offset) { let central = offset !== undefined; let ret = []; if (central) { Bundle.writeInt (ret, 4, 0x02014B50); // Signature Bundle.writeInt (ret, 2, 20); // Version created by } else Bundle.writeInt (ret, 4, 0x04034B50); // Signature Bundle.writeInt (ret, 2, 20); // Version required Bundle.writeInt (ret, 2, 0); // Bit flags Bundle.writeInt (ret, 2, 0); // Compression method Bundle.writeInt (ret, 2, 0); // Modified time Bundle.writeInt (ret, 2, 0); // Modified date Bundle.writeInt (ret, 4, crc32); // Checksum Bundle.writeInt (ret, 4, this.data.length); // Compressed size Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size Bundle.writeInt (ret, 2, this.name.length); // Filename length Bundle.writeInt (ret, 2, 0); // Extra field length if (central) { Bundle.writeInt (ret, 2, 0); // File comment length Bundle.writeInt (ret, 2, 0); // Disk number start Bundle.writeInt (ret, 2, 0); // Internal attributes Bundle.writeInt (ret, 4, 0); // External attributes Bundle.writeInt (ret, 4, offset); // Relative offset } Bundle.writeString (ret, this.name); // Filename if (!central) Bundle.writeBytes(ret, this.data); // File data return Uint8Array.from(ret); } } /////////////////////////////////////////////////////////////////////////////// // Boot Program // /////////////////////////////////////////////////////////////////////////////// // De-register the boot function delete globalThis.a; // Remove the bundle image element from the document Bundle.src = arguments[0].src; arguments[0].remove(); // Convert the file manifest into BundledFile objects let buffer = arguments[1]; let offset = arguments[2] - manifest[0][1]; for (let entry of manifest) { Bundle.add(entry[0], buffer.subarray(offset, offset + entry[1])); offset += entry[1]; if (Bundle.length == 1) offset++; // Skip null delimiter } // Begin program operations import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript());