282 lines
8.8 KiB
JavaScript
282 lines
8.8 KiB
JavaScript
|
// 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());
|
||
|
}
|