311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
// 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<manifest.length; x++) {
|
|
let entry = manifest[x];
|
|
bundle.add(new BundledFile(entry[0], buffer, offset, entry[1]));
|
|
offset += entry[1] + (x == 0 ? 1 : 0);
|
|
}
|
|
|
|
// Begin program operations
|
|
(await import(bundle.isDebug ?
|
|
"./web/App.js" :
|
|
bundle["web/App.js"].toProcURL()
|
|
)).App.main(bundle);
|