]> git.immae.eu Git - github/fretlink/purs-loader.git/blob - src/index.js
Annotate errors to prevent redundant parsing by other loaders
[github/fretlink/purs-loader.git] / src / index.js
1 'use strict'
2
3 const debug_ = require('debug');
4
5 const debug = debug_('purs-loader');
6
7 const debugVerbose = debug_('purs-loader:verbose');
8
9 const loaderUtils = require('loader-utils')
10
11 const Promise = require('bluebird')
12
13 const path = require('path')
14
15 const PsModuleMap = require('./purs-module-map');
16
17 const compile = require('./compile');
18
19 const bundle = require('./bundle');
20
21 const ide = require('./ide');
22
23 const toJavaScript = require('./to-javascript');
24
25 const sourceMaps = require('./source-maps');
26
27 const dargs = require('./dargs');
28
29 const utils = require('./utils');
30
31 const spawn = require('cross-spawn').sync
32
33 const eol = require('os').EOL
34
35 var CACHE_VAR = {
36 rebuild: false,
37 deferred: [],
38 bundleModules: [],
39 ideServer: null,
40 psModuleMap: null,
41 warnings: [],
42 errors: [],
43 compilationStarted: false,
44 compilationFinished: false,
45 compilationFailed: false,
46 installed: false,
47 srcOption: []
48 };
49
50 module.exports = function purescriptLoader(source, map) {
51 this.cacheable && this.cacheable();
52
53 const webpackContext = (this.options && this.options.context) || this.rootContext;
54
55 const callback = this.async();
56
57 const loaderOptions = loaderUtils.getOptions(this) || {};
58
59 const srcOption = (pscPackage => {
60 const srcPath = path.join('src', '**', '*.purs');
61
62 const bowerPath = path.join('bower_components', 'purescript-*', 'src', '**', '*.purs');
63
64 if (CACHE_VAR.srcOption.length > 0) {
65 return CACHE_VAR.srcOption;
66 }
67 else if (pscPackage) {
68 const pscPackageCommand = 'psc-package';
69
70 const pscPackageArgs = ['sources'];
71
72 const loaderSrc = loaderOptions.src || [
73 srcPath
74 ];
75
76 debug('psc-package %s %o', pscPackageCommand, pscPackageArgs);
77
78 const cmd = spawn(pscPackageCommand, pscPackageArgs);
79
80 if (cmd.error) {
81 throw new Error(cmd.error);
82 }
83 else if (cmd.status !== 0) {
84 const error = cmd.stdout.toString();
85
86 throw new Error(error);
87 }
88 else {
89 const result = cmd.stdout.toString().split(eol).filter(v => v != '').concat(loaderSrc);
90
91 debug('psc-package result: %o', result);
92
93 CACHE_VAR.srcOption = result;
94
95 return result;
96 }
97 }
98 else {
99 const result = loaderOptions.src || [
100 bowerPath,
101 srcPath
102 ];
103
104 CACHE_VAR.srcOption = result;
105
106 return result;
107 }
108 })(loaderOptions.pscPackage);
109
110 const options = Object.assign({
111 context: webpackContext,
112 psc: null,
113 pscArgs: {},
114 pscBundle: null,
115 pscBundleArgs: {},
116 pscIdeClient: null,
117 pscIdeClientArgs: {},
118 pscIdeServer: null,
119 pscIdeServerArgs: {},
120 pscIde: false,
121 pscIdeColors: loaderOptions.psc === 'psa',
122 pscPackage: false,
123 bundleOutput: 'output/bundle.js',
124 bundleNamespace: 'PS',
125 bundle: false,
126 warnings: true,
127 watch: false,
128 output: 'output',
129 src: []
130 }, loaderOptions, {
131 src: srcOption
132 });
133
134 if (!CACHE_VAR.installed) {
135 debugVerbose('installing purs-loader with options: %O', options);
136
137 CACHE_VAR.installed = true;
138
139 // invalidate loader CACHE_VAR when bundle is marked as invalid (in watch mode)
140 this._compiler.plugin('invalid', () => {
141 debugVerbose('invalidating loader CACHE_VAR');
142
143 CACHE_VAR = {
144 rebuild: options.pscIde,
145 deferred: [],
146 bundleModules: [],
147 ideServer: CACHE_VAR.ideServer,
148 psModuleMap: CACHE_VAR.psModuleMap,
149 warnings: [],
150 errors: [],
151 compilationStarted: false,
152 compilationFinished: false,
153 compilationFailed: false,
154 installed: CACHE_VAR.installed,
155 srcOption: []
156 };
157 });
158
159 // add psc warnings to webpack compilation warnings
160 this._compiler.plugin('after-compile', (compilation, callback) => {
161 CACHE_VAR.warnings.forEach(warning => {
162 compilation.warnings.push(warning);
163 });
164
165 CACHE_VAR.errors.forEach(error => {
166 compilation.errors.push(error);
167 });
168
169 callback()
170 });
171 }
172
173 const psModuleName = PsModuleMap.matchModule(source);
174
175 const psModule = {
176 name: psModuleName,
177 source: source,
178 load: ({js, map}) => callback(null, js, map),
179 reject: error => callback(error),
180 srcPath: this.resourcePath,
181 remainingRequest: loaderUtils.getRemainingRequest(this),
182 srcDir: path.dirname(this.resourcePath),
183 jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')),
184 options: options,
185 cache: CACHE_VAR,
186 emitWarning: warning => {
187 if (options.warnings && warning.length) {
188 CACHE_VAR.warnings.push(warning);
189 }
190 },
191 emitError: pscMessage => {
192 if (pscMessage.length) {
193 const modules = [];
194
195 const matchErrorsSeparator = /\n(?=Error)/;
196 const errors = pscMessage.split(matchErrorsSeparator);
197 for (const error of errors) {
198 const matchErrLocation = /at (.+\.purs) line (\d+), column (\d+) - line (\d+), column (\d+)/;
199 const [, filename] = matchErrLocation.exec(error) || [];
200 if (!filename) continue;
201
202 const baseModulePath = path.join(this.rootContext, filename);
203 this.addDependency(baseModulePath);
204
205 const matchErrModuleName = /in module ((?:\w+\.)*\w+)/;
206 const [, baseModuleName] = matchErrModuleName.exec(error) || [];
207 if (!baseModuleName) continue;
208
209 const matchMissingModuleName = /Module ((?:\w+\.)*\w+) was not found/;
210 const matchMissingImportFromModuleName = /Cannot import value \w+ from module ((?:\w+\.)*\w+)/;
211 for (const re of [matchMissingModuleName, matchMissingImportFromModuleName]) {
212 const [, targetModuleName] = re.exec(error) || [];
213 if (targetModuleName) {
214 const resolved = utils.resolvePursModule({
215 baseModulePath,
216 baseModuleName,
217 targetModuleName
218 });
219 this.addDependency(resolved);
220 }
221 }
222
223 const desc = {
224 name: baseModuleName,
225 filename: baseModulePath
226 };
227
228 if (typeof this.extractPursDependenciesFromError === 'function') {
229 const dependencies = this.extractPursDependenciesFromError(error) || [];
230
231 for (const dep of dependencies) {
232 this.addDependency(dep);
233 }
234 }
235
236 modules.push(desc);
237 }
238
239 CACHE_VAR.errors.push(Object.assign(new Error(pscMessage), { modules }));
240 }
241 }
242 }
243
244 debug('loading %s', psModule.name);
245
246 if (options.bundle) {
247 CACHE_VAR.bundleModules.push(psModule.name);
248 }
249
250 if (CACHE_VAR.rebuild) {
251 const connect = () => {
252 if (!CACHE_VAR.ideServer) {
253 CACHE_VAR.ideServer = true;
254
255 return ide.connect(psModule)
256 .then(ideServer => {
257 CACHE_VAR.ideServer = ideServer;
258 return psModule;
259 })
260 .then(ide.loadWithRetry)
261 .catch(error => {
262 if (CACHE_VAR.ideServer.kill) {
263 debug('ide failed to initially load modules, stopping the ide server process');
264
265 CACHE_VAR.ideServer.kill();
266 }
267
268 CACHE_VAR.ideServer = null;
269
270 return Promise.reject(error);
271 })
272 ;
273 }
274 else {
275 return Promise.resolve(psModule);
276 }
277 };
278
279 const rebuild = () =>
280 ide.rebuild(psModule)
281 .then(() =>
282 toJavaScript(psModule)
283 .then(js => sourceMaps(psModule, js))
284 .then(psModule.load)
285 .catch(psModule.reject)
286 )
287 .catch(error => {
288 if (error instanceof ide.UnknownModuleError) {
289 // Store the modules that trigger a recompile due to an
290 // unknown module error. We need to wait until compilation is
291 // done before loading these files.
292
293 CACHE_VAR.deferred.push(psModule);
294
295 if (!CACHE_VAR.compilationStarted) {
296 CACHE_VAR.compilationStarted = true;
297
298 return compile(psModule)
299 .then(() => {
300 CACHE_VAR.compilationFinished = true;
301 })
302 .then(() =>
303 Promise.map(CACHE_VAR.deferred, psModule =>
304 ide.load(psModule)
305 .then(() => toJavaScript(psModule))
306 .then(js => sourceMaps(psModule, js))
307 .then(psModule.load)
308 )
309 )
310 .catch(error => {
311 CACHE_VAR.compilationFailed = true;
312
313 CACHE_VAR.deferred[0].reject(error);
314
315 CACHE_VAR.deferred.slice(1).forEach(psModule => {
316 psModule.reject(new Error('purs-loader failed'));
317 })
318 })
319 ;
320 } else if (CACHE_VAR.compilationFailed) {
321 CACHE_VAR.deferred.pop().reject(new Error('purs-loader failed'));
322 } else {
323 // The compilation has started. We must wait until it is
324 // done in order to ensure the module map contains all of
325 // the unknown modules.
326 }
327 }
328 else {
329 debug('ide rebuild failed due to an unhandled error: %o', error);
330
331 psModule.reject(error);
332 }
333 })
334 ;
335
336 connect().then(rebuild);
337 }
338 else if (CACHE_VAR.compilationFinished) {
339 debugVerbose('compilation is already finished, loading module %s', psModule.name);
340
341 toJavaScript(psModule)
342 .then(js => sourceMaps(psModule, js))
343 .then(psModule.load)
344 .catch(psModule.reject);
345 }
346 else {
347 // The compilation has not finished yet. We need to wait for
348 // compilation to finish before the loaders run so that references
349 // to compiled output are valid. Push the modules into the CACHE_VAR to
350 // be loaded once the complation is complete.
351
352 CACHE_VAR.deferred.push(psModule);
353
354 if (!CACHE_VAR.compilationStarted) {
355 CACHE_VAR.compilationStarted = true;
356
357 compile(psModule)
358 .then(() => {
359 CACHE_VAR.compilationFinished = true;
360 })
361 .then(() => {
362 if (options.bundle) {
363 return bundle(options, CACHE_VAR.bundleModules);
364 }
365 })
366 .then(() =>
367 Promise.map(CACHE_VAR.deferred, psModule =>
368 toJavaScript(psModule)
369 .then(js => sourceMaps(psModule, js))
370 .then(psModule.load)
371 )
372 )
373 .catch(error => {
374 CACHE_VAR.compilationFailed = true;
375
376 CACHE_VAR.deferred[0].reject(error);
377
378 CACHE_VAR.deferred.slice(1).forEach(psModule => {
379 psModule.reject(new Error('purs-loader failed'));
380 })
381 })
382 ;
383 } else if (CACHE_VAR.compilationFailed) {
384 CACHE_VAR.deferred.pop().reject(new Error('purs-loader failed'));
385 } else {
386 // The complation has started. Nothing to do but wait until it is
387 // done before loading all of the modules.
388 }
389 }
390 }