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