1 | 'use strict';
|
2 |
|
3 | const path = require('path');
|
4 | const $ = require('./utils');
|
5 |
|
6 |
|
7 | const tarima = require('../../lib');
|
8 |
|
9 | const plugableSupportAPI = require('./hooks');
|
10 | const cacheableSupportAPI = require('./caching');
|
11 |
|
12 |
|
13 | let htmlCompressor;
|
14 | let cssCompressor;
|
15 | let jsCompressor;
|
16 |
|
17 | function 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 |
|
37 | let ctx;
|
38 |
|
39 | module.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 |
|
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 |
|
79 |
|
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 |
|
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 |
|
129 | ctx.onWrite = ctx.emit.bind(null, 'write');
|
130 | ctx.onDelete = ctx.emit.bind(null, 'delete');
|
131 |
|
132 |
|
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 |
|
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 |
|
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 |
|
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 |
|
371 | module.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 | };
|