Grunt で複数の CoffeeScript を一つの無名関数内にまとめる


Coffee をクラス毎にファイルを分けて開発したくて調べてみたのでメモ。

### 準備

プロジェクト用のディレクトリを作成して grunt とプラグインをインストール。

```shell-session
% sudo apt-get install -y nodejs-legacy npm
% sudo npm install -g grunt-cli
% mkdir -p project/{src{/coffee,/js},dest}
% cd project
% npm init
% npm install grunt grunt-contrib-coffee grunt-contrib-concat --save-dev
```

これで以下のようなディレクトリ構造になる。

```shell-session
project
 ├── dest
 ├── lib
 ├── node_modules
 ├── package.json
 └── src
     ├── coffee
     └── js
```


### スクリプトの作成

以下のような感じでスクリプトを作成。

```coffee:project/src/coffee/foo.coffee
class Foo
  name: ->
    'Foo'
```

```coffee:project/src/coffee/bar.coffee
class Bar
  name: ->
    'Bar'
```

既存の js なども組み込む事を想定して js ファイルも用意してみた。

```javascript:project/src/js/baz.js
var Baz = (function() {
  function Baz() {}
  Baz.prototype.name = function() {
    return 'Baz';
  };
  return Baz;
})();
```

呼び出し元となるメインのスクリプトは以下のような感じ。

```coffee:project/src/coffee/main.coffee
foo = new Foo()
bar = new Bar()
baz = new Baz()
console.log(foo.name())
console.log(bar.name())
console.log(baz.name())
```

### 普通にビルドする

まずは以下の Gruntfile.js で普通にビルドしてみる

```javascript:project/Gruntfile.js
module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        coffee: {
            compile: {
                files: [{
                    expand: true,
                    cwd: 'src/coffee',
                    src: ['*.coffee'],
                    dest: 'src/js/',
                    ext: '.js',
                }]
            }
        },
        concat: {
            all: {
                src : [
                    'src/js/*.js',
                ],
                dest: 'dest/all.js',
            },
        },
    });
    grunt.loadNpmTasks('grunt-contrib-coffee');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.registerTask('default', ['coffee', 'concat']);
};
```

grunt 実行。

```shell-session
% grunt
Running "coffee:compile" (coffee) task
>> 3 files created.

Running "concat:all" (concat) task
File dest/all.js created.

Done, without errors.
```

以下のような感じでファイルが出力されている。

```shell-session
% ls src/js 
bar.js
baz.js
foo.js
main.js
% ls dest
all.js
```

生成されたスクリプトが動作するか試してみるがエラー。

```shell-session
% node dest/all.js

/home/akishin/src/nodejs/project/dest/all.js:44
  foo = new Foo();
            ^
ReferenceError: Foo is not defined
    at Object.<anonymous> (/home/akishin/src/nodejs/project/dest/all.js:44:13)
    at Object.<anonymous> (/home/akishin/src/nodejs/project/dest/all.js:56:4)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:902:3
```

これは CoffeeScript のコンパイル結果が以下のようにファイル毎に無名関数でラップされてしまうことによるもの。

```javascript:project/dest/all.js
(function() {
  var Bar;

  Bar = (function() {
    function Bar() {}

    Bar.prototype.name = function() {
      return 'Bar';
    };

    return Bar;

  })();

}).call(this);

var Baz = (function() {
  function Baz() {}
  Baz.prototype.name = function() {
    return 'Baz';
  };
  return Baz;
})();

(function() {
  var Foo;

  Foo = (function() {
    function Foo() {}

    Foo.prototype.name = function() {
      return 'Foo';
    };

    return Foo;

  })();

}).call(this);

(function() {
  var bar, baz, foo;

  foo = new Foo();

  bar = new Bar();

  baz = new Baz();

  console.log(foo.name());

  console.log(bar.name());

  console.log(baz.name());

}).call(this);
```

### 回避策

これを回避するためには coffee タスクにオプション `bare = true` を指定してやればいい。

が、そうすると今度は全てグローバルになってしまう。

これに対処するため、以下のような内容の intro.js, outro.js を用意する。

無名関数にもいろいろ書き方があるようだが、ここでは coffee タスクでコンパイルした結果の js と同じ内容にした。

```javascript:project/src/js/intro.js
(function() {
```

```javascript:project/src/js/outro.js
}).call(this);
```

わざわざファイルを用意したくなければ concat の banner オプションと footer オプションを使って指定しても上手く動作する。

Gruntfile.js を以下のように修正。
修正したのは以下の点。

- coffee のオプションに `bare = true` を指定
- concat 時に intro.js と outro.js でその他のスクリプトを囲む

```javascript:project/Gruntfile.js
module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        coffee: {
            compile: {
                options: {
                    bare: true,
                },
                files: [{
                    expand: true,
                    cwd: 'src/coffee',
                    src: ['*.coffee'],
                    dest: 'src/js/',
                    ext: '.js',
                }]
            }
        },
        concat: {
            all: {
                src : [
                    'src/js/intro.js',
                    'src/js/*.js',
                    'src/js/outro.js',
                ],
                dest: 'dest/all.js',
            },
        },
    });
    grunt.loadNpmTasks('grunt-contrib-coffee');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.registerTask('default', ['coffee', 'concat']);
};
```
再度 grunt を実行し、生成された all.js を node コマンドで実行してみる。

```shell-session
% grunt
% node dest/all.js
Foo
Bar
Baz
```

今度は上手く動作した。
html で読み込んでデベロッパーツールで確認しても問題無し。

生成された all.js は以下のようになっている。

```javascript:project/dest/all.js
(function() {

var Bar;

Bar = (function() {
  function Bar() {}

  Bar.prototype.name = function() {
    return 'Bar';
  };

  return Bar;

})();

var Baz = (function() {
  function Baz() {}
  Baz.prototype.name = function() {
    return 'Baz';
  };
  return Baz;
})();

var Foo;

Foo = (function() {
  function Foo() {}

  Foo.prototype.name = function() {
    return 'Foo';
  };

  return Foo;

})();

var bar, baz, foo;

foo = new Foo();

bar = new Bar();

baz = new Baz();

console.log(foo.name());

console.log(bar.name());

console.log(baz.name());

}).call(this);
```

これでやりたい事は出来た。

でもこの方法だと一つのスコープに全部押し込めてるのでスクリプト量が多い場合破綻するかも。

Michael Rambeau ? Working with CoffeeScript and Grunt
http://michaelrambeau.com/posts/2013-12-working-with-coffeescript/

この記事の「Default private scope」の「Solution 2: using namespaces」のように namespace を作るのがより正しい対処法なのかも知れない。

もしくは規模がある程度大きくなってきたら browserify とか grunt-sprockets なんかを使うのがいいのかな。

### 参考

grunt-cliでgrunt-contrib-concatを使用して、全体を無名関数で囲ったファイルを出力する。 | niwaringo() {Tumblr}
http://niwaringo.tumblr.com/post/42918032996/grunt-cli-grunt-contrib-concat

[Grunt] JSファイルを結合するときに無名関数として出力する | un-T factory! Interactive blog
http://www.un-t.com/interactive/?p=437

Gruntでbrowserify使ってCoffeescriptをコンパイルする。 ::: Toro_Unit
http://www.torounit.com/blog/2014/08/05/1782/

grunt-sprocketsタスクプラグイン作成とテストとTravis CIまでやってみる | ブログ :: Web notes.log
http://blog.wnotes.net/blog/article/create-grunt-task-plugin-and-testing-travis-ci