aboutsummaryrefslogtreecommitdiffhomepage
path: root/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'index.js')
-rw-r--r--index.js478
1 files changed, 0 insertions, 478 deletions
diff --git a/index.js b/index.js
deleted file mode 100644
index 5f41361..0000000
--- a/index.js
+++ /dev/null
@@ -1,478 +0,0 @@
1'use strict';
2
3var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
4
5var colors = require('chalk');
6var debug = require('debug')('purs-loader');
7var loaderUtils = require('loader-utils');
8var globby = require('globby');
9var Promise = require('bluebird');
10var fs = Promise.promisifyAll(require('fs'));
11var spawn = require('child_process').spawn;
12var path = require('path');
13var retryPromise = require('promise-retry');
14
15var psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i;
16var requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g;
17
18module.exports = function purescriptLoader(source, map) {
19 var callback = this.async();
20 var config = this.options;
21 var query = loaderUtils.parseQuery(this.query);
22 var webpackOptions = this.options.purescriptLoader || {};
23
24 var options = Object.assign({
25 context: config.context,
26 psc: 'psc',
27 pscArgs: {},
28 pscBundle: 'psc-bundle',
29 pscBundleArgs: {},
30 pscIde: false,
31 pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa',
32 pscIdeArgs: {},
33 bundleOutput: 'output/bundle.js',
34 bundleNamespace: 'PS',
35 bundle: false,
36 warnings: true,
37 output: 'output',
38 src: [path.join('src', '**', '*.purs'), path.join('bower_components', 'purescript-*', 'src', '**', '*.purs')],
39 ffi: [path.join('src', '**', '*.js'), path.join('bower_components', 'purescript-*', 'src', '**', '*.js')]
40 }, webpackOptions, query);
41
42 this.cacheable && this.cacheable();
43
44 var cache = config.purescriptLoaderCache = config.purescriptLoaderCache || {
45 rebuild: false,
46 deferred: [],
47 bundleModules: []
48 };
49
50 if (!config.purescriptLoaderInstalled) {
51 config.purescriptLoaderInstalled = true;
52
53 // invalidate loader cache when bundle is marked as invalid (in watch mode)
54 this._compiler.plugin('invalid', function () {
55 cache = config.purescriptLoaderCache = {
56 rebuild: options.pscIde,
57 deferred: [],
58 ideServer: cache.ideServer
59 };
60 });
61
62 // add psc warnings to webpack compilation warnings
63 this._compiler.plugin('after-compile', function (compilation, callback) {
64 if (options.warnings && cache.warnings) {
65 compilation.warnings.unshift('PureScript compilation:\n' + cache.warnings);
66 }
67
68 if (cache.errors) {
69 compilation.errors.unshift('PureScript compilation:\n' + cache.errors);
70 }
71
72 callback();
73 });
74 }
75
76 var psModuleName = match(psModuleRegex, source);
77 var psModule = {
78 name: psModuleName,
79 load: function load(js) {
80 return callback(null, js);
81 },
82 reject: function reject(error) {
83 return callback(error);
84 },
85 srcPath: this.resourcePath,
86 srcDir: path.dirname(this.resourcePath),
87 jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')),
88 options: options,
89 cache: cache
90 };
91
92 debug('loader called', psModule.name);
93
94 if (options.bundle) {
95 cache.bundleModules.push(psModule.name);
96 }
97
98 if (cache.rebuild) {
99 return connectIdeServer(psModule).then(rebuild).then(toJavaScript).then(psModule.load).catch(psModule.reject);
100 }
101
102 if (cache.compilationFinished) {
103 return toJavaScript(psModule).then(psModule.load).catch(psModule.reject);
104 }
105
106 // We need to wait for compilation to finish before the loaders run so that
107 // references to compiled output are valid.
108 cache.deferred.push(psModule);
109
110 if (!cache.compilationStarted) {
111 return compile(psModule).then(function () {
112 return Promise.map(cache.deferred, function (psModule) {
113 if (_typeof(cache.ideServer) === 'object') cache.ideServer.kill();
114 return toJavaScript(psModule).then(psModule.load);
115 });
116 }).catch(function (error) {
117 cache.deferred[0].reject(error);
118 cache.deferred.slice(1).forEach(function (psModule) {
119 return psModule.reject(true);
120 });
121 });
122 }
123};
124
125// The actual loader is executed *after* purescript compilation.
126function toJavaScript(psModule) {
127 var options = psModule.options;
128 var cache = psModule.cache;
129 var bundlePath = path.resolve(options.bundleOutput);
130 var jsPath = cache.bundle ? bundlePath : psModule.jsPath;
131
132 debug('loading JavaScript for', psModule.name);
133
134 return Promise.props({
135 js: fs.readFileAsync(jsPath, 'utf8'),
136 psModuleMap: psModuleMap(options.src, cache)
137 }).then(function (result) {
138 var js = '';
139
140 if (options.bundle) {
141 // if bundling, return a reference to the bundle
142 js = 'module.exports = require("' + path.relative(psModule.srcDir, options.bundleOutput) + '")["' + psModule.name + '"]';
143 } else {
144 // replace require paths to output files generated by psc with paths
145 // to purescript sources, which are then also run through this loader.
146 var foreignRequire = 'require("' + path.resolve(path.join(psModule.options.output, psModule.name, 'foreign.js')) + '")';
147
148 js = result.js.replace(requireRegex, function (m, p1) {
149 return 'require("' + result.psModuleMap[p1] + '")';
150 }).replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire);
151 }
152
153 return js;
154 });
155}
156
157function compile(psModule) {
158 var options = psModule.options;
159 var cache = psModule.cache;
160 var stderr = [];
161
162 if (cache.compilationStarted) return Promise.resolve(psModule);
163
164 cache.compilationStarted = true;
165
166 var args = dargs(Object.assign({
167 _: options.src,
168 ffi: options.ffi,
169 output: options.output
170 }, options.pscArgs));
171
172 debug('spawning compiler %s %o', options.psc, args);
173
174 return new Promise(function (resolve, reject) {
175 console.log('\nCompiling PureScript...');
176
177 var compilation = spawn(options.psc, args);
178
179 compilation.stdout.on('data', function (data) {
180 return stderr.push(data.toString());
181 });
182 compilation.stderr.on('data', function (data) {
183 return stderr.push(data.toString());
184 });
185
186 compilation.on('close', function (code) {
187 console.log('Finished compiling PureScript.');
188 cache.compilationFinished = true;
189 if (code !== 0) {
190 cache.errors = stderr.join('');
191 reject(true);
192 } else {
193 cache.warnings = stderr.join('');
194 resolve(psModule);
195 }
196 });
197 }).then(function (compilerOutput) {
198 if (options.bundle) {
199 return bundle(options, cache).then(function () {
200 return psModule;
201 });
202 }
203 return psModule;
204 });
205}
206
207function rebuild(psModule) {
208 var options = psModule.options;
209 var cache = psModule.cache;
210
211 debug('attempting rebuild with psc-ide-client %s', psModule.srcPath);
212
213 var request = function request(body) {
214 return new Promise(function (resolve, reject) {
215 var args = dargs(options.pscIdeArgs);
216 var ideClient = spawn('psc-ide-client', args);
217
218 ideClient.stdout.once('data', function (data) {
219 var res = null;
220
221 try {
222 res = JSON.parse(data.toString());
223 debug(res);
224 } catch (err) {
225 return reject(err);
226 }
227
228 if (res && !Array.isArray(res.result)) {
229 return res.resultType === 'success' ? resolve(psModule) : reject('psc-ide rebuild failed');
230 }
231
232 Promise.map(res.result, function (item, i) {
233 debug(item);
234 return formatIdeResult(item, options, i, res.result.length);
235 }).then(function (compileMessages) {
236 if (res.resultType === 'error') {
237 if (res.result.some(function (item) {
238 return item.errorCode === 'UnknownModule';
239 })) {
240 console.log('Unknown module, attempting full recompile');
241 return compile(psModule).then(function () {
242 return request({ command: 'load' });
243 }).then(resolve).catch(function () {
244 return reject('psc-ide rebuild failed');
245 });
246 }
247 cache.errors = compileMessages.join('\n');
248 reject('psc-ide rebuild failed');
249 } else {
250 cache.warnings = compileMessages.join('\n');
251 resolve(psModule);
252 }
253 });
254 });
255
256 ideClient.stderr.once('data', function (data) {
257 return reject(data.toString());
258 });
259
260 ideClient.stdin.write(JSON.stringify(body));
261 ideClient.stdin.write('\n');
262 });
263 };
264
265 return request({
266 command: 'rebuild',
267 params: {
268 file: psModule.srcPath
269 }
270 });
271}
272
273function formatIdeResult(result, options, index, length) {
274 var srcPath = path.relative(options.context, result.filename);
275 var pos = result.position;
276 var fileAndPos = srcPath + ':' + pos.startLine + ':' + pos.startColumn;
277 var numAndErr = '[' + (index + 1) + '/' + length + ' ' + result.errorCode + ']';
278 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr;
279
280 return fs.readFileAsync(result.filename, 'utf8').then(function (source) {
281 var lines = source.split('\n').slice(pos.startLine - 1, pos.endLine);
282 var endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine;
283 var up = options.pscIdeColors ? colors.red('^') : '^';
284 var down = options.pscIdeColors ? colors.red('v') : 'v';
285 var trimmed = lines.slice(0);
286
287 if (endsOnNewline) {
288 lines.splice(lines.length - 1, 1);
289 pos.endLine = pos.endLine - 1;
290 pos.endColumn = lines[lines.length - 1].length || 1;
291 }
292
293 // strip newlines at the end
294 if (endsOnNewline) {
295 trimmed = lines.reverse().reduce(function (trimmed, line, i) {
296 if (i === 0 && line === '') trimmed.trimming = true;
297 if (!trimmed.trimming) trimmed.push(line);
298 if (trimmed.trimming && line !== '') {
299 trimmed.trimming = false;
300 trimmed.push(line);
301 }
302 return trimmed;
303 }, []).reverse();
304 pos.endLine = pos.endLine - (lines.length - trimmed.length);
305 pos.endColumn = trimmed[trimmed.length - 1].length || 1;
306 }
307
308 var spaces = ' '.repeat(String(pos.endLine).length);
309 var snippet = trimmed.map(function (line, i) {
310 return ' ' + (pos.startLine + i) + ' ' + line;
311 }).join('\n');
312
313 if (trimmed.length === 1) {
314 snippet += '\n ' + spaces + ' ' + ' '.repeat(pos.startColumn - 1) + up.repeat(pos.endColumn - pos.startColumn + 1);
315 } else {
316 snippet = ' ' + spaces + ' ' + ' '.repeat(pos.startColumn - 1) + down + '\n' + snippet;
317 snippet += '\n ' + spaces + ' ' + ' '.repeat(pos.endColumn - 1) + up;
318 }
319
320 return Promise.resolve('\n' + numAndErr + ' ' + fileAndPos + '\n\n' + snippet + '\n\n' + result.message);
321 });
322}
323
324function bundle(options, cache) {
325 if (cache.bundle) return Promise.resolve(cache.bundle);
326
327 var stdout = [];
328 var stderr = cache.bundle = [];
329
330 var args = dargs(Object.assign({
331 _: [path.join(options.output, '*', '*.js')],
332 output: options.bundleOutput,
333 namespace: options.bundleNamespace
334 }, options.pscBundleArgs));
335
336 cache.bundleModules.forEach(function (name) {
337 return args.push('--module', name);
338 });
339
340 debug('spawning bundler %s %o', options.pscBundle, args.join(' '));
341
342 return new Promise(function (resolve, reject) {
343 console.log('Bundling PureScript...');
344
345 var compilation = spawn(options.pscBundle, args);
346
347 compilation.stdout.on('data', function (data) {
348 return stdout.push(data.toString());
349 });
350 compilation.stderr.on('data', function (data) {
351 return stderr.push(data.toString());
352 });
353 compilation.on('close', function (code) {
354 if (code !== 0) {
355 cache.errors = (cache.errors || '') + stderr.join('');
356 return reject(true);
357 }
358 cache.bundle = stderr;
359 resolve(fs.appendFileAsync('output/bundle.js', 'module.exports = ' + options.bundleNamespace));
360 });
361 });
362}
363
364// map of PS module names to their source path
365function psModuleMap(globs, cache) {
366 if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap);
367
368 return globby(globs).then(function (paths) {
369 return Promise.props(paths.reduce(function (map, file) {
370 map[file] = fs.readFileAsync(file, 'utf8');
371 return map;
372 }, {})).then(function (srcMap) {
373 cache.psModuleMap = Object.keys(srcMap).reduce(function (map, file) {
374 var source = srcMap[file];
375 var psModuleName = match(psModuleRegex, source);
376 map[psModuleName] = path.resolve(file);
377 return map;
378 }, {});
379 return cache.psModuleMap;
380 });
381 });
382}
383
384function connectIdeServer(psModule) {
385 var options = psModule.options;
386 var cache = psModule.cache;
387
388 if (cache.ideServer) return Promise.resolve(psModule);
389
390 cache.ideServer = true;
391
392 var connect = function connect() {
393 return new Promise(function (resolve, reject) {
394 var args = dargs(options.pscIdeArgs);
395
396 debug('attempting to connect to psc-ide-server', args);
397
398 var ideClient = spawn('psc-ide-client', args);
399
400 ideClient.stderr.on('data', function (data) {
401 debug(data.toString());
402 cache.ideServer = false;
403 reject(true);
404 });
405 ideClient.stdout.once('data', function (data) {
406 debug(data.toString());
407 if (data.toString()[0] === '{') {
408 var res = JSON.parse(data.toString());
409 if (res.resultType === 'success') {
410 cache.ideServer = ideServer;
411 resolve(psModule);
412 } else {
413 cache.ideServer = ideServer;
414 reject(true);
415 }
416 } else {
417 cache.ideServer = false;
418 reject(true);
419 }
420 });
421 ideClient.stdin.resume();
422 ideClient.stdin.write(JSON.stringify({ command: 'load' }));
423 ideClient.stdin.write('\n');
424 });
425 };
426
427 var args = dargs(Object.assign({
428 outputDirectory: options.output
429 }, options.pscIdeArgs));
430
431 debug('attempting to start psc-ide-server', args);
432
433 var ideServer = cache.ideServer = spawn('psc-ide-server', []);
434 ideServer.stderr.on('data', function (data) {
435 debug(data.toString());
436 });
437
438 return retryPromise(function (retry, number) {
439 return connect().catch(function (error) {
440 if (!cache.ideServer && number === 9) {
441 debug(error);
442
443 console.log('failed to connect to or start psc-ide-server, ' + 'full compilation will occur on rebuild');
444
445 return Promise.resolve(psModule);
446 }
447
448 return retry(error);
449 });
450 }, {
451 retries: 9,
452 factor: 1,
453 minTimeout: 333,
454 maxTimeout: 333
455 });
456}
457
458function match(regex, str) {
459 var matches = str.match(regex);
460 return matches && matches[1];
461}
462
463function dargs(obj) {
464 return Object.keys(obj).reduce(function (args, key) {
465 var arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
466 var val = obj[key];
467
468 if (key === '_') val.forEach(function (v) {
469 return args.push(v);
470 });else if (Array.isArray(val)) val.forEach(function (v) {
471 return args.push(arg, v);
472 });else args.push(arg, obj[key]);
473
474 return args.filter(function (arg) {
475 return typeof arg !== 'boolean';
476 });
477 }, []);
478}