2022-04-15 01:51:09 +00:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
2021-08-22 22:32:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Bundle //
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2021-08-22 22:32:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
2021-08-22 22:32:18 +00:00
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Add a file to the bundle
|
|
|
|
add(name, data) {
|
|
|
|
this.push(this[name] = new BundledFile(name, data));
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// 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);
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Write an integer into an output buffer
|
|
|
|
writeInt(data, size, value) {
|
|
|
|
for (; size > 0; size--, value >>= 8)
|
|
|
|
data.push(value & 0xFF);
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Write a string of text as bytes into an output buffer
|
|
|
|
writeString(data, text) {
|
|
|
|
data.push(... this.encoder.encode(text));
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
///////////////////////////// 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 {
|
|
|
|
|
2021-08-22 22:32:18 +00:00
|
|
|
constructor(name, data) {
|
|
|
|
|
|
|
|
// Configure instance fields
|
|
|
|
this.data = data;
|
|
|
|
this.name = name;
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Resolve the MIME type
|
2021-08-22 22:32:18 +00:00
|
|
|
this.mime =
|
2022-04-15 01:51:09 +00:00
|
|
|
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" :
|
2021-08-22 22:32:18 +00:00
|
|
|
"application/octet-stream"
|
|
|
|
;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Install a font from a bundled resource
|
|
|
|
async installFont(name) {
|
|
|
|
if (name === undefined) {
|
|
|
|
name = "/" + this.name;
|
|
|
|
name = name.substring(name.lastIndexOf("/") + 1);
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
2022-04-15 01:51:09 +00:00
|
|
|
let ret = new FontFace(name, "url('"+
|
|
|
|
(Bundle.debug ? this.name : this.toDataURL()) + "'");
|
|
|
|
await ret.load();
|
|
|
|
document.fonts.add(ret);
|
|
|
|
return ret;
|
|
|
|
}
|
2021-08-22 22:32:18 +00:00
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// 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;
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// 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=>{
|
2021-08-23 23:56:36 +00:00
|
|
|
if (enabled)
|
2022-04-15 01:51:09 +00:00
|
|
|
ret.removeAttribute("disabled");
|
|
|
|
else ret.setAttribute("disabled", "");
|
2021-08-23 23:56:36 +00:00
|
|
|
};
|
2022-04-15 01:51:09 +00:00
|
|
|
ret.setEnabled(!!enabled);
|
|
|
|
document.head.appendChild(ret);
|
|
|
|
return ret;
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Encode the file data as a data URL
|
|
|
|
toDataURL() {
|
2022-04-15 01:51:09 +00:00
|
|
|
return "data:" + this.mime + ";base64," +
|
|
|
|
btoa(String.fromCharCode(...this.data));
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// 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);
|
|
|
|
}
|
2021-08-22 22:32:18 +00:00
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Encode the transformed source as a data URL
|
|
|
|
return "data:" + this.mime + ";base64," + btoa(src);
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Decode the file data as a UTF-8 string
|
|
|
|
toString() {
|
|
|
|
return Bundle.decoder.decode(this.data);
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
///////////////////////////// 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);
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
2022-04-15 01:51:09 +00:00
|
|
|
// Boot Program //
|
2021-08-22 22:32:18 +00:00
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// De-register the boot function
|
|
|
|
delete globalThis.a;
|
2021-08-22 22:32:18 +00:00
|
|
|
|
|
|
|
// Remove the bundle image element from the document
|
2022-04-15 01:51:09 +00:00
|
|
|
Bundle.src = arguments[0].src;
|
2021-08-22 22:32:18 +00:00
|
|
|
arguments[0].remove();
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// 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
|
2021-08-22 22:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Begin program operations
|
|
|
|
import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript());
|