├── .bowerrc ├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── README.md ├── app ├── app.js ├── components │ ├── current-queue-blip.js │ └── run-loop-queue.js ├── controllers │ └── application.js ├── models │ └── stubbed_ember.js ├── router.js ├── routes │ └── application.js ├── styles │ └── app.scss └── templates │ ├── application.hbs │ └── components │ ├── current-queue-blip.hbs │ └── run-loop-queue.hbs ├── bower.json ├── karma.conf.js ├── package.json ├── public ├── assets │ └── .gitkeep └── index.html ├── tasks ├── helpers.js ├── locking.js └── options │ ├── clean.js │ ├── coffee.js │ ├── compass.js │ ├── concat_sourcemap.js │ ├── connect.js │ ├── copy.js │ ├── cssmin.js │ ├── dom_munger.js │ ├── emberTemplates.js │ ├── emblem.js │ ├── jshint.js │ ├── karma.js │ ├── less.js │ ├── rev.js │ ├── sass.js │ ├── stylus.js │ ├── transpile.js │ ├── usemin.js │ ├── useminPrepare.js │ └── watch.js ├── tests ├── acceptance │ └── index_test.js ├── index.html ├── test_helper.js ├── test_loader.js └── unit │ ├── .gitkeep │ └── routes │ └── index_test.js └── vendor └── loader.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "vendor" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /vendor/* 10 | !/vendor/loader.js 11 | 12 | # misc 13 | /.sass-cache 14 | /connect.lock 15 | /libpeerconnection.log 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "Ember", 8 | "Em", 9 | "$", 10 | "QUnit", 11 | "define", 12 | "console", 13 | "require", 14 | "requireModule", 15 | "equal", 16 | "notEqual", 17 | "notStrictEqual", 18 | "test", 19 | "asyncTest", 20 | "testBoth", 21 | "testWithDefault", 22 | "raises", 23 | "throws", 24 | "deepEqual", 25 | "start", 26 | "stop", 27 | "ok", 28 | "strictEqual", 29 | "module", 30 | "process", 31 | "expect", 32 | "visit", 33 | "exists", 34 | "fillIn", 35 | "click", 36 | "find" 37 | ], 38 | "node" : false, 39 | "browser" : false, 40 | "boss" : true, 41 | "curly": false, 42 | "debug": false, 43 | "devel": false, 44 | "eqeqeq": true, 45 | "evil": true, 46 | "forin": false, 47 | "immed": false, 48 | "laxbreak": false, 49 | "newcap": true, 50 | "noarg": true, 51 | "noempty": false, 52 | "nonew": false, 53 | "nomen": false, 54 | "onevar": false, 55 | "plusplus": false, 56 | "regexp": false, 57 | "undef": true, 58 | "sub": true, 59 | "strict": false, 60 | "white": false, 61 | "eqnull": true, 62 | "esnext": true 63 | } 64 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | // To support Coffeescript, SASS, LESS and others, just install 3 | // the appropriate grunt package and it will be automatically included 4 | // in the build process: 5 | // 6 | // * for Coffeescript, run `npm install --save-dev grunt-contrib-coffee` 7 | // 8 | // * for SASS (SCSS only), run `npm install --save-dev grunt-sass` 9 | // * for SCSS/SASS support (may be slower), run 10 | // `npm install --save-dev grunt-contrib-sass` 11 | // This depends on the ruby sass gem, which can be installed with 12 | // `gem install sass` 13 | // 14 | // * for LESS, run `npm install --save-dev grunt-contrib-less` 15 | // 16 | // * for Stylus/Nib, `npm install --save-dev grunt-contrib-stylus` 17 | // 18 | // * for Compass, run `npm install --save-dev grunt-contrib-compass` 19 | // This depends on the ruby compass gem, which can be installed with 20 | // `gem install compass` 21 | // You should not install SASS if you have installed Compass. 22 | // 23 | // * for Emblem, run the following commands: 24 | // `npm uninstall --save-dev grunt-ember-templates` 25 | // `npm install --save-dev grunt-emblem` 26 | // `bower install emblem.js --save` 27 | // 28 | // If you use SASS, LESS or Stylus, don't forget to delete 29 | // `public/assets/app.css` and create `app/styles/app.scss` instead. 30 | 31 | var Helpers = require('./tasks/helpers'), 32 | config = Helpers.config, 33 | filterAvailable = Helpers.filterAvailableTasks, 34 | _ = grunt.util._; 35 | 36 | config = _.extend(config, Helpers.loadConfig('./tasks/options/')); 37 | 38 | require('load-grunt-tasks')(grunt); 39 | grunt.loadTasks('tasks'); 40 | 41 | grunt.registerTask('default', "Build (in debug mode) & test your application.", ['test']); 42 | 43 | config.concurrent = { 44 | dist: [ 45 | "build:templates:dist", 46 | "build:scripts", 47 | "build:styles", 48 | "build:other" 49 | ], 50 | debug: [ 51 | "build:templates:debug", 52 | "build:scripts", 53 | "build:styles", 54 | "build:other" 55 | ] 56 | }; 57 | 58 | // All tasks except build:before and build:after are run concurrently 59 | grunt.registerTask('build:before:dist', [ 60 | 'clean:build', 61 | 'clean:release', 62 | 'lock' 63 | ]); 64 | 65 | grunt.registerTask('build:before:debug', [ 66 | 'clean:build', 67 | 'lock' 68 | ]); 69 | 70 | grunt.registerTask('build:templates:dist', filterAvailable([ 71 | 'emblem:compile', 72 | 'emberTemplates:dist' 73 | ])); 74 | 75 | grunt.registerTask('build:templates:debug', filterAvailable([ 76 | 'emblem:compile', 77 | 'emberTemplates:debug' 78 | ])); 79 | 80 | grunt.registerTask('build:scripts', filterAvailable([ 81 | 'coffee', 82 | 'copy:prepare', 83 | 'transpile', 84 | 'jshint', 85 | 'concat_sourcemap' 86 | ])); 87 | 88 | grunt.registerTask('build:styles', filterAvailable([ 89 | 'compass:compile', 90 | 'sass:compile', 91 | 'less:compile', 92 | 'stylus:compile', 93 | 'cssmin' 94 | ])); 95 | 96 | grunt.registerTask('build:other', filterAvailable([ 97 | 'copy:vendor' 98 | ])); 99 | 100 | grunt.registerTask('build:after:dist', filterAvailable([ 101 | 'copy:stage', 102 | 'unlock', 103 | 'dom_munger:distEmber', 104 | 'dom_munger:distHandlebars', 105 | 'useminPrepare', 106 | 'concat', 107 | 'uglify', 108 | 'copy:dist', 109 | 'rev', 110 | 'usemin' 111 | ])); 112 | 113 | grunt.registerTask('build:after:debug', filterAvailable([ 114 | 'copy:stage', 115 | 'unlock' 116 | ])); 117 | 118 | grunt.registerTask('build:dist', "Build a minified & production-ready version of your app.", [ 119 | 'build:before:dist', 120 | 'concurrent:dist', 121 | 'build:after:dist' 122 | ]); 123 | 124 | grunt.registerTask('build:debug', "Build a development-friendly version of your app.", [ 125 | 'build:before:debug', 126 | 'concurrent:debug', 127 | 'build:after:debug' 128 | ]); 129 | 130 | grunt.registerTask('test', "Run your apps's tests once. Uses Google Chrome by default. Logs coverage output to tmp/public/coverage.", [ 131 | 'build:debug', 'karma:test' ]); 132 | 133 | grunt.registerTask('test:ci', "Run your app's tests in PhantomJS. For use in continuous integration (i.e. Travis CI).", [ 134 | 'build:debug', 'karma:ci' ]); 135 | 136 | grunt.registerTask('test:browsers', "Run your app's tests in multiple browsers (see tasks/options/karma.js for configuration).", [ 137 | 'build:debug', 'karma:browsers' ]); 138 | 139 | grunt.registerTask('test:server', "Start a Karma test server. Automatically reruns your tests when files change and logs the results to the terminal.", [ 140 | 'build:debug', 'karma:server', 'connect:server', 'watch:test']); 141 | 142 | grunt.registerTask('server', "Run your server in development mode, auto-rebuilding when files change.", 143 | ['build:debug', 'connect:server', 'watch:main']); 144 | grunt.registerTask('server:dist', "Build and preview production (minified) assets.", 145 | ['build:dist', 'connect:dist:keepalive']); 146 | 147 | grunt.initConfig(config); 148 | }; 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Ember Run Loop, Visualized 2 | 3 | See it live 4 | [here](https://machty.s3.amazonaws.com/ember-run-loop-visual/index.html). 5 | 6 | ## Building 7 | 8 | Clone this repo, `cd` in the new directory, then: 9 | 10 | npm install 11 | bower install 12 | 13 | To run a local server: 14 | 15 | grunt server 16 | 17 | To build for deployment: 18 | 19 | grunt build:dist 20 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'resolver'; 2 | import router from 'appkit/router'; 3 | 4 | var App = Ember.Application.create({ 5 | modulePrefix: 'appkit', // TODO: loaded via config 6 | Resolver: Resolver, 7 | Router: Ember.Router.extend({ 8 | router: router, 9 | location: 'none' 10 | }) 11 | }); 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /app/components/current-queue-blip.js: -------------------------------------------------------------------------------- 1 | var QUEUE_WIDTH = 160; 2 | 3 | var CurrentQueueBlip = Ember.Component.extend({ 4 | attributeBindings: ['style'], 5 | 6 | queueIndex: null, 7 | 8 | style: function() { 9 | var leftOffset = this.get('queueIndex') * QUEUE_WIDTH; 10 | return "left: " + leftOffset + "px"; 11 | }.property('queueIndex') 12 | }); 13 | 14 | export default CurrentQueueBlip; 15 | -------------------------------------------------------------------------------- /app/components/run-loop-queue.js: -------------------------------------------------------------------------------- 1 | var RunLoopQueue = Ember.Component.extend({ 2 | classNames: ['run-loop-queue'], 3 | foo: "nork" 4 | }); 5 | 6 | export default RunLoopQueue; 7 | -------------------------------------------------------------------------------- /app/controllers/application.js: -------------------------------------------------------------------------------- 1 | var DEFAULT_CODE = [ 2 | "function foo() {", 3 | " Ember.run.once(function() { console.log('FOO!'); });", 4 | "}", 5 | "", 6 | "function bar() {", 7 | " Ember.run.schedule('afterRender', function() { console.log('BAR!'); });", 8 | "}", 9 | "", 10 | "Ember.run(function() {", 11 | " Ember.run.schedule('render', function() {", 12 | " Ember.run.scheduleOnce('sync', foo);", 13 | " Ember.run.scheduleOnce('actions', bar);", 14 | " });", 15 | "});" 16 | ].join("\n"); 17 | 18 | var ApplicationController = Ember.ObjectController.extend({ 19 | code: DEFAULT_CODE, 20 | logs: [] 21 | }); 22 | 23 | export default ApplicationController; 24 | -------------------------------------------------------------------------------- /app/models/stubbed_ember.js: -------------------------------------------------------------------------------- 1 | var slice = [].slice; 2 | 3 | var StubbedEmber = Ember.create(Ember); 4 | 5 | var FakeRunLoop = Ember.Object.extend({ 6 | currentQueueIndex: 0, 7 | isPlaying: false, 8 | queues: Ember.run.queues.map(function(queueName) { 9 | return { name: queueName, actions: [] }; 10 | }), 11 | 12 | hasItems: false, 13 | 14 | schedule: function(queueName, action, once) { 15 | var queue = this.queues.findProperty('name', queueName); 16 | 17 | if (once){ 18 | var count = queue.actions.reduce(function(sum, record) { 19 | return (record.fn.name===action.fn.name) ? sum+1 : 0; 20 | }, 0); 21 | 22 | if (count>0) { return; } 23 | } 24 | 25 | queue.actions.pushObject(action); 26 | this.set('hasItems', true); 27 | }, 28 | 29 | nextStep: function() { 30 | var currentQueueIndex = this.get('currentQueueIndex'), 31 | queues = this.get('queues'), 32 | index; 33 | 34 | for (index = 0; index < currentQueueIndex; ++index) { 35 | if (queues[index].actions.length) { 36 | // Backtrack. 37 | this.set('currentQueueIndex', index); 38 | return; 39 | } 40 | } 41 | 42 | var queue = queues[index]; 43 | if (queue.actions.length === 0) { 44 | currentQueueIndex += 1; 45 | if (currentQueueIndex === queues.length) { 46 | this.set('isPlaying', false); 47 | this.set('currentQueueIndex', 0); 48 | this.set('hasItems', false); 49 | } else { 50 | this.set('currentQueueIndex', currentQueueIndex); 51 | } 52 | return; 53 | } 54 | 55 | var action = queue.actions.shiftObject(); 56 | action.fn.apply(action.target, action.args); 57 | }, 58 | 59 | play: function() { 60 | if (!this.get('isPlaying')) { return; } 61 | 62 | this.nextStep(); 63 | 64 | Ember.run.later(this, 'play', 600); 65 | }.observes('isPlaying') 66 | }); 67 | 68 | var runLoop = StubbedEmber.fakeRunLoop = FakeRunLoop.create(); 69 | 70 | function normalize() { 71 | var args = slice.call(arguments), 72 | target = args.shift(), 73 | fn; 74 | 75 | if (typeof target === 'function') { 76 | fn = target; 77 | target = null; 78 | } else { 79 | args.shift(); 80 | fn = args.shift(); 81 | } 82 | 83 | return { 84 | target: target, 85 | fn: fn, 86 | args: args 87 | }; 88 | } 89 | 90 | StubbedEmber.run = function(fn) { 91 | fn(); 92 | }; 93 | 94 | StubbedEmber.run.schedule = function(queueName) { 95 | runLoop.schedule(queueName, normalize.apply(null, slice.call(arguments, 1)), false); 96 | }; 97 | 98 | StubbedEmber.run.scheduleOnce = function(queueName, args) { 99 | runLoop.schedule(queueName, normalize.apply(null, slice.call(arguments, 1)), true); 100 | }; 101 | 102 | StubbedEmber.run.once = function(queue, args) { 103 | StubbedEmber.run.scheduleOnce.apply(null, ['actions'].concat(slice.call(arguments))); 104 | }; 105 | 106 | StubbedEmber.run.next = function() { 107 | alert("run.next hasn't been implemented."); 108 | }; 109 | 110 | StubbedEmber.run.later = function() { 111 | alert("run.later hasn't been implemented."); 112 | }; 113 | 114 | export default StubbedEmber; 115 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | var router = Ember.Router.map(function(){ 2 | }); 3 | 4 | export default router; 5 | -------------------------------------------------------------------------------- /app/routes/application.js: -------------------------------------------------------------------------------- 1 | import StubbedEmber from 'appkit/models/stubbed_ember'; 2 | 3 | var map = Ember.ArrayPolyfills.map, 4 | runLater = Ember.run.later; 5 | 6 | var ApplicationRoute = Ember.Route.extend({ 7 | model: function() { 8 | return StubbedEmber.fakeRunLoop; 9 | }, 10 | actions: { 11 | runCode: function() { 12 | var Ember = StubbedEmber, 13 | console = Ember.create(window.console), 14 | controller = this.controller; 15 | 16 | console.log = function() { 17 | map.call(arguments, function(msg) { 18 | window.console.log(msg); 19 | controller.logs.pushObject('' + msg); 20 | }); 21 | }; 22 | 23 | eval(this.controller.get('code')); 24 | }, 25 | 26 | play: function() { 27 | StubbedEmber.fakeRunLoop.set('isPlaying', true); 28 | }, 29 | 30 | pause: function() { 31 | StubbedEmber.fakeRunLoop.set('isPlaying', false); 32 | }, 33 | 34 | step: function() { 35 | StubbedEmber.fakeRunLoop.nextStep(); 36 | } 37 | } 38 | }); 39 | 40 | export default ApplicationRoute; 41 | -------------------------------------------------------------------------------- /app/styles/app.scss: -------------------------------------------------------------------------------- 1 | // CSS reset 2 | html, body, div, span, applet, object, iframe, 3 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 4 | a, abbr, acronym, address, big, cite, code, 5 | del, dfn, em, img, ins, kbd, q, s, samp, 6 | small, strike, strong, sub, sup, tt, var, 7 | b, u, i, center, 8 | dl, dt, dd, ol, ul, li, 9 | fieldset, form, label, legend, 10 | table, caption, tbody, tfoot, thead, tr, th, td, 11 | article, aside, canvas, details, embed, 12 | figure, figcaption, footer, header, hgroup, 13 | menu, nav, output, ruby, section, summary, 14 | time, mark, audio, video { 15 | margin: 0; 16 | padding: 0; 17 | border: 0; 18 | font-size: 100%; 19 | font: inherit; 20 | vertical-align: baseline; 21 | } 22 | /* HTML5 display-role reset for older browsers */ 23 | article, aside, details, figcaption, figure, 24 | footer, header, hgroup, menu, nav, section { 25 | display: block; 26 | } 27 | body { 28 | line-height: 1; 29 | } 30 | ol, ul { 31 | list-style: none; 32 | } 33 | blockquote, q { 34 | quotes: none; 35 | } 36 | blockquote:before, blockquote:after, 37 | q:before, q:after { 38 | content: ''; 39 | content: none; 40 | } 41 | table { 42 | border-collapse: collapse; 43 | border-spacing: 0; 44 | } 45 | 46 | // Box-size all the things 47 | *, *:before, *:after { 48 | -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; 49 | } 50 | 51 | html { 52 | position: relative; 53 | } 54 | 55 | .cf:before, 56 | .cf:after { 57 | content: " "; /* 1 */ 58 | display: table; /* 2 */ 59 | } 60 | 61 | .cf:after { 62 | clear: both; 63 | } 64 | 65 | /** 66 | * For IE 6/7 only 67 | * Include this rule to trigger hasLayout and contain floats. 68 | */ 69 | .cf { 70 | *zoom: 1; 71 | } 72 | 73 | #queues { 74 | margin: 20px 0; 75 | } 76 | 77 | .run-loop-queue { 78 | float: left; 79 | border: 1px solid black; 80 | width: 150px; 81 | margin: 0 5px; 82 | background: white; 83 | z-index: 5; 84 | position: relative; 85 | } 86 | 87 | #current-queue-blip { 88 | background: blue; 89 | border-radius: 4px; 90 | position: absolute; 91 | z-index: 1; 92 | width: 161px; 93 | height: 30px; 94 | left: 0; 95 | -webkit-transition: left 0.4s; 96 | -moz-transition: left 0.4s; 97 | transition: left 0.4s; 98 | margin-top: -6px; 99 | 100 | background: red; 101 | } 102 | 103 | textarea#code { 104 | display: block; 105 | width: 600px; 106 | height: 200px; 107 | } 108 | 109 | .actions { 110 | } 111 | 112 | .action { 113 | margin: 3px; 114 | border: 1px solid blue; 115 | padding: 3px; 116 | font-size: 13px; 117 | 118 | overflow: hidden; 119 | &:hover { 120 | overflow: visible; 121 | pre { 122 | box-shadow: 0 0 3px black; 123 | -webkit-box-shadow: 0 0 3px black; 124 | -moz-box-shadow: 0 0 3px black; 125 | } 126 | } 127 | pre { 128 | background: white; 129 | float: left; 130 | display: block; 131 | position: relative; 132 | 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

