1 | 'use strict';
|
2 |
|
3 | var babyTolk = require('baby-tolk');
|
4 | var readdirp = require('readdirp');
|
5 | var through = require('through2');
|
6 | var path = require('path');
|
7 | var pathCompleteExtname = require('path-complete-extname');
|
8 | var when = require('when');
|
9 | var fs = require('fs');
|
10 | var mm = require('micromatch');
|
11 | var events = require('events');
|
12 | var checkDir = require('checkdir');
|
13 | var nodefn = require('when/node');
|
14 | var crypto = require('crypto');
|
15 | var rimraf = nodefn.lift(require('rimraf'));
|
16 | var mkdirp = nodefn.lift(require('mkdirp'));
|
17 | var fsp = nodefn.liftAll(fs);
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | var extname = function(filePath) {
|
23 | var ext = path.extname(filePath);
|
24 | if (babyTolk.targetExtensionMap[ext]) { return ext; }
|
25 | return pathCompleteExtname(filePath);
|
26 | };
|
27 |
|
28 | var replaceExtension = function(filePath, newExtension) {
|
29 | var ext = extname(filePath);
|
30 | return filePath.slice(0,-(ext.length)) + newExtension;
|
31 | };
|
32 |
|
33 | var addSrcExtension = function(filePath) {
|
34 | var ext = extname(filePath);
|
35 | return replaceExtension(filePath, '.src' + ext);
|
36 | };
|
37 |
|
38 | var 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 |
|
60 | var createHash = function(data) {
|
61 | var shasum = crypto.createHash('sha1');
|
62 | shasum.update(data);
|
63 | return shasum.digest('hex');
|
64 | };
|
65 |
|
66 | var noop = () => null;
|
67 |
|
68 | module.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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
141 | output: [].concat.apply([], filtered.map(x => x.output))
|
142 | };
|
143 | }, noop
|
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 |
|
219 | compiled.result = compiled.result + '\n/*# ' + sourcemapStr + '*/';
|
220 | } else if (compiled.extension === '.js') {
|
221 |
|
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 | };
|
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 |
|
257 | compile().then(() => options.sourcemaps ? copy() : fileDone(), reject);
|
258 | } else if (babyTolk.isMinifiable(ext) && options.minify && doCompile) {
|
259 |
|
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 |
|
292 | .catch(function(err) {
|
293 |
|
294 |
|
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 |
|
305 | module.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 | };
|