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