1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | var crypto = require('crypto');
|
10 | var fs = require('fs');
|
11 | var path = require('path');
|
12 | var chokidar = require('chokidar');
|
13 | var through = require('through');
|
14 | var derbyTemplates = require('derby-templates');
|
15 | var racer = require('racer');
|
16 | var util = racer.util;
|
17 | var App = require('./App');
|
18 | var files = require('./files');
|
19 |
|
20 | var STYLE_EXTENSIONS = ['.css'];
|
21 | var VIEW_EXTENSIONS = ['.html'];
|
22 | var COMPILERS = {
|
23 | '.css': files.cssCompiler
|
24 | , '.html': files.htmlCompiler
|
25 | };
|
26 |
|
27 | App.prototype._init = function(options) {
|
28 | this.scriptFilename = null;
|
29 | this.scriptMapFilename = null;
|
30 | this.scriptBaseUrl = (options && options.scriptBaseUrl) || '';
|
31 | this.scriptMapBaseUrl = (options && options.scriptMapBaseUrl) || '';
|
32 | this.scriptCrossOrigin = (options && options.scriptCrossOrigin) || false;
|
33 | this.scriptUrl = null;
|
34 | this.scriptMapUrl = null;
|
35 | this.agents = null;
|
36 | this.styleExtensions = STYLE_EXTENSIONS.slice();
|
37 | this.viewExtensions = VIEW_EXTENSIONS.slice();
|
38 | this.compilers = util.copyObject(COMPILERS);
|
39 |
|
40 | this.serializedDir = path.dirname(this.filename || '') + '/derby-serialized';
|
41 | this.serializedBase = this.serializedDir + '/' + this.name;
|
42 | if (fs.existsSync(this.serializedBase + '.json')) {
|
43 | this.deserialize();
|
44 | this.loadViews = function() {};
|
45 | this.loadStyles = function() {};
|
46 | return;
|
47 | }
|
48 | this.views.register('Page',
|
49 | '<!DOCTYPE html>' +
|
50 | '<meta charset="utf-8">' +
|
51 | '<view is="{{$render.prefix}}TitleElement"></view>' +
|
52 | '<view is="{{$render.prefix}}Styles"></view>' +
|
53 | '<view is="{{$render.prefix}}Head"></view>' +
|
54 | '<view is="{{$render.prefix}}BodyElement"></view>',
|
55 | {serverOnly: true}
|
56 | );
|
57 | this.views.register('TitleElement',
|
58 | '<title><view is="{{$render.prefix}}Title"></view></title>'
|
59 | );
|
60 | this.views.register('BodyElement',
|
61 | '<body class="{{$bodyClass($render.ns)}}">' +
|
62 | '<view is="{{$render.prefix}}Body"></view>'
|
63 | );
|
64 | this.views.register('Title', 'Derby App');
|
65 | this.views.register('Styles', '', {serverOnly: true});
|
66 | this.views.register('Head', '', {serverOnly: true});
|
67 | this.views.register('Body', '');
|
68 | this.views.register('Tail', '');
|
69 | };
|
70 |
|
71 | App.prototype.createPage = function(req, res, next) {
|
72 | var model = req.model || new racer.Model();
|
73 | this.emit('model', model);
|
74 | var page = new this.Page(this, model, req, res);
|
75 | if (next) {
|
76 | model.on('error', function(err){
|
77 | model.hasErrored = true;
|
78 | next(err);
|
79 | });
|
80 | page.on('error', next);
|
81 | }
|
82 | return page;
|
83 | };
|
84 |
|
85 | App.prototype.bundle = function(backend, options, cb) {
|
86 | var app = this;
|
87 | if (typeof options === 'function') {
|
88 | cb = options;
|
89 | options = null;
|
90 | }
|
91 | options || (options = {});
|
92 | if (options.minify == null) options.minify = util.isProduction;
|
93 |
|
94 |
|
95 | var viewsSource = this._viewsSource(options);
|
96 | var bundleFiles = [];
|
97 | backend.once('bundle', function(bundle) {
|
98 | bundle.require(path.dirname(__dirname), {expose: 'derby'});
|
99 |
|
100 |
|
101 | var viewsFilename = require.resolve('./_views');
|
102 | bundle.transform(function(filename) {
|
103 | if (filename !== viewsFilename) return through();
|
104 | return through(
|
105 | function write() {}
|
106 | , function end() {
|
107 | this.queue(viewsSource);
|
108 | this.queue(null);
|
109 | }
|
110 | );
|
111 | }, {global: true});
|
112 | bundle.on('file', function(filename) {
|
113 | bundleFiles.push(filename);
|
114 | });
|
115 | app.emit('bundle', bundle);
|
116 | });
|
117 | backend.bundle(app.filename, options, function(err, source, map) {
|
118 | if (err) return cb(err);
|
119 | app.scriptHash = crypto.createHash('md5').update(source).digest('hex');
|
120 | source = source.replace('{{DERBY_SCRIPT_HASH}}', app.scriptHash);
|
121 | source = source.replace(/['"]{{DERBY_BUNDLED_AT}}['"]/, Date.now());
|
122 | if (!util.isProduction) {
|
123 | app._autoRefresh(backend);
|
124 | app._watchBundle(bundleFiles);
|
125 | }
|
126 | cb(null, source, map);
|
127 | });
|
128 | };
|
129 |
|
130 | App.prototype.writeScripts = function(backend, dir, options, cb) {
|
131 | var app = this;
|
132 | this.bundle(backend, options, function(err, source, map) {
|
133 | if (err) return cb(err);
|
134 | dir = path.join(dir, 'derby');
|
135 | if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
136 | var filename = app.name + '-' + app.scriptHash;
|
137 | var base = path.join(dir, filename);
|
138 | app.scriptUrl = app.scriptBaseUrl + '/derby/' + filename + '.js';
|
139 |
|
140 |
|
141 | if (!(options && options.disableScriptMap)) {
|
142 | app.scriptMapUrl = app.scriptMapBaseUrl + '/derby/' + filename + '.map.json';
|
143 | source += '\n//# sourceMappingURL=' + app.scriptMapUrl;
|
144 | app.scriptMapFilename = base + '.map.json';
|
145 | fs.writeFileSync(app.scriptMapFilename, map, 'utf8');
|
146 | }
|
147 | app.scriptFilename = base + '.js';
|
148 | fs.writeFileSync(app.scriptFilename, source, 'utf8');
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | if (!util.isProduction) {
|
156 | var appPrefix = app.name + '-';
|
157 | var currentBundlePrefix = appPrefix + app.scriptHash;
|
158 | var filenames = fs.readdirSync(dir);
|
159 | for (var i = 0; i < filenames.length; i++) {
|
160 | var filename = filenames[i];
|
161 | if (filename.indexOf(appPrefix) !== 0) {
|
162 |
|
163 | continue;
|
164 | }
|
165 | if (filename.indexOf(currentBundlePrefix) === 0) {
|
166 |
|
167 | continue;
|
168 | }
|
169 |
|
170 | var oldFilename = path.join(dir, filename);
|
171 | fs.unlinkSync(oldFilename);
|
172 | }
|
173 | }
|
174 | cb && cb();
|
175 | });
|
176 | };
|
177 |
|
178 | App.prototype._viewsSource = function(options) {
|
179 | return '/*DERBY_SERIALIZED_VIEWS*/' +
|
180 | 'module.exports = ' + this.views.serialize(options) + ';' +
|
181 | '/*DERBY_SERIALIZED_VIEWS_END*/';
|
182 | };
|
183 |
|
184 | App.prototype.serialize = function() {
|
185 | if (!fs.existsSync(this.serializedDir)) {
|
186 | fs.mkdirSync(this.serializedDir);
|
187 | }
|
188 |
|
189 |
|
190 | var viewsSource = this._viewsSource({server: true, minify: true});
|
191 | fs.writeFileSync(this.serializedBase + '.views.js', viewsSource, 'utf8');
|
192 | var scriptUrl = (this.scriptUrl.indexOf(this.scriptBaseUrl) === 0) ?
|
193 | this.scriptUrl.slice(this.scriptBaseUrl.length) :
|
194 | this.scriptUrl;
|
195 | var scriptMapUrl = (this.scriptMapUrl.indexOf(this.scriptMapBaseUrl) === 0) ?
|
196 | this.scriptMapUrl.slice(this.scriptMapBaseUrl.length) :
|
197 | this.scriptMapUrl;
|
198 | var serialized = JSON.stringify({
|
199 | scriptBaseUrl: this.scriptBaseUrl
|
200 | , scriptMapBaseUrl: this.scriptMapBaseUrl
|
201 | , scriptUrl: scriptUrl
|
202 | , scriptMapUrl: scriptMapUrl
|
203 | });
|
204 | fs.writeFileSync(this.serializedBase + '.json', serialized, 'utf8');
|
205 | };
|
206 |
|
207 | App.prototype.deserialize = function() {
|
208 | var serializedViews = require(this.serializedBase + '.views.js');
|
209 | var serialized = require(this.serializedBase + '.json');
|
210 | serializedViews(derbyTemplates, this.views);
|
211 | this.scriptUrl = (this.scriptBaseUrl || serialized.scriptBaseUrl) + serialized.scriptUrl;
|
212 | this.scriptMapUrl = (this.scriptMapBaseUrl || serialized.scriptMapBaseUrl) + serialized.scriptMapUrl;
|
213 | };
|
214 |
|
215 | App.prototype.loadViews = function(filename, namespace) {
|
216 | var data = files.loadViewsSync(this, filename, namespace);
|
217 | for (var i = 0, len = data.views.length; i < len; i++) {
|
218 | var item = data.views[i];
|
219 | this.views.register(item.name, item.source, item.options);
|
220 | }
|
221 | if (!util.isProduction) this._watchViews(data.files, filename, namespace);
|
222 |
|
223 | return this;
|
224 | };
|
225 |
|
226 | App.prototype.loadStyles = function(filename, options) {
|
227 | this._loadStyles(filename, options);
|
228 | var stylesView = this.views.find('Styles');
|
229 | stylesView.source += '<view is="' + filename + '"></view>';
|
230 |
|
231 | return this;
|
232 | };
|
233 |
|
234 | App.prototype._loadStyles = function(filename, options) {
|
235 | var styles = files.loadStylesSync(this, filename, options);
|
236 |
|
237 | var filepath = '';
|
238 | if (!util.isProduction) {
|
239 | |
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 | filepath = ' data-filename="' + filename + '"';
|
246 | }
|
247 | var source = '<style' + filepath + '>' + styles.css + '</style>';
|
248 |
|
249 | this.views.register(filename, source, {
|
250 | serverOnly: true
|
251 | });
|
252 |
|
253 | if (!util.isProduction) {
|
254 | this._watchStyles(styles.files, filename, options);
|
255 | }
|
256 |
|
257 | return styles;
|
258 | };
|
259 |
|
260 | App.prototype._watchViews = function(filenames, filename, namespace) {
|
261 | var app = this;
|
262 | var watcher = chokidar.watch(filenames);
|
263 | watcher.on('change', function() {
|
264 | watcher.close();
|
265 | app.loadViews(filename, namespace);
|
266 | app._updateScriptViews();
|
267 | app._refreshClients();
|
268 | });
|
269 | };
|
270 |
|
271 | App.prototype._watchStyles = function(filenames, filename, options) {
|
272 | var app = this;
|
273 | var watcher = chokidar.watch(filenames);
|
274 | watcher.on('change', function() {
|
275 | watcher.close();
|
276 | var styles = app._loadStyles(filename, options);
|
277 | app._updateScriptViews();
|
278 | app._refreshStyles(filename, styles);
|
279 | });
|
280 | };
|
281 |
|
282 | App.prototype._watchBundle = function(filenames) {
|
283 | if (!process.send) return;
|
284 | var app = this;
|
285 | var watcher = chokidar.watch(filenames);
|
286 | watcher.on('change', function() {
|
287 | watcher.close();
|
288 | process.send({type: 'reload'});
|
289 | });
|
290 | };
|
291 |
|
292 | App.prototype._updateScriptViews = function() {
|
293 | if (!this.scriptFilename) return;
|
294 | var script = fs.readFileSync(this.scriptFilename, 'utf8');
|
295 | var i = script.indexOf('/*DERBY_SERIALIZED_VIEWS*/');
|
296 | var before = script.slice(0, i);
|
297 | var i = script.indexOf('/*DERBY_SERIALIZED_VIEWS_END*/');
|
298 | var after = script.slice(i + 30);
|
299 | var viewsSource = this._viewsSource();
|
300 | fs.writeFileSync(this.scriptFilename, before + viewsSource + after, 'utf8');
|
301 | };
|
302 |
|
303 | App.prototype._autoRefresh = function(backend) {
|
304 | var agents = this.agents = {};
|
305 | var app = this;
|
306 |
|
307 | backend.use('receive', function(request, next) {
|
308 | var data = request.data;
|
309 | if (data.derby) {
|
310 | app._handleMessage(request.agent, data.derby, data);
|
311 | }
|
312 | next();
|
313 | });
|
314 | };
|
315 |
|
316 | App.prototype._handleMessage = function(agent, action, message) {
|
317 | if (action === 'app') {
|
318 | if (message.name !== this.name) {
|
319 | return;
|
320 | }
|
321 | if (message.hash !== this.scriptHash) {
|
322 | return agent.send({derby: 'reload'});
|
323 | }
|
324 | this._addAgent(agent);
|
325 | }
|
326 | };
|
327 |
|
328 | App.prototype._addAgent = function(agent) {
|
329 | this.agents[agent.clientId] = agent;
|
330 | var app = this;
|
331 | agent.stream.once('end', function() {
|
332 | delete app.agents[agent.clientId];
|
333 | });
|
334 | };
|
335 |
|
336 | App.prototype._refreshClients = function() {
|
337 | if (!this.agents) return;
|
338 | var views = this.views.serialize({minify: true});
|
339 | var message = {
|
340 | derby: 'refreshViews',
|
341 | views: views
|
342 | };
|
343 | for (var id in this.agents) {
|
344 | this.agents[id].send(message);
|
345 | }
|
346 | };
|
347 |
|
348 | App.prototype._refreshStyles = function(filename, styles) {
|
349 | if (!this.agents) return;
|
350 | var data = {filename: filename, css: styles.css};
|
351 | var message = {
|
352 | derby: 'refreshStyles',
|
353 | filename: filename,
|
354 | css: styles.css
|
355 | };
|
356 | for (var id in this.agents) {
|
357 | this.agents[id].send(message);
|
358 | }
|
359 | };
|