shrooms-vb-web/main.js

282 lines
8.8 KiB
JavaScript
Raw Normal View History

2025-02-18 22:39:36 +00:00
// Packaged asset manager
class Bundle extends Map {
// Instance fields
#isDebug; // True if running in debug mode
///////////////////////// Initialization Methods //////////////////////////
constructor() {
super();
this.#isDebug = location.host=="localhost" && location.hash=="#debug";
}
/////////////////////////////// Properties ////////////////////////////////
// Determine whether debug mode is active
get isDebug() { return this.#isDebug; }
///////////////////////////// Public Methods //////////////////////////////
// Insert an asset file
add(name, data) {
let asset = new Bundle.#Asset(this, name, data);
this.set(name, asset);
return asset;
}
// List files with names matching a given prefix
list(prefix = "") {
prefix = String(prefix);
let ret = [];
for (let file of this.values()) {
if (file.name.startsWith(prefix))
ret.push(file);
}
return ret.sort((a,b)=>a.name.localeCompare(b.name));
}
///////////////////////////////// Classes /////////////////////////////////
// Packaged asset file
static #Asset = class Asset {
// Private fields
#blobURL; // Cached blob: URL
#bundle; // Parent Bundle object
#data; // Byte contents
#dataURL; // Cached data: URL
#mime; // MIME type
#name; // Filename
#transform; // Transform URLs when not in debug mode
////////////////////////////// Constants //////////////////////////////
// Mime types by file extension
static #MIMES = {
"html" : "text/html;charset=UTF-8" ,
"css" : "text/css;charset=UTF-8" ,
"frag" : "text/plain;charset=UTF-8" ,
"gif" : "image/gif" ,
"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" ,
"webp" : "image/webp" ,
"woff2": "font/woff2"
}
/////////////////////// Initialization Methods ////////////////////////
constructor(bundle, name, data) {
// Select the MIME type from the file extension
let mime = "." + name;
let ext = mime.substring(mime.lastIndexOf(".") + 1).toLowerCase();
mime = Bundle.#Asset.#MIMES[ext] ?? "application/octet-stream";
// Configure instanc fields
this.#blobURL = null;
this.#bundle = bundle;
this.#data = data;
this.#dataURL = null;
this.#mime = mime;
this.#name = name;
this.#transform = ext == "css" || ext == "js";
}
///////////////////////////// Properties //////////////////////////////
// Retrieve and potentially cache the blob: URL
get blobURL() {
if (this.#blobURL == null) {
this.#blobURL = URL.createObjectURL(
new Blob([this.#urlData()], { type: this.#mime }));
}
return this.#blobURL;
}
// Byte contents
get data() { return this.#data; }
// Retrieve and potentially cache the data: URL
get dataURL() {
if (this.#dataURL == null) {
this.#dataURL = "data:" + this.#mime + ";base64," + btoa(
Array.from(this.#urlData()).map(b=>String.fromCodePoint(b))
.join(""));
}
return this.#dataURL;
}
// Filename
get name() { return this.#name; }
// Text contents as UTF-8
get text() { return new TextDecoder().decode(this.#data); }
// Produce any suitable URL to fetch this file
get url() {
// Use the blob: URL in debug mode
if (!this.#bundle.isDebug)
return this.blobURL;
// Resolve the virtual path otherwise
let href = location.href.split("/");
href.pop();
return href.join("/") + "/" + this.name;
}
/////////////////////////// Private Methods ///////////////////////////
// Prepare a data buffer for use in a data or blob URL
#urlData() {
// No need to transform inner URLs
if (!this.#transform)
return this.#data;
// Working variables
let regex = /\/\*\*?\*\//g;
let ret = [];
let src = 0;
let text = this.text;
// Transform all inner URLs
for (;;) {
let match = regex.exec(text);
// No more inner URLs
if (match == null)
break;
// Locate the URL to transform
let end, start;
try {
start = text.indexOf("\"", match.index);
if (start == -1)
throw 0;
end = text.indexOf("\"", ++start);
if (end == -1)
throw 0;
} catch {
throw new Error(
"Malformed URL designator.\n" +
"File: " + this.name
);
}
// Working variables
let url = text.substring(start, end);
let parts = url.split("/");
let stack = [];
// Initialize the stack to current path if URL is relative
if (parts[0] == "." || parts[0] == "..") {
stack = this.name.split("/");
stack.pop();
}
// Process the URL path
while (parts.length > 1) {
let part = parts.shift();
switch (part) {
// Current directory--do not modify stack
case ".": break;
// Parent directory--pop from stack
case "..":
if (stack.length == 0) {
throw new Error(
"Stack underflow when parsing URL.\n" +
"File: " + this.name + "\n" +
"URL: " + url
);
}
stack.pop();
break;
// Child directory--push to stack
default: stack.push(part);
}
}
// Compose the resolved filename
let filename = stack.concat(parts).join("/");
if (!this.#bundle.has(filename)) {
throw new Error(
"Referenced file does not exist.\n" +
"File: " + this.name + "\n" +
"URL: " + url + "\n" +
"Path: " + filename
);
}
// Working variables
let file = this.#bundle.get(filename);
let newUrl = match[0] == "/**/" ? file.blobURL : file.dataURL;
// Append the output text
ret.push(text.substring(src, start), newUrl);
src = end;
}
// Incorporate remaining text
ret.push(text.substring(src));
return new TextEncoder().encode(ret.join(""));
}
};
}
// Program entry point
{
// Remove startup <script> elements
let bytes = document.querySelectorAll("script");
for (let script of bytes)
script.remove();
bytes = bytes[1].bytes;
// Wait for the bundle element to finish loading
if (document.readyState != "complete")
await new Promise(resolve=>window.addEventListener("load", resolve));
// Parse the manifest from the byte buffer
let x = bytes.indexOf(0) + 1;
let y = bytes.indexOf(0, x);
let manifest = JSON.parse(new TextDecoder().decode(bytes.subarray(x, y)));
// Compose the bundle from the packaged asset files
let bundle = new Bundle();
bundle.add("shrooms-vb-web/main.js", bytes.subarray(0, x - 1));
for (x = 0, y++; x < manifest.length; x += 2)
bundle.add(manifest[x], bytes.subarray(y, y += manifest[x + 1]));
// Launch the application
new (await import(bundle.get("shrooms-vb-web/App.js").url))
.App(bundle, manifest.pop());
}