aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--index.js472
2 files changed, 472 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index 548b3c7..af15f97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,3 @@
2build/ 2build/
3node_modules/ 3node_modules/
4bower_components/ 4bower_components/
5/index.js
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..b06aed0
--- /dev/null
+++ b/index.js
@@ -0,0 +1,472 @@
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 && cache.warnings.length) {
65 compilation.warnings.unshift('PureScript compilation:\n' + cache.warnings.join(''));
66 }
67
68 if (cache.errors && cache.errors.length) {
69 compilation.errors.unshift('PureScript compilation:\n' + cache.errors.join('\n'));
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 if (options.bundle) {
93 cache.bundleModules.push(psModule.name);
94 }
95
96 if (cache.rebuild) {
97 return connectIdeServer(psModule).then(rebuild).then(toJavaScript).then(psModule.load).catch(psModule.reject);
98 }
99
100 if (cache.compilation && cache.compilation.length) {
101 return toJavaScript(psModule).then(psModule.load).catch(psModule.reject);
102 }
103
104 // We need to wait for compilation to finish before the loaders run so that
105 // references to compiled output are valid.
106 cache.deferred.push(psModule);
107
108 if (!cache.compilation) {
109 return compile(psModule).then(function () {
110 return Promise.map(cache.deferred, function (psModule) {
111 if (_typeof(cache.ideServer) === 'object') cache.ideServer.kill();
112 return toJavaScript(psModule).then(psModule.load);
113 });
114 }).catch(function (error) {
115 cache.deferred[0].reject(error);
116 cache.deferred.slice(1).forEach(function (psModule) {
117 return psModule.reject(true);
118 });
119 });
120 }
121};
122
123// The actual loader is executed *after* purescript compilation.
124function toJavaScript(psModule) {
125 var options = psModule.options;
126 var cache = psModule.cache;
127 var bundlePath = path.resolve(options.bundleOutput);
128 var jsPath = cache.bundle ? bundlePath : psModule.jsPath;
129
130 debug('loading JavaScript for', psModule.srcPath);
131
132 return Promise.props({
133 js: fs.readFileAsync(jsPath, 'utf8'),
134 psModuleMap: psModuleMap(options.src, cache)
135 }).then(function (result) {
136 var js = '';
137
138 if (options.bundle) {
139 // if bundling, return a reference to the bundle
140 js = 'module.exports = require("' + path.relative(psModule.srcDir, options.bundleOutput) + '")["' + psModule.name + '"]';
141 } else {
142 // replace require paths to output files generated by psc with paths
143 // to purescript sources, which are then also run through this loader.
144 var foreignRequire = 'require("' + path.resolve(path.join(psModule.options.output, psModule.name, 'foreign.js')) + '")';
145
146 js = result.js.replace(requireRegex, function (m, p1) {
147 return 'require("' + result.psModuleMap[p1] + '")';
148 }).replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire);
149 }
150
151 return js;
152 });
153}
154
155function compile(psModule) {
156 var options = psModule.options;
157 var cache = psModule.cache;
158 var stderr = [];
159
160 if (cache.compilation) return Promise.resolve(cache.compilation);
161
162 cache.compilation = [];
163 cache.warnings = [];
164 cache.errors = [];
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.stderr.on('data', function (data) {
180 return stderr.push(data.toString());
181 });
182
183 compilation.on('close', function (code) {
184 console.log('Finished compiling PureScript.');
185 if (code !== 0) {
186 cache.compilation = cache.errors = stderr;
187 reject(true);
188 } else {
189 cache.compilation = cache.warnings = stderr;
190 resolve(psModule);
191 }
192 });
193 }).then(function (compilerOutput) {
194 if (options.bundle) {
195 return bundle(options, cache).then(function () {
196 return psModule;
197 });
198 }
199 return psModule;
200 });
201}
202
203function rebuild(psModule) {
204 var options = psModule.options;
205 var cache = psModule.cache;
206
207 debug('attempting rebuild with psc-ide-client %s', psModule.srcPath);
208
209 var request = function request(body) {
210 return new Promise(function (resolve, reject) {
211 var args = dargs(options.pscIdeArgs);
212 var ideClient = spawn('psc-ide-client', args);
213
214 ideClient.stdout.once('data', function (data) {
215 var res = null;
216
217 try {
218 res = JSON.parse(data.toString());
219 debug(res);
220 } catch (err) {
221 return reject(err);
222 }
223
224 if (res && !Array.isArray(res.result)) {
225 return res.resultType === 'success' ? resolve(psModule) : reject('psc-ide rebuild failed');
226 }
227
228 Promise.map(res.result, function (item, i) {
229 debug(item);
230 return formatIdeResult(item, options, i, res.result.length);
231 }).then(function (compileMessages) {
232 if (res.resultType === 'error') {
233 if (res.result.some(function (item) {
234 return item.errorCode === 'UnknownModule';
235 })) {
236 console.log('Unknown module, attempting full recompile');
237 return compile(psModule).then(function () {
238 return request({ command: 'load' });
239 }).then(resolve).catch(function () {
240 return reject('psc-ide rebuild failed');
241 });
242 }
243 cache.errors = compileMessages;
244 reject('psc-ide rebuild failed');
245 } else {
246 cache.warnings = compileMessages;
247 resolve(psModule);
248 }
249 });
250 });
251
252 ideClient.stderr.once('data', function (data) {
253 return reject(data.toString());
254 });
255
256 ideClient.stdin.write(JSON.stringify(body));
257 ideClient.stdin.write('\n');
258 });
259 };
260
261 return request({
262 command: 'rebuild',
263 params: {
264 file: psModule.srcPath
265 }
266 });
267}
268
269function formatIdeResult(result, options, index, length) {
270 var srcPath = path.relative(options.context, result.filename);
271 var pos = result.position;
272 var fileAndPos = srcPath + ':' + pos.startLine + ':' + pos.startColumn;
273 var numAndErr = '[' + (index + 1) + '/' + length + ' ' + result.errorCode + ']';
274 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr;
275
276 return fs.readFileAsync(result.filename, 'utf8').then(function (source) {
277 var lines = source.split('\n').slice(pos.startLine - 1, pos.endLine);
278 var endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine;
279 var up = options.pscIdeColors ? colors.red('^') : '^';
280 var down = options.pscIdeColors ? colors.red('v') : 'v';
281 var trimmed = lines.slice(0);
282
283 if (endsOnNewline) {
284 lines.splice(lines.length - 1, 1);
285 pos.endLine = pos.endLine - 1;
286 pos.endColumn = lines[lines.length - 1].length || 1;
287 }
288
289 // strip newlines at the end
290 if (endsOnNewline) {
291 trimmed = lines.reverse().reduce(function (trimmed, line, i) {
292 if (i === 0 && line === '') trimmed.trimming = true;
293 if (!trimmed.trimming) trimmed.push(line);
294 if (trimmed.trimming && line !== '') {
295 trimmed.trimming = false;
296 trimmed.push(line);
297 }
298 return trimmed;
299 }, []).reverse();
300 pos.endLine = pos.endLine - (lines.length - trimmed.length);
301 pos.endColumn = trimmed[trimmed.length - 1].length || 1;
302 }
303
304 var spaces = ' '.repeat(String(pos.endLine).length);
305 var snippet = trimmed.map(function (line, i) {
306 return ' ' + (pos.startLine + i) + ' ' + line;
307 }).join('\n');
308
309 if (trimmed.length === 1) {
310 snippet += '\n ' + spaces + ' ' + ' '.repeat(pos.startColumn - 1) + up.repeat(pos.endColumn - pos.startColumn + 1);
311 } else {
312 snippet = ' ' + spaces + ' ' + ' '.repeat(pos.startColumn - 1) + down + '\n' + snippet;
313 snippet += '\n ' + spaces + ' ' + ' '.repeat(pos.endColumn - 1) + up;
314 }
315
316 return Promise.resolve('\n' + numAndErr + ' ' + fileAndPos + '\n\n' + snippet + '\n\n' + result.message);
317 });
318}
319
320function bundle(options, cache) {
321 if (cache.bundle) return Promise.resolve(cache.bundle);
322
323 var stdout = [];
324 var stderr = cache.bundle = [];
325
326 var args = dargs(Object.assign({
327 _: [path.join(options.output, '*', '*.js')],
328 output: options.bundleOutput,
329 namespace: options.bundleNamespace
330 }, options.pscBundleArgs));
331
332 cache.bundleModules.forEach(function (name) {
333 return args.push('--module', name);
334 });
335
336 debug('spawning bundler %s %o', options.pscBundle, args.join(' '));
337
338 return new Promise(function (resolve, reject) {
339 console.log('Bundling PureScript...');
340
341 var compilation = spawn(options.pscBundle, args);
342
343 compilation.stdout.on('data', function (data) {
344 return stdout.push(data.toString());
345 });
346 compilation.stderr.on('data', function (data) {
347 return stderr.push(data.toString());
348 });
349 compilation.on('close', function (code) {
350 if (code !== 0) {
351 cache.errors.concat(stderr);
352 return reject(true);
353 }
354 cache.bundle = stderr;
355 resolve(fs.appendFileAsync('output/bundle.js', 'module.exports = ' + options.bundleNamespace));
356 });
357 });
358}
359
360// map of PS module names to their source path
361function psModuleMap(globs, cache) {
362 if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap);
363
364 return globby(globs).then(function (paths) {
365 return Promise.props(paths.reduce(function (map, file) {
366 map[file] = fs.readFileAsync(file, 'utf8');
367 return map;
368 }, {})).then(function (srcMap) {
369 cache.psModuleMap = Object.keys(srcMap).reduce(function (map, file) {
370 var source = srcMap[file];
371 var psModuleName = match(psModuleRegex, source);
372 map[psModuleName] = path.resolve(file);
373 return map;
374 }, {});
375 return cache.psModuleMap;
376 });
377 });
378}
379
380function connectIdeServer(psModule) {
381 var options = psModule.options;
382 var cache = psModule.cache;
383
384 if (cache.ideServer) return Promise.resolve(psModule);
385
386 cache.ideServer = true;
387
388 var connect = function connect() {
389 return new Promise(function (resolve, reject) {
390 var args = dargs(options.pscIdeArgs);
391
392 debug('attempting to connect to psc-ide-server', args);
393
394 var ideClient = spawn('psc-ide-client', args);
395
396 ideClient.stderr.on('data', function (data) {
397 debug(data.toString());
398 cache.ideServer = false;
399 reject(true);
400 });
401 ideClient.stdout.once('data', function (data) {
402 debug(data.toString());
403 if (data.toString()[0] === '{') {
404 var res = JSON.parse(data.toString());
405 if (res.resultType === 'success') {
406 cache.ideServer = ideServer;
407 resolve(psModule);
408 } else {
409 cache.ideServer = ideServer;
410 reject(true);
411 }
412 } else {
413 cache.ideServer = false;
414 reject(true);
415 }
416 });
417 ideClient.stdin.resume();
418 ideClient.stdin.write(JSON.stringify({ command: 'load' }));
419 ideClient.stdin.write('\n');
420 });
421 };
422
423 var args = dargs(Object.assign({
424 outputDirectory: options.output
425 }, options.pscIdeArgs));
426
427 debug('attempting to start psc-ide-server', args);
428
429 var ideServer = cache.ideServer = spawn('psc-ide-server', []);
430 ideServer.stderr.on('data', function (data) {
431 debug(data.toString());
432 });
433
434 return retryPromise(function (retry, number) {
435 return connect().catch(function (error) {
436 if (!cache.ideServer && number === 9) {
437 debug(error);
438
439 console.log('failed to connect to or start psc-ide-server, ' + 'full compilation will occur on rebuild');
440
441 return Promise.resolve(psModule);
442 }
443
444 return retry(error);
445 });
446 }, {
447 retries: 9,
448 factor: 1,
449 minTimeout: 333,
450 maxTimeout: 333
451 });
452}
453
454function match(regex, str) {
455 var matches = str.match(regex);
456 return matches && matches[1];
457}
458
459function dargs(obj) {
460 return Object.keys(obj).reduce(function (args, key) {
461 var arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
462 var val = obj[key];
463
464 if (key === '_') val.forEach(function (v) {
465 return args.push(v);
466 });else if (Array.isArray(val)) val.forEach(function (v) {
467 return args.push(arg, v);
468 });else args.push(arg, obj[key]);
469
470 return args;
471 }, []);
472}