From 7850b0eaa9f705b63af40908ffc0a335dfe45245 Mon Sep 17 00:00:00 2001 From: blue Date: Fri, 8 Sep 2023 16:52:21 -0300 Subject: [PATCH] Minimal first working version --- browser.js | 680 +++++++++++++++++++++++++++++++++++++++++ dependency.js | 187 ++++++++++++ errors.js | 45 +++ loadingFile.js | 68 +++++ module.js | 102 +++++++ queue.js | 77 +++++ test/background.css | 3 + test/index.html | 13 + test/index.js | 44 +++ test/index_main.html | 12 + test/module.js | 7 + test/terminalModule.js | 3 + 12 files changed, 1241 insertions(+) create mode 100644 browser.js create mode 100644 dependency.js create mode 100644 errors.js create mode 100644 loadingFile.js create mode 100644 module.js create mode 100644 queue.js create mode 100644 test/background.css create mode 100644 test/index.html create mode 100644 test/index.js create mode 100644 test/index_main.html create mode 100644 test/module.js create mode 100644 test/terminalModule.js diff --git a/browser.js b/browser.js new file mode 100644 index 0000000..290f319 --- /dev/null +++ b/browser.js @@ -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} 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; +})(); \ No newline at end of file diff --git a/dependency.js b/dependency.js new file mode 100644 index 0000000..c278fa8 --- /dev/null +++ b/dependency.js @@ -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 + * */ + 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} types - collection of paths to specific content types + * @param {Map} 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} 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} types - collection of paths to specific content types + * @param {Map} 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; +})(); + + diff --git a/errors.js b/errors.js new file mode 100644 index 0000000..7bbbd10 --- /dev/null +++ b/errors.js @@ -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}; +})(); \ No newline at end of file diff --git a/loadingFile.js b/loadingFile.js new file mode 100644 index 0000000..199f155 --- /dev/null +++ b/loadingFile.js @@ -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; +})(); \ No newline at end of file diff --git a/module.js b/module.js new file mode 100644 index 0000000..2e7a9e3 --- /dev/null +++ b/module.js @@ -0,0 +1,102 @@ +"use strict"; +(function modulePrivateClosure () { + /** + * A class that represents Mimicry Module + * */ + class Module { + /** + * @param {Array} 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; +})(); \ No newline at end of file diff --git a/queue.js b/queue.js new file mode 100644 index 0000000..999722c --- /dev/null +++ b/queue.js @@ -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; +})(); \ No newline at end of file diff --git a/test/background.css b/test/background.css new file mode 100644 index 0000000..5500512 --- /dev/null +++ b/test/background.css @@ -0,0 +1,3 @@ +html { + background-color: #c0c0f0; +} \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..469924e --- /dev/null +++ b/test/index.html @@ -0,0 +1,13 @@ + + + + + Title + + + + + + Initial page + + \ No newline at end of file diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..9eea9c7 --- /dev/null +++ b/test/index.js @@ -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); +} \ No newline at end of file diff --git a/test/index_main.html b/test/index_main.html new file mode 100644 index 0000000..08afdef --- /dev/null +++ b/test/index_main.html @@ -0,0 +1,12 @@ + + + + + Title + + + + + Initial page + + \ No newline at end of file diff --git a/test/module.js b/test/module.js new file mode 100644 index 0000000..f2511b3 --- /dev/null +++ b/test/module.js @@ -0,0 +1,7 @@ +"use strict"; +mimicry.module([ + "terminalModule", + "background", Mimicry.Type.css +], function (global, [terminalModule]) { + return terminalModule === "terminal"; +}); \ No newline at end of file diff --git a/test/terminalModule.js b/test/terminalModule.js new file mode 100644 index 0000000..fa9b6b0 --- /dev/null +++ b/test/terminalModule.js @@ -0,0 +1,3 @@ +mimicry.module(() => { + return "terminal"; +}); \ No newline at end of file