spa
Version:
Single-page applications builder with continuous integration, granular updates and loader.
763 lines (669 loc) • 27.7 kB
text/coffeescript
class AbstractMethodError extends Error
constructor: (name) ->
= "AbstractMethodError"
= "Calling abstract method `#{name}` detected."
class UndeclaredRequireError extends Error
constructor: (, ) ->
= "UndeclaredRequireError"
= "Found unreserved attempt to require of `#{@require_name}` inside `#{@self_name}`"
class ChangesInWindowError extends Error
constructor: (, ) ->
= "ChangesInWindowError"
= "During `#{@self_name}` loading window object was polluted with: #{props}"
class DBError extends Error
constructor: (, ) ->
= "DBError"
= "Error #{@error} occured during loading of module #{@url}."
class NoSourceError extends Error
constructor: () ->
= "NoSourceError"
= "Module #{@url} source was not found in local database. Probably it was not loaded."
class ExportsViolationError extends Error
constructor: () ->
= "ExportsViolationError"
= "Modules `#{@self_name}` overrides own exports by replacing. `exports` != `module.exports`"
class ReturnPollutionError extends Error
constructor: (, ) ->
= "ReturnPollutionError"
= "Code of `#{@self_name}` contains `return` statement in module scope."
class ThisPollutionError extends Error
constructor: (, ) ->
= "ThisPollutionError"
= "Code of `#{@self_name}` trying to modify host object."
class AMDReturnsNothingError extends Error
constructor: () ->
= "AMDReturnsNothingError"
= "AMD module `#{@self_name}` returns nothing. Should return empty object!"
class VersionMismatchedError extends Error
constructor: (, ) ->
= "VersionMismatchedError"
= "Invalid manifest version. Got: #{@got}. Expected: #{@expected}"
class HashFuncMismatchedError extends Error
constructor: (, ) ->
= "HashFuncMismatchedError"
= "Invalid manifest hash function. Got: #{@got}. Expected: #{@expected}"
class HashMismatchedError extends Error
constructor: (, , ) ->
= "HashMismatchedError"
= "Downloaded from url `#{@url}` file has unexpected checksum hash. Got: #{@got}. Expected: #{@expected}"
waitAll = (array, reduce, map) ->
items = array.concat()
results = []
received = []
counter = 0
items.forEach (item, index) ->
map item, (result) ->
counter++
results[index] = result
received[index] = true
for i in [0..items.length-1]
return if !received[i]
return reduce(results)
return
return
XHR = -> new XMLHttpRequest()
class BasicEvaluator
constructor: (options) ->
= options.id
= options.url
= options.source
= options.dependencies
= {}
= []
=
= []
render: -> throw new AbstractMethodError("render")
run: ->
code =
func = new Function(code)
result = func.call(this)
return null if .length > 0
return
get_window: ->
WrapperType = -> return
WrapperType.prototype = window
wrapper = new WrapperType()
= []
for prop of window
.push(prop)
define_property = (prop) ->
cell =
value: window[prop]
if typeof window[prop] is 'function'
Object.defineProperty wrapper, prop,
get: ->
return -> cell.value.apply(window, arguments)
set: (value) ->
cell.value = value
else
Object.defineProperty wrapper, prop,
get: ->
return wrapper if window[prop] is window
return cell.value
set: (value) ->
if typeof value is 'function'
cell.value = -> value.apply(wrapper, arguments)
else
cell.value = value
return
define_property(prop)
wrapper.addEventListener = (type, listener, useCapture) ->
if 'message' is type
wrap_listener = (originalListener) ->
return (event) ->
newEvent = document.createEvent('MessageEvent')
newEvent.initMessageEvent(
'message',
event.bubbles,
event.cancelable,
event.data,
event.origin,
event.lastEventId,
wrapper,
event.ports
)
originalListener(newEvent)
listener = wrap_listener(listener)
return window.addEventListener.call(window, type, listener, useCapture)
return wrapper
get_require: -> throw new AbstractMethodError("get_require")
_fail: (reason) ->
.push(reason)
throw reason
_check: (result) -> throw new AbstractMethodError("_check")
_check_window: (wrapper) ->
added_props = (prop for prop of wrapper when prop not in )
unless added_props.length == 0
throw new ChangesInWindowError(, added_props)
return
_make: -> throw new AbstractMethodError("_make")
get_require: ->
require = (name) ->
if name not of
return [name]
return require.bind(this);
class CJSEvaluator extends BasicEvaluator
constructor: (options) ->
super(options)
= {}
= {}
.exports =
=
=
env: {}
["exports"] =
["module"] =
["require"] =
render: -> return """
return (function(module, exports, require, window, process) {
#{@source};
}).call(this.this, this.module, this.exports, this.require, this.window, this.process);
\n//# sourceURL=#{@url}\n
"""
_check: (result) ->
unless is .exports or Object.keys().length == 0
throw new ExportsViolationError()
if result?
throw new ReturnPollutionError(, Object.keys(result))
this_keys = Object.keys()
unless this_keys.length == 0
throw new ThisPollutionError(, this_keys)
_make: ->
return .exports
class AMDEvaluator extends BasicEvaluator
constructor: (options) ->
super(options)
=
render: -> return """
return (function(define, window) {
#{@source};
}).call(this.this, this.define, this.window);
\n//# sourceURL=#{@url}\n
"""
_check: (result) ->
if result?
throw new ReturnPollutionError(, Object.keys(result))
this_keys = Object.keys()
unless this_keys.length == 0
throw new ThisPollutionError(, this_keys)
class NodepsAMDEvaluator extends AMDEvaluator
get_define: ->
define = (exports) ->
= exports
return define.bind(this);
_make: ->
return
class FactoryAMDEvaluator extends AMDEvaluator
constructor: (options) ->
super(options)
=
get_define: ->
define = (func) ->
= func.call(this.this, )
return define.bind(this)
_check: (result) ->
super(result)
unless ?
throw new AMDReturnsNothingError()
_make: ->
return
class REMAMDEvaluator extends AMDEvaluator
constructor: (options) ->
super(options)
= {}
= {}
.exports =
=
get_define: ->
define = (func) ->
= func.call(this.this, , , )
return define.bind(this)
_check: (result) ->
super(result)
unless is .exports or Object.keys().length == 0
throw new ExportsViolationError()
if ?
throw new ReturnPollutionError(, Object.keys())
_make: ->
return .exports
class DepsAMDEvaluator extends AMDEvaluator
constructor: (options) ->
super(options)
= {}
= {}
.exports =
=
["exports"] =
["module"] =
["require"] =
get_define: ->
define = (names, func) ->
deps = ([name] for name in names)
= func.apply(this.this, deps)
return define.bind(this)
_check: (result) ->
super(result)
if ?
unless Object.keys().length == 0
throw new ExportsViolationError()
unless .exports is null or Object.keys(.exports).length == 0
throw new ExportsViolationError()
else
unless is .exports or Object.keys().length == 0
throw new ExportsViolationError()
if is .exports and Object.keys().length == 0
throw new AMDReturnsNothingError()
_make: ->
return if ?
return .exports
class NamedAMDEvaluator extends DepsAMDEvaluator
get_define: ->
define = (own_name, names, func) ->
deps = ([name] for name in names)
= func.apply(this.this, deps)
return define.bind(this)
class PollutionEvaluator extends BasicEvaluator
render: ->
names = ["window"]
.concat(name for name of )
.join(", ")
args = ["this.this", "this.window"]
.concat("this.deps[\"#{name}\"]" for name of )
.join(", ")
return """
return (function(#{names}) {
#{@source};
}).call(#{args});
\n//# sourceURL=#{@url}\n
"""
_check: (result) ->
if result?
throw new ReturnPollutionError(, Object.keys(result))
get_window: ->
result = super()
for name, value of
result[name] = value
return result
_make: ->
result = {}
for own name, value of
result[name] = value
for own name, value of
result[name] = value
return result
class RawEvaluator extends BasicEvaluator
render: -> return """
#{@source}
\n//# sourceURL=#{@url}\n
"""
_check: (result) ->
get_window: -> return window
_make: -> return {}
SAFE_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
class Logger
constructor: () ->
info: (args...) ->
console.info(, args...)
warn: (args...) ->
console.warn(, args...)
error: (args...) ->
console.error(, args...)
class Loader
constructor: (options) ->
= {}
= null
= false
= []
= null
= 0
=
evaluate_module_start: []
evaluate_module_loaded_from_db: []
update_module_start: []
update_module_loaded_from_db: []
evaluate_module_decoded: []
= options.version
= options.prefix
= options.hash_name
= options.hash_func
= options.decoder_func
= options.randomize_urls
= options.manifest_location ? "manifest.json"
= options
= options.logger ? new Logger("LOADER:#{@prefix}")
= + "::manifest"
localforage.config
name: + "-db"
_get_evaluator: (module) ->
return switch module.type ? "cjs"
when "cjs" then CJSEvaluator
when "amd" then switch module.amdtype ? "deps"
when "nodeps" then NodepsAMDEvaluator
when "factory" then FactoryAMDEvaluator
when "rem" then REMAMDEvaluator
when "deps" then DepsAMDEvaluator
when "named" then NamedAMDEvaluator
else throw new TypeError("Invalid amd-type module `#{module.amdtype}`")
when "junk" then PollutionEvaluator
when "raw" then RawEvaluator
else throw new TypeError("Invalid module type `#{module.type}`")
_prepare_url: (url) ->
return escape(url) unless
result = ''
for i in [0..16]
result += SAFE_CHARS[Math.round(Math.random() * (SAFE_CHARS.length - 1))]
return escape(url) + '?' + result
_parse_manifest: (content) ->
throw ReferenceError("Manifest was not defined") unless content?
raw = JSON.parse(content)
throw TypeError("Invalid manifest format") unless raw instanceof Object
throw TypeError("Invalid manifest format") unless raw.modules?
throw new VersionMismatchedError(raw.version, ) unless raw.version is
throw new HashFuncMismatchedError(raw.hash_func, ) unless raw.hash_func is
manifest =
content: content
modules: raw.modules
version: raw.version
hash_func: raw.hash_func
hash:
if raw.bundle?
manifest.bundle =
hash: raw.bundle.hash
url: raw.bundle.url
return manifest
get_manifest: ->
return
set_manifest: (manifest) ->
window.localStorage.setItem(, manifest.content)
return
del_manifest: ->
window.localStorage.removeItem()
return
make_key: (module) ->
return + ":" + module.hash + ":" + module.url
get_content: (key, cb) ->
return localforage.getItem(key, cb)
set_content: (key, content, cb) ->
.info("storing", key)
return localforage.setItem(key, content, cb)
get_contents_keys: (cb) ->
localforage.keys()
.then (keys) =>
for key in keys
cb(key)
return
.catch (err) =>
return .error(err)
return
del_content: (key, cb) ->
.warn("removing", key)
return localforage.removeItem(key, cb)
onNoManifest: (error) ->
onUpToDate: (event, manifest) ->
onUpdateFound: (event, manifest) ->
onUpdateFailed: (event, error)->
onUpdateCompleted: (manifest) -> return true
onModuleBeginDownload: (module) ->
onModuleDownloadFailed: (event, module, error) ->
onModuleDownloadProgress: (event, module) ->
onTotalDownloadProgress: (progress) ->
onModuleDownloaded: (module) ->
onEvaluationStarted: (manifest) -> return true
onEvaluationError: (module, error) ->
onModuleEvaluated: (module) ->
onApplicationReady: (manifest) ->
emit: (name, args...) ->
.info(name, args...)
try
return this["on" + name](args...)
catch error
.error(error)
load: ->
try
=
catch error
return
.info("Current manifest", )
unless is false
.then =>
return
evaluate: (queue) ->
.evaluate_module_start.push(new Date().getTime())
queue = queue.concat()
if queue.length is 0
return
module = queue.shift()
key =
return
.then (module_source) =>
.evaluate_module_loaded_from_db.push(new Date().getTime())
unless module_source?
return
try
#28 ISSUE. Decoder function could take module specific parameters, manifest specific parameters or ask for some cooperation
module.source =
catch error
return
.evaluate_module_decoded.push(new Date().getTime())
deps = {}
for alias, dep of module.deps
deps[alias] = [dep]
deps["loader"] = this
evaluator_type =
evaluator = new evaluator_type
id: module.id
source: module.source
dependencies: deps
url: module.url
try
namespace = evaluator.run()
catch error
return
[module.id] = namespace
module.namespace = namespace
return
.catch (err) =>
return
checkUpdate: () ->
return false if
.info("Checking for update...")
manifest_request = XHR()
manifest_request.open("GET", , true)
manifest_request.overrideMimeType("application/json; charset=utf-8")
manifest_request.onload = (event) =>
if event.target.status is 404
return
try
=
catch error
return
.info("New manifest", )
if ?
if .hash == .hash
return
manifest_request.onerror = (event) =>
manifest_request.onabort = (event) =>
manifest_request.send()
return true
startUpdate: ->
return false if
.info("Starting update...")
= true
for module in .modules
module.loaded = 0
= .modules.concat()
if not ? and .bundle?
else
for module in .splice(0, 4)
return true
dropData: ->
_downloadBundle: ->
bundle = .bundle
bundle_request = XHR()
bundle_request.open("GET", , true)
bundle_request.responseType = "arraybuffer"
bundle_request.onload = (event) =>
bundle_source = event.target.response
hash =
if hash != bundle.hash
return
return
bundle_request.onprogress = (event) =>
total_size = 0
total_count = 0
for module in .modules
total_size += module.size
total_count++
progress =
loaded_count: 0
total_count: total_count
loaded_size: event.loaded
total_size: total_size
bundle_request.onerror = (event) =>
bundle_request.onabort = (event) =>
bundle_request.send()
return
_disassembleBundle: (bundle) ->
pointer = 0
for module in .modules
module_source = bundle.slice(pointer, pointer + module.size)
hash =
if hash != module.hash
return
module.source = module_source
module.loaded = module.size
pointer += module.size
_storeModule: (module) ->
onNextModule = (module) =>
.then (content) =>
return
.catch (err) =>
return
_updateModule: (module) ->
.update_module_start.push(new Date().getTime())
key =
onNextModule = (module) =>
onDownload = =>
.then (module_source) =>
.update_module_loaded_from_db.push(new Date().getTime())
return unless module_source?
if != module.hash
return
module.source = module_source
module.loaded = module.size
return
.catch (err) =>
return
return
_reportTotalProgress: ->
loaded_size = 0
total_size = 0
loaded_count = 0
total_count = 0
for module in .modules
total_size += module.size
loaded_size += module.loaded
total_count++
if module.source?
loaded_count++
progress =
loaded_count: loaded_count
total_count: total_count
loaded_size: loaded_size
total_size: total_size
_downloadModule: (module, cb) ->
module_request = XHR()
module_request.open("GET", , true)
module_request.responseType = "arraybuffer"
module_request.onload = (event) =>
module_source = event.target.response
hash =
if hash != module.hash
return
.then (content) =>
module.source = module_source
module.loaded = module.size
cb()
return
.catch (err) =>
return
module_request.onprogress = (event) =>
module.loaded = event.loaded
module_request.onerror = (event) =>
module_request.onabort = (event) =>
module_request.send()
return
_checkAllUpdated: (cb) ->
next = .shift()
if next?
cb(next)
return
for module in .modules
return unless module.source?
if
=
= null
= false
return
_cleanUp: ->
useful = ( for module in .modules)
useful.push()
(key) =>
return unless key? # wierd error
return unless key.indexOf() is 0
return if key in useful
.catch (error) => .error(error)
return
return