Minimal first working version
This commit is contained in:
parent
038cdb8596
commit
7850b0eaa9
680
browser.js
Normal file
680
browser.js
Normal file
@ -0,0 +1,680 @@
|
||||
"use strict";
|
||||
const Mimicry = (function privateMimicryClosure () {
|
||||
class Mimicry {
|
||||
/**
|
||||
* Creates an instance of the Mimicry.
|
||||
*
|
||||
* @param {Object} [config] - configuration of the Mimicry.
|
||||
* @param {Number} [config.maxParallelLoadings] - amount of files that are loaded in parallel.
|
||||
* @param {String} [config.baseUrl] - URL prefix that will be added to every.
|
||||
* @param {String} [config.seed] - request optional component allowing to drop user cached files.
|
||||
*/
|
||||
constructor (config) {
|
||||
instances.push(this);
|
||||
|
||||
this._queue = null; //deferred Queue
|
||||
|
||||
this._loadedFiles = new Set();
|
||||
this._config = Object.create(null);
|
||||
this._loadingFiles = new Map();
|
||||
this._plannedFiles = Object.create(null);
|
||||
this._modules = new Map();
|
||||
this._typePaths = new Map();
|
||||
this._loadedAssets = new Map();
|
||||
this._libs = new Map();
|
||||
this._errors = new Map();
|
||||
this._moduleErrors = [];
|
||||
this._errorHandlers = [];
|
||||
this._progressHandlers = [];
|
||||
|
||||
Object.assign(this._config, defaultOptions, config);
|
||||
|
||||
this._baseUrl = this._config.baseUrl;
|
||||
if (this._baseUrl && !this._baseUrl.endsWith("/"))
|
||||
this._baseUrl += "/";
|
||||
|
||||
this._globalErrorHandler = onGlobalError.bind(this);
|
||||
window.addEventListener("error", this._globalErrorHandler);
|
||||
|
||||
if (ready)
|
||||
createDeferredParts.call(this);
|
||||
else
|
||||
this._deferred = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes Mimicry, clears as much memory as it can, forgets all the handlers and contexts
|
||||
* */
|
||||
destructor () {
|
||||
window.off("error", this._globalErrorHandler);
|
||||
|
||||
const index = instances.indexOf(this);
|
||||
instances.splice(index, 1);
|
||||
|
||||
delete this._queue;
|
||||
delete this._loadedFiles;
|
||||
delete this._config;
|
||||
delete this._loadingFiles;
|
||||
delete this._plannedFiles;
|
||||
delete this._modules;
|
||||
delete this._typePaths;
|
||||
delete this._loadedAssets;
|
||||
delete this._libs;
|
||||
delete this._errors;
|
||||
delete this._moduleErrors;
|
||||
delete this._errorHandlers;
|
||||
delete this._progressHandlers;
|
||||
delete this._globalErrorHandler;
|
||||
delete this._deferred;
|
||||
delete this._baseUrl;
|
||||
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file, calls the callback if it has loaded successfully,
|
||||
* calls error callback if there was any sort of error
|
||||
*
|
||||
* @param {String} path - path to the module
|
||||
* @param {Function} [callback=contentType] - a callback that is called after module is loaded
|
||||
* @param {Function} [error=contentType] - a callback that is called if there was an error
|
||||
* @param {Mimicry.Type} [contentType=Mimicry.Type.js] - type of the content to load
|
||||
*/
|
||||
load (path, callback, error, contentType) {
|
||||
if (!ready) {
|
||||
this._deferred.push(["load", path, callback, error, contentType]);
|
||||
return;
|
||||
}
|
||||
if (!(error instanceof Function) && contentType === undefined) {
|
||||
contentType = error;
|
||||
error = undefined;
|
||||
}
|
||||
if (!(callback instanceof Function) && contentType === undefined) {
|
||||
contentType = callback;
|
||||
callback = undefined;
|
||||
}
|
||||
if (!contentType)
|
||||
contentType = Type.js;
|
||||
|
||||
scheduleLoad.call(this,
|
||||
Dependency.refinePath(path, contentType, this._baseUrl, this._typePaths, this._libs),
|
||||
callback, error, contentType, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a module.
|
||||
* The mainstream way to use Mimicry is to declare modules.
|
||||
* It's best If you declare one module per file.
|
||||
*
|
||||
* The best way to export anything from the module is to return it from the callback
|
||||
*
|
||||
* @param {Array<String>} dependencies - list of module names that this module depends on
|
||||
* @param {Function} [callback] - module body that will be executed as soon all dependencies are resolved
|
||||
*/
|
||||
module (dependencies, callback) {
|
||||
console.log(document.currentScript);
|
||||
if (!ready) {
|
||||
this._deferred.push(["module", dependencies, callback]);
|
||||
return;
|
||||
}
|
||||
|
||||
const href = getCurrentHref();
|
||||
if (!href.length)
|
||||
throw new Error("Couldn't understand module path");
|
||||
|
||||
const moduleName = Dependency.nameFromHref(href, this._baseUrl, this._typePaths, this._libs);
|
||||
if (this._modules.has(moduleName))
|
||||
throw new Error("An attempt to declare more than one module per file (" + moduleName + ")");
|
||||
|
||||
const {deps, body} = Dependency.refineModuleArguments(dependencies, callback);
|
||||
const module = new Module(deps, body);
|
||||
this._modules.set(moduleName, module);
|
||||
|
||||
for (let i = 0; i < deps.length; ++i) {
|
||||
const element = deps[i];
|
||||
const contentType = element.contentType;
|
||||
const elName = element.name;
|
||||
|
||||
this.load(elName, loadedFileForModule.bind(this, module, elName), contentType);
|
||||
}
|
||||
|
||||
if (module.allDependenciesResolved)
|
||||
module.execute(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously returns the module value if it was
|
||||
* already loaded and executed, returns undefined otherwise
|
||||
* */
|
||||
getModule(/*String*/moduleName) {
|
||||
const module = this._modules.get(moduleName);
|
||||
if (module && module.executed)
|
||||
return module.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously returns the asses if it was already loaded, returns undefined otherwise
|
||||
* */
|
||||
getAsset(/*String*/assetName) {
|
||||
const asset = this._loadedAssets.get(assetName);
|
||||
if (asset)
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you wish to keep all files of some types in a designated folder of your project
|
||||
* you can declare it using this function.
|
||||
*
|
||||
* Say, you want to have all JSON files in "json" folder, and all CSS files in "styles/css" folder.
|
||||
* After calling "mimicry.setTypePath(Mimicry.Type.json, 'json')" and
|
||||
* "mimicry.setTypePath(Mimicry.Type.css, 'styles/css')" you can access files from those folders
|
||||
* in a following manner
|
||||
*
|
||||
* "mimicry.load('json/config/main', () => {}, Mimicry.Type.json)" changes to
|
||||
* "mimicry.load('config/main', () => {}, Mimicry.Type.json)", and
|
||||
*
|
||||
* "mimicry.load('styles/css/basic', () => {}, Mimicry.Type.css)" to
|
||||
* "mimicry.load('basic', () => {}, Mimicry.Type.css)"
|
||||
* */
|
||||
setTypePath (/*Type*/type, /*String*/path) {
|
||||
if (path && !path.endsWith("/"))
|
||||
path += "/";
|
||||
|
||||
this._typePaths.set(type, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* You might want to include other projects in your project. I will call those imported subprojects libraries
|
||||
* In mimicry there is a way to hide actual path behind library name.
|
||||
*
|
||||
* Say you included a library called "flame", and placed in "libs/external/flame".
|
||||
* After calling "mimicry.setLibPath('flame', 'libs/external/flame')" you can access files from those libraries
|
||||
* in a following manner
|
||||
*
|
||||
* "mimicry.load('libs/external/flame/index', () => {})" changes to
|
||||
* "mimicry.load('@flame/index', () => {})"
|
||||
* */
|
||||
setLibPath (/*String*/libraryName, /*String*/libraryPath) {
|
||||
const path = this._libs.get(libraryName);
|
||||
if (path && path !== libraryPath)
|
||||
throw new Error("Mimicry error: library " + libraryName + " already had a path " + path +
|
||||
", assigning new path " + libraryPath + " would probably break everything");
|
||||
|
||||
this._libs.set(libraryName, libraryPath);
|
||||
}
|
||||
/**
|
||||
* Adds a handler for you to handle errors that this instance of mimicry
|
||||
* faces loading and evaluating files.
|
||||
*
|
||||
* These handlers get called after (if there was any) error callbacks passed to load operations.
|
||||
*
|
||||
* @param {Function} handler - a handler that would be called in case of error
|
||||
*
|
||||
* Handler is called with a single argument of error, which can one of the following types:
|
||||
* - Mimicry.FileError - if there was an error on file evaluation
|
||||
* - Mimicry.LoadError - if mimicry could not load a file
|
||||
* */
|
||||
addErrorHandler (handler) {
|
||||
this._errorHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method provides you information about the progress of your loads.
|
||||
* Sometimes when you load lots of files in bulk you wish to show a progress indicator.
|
||||
*
|
||||
* @param {Function} handler - a handler which is called when the progress changes
|
||||
* Passed handler is called with following arguments:
|
||||
* {Number} loaded - how many files mimicry have loaded so far
|
||||
* {Number} pending - how many files are currently loading or in a queue
|
||||
* {Number} total - how many files does mimicry even know about
|
||||
* */
|
||||
addProgressHandler (handler) {
|
||||
this._progressHandlers.push(handler);
|
||||
}
|
||||
/**
|
||||
* If you use Mimicry in a script file included right after mimicry
|
||||
* it might not be able to work yet: it doesn't yet have some classes for normal work.
|
||||
* In this "unready" state you can not
|
||||
* - access Mimicry.FileError nad Mimicry.LoadError classes
|
||||
* - access Mimicry.Type enum.
|
||||
*
|
||||
* You can, however, perform default (meaning Mimicry.type.js) loads, declare modules,
|
||||
* set up paths, and subscribe to handlers.
|
||||
*
|
||||
* @returns {Boolean} - if the instance is ready to be used
|
||||
* */
|
||||
get ready () {return ready;}
|
||||
|
||||
/**
|
||||
* @returns {Number} - amount of files that are currently loading or in a queue
|
||||
* */
|
||||
get pendingFilesAmount () {return this._loadingFiles.size + this._queue.length;}
|
||||
|
||||
/**
|
||||
* @returns {Number} - amount of files mimicry have loaded until this moment
|
||||
* */
|
||||
get loadedFilesAmount () {return this._loadedFiles.size;}
|
||||
|
||||
/**
|
||||
* @returns {Object} - default mimicry options object
|
||||
* */
|
||||
static get defaultOptions () {return defaultOptions;}
|
||||
|
||||
/**
|
||||
* If you use Mimicry in a script file included right after mimicry
|
||||
* it might not be able to work yet: it doesn't yet have some classes for normal work.
|
||||
* In this "unready" state you can not
|
||||
* - access Mimicry.FileError nad Mimicry.LoadError classes
|
||||
* - access Mimicry.Type enum.
|
||||
*
|
||||
* You can, however, perform default (meaning Mimicry.type.js) loads, declare modules,
|
||||
* set up paths, and subscribe to handlers.
|
||||
*
|
||||
* This state is on the static level: once all necessary files are loaded all mimicry instances are
|
||||
* ready and don't need to be waited on
|
||||
*
|
||||
* @returns {Boolean} - if the Mimicry system is ready to be used
|
||||
* */
|
||||
static get ready () {return ready;}
|
||||
|
||||
/**
|
||||
* Normally mimicry loads .js files, but you can also load other stuff like json, or css, or binary
|
||||
* I expect you to pass values from this enum to specify what exactly you are loading if it is not a
|
||||
* JavaScript file
|
||||
*
|
||||
* @returns {Mimicry.Type} - type of content
|
||||
* */
|
||||
static get Type () {return Type;}
|
||||
static get FileError() {return FileError;}
|
||||
static get LoadError() {return LoadError;}
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
maxParallelLoadings: 10,
|
||||
baseUrl: "",
|
||||
seed: ""
|
||||
};
|
||||
Object.freeze(defaultOptions)
|
||||
|
||||
function createDeferredParts () {
|
||||
this._queue = new Queue();
|
||||
}
|
||||
|
||||
function launchDeferredRequests () {
|
||||
if (this._deferred instanceof Array) {
|
||||
while (this._deferred.length) {
|
||||
const args = this._deferred.shift();
|
||||
const methodName = args.shift();
|
||||
this[methodName].apply(this, args);
|
||||
}
|
||||
|
||||
delete this._deferred;
|
||||
}
|
||||
}
|
||||
|
||||
function callCallback(/*Function*/callback, /*String*/originalName) {
|
||||
if (callback instanceof Function) {
|
||||
const module = this._modules.get(originalName);
|
||||
if (module) {
|
||||
module.addCallback(callback);
|
||||
} else {
|
||||
const asset = this._loadedAssets.get(originalName);
|
||||
if (asset)
|
||||
callback(asset);
|
||||
else
|
||||
callback(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} path
|
||||
* @param {Function} callback
|
||||
* @param {Function} error
|
||||
* @param {Type} contentType
|
||||
* @param {String} originalName
|
||||
* */
|
||||
function scheduleLoad (path, callback, error, contentType, originalName) {
|
||||
if (this._loadedFiles.has(path))
|
||||
return callCallback.call(this, callback, originalName);
|
||||
|
||||
let loadingFile = this._loadingFiles.get(path);
|
||||
if (loadingFile === undefined)
|
||||
loadingFile = this._plannedFiles[path];
|
||||
|
||||
if (loadingFile) {
|
||||
Dependency.checkContentType(loadingFile.contentType, contentType, path);
|
||||
loadingFile.addSuccessHandler(callback);
|
||||
loadingFile.addErrorHandler(error);
|
||||
return;
|
||||
}
|
||||
|
||||
loadingFile = new LoadingFile(contentType, originalName, callback, error);
|
||||
if (this._loadingFiles.size < this._config.maxParallelLoadings) {
|
||||
executeLoad.call(this, path, loadingFile);
|
||||
} else {
|
||||
this._queue.push(path);
|
||||
this._plannedFiles[path] = loadingFile;
|
||||
}
|
||||
|
||||
notifyProgressSubscribers.call(this);
|
||||
}
|
||||
|
||||
function executeLoad (/*String*/path, /*LoadingFile*/loadingFile) {
|
||||
this._loadingFiles.set(path, loadingFile);
|
||||
|
||||
const boundLoad = onLoaded.bind(this, path);
|
||||
const boundError = onError.bind(this, path);
|
||||
switch (loadingFile.contentType) {
|
||||
case Type.js:
|
||||
getScriptWithTag(path, boundLoad, boundError, this._config.seed);
|
||||
break;
|
||||
case Type.css:
|
||||
getStylesheetWithTag(path, boundLoad, boundError, this._config.seed);
|
||||
break;
|
||||
case Type.json:
|
||||
getByXHR(path, boundLoad, boundError, "json", this._config.seed);
|
||||
break;
|
||||
case Type.binary:
|
||||
getByXHR(path, boundLoad, boundError, "arraybuffer", this._config.seed);
|
||||
break;
|
||||
case Type.text:
|
||||
getByXHR(path, boundLoad, boundError, "text", this._config.seed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function notifyProgressSubscribers() {
|
||||
const pending = this.pendingFilesAmount;
|
||||
const loaded = this.loadedFilesAmount;
|
||||
for (let i = 0; i < this._progressHandlers.length; ++i) {
|
||||
const handler = this._progressHandlers[i];
|
||||
handler(loaded, pending, loaded + pending);
|
||||
}
|
||||
}
|
||||
|
||||
function onLoaded(/*String*/path, /*HTMLScriptElement*/event) {
|
||||
if (this._moduleErrors.length)
|
||||
return onModuleError.call(this, path, event);
|
||||
|
||||
if (this._errors.has(path))
|
||||
return onError.call(this, path, event);
|
||||
|
||||
const loading = this._loadingFiles.get(path);
|
||||
this._loadedFiles.add(path);
|
||||
switch (loading.contentType) {
|
||||
case Type.json:
|
||||
case Type.binary:
|
||||
case Type.text:
|
||||
this._loadedAssets.set(loading.originalName, event.target.response);
|
||||
break;
|
||||
}
|
||||
loading.callSuccessHandlers(callCallback, this);
|
||||
cleanUp.call(this, path, event);
|
||||
checkNextElement.call(this);
|
||||
notifyProgressSubscribers.call(this);
|
||||
}
|
||||
|
||||
function onError (/*String*/path, /*HTMLScriptElement*/element) {
|
||||
const loading = this._loadingFiles.get(path);
|
||||
let error = this._errors.get(path);
|
||||
if (!(error instanceof BasicError))
|
||||
error = new LoadError(path, loading.originalName);
|
||||
|
||||
loading.callErrorHandlers(error);
|
||||
for (let i = 0; i < this._errorHandlers.length; ++i)
|
||||
this._errorHandlers[i](error);
|
||||
|
||||
cleanUp.call(this, path, element);
|
||||
checkNextElement.call(this);
|
||||
notifyProgressSubscribers.call(this);
|
||||
}
|
||||
|
||||
function onModuleError (/*String*/path, /*HTMLScriptElement*/element) {
|
||||
for (let i = 0; i < this._moduleErrors.length; ++i) {
|
||||
const error = this._moduleErrors[i];
|
||||
error.path = path;
|
||||
|
||||
for (let i = 0; i < this._errorHandlers.length; ++i)
|
||||
this._errorHandlers[i](error)
|
||||
}
|
||||
this._moduleErrors = [];
|
||||
cleanUp.call(this, path, element);
|
||||
checkNextElement.call(this);
|
||||
notifyProgressSubscribers.call(this);
|
||||
}
|
||||
|
||||
function cleanUp (/*String*/path, /*Event*/element) {
|
||||
const loading = this._loadingFiles.get(path);
|
||||
this._loadingFiles.delete(path);
|
||||
this._errors.delete(path);
|
||||
switch (loading.contentType) {
|
||||
case Type.js:
|
||||
cleanTag(element.target);
|
||||
break;
|
||||
case Type.css:
|
||||
cleanUpHandlers(element.target);
|
||||
break;
|
||||
case Type.json:
|
||||
case Type.binary:
|
||||
case Type.text:
|
||||
cleanUpHandlers(element);
|
||||
break
|
||||
}
|
||||
loading.destructor();
|
||||
}
|
||||
|
||||
function cleanTag (/*HTMLElement or Event*/tag) {
|
||||
tag.remove();
|
||||
cleanUpHandlers(tag);
|
||||
}
|
||||
|
||||
function cleanUpHandlers (/*HTMLElement*/tag) {
|
||||
delete tag.onload;
|
||||
delete tag.onerror;
|
||||
}
|
||||
|
||||
function onGlobalError (event, url, line, col, errorObj) {
|
||||
let message;
|
||||
if (event instanceof ErrorEvent) {
|
||||
url = event.filename;
|
||||
line = event.lineno;
|
||||
col = event.colno;
|
||||
errorObj = event.error;
|
||||
message = event.message;
|
||||
} else {
|
||||
message = event;
|
||||
}
|
||||
if (url && url.indexOf(location.origin) === 0)
|
||||
url = url.slice(location.origin.length);
|
||||
|
||||
if (!setFileError(url, message, line, col, errorObj)) {
|
||||
let href = getCurrentHref();
|
||||
const index = url.lastIndexOf(href);
|
||||
if (href.length && index === url.length - href.length)
|
||||
setFileError(href, message, line, col, errorObj)
|
||||
}
|
||||
}
|
||||
|
||||
function setFileError(name, message, line, col, errorObj) {
|
||||
let result = true;
|
||||
let loading = this._loadingFiles.get(name);
|
||||
if (loading instanceof LoadingFile)
|
||||
this._errors.set(name, new FileError(name, loading.originalName, message, line, col, errorObj));
|
||||
else
|
||||
result = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function checkNextElement () {
|
||||
while (!this._queue.empty && (this._loadingFiles.size < this._config.maxParallelLoadings)) {
|
||||
const path = this._queue.pop();
|
||||
const loadingFile = this._plannedFiles[path];
|
||||
delete this._plannedFiles[path];
|
||||
executeLoad.call(this, path, loadingFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} path - path of the file you wish to load
|
||||
* @param {Function} [callback] - handler that would be called upon successful load
|
||||
* @param {Function} [error] - handler that would ba called upon error
|
||||
* @param {String} [seed] - a short seed to avoid unwanted caching
|
||||
* */
|
||||
function getScriptWithTag (path, callback, error, seed) {
|
||||
const tag = document.createElement("script");
|
||||
tag.setAttribute("async", "");
|
||||
document.head.appendChild(tag);
|
||||
tag.onload = callback;
|
||||
tag.onerror = error;
|
||||
if (seed)
|
||||
path += "?seed=" + seed;
|
||||
|
||||
tag.src = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} path - path of the file you wish to load
|
||||
* @param {Function} [callback] - handler that would be called upon successful load
|
||||
* @param {Function} [error] - handler that would ba called upon error
|
||||
* @param {String} [seed] - a short seed to avoid unwanted caching
|
||||
* */
|
||||
function getStylesheetWithTag (path, callback, error, seed) {
|
||||
const tag = document.createElement("link");
|
||||
tag.setAttribute("type", "text/css");
|
||||
tag.setAttribute("rel", "stylesheet");
|
||||
document.head.appendChild(tag);
|
||||
tag.onload = callback;
|
||||
tag.onerror = error;
|
||||
if (seed)
|
||||
path += "?seed=" + seed;
|
||||
|
||||
tag.href = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} url
|
||||
* @param {Function} callback
|
||||
* @param {Function} error
|
||||
* @param {XMLHttpRequestResponseType} [responseType="arraybuffer"]
|
||||
* @param {String} [seed=""]
|
||||
* */
|
||||
function getByXHR (url, callback, error, responseType="arraybuffer", seed="") {
|
||||
const xhr = new XMLHttpRequest();
|
||||
if (seed)
|
||||
url += "?seed=" + seed;
|
||||
|
||||
xhr.open("GET", url, true);
|
||||
xhr.responseType = responseType;
|
||||
xhr.onload = callback.bind(xhr);
|
||||
xhr.onerror = error.bind(xhr);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getCurrentHref() {
|
||||
const script = document.currentScript || launcher;
|
||||
if (script instanceof HTMLScriptElement)
|
||||
return script.attributes.src.value;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
function loadedFileForModule (/*Module*/requester, /*String*/fileName) {
|
||||
const dep = this._modules.get(fileName);
|
||||
if (dep)
|
||||
dep.addCallback(requester.incrementDependency.bind(requester, this));
|
||||
else
|
||||
requester.incrementDependency(this);
|
||||
}
|
||||
|
||||
const launcher = document.currentScript;
|
||||
const instances = [];
|
||||
let BasicError, LoadError, FileError;
|
||||
let LoadingFile;
|
||||
let Queue;
|
||||
let Dependency, Type;
|
||||
let Module;
|
||||
let ready = false;
|
||||
let errorsReady = false;
|
||||
let fileReady = false;
|
||||
let queueReady = false;
|
||||
let dependencyReady = false;
|
||||
let moduleReady = false;
|
||||
|
||||
function checkDeferred () {
|
||||
if (!ready) {
|
||||
if (errorsReady && fileReady && queueReady && dependencyReady && moduleReady) {
|
||||
ready = true;
|
||||
|
||||
for (let i = 0; i < instances.length; ++i) {
|
||||
const instance = instances[i];
|
||||
createDeferredParts.call(instance);
|
||||
launchDeferredRequests.call(instance);
|
||||
}
|
||||
|
||||
if (launcher instanceof HTMLScriptElement) {
|
||||
const main = launcher.dataset.main
|
||||
if (main)
|
||||
getScriptWithTag(main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const src = launcher.src.split("/");
|
||||
src.pop();
|
||||
let pathPrefix = src.join('/');
|
||||
if (pathPrefix !== "")
|
||||
pathPrefix += "/";
|
||||
|
||||
getScriptWithTag(pathPrefix + "errors.js", function (event) {
|
||||
cleanTag(event.target);
|
||||
BasicError = window.__MimicryErrors__.BasicError;
|
||||
LoadError = window.__MimicryErrors__.LoadError;
|
||||
FileError = window.__MimicryErrors__.FileError;
|
||||
delete window.__MimicryErrors__;
|
||||
|
||||
errorsReady = true;
|
||||
checkDeferred();
|
||||
});
|
||||
|
||||
getScriptWithTag(pathPrefix + "loadingFile.js", function (event) {
|
||||
cleanTag(event.target);
|
||||
LoadingFile = window.__MimicryLoadingFile__;
|
||||
delete window.__MimicryLoadingFile__;
|
||||
|
||||
fileReady = true;
|
||||
checkDeferred();
|
||||
});
|
||||
|
||||
getScriptWithTag(pathPrefix + "queue.js", function (event) {
|
||||
cleanTag(event.target);
|
||||
Queue = window.__MimicryQueue__;
|
||||
delete window.__MimicryQueue__;
|
||||
|
||||
queueReady = true;
|
||||
checkDeferred();
|
||||
});
|
||||
|
||||
getScriptWithTag(pathPrefix + "dependency.js", function (event) {
|
||||
cleanTag(event.target);
|
||||
Dependency = window.__MimicryDependency__;
|
||||
Type = Dependency.Type;
|
||||
delete window.__MimicryDependency__;
|
||||
|
||||
dependencyReady = true;
|
||||
checkDeferred();
|
||||
});
|
||||
|
||||
getScriptWithTag(pathPrefix + "module.js", function (event) {
|
||||
cleanTag(event.target);
|
||||
Module = window.__MimicryModule__;
|
||||
delete window.__MimicryModule__;
|
||||
|
||||
moduleReady = true;
|
||||
checkDeferred();
|
||||
});
|
||||
|
||||
return Mimicry;
|
||||
})();
|
187
dependency.js
Normal file
187
dependency.js
Normal file
@ -0,0 +1,187 @@
|
||||
(function privateDependencyClosure () {
|
||||
/**
|
||||
* This primitive class represents Dependency,
|
||||
* just to make the work a bit more structured
|
||||
* */
|
||||
class Dependency {
|
||||
constructor (name, contentType) {
|
||||
this.name = name;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function goes through an array of strings and number to make sense of it
|
||||
*
|
||||
* @param {Array} deps - an array of strings and numbers
|
||||
*
|
||||
* @returns Array<Dependency>
|
||||
* */
|
||||
static refineDependencies (deps) {
|
||||
const result = [];
|
||||
const collection = Object.create(null);
|
||||
for (let i = 0; i < deps.length; ++i) {
|
||||
let element = deps[i];
|
||||
const elementType = typeof element
|
||||
if (!collection[element] && elementType === "string") {
|
||||
result.push(new Dependency(element));
|
||||
collection[element] = true;
|
||||
} else if (result.length > 0 && elementType === "number" && ReverseType[element] !== undefined) {
|
||||
let prevResult = result[result.length - 1];
|
||||
if (deps[i - 1] === prevResult.name)
|
||||
prevResult.contentType = element;
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* Checks one type against another and writes a warning if they don't match
|
||||
*
|
||||
* @param {Type} original
|
||||
* @param {Type} current
|
||||
* @param {String} path
|
||||
* */
|
||||
static checkContentType (original, current, path) {
|
||||
if (original !== current) {
|
||||
const originalType = ReverseType[original];
|
||||
const currentType = ReverseType[current];
|
||||
console.warn("Mimicry error: file " + path +
|
||||
" was originally requested as " + originalType +
|
||||
" but next time it was requested as " + currentType +
|
||||
" ignoring second request type and treating file as " + originalType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refines path turning all meta information into a path to query
|
||||
*
|
||||
* @typedef Mimicry.Type Type
|
||||
*
|
||||
* @param {String} path - a source path
|
||||
* @param {Type} type - type of the content
|
||||
* @param {String} baseUrl - path to all project files
|
||||
* @param {Map<Type, String>} types - collection of paths to specific content types
|
||||
* @param {Map<String, String>} libs - collection of paths to libraries
|
||||
*
|
||||
* @returns {String}
|
||||
* */
|
||||
static refinePath (path, type, baseUrl, types, libs) {
|
||||
const suffix = suffixes[type];
|
||||
const typePath = types.get(type) || "";
|
||||
if (!path.endsWith(suffix))
|
||||
path += suffix;
|
||||
|
||||
switch (type) {
|
||||
case Type.js:
|
||||
if (path[0] === "@") {
|
||||
const hops = path.split("/");
|
||||
const libName = hops[0].slice(1);
|
||||
let libPath = libs.get(libName);
|
||||
if (libPath === undefined)
|
||||
libPath = libName;
|
||||
|
||||
path = baseUrl + libPath + path.slice(libName.length + 1);
|
||||
} else {
|
||||
path = baseUrl + typePath + path;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
path = baseUrl + typePath + path;
|
||||
break;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<String>} dependencies - list of module names that this module depends on
|
||||
* @param {Function} [callback] - module body that will be executed as soon all dependencies are resolved
|
||||
* */
|
||||
static refineModuleArguments (dependencies, callback) {
|
||||
let deps, body;
|
||||
if (dependencies instanceof Function) {
|
||||
body = dependencies;
|
||||
deps = [];
|
||||
} else {
|
||||
deps = dependencies;
|
||||
body = callback;
|
||||
}
|
||||
|
||||
deps = this.refineDependencies(deps);
|
||||
|
||||
return {deps, body};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} href - script DOM element that launched this function
|
||||
* @param {String} baseUrl - path to all project files
|
||||
* @param {Map<Type, String>} types - collection of paths to specific content types
|
||||
* @param {Map<String, String>} libs - collection of paths to libraries
|
||||
* */
|
||||
static nameFromHref (href, baseUrl, types, libs) {
|
||||
const suffix = suffixes[Type.js];
|
||||
const typePath = types.get(Type.js) || "";
|
||||
|
||||
if (href.endsWith(suffix))
|
||||
href = href.slice(0, -suffix.length);
|
||||
|
||||
if (baseUrl.length) {
|
||||
const baseIndex = href.indexOf(baseUrl);
|
||||
if (baseIndex !== 0)
|
||||
console.warn("Module path " + href +
|
||||
" doesn't start with baseUrl " + baseUrl +
|
||||
", this is probably an error");
|
||||
else
|
||||
href = href.slice(baseUrl.length + 1);
|
||||
}
|
||||
|
||||
let foundAmongLibs = false;
|
||||
for (const [libName, libPath] of libs) {
|
||||
const index = href.indexOf(libPath);
|
||||
if (index === 0) {
|
||||
href = "@" + libName + href.slice(libPath.length);
|
||||
foundAmongLibs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundAmongLibs && typePath.length) {
|
||||
const typeIndex = href.indexOf(typePath);
|
||||
if (typeIndex !== 0)
|
||||
console.warn("Module path " + href +
|
||||
" doesn't start with a proper type path " + typePath +
|
||||
", this is probably an error");
|
||||
else
|
||||
href = href.slice(typePath.length + 1);
|
||||
}
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
static get Type () {return Type;}
|
||||
static getTypeTitle (/*Type*/type) {return ReverseType[type];}
|
||||
}
|
||||
|
||||
const Type = {
|
||||
none: 0, //just to prevent implicit situations, DON'T USE IT!
|
||||
js: 1,
|
||||
css: 2,
|
||||
json: 3,
|
||||
binary: 4,
|
||||
text: 5
|
||||
};
|
||||
|
||||
const suffixes = ["", ".js", ".css", ".json", "", ""];
|
||||
const ReverseType = ["None", "JavaScript", "CSS", "JSON", "Binary", "Text"];
|
||||
|
||||
Object.freeze(Type);
|
||||
Object.freeze(suffixes);
|
||||
Object.freeze(ReverseType);
|
||||
|
||||
if (typeof module === 'object' && module.exports)
|
||||
module.exports = Dependency;
|
||||
else
|
||||
window.__MimicryDependency__ = Dependency;
|
||||
})();
|
||||
|
||||
|
45
errors.js
Normal file
45
errors.js
Normal file
@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
(function errorsPrivateClosure () {
|
||||
class BasicError {
|
||||
/**
|
||||
* @param {String} path - path to the file that caused an error
|
||||
* @param {String} originalName - original name of the loading file
|
||||
* */
|
||||
constructor(path, originalName) {
|
||||
this.path = path;
|
||||
this.originalName = originalName;
|
||||
}
|
||||
|
||||
get message () {return "Unknown error about file " + this.originalName + " (" + this.path + ")"};
|
||||
}
|
||||
|
||||
/**
|
||||
* This error happens when Mimicry couldn't access a file
|
||||
* */
|
||||
class LoadError extends BasicError {
|
||||
get message () {
|
||||
return "Couldn't load file " + this.originalName +
|
||||
" (" + this.path + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This error happens when Mimicry couldn't evaluate a file
|
||||
* */
|
||||
class FileError extends BasicError {
|
||||
constructor(path, originalName, message, line, col, nativeError) {
|
||||
super(path, originalName);
|
||||
this._message = message;
|
||||
this._line = line;
|
||||
this._col = col;
|
||||
this._err = nativeError;
|
||||
}
|
||||
get message () {return this._message;}
|
||||
get native () {return this._err;}
|
||||
}
|
||||
|
||||
if (typeof module === 'object' && module.exports)
|
||||
module.exports = {BasicError, LoadError, FileError};
|
||||
else
|
||||
window.__MimicryErrors__ = {BasicError, LoadError, FileError};
|
||||
})();
|
68
loadingFile.js
Normal file
68
loadingFile.js
Normal file
@ -0,0 +1,68 @@
|
||||
"use strict";
|
||||
(function loadingFilePrivateClosure () {
|
||||
/**
|
||||
* A small data structure to give shape to an entity that represents a loading file
|
||||
* */
|
||||
class LoadingFile {
|
||||
/**
|
||||
* @param {Mimicry.Type} contentType - what kind of file is loading
|
||||
* @param {String} originalName - what was the original name before refining
|
||||
* @param {Function} [callback] - optional success handler
|
||||
* @param {Function} [error] - optional error handler
|
||||
* */
|
||||
constructor (contentType, originalName, callback, error) {
|
||||
this.contentType = contentType;
|
||||
this.originalName = originalName;
|
||||
this.success = [];
|
||||
this.error = [];
|
||||
|
||||
this.addSuccessHandler(callback);
|
||||
this.addErrorHandler(error);
|
||||
}
|
||||
/**
|
||||
* Finalizes the object
|
||||
* */
|
||||
destructor () {
|
||||
delete this.contentType;
|
||||
delete this.originalName;
|
||||
delete this.success;
|
||||
delete this.error;
|
||||
}
|
||||
/**
|
||||
* Adds a handler for the case of success
|
||||
* */
|
||||
addSuccessHandler (/*Function*/callback) {
|
||||
if (callback instanceof Function)
|
||||
this.success.push(callback);
|
||||
}
|
||||
/**
|
||||
* Adds a handler for the case of error
|
||||
* */
|
||||
addErrorHandler (/*Function*/callback) {
|
||||
if (callback instanceof Function)
|
||||
this.error.push(callback);
|
||||
}
|
||||
/**
|
||||
* Notifies about success
|
||||
*
|
||||
* @param {Function} callback - a wrapper caller function, to call the actual callback (callCallback function in this file)
|
||||
* @param {Object} context - a context for the wrapper caller function (Mimicry instance in this file)
|
||||
* */
|
||||
callSuccessHandlers (callback, context) {
|
||||
for (let i = 0; i < this.success.length; ++i)
|
||||
callback.call(context, this.success[i], this.originalName);
|
||||
}
|
||||
/**
|
||||
* @param {BasicError} error - what happened
|
||||
* */
|
||||
callErrorHandlers (error) {
|
||||
for (let i = 0; i < this.error.length; ++i)
|
||||
this.error[i](error);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module === 'object' && module.exports)
|
||||
module.exports = LoadingFile;
|
||||
else
|
||||
window.__MimicryLoadingFile__ = LoadingFile;
|
||||
})();
|
102
module.js
Normal file
102
module.js
Normal file
@ -0,0 +1,102 @@
|
||||
"use strict";
|
||||
(function modulePrivateClosure () {
|
||||
/**
|
||||
* A class that represents Mimicry Module
|
||||
* */
|
||||
class Module {
|
||||
/**
|
||||
* @param {Array<Dependency>} deps - array of files that this module depends on
|
||||
* @param {Function} body - body function of the module that defines the module itself
|
||||
* */
|
||||
constructor (deps, body) {
|
||||
this._dependencies = deps;
|
||||
this._body = body;
|
||||
this._loaded = 0;
|
||||
this._callbacks = [];
|
||||
this._value = null;
|
||||
this._executed = false;
|
||||
}
|
||||
/**
|
||||
* @returns {Boolean} - true if the module have already been executed
|
||||
* */
|
||||
get executed () {
|
||||
return this._executed;
|
||||
}
|
||||
/**
|
||||
* @returns {any} - a value that is returned from the module body function
|
||||
* */
|
||||
get value () {
|
||||
return this._value;
|
||||
}
|
||||
/**
|
||||
* @returns {Boolean} - true if the module has its dependencies resolved
|
||||
* */
|
||||
get allDependenciesResolved () {
|
||||
return this.executed || this._loaded === this._dependencies.length;
|
||||
}
|
||||
/**
|
||||
* This function should be called when one of the dependencies are considered to be satisfied
|
||||
* If the dependency is not a module this happens when the file is loaded
|
||||
* If the dependency is a module this happens when the module is executed
|
||||
*
|
||||
* @param {Mimicry} mimicry - Mimicry instance that was loading this module,
|
||||
* it is needed for calling "execute" if the dependencies are resolved
|
||||
* */
|
||||
incrementDependency (mimicry) {
|
||||
++this._loaded
|
||||
if (this.allDependenciesResolved)
|
||||
this.execute(mimicry);
|
||||
}
|
||||
/**
|
||||
* If the modules is executed - calls the callback immediately
|
||||
* Otherwise remembers the callback and executes all of them in the same order
|
||||
* that they were added.
|
||||
*
|
||||
* Callback is called with the following arguments:
|
||||
* first: global object
|
||||
* second: the value of this module
|
||||
*
|
||||
* @param {Function} callback - a callback to call
|
||||
* */
|
||||
addCallback (/*Function*/callback) {
|
||||
if (this._executed)
|
||||
callback.call(null, window, this._value);
|
||||
else
|
||||
this._callbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a module, launching its body function
|
||||
*
|
||||
* @param {Mimicry} mimicry - an instance of Mimicry that was loading this module
|
||||
* It is needed to acquire values of dependencies
|
||||
* */
|
||||
execute (mimicry) {
|
||||
if (this.executed)
|
||||
throw new Error("An attempt to execute a module for the second time");
|
||||
|
||||
if (this._body instanceof Function) {
|
||||
const values = [];
|
||||
for (let i = 0; i < this._dependencies.length; ++i) {
|
||||
const name = this._dependencies[i].name;
|
||||
values.push(mimicry.getModule(name) || mimicry.getAsset(name));
|
||||
}
|
||||
this._value = this._body.call(null, window, values);
|
||||
} else {
|
||||
this._value = null;
|
||||
}
|
||||
this._executed = true;
|
||||
const callbacks = this._callbacks;
|
||||
delete this._callbacks;
|
||||
delete this._dependencies;
|
||||
|
||||
for (let i = 0; i < callbacks.length; ++i)
|
||||
callbacks[i].call(null, window, this._value);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module === 'object' && module.exports)
|
||||
module.exports = Module;
|
||||
else
|
||||
window.__MimicryModule__ = Module;
|
||||
})();
|
77
queue.js
Normal file
77
queue.js
Normal file
@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
(function queuePrivateClosure () {
|
||||
/**
|
||||
* This class is a primitive doubly linked list, I need it to implement a queue
|
||||
* */
|
||||
class Queue {
|
||||
constructor () {
|
||||
this._head = new Node(null);
|
||||
this._tail = this._head;
|
||||
this._head.next = this._tail;
|
||||
this._head.prev = this._tail;
|
||||
this.length = 0;
|
||||
}
|
||||
/**
|
||||
* Check if the queue is empty
|
||||
*
|
||||
* @returns Boolean
|
||||
* */
|
||||
get empty () {return this._head === this._tail;}
|
||||
/**
|
||||
* Adds element to the end of the queue.
|
||||
* Data is not copied, now ownership taken
|
||||
*
|
||||
* @param {any} data - a data you wish to add
|
||||
* */
|
||||
push (data) {
|
||||
const node = new Node(data);
|
||||
node.prev = this._head.prev;
|
||||
node.next = this._head;
|
||||
this._head.prev = node;
|
||||
this._head = node;
|
||||
++this.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes element from the beginning of the queue and returns it
|
||||
*
|
||||
* @returns {any} - a data from the list
|
||||
* @throws Error if the list was empty
|
||||
* */
|
||||
pop () {
|
||||
if (this.empty)
|
||||
throw new Error("An attempt to pop from an empty queue");
|
||||
|
||||
const node = this._tail.prev;
|
||||
const data = node.data;
|
||||
node.prev.next = node.next;
|
||||
node.next.prev = node.prev;
|
||||
if (this._head === node) {
|
||||
this._head = node.next;
|
||||
}
|
||||
--this.length;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class for the Queue, just a data structure with getters
|
||||
* */
|
||||
class Node {
|
||||
constructor (data) {
|
||||
this._data = data;
|
||||
this._next = null; //Node
|
||||
this._prev = null; //Node
|
||||
}
|
||||
get data () {return this._data;}
|
||||
get next () {return this._next;}
|
||||
set next (/*Node*/node) {this._next = node;}
|
||||
get prev () {return this._prev;}
|
||||
set prev (/*Node*/node) {this._prev = node;}
|
||||
}
|
||||
|
||||
if (typeof module === 'object' && module.exports)
|
||||
module.exports = Queue;
|
||||
else
|
||||
window.__MimicryQueue__ = Queue;
|
||||
})();
|
3
test/background.css
Normal file
3
test/background.css
Normal file
@ -0,0 +1,3 @@
|
||||
html {
|
||||
background-color: #c0c0f0;
|
||||
}
|
13
test/index.html
Normal file
13
test/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
|
||||
<script src="../browser.js"></script>
|
||||
<script src="index.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
Initial page
|
||||
</body>
|
||||
</html>
|
44
test/index.js
Normal file
44
test/index.js
Normal file
@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
test();
|
||||
function test () {
|
||||
if (!Mimicry) {
|
||||
log("Mimicry class was not loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
log("Mimicry class is loaded");
|
||||
log("Mimicry ready: " + Mimicry.ready);
|
||||
|
||||
let mimicry;
|
||||
try {
|
||||
mimicry = new Mimicry();
|
||||
window.mimicry = mimicry;
|
||||
log("Mimicry was successfully instantiated");
|
||||
} catch (e) {
|
||||
log("Error instantiating Mimicry");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mimicry.module(["module"], function (global, [module]) {
|
||||
log("Mimicry empty module successfully resolved");
|
||||
log("The background now is supposed to be bluish if you're testing in browser")
|
||||
if (module === true)
|
||||
log("Value returned from additional module is correct");
|
||||
else
|
||||
log("Value returned from additional module is \"" + module + "\", which is incorrect");
|
||||
});
|
||||
log("Successfully launched Mimicry empty module");
|
||||
} catch (e) {
|
||||
log("Error launching Mimicry empty module");
|
||||
//return;
|
||||
}
|
||||
}
|
||||
|
||||
function log (message) {
|
||||
const node = document.createTextNode(message);
|
||||
const br = document.createElement("br");
|
||||
document.body.appendChild(node);
|
||||
document.body.appendChild(br);
|
||||
}
|
12
test/index_main.html
Normal file
12
test/index_main.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
|
||||
<script src="../browser.js" data-main="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Initial page
|
||||
</body>
|
||||
</html>
|
7
test/module.js
Normal file
7
test/module.js
Normal file
@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
mimicry.module([
|
||||
"terminalModule",
|
||||
"background", Mimicry.Type.css
|
||||
], function (global, [terminalModule]) {
|
||||
return terminalModule === "terminal";
|
||||
});
|
3
test/terminalModule.js
Normal file
3
test/terminalModule.js
Normal file
@ -0,0 +1,3 @@
|
||||
mimicry.module(() => {
|
||||
return "terminal";
|
||||
});
|
Loading…
Reference in New Issue
Block a user