Nette with grunt 24/04/2015

Why this friendship? Because everyone can have only one javascript and one stylesheet file. In era of mobile internet, when every request is expensive, we don’t want header like this (in better example) What we want is that one minified javascript file and one minified file with stylesheets. I have got the best experience with Grunt and his great garden of plugins. I will show you simple example how to automate this task.

Why this friendship?

Because everyone can have only one javascript and one stylesheet file. In era of mobile internet, when every request is expensive, we don’t want header like this (in better example)

What we want is that one minified javascript file and one minified file with stylesheets. I have got the best experience with Grunt and his great garden of plugins. I will show you simple example how to automate this task.

What we need

Clean installation of Nette sandbox https://github.com/nette/sandbox and working Grunt http://gruntjs.com.

Grunt is JavaScript task runner. It’s similar to Make or Ant but written in JavaScript. Grunt has a lot of interesting plugins for better fronted development, but not only for frontend. Look at gruntjs.com/plugins.

How to install grunt-cli:

Grunt requires node.js in the latest version and a package manager npm http://nodejs.org.

When we already have npm with node.js installed, we can install grunt-cli to global space. npm install -g grunt-cli

If everything works, after you run command grunt --version you should see something like this.

Let’s prepare working version of Nette Sandbox in ProjectGrunt folder. The best way how to do this, is by using composer.

composer create-project nette/sandbox ProjectGrunt

Check folder content.

After you configured web server and adjusted permission for log and temp folders (chmod -R a+rw temp log) sandbox clone should work. If it doesn’t work, you should follow Quick start guide.

Configuration

Create file package.json in root folder. It contains list of node.js modules what we need to install (using npm).

{
  "name": "ProjectGrunt",
  "version": "1.0.0",
  "devDependencies": {
    "grunt": "~0.4.4",
    "grunt-usemin": "~2.1.0",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-uglify": "~0.4.0",
    "grunt-contrib-cssmin": "~0.9.0",
    "grunt-nette-basepath": "~0.2.0"
  }
}

grunt - use latest version of Grunt

grunt-usemin - plugin for analysing our templates and prepare blocks with files for the next processes

grunt-contrib-concat - plugin for concatenation files

grunt-contrib-uglify - plugin for javascript minification

grunt-contrib-cssmin - plugin for css minification

grunt-nette-basepath - plugin for removing latte variable {$basePath} from file paths

Install these dependencies npm install

Create file Gruntfile.coffee in project root. You can use Gruntfile.js but I prefer Coffee for its expressiveness.

module.exports = (grunt) ->
  grunt.initConfig
    useminPrepare:
      html: ['app/templates/@layout.latte']
      options:
        dest: '.'

    netteBasePath:
      basePath: 'www'
      options:
        removeFromPath: ['app/templates/']

  # These plugins provide necessary tasks.
  grunt.loadNpmTasks 'grunt-contrib-concat'
  grunt.loadNpmTasks 'grunt-contrib-uglify'
  grunt.loadNpmTasks 'grunt-contrib-cssmin'
  grunt.loadNpmTasks 'grunt-usemin'
  grunt.loadNpmTasks 'grunt-nette-basepath'

  # Default task.
  grunt.registerTask 'default', [
    'useminPrepare'
    'netteBasePath'
    'concat'
    'uglify'
    'cssmin'
  ]

This includes definition of all tasks that will be loaded. And then it will be specified defaul main task which is run after command grunt and configuration for tasks useminPrepare and netteBasePath. For more information how plugin Usemin works visit https://github.com/yeoman/grunt-usemin.

In our case, we check config of netteBasePath task, where we said that basePath folder is in folder www, because Usemin plugin add to path folder, where file is located. We need to remove this path app/templates.

if you are running grunt under windows, this setting must be in windows style

removeFromPath: ['app\\templates\\']

(I know this bug, and I will fix it later)

Run minification

Now we can run grunt , but we got this error, because we don’t have blocks for usemin task in our template

So we add these block to our template. In @layout.latte file we find classic javascript and css including and wrap it to build blocks.

<!-- build:css {$basePath}/css/screen.min.css -->
<link rel="stylesheet" media="screen,projection,tv" href="{$basePath}/css/screen.css">
<!-- endbuild -->
<!-- build:css {$basePath}/css/print.min.css -->
<link rel="stylesheet" media="print" href="{$basePath}/css/print.css">
<!-- endbuild -->

and

<!-- build:js {$basePath}/js/app.min.js -->
<script src="{$basePath}/js/jquery.js"></script>
<script src="{$basePath}/js/netteForms.js"></script>
<script src="{$basePath}/js/main.js"></script>
<!-- endbuild -->

and now grunt looks for block definitions and prepares files for next tasks.

Great! Now we have minified files in folders, what is next?

Version switching in Latte

For devel version we want uncompressed files, but for production we want use minified version.

How can we do this? I use configuration in neon.config where I define what version want to use and second config for version name to solve cache problems

config.neon

#
# SECURITY WARNING: it is CRITICAL that this file & directory are NOT accessible directly via a web browser!
#
# If you don't protect this directory from direct web access, anybody will be able to see your passwords.
# http://nette.org/security-warning
#
parameters:
	site: # <---
		develMode: false # <---
		version: blackhawk # <---

php:
	date.timezone: Europe/Prague
	# zlib.output_compression: yes


nette:
	application:
		errorPresenter: Error
		mapping:
			*: App\*Module\Presenters\*Presenter

	session:
		expiration: 14 days


services:
	- App\Model\UserManager
	- App\RouterFactory
	router: @App\RouterFactory::createRouter

Update in BasePresenter so that templates know about these variables. Maybe it’s not the best solution, but good enough for this example.

BasePresenter.php

/**
 * Base presenter for all application presenters.
 */
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
	public function beforeRender()
	{
		parent::beforeRender();

		$this->template->production = !$this->context->parameters['site']['develMode'];
		$this->template->version = $this->context->parameters['site']['version'];
	}
}

and update @layout.latte

{if $production}
	<link rel="stylesheet" media="print" href="{$basePath}/css/screen.min.css?{$version}">
	<link rel="stylesheet" media="print" href="{$basePath}/css/print.min.css?{$version}">
{else}
	<!-- build:css {$basePath}/css/screen.min.css -->
	<link rel="stylesheet" media="screen,projection,tv" href="{$basePath}/css/screen.css">
	<!-- endbuild -->
	<!-- build:css {$basePath}/css/print.min.css -->
	<link rel="stylesheet" media="print" href="{$basePath}/css/print.css">
	<!-- endbuild -->
{/if}

and

{if $production}
	<script src="{$basePath}/js/app.min.js?{$version}"></script>
{else}
	<!-- build:js {$basePath}/js/app.min.js -->
	<script src="{$basePath}/js/jquery.js"></script>
	<script src="{$basePath}/js/netteForms.js"></script>
	<script src="{$basePath}/js/main.js"></script>
	<!-- endbuild -->
{/if}

and now our application uses minified version

You can find whole example on github https://github.com/chemix/Nette-Grunt