***TIP Instead of Grunt, you might want to check out Gulp! It's at least as powerful and a lot easier. Checkout our series Gulp! Refreshment for Your Frontend Assets.
A few weeks ago, Leanna and I were one of the lucky 600+ that attended SymfonyCon in Warsaw - one of the best conferences we've been to! We hung out with some of our best tech friends, watched Leanna win tech Jeopardy, and had the pleasure to meet a lot of new friends!
I also gave a Christmas-themed talk on a really neat subject: "[Cool like a Frontend] Developer(http://www.slideshare.net/weaverryan/cool-like-frontend-developer-grunt-requirejs-bower-and-other-tools-29177248)", renamed to "Deck the Halls with Grunt, RequireJS & Bower". And because examples are best, an example project from the presentation lives on GitHub: knpuniversity/symfonycon-frontend
If you're curious about this stuff and couldn't be there for my talk, go read those slides. Then come back. We have a new piece to talk about.
Like most new tech, what makes this stuff tricky is the lack of real projects and best practices when you're learning it. That was the point of my talk: to give you something real to build off of.
But I also knew that my solution wouldn't end up being the best. In fact, a tip from Gediminas (of DoctrineExtensions fame) and some others have already led me to one big change.
In my talk, I propose having a web/assets/
directory where you put all
of your JS, CSS/SASS, fonts, etc. When you run Grunt (which runs the RequireJS
optimizer), this is copied to web/assets-built
, and then some changes
are made to it. In the end, the only change we need to make to our Symfony
project is to point all of our assets to /web/assets-built
instead of
/web/assets
when we're in the prod
environment.
But a better solution may be to put the assets
directory at the root
of your project. This has a few advantages:
#. Any source files (like SASS files) aren't exposed to the web;
#. You no longer need to worry about changing between pointing to /assets
and /assets-built
in your Symfony project.
The second point is very nice. By the end of my presentation, I defined two important Grunt tasks:
grunt
- operates onweb/assets
and does some basic things like SASS compilation';grunt production
- copiesweb/assets
toweb/assets-built
and then does several things to it.
With this new setup, we would change this slightly:
grunt
- copiesassets/
toweb/assets
and does some basic things like SASS compilation';grunt production
- copiesassets
toweb/assets
and then does several things to it.
The difference is that - whether we're developing or deploying - our assets
always live in web/assets
. This means that you don't need any logic
in your Symfony application to change paths from /assets/
to /assets-built
.
Developing? Just use grunt
(or, more usefully grunt watch
). Want
to use the assets as they'll be built for production? Just run grunt production
.
If you want to try this, let's talk about the exact changes we need. It's a simple 3-step process. If you want to skip and see the end result, check out the assets-in-root branch on GitHub.
-
First, the easy part: move
web/assets
toassets
. Awesome. -
Next, update your Twig templates to simply point at the
assets
directory, replacing the "smarter" variable used before.
Before:
<script src="{{ asset(assetsPath~'/vendor/requirejs/require.js') }}"></script>
<script>
requirejs.config({
baseUrl: '{{ asset(assetsPath~'/js') }}'
});
// ...
</script>
After:
<script src="{{ asset('assets/vendor/requirejs/require.js') }}"></script>
<script>
requirejs.config({
baseUrl: '{{ asset('assets/js') }}'
});
// ...
</script>
- Modify the
Gruntfile.js
to copyassets/
toweb/assets
and then operate entirely on theweb/assets
directory.
Ok, this part isn't so simple. First, you'll need a new Grunt plugin:
grunt-contrib-copy by adding it to package.json
, and importing its
tasks in Gruntfile.js
:
// Gruntfile.js
// ...
module.exports = function (grunt) {
// ...
grunt.loadNpmTasks('grunt-contrib-copy');
// ...
};
With some configuration, this will copy one directory (e.g. assets
) to
another directory (web/assets
). We've been relying on RequireJS to do
this until now, but I now want something that will copy these files, even
if I'm not using the RequireJS optimizer:
// Gruntfile.js
// ...
copy: {
main: {
files: [
{
expand: true,
src: ['assets/**'], dest: 'web'}
]
}
},
// ...
With this, we now have a new grunt copy
command, which will copy assets/
to web/assets
. That's not very useful on its own, but we can now point
all the other tasks in Gruntfile.js
to operate on the web/assets
directory,
including Compass, JSHint and RequireJS.
We also have two "watch" sub-commands that guarantee that JSHint is run whenever
JavaScript files change and Compass whenever .scss
files change. We'll
continue to have the watch sub-task look for file changes in the assets/
directory at the root of our project, since that's where we edit files. But
before running jshint
or compass
, each will call copy
first, to
copy things into web/assets
:
// Gruntfile.js
// ...
watch: {
scripts: {
files: ['assets/js/**'],
tasks: ['copy', 'jshint']
},
// watch all .scss files and run compass
compass: {
files: 'assets/sass/*.scss',
tasks: ['copy', 'compass:dev'],
options: {
spawn: false
}
}
}
The setup probably still has a few imperfections, but to see it all put together,
see the grunt-contrib-copy branch on GitHub. This setup adds a small amount
of complexity, since you must copy files every time any change is made, even
while developing. But since this is all handled in Grunt and grunt watch
,
we only feel that complexity when we're first getting things configured.
I've also been talking with a Matt Davis, we brought up some more potential improvements/problems:
-
The SASS files no longer live in
web/
, but are still copied toweb/
when Grunt runs. If you really want to hide these files, you'll need to omit them from thecopy
task, or remove them afterwards. -
If you delete a file from
assets/
, it will still live inweb/assets/
, because thecopy
task copies new files, but nothing ever removes the old files.
The answer to both of these is the grunt-contrib-clean plugin.
***TIP The solution to this has been even further evolved to never copy the sass files at all. Just check out the assets-in-root branch on GitHub or pull request #7 for more details. Thanks to Daniel Paschke for the tips.
First, install it like any Grunt plugins:
$ npm install grunt-contrib-clean --save-dev
Then activate its tasks in Gruntfile.js
:
// Gruntfile.js
module.exports = function (grunt) {
// ...
grunt.loadNpmTasks('grunt-contrib-clean');
// ...
};
We'll create 2 subtasks: one for cleaning out web/assets
before copying
and another for cleaning out the web/assets/sass
directory after copying:
// Gruntfile.js
// ...
grunt.initConfig({
clean: {
build: {
src: ['<%= targetDir %>/**']
},
sass: {
src: ['<%= targetDir %>/sass']
}
},
});
// ...
// sub-task that copies assets to web/assets, and also cleans some things
grunt.registerTask('copy:assets', ['clean:build', 'copy', 'clean:sass']);
// the "default" task (e.g. simply "Grunt") runs tasks for development
grunt.registerTask('default', ['copy:assets', 'jshint', 'compass:dev']);
// register a "production" task that sets everything up before deployment
grunt.registerTask('production', ['copy:assets', 'jshint', 'requirejs', 'uglify', 'compass:dist']);
We've also created a new convenience task: copy:assets
, which cleans
web/assets
, copies assets/
to web/assets/
, then removes web/assets/sass
.
Phew! Just make sure that this new copy:assets
is the first step
in our default
and production
tasks. Now, when we run grunt
or
grunt production
, all the copying and cleaning will happen first.
This was the first big change that I've come across, but if you see other improvements, I'd love to hear them!
Have fun!