1 | const { execSync } = require('child_process');
|
2 | const path = require('path');
|
3 | const { stat, readFile: readFileCallback, unlink } = require('fs');
|
4 | const babel = require('babel-core');
|
5 | const chokidar = require('chokidar');
|
6 | const glob = require('glob');
|
7 | const readdir = require('fs-readdir-recursive');
|
8 | const slash = require('slash');
|
9 | const Lock = require('lock');
|
10 | const Queue = require('promise-queue');
|
11 | const promiseCallback = require('promise-callback-factory').default;
|
12 | const Logger = require('nightingale').default;
|
13 | const copyChmod = require('./utils/copyChmod');
|
14 | const copyFile = require('./utils/copyFile');
|
15 | const writeFile = require('./utils/writeFile');
|
16 | const destFromSrc = require('./utils/destFromSrc');
|
17 | const plugins = require('./plugins');
|
18 | const createBabelOptions = require(`./babel-options`);
|
19 | const { logger: parentLogger } = require('./logger');
|
20 | const Task = require('./cli-spinner');
|
21 |
|
22 | const readFile = filepath => promiseCallback(done => readFileCallback(filepath, done));
|
23 |
|
24 |
|
25 | const queue = new Queue(40, Infinity);
|
26 |
|
27 | function toErrorStack(err) {
|
28 | if (err._babel && err instanceof SyntaxError) {
|
29 | return `${err.name}: ${err.message}\n${err.codeFrame}`;
|
30 | } else {
|
31 | return err.stack;
|
32 | }
|
33 | }
|
34 |
|
35 | module.exports = function transpile(pobrc, cwd, src, outFn, envs, watch, options) {
|
36 | const srcFiles = glob.sync(src, { cwd });
|
37 | const _lock = Lock();
|
38 | const lock = resource => new Promise(resolve => _lock(resource, release => resolve(() => release()())));
|
39 | let task = new Task(`build ${src}`);
|
40 |
|
41 | let logger = parentLogger.child('build', 'build');
|
42 | const watchLogger = parentLogger.child('watch', 'watch');
|
43 | let watching = false;
|
44 |
|
45 | const timeBuildStarted = Date.now();
|
46 | logger.debug('envs', { envs });
|
47 |
|
48 | const optsManagers = {};
|
49 |
|
50 | envs.forEach(env => {
|
51 | const optsManager = new babel.OptionManager;
|
52 |
|
53 | optsManager.mergeOptions({
|
54 | options: createBabelOptions(env, pobrc.react, options),
|
55 | alias: 'base',
|
56 | loc: cwd,
|
57 | });
|
58 |
|
59 | optsManagers[env] = optsManager;
|
60 | });
|
61 |
|
62 |
|
63 | function handle(filename) {
|
64 | return promiseCallback(done => stat(filename, done))
|
65 | .catch(err => {
|
66 | console.log(err);
|
67 | process.exit(1);
|
68 | })
|
69 | .then(stat => {
|
70 | if (stat.isDirectory(filename)) {
|
71 | let dirname = filename;
|
72 |
|
73 | const allSrcFiles = readdir(dirname);
|
74 | const allAllowedDestFiles = allSrcFiles.map(relative => destFromSrc(relative));
|
75 |
|
76 | envs.forEach(env => {
|
77 | const out = outFn(env);
|
78 | const envAllFiles = readdir(out);
|
79 |
|
80 | const diff = envAllFiles.filter(path => !allAllowedDestFiles.includes(path.replace(/.map$/, '')));
|
81 | if (diff.length) {
|
82 | logger.debug(`${out}: removing: ${diff.join(',')}`);
|
83 | execSync('rm -Rf ' + diff.map(filename => path.join(out, filename)).join(' '));
|
84 | }
|
85 | });
|
86 |
|
87 | return Promise.all(allSrcFiles.map(filename => {
|
88 | let src = path.join(dirname, filename);
|
89 | return handleFile(src, filename);
|
90 | }));
|
91 | } else {
|
92 | }
|
93 | });
|
94 | }
|
95 |
|
96 | function handleFile(src, relative) {
|
97 | if (_lock.isLocked(relative)) logger.debug(relative + ' locked, waiting...');
|
98 | return lock(relative).then(release => {
|
99 | return queue.add(() => {
|
100 | if (babel.util.canCompile(relative, options && options.babelExtensions)) {
|
101 | const subtask = task.subtask('compiling: ' + relative);
|
102 | logger.debug('compiling: ' + relative);
|
103 |
|
104 | return Promise.resolve(src)
|
105 | .then(src => readFile(src))
|
106 | .then(content => {
|
107 | return Promise.all(envs.map(env => {
|
108 | const dest = path.join(outFn(env), destFromSrc(relative));
|
109 |
|
110 | const opts = optsManagers[env].init({ filename: relative });
|
111 | opts.babelrc = false;
|
112 | opts.sourceMap = true;
|
113 | opts.sourceFileName = slash(path.relative(dest + "/..", src));
|
114 | opts.sourceMapTarget = path.basename(relative);
|
115 |
|
116 | return Promise.resolve(content)
|
117 | .then(content => babel.transform(content, opts))
|
118 | .catch(watch && (err => {
|
119 | console.log(toErrorStack(err));
|
120 |
|
121 | return { map: null, code: 'throw new Error("Syntax Error");' };
|
122 | }))
|
123 | .then(data => {
|
124 | const mapLoc = dest + ".map";
|
125 | data.code = data.code + "\n//# sourceMappingURL=" + path.basename(mapLoc);
|
126 | return writeFile(dest, data.code)
|
127 | .then(() => Promise.all([
|
128 | copyChmod(src, dest),
|
129 | writeFile(mapLoc, JSON.stringify(data.map)),
|
130 | ]));
|
131 | });
|
132 | }));
|
133 | })
|
134 | .then(() => {
|
135 | logger[watching ? 'success' : 'debug']('compiled: ' + relative);
|
136 | })
|
137 | .then(() => release(), err => { release(); throw err; })
|
138 | .then(() => subtask.done(), err => { subtask.done(); throw err; });
|
139 | } else {
|
140 | const extension = path.extname(relative).substr(1);
|
141 | const plugin = plugins.findByExtension(extension);
|
142 | if (plugin) {
|
143 | const subtask = task.subtask(plugin.extension + ': ' + relative);
|
144 | logger.debug(plugin.extension + ': ' + relative);
|
145 | const destRelative = destFromSrc(relative, plugin);
|
146 | return Promise.resolve(src)
|
147 | .then(src => readFile(src))
|
148 | .then(content => plugin.transform(content, { src, relative }))
|
149 | .catch(watch && (err => {
|
150 | console.log(toErrorStack(err));
|
151 |
|
152 | return { map: null, code: 'throw new Error("Syntax Error");' };
|
153 | }))
|
154 | .then(result => {
|
155 | if (!result) {
|
156 |
|
157 | return Promise.all(envs.map(env => (
|
158 | Promise.all([
|
159 | promiseCallback(done => unlink(dest, done)).catch(() => {}),
|
160 | promiseCallback(done => unlink(`${dest}.map`, done)).catch(() => {}),
|
161 | ])
|
162 | )));
|
163 | }
|
164 |
|
165 | const { code, map } = result;
|
166 | return Promise.all(envs.map(env => {
|
167 | const dest = path.join(outFn(env), destRelative);
|
168 | const mapLoc = dest + ".map";
|
169 | return writeFile(dest, code)
|
170 | .then(() => Promise.all([
|
171 | copyChmod(src, dest),
|
172 | map && writeFile(mapLoc, JSON.stringify(map)),
|
173 | ]));
|
174 | }))
|
175 | })
|
176 | .then(() => release(), err => { release(); throw err; })
|
177 | .then(() => subtask.done(), err => { subtask.done(); throw err; });
|
178 | } else {
|
179 | const subtask = task.subtask('copy: ' + relative);
|
180 | logger.debug('copy: ' + relative);
|
181 | return Promise.all(envs.map(env => {
|
182 | const out = outFn(env);
|
183 | const dest = path.join(out, relative);
|
184 | return copyFile(src, dest).then(() => copyChmod(src, dest));
|
185 | }))
|
186 | .then(() => release(), err => { release(); throw err; })
|
187 | .then(() => subtask.done(), err => { subtask.done(); throw err; });
|
188 | }
|
189 | }
|
190 | });
|
191 | }).catch(err => {
|
192 | console.log(toErrorStack(err));
|
193 | if (!watch) {
|
194 | process.exit(1);
|
195 | }
|
196 | });
|
197 | }
|
198 |
|
199 | if (watch) {
|
200 | process.nextTick(() => {
|
201 | srcFiles.forEach(dirname => {
|
202 | const watcher = chokidar.watch(dirname, {
|
203 | persistent: true,
|
204 | ignoreInitial: true
|
205 | });
|
206 |
|
207 | function handleChange(filename) {
|
208 | let relative = path.relative(dirname, filename) || filename;
|
209 | watchLogger.debug('changed: ' + relative);
|
210 | task.subtask('changed: ' + relative);
|
211 | handleFile(filename, relative)
|
212 | .catch(err => {
|
213 | console.log(err.stack);
|
214 | })
|
215 | .then(() => watch.emit('changed', filename));
|
216 | }
|
217 |
|
218 | watcher.on('add', handleChange);
|
219 | watcher.on('change', handleChange);
|
220 | watcher.on('unlink', filename => {
|
221 | let relative = path.relative(dirname, filename) || filename;
|
222 | watchLogger.debug('unlink: ' + relative);
|
223 | const subtask = task.subtask('delete: ' + relative);
|
224 | if (_lock.isLocked(relative)) watchLogger.debug(relative + ' locked, waiting...');
|
225 | lock(relative).then(release => {
|
226 | return Promise.all(envs.map(env => {
|
227 | const dest = path.join(outFn(env), destFromSrc(relative));
|
228 |
|
229 | return Promise.all([
|
230 | promiseCallback(done => unlink(dest, done)).catch(() => {}),
|
231 | promiseCallback(done => unlink(`${dest}.map`, done)).catch(() => {}),
|
232 | ]);
|
233 | }))
|
234 | .then(() => release())
|
235 | .then(() => watch.emit('changed', filename))
|
236 | .then(() => subtask.done());
|
237 | });
|
238 | });
|
239 | });
|
240 | });
|
241 | }
|
242 |
|
243 | return Promise.all(srcFiles.map(filename => handle(filename)))
|
244 | .then(() => {
|
245 | logger.infoSuccessTimeEnd(timeBuildStarted, 'build finished');
|
246 | task.succeed();
|
247 | if (watch) {
|
248 | task = new Task('watch');
|
249 | logger.info('watching');
|
250 | watching = true;
|
251 | logger = watchLogger;
|
252 | }
|
253 | })
|
254 | .catch(err => {
|
255 | console.log(err.stack);
|
256 | process.exit(1);
|
257 | });
|
258 | };
|