diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2367f8c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## 2.0.0 +### Added +- memo-is for using memoization in specs + +### Changed + +- Updated Marionette to 3.1 +- Updated Backbone to 1.3.3 +- Updated Mocha to 3.1.2 +- Updated jQuery to 3.0.0 +- Node.js >= 6 is now required +- Switched from Browserify to webpack module bundler + +### Removed + +- Maji plugin management (`cordova/plugins.txt`) in favor of Cordova plugin management +- Maji application bus in favor of Backbone.Radio +- Maji Cordova platform management, see upgrade guide for details diff --git a/README.md b/README.md index 7d1d011..c5e9c39 100644 --- a/README.md +++ b/README.md @@ -17,26 +17,15 @@ Before you can use Maji, make sure you have the following: ### General -* Ruby, for the Cucumber specs -* NodeJS, for the build system (`bin/setup` will install this if you've got Homebrew) -* Homebrew (`bin/setup` will use this to hook you up with all of the dependencies, except Ruby) - -### Mac OS X - -* [Homebrew](http://brew.sh) a Mac OS X package manager - -### Linux - -* [Nodejs](http://nodejs.org) allows JavaScript to be run Server-side -* [Apache Ant](http://ant.apache.org) is used for building Java Applications -* [PhantomJS](http://phantomjs.org/download.html) is a headless WebKit Browser, scriptable with a JavaScript API +* Ruby + Bundler, for the integration specs +* Node.js >= 6 + NPM, for the build system ## Getting started To create a new app execute the following commands in your shell: ``` -$ npm install kabisa/maji +$ npm install maji $ ./node_modules/.bin/maji new org.example.my-app /desired/path/to/your/project/ $ cd /desired/path/to/your/project/ $ bin/setup @@ -51,9 +40,11 @@ Your new Maji app will now be generated at the supplied path. Commands: - run [options] build and run a native app for the specified platform - build [options] build a native app for the specified platform - new create a new Maji app + new Create a new Maji app + run [options] Build and run a native app for the specified platform + build [options] [platform] Build a native app for the specified platform + test [options] Run your project tests + start [options] Run the maji dev server and compile changes on the fly Options: @@ -64,15 +55,15 @@ Your new Maji app will now be generated at the supplied path. ### Starting in the browser -To start your app, `cd` into its directory, execute `make watch` and navigate to http://localhost:9090/ with your browser. +To start your app, `cd` into its directory, execute `bin/maji start` and navigate to http://localhost:9090/ with your browser. ### Running tests To run test, you have several options: -* To run JavaScript tests run `bin/karma start`. This will start a Karma server with Phantomjs and will continuously watch your Javascript files and run tests on changes. -* To run JavaScript tests once, run `bin/karma start --single-run`. -* To run features specs once, run `bundle exec rspec`. -* To run all tests once, run `bin/ci`. +* To run JavaScript tests run `bin/maji test --watch`. This will start a Karma server with Phantomjs headless browser and will continuously watch your Javascript files and run tests on changes. +* To run JavaScript tests once, run `bin/maji test --unit`. +* To run features specs once, run `bin/maji test --integration`. +* To run all tests once, run `bin/maji test`. ### Creating builds @@ -96,7 +87,7 @@ A Maji Mobile App comes with several frameworks built-in and configured to work * [MarionetteJS](http://marionettejs.com) Marionette simplifies Backbone Views * [FastClick](http://ftlabs.github.io/fastclick/) disable the delay between click and the action on iOS * [jQuery](http://jquery.com) JavaScript library for working with the DOM - * [Browserify](http://browserify.org) is a JavaScript module system that supports CommonJS syntax + * [Webpack](https://webpack.js.org/) is a JavaScript module bundler * [BugSnagJS](https://github.com/bugsnag/bugsnag-js) JavaScript client for [BugSnag](http://bugsnag.com/) exception tracker * [Karma](http://karma-runner.github.io/) is a JavaScript test runner * [MochaJS](http://mochajs.org) a JavaScript testing framework that supports a BDD style of writing tests diff --git a/bin/test-project-template b/bin/test-project-template index c29baf0..bd5a4f4 100755 --- a/bin/test-project-template +++ b/bin/test-project-template @@ -2,9 +2,11 @@ set -e TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t maji-tests) +npm link lib/cli.js new nl.kabisa.test-app $TMP_DIR pushd $TMP_DIR bin/setup + npm link maji bin/ci popd diff --git a/cordova/hooks/before_platform_add.js b/cordova/hooks/before_platform_add.js new file mode 100644 index 0000000..9552b80 --- /dev/null +++ b/cordova/hooks/before_platform_add.js @@ -0,0 +1,30 @@ +function ensureDependencyInstalled(Q, mod) { + try { + require(`${mod}/package.json`); + return Q(); + } catch(e) { + console.log(`Installing ${mod}`); + const deferred = Q.defer(); + + exec(`npm install ${mod}`, function(exitCode, stdout, stderr) { + if(exitCode == 0) + deferred.resolve(); + else + deferred.reject(); + }); + + return deferred.promise; + } +} + +module.exports = function(context) { + if(context.opts.cordova.platforms.includes('ios')) { + context.requireCordovaModule('shelljs/global'); + + var Q = context.requireCordovaModule('q'); + return Q.all([ + ensureDependencyInstalled(Q, 'ios-sim'), + ensureDependencyInstalled(Q, 'ios-deploy') + ]); + } +} diff --git a/dockerfiles/ci/Dockerfile b/dockerfiles/ci/Dockerfile index 55a89b1..8030d32 100644 --- a/dockerfiles/ci/Dockerfile +++ b/dockerfiles/ci/Dockerfile @@ -13,16 +13,9 @@ RUN apt-get update \ locales \ && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Install NodeJS 5 -RUN curl -sL https://deb.nodesource.com/setup_5.x | bash - \ +RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \ && apt-get install --yes nodejs -# Install PhantomJS 2.1.1 -RUN cd /tmp && curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -o phantomjs-2.1.1-linux-x86_64.tar.bz2 \ - && tar xjf phantomjs-2.1.1-linux-x86_64.tar.bz2 \ - && mv /tmp/phantomjs-*/bin/phantomjs /usr/bin/ \ - && rm -r /tmp/phantomjs* - ENV CONTAINER_INIT /usr/local/bin/init-container RUN echo '#!/usr/bin/env bash' > $CONTAINER_INIT ; chmod +x $CONTAINER_INIT @@ -37,5 +30,4 @@ RUN echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && locale-gen ENV LC_ALL en_US.UTF-8 ENV LANG en_US.UTF-8 ENV LANGUAGE en_US.UTF-8 -ENV PHANTOMJS_BIN /usr/bin/phantomjs ENV BUNDLE_PATH /cache/bundle diff --git a/docs/README.md b/docs/README.md index 8228f58..b11ef94 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ * Fast Node based build system * Compilation of Coffeescript and Sass * Javascript bundling and minification including source maps - * CommonJS module system using Browserify + * CommonJS module system using Webpack * CSS autoprefixer (no more vendor prefixes!) * Iconfont builder. Drop an SVG in your project and it's instantly available in your iconfont. [Read more](./icons.md). * Livereload integration. Make changes and your browser will automatically reload, works on mobile too. diff --git a/docs/upgrade_guide.md b/docs/upgrade_guide.md new file mode 100644 index 0000000..02021d1 --- /dev/null +++ b/docs/upgrade_guide.md @@ -0,0 +1,202 @@ +# Upgrade guide Maji 1.1.0 -> 2.0.0 + +Maji 2.0 contains several (breaking) changes. Follow the steps outlined in this document to upgrade your app to Maji 2.0. + +## MarionetteJS + +In Maji 2.0.0 the Marionette dependency has been updated to 3.1.0. + +This means that your app has to update from Marionette 2.4.4 to 3.1.0. +The `Maji.bus` (using `backbone.wreqr`) is also removed in favor of using `backbone.radio`. + +Marionette has an [upgrade guide available online][marionette-upgrade], which you can use to find more help upgrading after using this guide. + +The best way to start is to checkout the updated example app and compare it to your app. + +1. Update `app/application.coffee`: + 1. At the top of the file, replace + ```coffee + attachFastClick = require('fastclick') + ``` + + with + + ```coffee + FastClick = require('fastclick) + ``` + + 1. In the method `setTimeout`, replace both instances of + ```coffee + app.mainRegion.$el + ``` + with + ```coffee + $(app.getRegion().el) + ``` + +2. Update `app/app.coffee`: + + Add in the top of the file: + + ```coffee + Radio = require('backbone.radio') + ``` + + Change the following: + + ```coffee + app.addInitializer -> + require('./modules/home/home_app').start() + ``` + + to: + + ```coffee + app.on 'before:start', -> + require('./modules/home/home_app') + ``` + + Change the kickoff of the backbone history: + + ```coffee + app.on 'initialize:after', (options) -> + Backbone.history.start() + ``` + + to: + + ```coffee + app.on 'before:start', (options) -> + # Bind the start event in after the inclusion of + # the modules, so that the routers are initialized + # before the kick-off of the backbone history + app.on 'start', (options) -> + Backbone.history.start() + ``` + + Update the implementation of the bus functions to: + + ```coffee + Radio.channel('app').reply( + 'view:current': -> app.getView() + 'uri:current': -> Backbone.history.fragment + 'navigate': (location, options = {}) -> + app.getRegion().navigate(location, options, Backbone.history) + 'go-back': (where, opts = {}) -> + where = undefined if where == '#' + app.getRegion().goBack(where, opts) + ) + ``` + +3. The best approachs is to disable all modules, and enable/update them one by one, starting with the main module 'app' file: + + Change: + + ```coffee + MyModuleApp = app.module('my_module') + MyModuleApp.startWithParent = false + + class MyModuleApp.Router extends Marionette.AppRouter + appRoutes: + 'my_route/:resource_id' : 'openMyRoute' + + API = + openMyRoute: (resourceId) -> + app.mainRegion.show new EditorPage( + model: model + ) + + MyModuleApp.addInitializer -> + new EditorApp.Router + controller: API + + module.exports = EditorApp + ``` + + To: + + ```coffee + # Notice creation of module is gone; + # New name for router + + class MyModuleRouter extends Marionette.AppRouter + routes: # previously appRoutes + 'my_route/:resource_id' : 'openMyRoute' + + openMyRoute: (resourceId) -> # Method in class now + app.showView new EditorPage( # Use showView + model: model + ) + + app.on 'start', -> + new MyModuleRouter # No passing of controller + + # No Module exports + ``` + +4. Update all your views: + + * everything extending from `Marionette.ItemView` must now extend from `Marionette.View` + * `onShow` is gone. Use `onAttach` instead + * Update use of *Regions*. `@myRegion.show new MySubView` is now `@showChildView('myRegion', new MySubView)` + * `getChildView: ->` is now `childView: ->` + + +## Cordova plugin management + +Maji 2.x uses Cordova plugin management + +This means that the Cordova plugins you wish to use should now be defined in `cordova/config.xml`, instead of `cordova/plugins.txt`. + +Refer to the Cordova documentation for details: https://cordova.apache.org/docs/en/latest/platform_plugin_versioning_ref/ + +## Cordova platform management + +In Maji 2.0, Maji no longer automatically adds Cordova platforms when you invoke `maji run` or `maji build`. Instead it is expected that you define the platform versions in your `cordova/config.xml` like so: + +```xml + + + +``` + +All projects created with Maji 2.0 are configured like this out of the box. + +## Maji bus + +Update all uses of the `Maji.bus` to Backbone.Radio ([see documentation of radio here][backbone-radio]) + +## Switch from Browserify to webpack (optional) + +With Maji 2.x new projects use webpack for module bundling. While not required it's recommended that you migrate your projects to webpack, see [this Pull Request](https://github.com/kabisa/maji/pull/128) for reasoning and benefits. This PR can be used as a basis to migrate your project to webpack. + +With these steps your app should become functional again, depending on how much custom stuff your app uses (like overwriting private implementations of Marionette). + +## Maji CLI + +Maji 2.x adds several new subcommands to the Maji CLI, streamlining the development workflow to have a single way to interact with your project: + +* `maji start` - starts the development server +* `maji build` - build static assets +* `maji test` - runs your projects tests once +* `maji test --unit` - runs your unit test once +* `maji test --integration` - runs your integration test once +* `maji test --watch` - runs your tests whenever files are changed + +All these commands delegate to NPM run scripts. When upgrading from Maji 1.x to Maji 2.x you can add the following snippet to your `package.json` to add support for these commands: + +```json +"scripts": { + "test": "bin/ci", + "test:unit": "APP_ENV=test karma start --single-run", + "test:integration": "APP_ENV=test PRE_BUILT=true bundle exec rspec", + "test:watch": "APP_ENV=test karma start --watch", + "start": "make watch", + "build": "make dist" +} +``` + +Should you choose the use a different test runner for example then the only thing that needs to change are the NPM scripts defined in your `package.json` and everything will continue to work. + +[marionette-upgrade]: http://marionettejs.com/docs/v3.1.0/upgrade.html +[backbone-radio]: https://github.com/marionettejs/backbone.radio diff --git a/lib/cli.js b/lib/cli.js index 9504c6b..da53a5d 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,50 +1,129 @@ #!/usr/bin/env node // Generated by CoffeeScript 1.9.3 (function() { - var executeScript, path, program, spawn; + var literalArgs, maji_package, parseBoolean, parsePort, path, program, runCmd, runNpm, runScript, spawn, + slice = [].slice; spawn = require('child_process').spawn; path = require('path'); + maji_package = require('../package.json'); + + parseBoolean = function(value) { + return value === 'true'; + }; + + parsePort = function(value) { + return parseInt(value) || null; + }; + program = require('commander'); - program.version('1.0.0'); + program.version(maji_package.version); - executeScript = function(scriptName, args) { - var child; - child = spawn(path.resolve(__dirname + ("/../script/" + scriptName)), args); - child.stdout.on('data', function(data) { - return process.stdout.write(data); - }); - child.stderr.on('data', function(data) { - return process.stderr.write(data.toString()); + runNpm = function(args, env_args) { + if (env_args == null) { + env_args = {}; + } + return runCmd('npm', slice.call(args).concat(['--silent']), env_args); + }; + + runCmd = function(cmd, args, env_args) { + var child, env, ref, ref1; + if (env_args == null) { + env_args = {}; + } + env = Object.create(process.env); + Object.assign(env, env_args); + child = spawn(cmd, args, { + env: env, + stdio: 'inherit' }); + if ((ref = child.stdout) != null) { + ref.on('data', function(data) { + return process.stdout.write(data); + }); + } + if ((ref1 = child.stderr) != null) { + ref1.on('data', function(data) { + return process.stderr.write(data.toString()); + }); + } return child.on('exit', function(exitCode) { return process.exit(exitCode); }); }; - program.command('run ').description('build and run a native app for the specified platform').option('-e, --emulator', 'run on emulator instead of an actual device').action(function(platform, options) { - var deviceTypeArg; - deviceTypeArg = options.emulator ? '--emulator' : '--device'; - return executeScript('run-on-device', [platform, deviceTypeArg]); - }); + runScript = function(scriptName, args, env_args) { + if (env_args == null) { + env_args = {}; + } + return runCmd(path.resolve(__dirname + ("/../script/" + scriptName)), args, env_args); + }; - program.command('build ').description('build a native app for the specified platform').option('--release', 'create a release build').action(function(platform, options) { - var releaseArg; - releaseArg = options.release ? '--release' : '--debug'; - return executeScript('build-app', [platform, releaseArg]); - }); + literalArgs = function() { + if (program.rawArgs.indexOf('--') === -1) { + return []; + } else { + return program.rawArgs.slice(program.rawArgs.indexOf('--') + 1); + } + }; - program.command('new ').description('create a new Maji app').on('--help', function() { + program.command('new ').description('Create a new Maji app').on('--help', function() { return console.log(' Example:\n maji new org.example.my-app ~/Code/my-app'); }).action(function(packageName, path) { if (!packageName.match(/.*\..*\..*/)) { console.log('Please specify a valid package name, for example org.example.my-app'); process.exit(1); } - return executeScript('create-project', [packageName, path]); + return runScript('create-project', [packageName, path]); + }); + + program.command('run ').description('Build and run a native app for the specified platform').option('-e, --emulator', 'run on emulator instead of an actual device').option('--env --environment [environment]', 'APP_ENV to run with [development]').action(function(platform, options) { + var app_env, deviceTypeArg, env; + app_env = options.environment || 'development'; + env = { + 'APP_ENV': app_env + }; + deviceTypeArg = options.emulator ? '--emulator' : '--device'; + return runScript('run-on-device', [platform, deviceTypeArg].concat(slice.call(literalArgs())), env); + }); + + program.command('build [platform]').description('Build a native app for the specified platform').option('--release', 'create a release build').option('--env --environment [environment]', 'APP_ENV to build with [production]').action(function(platform, options) { + var app_env, env, releaseArg; + app_env = options.environment || 'production'; + env = { + 'APP_ENV': app_env + }; + if (platform) { + releaseArg = options.release ? '--release' : '--debug'; + return runScript('build-app', [platform, releaseArg].concat(slice.call(literalArgs())), env); + } else { + return runNpm(['run', 'build'], env); + } + }); + + program.command('test').option('--watch', 'Run tests when project files change').option('--unit', 'Run unit tests').option('--integration', 'Run integration tests').description('Run your project tests').action(function(options) { + if (options.watch) { + return runNpm(['run', 'test:watch']); + } + if (options.unit) { + return runNpm(['run', 'test:unit']); + } + if (options.integration) { + return runNpm(['run', 'test:integration']); + } + return runNpm(['test']); + }); + + program.command('start').description('Run the maji dev server and compile changes on the fly').option('-p --port [port]', 'Port to listen on [9090]', parsePort, 9090).option('-l --livereload [flag]', 'Enable livereload [false]', parseBoolean, false).action(function(options) { + var env; + env = { + 'SERVER_PORT': options.port, + 'LIVERELOAD': options.livereload + }; + return runNpm(['start'], env); }); program.on('--help', function() { diff --git a/lib/cordova_support.js b/lib/cordova_support.js index fc9c6d5..c94aedc 100644 --- a/lib/cordova_support.js +++ b/lib/cordova_support.js @@ -1,25 +1,21 @@ // Generated by CoffeeScript 1.9.3 (function() { - var $, bus, initCordova, publishOnBus; + var $, Radio, initCordova; $ = require('jquery'); - bus = require('./lib/bus'); - - publishOnBus = function(e) { - return bus.trigger("app:" + e.type); - }; + Radio = require('backbone.radio'); initCordova = function() { var eventName, i, len, ref; $(document).on('deviceready', function() { return require('./cordova/ios_network_activity').init(); }); - ref = ['pause', 'resume', 'backbutton', 'offline', 'online']; + ref = ['deviceready', 'pause', 'resume', 'backbutton', 'offline', 'online']; for (i = 0, len = ref.length; i < len; i++) { eventName = ref[i]; $(document).on(eventName, function(e) { - return publishOnBus(e); + return Radio.channel('app').trigger(e.type); }); } return $(function() { diff --git a/lib/index.js b/lib/index.js index 19a9d91..8cfa5e9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,12 +5,11 @@ UnrecoverableError: require('./errors/base_error').UnrecoverableError, ServiceError: require('./errors/service_error'), ServiceUnavailableError: require('./errors/service_unavailable_error'), - bus: require('./lib/bus'), I18n: require('./lib/i18n'), TemplateHelpers: require('./lib/template_helpers'), - Application: require('./marionnette_extensions/application'), - AnimatableRegion: require('./marionnette_extensions/animatable_region'), - Page: require('./marionnette_extensions/page'), + Application: require('./marionette_extensions/application'), + AnimatableRegion: require('./marionette_extensions/animatable_region'), + Page: require('./marionette_extensions/page'), ApplicationState: require('./storage/application_state'), Cache: require('./storage/cache'), JsonSetting: require('./storage/json_setting') diff --git a/lib/lib/bus.js b/lib/lib/bus.js deleted file mode 100644 index 4b2cdad..0000000 --- a/lib/lib/bus.js +++ /dev/null @@ -1,28 +0,0 @@ -// Generated by CoffeeScript 1.9.3 -(function() { - var Wreqr, commands, reqres, vent; - - Wreqr = require('backbone.wreqr'); - - commands = new Wreqr.Commands(); - - vent = new Wreqr.EventAggregator(); - - reqres = new Wreqr.RequestResponse(); - - module.exports = { - commands: commands, - vent: vent, - reqres: reqres, - execute: function() { - return commands.execute.apply(commands, arguments); - }, - request: function() { - return reqres.request.apply(reqres, arguments); - }, - trigger: function() { - return vent.trigger.apply(vent, arguments); - } - }; - -}).call(this); diff --git a/lib/marionnette_extensions/animatable_region.js b/lib/marionette_extensions/animatable_region.js similarity index 95% rename from lib/marionnette_extensions/animatable_region.js rename to lib/marionette_extensions/animatable_region.js index 4a7d4cf..53b9ac0 100644 --- a/lib/marionnette_extensions/animatable_region.js +++ b/lib/marionette_extensions/animatable_region.js @@ -24,6 +24,7 @@ }); this.showTransitions = opts.showTransitions; this.navigationStack = new NavigationStack(); + this.transitioning = false; AnimatableRegion.__super__.constructor.apply(this, arguments); } @@ -65,6 +66,12 @@ return window.location.hash = route; }; + AnimatableRegion.prototype._empty = function(view, shouldDestroy) { + if (!this.transitioning) { + return AnimatableRegion.__super__._empty.apply(this, arguments); + } + }; + AnimatableRegion.prototype.show = function(view, options) { var ref; if (options == null) { @@ -78,9 +85,9 @@ if (this.transition == null) { this.transition = view.transition || options.transition; } + this.transitioning = true; if (this.transition) { this.currentPage = this.currentView; - options.preventDestroy = true; } return AnimatableRegion.__super__.show.call(this, view, options); }; @@ -146,6 +153,7 @@ _this.currentPage = null; } _this.back = void 0; + _this.transitioning = false; _this.$el.removeClass('viewport-transitioning'); _this.$el.removeClass("viewport-" + _this.transition); return newPage.trigger('transitioned'); @@ -155,6 +163,7 @@ } else { newPage.$el.removeClass('page-pre-in'); this.back = void 0; + this.transitioning = false; return newPage.trigger('transitioned'); } }; diff --git a/lib/marionnette_extensions/application.js b/lib/marionette_extensions/application.js similarity index 69% rename from lib/marionnette_extensions/application.js rename to lib/marionette_extensions/application.js index 4dab5ca..d3ed131 100644 --- a/lib/marionnette_extensions/application.js +++ b/lib/marionette_extensions/application.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.9.3 (function() { - var AnimatableRegion, Application, Marionette, _, bus, + var AnimatableRegion, Application, Marionette, _, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; @@ -10,29 +10,28 @@ AnimatableRegion = require('./animatable_region'); - bus = require('../lib/bus'); - Application = (function(superClass) { extend(Application, superClass); - function Application(opts) { + function Application() { + return Application.__super__.constructor.apply(this, arguments); + } + + Application.prototype.region = '#maji-app'; + + Application.prototype.regionClass = AnimatableRegion; + + Application.prototype.initialize = function(opts) { if (opts == null) { opts = {}; } - Application.__super__.constructor.apply(this, arguments); - _.defaults(opts, { - showTransitions: true - }); require('./marionette_renderer').setup(); require('../cordova_support'); - this.bus = bus; - this.addRegions({ - mainRegion: new AnimatableRegion({ - el: '#maji-app', - showTransitions: opts.showTransitions - }) + _.defaults(opts, { + showTransitions: true }); - } + return this.getRegion().showTransitions = opts.showTransitions; + }; return Application; diff --git a/lib/marionnette_extensions/marionette_renderer.js b/lib/marionette_extensions/marionette_renderer.js similarity index 100% rename from lib/marionnette_extensions/marionette_renderer.js rename to lib/marionette_extensions/marionette_renderer.js diff --git a/lib/marionnette_extensions/page.js b/lib/marionette_extensions/page.js similarity index 97% rename from lib/marionnette_extensions/page.js rename to lib/marionette_extensions/page.js index 7397b07..a8b583b 100644 --- a/lib/marionnette_extensions/page.js +++ b/lib/marionette_extensions/page.js @@ -31,7 +31,7 @@ return Page; - })(Marionette.LayoutView); + })(Marionette.View); module.exports = Page; diff --git a/lib/marionette_renderer.js b/lib/marionette_renderer.js index 187a316..5920bb5 100644 --- a/lib/marionette_renderer.js +++ b/lib/marionette_renderer.js @@ -1,5 +1,5 @@ // Generated by CoffeeScript 1.9.3 (function() { - module.exports = require('./marionnette_extensions/marionette_renderer'); + module.exports = require('./marionette_extensions/marionette_renderer'); }).call(this); diff --git a/package.json b/package.json index 7a00340..d739a3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "maji", "version": "1.2.1", + "version": "2.0.0", "license": "MIT", "main": "lib/index.js", "bin": { @@ -12,9 +13,9 @@ "url": "https://github.com/kabisa/maji" }, "devDependencies": { - "backbone": "^1.2.3", - "backbone.marionette": "^2.4.4", - "backbone.wreqr": "^1.3.5", + "backbone": "1.3.3", + "backbone.marionette": "3.1.0", + "backbone.radio": "^2.0.0", "browserify": "~13.0.0", "coffee-script": "1.9.3", "coffeeify": "^1.0.0", @@ -40,5 +41,8 @@ "tiny-lr": "1.0.3", "chokidar": "1.6.1", "connect-livereload": "0.6.0" + }, + "engines": { + "node": ">=6" } } diff --git a/project_template/.scss-lint.yml b/project_template/.scss-lint.yml index 84f8f29..926a2cd 100644 --- a/project_template/.scss-lint.yml +++ b/project_template/.scss-lint.yml @@ -15,3 +15,5 @@ linters: PropertySpelling: extra_properties: - text-size-adjust + Indentation: + width: 4 \ No newline at end of file diff --git a/project_template/Makefile b/project_template/Makefile index 5b971ea..e052404 100644 --- a/project_template/Makefile +++ b/project_template/Makefile @@ -1,67 +1,81 @@ -export DIST_DIR ?= ./dist -export SHELL := /bin/bash -e -o pipefail +export SHELL := /usr/bin/env bash -e -o pipefail export PATH := $(PATH):$(shell npm bin) export APP_ENV ?=development -SERVER_PORT ?= 9090 -BROWSERIFY_BASE_CONFIG = \ - -t coffeeify \ - -t aliasify \ - -t yamlify \ - -t [ haml-coffee-browserify ] \ - -t [ envify purge ] \ - -t brfs \ - --extension .hamlc --extension .coffee +DIST_DIR := ./dist +BUILD_DIR ?= ./tmp/build +WATCH_BUILD_DIR := ./tmp/watch-build +JS_BUNDLE_MAIN := app/application.coffee -dist: clean build-statics build-js build-icons build-css revhash +WEBPACK_FILES := $(shell find app -name '*.coffee' -o -name '*.hamlc' -o -name '*.yml' -o -name '*.json') +SCSS_FILES := $(shell find app/styles -name '*.scss') +PUBLIC_FILES := $(shell find public -type f) +ICON_FILES := $(shell find app/styles/icons -name '*.svg') +ICON_FILES_TARGET := $(patsubst %, $(BUILD_DIR)/assets/fonts/icons.%, woff eot ttf) +PUBLIC_FILES_TARGET := $(PUBLIC_FILES:public/%=$(BUILD_DIR)/%) +ALL_TARGETS := $(BUILD_DIR)/assets/app.js \ + $(ICON_FILES_TARGET) \ + $(BUILD_DIR)/assets/app.css \ + $(PUBLIC_FILES_TARGET) -production: - APP_ENV=production make build +dist: check-env $(ALL_TARGETS) copy-to-dist -build-statics: - cp -R public/* $(DIST_DIR)/ - [ '$(LIVERELOAD)' = 'true' ] && node script/inject-livereload.js '$(DIST_DIR)/index.html' || exit 0 +$(BUILD_DIR)/assets/app.js: $(WEBPACK_FILES) + @echo Compiling js + @mkdir -p $(@D) + @webpack --hide-modules --output-path $(BUILD_DIR)/assets/ -revhash: - source bin/_functions ; HASH=$$(cat $(DIST_DIR)/assets/app.js | _md5) && \ - mv $(DIST_DIR)/assets/app.js $(DIST_DIR)/assets/app-$$HASH.js && \ - perl -i -pe s/app.js/app-$$HASH.js/ dist/index.html +$(BUILD_DIR)/assets/app.css: $(SCSS_FILES) $(ICON_FILES) + @echo Compiling sass + @mkdir -p $(@D) + @node-sass --stdout --output-style $${CSS_OUTPUT_STYLE:-compressed} --include-path vendor/styles app/styles/application.scss | postcss --use autoprefixer --autoprefixer.browsers 'ios >= 8, android >= 4, ie >=10' > $(BUILD_DIR)/assets/app.css - source bin/_functions ; HASH=$$(cat $(DIST_DIR)/assets/app.css | _md5) && \ - mv $(DIST_DIR)/assets/app.css $(DIST_DIR)/assets/app-$$HASH.css && \ - perl -i -pe s/app.css/app-$$HASH.css/ dist/index.html +$(ICON_FILES_TARGET): $(ICON_FILES) + @echo Building icon font + @DIST_DIR='$(BUILD_DIR)' node script/build-iconfont.js -build-js: - browserify -g uglifyify $(BROWSERIFY_BASE_CONFIG) --debug \ - app/application.coffee | exorcist $(DIST_DIR)/assets/app.js.map | sed '/^\s*$$/d' > $(DIST_DIR)/assets/app.js +$(BUILD_DIR)/%: public/% + @echo "Copying $<" + @mkdir -p $(@D) + @cp "$<" "$@" -build-css: - node-sass --stdout --output-style $${CSS_OUTPUT_STYLE:-compressed} --include-path vendor/styles app/styles/application.scss | postcss --use autoprefixer --autoprefixer.browsers 'ios >= 8, android >= 4, ie >=10' > $(DIST_DIR)/assets/app.css +copy-to-dist: + @echo Copying to $(DIST_DIR) + @rm -rf $(DIST_DIR)/* + @mkdir -p $(DIST_DIR) + @cp -R $(BUILD_DIR)/* $(DIST_DIR) -build-icons: - DIST_DIR='$(DIST_DIR)' node script/build-iconfont.js + @source bin/_functions ; HASH=$$(cat $(DIST_DIR)/assets/app.js | _md5) && \ + mv $(DIST_DIR)/assets/app.js $(DIST_DIR)/assets/app-$$HASH.js && \ + perl -i -pe s/app.js/app-$$HASH.js/ $(DIST_DIR)/index.html -livereload: - [ -n "$$LIVERELOAD" ] && ((sleep 5 && node script/livereload.js '$(DIST_DIR)/**/*') &) || exit 0 + @source bin/_functions ; HASH=$$(cat $(DIST_DIR)/assets/app.css | _md5) && \ + mv $(DIST_DIR)/assets/app.css $(DIST_DIR)/assets/app-$$HASH.css && \ + perl -i -pe s/app.css/app-$$HASH.css/ $(DIST_DIR)/index.html -check-server-port: - @nc -z 127.0.0.1 $(SERVER_PORT) >/dev/null && echo 'Port $(SERVER_PORT) occupied, is a server already running?' && exit 1 || exit 0 - -serve: DIST_DIR := './tmp/watch-build' -serve: check-server-port clean build-statics build-icons build-css livereload - onchange 'public/**/*' -- make build-statics & - CSS_OUTPUT_STYLE=expanded onchange 'app/styles/**/*.scss' -- make build-css & - onchange 'app/styles/icons/*.svg' -- make build-icons & - watchify -d -v $(BROWSERIFY_BASE_CONFIG) -x bugsnag-js \ - app/application.coffee -o $(DIST_DIR)/assets/app.js & - sleep 2 && http-server -p $(SERVER_PORT) $(DIST_DIR) - -watch: serve +clean: + @rm -rf $(BUILD_DIR) + @rm -rf $(WATCH_BUILD_DIR) test: bin/ci -clean: - rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)/assets +check-env: + @# check if we previously build with a different APP_ENV. If the APP_ENV has changed + @# we need to recompile + @[ "$$(cat tmp/.build-env 2>/dev/null)" == "$(APP_ENV)" ] || (echo '$(APP_ENV)' > tmp/.build-env && touch $(JS_BUNDLE_MAIN)) + @echo Building with APP_ENV = $(APP_ENV) + +watch: + @BUILD_DIR=$(WATCH_BUILD_DIR) $(MAKE) doWatch + +doWatch: check-env $(ALL_TARGETS) + @onchange 'public/**/*' -- $(MAKE) BUILD_DIR="$(WATCH_BUILD_DIR)" FILE='{{changed}}' copy-watched-file & + @CSS_OUTPUT_STYLE=expanded onchange 'app/styles/**/*.scss' -- $(MAKE) $(BUILD_DIR)/assets/app.css & + @onchange 'app/styles/icons/*.svg' -- $(MAKE) -s $(BUILD_DIR)/assets/fonts/icons.eot & + @webpack --output-path $(BUILD_DIR)/assets/ --watch & + @sleep 2 && maji-dev-server $(BUILD_DIR) --port=$(SERVER_PORT) --livereload=$(LIVERELOAD) + + +copy-watched-file: $(FILE:public/%=$(BUILD_DIR)/%) -.PHONY: clean build-statics build-js build-css build-icons serve watch -.SILENT: serve revhash +.PHONY: clean dist test check-env copy-to-dist watch doWatch copy-watched-file diff --git a/project_template/README.md b/project_template/README.md index 495ca85..920e0c4 100644 --- a/project_template/README.md +++ b/project_template/README.md @@ -8,13 +8,13 @@ ## Development workflow -* While developing you can run a local serve using `make watch`. This will start a server on http://localhost:9090. -* To create a static HTML5 app build run `make dist`. The app will be build into the `dist/` directory. +* While developing you can run a local serve using `bin/maji start`. This will start a server on http://localhost:9090. +* To create a static HTML5 app build run `bin/maji build`. The app will be build into the `dist/` directory. * To run the app on a connected mobile device run `bin/maji run `. * To build a Cordova app run `bin/maji build `. -* To run Javascript tests run `bin/karma start`. This will start a Karma server with Phantomjs and will continuously watch your Javascript files and run tests on changes. -* To run features specs run `bundle exec rspec`. -* To run all tests run `bin/ci`. +* To run Javascript tests run `bin/maji test --watch`. This will start a Karma server with Phantomjs and will continuously watch your Javascript files and run tests on changes. +* To run integration specs run `bin/maji test --integration`. +* To run all tests run `bin/maji test`. ## Packaging native apps / running on your device @@ -27,9 +27,8 @@ ### General -* Ruby, for the Capybara integration specs -* NodeJS, for the build system (`bin/setup` will install this if you've got Homebrew) -* Homebrew (`bin/setup` will use this to hook you up with all of the dependencies, except Ruby) +* Ruby + Bundler, for the integration specs +* Node.js >=6 + NPM, for the build system ### iOS @@ -41,5 +40,4 @@ * Android SDK * Android platform tools installed * Android platform 10+. -* Ant (`brew install ant`) * `android` and `adb` in your $PATH (add `path/to/android-sdk-macosx/tools` and `path/to/android-sdk-macosx/platform-tools` to your $PATH). diff --git a/project_template/app/app.coffee b/project_template/app/app.coffee index a039012..a84d6ef 100644 --- a/project_template/app/app.coffee +++ b/project_template/app/app.coffee @@ -5,30 +5,32 @@ require('./config/template_helpers') Backbone = require('backbone') Marionette = require('backbone.marionette') +Radio = require('backbone.radio') PageTransitionSupportDetector = require('./support/page_transition_support_detector') Maji = require('maji') app = new Maji.Application showTransitions: PageTransitionSupportDetector.supportsTransitions() -app.addInitializer -> - require('./modules/home/home_app').start() - -app.on 'start', (options) -> - Backbone.history.start() - Backbone.history.navigate(Backbone.history.fragment) - -app.bus.reqres.setHandler 'view:current', -> - app.mainRegion.currentView - -app.bus.reqres.setHandler 'uri:current', -> - Backbone.history.fragment - -app.bus.commands.setHandler 'navigate', (location, options = {}) -> - app.mainRegion.navigate(location, options, Backbone.history) - -app.bus.commands.setHandler 'go-back', (where, opts) -> - where = undefined if where == '#' - app.mainRegion.goBack(where, opts) +app.on 'before:start', -> + # include modules + require('./modules/home/home_app') + +app.on 'before:start', -> + # Bind the start event in after the inclusion of + # the modules, so that the routers are initialized + # before the kick-off of the backbone history + app.on 'start', (options) -> + Backbone.history.start() + +Radio.channel('app').reply( + 'view:current': -> app.getView() + 'uri:current': -> Backbone.history.fragment + 'navigate': (location, options = {}) -> + app.getRegion().navigate(location, options, Backbone.history) + 'go-back': (where, opts) -> + where = undefined if where == '#' + app.getRegion().goBack(where, opts) +) module.exports = app diff --git a/project_template/app/application.coffee b/project_template/app/application.coffee index cc395cf..5d48cdd 100644 --- a/project_template/app/application.coffee +++ b/project_template/app/application.coffee @@ -1,15 +1,15 @@ $ = require('jquery') -attachFastClick = require('fastclick') +FastClick = require('fastclick') app = require('./app') $ -> app.start() - attachFastClick(document.body) + FastClick.attach(document.body) # Stretch main container height so it's not resized when the viewport is # resized. This happens on Android when the keyboard pops up. setTimeout -> - window.containerOffsetTop = app.mainRegion.$el.offset().top + window.containerOffsetTop = $(app.getRegion().el).offset().top initialWindowHeight = $(window).height() - window.containerOffsetTop - app.mainRegion.$el.css('height', initialWindowHeight) + $(app.getRegion().el).css('height', initialWindowHeight) , 0 diff --git a/project_template/app/config/bugsnag.coffee b/project_template/app/config/bugsnag.coffee index ebc3034..00ab229 100644 --- a/project_template/app/config/bugsnag.coffee +++ b/project_template/app/config/bugsnag.coffee @@ -1,7 +1,5 @@ Settings = require('./settings') -try +if Settings.bugsnagApiKey Bugsnag = require('bugsnag-js') - Bugsnag.apiKey = Settings.bugsnagApiKey if Settings.bugsnagApiKey -catch - # ignore, bugsnag may not be included + Bugsnag.apiKey = Settings.bugsnagApiKey diff --git a/project_template/app/config/settings.coffee b/project_template/app/config/settings.coffee index 6580af6..031cef4 100644 --- a/project_template/app/config/settings.coffee +++ b/project_template/app/config/settings.coffee @@ -1,5 +1 @@ -fs = require('fs') - -module.exports = JSON.parse( - fs.readFileSync(__dirname + "/settings.#{process.env.APP_ENV}.json", 'utf8') -) +module.exports = require("./settings.#{process.env.APP_ENV}.json") diff --git a/project_template/app/config/template_helpers.coffee b/project_template/app/config/template_helpers.coffee index b1be7e2..376df64 100644 --- a/project_template/app/config/template_helpers.coffee +++ b/project_template/app/config/template_helpers.coffee @@ -5,7 +5,7 @@ Maji = require('maji') # These helpers will be exposed in templates under the 'h' namespace. # # E.g a 'pluralize' helper could be called from a template as -# h.pluaralize 'something' +# h.pluralize 'something' class AppTemplateHelpers Maji.TemplateHelpers.register(new AppTemplateHelpers()) diff --git a/project_template/app/modules/home/home_app.coffee b/project_template/app/modules/home/home_app.coffee index 2ca3e05..c019e02 100644 --- a/project_template/app/modules/home/home_app.coffee +++ b/project_template/app/modules/home/home_app.coffee @@ -1,24 +1,18 @@ app = require('app/app') Marionette = require('backbone.marionette') -HomeApp = app.module('home') -HomeApp.startWithParent = false - -class HomeApp.Router extends Marionette.AppRouter - appRoutes: +class HomeRouter extends Marionette.AppRouter + routes: '' : 'home' 'detail' : 'detail' -API = home: -> IndexPage = require('./views/index_page') - app.mainRegion.show new IndexPage() + app.showView new IndexPage() + detail: -> DetailPage = require('./views/detail_page') - app.mainRegion.show new DetailPage() - -HomeApp.addInitializer -> - new HomeApp.Router - controller: API + app.showView new DetailPage() -module.exports = HomeApp +app.on 'start', -> + new HomeRouter diff --git a/project_template/app/modules/home/templates/detail.hamlc b/project_template/app/modules/home/templates/detail.hamlc index 963f838..1a66aac 100644 --- a/project_template/app/modules/home/templates/detail.hamlc +++ b/project_template/app/modules/home/templates/detail.hamlc @@ -1,5 +1,8 @@ -.header - %a{href:'#/', class:'btn left', 'data-rel': 'back'} - = 'Detail' +%header.app-header + %a.app-header-back{href:'#/', 'data-rel': 'back'} + %h1= 'Detail' -.body + +%main.app-content + %p + This is the detail page \ No newline at end of file diff --git a/project_template/app/modules/home/templates/index.hamlc b/project_template/app/modules/home/templates/index.hamlc index 388c080..a8796f7 100644 --- a/project_template/app/modules/home/templates/index.hamlc +++ b/project_template/app/modules/home/templates/index.hamlc @@ -1,15 +1,19 @@ -.header= '##APP_NAME##' +%header.app-header + %h1= '##APP_NAME##' -.body - %p.welcome{style: 'margin-top: 0'}= @t('hello') +%main.app-content + %p= @t('hello') - %p - %strong Some example transitions: + %p + %strong Some example transitions: - %ul.listview - %li - %a{href: '#/detail', 'data-transition': 'slide'}= 'Slide' - %li - %a{href: '#/detail', 'data-transition': 'slideup'}= 'Slide up' - %li - %a{href: '#/detail', 'data-transition': 'flip'}= 'Flip' + %ul.navigation-list + %li + %a{href: '#/detail', 'data-transition': 'slide'} + %span= 'Slide' + %li + %a{href: '#/detail', 'data-transition': 'slideup'} + %span= 'Slide up' + %li + %a{href: '#/detail', 'data-transition': 'flip'} + %span= 'Flip' diff --git a/project_template/app/modules/home/views/index_page.coffee b/project_template/app/modules/home/views/index_page.coffee index 4c43d12..f7c050b 100644 --- a/project_template/app/modules/home/views/index_page.coffee +++ b/project_template/app/modules/home/views/index_page.coffee @@ -1,16 +1,14 @@ ApplicationPage = require('app/views/application_page') template = require('../templates/index') $ = require('jquery') -bus = require('maji').bus class IndexPage extends ApplicationPage template: template events: - 'click': (e) -> - e.preventDefault() - target = $(e.target) - + 'click a[data-transition]': (e) -> + target = $(e.currentTarget) @navigate(target.attr('href'), transition: target.data('transition')) + return false module.exports = IndexPage diff --git a/project_template/app/styles/_forms.scss b/project_template/app/styles/_forms.scss deleted file mode 100644 index a1e4f1e..0000000 --- a/project_template/app/styles/_forms.scss +++ /dev/null @@ -1,41 +0,0 @@ -:focus { - outline: none; -} - -::-moz-focus-inner { - border: 0; -} - -form { - input[type=text], input[type=password], textarea, input[type=number] { - width: 100%; - - padding: 10px; - margin-bottom: 5px; - - outline: none; - background: $color-form-input-bg; - border: 1px solid $color-form-input-border; - - appearance: none; - box-sizing: border-box; - box-shadow: none; - border-radius: 0; - - &:focus { - border: 1px solid $color-form-input-border-focus; - } - } - - input[type=submit] { - width: 100%; - background: $color-primary; - color: $color-primary-text; - -webkit-appearance: none; - outline: none; - - border: 0; - border-radius: 2px; - padding: 10px 19px; - } -} diff --git a/project_template/app/styles/application.scss b/project_template/app/styles/application.scss index a59f7be..46d38bc 100644 --- a/project_template/app/styles/application.scss +++ b/project_template/app/styles/application.scss @@ -1,127 +1,57 @@ -@import 'normalize'; -@import 'maji/screen_transitions'; -@import 'fonts/lato'; -@import 'fonts/icons'; -@import 'variables'; -@import 'forms'; -@import 'platforms/ios'; - @charset 'utf-8'; -html, body { - margin: 0; - padding: 0; - font-family: 'Lato'; - text-size-adjust: 100%; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - user-select: none; -} - -a, a:active, a:hover, a:visited { - text-decoration: none; - color: $color-link; -} - -#maji-app { - position: relative; -} - -.page { - position: absolute; - width: 100%; - height: 100%; - left: 0px; - top: 0px; - - z-index: 2; - background: $color-page-background; - - &.animated .header, &.slideout { - position: absolute; - } -} - -.header { - left: 0; - right: 0; - position: absolute; - z-index: 1000; - - margin: 0; - height: 30px; - line-height: 30px; - - padding: 10px; - background: $color-header-bg; - - text-align: center; - color: $color-header-text; - font-size: 1.2em; - - .btn { - position: absolute; - top: 0px; - - color: $color-header-text; - font-size: 0.8em; - - &[data-rel=back] { - @include icon(back); - margin-left: -10px; - - // scss-lint:disable NestingDepth - &:before { float: left; } - } - - &.left { - left: 5px; - } - - &.right { - right: 5px; - } - } -} - -.body { - padding: 65px 15px 15px; - box-sizing: border-box; -} - -.btn { - display: inline-block; - padding: 10px; -} - -.listview { - list-style-type: none; - padding: 0; - - margin: -15px; - - li { - @include icon(back, after); - - position: relative; - padding: 10px 15px; - - border-bottom: 1px solid $color-listview-li-border; - - &:after { - font-size: 12px; - position: absolute; - top: 50%; - right: 10px; - margin-top: -6px; - transform: rotate(180deg); - } - - a { - display: block; - margin: -10px -15px; - padding: 10px 15px; +/////////////////////////////////////////////////////////// +// +// Use Maji's app-like screen transitions +// +/////////////////////////////////////////////////////////// +@import 'maji/screen_transitions'; +@import 'fonts/icons'; - -webkit-tap-highlight-color: rgba(130, 130, 130, 0.2); - } - } -} +/////////////////////////////////////////////////////////// +// +// Global settings for the project +// +/////////////////////////////////////////////////////////// +@import 'settings/general'; +@import 'settings/colors'; +@import 'settings/typography'; + +/////////////////////////////////////////////////////////// +// +// Globally available tooling +// +/////////////////////////////////////////////////////////// +@import 'tools/mixins'; + +/////////////////////////////////////////////////////////// +// +// High-level styles +// +/////////////////////////////////////////////////////////// +@import 'generic/reset'; +@import 'generic/box-sizing'; + +/////////////////////////////////////////////////////////// +// +// Bare, unclassed Elements +// +/////////////////////////////////////////////////////////// +@import 'elements/base'; + +/////////////////////////////////////////////////////////// +// +// Basic building blocks of the layout +// +/////////////////////////////////////////////////////////// +@import 'objects/media-object'; + +/////////////////////////////////////////////////////////// +// +// UI components +// +/////////////////////////////////////////////////////////// +@import 'components/page'; +@import 'components/header'; +@import 'components/content'; +@import 'components/navigation-list'; diff --git a/project_template/app/styles/components/_content.scss b/project_template/app/styles/components/_content.scss new file mode 100644 index 0000000..1250f1c --- /dev/null +++ b/project_template/app/styles/components/_content.scss @@ -0,0 +1,13 @@ +/////////////////////////////////////////////////////////// +// +// Main content for your application +// +/////////////////////////////////////////////////////////// +.app-content { + background: $page-background; + color: $text-color; + + overflow-y: auto; + flex: 1; + padding: $page-padding; +} diff --git a/project_template/app/styles/components/_header.scss b/project_template/app/styles/components/_header.scss new file mode 100644 index 0000000..f8d31ac --- /dev/null +++ b/project_template/app/styles/components/_header.scss @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////// +// +// Header for your application +// +/////////////////////////////////////////////////////////// +.app-header { + background: $header-background; + color: $header-text; + height: $header-height; + + display: flex; + justify-content: center; + align-items: center; + + // Make room for iOS bar + .platform-ios & { + height: calc(#{$header-height} + #{$header-ios-offset}); + padding-top: $header-ios-offset; + } + + a, + h1 { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + a { + width: $header-button-width; + } + + h1 { + flex: 1; + + &:first-child { + // Compensate for no button on the left side + margin-left: $header-button-width; + } + + &:last-child { + // Compensate for no button on the right side + margin-right: $header-button-width; + } + } +} + +.app-header-back { + @include icon(back); +} diff --git a/project_template/app/styles/components/_navigation-list.scss b/project_template/app/styles/components/_navigation-list.scss new file mode 100644 index 0000000..78c35b1 --- /dev/null +++ b/project_template/app/styles/components/_navigation-list.scss @@ -0,0 +1,33 @@ +/////////////////////////////////////////////////////////// +// +// List of clickable links +// +/////////////////////////////////////////////////////////// +.navigation-list { + li { + margin-left: -$page-padding; + margin-right: -$page-padding; + border-bottom: 1px solid $navigation-list-border; + + &:first-child { + border-top: 1px solid $navigation-list-border; + } + } + + a { + @include icon(forward, after); + + height: $navigation-list-height; + padding: 0 $page-padding; + display: flex; + align-items: center; + + span { + flex: 1; + } + + &::after { + color: $navigation-list-chevron; + } + } +} diff --git a/project_template/app/styles/components/_page.scss b/project_template/app/styles/components/_page.scss new file mode 100644 index 0000000..ea4bd67 --- /dev/null +++ b/project_template/app/styles/components/_page.scss @@ -0,0 +1,12 @@ +/////////////////////////////////////////////////////////// +// +// Maji page +// +/////////////////////////////////////////////////////////// +.page { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} diff --git a/project_template/app/styles/elements/_base.scss b/project_template/app/styles/elements/_base.scss new file mode 100644 index 0000000..75d6568 --- /dev/null +++ b/project_template/app/styles/elements/_base.scss @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////// +// +// Base styles +// +/////////////////////////////////////////////////////////// +html { + background: $page-background; + font-family: $default-font-stack; + text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + // Avoid horizontal squeeze under minimum mobile screen width + min-width: 320px; +} + +html, +body, +#maji-app { + position: relative; + height: 100%; + margin: 0; + padding: 0; + line-height: 1.25; +} + +// Maji sets height hardcoded on this element. We don't want +// that so we overwrite it here. Better would be to not +// set this height at all! (TODO) +#maji-app { + // scss-lint:disable ImportantRule + height: 100% !important; +} + +button, +a { + -webkit-tap-highlight-color: transparent; + text-decoration: none; +} + +a, +a:hover, +a:active, +a:visited { + color: inherit; +} + +strong { + font-weight: $font-weight-bold; +} + +p { + margin-bottom: 1em; +} diff --git a/project_template/app/styles/fonts/lato.scss b/project_template/app/styles/fonts/lato.scss deleted file mode 100644 index 7e4e21b..0000000 --- a/project_template/app/styles/fonts/lato.scss +++ /dev/null @@ -1,35 +0,0 @@ -// -// Font files can be found in /public/assets/fonts -// -@font-face { - font-family: 'Lato'; - font-style: normal; - font-weight: 400; - src: local('Lato Regular'), - local('Lato-Regular'), - url('fonts/lato-regular.ttf') format('truetype'); -} -@font-face { - font-family: 'Lato'; - font-style: normal; - font-weight: 700; - src: local('Lato Bold'), - local('Lato-Bold'), - url('fonts/lato-bold.ttf') format('truetype'); -} -@font-face { - font-family: 'Lato'; - font-style: normal; - font-weight: 900; - src: local('Lato Black'), - local('Lato-Black'), - url('fonts/lato-black.ttf') format('truetype'); -} -@font-face { - font-family: 'Lato'; - font-style: italic; - font-weight: 400; - src: local('Lato Italic'), - local('Lato-Italic'), - url('fonts/lato-italic.ttf') format('truetype'); -} diff --git a/project_template/app/styles/generic/_box-sizing.scss b/project_template/app/styles/generic/_box-sizing.scss new file mode 100644 index 0000000..b222bf8 --- /dev/null +++ b/project_template/app/styles/generic/_box-sizing.scss @@ -0,0 +1,10 @@ +// apply a natural box layout model to all elements, but allowing components to change +html { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} diff --git a/project_template/app/styles/generic/_reset.scss b/project_template/app/styles/generic/_reset.scss new file mode 100644 index 0000000..c315d57 --- /dev/null +++ b/project_template/app/styles/generic/_reset.scss @@ -0,0 +1,50 @@ +// scss-lint:disable all + +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/project_template/app/styles/icons/back.svg b/project_template/app/styles/icons/back.svg index df7f320..414df9d 100644 --- a/project_template/app/styles/icons/back.svg +++ b/project_template/app/styles/icons/back.svg @@ -1,8 +1 @@ - - - - - - + \ No newline at end of file diff --git a/project_template/app/styles/icons/forward.svg b/project_template/app/styles/icons/forward.svg new file mode 100644 index 0000000..3a41f60 --- /dev/null +++ b/project_template/app/styles/icons/forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_template/app/styles/objects/_media-object.scss b/project_template/app/styles/objects/_media-object.scss new file mode 100644 index 0000000..f859666 --- /dev/null +++ b/project_template/app/styles/objects/_media-object.scss @@ -0,0 +1,26 @@ +/////////////////////////////////////////////////////////// +// +// Media object +// +/////////////////////////////////////////////////////////// +.media { + display: flex; + align-items: flex-start; +} + +.media-image { + margin-right: 0.5em; + + &.right { + margin-right: auto; + margin-left: 0.5em; + } +} + +.media-body { + flex: 1 0 auto; + + &.center { + align-self: center; + } +} diff --git a/project_template/app/styles/platforms/ios.scss b/project_template/app/styles/platforms/ios.scss deleted file mode 100644 index 501cfa7..0000000 --- a/project_template/app/styles/platforms/ios.scss +++ /dev/null @@ -1,19 +0,0 @@ -.platform-ios { - $offset-top: 15px; - - #maji-app { - margin-top: $offset-top; - } - - .header { - &:before { - content: ''; - background: $color-header-bg; - height: $offset-top; - display: block; - position: absolute; - left: 0; right: 0; - top: -$offset-top; - } - } -} diff --git a/project_template/app/styles/settings/_colors.scss b/project_template/app/styles/settings/_colors.scss new file mode 100644 index 0000000..54a4bd6 --- /dev/null +++ b/project_template/app/styles/settings/_colors.scss @@ -0,0 +1,49 @@ + +/////////////////////////////////////////////////////////// +// +// Base brand colors +// +/////////////////////////////////////////////////////////// +$brand-black: #000; +$brand-white: #fff; +$brand-grey-medium: #777; +$brand-grey-dark: #333; +$brand-red: #f00; +$brand-green: #0f0; +$brand-blue: #009cff; + +/////////////////////////////////////////////////////////// +// +// General colors +// +/////////////////////////////////////////////////////////// +$text-color: $brand-black; +$text-color-light: $brand-white; +$page-background: $brand-white; + +/////////////////////////////////////////////////////////// +// +// Button colors +// +/////////////////////////////////////////////////////////// +$primary-button-background: $brand-blue; +$primary-button-text: $brand-white; + +$secondary-button-background: $brand-grey-medium; +$secondary-button-text: $brand-white; + +/////////////////////////////////////////////////////////// +// +// Header +// +/////////////////////////////////////////////////////////// +$header-background: $brand-blue; +$header-text: $text-color-light; + +/////////////////////////////////////////////////////////// +// +// Navigation list +// +/////////////////////////////////////////////////////////// +$navigation-list-border: $brand-grey-medium; +$navigation-list-chevron: $brand-blue; diff --git a/project_template/app/styles/settings/_general.scss b/project_template/app/styles/settings/_general.scss new file mode 100644 index 0000000..9d002db --- /dev/null +++ b/project_template/app/styles/settings/_general.scss @@ -0,0 +1,15 @@ +/////////////////////////////////////////////////////////// +// +// Generic values +// +/////////////////////////////////////////////////////////// +$page-padding: 1rem; // Distance from edge of screen +$border-radius: 0.5rem; +$border-radius-small: 0.125rem; +$easing: cubic-bezier(0.4, 0, 0.2, 1); // Uniform animation easing + +$header-height: 3rem; +$header-ios-offset: 16px; +$header-button-width: $header-height; + +$navigation-list-height: 3rem; diff --git a/project_template/app/styles/settings/_typography.scss b/project_template/app/styles/settings/_typography.scss new file mode 100644 index 0000000..ff14dd0 --- /dev/null +++ b/project_template/app/styles/settings/_typography.scss @@ -0,0 +1,15 @@ +/////////////////////////////////////////////////////////// +// +// Typography related values +// +/////////////////////////////////////////////////////////// +$default-font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + +$font-size-huge: 2.75rem; +$font-size-large: 1.75rem; +$font-size-medium: 1.25rem; +$font-size-regular: 1rem; +$font-size-small: 0.75rem; + +$font-weight-normal: 400; +$font-weight-bold: 700; diff --git a/project_template/app/styles/tools/_mixins.scss b/project_template/app/styles/tools/_mixins.scss new file mode 100644 index 0000000..e69de29 diff --git a/project_template/app/styles/variables.scss b/project_template/app/styles/variables.scss deleted file mode 100644 index 3bd24b8..0000000 --- a/project_template/app/styles/variables.scss +++ /dev/null @@ -1,13 +0,0 @@ -$color-primary: #004c9c; -$color-primary-text: #fff; - -$color-header-bg: $color-primary; -$color-header-text: #fff; - -$color-form-input-bg: #fff; -$color-form-input-border: #ccc; -$color-form-input-border-focus: #b5b5b5; - -$color-link: #000; -$color-page-background: #fff; -$color-listview-li-border: #e8e8e8; diff --git a/project_template/app/views/application_page.coffee b/project_template/app/views/application_page.coffee index d667d09..48d5b62 100644 --- a/project_template/app/views/application_page.coffee +++ b/project_template/app/views/application_page.coffee @@ -1,5 +1,5 @@ Maji = require('maji') -bus = require('../app').bus +Radio = require('backbone.radio') class ApplicationPage extends Maji.Page events: @@ -8,7 +8,7 @@ class ApplicationPage extends Maji.Page constructor: -> super - @listenTo bus.vent, 'app:backbutton', @onBackButton + @listenTo Radio.channel('app'), 'backbutton', @onBackButton goHome: (e) -> e && e.preventDefault() @@ -27,6 +27,6 @@ class ApplicationPage extends Maji.Page false navigate: (href, options = {}) -> - bus.commands.execute 'navigate', href, options + Radio.channel('app').request('navigate', href, options) module.exports = ApplicationPage diff --git a/project_template/bin/_functions b/project_template/bin/_functions index 6703601..f97f1dc 100755 --- a/project_template/bin/_functions +++ b/project_template/bin/_functions @@ -12,23 +12,10 @@ function _md5() { return 0 } -function brew_dep_installed() { - brew list | grep "^$@$" >/dev/null 2>&1 -} - function command_available() { command -v "$@" >/dev/null 2>&1 } -function brew_available() { - command_available 'brew' -} - function warning() { echo "$(tput setaf 3)$@$(tput sgr0)" } - -function npm_package_available() { - local package=$(npm list --depth=0 2>/dev/null | grep "$@" | cut -d ' ' -f2) - [[ $package == "$@" ]] -} diff --git a/project_template/bin/ci b/project_template/bin/ci index 918e467..4add609 100755 --- a/project_template/bin/ci +++ b/project_template/bin/ci @@ -3,8 +3,8 @@ set -e export APP_ENV=test -PRE_BUILT=true bundle exec rspec -./node_modules/karma/bin/karma start --singleRun +npm run test:unit +npm run test:integration find app/ \ -type f -name '*.coffee' \ diff --git a/project_template/bin/karma b/project_template/bin/karma deleted file mode 100755 index 32b9b67..0000000 --- a/project_template/bin/karma +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -export APP_ENV=test -./node_modules/karma/bin/karma "$@" diff --git a/project_template/bin/setup b/project_template/bin/setup index 56ff5d6..d8d6a03 100755 --- a/project_template/bin/setup +++ b/project_template/bin/setup @@ -3,35 +3,7 @@ source "$(dirname ${BASH_SOURCE[0]})/_functions" PROJECT=$(basename $(pwd)) echo "Setting up $PROJECT" -if brew_available ; then - brew_dep_installed 'node' || brew install node - brew_dep_installed 'ant' || brew install ant - - if ! command_available 'phantomjs' ; then - brew install phantomjs - fi - - # npm is included with node in newer version, so this normally shouldn't be needed. - command_available 'npm' || brew install npm -else - warning 'Warning: Homebrew not detected. Not installing some dependencies.' -fi - -if [[ "$OSTYPE" == "darwin"* ]]; then - npm_package_available 'ios-deploy@1.8.5' || npm install 'ios-deploy@1.8.5' - npm_package_available 'ios-sim@5.0.6' || npm install 'ios-sim@5.0.6' -fi - npm install - -# Temporary fix because falafel has a dependency on acorn 0.11.0, this can be removed when those packages are upgraded -# my-app@1.0.0 -# └─┬ aliasify@1.8.0 -# └─┬ browserify-transform-tools@1.3.3 -# └─┬ falafel@1.0.1 -# └── acorn@0.11.0 -npm upgrade acorn - bundle install if [ -d .git ]; then diff --git a/project_template/config/iconfont.scss.js b/project_template/config/iconfont.scss.js index bc3fd92..64ea4ea 100644 --- a/project_template/config/iconfont.scss.js +++ b/project_template/config/iconfont.scss.js @@ -1,44 +1,45 @@ @font-face { - font-family: '<%= fontName %>'; - src: url('<%= fontPath + fontName %>.eot'); - src: url('<%= fontPath + fontName %>.eot?#iefix') format('eot'), + font-family: '<%= fontName %>'; + src: url('<%= fontPath + fontName %>.eot'); + src: url('<%= fontPath + fontName %>.eot?#iefix') format('eot'), url('<%= fontPath + fontName %>.woff') format('woff'), url('<%= fontPath + fontName %>.ttf') format('truetype'), url('<%= fontPath + fontName %>.svg#<%= fontName %>') format('svg'); } %icon { - font-family: '<%= fontName %>'; - font-smoothing: antialiased; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-style: normal; - font-variant: normal; - font-weight: normal; - text-decoration: none; - text-transform: none; - text-rendering: auto; + font-family: '<%= fontName %>'; + font-smoothing: antialiased; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-style: normal; + font-variant: normal; + font-weight: normal; + text-decoration: none; + text-transform: none; + text-rendering: auto; + user-select: none; // Avoid text being selectable } @function icon-char($filename) { - $char: ''; -<% _.each(glyphs, function(glyph) { %> - @if $filename == <%= glyph.fileName %> { - $char: '\<%= glyph.codePoint %>'; - }<% }); %> - - @return $char; + $char: ''; + <% _.each(glyphs, function(glyph) { %> + @if $filename == <%= glyph.fileName %> { + $char: '\<%= glyph.codePoint %>'; + } + <% }); %> + @return $char; } @mixin icon($filename, $insert: before) { - &:#{$insert} { - @extend %icon; - content: icon-char($filename); - } + &:#{$insert} { + @extend %icon; + content: icon-char($filename); + } } <% _.each(glyphs, function(glyph) { %>%icon-<%= glyph.fileName %> { - @include icon(<%= glyph.fileName %>); + @include icon(<%= glyph.fileName %>); } <% }); %> diff --git a/project_template/cordova/config.xml b/project_template/cordova/config.xml index f366b34..5dfeaf0 100644 --- a/project_template/cordova/config.xml +++ b/project_template/cordova/config.xml @@ -5,6 +5,11 @@ + + + + + @@ -12,9 +17,8 @@ - - + @@ -22,4 +26,8 @@ + + + + diff --git a/project_template/cordova/plugins.txt b/project_template/cordova/plugins.txt deleted file mode 100644 index 4270a10..0000000 --- a/project_template/cordova/plugins.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Add your plugins here, one per line -org.apache.cordova.splashscreen -com.mallzee.plugin.networkactivity https://github.com/mallzee/network-activity.git#0904b83c3109560bad8e7dd9be1320bb9303899d diff --git a/project_template/dockerfiles/ci/Dockerfile b/project_template/dockerfiles/ci/Dockerfile index 55a89b1..8030d32 100644 --- a/project_template/dockerfiles/ci/Dockerfile +++ b/project_template/dockerfiles/ci/Dockerfile @@ -13,16 +13,9 @@ RUN apt-get update \ locales \ && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Install NodeJS 5 -RUN curl -sL https://deb.nodesource.com/setup_5.x | bash - \ +RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \ && apt-get install --yes nodejs -# Install PhantomJS 2.1.1 -RUN cd /tmp && curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -o phantomjs-2.1.1-linux-x86_64.tar.bz2 \ - && tar xjf phantomjs-2.1.1-linux-x86_64.tar.bz2 \ - && mv /tmp/phantomjs-*/bin/phantomjs /usr/bin/ \ - && rm -r /tmp/phantomjs* - ENV CONTAINER_INIT /usr/local/bin/init-container RUN echo '#!/usr/bin/env bash' > $CONTAINER_INIT ; chmod +x $CONTAINER_INIT @@ -37,5 +30,4 @@ RUN echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && locale-gen ENV LC_ALL en_US.UTF-8 ENV LANG en_US.UTF-8 ENV LANGUAGE en_US.UTF-8 -ENV PHANTOMJS_BIN /usr/bin/phantomjs ENV BUNDLE_PATH /cache/bundle diff --git a/project_template/karma.conf.js b/project_template/karma.conf.js index 0827179..36a0145 100644 --- a/project_template/karma.conf.js +++ b/project_template/karma.conf.js @@ -3,16 +3,14 @@ module.exports = function(karma) { karma.set({ - frameworks: [ 'mocha', 'sinon-chai', 'browserify' ], + frameworks: [ 'mocha', 'sinon-chai' ], files: [ - { pattern: 'spec/spec_helper.coffee', watched: false, included: true, served: true }, - { pattern: 'spec/**/*spec.coffee', watched: false, included: true, served: true } + 'spec/spec_index.coffee' ], preprocessors: { - 'spec/spec_helper.coffee': [ 'browserify' ], - 'spec/**/*spec.coffee': [ 'browserify' ] + 'spec/spec_index.coffee': ['webpack'] }, client: { @@ -25,11 +23,9 @@ module.exports = function(karma) { reporters: ['mocha'], browsers: [ 'PhantomJS' ], - // browserify configuration - browserify: { - debug: true, - extensions: ['.hamlc', '.coffee'], - transform: [ 'coffeeify', 'aliasify', 'yamlify', 'haml-coffee-browserify', ['envify', { _: 'purge' }], 'brfs' ] + webpack: require('./webpack.config.js'), + webpackMiddleware: { + stats: 'errors-only' } }); }; diff --git a/project_template/package.json b/project_template/package.json index 126c584..035fe2c 100644 --- a/project_template/package.json +++ b/project_template/package.json @@ -1,61 +1,52 @@ - { - "name": "##APP_NAME##", +{ + "name": "example", "version": "1.0.0", "private": true, "scripts": { - "postinstall": "npm dedupe" + "test": "bin/ci", + "test:unit": "APP_ENV=test karma start --single-run", + "test:integration": "APP_ENV=test PRE_BUILT=true bundle exec rspec", + "test:watch": "APP_ENV=test karma start --watch", + "start": "make watch", + "build": "make dist" }, "dependencies": { - "backbone": "~1.2.3", + "backbone": "1.3.3", "backbone.localstorage": "~1.1.16", - "backbone.marionette": "~2.4.4", - "backbone.wreqr": "~1.3.5", + "backbone.marionette": "3.1.0", + "backbone.radio": "^2.0.0", "fastclick": "~1.0.6", - "jquery": "~2.1.3", + "jquery": "~3.0.0", "underscore": "~1.8.3", "bugsnag-js": "~2.4.9", - "maji": "kabisa/maji" + "maji": "kabisa/maji#2-0-stable" }, "devDependencies": { - "acorn": "~2.6.4", "autoprefixer": "~6.3.3", - "postcss-cli": "~2.5.1", - "brfs": "~1.4.3", - "browserify": "~13.0.0", - "coffeeify": "~2.0.1", - "aliasify": "~1.9.0", "chai": "~3.5.0", - "coffeelint": "~1.15.0", - "cordova": "~6.0.0", - "envify": "~3.4.0", - "exorcist": "~0.4.0", - "chokidar": "~1.4.1", + "coffee-loader": "^0.7.2", + "coffeelint": "~1.16.0", + "cordova": "~6.4.0", "gulp-iconfont": "~6.0.0", "gulp-iconfont-css": "~2.0.0", - "haml-coffee-browserify": "~0.0.4", - "http-server": "~0.9.0", - "sinon": "~1.17.2", + "hamlc-loader": "kabisa/hamlc-loader#fbd17dda7c5610dd42a88403e1d175754975d70e", + "json-loader": "^0.5.4", "karma": "~0.13.22", - "karma-browserify": "~5.0.2", "karma-mocha": "~0.2.2", "karma-mocha-reporter": "~2.0.0", - "karma-sinon-chai": "~1.2.0", "karma-phantomjs-launcher": "~1.0.0", - "lolex": "^1.4.0", - "mocha": "~2.4.5", - "node-sass": "~3.4.2", + "karma-sinon-chai": "~1.2.0", + "karma-webpack": "^1.8.1", + "memo-is": "0.0.2", + "mocha": "~3.1.2", + "node-sass": "~4.1.1", "onchange": "~2.1.2", "phantomjs-prebuilt": "~2.1.3", + "postcss-cli": "~2.5.1", + "sinon": "~1.17.2", "sinon-chai": "2.8.0", - "tiny-lr": "~0.2.1", - "uglifyify": "~3.0.1", "vinyl-fs": "~2.4.2", - "watchify": "~3.7.0", - "yamlify": "~0.1.2" - }, - "aliasify": { - "aliases": { - "app": "./app" - } + "webpack": "^2.2.1", + "yaml-loader": "^0.4.0" } } diff --git a/project_template/public/assets/fonts/lato-black.ttf b/project_template/public/assets/fonts/lato-black.ttf deleted file mode 100644 index 543bc8b..0000000 Binary files a/project_template/public/assets/fonts/lato-black.ttf and /dev/null differ diff --git a/project_template/public/assets/fonts/lato-bold.ttf b/project_template/public/assets/fonts/lato-bold.ttf deleted file mode 100644 index 93166dc..0000000 Binary files a/project_template/public/assets/fonts/lato-bold.ttf and /dev/null differ diff --git a/project_template/public/assets/fonts/lato-italic.ttf b/project_template/public/assets/fonts/lato-italic.ttf deleted file mode 100644 index e2027ff..0000000 Binary files a/project_template/public/assets/fonts/lato-italic.ttf and /dev/null differ diff --git a/project_template/public/assets/fonts/lato-regular.ttf b/project_template/public/assets/fonts/lato-regular.ttf deleted file mode 100644 index d6101f3..0000000 Binary files a/project_template/public/assets/fonts/lato-regular.ttf and /dev/null differ diff --git a/project_template/script/inject-livereload.js b/project_template/script/inject-livereload.js deleted file mode 100644 index 3ae8339..0000000 --- a/project_template/script/inject-livereload.js +++ /dev/null @@ -1,17 +0,0 @@ -var fs = require('fs'); - -var injectLiveReloadTag = function(html) { - var livereloadScriptTag = ""; - - var bodyTagLocation = html.indexOf(''); - - if(bodyTagLocation != -1) { - html = html.slice(0, bodyTagLocation) + livereloadScriptTag + html.slice(bodyTagLocation); - } - - return html; -}; - -var file = process.argv[2]; -var html = fs.readFileSync(file).toString(); -fs.writeFileSync(file, injectLiveReloadTag(html)); diff --git a/project_template/script/livereload.js b/project_template/script/livereload.js deleted file mode 100755 index ab95594..0000000 --- a/project_template/script/livereload.js +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node - -var tinylr = require('tiny-lr'); -var chokidar = require('chokidar'); - -var lr = new tinylr.Server -lr.listen(35729, function (err) { if(err) { throw err } }); - -var watcher = chokidar.watch(process.argv[2]); -watcher.on('ready', function(){ - watcher.on('all', function(event, path) { - console.log('Reloading browser...') - lr.changed({body: {files: [path]}}) - }); -}); diff --git a/project_template/spec/spec_helper.coffee b/project_template/spec/spec_helper.coffee index e242af8..f7e0860 100644 --- a/project_template/spec/spec_helper.coffee +++ b/project_template/spec/spec_helper.coffee @@ -1,3 +1,47 @@ Backbone = require('backbone') # Backbone.$ is not automatically set when backbone is loaded as CommonJS module $ = Backbone.$ = require('jquery') + +# Allow to use memo-isation in tests +# +# describe 'some feature', -> +# +# data = memo().is -> { foo: 'bar' } +# +# it 'shows bar', -> +# expect(data().foo).to.eql 'bar' +# +# describe 'when undefined', -> +# data.is -> { foo: undefined } +# +# it 'shows undefined', -> +# expect(data().foo).to.be.undefined +# +window.memo = require('memo-is') + +# Use wait for to wait for data to come in, +# DOM elements to exist, or spies +# to have been called +# +# waitFor(=> @collection.last()).then (lastMessage) -> # assertions +# waitFor(=> @spy.called).then -> expect(spy).to.have.been.calledWith('args') +# waitFor(=> @view.$('element').length > 0).then -> # assertions +# +window.waitFor = (test) -> + d = $.Deferred() + timeout = null + i = setInterval( + -> + return unless (result = test()) + clearInterval i + clearTimeout timeout + d.resolve(result) + 2 + ) + timeout = setTimeout( + -> + clearInterval i + d.reject(new Error('waitFor never resolved')) + 1500 + ) + d.promise() diff --git a/project_template/spec/spec_helper.rb b/project_template/spec/spec_helper.rb index 13fbf16..9202633 100644 --- a/project_template/spec/spec_helper.rb +++ b/project_template/spec/spec_helper.rb @@ -11,14 +11,17 @@ end Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, {extensions: ['spec/support/function_bind_polyfill.js']}) + Capybara::Poltergeist::Driver.new(app, { + extensions: ['spec/support/function_bind_polyfill.js'], + phantomjs: File.expand_path('node_modules/phantomjs-prebuilt/bin/phantomjs') + }) end Capybara.default_driver = :poltergeist if ENV['PRE_BUILT'] # Make sure our app is built - system('make dist') + system('bin/maji build') Capybara.app = Rack::File.new('dist/') else Capybara.app_host = 'http://localhost:9090/' diff --git a/project_template/spec/spec_index.coffee b/project_template/spec/spec_index.coffee new file mode 100644 index 0000000..864df66 --- /dev/null +++ b/project_template/spec/spec_index.coffee @@ -0,0 +1,14 @@ +# https://github.com/webpack/karma-webpack#alternative-usage +# +# Without this, karma and webpack don't play nice together since webpack +# will compile a bundle with jquery, marionette etc for each spec file. +# +# With this, all specs are compiled together. Single specs can be run with +# mochas describe.only feature. +testsContext = require.context('.', true, /_spec$/) +testsContext.keys().forEach (path) -> + try + testsContext(path) + catch err + console.error('[ERROR] WITH SPEC FILE:', path) + console.error(err) diff --git a/project_template/spec/views/index_page_spec.coffee b/project_template/spec/views/index_page_spec.coffee index d1ab056..4741750 100644 --- a/project_template/spec/views/index_page_spec.coffee +++ b/project_template/spec/views/index_page_spec.coffee @@ -7,4 +7,4 @@ describe 'IndexPage', -> it 'shows a message', -> DOM.append @view.render().el - expect(@view.$el.find('p.welcome').text().trim()).to.eq('Welcome to your Maji app!') + expect(@view.$el.find('p:first-child').text().trim()).to.eq('Welcome to your Maji app!') diff --git a/project_template/webpack.config.js b/project_template/webpack.config.js new file mode 100644 index 0000000..24ec666 --- /dev/null +++ b/project_template/webpack.config.js @@ -0,0 +1,44 @@ +const path = require('path'); +const webpack = require('webpack'); + +const APP_ENV = process.env.APP_ENV || 'development'; +const IS_PROD_BUILD = ! ["development", "test"].includes(APP_ENV); + +const plugins = [ + new webpack.DefinePlugin({ + 'process.env':{ + 'APP_ENV': JSON.stringify(APP_ENV), + } + }) +]; + +if(IS_PROD_BUILD) plugins.push(new webpack.optimize.UglifyJsPlugin({minimize: true})); + +module.exports = { + entry: './app/application.coffee', + output: { + path: path.resolve(__dirname, 'dist/assets'), + filename: 'app.js' + }, + module: { + rules: [ + { test: /\.coffee$/, loader: 'coffee-loader' }, + { test: /\.hamlc$/, loader: 'hamlc-loader' }, + { test: /\.json$/, loader: 'json-loader' }, + { test: /\.yml$/, loader: 'json-loader!yaml-loader' }, + ] + }, + resolve: { + extensions: ['.js', '.coffee', '.hamlc'], + alias: { + 'app': path.resolve(__dirname, 'app/') + } + }, + node: { + process: false + }, + plugins: plugins, + performance: { + hints: false + } +} diff --git a/script/_functions b/script/_functions index c5b29fd..978927f 100755 --- a/script/_functions +++ b/script/_functions @@ -1,23 +1,12 @@ set -e -function platform_installed() { - cordova platforms | head -n 1 | grep $1 >/dev/null -} - -function brew_dep_installed() { - brew list | grep "^$@$" >/dev/null 2>&1 -} - function command_available() { command -v "$@" >/dev/null 2>&1 } -function brew_available() { - command_available 'brew' -} - function warning() { - echo "$(tput setaf 3)$@$(tput sgr0)" + [[ $# -eq 0 ]] && local message=$(cat -) || local message="$@" + echo "$(tput setaf 3)$message$(tput sgr0)" } function npm_package_available() { diff --git a/script/build-app b/script/build-app index f0e28d5..9c292c2 100755 --- a/script/build-app +++ b/script/build-app @@ -4,8 +4,10 @@ export APP_ENV=$APP_ENV export PATH=$PATH:$(npm bin) export APP_PATH=$(pwd) -# force reinstall all cordova plugins, in case those are updated -cd cordova && cordova plugins ls | cut -d ' ' -f 1 | xargs cordova plugin rm +if [[ $* == *--release* ]]; then + # force reinstall all cordova plugins, in case those are updated + cd cordova && cordova plugins ls | cut -d ' ' -f 1 | xargs cordova plugin rm +fi echo "Building with environment: $APP_ENV" cd $APP_PATH && $(dirname ${BASH_SOURCE[0]})/cordova-exec build "$@" diff --git a/script/cordova-exec b/script/cordova-exec index c434f2f..a5b1ff5 100755 --- a/script/cordova-exec +++ b/script/cordova-exec @@ -8,7 +8,7 @@ source "$(dirname ${BASH_SOURCE[0]})/_functions" PLATFORM=$2 # build all assets -make dist +bin/maji build export PATH=$(npm bin):$PATH $(dirname ${BASH_SOURCE[0]})/prepare-cordova-platform "${@:2}" diff --git a/script/prepare-cordova-platform b/script/prepare-cordova-platform index d5fc2bd..5a374b1 100755 --- a/script/prepare-cordova-platform +++ b/script/prepare-cordova-platform @@ -3,25 +3,19 @@ source "$(dirname ${BASH_SOURCE[0]})/_functions" PLATFORM=$1 pushd cordova >/dev/null 2>&1 - platform_installed $PLATFORM || cordova platforms add $PLATFORM cordova prepare $PLATFORM - # install cordova plugins if [ -f plugins.txt ]; then - while read PLUGIN_LINE; do - [[ "$PLUGIN_LINE" == \#* ]] && continue + warning </dev/null 2>&1 || cordova plugins add $PLUGIN_FETCH_TARGET - done < plugins.txt +Refer to the Cordova documentation for details: +https://cordova.apache.org/docs/en/latest/platform_plugin_versioning_ref/ +EOF + exit 1 fi popd >/dev/null 2>&1 diff --git a/spec/cordova_support_spec.coffee b/spec/cordova_support_spec.coffee new file mode 100644 index 0000000..2bdcc28 --- /dev/null +++ b/spec/cordova_support_spec.coffee @@ -0,0 +1,14 @@ +window.cordova = { platformId: 'web' } +require('./spec_helper') +require('../src/cordova_support') + +Radio = require('backbone.radio') + +describe 'CordovaSupport', -> + ['deviceready', 'pause', 'resume', 'backbutton', 'offline', 'online'].forEach (event) -> + it "publishes '#{event}' on the app channel", (done) -> + Radio.channel('app').on 'deviceready', done + $(document).trigger('deviceready') + + it 'adds a class on document.body indicating cordova platformId', -> + expect($(document.body)).to.have.class('platform-web') diff --git a/spec/marionnette_extensions/animatable_region_spec.coffee b/spec/marionette_extensions/animatable_region_spec.coffee similarity index 85% rename from spec/marionnette_extensions/animatable_region_spec.coffee rename to spec/marionette_extensions/animatable_region_spec.coffee index 79f687d..5133c1b 100644 --- a/spec/marionnette_extensions/animatable_region_spec.coffee +++ b/spec/marionette_extensions/animatable_region_spec.coffee @@ -1,5 +1,5 @@ -AnimatableRegion = require '../../src/marionnette_extensions/animatable_region' -Page = require '../../src/marionnette_extensions/page' +AnimatableRegion = require '../../src/marionette_extensions/animatable_region' +Page = require '../../src/marionette_extensions/page' class TestPage extends Page render: -> diff --git a/src/cli.coffee b/src/cli.coffee index d6c03cf..a4031b0 100644 --- a/src/cli.coffee +++ b/src/cli.coffee @@ -1,47 +1,117 @@ spawn = require('child_process').spawn path = require('path') +maji_package = require('../package.json') + +parseBoolean = (value) -> + value == 'true' + +parsePort = (value) -> + parseInt(value) || null program = require('commander') -program.version('1.0.0') +program.version(maji_package.version) + +runNpm = (args, env_args = {}) -> + runCmd('npm', [args..., '--silent'], env_args) + +runCmd = (cmd, args, env_args = {}) -> + env = Object.create(process.env) + Object.assign(env, env_args) -executeScript = (scriptName, args) -> - child = spawn(path.resolve(__dirname + "/../script/#{scriptName}"), args) + child = spawn(cmd, args, { env: env, stdio: 'inherit' }) - child.stdout.on 'data', (data) -> + child.stdout?.on 'data', (data) -> process.stdout.write(data) - child.stderr.on 'data', (data) -> + child.stderr?.on 'data', (data) -> process.stderr.write(data.toString()) child.on 'exit', (exitCode) -> process.exit(exitCode) +runScript = (scriptName, args, env_args = {}) -> + runCmd(path.resolve(__dirname + "/../script/#{scriptName}"), args, env_args) + +literalArgs = -> + # commander.js program.args is broken for this purpose + # https://github.com/tj/commander.js/issues/582 + if program.rawArgs.indexOf('--') == -1 + [] + else + program.rawArgs.slice(program.rawArgs.indexOf('--') + 1) + +program + .command('new ') + .description('Create a new Maji app') + .on '--help', -> + console.log ' Example:\n maji new org.example.my-app ~/Code/my-app' + .action (packageName, path) -> + if ! packageName.match /.*\..*\..*/ + console.log 'Please specify a valid package name, for example org.example.my-app' + process.exit(1) + runScript('create-project', [packageName, path]) + program .command('run ') - .description('build and run a native app for the specified platform') + .description('Build and run a native app for the specified platform') .option('-e, --emulator', 'run on emulator instead of an actual device') + .option('--env --environment [environment]', 'APP_ENV to run with [development]') .action (platform, options) -> + app_env = options.environment || 'development' + env = { + 'APP_ENV': app_env + } + deviceTypeArg = if options.emulator then '--emulator' else '--device' - executeScript('run-on-device', [platform, deviceTypeArg]) + runScript('run-on-device', [platform, deviceTypeArg, literalArgs()...], env) program - .command('build ') - .description('build a native app for the specified platform') + .command('build [platform]') + .description('Build a native app for the specified platform') .option('--release', 'create a release build') + .option('--env --environment [environment]', 'APP_ENV to build with [production]') .action (platform, options) -> - releaseArg = if options.release then '--release' else '--debug' - executeScript('build-app', [platform, releaseArg]) + app_env = options.environment || 'production' + env = { + 'APP_ENV': app_env + } + + if platform + releaseArg = if options.release then '--release' else '--debug' + runScript('build-app', [platform, releaseArg, literalArgs()...], env) + else + runNpm(['run', 'build'], env) program - .command('new ') - .description('create a new Maji app') - .on '--help', -> - console.log ' Example:\n maji new org.example.my-app ~/Code/my-app' - .action (packageName, path) -> - if ! packageName.match /.*\..*\..*/ - console.log 'Please specify a valid package name, for example org.example.my-app' - process.exit(1) - executeScript('create-project', [packageName, path]) + .command('test') + .option('--watch', 'Run tests when project files change') + .option('--unit', 'Run unit tests') + .option('--integration', 'Run integration tests') + .description('Run your project tests') + .action (options) -> + if options.watch + return runNpm(['run', 'test:watch']) + + if options.unit + return runNpm(['run', 'test:unit']) + + if options.integration + return runNpm(['run', 'test:integration']) + + runNpm(['test']) + +program + .command('start') + .description('Run the maji dev server and compile changes on the fly') + .option('-p --port [port]', 'Port to listen on [9090]', parsePort, 9090) + .option('-l --livereload [flag]', 'Enable livereload [false]', parseBoolean, false) + .action (options) -> + env = { + 'SERVER_PORT': options.port, + 'LIVERELOAD': options.livereload + } + + runNpm(['start'], env) program.on '--help', -> process.exit(1) diff --git a/src/cordova_support.coffee b/src/cordova_support.coffee index 497b5d3..4d52b8f 100644 --- a/src/cordova_support.coffee +++ b/src/cordova_support.coffee @@ -1,16 +1,14 @@ -$ = require('jquery') -bus = require('./lib/bus') +$ = require('jquery') +Radio = require('backbone.radio') -publishOnBus = (e) -> - bus.trigger("app:#{e.type}") initCordova = -> $(document).on 'deviceready', -> require('./cordova/ios_network_activity').init() - for eventName in ['pause', 'resume', 'backbutton', 'offline', 'online'] + for eventName in ['deviceready', 'pause', 'resume', 'backbutton', 'offline', 'online'] $(document).on eventName, (e) -> - publishOnBus(e) + Radio.channel('app').trigger(e.type) $ -> $('body').addClass("platform-#{cordova.platformId}") diff --git a/src/index.coffee b/src/index.coffee index af0a21f..ad2471f 100644 --- a/src/index.coffee +++ b/src/index.coffee @@ -4,13 +4,12 @@ module.exports = ServiceError : require('./errors/service_error') ServiceUnavailableError : require('./errors/service_unavailable_error') - bus : require('./lib/bus') I18n : require('./lib/i18n') TemplateHelpers : require('./lib/template_helpers') - Application : require('./marionnette_extensions/application') - AnimatableRegion : require('./marionnette_extensions/animatable_region') - Page : require('./marionnette_extensions/page') + Application : require('./marionette_extensions/application') + AnimatableRegion : require('./marionette_extensions/animatable_region') + Page : require('./marionette_extensions/page') ApplicationState : require('./storage/application_state') Cache : require('./storage/cache') diff --git a/src/lib/bus.coffee b/src/lib/bus.coffee deleted file mode 100644 index 463b743..0000000 --- a/src/lib/bus.coffee +++ /dev/null @@ -1,13 +0,0 @@ -Wreqr = require('backbone.wreqr') - -commands = new Wreqr.Commands() -vent = new Wreqr.EventAggregator() -reqres = new Wreqr.RequestResponse() - -module.exports = - commands : commands - vent : vent - reqres : reqres - execute : -> commands.execute.apply(commands, arguments) - request : -> reqres.request.apply(reqres, arguments) - trigger : -> vent.trigger.apply(vent, arguments) diff --git a/src/maji-dev-server.js b/src/maji-dev-server.js index 1d5cb5b..1b23880 100755 --- a/src/maji-dev-server.js +++ b/src/maji-dev-server.js @@ -17,10 +17,12 @@ program .usage('[options] ') .option('-p, --port [port]', 'Port to listen on [9090]', parsePort, 9090) .option('-l, --livereload [flag]', 'Enable livereload [true]', parseBoolean, true) + .option('-c, --cors', 'Enable cors') .parse(process.argv); var port = program.port; var livereload = program.livereload; +var cors = program.cors; var assetPath = program.args[0]; if (! assetPath) { @@ -53,6 +55,14 @@ if(livereload) { })); } +if(cors) { + server.use(function(req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', req.get('Access-Control-Request-Headers') || '*'); + next(); + }); +} + server .use(express.static(assetPath, { etag: false, diff --git a/src/marionnette_extensions/animatable_region.coffee b/src/marionette_extensions/animatable_region.coffee similarity index 89% rename from src/marionnette_extensions/animatable_region.coffee rename to src/marionette_extensions/animatable_region.coffee index 552b497..ff00104 100644 --- a/src/marionnette_extensions/animatable_region.coffee +++ b/src/marionette_extensions/animatable_region.coffee @@ -10,6 +10,7 @@ class AnimatableRegion extends Marionette.Region @showTransitions = opts.showTransitions @navigationStack = new NavigationStack() + @transitioning = false super @@ -35,6 +36,12 @@ class AnimatableRegion extends Marionette.Region window.location.hash = route + _empty: (view, shouldDestroy) -> + # we manage destroying views ourselves, if we're transitioning + # In this case we need to prevent Marionette from destroying the currentView + # before we've transitioned to the new view + super unless @transitioning + show: (view, options = {}) -> view._parent = this return super(view, options) unless @showTransitions @@ -42,10 +49,10 @@ class AnimatableRegion extends Marionette.Region # determine what transition to use, @navigationOptions takes precedence @transition = @navigationOptions?.transition @transition ?= view.transition || options.transition + @transitioning = true if @transition @currentPage = @currentView - options.preventDestroy = true super(view, options) @@ -94,13 +101,13 @@ class AnimatableRegion extends Marionette.Region newPage.$el.addClass('page-pre-in') newPage.$el.css('z-index', 10) - this.$el.append(newPage.$el) + @$el.append(newPage.$el) @_log 'Using transition', @transition, 'and back =', @back if @currentPage @currentPage.trigger 'transitionstart' - this.$el.addClass("viewport-transitioning viewport-#{@transition}") + @$el.addClass("viewport-transitioning viewport-#{@transition}") setTimeout((=> window.scrollTo(0,0) @@ -124,9 +131,10 @@ class AnimatableRegion extends Marionette.Region @currentPage = null @back = undefined + @transitioning = false - this.$el.removeClass('viewport-transitioning') - this.$el.removeClass("viewport-#{@transition}") + @$el.removeClass('viewport-transitioning') + @$el.removeClass("viewport-#{@transition}") newPage.trigger 'transitioned' ), @_transitionDuration(@transition)) @@ -134,6 +142,7 @@ class AnimatableRegion extends Marionette.Region else newPage.$el.removeClass('page-pre-in') @back = undefined + @transitioning = false newPage.trigger 'transitioned' _isNavigatingBack: (fragment) -> diff --git a/src/marionnette_extensions/application.coffee b/src/marionette_extensions/application.coffee similarity index 59% rename from src/marionnette_extensions/application.coffee rename to src/marionette_extensions/application.coffee index 055dc13..daf2eb6 100644 --- a/src/marionnette_extensions/application.coffee +++ b/src/marionette_extensions/application.coffee @@ -1,23 +1,17 @@ _ = require('underscore') Marionette = require('backbone.marionette') AnimatableRegion = require('./animatable_region') -bus = require('../lib/bus') class Application extends Marionette.Application - constructor: (opts = {}) -> - super - - _.defaults opts, - showTransitions: true + region: '#maji-app' + regionClass: AnimatableRegion + initialize: (opts = {}) -> require('./marionette_renderer').setup() require('../cordova_support') - this.bus = bus - @addRegions - mainRegion: new AnimatableRegion( - el: '#maji-app', - showTransitions: opts.showTransitions - ) + _.defaults opts, + showTransitions: true + @getRegion().showTransitions = opts.showTransitions module.exports = Application diff --git a/src/marionnette_extensions/marionette_renderer.coffee b/src/marionette_extensions/marionette_renderer.coffee similarity index 100% rename from src/marionnette_extensions/marionette_renderer.coffee rename to src/marionette_extensions/marionette_renderer.coffee diff --git a/src/marionnette_extensions/page.coffee b/src/marionette_extensions/page.coffee similarity index 88% rename from src/marionnette_extensions/page.coffee rename to src/marionette_extensions/page.coffee index cd5d2b1..143ebd3 100644 --- a/src/marionnette_extensions/page.coffee +++ b/src/marionette_extensions/page.coffee @@ -1,6 +1,6 @@ Marionette = require('backbone.marionette') -class Page extends Marionette.LayoutView +class Page extends Marionette.View transition: 'slide' className: 'page' diff --git a/src/marionette_renderer.coffee b/src/marionette_renderer.coffee index ce4aa7f..4c01f4b 100644 --- a/src/marionette_renderer.coffee +++ b/src/marionette_renderer.coffee @@ -1,2 +1,2 @@ # for backwards compatibility -module.exports = require('./marionnette_extensions/marionette_renderer') +module.exports = require('./marionette_extensions/marionette_renderer')