ビットマスクによるフラグ
参考:goog.ui.Control
複数の状態(STATE)を、1つの変数で保持する。例えば、DISABLED状態であり、同時にHOVER状態でもある状態を1つの変数で表現する。以下、そのときのフラグの上げ下げのしかた。
まずビットマスクに使う定数を用意する。1,2,4,8,16,32...とする。
/** * @enum {number} */ var STATE = { DISABLED: 1, HOVER: 2, ACTIVE: 4, FOCUSED: 8 }; /** * @type {number} */ var state = 0;
フラグが上がっているかの確認。if文で使うときは、 if (state & STATE.DISABLED) で判定できる。まだ、どのフラグも上がってない。
console.log(!!(state & STATE.DISABLED)); // false console.log(!!(state & STATE.HOVER)); // false console.log(!!(state & STATE.ACTIVE)); // false console.log(!!(state & STATE.FOCUSED)); // false
それでは、2つのフラグを上げてみる。STATE.DISABLEDとSTATE.HOVERだけ。2つのやり方があり、同じ意味。
state |= STATE.DISABLED; state = state | STATE.HOVER; console.log(!!(state & STATE.DISABLED)); // true console.log(!!(state & STATE.HOVER)); // true console.log(!!(state & STATE.ACTIVE)); // false console.log(!!(state & STATE.FOCUSED)); // false
他のフラグには影響を与えていないのが分かる。
次に、挙げたフラグを、下げてみる。これにも2つやり方があり、同じ意味。
state &= ~STATE.DISABLED; state = state & ~STATE.HOVER; console.log(!!(state & STATE.DISABLED)); // false console.log(!!(state & STATE.HOVER)); // false console.log(!!(state & STATE.ACTIVE)); // false console.log(!!(state & STATE.FOCUSED)); // false
続いて、いっぺんに複数のフラグを上げてみる。
state = STATE.DISABLED | STATE.HOVER | STATE.ACTIVE; console.log(!!(state & STATE.DISABLED)); // true console.log(!!(state & STATE.HOVER)); // true console.log(!!(state & STATE.ACTIVE)); // true console.log(!!(state & STATE.FOCUSED)); // false
上記は、初期値の時点でいくつかのフラグを上げておきたいときなどに使える。代入文であることからも分かるように、元のフラグを無視して上書きする。
全てのフラグを下げるには、0を代入する。
state = 0; console.log(!!(state & STATE.DISABLED)); // false console.log(!!(state & STATE.HOVER)); // false console.log(!!(state & STATE.ACTIVE)); // false console.log(!!(state & STATE.FOCUSED)); // false
CentOS6、nginx -> node 最小構成
僕はセキュリティとかに詳しくない人間です。実運用は十分ご注意ください。
nginx インストール
$ sudo vim /etc/yum.repos.d/nginx.repo
# /etc/yum.repos.d/nginx.repo [nginx] name=nginx repo baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck=0 enabled=1
$ sudo yum install nginx $ sudo nginx # :80 でlistenできてるか、ブラウザで確認する。「Welcome to nginx!」ならOK $ sudo nginx -s quit
nodeでサーバを起動
$ cd $ vim ./test_server.js
// ./test_server.js var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('YEAH!!!!!!\n'); }).listen(3000, '127.0.0.1'); console.log('Server running at http://127.0.0.1:1337/');
$ npm install forever -g # foreverお気に入りです 何度も生き返してくれました $ forever start test_server.js # nodeサーバが127.0.0.1:3000で起動
nginx 設定ファイル編集
$ sudo vim /etc/nginx/conf.d/mynginx.conf # 新規ファイル
# /etc/nginx/conf.d/mynginx.conf server { listen 80; server_name localhost; location / { proxy_pass http://localhost:3000; # :3000 に投げ渡す break; } }
nginxサーバ起動
$ sudo service nginx start # nginx サーバ起動
ブラウザで、:80 にアクセス。nodeサーバの返す「YEAH!!!!!」が表示されました。
設定、もっと
- nginx の設定回り。もっとかっこいい設定にして、ちゃんとしたリバースプロキシサーバにする。
yesod-test-0.2.0.2 でビルドエラー
今直し中だそうです(僕はHaskell1ミリも読めない)
Mac OS 10.7.3
yesod-core version:1.0.0.2
$ cabal-dev install && yesod --dev devel
でlocalhost:3000 が立ち上がるはずが、だめ。
最後のほうのログ:
cabal: Error: some packages failed to install: yeah-0.0.0 depends on yesod-test-0.2.0.2 which failed to install. yesod-test-0.2.0.2 failed during the building phase. The exception was: ExitFailure 1
そしたら
.. Thanks for the heads up, I'm in the process of fixing it right now.
http://groups.google.com/group/yesodweb/browse_thread/thread/ffbc9ea24d002c05/7b12f9532ced65d2?show_docid=7b12f9532ced65d2
Actually, I'd recommend waiting until I post the new yesod-platform, ...
expressでOAuth。Twitterに投稿。
なんだかよく分からなかった。やっとできた。基本的なことが分かってないんだね。
追)githubにサンプルを作りました https://github.com/piglovesyou/express-twitter-oauth-sample
やること:
- 下準備。ライブラリの準備。
- app.get('/', routes.index);。ログインしてなかったら、/login にリダイレクト。
- app.get('/login', routes.login); ログイン画面。ユーザーに「Twitterでログイン」をクリックしてもらう。
- app.get('/auth/twitter', routes.auth.twitter); request_token を取得。取得したら、authenticate を叩く。
- app.get('/auth/twitter/callback', routes.auth.twitter.callback); verifierを取得。取得したら、access_tokenを取得。
- 準備完了。ユーザーのscreen_name を出して、ログイン中画面にユーザーをお招き。
- app.post('/post', routes.post); アプリ経由でtweet を投稿してみる。
- app.get('/logout', routes.logout); ログアウト。セッションを破壊。大爆発。
1. 下準備。ライブラリの準備。
https://dev.twitter.com/appsで、アプリを登録。今回はtweetしたいので、read and write に設定しておく。
expressは、sessionを使うオプションでプロジェクトを作成。
npm install oauth しておいて、./routes/index.js で読みこむ。 ./routes/index.js の先頭で、oauth の設定をする(インスタンス作り)。
// ./routes/index.js var OAuth = require('oauth').OAuth; var oa = new OAuth( "https://twitter.com/oauth/request_token", "https://twitter.com/oauth/access_token", "Your consumer key is here.", "Your consumer secret is here.", "1.0", "http://localhost.com:3000/auth/twitter/callback", "HMAC-SHA1");
2. app.get('/', routes.index);
このページは、ログインユーザーしか見れなくする。
ログインしてる人のために、テンプレート「index」が必要。
ログインしてない人は、ただちに /login に飛んでもらう。
exports.index = function (req, res) { if(req.session.oauth && req.session.oauth.access_token) { res.render('index', { screen_name: req.session.twitter.screen_name }); } else { res.redirect("/login"); } };
3. app.get('/login', routes.login);
ログインしてない人が、「ログイン」リンクをクリックしてもらうためのページ。無くてもいい。ここに「Twitterでログイン」「Facebookでログイン」の2つをあとで置こうかなと思ってます。今は、Twitterだけ。
テンプレート「login」が要ります。
exports.login = function (req, res) { if(req.session.oauth && req.session.oauth.access_token) { } else { res.render('login'); } };
4. app.get('/auth/twitter', routes.auth.twitter);
認証のためのURL。テンプレートはなし。アクセスと同時に、oauthインスタンスにrequest_tokenを取りに行かせる。
request_tokenをtwitterにもらったら、ユーザーにそれを握らせて、アプリの認証画面に行ってもらう。あと、貰ったほかのものはsessionにしまっておく。ユーザーが「許可」してくれることを祈って待つ。
exports.auth = {}; exports.auth.twitter = function(req, res){ oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){ if (error) { res.send("yeah no. didn't work.") } else { req.session.oauth = {}; req.session.oauth.token = oauth_token; req.session.oauth.token_secret = oauth_token_secret; res.redirect('https://twitter.com/oauth/authenticate?oauth_token='+oauth_token) } }); };
5. app.get('/auth/twitter/callback', routes.auth.twitter.callback);
OAuthをnew したところと同じcallback用のURL。これはTwitterのアプリ設定画面でも同じものを登録しとく必要がある。
ユーザーが「許可します」ボタンを押してくれたら、Twitterはユーザーをここに連れてくる。パラメータには、ユーザーからのOKサインである「verifier」が入っているので、ありがたく貰っておく。
verifierと、さっきsessionにしまったものをきっちり揃えて、Twitterにお願いしに行く。書類をそろえて、「このユーザーと懇意にさせてください」とお願いする。書類に不備がなければ、OKのハンコがもらえる。OKハンコは「access_token」と「access_token_secret」の2つ。これがずっと欲しかったもの。目的を達成したから、それをsessionに大事にしまって、ユーザーをログイン中画面に連れていく。
exports.auth.twitter.callback = function(req, res, next){ if (req.session.oauth) { req.session.oauth.verifier = req.query.oauth_verifier; var oauth = req.session.oauth; oa.getOAuthAccessToken(oauth.token, oauth.token_secret, oauth.verifier, function(error, oauth_access_token, oauth_access_token_secret, results){ if (error){ res.send("yeah something broke."); } else { req.session.oauth.access_token = oauth_access_token; req.session.oauth.access_token_secret = oauth_access_token_secret; req.session.twitter = results; res.redirect("/"); } } ); } else next(new Error("you're not supposed to be here.")); };
6. 準備完了。
ユーザーのscreen_name を出して、「ログイン中画面」にユーザーをお招きする。
exports.index = function (req, res) { if(req.session.oauth && req.session.oauth.access_token) { // コッチ。 res.render('index', { screen_name: req.session.twitter.screen_name , title: 'Express' }); } else { res.redirect("/login"); } };
これで「Twitterでログイン」ができました。
7. app.post('/post', routes.post);
このアプリ経由で、Tweetしてもらう。
ここは、xhrで叩く想定。ユーザーがここにPOSTすると、textパラメータの値をTwitterに投稿するしくみにする。
クライアントサイドのHTMLとJSはこんな。さきにjQueryを読んでおく。
// index.jade form#tweet input(type="text", name="text") input(type="submit", value="ツイート。")
// client side script $('#tweet').submit(function (e) { var text = e.target.text.value; $.ajax({ url: '/post' , type: 'POST' , data: {text: text} }); return false; });
うん。
nodeは、リクエストを受け取ると、こんどはoauthインスタンスにリクエストを投げさせる。引数に、TwitterのOKハンコであるaccess_tokenとaccess_token_secretを渡す(ユーザ特定のヒントとユーザ許可済みのしるし)。oauthインスタインスは、Twitterに特別なリクエストを投げてくれる。
特別っていうか、セッションに保存しているtokenやらいろいろを使って、特別なヘッダを作って、リクエストを飛ばす。
このときどんなリクエストを投げて欲しいかを、Twitterは丁寧に解説している。
... At a very simplified level, Twitter's implementation requires that requests needing authorization contain an additional HTTP Authorization header with enough information to answer the questions listed above. ...
https://dev.twitter.com/docs/auth/authorizing-requestPOST /1/statuses/update.json?include_entities=true HTTP/1.1
Accept: */*
Connection: close
User-Agent: OAuth gem v0.4.4
Content-Type: application/x-www-form-urlencoded
Authorization:
OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog",
oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg",
oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1318622958",
oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb",
oauth_version="1.0"
Content-Length: 76
Host: api.twitter.comstatus=Hello%20Ladies%20%2b%20Gentlemen%2c%20a%20signed%20OAuth%20request%21
consumer_keyやら、access_tokenやら。もう必要なものは全部持っているんだけど、この特別なヘッダ(Authorization header)を作るのが大変。いろいろ混ぜたりこねたりしなくちゃいけない。
これをやってくれるのが、node-oauthモジュール。node-oauthの中身をのぞくと、たとえばaccess_token_secretを使って、まぜたりこねたりして、シグネチャーを作っているのが分かる。絶対に作りたくないね。それをやってくれる。ありがたいね、本当の話。
exports.post = function (req, res) { if(req.session.oauth && req.session.oauth.access_token) { var text = req.body.text; oa.post( 'https://api.twitter.com/1/statuses/update.json', req.session.oauth.access_token, req.session.oauth.access_token_secret, {"status": text}, function (err, data, response) { if (err) { res.send('too bad.' + JSON.stringify(err)); } else { res.send('posted successfully...!'); } }); } else { res.send('fail.'); } };
登録しているアプリ経由で、投稿できたはず。
8. app.get('/logout', routes.logout);
ログアウトする。
ログアウトのリンクをどっかに置いておいて、テンプレート「logout」も作っておく。テンプレートには「完全にログアウトしたから安心するように」と書いておく。
exports.logout = function (req, res) { req.session.destroy(); res.render('logout'); };
ボンッッ。おしまい。
参考:
express でファイルアップロード
参考:
- http://tjholowaychuk.com/post/12943975936/connect-1-8-0-multipart-support
- https://github.com/felixge/node-formidable/issues/108
- http://www.hacksparrow.com/handle-file-uploads-in-express-node-js.html
nodeでファイルアップロードする方法、難しい順。
1. req.on('data') で、ちょこちょこデータを溜め込んでいく。(僕には無理っぽい)
2. それを楽にするformidable というモジュール。丁寧に 'progress' というイベントまでemit してくれる。
3. express のapp.configure で app.use(express.bodyParser()) していたら、req.files で更に簡単にupload できる。formidable は必要なくなる。
express でファイルアップロードを書く
ただアップロードする他に、ファイルを配置するディレクトリを作ったり、DBにファイルを保存したりしてみました。
でも、ほとんどhttp://www.hacksparrow.com/handle-file-uploads-in-express-node-js.htmlの内容です。
html:
<form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="upfile"/> <input type="submit" value="送信"/> </form>
server:
/* app.js */ var TMP_DIR = './tmp'; app.configure(function(){ . . app.use(express.bodyParser({ uploadDir: TMP_DIR /* 追記 */ })); . . }); . . app.get('/', routes.index); app.post('/upload', routes.upload); /* 追記 */ .
/* route/index.js */ /* * GET home page. */ // Require modules. var mkdirp = require('mkdirp') /* $ npm install mkdirp */ , fs = require('fs') , _ = require('underscore') , path = require('path') , db = require('../resources/server/db'); // Pad string function. _.mixin({ pad: function (target, length) { var padStr = '0'; var target = target.toString(); var result = target; var num = length - target.length; for (var i=0; i<num; i++) { result = padStr + result; } return result; } }); // Handler for GET '/'. exports.index = function(req, res){ db.Files.find({}, [], {}, function (err, files) { if (err) return; res.render('index', { title: 'Express' , IS_PRODUCTION: process.env.NODE_ENV === 'production' // new , files: files ? files : [] }) }); } // Handler for POST '/upload'. var UPLOAD_BASEPATH = __dirname + '/../public/uploaded/'; var getUpfileDir = function () { var d = new Date(); return d.getFullYear() + _.pad(d.getMonth() + 1, 2) + '/' + _.pad(d.getDate() + '/', 2); }; exports.upload = function(req, res){ var upfile = req.files.upfile if (upfile) { var tmpPath = './' + upfile.path; var saveDir = getUpfileDir(); var dir_ = path.normalize(UPLOAD_BASEPATH + saveDir); if (!path.existsSync(dir_)) mkdirp.sync(dir_); var savePath = saveDir + path.basename(tmpPath) + path.extname(upfile.name); var targetPath = path.normalize(UPLOAD_BASEPATH + savePath); fs.rename(tmpPath, targetPath, function (err) { if (err) throw err; var file = new db.Files(); file = _.extend(file, { title: req.body.title , name: upfile.name , type: upfile.type , path: savePath , createdAt: new Date() }); file.save(function (err) { res.redirect('/'); }); }); } };
/* resources/server/db.js */ // Setting mongoose. var mongoose = require('mongoose'); var FileUploadSchema = new mongoose.Schema({ title: String , name: String , type: String , path: String , createdAt: Date }); mongoose.model('Files', FileUploadSchema) mongoose.connect('mongodb://localhost/fileupload') exports.Files = mongoose.model('Files')
その他
- <input type="file" name="upfile" multiple="multiple" / >と書くと、複数ファイルを同時にアップロードできる(対応ブラウザが限られると思います、ご注意ください)
- その場合は req.files.upfile がオブジェクトじゃなくて配列になる。
node のexpress で Closure Library
node で Closure Library をやる一例です。
やること
- express プロジェクトを作る
- 使うnodeモジュールのインストール
- Closure Library を落とす
- Closure Compiler を落とす
- 実験用のapp.jsコードを置く
- production モードとそうでないモードを分ける
- Cakefileにタスクを書く
- 配備してみる
1. まず、express プロジェクトをつくります
$ express my_project
$ cd my_project
2. ./package.json を編集して、こんなふうにします。
/* ./package.json */ { "name": "application-name" , "version": "0.0.1" , "private": true , "dependencies": { "express": "2.5.8" , "jade": ">= 0.0.1" , "q": ">= 0.8.2" // new , "muffin": ">= 0.2.7" // new , "underscore": "" // new } } 追記:underscoreを追加
nodeモジュールをインストールします。
$ npm install ... q@0.8.2 ./node_modules/q └── event-queue@0.2.0 jade@0.20.3 ./node_modules/jade ├── mkdirp@0.3.0 └── commander@0.5.2 express@2.5.8 ./node_modules/express ├── mime@1.2.4 ├── mkdirp@0.3.0 ├── qs@0.4.2 └── connect@1.8.5 muffin@0.2.7 ./node_modules/muffin ├── temp@0.2.0 ├── coffee-script@1.1.3 ├── docco@0.3.0 ├── uglify-js@1.1.0 ├── q-fs@0.1.17 (q-io@0.0.10 qq@0.3.2 fs-boot@0.0.7) ├── q@0.7.1 (event-queue@0.2.0) ├── glob@3.0.1 (graceful-fs@1.1.5 fast-list@1.0.2 inherits@1.0.0 minimatch@0.1.5) └── prompt@0.1.12
3. Closure Library を落っことします。
$ svn checkout http://closure-library.googlecode.com/svn/trunk/ ./public/closure-library
publicフォルダの下には、置きたくないんですけどね。でも置きます。public/closure-library/closure/goog/base.js をhttpで読まないといけないので、ここに置いているんです。ハードリンクとか貼る人もいるんでしょうか?
4. Closure Compiler を落っことします。
$ mkdir -p library/closure-compiler $ wget -P library/closure-compiler/ http://closure-compiler.googlecode.com/files/compiler-latest.tar.gz $ tar -C library/closure-compiler/ -xf library/closure-compiler/compiler-latest.tar.gz $ ls library/closure-compiler/ COPYING compiler-latest.tar.gz README compiler.jar
ふーっ。ライブラリとかの準備はできました。
5. 次に、自分で書くクライアントサイドのコードを置く場所をつくります。あと、実験用のコードも置いておきます。
$ mkdir -p resources/client $ touch resources/client/app.js
./resources/client/app.js には試しにこう書いておきます。
/* ./resources/client/app.js */ goog.provide('my.app'); goog.require('goog.dom'); my.app = function () { var elm = goog.dom.createDom('h1', { style: 'font-size: 600%' }, 'yeah..'); goog.dom.append(document.body, elm); }; goog.exportSymbol('my.app', my.app);
./resources/client 配下にファイルを並べて、goog.provide/ goog.require していく感じです。
6. production モードと開発モードの区別ができるようにします。
作戦としては、
- node を「NODE_ENV=production node app.js」で起動したときは本番用(JSを小さくします)。
- ただの「node app.js」で起動したときは、開発用(ビルドにできるだけ時間をかけないようにします)。
まず、サーバ側のコードの修正です。
./routes/index.js
/* ./routes/index.js */ exports.index = function(req, res){ res.render('index', { title: 'Express' , IS_PRODUCTION: process.env.NODE_ENV === 'production' // new }) };
次に、この値をHTMLテンプレート側で拾って、クライアント側のjsファイルの読み分けをします。
// ./view/layout.jade !!! html head title= title link(rel='stylesheet', href='/stylesheets/style.css') // new code - if (IS_PRODUCTION) script(src='/javascripts/app-min.js') - else script(src='/closure-library/closure/goog/base.js') script(src='/javascripts/app.js') // modified body(onload='my.app()')!= body
OK。だと思います。ついでに、onload でスクリプトを実行するように書いておきました。
7. Cakefile にタスクを書いて、./public/javascript/app.js を作ってもらいます。
以下、muffin を使いたいのでcoffeescriptが必要です。coffeescriptをインストールしておく必要があります(npm install -g coffee-script)。
プロジェクト直下に Cakefile というファイルを作って、こんなファイルにします。
※修正しました。muffinだとoutputするやりかたが分からなかったのでネイティブのプロセス使った。
Cakefile
# Include required libraries. muffin = require 'muffin' child = require 'child_process' Q = require 'q' _ = require 'underscore' # Options. option '-w', '--watch', 'Keep watching file modifying.' option '-c', '--compile', 'Compile client scripts as ADVANCED_OPTIMIZATIONS.' # Constants. commonArgs = " public/closure-library/closure/bin/build/closurebuilder.py --root=./public/closure-library/ --root=./resources/client/ --namespace=\"my.app\" " COMMAND = BUILD: commonArgs + " --output_mode=script --output_file=./public/javascripts/app.js " COMPILE: commonArgs + " --output_mode=compiled --output_file=./public/javascripts/app-min.js --compiler_jar=./library/closure-compiler/compiler.jar --compiler_flags=\"--compilation_level=ADVANCED_OPTIMIZATIONS\"" # Tasks. stamp = ( -> startTime = -1 return { start: -> startTime = Date.now() if startTime is -1 end: -> t = if startTime isnt -1 then Date.now() - startTime else 0 startTime = -1 return t } )() execCommand = (command) -> stamp.start() child.exec command, (error, stdout, stderr) -> console.log """ \n\n====== #{stderr} --IT TOOK #{stamp.end() / 1000} sec.--\n """ task 'build', 'Make closure builder build scripts.', (options) -> command = if options.compile then COMMAND.COMPILE else COMMAND.BUILD if !options.watch execCommand(command) else isFirst = true muffin.run files: './**/*' options: options map: 'client/.*?\.js': (matches) -> return if isFirst execCommand(command) after: -> execCommand(command) if isFirst isFirst = false
これで、cakeタスクが使えるようになりました。
- 開発用に、更新監視しながらビルドするとき: cake -w build
- 本番用にコンパイルするとき。:cake -c build
8. ビルドしてみましょう。
$ cake build The "sys" module is now called "util". It should have a similar interface. public/closure-library/closure/bin/build/closurebuilder.py: Scanning paths... public/closure-library/closure/bin/build/closurebuilder.py: 797 sources scanned. public/closure-library/closure/bin/build/closurebuilder.py: Building dependency tree.. $ node app.js
本番環境用はどうでしょうか?
$ cake -c build The "sys" module is now called "util". It should have a similar interface. public/closure-library/closure/bin/build/closurebuilder.py: Scanning paths... public/closure-library/closure/bin/build/closurebuilder.py: 797 sources scanned. public/closure-library/closure/bin/build/closurebuilder.py: Building dependency tree.. public/closure-library/closure/bin/build/closurebuilder.py: Compiling with the following command: java -jar ./library/closure-compiler/compiler.jar --js public/closure-library/closure/goog/base.js --js public/closure-library/closure/goog/debug/error.js --js public/closure-library/closure/goog/string/string.js --js public/closure-library/closure/goog/asserts/asserts.js --js public/closure-library/closure/goog/array/array.js --js public/closure-library/closure/goog/dom/classes.js --js public/closure-library/closure/goog/object/object.js --js public/closure-library/closure/goog/dom/tagname.js --js public/closure-library/closure/goog/useragent/useragent.js --js public/closure-library/closure/goog/math/size.js --js public/closure-library/closure/goog/dom/browserfeature.js --js public/closure-library/closure/goog/math/coordinate.js --js public/closure-library/closure/goog/dom/dom.js --js resources/client/app.js --compilation_level=ADVANCED_OPTIMIZATIONS public/closure-library/closure/bin/build/closurebuilder.py: JavaScript compilation succeeded. $ NODE_ENV=production node app.js
-
-
- -
-
追記:Cakefileをちょっと修正しました。深い階層のjsを更新したら、階層の数だけ無駄にcompileしてた。