UNPKG

11.6 kBJavaScriptView Raw
1'use strict';
2
3var babyTolk = require('baby-tolk');
4var readdirp = require('readdirp');
5var through = require('through2');
6var path = require('path');
7var pathCompleteExtname = require('path-complete-extname');
8var when = require('when');
9var fs = require('fs');
10var mm = require('micromatch');
11var events = require('events');
12var checkDir = require('checkdir');
13var nodefn = require('when/node');
14var crypto = require('crypto');
15var rimraf = nodefn.lift(require('rimraf'));
16var mkdirp = nodefn.lift(require('mkdirp'));
17var fsp = nodefn.liftAll(fs);
18
19// TODO: Consider using node-glob module instead of readdirp + through2
20// I think that will simplify the code.
21
22var extname = function(filePath) {
23 var ext = path.extname(filePath);
24 if (babyTolk.targetExtensionMap[ext]) { return ext; }
25 return pathCompleteExtname(filePath);
26};
27
28var replaceExtension = function(filePath, newExtension) {
29 var ext = extname(filePath);
30 return filePath.slice(0,-(ext.length)) + newExtension;
31};
32
33var addSrcExtension = function(filePath) {
34 var ext = extname(filePath);
35 return replaceExtension(filePath, '.src' + ext);
36};
37
38var useExclusionsApi = function(options) {
39 options.directoryFilter = options.exclusions.filter(
40 excl => excl.action === 'exclude' && excl.type === 'dir'
41 ).map(excl => '!' + excl.path);
42 options.fileFilter = options.exclusions.filter(
43 excl => excl.action === 'exclude' && excl.type === 'file'
44 ).map(excl => '!' + excl.path);
45
46 options.blacklist = options.exclusions.filter(
47 excl => excl.action === 'dontCompile' && excl.type === 'dir'
48 ).map(excl => excl.path + '/**').concat(
49 options.exclusions.filter(
50 excl => excl.action === 'dontCompile' && excl.type === 'file'
51 ).map(excl => excl.path)
52 );
53
54 options.directoryFilter = options.directoryFilter.length ? options.directoryFilter : null;
55 options.fileFilter = options.fileFilter.length ? options.fileFilter : null;
56 options.blacklist = options.blacklist.length ? options.blacklist : null;
57 return options;
58};
59
60var createHash = function(data) {
61 var shasum = crypto.createHash('sha1');
62 shasum.update(data);
63 return shasum.digest('hex');
64};
65
66var noop = () => null;
67
68module.exports = function(inputDir, outputDir, options) {
69 var eventEmitter = new events.EventEmitter();
70 var shasFilename = '.shas.json';
71 var fileWhitelist = [shasFilename];
72 var filesCopied = 0;
73 options = options || {};
74 var previousDir = null;
75 var abortRequested = false;
76 var shas = [];
77 var shasPath = path.join(outputDir, shasFilename);
78 var existingShas;
79
80 // Default compile and minify options to `true`
81 options.compile = options.compile === false ? false : true;
82 options.sourcemaps = options.sourcemaps === false ? false : true;
83
84 var babyTolkOptions = {
85 minify: options.minify,
86 sourceMap: options.sourcemaps,
87 inputSha: true,
88 };
89
90 if (options.exclusions) { useExclusionsApi(options); }
91
92 var readAndValidateShas = function() {
93 var getMainFile = sha => {
94 var files = sha.output;
95 var mainFile = files[files.length - 1];
96 return path.join(outputDir, mainFile);
97 };
98
99 var getInputFile = sha => path.join(inputDir, sha.input);
100
101 return fsp.readFile(shasPath, 'utf8').then(contents => {
102 existingShas = JSON.parse(contents);
103
104 var matchingCompilerShas = existingShas.filter(sha =>
105 babyTolk.getTransformId(getInputFile(sha), babyTolkOptions) === sha.type
106 );
107
108 var outFiles = existingShas.map(sha =>
109 sha && fsp.readFile(getMainFile(sha), 'utf8').then(contents => contents, noop)
110 );
111
112 // Verify that the output file hasn't changed
113 return when.all(outFiles)
114 .then(fileContents =>
115 matchingCompilerShas.filter((sha, i) =>
116 (fileContents[i] && (sha.outputSha === createHash(fileContents[i])))
117 )
118 );
119 })
120 .then(filtered =>
121 // Verify that the input files haven't changed
122 when.all(filtered.map(sha =>
123 when.all(sha.inputSha.map(input =>
124 fsp.readFile(path.join(inputDir, input.file), 'utf8').then(contents =>
125 input.sha === createHash(contents)
126 )
127 ))
128 )).then(equalShaBoolArr => {
129 var filterBools = equalShaBoolArr.map(
130 // i.e.. [true, true, false] => false
131 x => x.reduce((prev, curr) => prev && curr, true)
132 );
133 return filtered.filter((el, i) => filterBools[i]);
134 })
135 )
136 .then(filtered => {
137 if (!Array.isArray(filtered)) { return; }
138 return {
139 input: filtered.map(x => x.input),
140 // Flatten output files to single array
141 output: [].concat.apply([], filtered.map(x => x.output))
142 };
143 }, noop // ignore errors (shas are not required, just good for perf)
144 );
145 };
146
147 var compileAndCopy = function(reusableFiles) {
148 babyTolk.reload();
149 return when.promise(function(resolve, reject) {
150 options.root = inputDir;
151 var stream = readdirp(options);
152
153 stream.pipe(through.obj(function (file, _, next) {
154 if (abortRequested) {
155 var err = new Error('Manually aborted by user');
156 err.code = 'ABORT';
157 return reject(err);
158 }
159 var outputFullPath = path.join(outputDir, file.path);
160 var doCompile = !mm(file.path, options.blacklist).length;
161
162 var addFileToWhitelist = filePath => { fileWhitelist.push(filePath); };
163
164 var fileDone = function() {
165 if (previousDir !== file.parentDir) {
166 eventEmitter.emit('chdir', file.parentDir);
167 previousDir = file.parentDir;
168 }
169 filesCopied = filesCopied + 1;
170 next();
171 };
172
173 var processFile = function() {
174 var ext = extname(file.name);
175 var compileExt = babyTolk.targetExtensionMap[ext];
176
177 var compile = function() {
178
179 if (reusableFiles && Array.isArray(reusableFiles.input)) {
180 var reuseExistingFile = reusableFiles.input.indexOf(file.path) > -1;
181 if (reuseExistingFile) {
182 var previousSha = existingShas.find(sha => sha.input === file.path);
183 if (previousSha) { shas.push(previousSha); }
184 eventEmitter.emit('compile-reuse', file);
185 previousSha.output.forEach(addFileToWhitelist);
186 return when.resolve(false);
187 }
188 }
189
190 eventEmitter.emit('compile-start', file);
191
192 return babyTolk.read(file.fullPath, babyTolkOptions).then(function(compiled) {
193 var relativePath = path.relative(inputDir, compiled.inputPath);
194 var writeFiles = [], fileNames = [];
195
196 var fileName = file.name;
197 var compiledOutputFullPath = outputFullPath;
198 var extensionChanged = false;
199 if (ext !== compiled.extension) {
200 extensionChanged = true;
201 compiledOutputFullPath = replaceExtension(compiledOutputFullPath, compiled.extension);
202 fileName = replaceExtension(file.name, compiled.extension);
203 }
204
205 if (compiled.sourcemap) {
206 var sourcemapStr = 'sourceMappingURL=' + fileName + '.map';
207
208 compiled.sourcemap.sources = compiled.sourcemap.sources
209 .map(source => path.resolve(process.cwd(), source))
210 .map(source => source.replace(inputDir, outputDir))
211 .map(source => path.relative(path.dirname(compiledOutputFullPath), source))
212 .map(source => extensionChanged ? source : addSrcExtension(source));
213
214 var srcMapFileName = compiledOutputFullPath + '.map';
215 writeFiles.push(fsp.writeFile(srcMapFileName, JSON.stringify(compiled.sourcemap)));
216 fileNames.push(path.relative(outputDir, srcMapFileName));
217 if (compiled.extension === '.css') {
218 /*# sourceMappingURL=screen.css.map */
219 compiled.result = compiled.result + '\n/*# ' + sourcemapStr + '*/';
220 } else if (compiled.extension === '.js') {
221 //# sourceMappingURL=script.js.map
222 compiled.result = compiled.result + '\n//# ' + sourcemapStr;
223 }
224 }
225
226 writeFiles.push(fsp.writeFile(compiledOutputFullPath, compiled.result));
227 fileNames.push(path.relative(outputDir, compiledOutputFullPath));
228
229 shas.push({
230 type: compiled.transformId,
231 inputSha: compiled.inputSha.map(x => ({
232 file: path.relative(inputDir, x.file),
233 sha: x.sha
234 })),
235 outputSha: createHash(compiled.result),
236 input: relativePath,
237 output: fileNames,
238 });
239 eventEmitter.emit('compile-done', file);
240 return when.all(writeFiles).then(() => {
241 fileNames.forEach(addFileToWhitelist);
242 });
243 });
244 }; // End compile Fn
245
246 var copy = function(renameToSrc) {
247 var rs = fs.createReadStream(file.fullPath);
248 var writePath = renameToSrc ? addSrcExtension(outputFullPath) : outputFullPath;
249 var ws = fs.createWriteStream(writePath);
250 addFileToWhitelist(path.relative(outputDir, writePath));
251 rs.pipe(ws).on('error', reject);
252 ws.on('finish', fileDone).on('error', reject);
253 };
254
255 if (options.compile && compileExt && doCompile) {
256 // compile
257 compile().then(() => options.sourcemaps ? copy() : fileDone(), reject);
258 } else if (babyTolk.isMinifiable(ext) && options.minify && doCompile) {
259 // minify
260 compile().then(() => options.sourcemaps ? copy(true) : fileDone(), () => copy());
261 } else { copy(); }
262 };
263
264
265 var dir = path.dirname(outputFullPath);
266 mkdirp(dir).then(processFile, reject);
267 }, function (next) {
268 resolve(filesCopied);
269 next();
270 })).on('error', reject);
271 }).then(filesCopied => {
272 eventEmitter.emit('cleaning-up');
273 return when.promise((resolve, reject) => {
274 readdirp({
275 root: outputDir,
276 fileFilter: file => fileWhitelist.indexOf(file.path) === -1
277 }).pipe(through.obj((file, _, next) => {
278 fs.unlink(file.fullPath);
279 next();
280 }, (finish) => {resolve(filesCopied); finish();})).on('error', reject);
281 });
282 });
283 };
284
285 var promise =
286 readAndValidateShas()
287 .then(compileAndCopy)
288 .then(numFiles =>
289 fsp.writeFile(shasPath, JSON.stringify(shas, null, 2)).then(() => numFiles)
290 )
291 // TODO: Don't delete dir after abort. Leave as is instead.
292 .catch(function(err) {
293 // If there was an error then back out, delete the output dir and forward
294 // the error up the promise chain
295 return rimraf(outputDir).then(function() {
296 return when.reject(err);
297 });
298 });
299
300 promise.abort = function() { abortRequested = true; };
301 promise.events = eventEmitter;
302 return promise;
303};
304
305module.exports.preflight = function preflight(dir) {
306 return Promise.all([
307 checkDir(dir),
308 checkDir(path.join(dir, 'node_modules')),
309 checkDir(path.join(dir, 'bower_components')),
310 ]).then(info => {
311 var mainDir = info[0];
312 var nodeModules = info[1];
313 var bowerComponents = info[2];
314 return Object.assign(
315 {},
316 mainDir,
317 { nodeModules: nodeModules.exists },
318 { bowerComponents: bowerComponents.exists }
319 );
320 });
321};