motion
Version:
motion - moving development forward
387 lines (308 loc) • 10.6 kB
JavaScript
import 'whatwg-fetch'
import 'reapp-object-assign'
import hashsum from 'hash-sum'
import ee from 'event-emitter'
import React from 'react'
import ReactDOM from 'react-dom'
import rafBatch from './lib/reactRaf'
import { StyleRoot, keyframes } from 'motion-radium'
import regeneratorRuntime from './vendor/regenerator'
import './shim/root'
import './shim/exports'
import './shim/on'
import './lib/promiseErrorHandle'
import internal from './internal'
import onError from './shim/motion'
import createComponent from './createComponent'
import range from './lib/range'
import iff from './lib/iff'
import router from './lib/router'
import requireFactory from './lib/requireFactory'
import staticStyles from './lib/staticStyles'
import safeRun from './lib/safeRun'
import reportError from './lib/reportError'
import arrayDiff from './lib/arrayDiff'
import createElement from './tag/createElement'
import ErrorDefinedTwice from './views/ErrorDefinedTwice'
import NotFound from './views/NotFound'
import LastWorkingMainFactory from './views/LastWorkingMain'
import MainErrorView from './views/Main'
const folderFromFile = (filePath) =>
filePath.indexOf('/') < 0 ? '' : filePath.substring(0, filePath.lastIndexOf('/'))
/*
Welcome to Motion!
This file deals mostly with setting up Motion,
loading views and files, rendering,
and exposing the public Motion functions
*/
const Motion = {
// set up motion shims
init() {
const originalWarn = console.warn
// filter radium warnings for now
console.warn = (...args) => {
if (args[0] && args[0].indexOf('Unsupported CSS property "display') == 0) return
if (args[0] && args[0].indexOf('Radium:') == 0) return
originalWarn.call(console, ...args)
}
// prevent breaking when writing $ styles in auto-save mode
if (!process.env.production) {
root.$ = null
}
if (process.env.production) {
rafBatch.inject()
}
// shims
root.React = React
root.ReactDOM = ReactDOM
root.global = root // for radium
root.regeneratorRuntime = regeneratorRuntime
root.on = on
root.fetch.json = (a, b, c) => fetch(a, b, c).then(res => res.json())
root.require = requireFactory(root)
},
// run an app
run(name, _opts, afterRenderCb) {
// default opts
const opts = Object.assign({
node: '_motionapp'
}, _opts)
const ID = ''+Math.random()
// init require
root.require.setApp(name)
// init Internal
const Internal = internal.init(ID)
root._Motion = Internal
// tools bridge
const Tools = root._DT
if (!process.env.production && Tools) {
// pass data from tools to internal
Tools.emitter.on('editor:state', () => {
Internal.editor = Tools.editor
})
}
// setup shims that use Internal
onError(Internal, Tools)
const LastWorkingMain = LastWorkingMainFactory(Internal)
const emitter = ee({})
//
// begin the motionception
//
const Motion = {
start() {
router.init(ID, { onChange: Motion.render })
Motion.render()
},
views: {},
// beta
_onViewInstance: (name, decorator) => !decorator
? Internal.instanceDecorator.all = name
: Internal.instanceDecorator[name] = decorator,
// decorate a view instance
decorateView: (name, decorator) => !decorator
? Internal.viewDecorator.all = name
: Internal.viewDecorator[name] = decorator,
keyframes,
router,
// async functions before loading app
preloaders: [],
preload(fn) { Motion.preloaders.push(fn) },
render() {
if (Motion.preloaders.length) {
return Promise
.all(Motion.preloaders.map(loader => typeof loader == 'function' ? loader() : loader))
.then(run)
}
else
run()
function run() {
Internal.isRendering++
if (Internal.isRendering > 3) return
// find Main
let Main = Internal.views.Main && Internal.views.Main.component
if (!Main && Internal.lastWorkingRenders.Main)
Main = LastWorkingMain
if (!Main)
Main = MainErrorView
// server render
if (!opts.node) {
Motion.renderedToString = React.renderToString(<Main />)
afterRenderCb && afterRenderCb(Motion.renderedToString)
}
// browser render
else {
if (window.__isDevingDevTools)
opts.node = '_motiondevtools'
ReactDOM.render(
<StyleRoot className="__motionRoot">
<Main />
</StyleRoot>,
document.getElementById(opts.node)
)
}
Internal.lastWorkingViews.Main = Main
emitter.emit('render:done')
Internal.isRendering = 0
}
},
// motion events
on(name, cb) {
emitter.on(name, cb)
},
// for use in jsx
debug: () => { debugger },
view(name, body) {
const comp = opts => createComponent(Motion, Internal, name, body, opts)
if (process.env.production)
return setView(name, comp())
const hash = hashsum(body)
function setView(name, component) {
Internal.views[name] = { hash, component, file: Internal.currentHotFile }
Motion.views[name] = component
}
// set view in cache
let viewsInFile = Internal.viewsInFile[Internal.currentHotFile]
if (viewsInFile)
viewsInFile.push(name)
// if new
if (!Internal.views[name]) {
setView(name, comp({ hash, changed: true }))
Internal.changedViews.push(name)
return
}
// dev stuff
if (!process.env.production) {
if (!Internal.mountedViews[name])
Internal.mountedViews[name] = []
// not new
// if defined twice during first run
if (Internal.firstRender && !Internal.runtimeErrors) {
Internal.views[name] = ErrorDefinedTwice(name)
throw new Error(`Defined a view twice: ${name}`)
}
// if unchanged
if (Internal.views[name].hash == hash) {
setView(name, comp({ hash, unchanged: true }))
return
}
// changed
setView(name, comp({ hash, changed: true }))
Internal.changedViews.push(name)
// this resets tool errors
window.onViewLoaded()
}
},
getView(name) {
return Motion.views[name] || NotFound(name)
},
//
// private API (TODO move this)
//
// styles, TODO: move to Internal
staticStyles,
viewRoots: {},
styleClasses: {},
styleObjects: {},
// load a file
file(file, run) {
if (!process.env.production) {
Internal.viewsInFile[file] = []
Internal.changedViews = []
Internal.currentHotFile = file
Internal.caughtRuntimeErrors = 0
// send runtime success before render
Tools && Tools.emitter.emit('runtime:success')
Internal.lastFileLoad = Date.now()
}
// set up require for file that resolves relative paths
const fileFolder = folderFromFile(file)
const scopedRequire = pkg => root.require(pkg, fileFolder)
// run file!
run(scopedRequire)
if (!process.env.production) {
const cached = Internal.viewCache[file] || Internal.viewsInFile[file]
const views = Internal.viewsInFile[file]
// views.map(view => {
// Motion.timer.time(view, Motion.timer.lastMsgInfo)
// })
// remove Internal.viewsInFile that werent made
const removedViews = arrayDiff(cached, views)
removedViews.map(Internal.removeView)
Internal.currentHotFile = null
Internal.viewCache[file] = Internal.viewsInFile[file]
if (Internal.firstRender)
return
const isNewFile = !cached.length
const addedViews = arrayDiff(views, cached)
// safe re-render
if (isNewFile || removedViews.length || addedViews.length)
return Motion.render()
// if outside of views the FILE changed, refresh all views in file
if (!Internal.changedViews.length && Internal.fileChanged[file]) {
Internal.changedViews = Internal.viewsInFile[file]
}
Internal.changedViews.forEach(name => {
if (!Internal.mountedViews[name]) return
Internal.mountedViews[name] = Internal.mountedViews[name].map(view => {
if (view.isMounted()) {
view.forceUpdate()
return view
}
}).filter(x => !!x)
emitter.emit('render:done')
// views.map(view => Motion.timer.done(view))
// Motion.timer.lastMsgInfo = null
})
}
},
removeFile(file) {
const viewsFromFile = Internal.viewsInFile[file]
viewsFromFile.forEach(Internal.removeView)
delete Internal.viewCache[file]
delete Internal.viewsInFile[file]
},
deleteFile(name) {
const viewsInFile = Internal.viewsInFile[name]
if (viewsInFile) {
Internal.viewsInFile[name].map(Internal.removeView)
delete Internal.viewsInFile[name]
}
delete Internal.viewCache[name]
Motion.render()
},
routeMatch(path) {
router.add(path)
return router.isActive(path)
},
routeParams(path) {
return router.getParams(path)
},
inspect(path, cb) {
Internal.inspector[path] = cb
Internal.setInspector(path)
},
reportError,
range,
iff,
noop: function(){},
}
// view shim (TODO freeze)
root.view = {
el(info, props, ...children) {
if (typeof info[0] === 'string' && info[0].charAt(0).toUpperCase() === info[0].charAt(0)) {
return React.createElement(Motion.getView(info[0]), props)
}
return React.createElement(info[0], props, ...children);
}
}
// prevent overwriting
Object.freeze(Motion)
// if given an app, run it
if (name && root.exports[name]) {
const app = root.exports[name]
app(Motion, opts)
}
return Motion
}
}
root.exports.motion = Motion