// Running as an async function // Prepended by Bundle.java: buffer, image, manifest, name /////////////////////////////////////////////////////////////////////////////// // Bundle // /////////////////////////////////////////////////////////////////////////////// // Resource manager for bundled files class Bundle extends Array { //////////////////////////////// Constants //////////////////////////////// // .ZIP support static CRC_LOOKUP = new Uint32Array(256); // Text processing static DECODER = new TextDecoder(); static ENCODER = new TextEncoder(); static initializer() { // Generate the CRC32 lookup table for (let x = 0; x <= 255; x++) { let l = x; for (let j = 7; j >= 0; j--) l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); this.CRC_LOOKUP[x] = l; } } ///////////////////////// Initialization Methods ////////////////////////// constructor(name, url, settings, isDebug) { super(); this.isDebug = isDebug; this.name = name; this.settings = settings; this.url = url; } ///////////////////////////// Public Methods ////////////////////////////// // 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 = Bundle.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 = []; Bundle.writeInt(end, 4, 0x06054B50); // Signature Bundle.writeInt(end, 2, 0); // Disk number Bundle.writeInt(end, 2, 0); // Central dir start disk Bundle.writeInt(end, 2, this.length); // # central dir this disk Bundle.writeInt(end, 2, this.length); // # central dir total Bundle.writeInt(end, 4, size); // Size of central dir Bundle.writeInt(end, 4, offset); // Offset of central dir Bundle.writeInt(end, 2, 0); // .ZIP comment length // Prompt the user to save the resulting file let a = document.createElement("a"); a.download = this.name + ".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 ///////////////////////////// // Add a BundledFile to the collection add(file) { file.bundle = this; this.push(this[file.name] = file); } // Write a byte array into an output buffer static writeBytes(data, bytes) { for (let b of bytes) data.push(b); } // Write an integer into an output buffer static 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 static writeString(data, text) { this.writeBytes(data, this.ENCODER.encode(text)); } ///////////////////////////// Private Methods ///////////////////////////// // Calculate the CRC32 checksum for a byte array static crc32(data) { let c = 0xFFFFFFFF; for (let x = 0; x < data.length; x++) c = ((c >>> 8) ^ this.CRC_LOOKUP[(c ^ data[x]) & 0xFF]); return ~c & 0xFFFFFFFF; } } Bundle.initializer(); /////////////////////////////////////////////////////////////////////////////// // BundledFile // /////////////////////////////////////////////////////////////////////////////// // Individual file in the bundled data class BundledFile { //////////////////////////////// Constants //////////////////////////////// // MIME types static MIMES = { ".css" : "text/css;charset=UTF-8", ".frag" : "text/plain;charset=UTF-8", ".js" : "text/javascript;charset=UTF-8", ".json" : "application/json;charset=UTF-8", ".png" : "image/png", ".svg" : "image/svg+xml;charset=UTF-8", ".txt" : "text/plain;charset=UTF-8", ".vert" : "text/plain;charset=UTF-8", ".wasm" : "application/wasm", ".woff2": "font/woff2" }; ///////////////////////// Initialization Methods ////////////////////////// constructor(name, buffer, offset, length) { // Configure instance fields this.data = buffer.slice(offset, offset + length); this.name = name; // Resolve the MIME type let index = name.lastIndexOf("."); this.mime = index != -1 && BundledFile.MIMES[name.substring(index)] || "application/octet-stream"; } ///////////////////////////// Public Methods ////////////////////////////// // Represent the file with a blob URL toBlobURL() { return this.blobURL || (this.blobURL = URL.createObjectURL( new Blob([ this.data ], { type: this.mime }))); } // Encode the file data as a data URL toDataURL() { return "data:" + this.mime + ";base64," + btoa( Array.from(this.data).map(b=>String.fromCharCode(b)).join("")); } // Pre-process URLs in a bundled file's contents toProcURL(asDataURL = false) { // The URL has already been computed if (this.url) return this.url; // Working variables let content = this.toString(); let pattern = /\/\*\*?\*\//g; let parts = content.split(pattern); let ret = [ parts.shift() ]; // Process all URLs prefixed with /**/ or /***/ for (let part of parts) { let start = part.indexOf("\""); let end = part.indexOf("\"", start + 1); let filename = part.substring(start + 1, end); let asData = pattern.exec(content)[0] == "/***/"; // Relative to current file if (filename.startsWith(".")) { let path = this.name.split("/"); path.pop(); // Current filename // Navigate to the path of the target file for (let dir of filename.split("/")) { switch (dir) { case "..": path.pop(); // Fallthrough case "." : break; default : path.push(dir); } } // Produce the fully-qualified filename filename = path.join("/"); } // Append the file as a data URL let file = this.bundle[filename]; ret.push( part.substring(0, start + 1), file[ file.mime.startsWith("text/javascript") || file.mime.startsWith("text/css") ? "toProcURL" : asData ? "toDataURL" : "toBlobURL" ](asData), part.substring(end) ); } // Represent the transformed source as a URL return this.url = asDataURL ? "data:" + this.mime + ";base64," + btoa(ret.join("")) : URL.createObjectURL(new Blob(ret, { type: this.mime })) ; } // 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 // /////////////////////////////////////////////////////////////////////////////// // Produce the application bundle let bundle = new Bundle(name, image.src, image.getAttribute("settings") || "", location.protocol != "file:" && location.hash == "#debug"); for (let x=0,offset=buffer.indexOf(0)-manifest[0][1]; x