UNPKG

10.5 kBJavaScriptView Raw
1'use strict';
2
3const path = require('path');
4const $ = require('./utils');
5
6// initialize meta-bundler
7const tarima = require('../../lib');
8
9const plugableSupportAPI = require('./hooks');
10const cacheableSupportAPI = require('./caching');
11
12// /* eslint-disable import/no-unresolved */
13let htmlCompressor;
14let cssCompressor;
15let jsCompressor;
16
17function prune(object) {
18 if (!object || typeof object !== 'object') {
19 return object;
20 }
21
22 if (Array.isArray(object)) {
23 return object.map(prune);
24 }
25
26 const copy = {};
27
28 Object.keys(object).forEach(key => {
29 if (key.charAt() !== '_') {
30 copy[key] = prune(object[key]);
31 }
32 });
33
34 return copy;
35}
36
37let ctx;
38
39module.exports.init = options => {
40 let _level;
41
42 _level = (options.flags.verbose && 'verbose') || (options.flags.debug ? 'debug' : 'info');
43 _level = (options.flags.quiet && !options.flags.version && !options.flags.help) ? false : _level;
44
45 const logger = require('log-pose').setLevel(_level).getLogger(12, process.stdout, process.stderr);
46
47 ctx = plugableSupportAPI(logger, options);
48
49 ctx.cache = cacheableSupportAPI(options.cacheFile);
50 ctx.match = $.makeFilter(false, Array.isArray(options.filter)
51 ? options.filter
52 : ['**']);
53
54 ctx.logger = logger;
55 ctx.started = true;
56 ctx.tarimaOptions = {};
57 ctx.tarimaOptions.cwd = options.cwd;
58 ctx.tarimaOptions.public = options.public;
59
60 Object.keys(options.bundleOptions).forEach(key => {
61 ctx.tarimaOptions[key] = options.bundleOptions[key];
62 });
63
64 let fixedBundle = $.toArray(options.bundle);
65
66 ctx.isBundle = () => false;
67 ctx._bundle = null;
68 ctx._cache = null;
69 ctx._data = [];
70
71 options.bundleOptions.cache = ctx.cache.all() || {};
72
73 // built-in helpers
74 options.bundleOptions.helpers.srcFile = _ => $.read(path.join(options.cwd, _.src));
75 options.bundleOptions.helpers.destFile = _ => $.read(path.join(options.output, _.src));
76 options.bundleOptions.helpers.resources = () => (options.bundleOptions.resources || []).join('\n');
77
78 /* eslint-disable prefer-rest-params */
79 /* eslint-disable prefer-spread */
80
81 options.bundleOptions.helpers.includeTag = function _include(_) {
82 return (typeof _.src === 'string' ? _.src.split(/[\s|;,]+/) : _.src)
83 .map(src => {
84 if (src.indexOf(':') === -1 && !$.exists(path.join(options.output, src))) {
85 if (_.required) throw new Error(`Required source to include: ${src}`);
86 return;
87 }
88
89 if (String(src).indexOf('.css') > -1) {
90 return `<link rel="stylesheet" href="${src}">`;
91 }
92
93 if (String(src).indexOf('.js') > -1) {
94 return `<script src="${src}"></script>`;
95 }
96
97 throw new Error(`Unsupported source to include: ${src}`);
98 })
99 .filter(Boolean)
100 .join('\n');
101 };
102
103 if ($.isFile(options.rollupFile)) {
104 /* eslint-disable global-require */
105 $.merge(options.bundleOptions.rollup, require(path.resolve(options.rollupFile)));
106 }
107
108 if (options.bundleOptions.entryCache) {
109 ctx._cache = {};
110 }
111
112 for (let i = 0, c = fixedBundle.length; i < c; i += 1) {
113 if (fixedBundle[i] === true) {
114 ctx.isBundle = () => true;
115 fixedBundle = [];
116 break;
117 }
118
119 if (!fixedBundle[i]) {
120 fixedBundle.splice(i, 1);
121 }
122 }
123
124 if (fixedBundle.length) {
125 ctx.isBundle = $.makeFilter(true, fixedBundle);
126 }
127
128 // custom events
129 ctx.onWrite = ctx.emit.bind(null, 'write');
130 ctx.onDelete = ctx.emit.bind(null, 'delete');
131
132 // pre-compile regex to reuse on all replacements!
133 if (!options.bundleOptions.helpers._regex) {
134 const keys = Object.keys(options.bundleOptions.helpers);
135
136 Object.defineProperty(options.bundleOptions.helpers, '_regex', {
137 value: new RegExp(`<(${keys.join('|')})([^<>]*?)(?:\\/>|>([^<>]*?)<\\/\\1>)`, 'g'),
138 });
139 }
140
141 function ensureRename(view) {
142 if (typeof options.rename === 'function') {
143 options.rename(view);
144 }
145 }
146
147 function ensureOptimize(name, contents, sourceMaps) {
148 if (name.indexOf('.html') > -1) {
149 htmlCompressor = htmlCompressor || require('html-minifier').minify;
150
151 return htmlCompressor(contents, {
152 collapseBooleanAttributes: true,
153 collapseInlineTagWhitespace: true,
154 collapseWhitespace: true,
155 minifyCSS: true,
156 minifyJS: true,
157 removeAttributeQuotes: true,
158 removeCDATASectionsFromCDATA: true,
159 removeComments: true,
160 removeCommentsFromCDATA: true,
161 removeEmptyAttributes: true,
162 removeOptionalTags: true,
163 removeRedundantAttributes: true,
164 removeScriptTypeAttributes: true,
165 removeStyleLinkTypeAttributes: true,
166 useShortDoctype: true,
167 });
168 }
169
170 if (name.indexOf('.css') > -1) {
171 cssCompressor = cssCompressor || require('csso').minify;
172
173 return cssCompressor(contents, {
174 filename: name,
175 sourceMap: sourceMaps,
176 }).css;
177 }
178
179 if (name.indexOf('.js') > -1) {
180 jsCompressor = jsCompressor || require('terser').minify;
181
182 return jsCompressor(contents, {
183 ie8: true,
184 compress: {
185 warnings: true,
186 drop_console: true,
187 unsafe_proto: true,
188 unsafe_undefined: true,
189 },
190 sourceMap: sourceMaps,
191 }).code;
192 }
193 }
194
195 ctx.ensureWrite = (view, index, params) =>
196 Promise.resolve()
197 .then(() => ctx.onWrite(view, index))
198 .then(() => {
199 if (params.$minify || options.bundleOptions.optimizations) {
200 const sourceMaps = Boolean(options.bundleOptions.compileDebug && view.sourceMap);
201 const fixedOutput = ensureOptimize(view.dest, view.output, sourceMaps);
202 const shouldMinify = /\.(?:css|js)$/.test(view.dest);
203 const fixedFilename = shouldMinify
204 ? view.dest.replace(/\.(\w+)$/, '.min.$1')
205 : view.dest;
206
207 $.write(fixedFilename, fixedOutput || view.output);
208 }
209
210 if (options.bundleOptions.sourceMapFiles === true && view.sourceMap) {
211 $.write(`${view.dest}.map`, JSON.stringify(view.sourceMap));
212 }
213
214 $.write(view.dest, view.output);
215 });
216
217 ctx.dest = (id, ext) => {
218 return path.relative(options.cwd, path.join(options.output, ext
219 ? id.replace(/\.[\w.]+$/, `.${ext}`)
220 : id));
221 };
222
223 ctx.sync = (id, resolve) => {
224 const entry = ctx.cache.get(id) || {};
225
226 entry.dirty = false;
227
228 if (resolve) {
229 resolve(entry);
230 }
231 };
232
233 ctx.copy = target => {
234 const entry = ctx.cache.get(target.src);
235
236 if ((entry && entry.deleted) || !$.exists(target.src)) {
237 target.type = 'delete';
238 ctx.dist(target);
239 return;
240 }
241
242 ctx._data.push(target.dest);
243
244 target.type = 'copy';
245
246 ctx.sync(target.src);
247 ctx.dist(target);
248 };
249
250 ctx.track = (src, sub) => {
251 ctx.sync(src, entry => {
252 entry.deps = entry.deps || [];
253
254 (sub || []).forEach(dep => {
255 ctx.sync(dep, _entry => {
256 _entry.deps = _entry.deps || [];
257
258 if (_entry.deps.indexOf(src) === -1) {
259 _entry.deps.push(src);
260 }
261
262 ctx.cache.set(dep, _entry);
263 });
264 });
265 });
266 };
267
268 ctx.compile = (src, cb) => {
269 const entry = ctx.cache.get(src) || {};
270 const opts = ctx.tarimaOptions;
271
272 let partial;
273
274 try {
275 opts._bundle = ctx._bundle;
276 opts._cache = ctx._cache;
277 partial = tarima.load(path.resolve(options.cwd, src), opts);
278 } catch (e) {
279 return cb(e);
280 }
281
282 // preset bundles through extensions, matching or inline data
283 const _method = (partial.params.parts.includes('bundle') || partial.params.data.$bundle || ctx.isBundle(src))
284 ? 'bundle'
285 : 'render';
286
287 return logger(_method, src, end =>
288 partial[_method]((err, output) => {
289 if (err) {
290 end(src, _method, 'failure');
291 return cb($.decorateError(err, partial.params));
292 }
293
294 // cached for later
295 if (options.bundleOptions.bundleCache) {
296 ctx._bundle = output._bundle || ctx._bundle;
297 }
298
299 const file = path.relative(options.cwd, output.filename);
300 const target = ctx.dest(file, output.extension);
301 const index = ctx.track.bind(null, file);
302 const tasks = [];
303
304 const result = {
305 src: file,
306 dest: target,
307 };
308
309 ensureRename(result);
310
311 if (output._chunks) {
312 output._chunks.forEach(chunk => {
313 tasks.push(() => {
314 const sub = {
315 dest: path.relative(options.cwd, path.resolve(result.dest, '..', chunk.filename)),
316 data: chunk.source,
317 type: 'write',
318 };
319
320 ensureRename(sub);
321
322 if (options.bundleOptions.optimizations) {
323 sub.data = ensureOptimize(chunk.filename, chunk.source) || sub.data;
324 }
325
326 ctx._data.push(sub.dest);
327 ctx.dist(sub);
328 });
329 });
330 }
331
332 ctx._data.push(result.dest);
333
334 result.output = output.source;
335 result.sourceMap = output.sourceMap;
336
337 // TODO: only track partials (?)
338 const fixedDeps = entry.deps || [];
339
340 output.deps.forEach(id => {
341 if (id.indexOf(options.cwd) !== 0) {
342 return;
343 }
344
345 const dep = path.relative(options.cwd, id);
346
347 if ((file.split('/')[0] === dep.split('/')[0])
348 && fixedDeps.indexOf(dep) === -1) {
349 fixedDeps.push(dep);
350 }
351 });
352
353 index(fixedDeps);
354 ctx.ensureWrite(result, index, partial.params.data)
355 .then(() => tasks.map(x => x()))
356 .then(() => {
357 ctx.cache.set(file, 'deps', fixedDeps);
358 ctx.cache.set(file, 'dest', result.dest);
359 ctx.cache.set(file, 'data', prune(output.data));
360
361 delete result.output;
362
363 end(result.dest);
364 cb();
365 })
366 .catch(cb);
367 }));
368 };
369};
370
371module.exports.run = (data, options, callback) => {
372 Promise.resolve()
373 .then(() => {
374 if (!data.dest) {
375 return new Promise((resolve, reject) => {
376 ctx.compile(data.src, err => {
377 if (err) {
378 reject(err);
379 } else {
380 resolve();
381 }
382 });
383 });
384 }
385
386 ctx.copy(data);
387 })
388 .then(() => {
389 callback(null, ctx._data, ctx.cache.get(data.src) || {});
390 })
391 .catch(e => {
392 callback(e);
393 });
394};