2 | Source Code on GitHub 3 |

4 | 5 | {{textarea value=code id='code' disabled=hasItems}} 6 | 7 | 8 | 9 | {{#if hasItems}} 10 | There are deferred actions that need to be flushed. 11 | 12 | {{#if isPlaying}} 13 | 14 | {{else}} 15 | 16 | 17 | {{/if}} 18 | {{/if}} 19 | 20 |
21 | {{#each queues}} 22 | {{run-loop-queue queue=this}} 23 | {{/each}} 24 | 25 | {{#if hasItems}} 26 | {{current-queue-blip queueIndex=currentQueueIndex id="current-queue-blip"}} 27 | {{/if}} 28 |
29 | 30 |

Logs:

31 |
32 | {{#each logs}} 33 |

{{this}}

34 | {{/each}} 35 |
36 | -------------------------------------------------------------------------------- /app/templates/components/current-queue-blip.hbs: -------------------------------------------------------------------------------- 1 | {{! TODO: remove this unnecessary file}} 2 | -------------------------------------------------------------------------------- /app/templates/components/run-loop-queue.hbs: -------------------------------------------------------------------------------- 1 |

{{queue.name}}

2 | 3 | 10 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-app-kit", 3 | "dependencies": { 4 | "ember": "http://builds.emberjs.com/stable/ember.js", 5 | "ember-prod": "http://builds.emberjs.com/stable/ember.prod.js", 6 | "handlebars": "1.0.0", 7 | "jquery": "~1.9.1", 8 | "qunit": "~1.12.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Jul 05 2013 01:57:57 GMT-0400 (EDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: 'tmp/public', 9 | 10 | // list of files / patterns to load in the browser 11 | files: [ 12 | 'vendor/loader.js', 13 | 'vendor/jquery/jquery.js', 14 | 'vendor/handlebars/handlebars.js', 15 | 'vendor/ember/index.js', 16 | 'assets/templates.js', 17 | 'assets/app.js', 18 | 'tests/test_helper.js', 19 | 'tests/tests.js', 20 | 'tests/test_loader.js' 21 | ], 22 | 23 | frameworks: ['qunit'], 24 | 25 | plugins: [ 26 | 'karma-qunit', 27 | 'karma-coverage', 28 | 'karma-phantomjs-launcher', 29 | 'karma-chrome-launcher', 30 | 'karma-firefox-launcher', 31 | //'karma-safari-launcher' // npm install karma-safari-launcher 32 | ], 33 | 34 | preprocessors: { 35 | 'assets/*.js': 'coverage' 36 | }, 37 | 38 | // list of files to exclude 39 | exclude: [], 40 | 41 | // test results reporter to use 42 | // possible values: 'dots', 'progress', 'junit' 43 | reporters: 'coverage', 44 | 45 | coverageReporter: { 46 | type : ['text'], 47 | dir : 'coverage/' 48 | }, 49 | 50 | // web server port 51 | port: parseInt(process.env.PORT, 10) + 1 || 9876, 52 | 53 | // cli runner port 54 | runnerPort: 9100, 55 | 56 | // enable / disable colors in the output (reporters and logs) 57 | colors: true, 58 | 59 | // level of logging 60 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 61 | logLevel: config.LOG_INFO, 62 | 63 | // enable / disable watching file and executing tests whenever any file changes 64 | autoWatch: false, 65 | 66 | // Start these browsers, currently available: 67 | // - Chrome 68 | // - ChromeCanary 69 | // - Firefox 70 | // - Opera 71 | // - Safari (only Mac) 72 | // - PhantomJS 73 | // - IE (only Windows) 74 | browsers: ['Chrome'], 75 | 76 | // If browser does not capture in given timeout [ms], kill it 77 | captureTimeout: 60000, 78 | 79 | // Continuous Integration mode 80 | // if true, it capture browsers, run tests and exit 81 | singleRun: false 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-kit", 3 | "namespace": "appkit", 4 | "version": "0.0.0", 5 | "private": true, 6 | "directories": { 7 | "doc": "doc", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "start": "grunt server", 12 | "build": "grunt build:debug", 13 | "test": "grunt test:ci" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/stefanpenner/app-kit.git" 18 | }, 19 | "author": "", 20 | "license": "BSD", 21 | "devDependencies": { 22 | "grunt-contrib-connect": "~0.3.0", 23 | "grunt": "~0.4.1", 24 | "grunt-contrib-watch": "~0.4.4", 25 | "grunt-contrib-copy": "~0.4.1", 26 | "grunt-es6-module-transpiler": "~0.4.1", 27 | "glob": "~3.2.1", 28 | "load-grunt-tasks": "~0.1.0", 29 | "grunt-contrib-concat": "~0.3.0", 30 | "grunt-contrib-clean": "~0.4.1", 31 | "grunt-contrib-jshint": "~0.6.2", 32 | "grunt-usemin": "~0.1.12", 33 | "grunt-contrib-uglify": "~0.2.2", 34 | "grunt-rev": "~0.1.0", 35 | "grunt-ember-templates": "~>0.4.13", 36 | "karma": "~0.9.4", 37 | "grunt-karma": "~0.5", 38 | "karma-qunit": "~0.0.3", 39 | "karma-coverage": "~0.0.3", 40 | "karma-phantomjs-launcher": "~0.0.2", 41 | "lockfile": "~>0.3.0", 42 | "grunt-concat-sourcemap": "~0.3.0", 43 | "grunt-contrib-cssmin": "~0.6.1", 44 | "grunt-concurrent": "~0.3.1", 45 | "grunt-dom-munger": "~2.0.1", 46 | "grunt-contrib-coffee": "~0.7.0", 47 | "grunt-sass": "~0.6.1", 48 | "connect-livereload": "~0.3.0" 49 | }, 50 | "dependencies": { 51 | "loom": "~2.0.0", 52 | "loom-generators-ember": "0.0.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machty/ember-run-loop-visual/5114f73d9d0443d60779e2787fb69ad0151250b8/public/assets/.gitkeep -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ember Interactive Run Loop 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tasks/helpers.js: -------------------------------------------------------------------------------- 1 | var pathUtils = require('path'), 2 | grunt = require('grunt'), 3 | _ = grunt.util._, 4 | Helpers = {}; 5 | 6 | // Grunt configuration. Object is expected to be mutated in Gruntfile. 7 | Helpers.config = { 8 | pkg: grunt.file.readJSON('./package.json'), 9 | env: process.env 10 | }; 11 | 12 | // List of package requisits for tasks 13 | var taskRequirements = { 14 | 'coffee': ['grunt-contrib-coffee'], 15 | 'compass': ['grunt-contrib-compass'], 16 | 'sass': ['grunt-sass', 'grunt-contrib-sass'], 17 | 'less': ['grunt-contrib-less'], 18 | 'stylus': ['grunt-contrib-stylus'], 19 | 'emberTemplates': ['grunt-ember-templates'], 20 | 'emblem': ['grunt-emblem'] 21 | }; 22 | 23 | Helpers.filterAvailableTasks = function(tasks){ 24 | tasks = tasks.filter(Helpers.whenTaskIsAvailable); 25 | return _.compact(tasks); 26 | }; 27 | 28 | // @returns taskName if given task is available, undefined otherwise 29 | Helpers.whenTaskIsAvailable = function(taskName) { 30 | var baseName, reqs, isAvailable; 31 | // baseName of 'coffee:compile' is 'coffee' 32 | baseName = taskName.split(':')[0]; 33 | reqs = taskRequirements[baseName]; 34 | isAvailable = Helpers.isPackageAvailable(reqs); 35 | return isAvailable ? taskName : undefined; 36 | }; 37 | 38 | Helpers.isPackageAvailable = function(pkgNames) { 39 | if (!pkgNames) return true; // packages are assumed to exist 40 | 41 | if (!_.isArray(pkgNames)) { 42 | pkgNames = [pkgNames]; 43 | } 44 | return _.any(pkgNames, function(pkgName){ 45 | return !!Helpers.config.pkg.devDependencies[pkgName]; 46 | }); 47 | }; 48 | 49 | Helpers.loadConfig = function(path) { 50 | var glob = require('glob'); 51 | var object = {}; 52 | var key; 53 | 54 | glob.sync('*', {cwd: path}).forEach(function(option) { 55 | key = option.replace(/\.js$/,''); 56 | object[key] = require("../" + path + option); 57 | }); 58 | 59 | return object; 60 | }; 61 | 62 | module.exports = Helpers; 63 | -------------------------------------------------------------------------------- /tasks/locking.js: -------------------------------------------------------------------------------- 1 | var lockFile = require('lockfile'); 2 | 3 | module.exports = function(grunt) { 4 | grunt.registerTask('lock', 'Set semaphore for connect server to wait on.', function() { 5 | grunt.file.mkdir('tmp'); 6 | lockFile.lockSync('tmp/connect.lock'); 7 | }); 8 | 9 | grunt.registerTask('unlock', 'Release semaphore that connect server waits on.', function() { 10 | console.log("unlocking"); 11 | lockFile.unlockSync('tmp/connect.lock'); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /tasks/options/clean.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "build": ['tmp'], 3 | "release": ['dist'], 4 | } 5 | -------------------------------------------------------------------------------- /tasks/options/coffee.js: -------------------------------------------------------------------------------- 1 | // CoffeeScript compilation. This must be enabled by modification 2 | // of Gruntfile.js. 3 | // 4 | // The `bare` option is used since this file will be transpiled 5 | // anyway. In CoffeeScript files, you need to escape out for 6 | // some ES6 features like import and export. For example: 7 | // 8 | // `import User from 'appkit/models/user'` 9 | // 10 | // Post = Em.Object.extend 11 | // init: (userId) -> 12 | // @set 'user', User.findById(userId) 13 | // 14 | // `export default Post` 15 | // 16 | 17 | module.exports = { 18 | "test": { 19 | options: { 20 | bare: true 21 | }, 22 | files: [{ 23 | expand: true, 24 | cwd: 'tests/', 25 | src: ['**/*.coffee', '!vendor/**/*.coffee'], 26 | dest: 'tmp/javascript/tests', 27 | ext: '.js' 28 | }] 29 | }, 30 | "app": { 31 | options: { 32 | bare: true 33 | }, 34 | files: [{ 35 | expand: true, 36 | cwd: 'app/', 37 | src: '**/*.coffee', 38 | dest: 'tmp/javascript/app', 39 | ext: '.js' 40 | }] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /tasks/options/compass.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | sassDir: "app/styles", 4 | cssDir: "tmp/public/assets", 5 | generatedImagesDir: "tmp/public/assets/images", 6 | imagesDir: "app/assets/images", 7 | javascriptsDir: "app", 8 | fontsDir: "app/styles/fonts", 9 | importPath: ["vendor"], 10 | httpImagesPath: "/images", 11 | httpGeneratedImagesPath: "/images/generated", 12 | httpFontsPath: "/styles/fonts", 13 | relativeAssets: false, 14 | debugInfo: true 15 | }, 16 | compile: {} 17 | } 18 | -------------------------------------------------------------------------------- /tasks/options/concat_sourcemap.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | app: { 3 | src: ['tmp/transpiled/app/**/*.js'], 4 | dest: 'tmp/public/assets/app.js', 5 | options: { 6 | sourcesContent: true 7 | }, 8 | }, 9 | 10 | test: { 11 | src: 'tmp/transpiled/tests/**/*.js', 12 | dest: 'tmp/public/tests/tests.js', 13 | options: { 14 | sourcesContent: true 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /tasks/options/connect.js: -------------------------------------------------------------------------------- 1 | var lockFile = require('lockfile'), 2 | fs = require('fs'), 3 | url = require('url'), 4 | Helpers = require('../helpers'); 5 | 6 | module.exports = { 7 | server: { 8 | options: { 9 | port: process.env.PORT || 8000, 10 | hostname: '0.0.0.0', 11 | base: 'tmp/public', 12 | // Use this option to have the catch-all return a different 13 | // page than index.html on any url not matching an asset. 14 | // wildcard: 'not_index.html' 15 | middleware: middleware 16 | } 17 | }, 18 | dist: { 19 | options: { 20 | port: process.env.PORT || 8000, 21 | hostname: '0.0.0.0', 22 | base: 'dist/', 23 | middleware: middleware 24 | } 25 | } 26 | }; 27 | 28 | // works with tasks/locking.js 29 | function lock(req, res, next) { 30 | (function retry() { 31 | if (lockFile.checkSync('tmp/connect.lock')) { 32 | setTimeout(retry, 30); 33 | } else { 34 | next(); 35 | } 36 | }()); 37 | } 38 | 39 | function wildcardResponseIsValid(request) { 40 | var urlSegments = request.url.split('.'), 41 | extension = urlSegments[urlSegments.length-1]; 42 | return ( 43 | ['GET', 'HEAD'].indexOf(request.method.toUpperCase()) > -1 && 44 | (urlSegments.length == 1 || extension.indexOf('htm') == 0 || extension.length > 5) 45 | ) 46 | } 47 | 48 | function buildWildcardMiddleware(options) { 49 | return function(request, response, next) { 50 | if (!wildcardResponseIsValid(request)) { return next(); } 51 | 52 | var wildcard = (options.wildcard || 'index.html'), 53 | wildcardPath = options.base + "/" + wildcard; 54 | 55 | fs.readFile(wildcardPath, function(err, data){ 56 | if (err) { return next('ENOENT' == err.code ? null : err); } 57 | 58 | response.writeHead(200, { 'Content-Type': 'text/html' }); 59 | response.end(data); 60 | }); 61 | } 62 | } 63 | 64 | function middleware(connect, options) { 65 | return [ 66 | lock, 67 | require("connect-livereload")(), 68 | connect['static'](options.base), 69 | connect.directory(options.base), 70 | // Remove this middleware to disable catch-all routing. 71 | buildWildcardMiddleware(options) 72 | ]; 73 | } 74 | -------------------------------------------------------------------------------- /tasks/options/copy.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // These copy tasks happen before transpile or hinting. They 3 | // prepare the build pipeline by moving JavaScript files to 4 | // tmp/javascript. 5 | // 6 | "prepare": { 7 | files: [{ 8 | expand: true, 9 | cwd: 'app/', 10 | src: '**/*.js', 11 | dest: 'tmp/javascript/app' 12 | }, 13 | { 14 | expand: true, 15 | cwd: 'tests/', 16 | src: ['**/*.js', '!test_helper.js', '!test_loader.js', '!vendor/**/*.js'], 17 | dest: 'tmp/javascript/tests/' 18 | }] 19 | }, 20 | // Stage moves files to their final destinations after the rest 21 | // of the build cycle has run. 22 | // 23 | "stage": { 24 | files: [{ 25 | expand: true, 26 | cwd: 'tests/', 27 | src: ['index.html', 'test_helper.js', 'test_loader.js', 'vendor/**/*'], 28 | dest: 'tmp/public/tests/' 29 | }, 30 | { 31 | expand: true, 32 | cwd: 'public/', 33 | src: ['**'], 34 | dest: 'tmp/public/' 35 | }] 36 | }, 37 | "vendor": { 38 | src: ['vendor/**/*.js', 'vendor/**/*.css'], 39 | dest: 'tmp/public/' 40 | }, 41 | "dist": { 42 | files: [{ 43 | expand: true, 44 | cwd: 'tmp/public', 45 | src: ['**', '!coverage'], 46 | dest: 'dist/' 47 | }] 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /tasks/options/cssmin.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | combine: { 3 | files: { 4 | 'tmp/public/assets/app.css': ['app/styles/**/*.css'] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tasks/options/dom_munger.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | distEmber: { 3 | options: { 4 | //Point the index.html file at ember-prod js instead of dev version 5 | update: { 6 | selector:'script[src="/vendor/ember/index.js"]', 7 | attribute:'src', 8 | value:'/vendor/ember-prod/index.js' 9 | } 10 | }, 11 | src: 'tmp/public/index.html' 12 | }, 13 | distHandlebars: { 14 | options: { 15 | update: { 16 | selector:'script[src="/vendor/handlebars/handlebars.js"]', 17 | attribute:'src', 18 | value:'/vendor/handlebars/handlebars.runtime.js' 19 | } 20 | }, 21 | src: 'tmp/public/index.html' 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /tasks/options/emberTemplates.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | templateBasePath: /app\/templates\//, 4 | templateFileExtensions: /\.(hbs|hjs|handlebars)/ 5 | }, 6 | debug: { 7 | options: { 8 | precompile: false 9 | }, 10 | src: "app/templates/**/*.{hbs,hjs,handlebars}", 11 | dest: "tmp/public/assets/templates.js" 12 | }, 13 | dist: { 14 | src: "<%= emberTemplates.debug.src %>", 15 | dest: "<%= emberTemplates.debug.dest %>" 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /tasks/options/emblem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compile: { 3 | files: { 4 | "tmp/public/assets/templates.js": ['app/templates/**/*.{emblem,hbs,hjs,handlebars}'] 5 | }, 6 | options: { 7 | root: 'app/templates/', 8 | dependencies: { 9 | jquery: 'vendor/jquery/jquery.js', 10 | ember: 'vendor/ember/index.js', 11 | handlebars: 'vendor/handlebars/handlebars.js', 12 | emblem: 'vendor/emblem.js/emblem.js' 13 | } 14 | } 15 | } 16 | }; -------------------------------------------------------------------------------- /tasks/options/jshint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | all: { 3 | src: [ 4 | 'Gruntfile.js', 5 | 'app/**/*.js', 6 | 'tests/**/*.js' 7 | ] 8 | }, 9 | options: { 10 | jshintrc: '.jshintrc', 11 | force: true 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /tasks/options/karma.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | configFile: 'karma.conf.js', 4 | browsers: ['Chrome'], 5 | reporters: ['coverage', 'dots'] 6 | }, 7 | ci: { 8 | singleRun: true, 9 | browsers: ['PhantomJS'] 10 | }, 11 | test: { 12 | singleRun: true 13 | }, 14 | server: { 15 | background: true, 16 | coverageReporter: { 17 | type : ['html'], 18 | dir : 'coverage/' 19 | } 20 | }, 21 | browsers: { 22 | singleRun: true, 23 | browsers: ['Chrome', 24 | 'ChromeCanary', 25 | 'Firefox', 26 | // 'Safari', // enable plugin in karma.conf.js to use 27 | 'PhantomJS'] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tasks/options/less.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compile: { 3 | files: { 4 | 'tmp/public/assets/app.css': 'app/styles/**/*.less' 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tasks/options/rev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | files: { 4 | src: [ 5 | 'dist/assets/app.min.js', 6 | 'dist/assets/vendor.min.js', 7 | 'dist/assets/app.css', 8 | 'dist/assets/vendor.css' 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tasks/options/sass.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compile: { 3 | files: { 4 | 'tmp/public/assets/app.css': 'app/styles/**/*.{scss,sass}' 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tasks/options/stylus.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compile: { 3 | files: { 4 | 'tmp/public/assets/app.css': 'app/styles/**/*.styl' 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tasks/options/transpile.js: -------------------------------------------------------------------------------- 1 | var grunt = require('grunt'); 2 | 3 | module.exports = { 4 | "tests": { 5 | type: 'amd', 6 | moduleName: function(path) { 7 | return grunt.config.process('<%= pkg.namespace %>/tests/') + path; 8 | }, 9 | files: [{ 10 | expand: true, 11 | cwd: 'tmp/javascript/tests/', 12 | src: '**/*.js', 13 | dest: 'tmp/transpiled/tests/' 14 | }] 15 | }, 16 | "app": { 17 | type: 'amd', 18 | moduleName: function(path) { 19 | return grunt.config.process('<%= pkg.namespace %>/') + path; 20 | }, 21 | files: [{ 22 | expand: true, 23 | cwd: 'tmp/javascript/app/', 24 | src: '**/*.js', 25 | dest: 'tmp/transpiled/app/' 26 | }] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tasks/options/usemin.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | html: ['dist/index.html'], 3 | }; 4 | -------------------------------------------------------------------------------- /tasks/options/useminPrepare.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | html: 'tmp/public/index.html', 3 | options: { 4 | dest: 'dist/' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /tasks/options/watch.js: -------------------------------------------------------------------------------- 1 | var Helpers = require('../helpers'); 2 | 3 | module.exports = { 4 | main: { 5 | files: ['app/**/*', 'public/**/*', 'vendor/**/*', 'tests/**/*'], 6 | tasks: ['build:debug'] 7 | }, 8 | test: { 9 | files: ['app/**/*', 'public/**/*', 'vendor/**/*', 'tests/**/*'], 10 | tasks: ['build:debug', 'karma:server:run'] 11 | }, 12 | options: { 13 | debounceDelay: 200, 14 | livereload: Helpers.isPackageAvailable("connect-livereload") 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /tests/acceptance/index_test.js: -------------------------------------------------------------------------------- 1 | import Index from 'appkit/routes/index'; 2 | import App from 'appkit/app'; 3 | 4 | module("Acceptances - Index", { 5 | setup: function(){ 6 | App.reset(); 7 | } 8 | }); 9 | 10 | test("index renders", function(){ 11 | expect(3); 12 | 13 | visit('/').then(function(){ 14 | ok(exists("h2:contains('Welcome to Ember.js')")); 15 | 16 | var list = find("ul li"); 17 | equal(list.length, 3); 18 | equal(list.text(), "redyellowblue"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App Kit 5 | 6 | 7 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/test_helper.js: -------------------------------------------------------------------------------- 1 | document.write('
'); 2 | 3 | Ember.testing = true; 4 | 5 | var App = requireModule('appkit/app'); 6 | 7 | App.rootElement = '#ember-testing'; 8 | App.setupForTesting(); 9 | App.injectTestHelpers(); 10 | 11 | function exists(selector) { 12 | return !!find(selector).length; 13 | } 14 | 15 | function equal(actual, expected, message) { 16 | message = message || QUnit.jsDump.parse(expected) + " expected but was " + QUnit.jsDump.parse(actual); 17 | QUnit.equal.call(this, expected, actual, message); 18 | } 19 | 20 | window.exists = exists; 21 | window.equal = equal; 22 | 23 | Ember.Container.prototype.stub = function(fullName, instance) { 24 | instance.destroy = instance.destroy || function() {}; 25 | this.cache.dict[fullName] = instance; 26 | }; 27 | -------------------------------------------------------------------------------- /tests/test_loader.js: -------------------------------------------------------------------------------- 1 | // TODO: load based on params 2 | Ember.keys(define.registry).filter(function(key) { 3 | return (/\_test/).test(key); 4 | }).forEach(requireModule); -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machty/ember-run-loop-visual/5114f73d9d0443d60779e2787fb69ad0151250b8/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/routes/index_test.js: -------------------------------------------------------------------------------- 1 | import Index from 'appkit/routes/index'; 2 | import App from 'appkit/app'; 3 | 4 | var route; 5 | 6 | module("Unit - IndexRoute", { 7 | setup: function(){ 8 | route = App.__container__.lookup('route:index'); 9 | } 10 | }); 11 | 12 | test("it exists", function(){ 13 | ok(route); 14 | ok(route instanceof Ember.Route); 15 | }); 16 | 17 | test("#model", function(){ 18 | deepEqual(route.model(), ['red', 'yellow', 'blue']); 19 | }); 20 | -------------------------------------------------------------------------------- /vendor/loader.js: -------------------------------------------------------------------------------- 1 | var define, requireModule; 2 | 3 | (function() { 4 | var registry = {}, seen = {}; 5 | 6 | define = function(name, deps, callback) { 7 | registry[name] = { deps: deps, callback: callback }; 8 | }; 9 | 10 | requireModule = function(name) { 11 | if (seen[name]) { return seen[name]; } 12 | seen[name] = {}; 13 | 14 | var mod = registry[name]; 15 | 16 | if (!mod) { 17 | throw new Error("Module: '" + name + "' not found."); 18 | } 19 | 20 | var deps = mod.deps, 21 | callback = mod.callback, 22 | reified = [], 23 | exports; 24 | 25 | for (var i=0, l=deps.length; i