UNPKG

7.38 kBJavaScriptView Raw
1/*
2 * pub-server serve-scripts.js
3 *
4 * serves browserified scripts
5 * as well as /pub/* routes for opts, plugins and source files.
6
7 * API: serveStatics(opts, server) returns serveStatics object
8 * server optional, if not passed, no routes served
9 * serveStatics.outputAll() - copy scripts to outputs[0] (for pub -O)
10 *
11 * copyright 2015-2020, Jürgen Leschner - github.com/jldec - MIT license
12 */
13
14/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
15
16var debug = require('debug')('pub:scripts');
17var u = require('pub-util');
18var through = require('through2');
19var fspath = require('path'); // for platform specific path.join
20var uglify = require('uglify-es');
21var asyncbuilder = require('asyncbuilder');
22var browserify = require('browserify');
23
24module.exports = function serveScripts(opts) {
25
26 if (!(this instanceof serveScripts)) return new serveScripts(opts);
27 var self = this;
28 var log = opts.log;
29
30 self.serveRoutes = serveRoutes;
31 self.outputAll = outputAll; // for pub -O
32
33 // prepare array of browserscripts including builtins
34 self.scripts = u.map(opts.browserScripts, function(script) {
35 var o = {
36 route: script.route,
37 path: script.path,
38 opts: u.omit(script, 'path', 'route', 'inject')
39 };
40 return o;
41 });
42
43 // sockjs scripts
44 if (!opts['no-sockets']) {
45 self.scripts.push( {
46 route: '/server/pub-sockets.js',
47 path: fspath.join(__dirname, '../client/pub-sockets.js'),
48 opts: {}
49 } );
50 }
51
52 // editor scripts
53 if (opts.editor) {
54 self.scripts.push( {
55 route: '/pub/pub-ux.js',
56 path: fspath.join(__dirname, '../client/pub-ux.js'),
57 opts: {}
58 } );
59 self.scripts.push( {
60 route: '/pub/_generator.js',
61 path: fspath.join(__dirname, '../client/_generator.js'),
62 opts: {}
63 } );
64 self.scripts.push( {
65 route: '/pub/_generator-plugins.js',
66 path: fspath.join(__dirname, '../client/_generator-plugins.js'),
67 opts: { transform: [transformPlugins] }
68 } );
69 }
70
71 // prepare for bundling by serveRoutes or outputAll
72 u.each(self.scripts, function(script) {
73 script._bundler = browserify(
74 script.path,
75 { debug:opts.dbg, transform:script.opts.transform }
76 ).ignore(['resolve', 'osenv', 'tmp']);
77 });
78
79 return;
80
81 //--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//
82
83 // deploy browserify scripts and editor/admin handlers
84 function serveRoutes(server) {
85 var app = server.app;
86 var generator = server.generator;
87
88 // route browserscripts, including builtins
89 u.each(self.scripts, function(script) {
90 app.get(script.route, function(req, res, next) {
91 if (script._buf) return sendBuf(res, script._buf); // send memoised
92 var time = u.timer();
93 script._bundler.bundle(function(err, buf) {
94 debug('bundle: %s (%d bytes, %d ms)', script.path, buf.length, time());
95 if(err) return next(err);
96 script._buf = buf; // memoise
97 sendBuf(res, buf);
98 });
99 });
100 });
101
102 // assume all browserScripts are .js, cache on client for 1hr
103 function sendBuf(res, buf) {
104 res.set( {
105 'Content-Type': 'text/javascript',
106 'Cache-Control': 'public, max-age=3600'
107 } );
108 res.send(buf);
109 }
110
111 // editor api
112 if (opts.editor) {
113 app.post('/pub/_files', function(req, res) {
114 generator.serverSave(req.body, req.user, function(err, results) {
115 if (err) return res.status(500).send(err);
116 res.status(200).send(results);
117 });
118 });
119 app.get('/pub/_opts.json', function(req, res) {
120 res.set('Cache-Control', 'no-cache');
121 res.send(serializeOpts(server.generator));
122 });
123 }
124 }
125
126 // output all browserscripts with uglify logic from
127 // https://github.com/ForbesLindesay/browserify-middleware/blob/master/lib/build-response.js
128 function outputAll(output, generator, cb) {
129 cb = u.maybe(cb);
130 output = output || opts.outputs[0];
131
132 var files = [];
133 var filemap = [];
134
135 var omit = output.omitRoutes;
136 if (omit && !u.isArray(omit)) { omit = [omit]; }
137
138 // TODO: re-use similar filter in server/serve-statics and generator.output
139 var filterRe = new RegExp( '^(/admin/|/server/' +
140 (opts.editor ? '' : '|/pub/') +
141 (omit ? '|' + u.map(omit, u.escapeRegExp).join('|') : '') +
142 ')');
143
144 // builder results collected in files and filemap
145 var ab = asyncbuilder(function(err) {
146 if (err) return cb(err, filemap);
147 output.src.put(files, function(err) {
148 if (err) return cb(err, filemap);
149 cb(null, filemap);
150 });
151 });
152
153 u.each(self.scripts, function(script) {
154 if (filterRe.test(script.route)) return;
155 var scriptDone = ab.asyncAppend();
156
157 var time = u.timer();
158
159 script._bundler.bundle(function (err, buf) {
160 if (err) {
161 log(err);
162 scriptDone(err, script.route);
163 return;
164 }
165 var str = buf.toString();
166 if (!opts.dbg) {
167 try {
168 // slow! TODO: offer a way to cache between builds
169 str = uglify.minify(str, {}).code;
170 }
171 catch(e) {}
172 }
173 files.push( { path:script.route, text:str } );
174 filemap.push( { path:script.route } );
175 log('output script: %s (%d bytes, %d ms)', script.route, str.length, time());
176 scriptDone(null, script.route);
177 });
178 });
179
180 if (opts.editor) {
181 var optsPath = '/pub/_opts.json';
182 var optsStr = JSON.stringify(serializeOpts(generator, true, output));
183 files.push( { path:optsPath, text:optsStr } );
184 filemap.push( { path:optsPath } );
185 log('output %s (%d bytes)', optsPath, optsStr.length);
186 }
187
188 ab.complete();
189 }
190
191 // browserify transform for sending plugins
192 // invoked using require('./__plugins')
193 function transformPlugins(path) {
194 if (!/_generator-plugins/.test(path)) return through();
195 return through(
196 function tform(chunk, enc, cb) { cb(); }, // ignore input
197 function flush(cb) {
198 this.push(requirePlugins());
199 cb();
200 }
201 );
202 }
203
204 function requirePlugins() {
205 var s = u.reduce(opts.generatorPlugins.reverse(), function(memo, plugin) {
206 return memo + 'require("' + plugin.path + '")(generator);\n';
207 }, '');
208
209 return s;
210 }
211
212 function serializeOpts(generator, toStatic, outputDest) {
213 var sOpts = u.omit(opts, 'output$', 'source$', 'log', 'session');
214
215 // provide for detection of static hosted editor
216 if (toStatic) { sOpts.staticHost = true; }
217
218 // pass output.fqImages -> static opts for use in static editor/generator
219 if (outputDest && outputDest.fqImages) { sOpts.fqImages = outputDest.fqImages; }
220
221 sOpts.staticPaths = u.map(opts.staticPaths, function(staticPath) {
222 return u.omit(staticPath, 'files', 'src');
223 });
224 sOpts.outputs = u.map(opts.outputs, function(output) {
225 return u.omit(output, 'files', 'src');
226 });
227 sOpts.sources = u.map(opts.sources, function(source) {
228 var rawSource = u.omit(source, 'files', 'src', 'file$', 'fragments', 'updates', 'snapshots', 'drafts', 'cache', 'redisOpts');
229 rawSource.files = source.type === 'FILE' ?
230 generator.serializeFiles(source.files) :
231 source.files;
232 return rawSource;
233 });
234 return sOpts;
235 }
236
237};