]> git.immae.eu Git - github/fretlink/purs-loader.git/blob - src/index.js
Added hooks support (#122)
[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 spawn = require('cross-spawn').sync
28
29 const eol = require('os').EOL
30
31 var CACHE_VAR = {
32 rebuild: false,
33 deferred: [],
34 bundleModules: [],
35 ideServer: null,
36 psModuleMap: null,
37 warnings: [],
38 errors: [],
39 compilationStarted: false,
40 compilationFinished: false,
41 installed: false,
42 srcOption: []
43 };
44
45 module.exports = function purescriptLoader(source, map) {
46 this.cacheable && this.cacheable();
47
48 const webpackContext = (this.options && this.options.context) || this.rootContext;
49
50 const callback = this.async();
51
52 const loaderOptions = loaderUtils.getOptions(this) || {};
53
54 const srcOption = (pscPackage => {
55 const srcPath = path.join('src', '**', '*.purs');
56
57 const bowerPath = path.join('bower_components', 'purescript-*', 'src', '**', '*.purs');
58
59 if (CACHE_VAR.srcOption.length > 0) {
60 return CACHE_VAR.srcOption;
61 }
62 else if (pscPackage) {
63 const pscPackageCommand = 'psc-package';
64
65 const pscPackageArgs = ['sources'];
66
67 const loaderSrc = loaderOptions.src || [
68 srcPath
69 ];
70
71 debug('psc-package %s %o', pscPackageCommand, pscPackageArgs);
72
73 const cmd = spawn(pscPackageCommand, pscPackageArgs);
74
75 if (cmd.error) {
76 throw new Error(cmd.error);
77 }
78 else if (cmd.status !== 0) {
79 const error = cmd.stdout.toString();
80
81 throw new Error(error);
82 }
83 else {
84 const result = cmd.stdout.toString().split(eol).filter(v => v != '').concat(loaderSrc);
85
86 debug('psc-package result: %o', result);
87
88 CACHE_VAR.srcOption = result;
89
90 return result;
91 }
92 }
93 else {
94 const result = loaderOptions.src || [
95 bowerPath,
96 srcPath
97 ];
98
99 CACHE_VAR.srcOption = result;
100
101 return result;
102 }
103 })(loaderOptions.pscPackage);
104
105 const options = Object.assign({
106 context: webpackContext,
107 psc: null,
108 pscArgs: {},
109 pscBundle: null,
110 pscBundleArgs: {},
111 pscIdeClient: null,
112 pscIdeClientArgs: {},
113 pscIdeServer: null,
114 pscIdeServerArgs: {},
115 pscIdeRebuildArgs: {},
116 pscIde: false,
117 pscIdeColors: loaderOptions.psc === 'psa',
118 pscPackage: false,
119 bundleOutput: 'output/bundle.js',
120 bundleNamespace: 'PS',
121 bundle: false,
122 warnings: true,
123 watch: false,
124 output: 'output',
125 src: []
126 }, loaderOptions, {
127 src: srcOption
128 });
129
130 if (!CACHE_VAR.installed) {
131 debugVerbose('installing purs-loader with options: %O', options);
132
133 CACHE_VAR.installed = true;
134
135 const invalidCb = () => {
136 debugVerbose('invalidating loader CACHE_VAR');
137
138 CACHE_VAR = {
139 rebuild: options.pscIde,
140 deferred: [],
141 bundleModules: [],
142 ideServer: CACHE_VAR.ideServer,
143 psModuleMap: CACHE_VAR.psModuleMap,
144 warnings: [],
145 errors: [],
146 compilationStarted: false,
147 compilationFinished: false,
148 installed: CACHE_VAR.installed,
149 srcOption: []
150 };
151 }
152
153 // invalidate loader CACHE_VAR when bundle is marked as invalid (in watch mode)
154 if(this._compiler.hooks){
155 this._compiler.hooks.invalid.tap('purs-loader', invalidCb);
156 } else {
157 this._compiler.plugin('invalid', invalidCb);
158 }
159
160 const afterCompileCb = (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 // add psc warnings to webpack compilation warnings
173 if(this._compiler.hooks) {
174 this._compiler.hooks.afterCompile.tapAsync('purs-loader', afterCompileCb);
175 } else {
176 this._compiler.plugin('after-compile', afterCompileCb);
177 }
178 }
179
180 const psModuleName = PsModuleMap.matchModule(source);
181
182 const psModule = {
183 name: psModuleName,
184 source: source,
185 load: ({js, map}) => callback(null, js, map),
186 reject: error => callback(error),
187 srcPath: this.resourcePath,
188 remainingRequest: loaderUtils.getRemainingRequest(this),
189 srcDir: path.dirname(this.resourcePath),
190 jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')),
191 options: options,
192 cache: CACHE_VAR,
193 emitWarning: warning => {
194 if (options.warnings && warning.length) {
195 CACHE_VAR.warnings.push(warning);
196 }
197 },
198 emitError: error => {
199 if (error.length) {
200 CACHE_VAR.errors.push(error);
201 }
202 }
203 }
204
205 debug('loading %s', psModule.name);
206
207 if (options.bundle) {
208 CACHE_VAR.bundleModules.push(psModule.name);
209 }
210
211 if (CACHE_VAR.rebuild) {
212 const connect = () => {
213 if (!CACHE_VAR.ideServer) {
214 CACHE_VAR.ideServer = true;
215
216 return ide.connect(psModule)
217 .then(ideServer => {
218 CACHE_VAR.ideServer = ideServer;
219 return psModule;
220 })
221 .then(ide.loadWithRetry)
222 .catch(error => {
223 if (CACHE_VAR.ideServer.kill) {
224 debug('ide failed to initially load modules, stopping the ide server process');
225
226 CACHE_VAR.ideServer.kill();
227 }
228
229 CACHE_VAR.ideServer = null;
230
231 return Promise.reject(error);
232 })
233 ;
234 }
235 else {
236 return Promise.resolve(psModule);
237 }
238 };
239
240 const rebuild = () =>
241 ide.rebuild(psModule)
242 .then(() =>
243 toJavaScript(psModule)
244 .then(js => sourceMaps(psModule, js))
245 .then(psModule.load)
246 .catch(psModule.reject)
247 )
248 .catch(error => {
249 if (error instanceof ide.UnknownModuleError) {
250 // Store the modules that trigger a recompile due to an
251 // unknown module error. We need to wait until compilation is
252 // done before loading these files.
253
254 CACHE_VAR.deferred.push(psModule);
255
256 if (!CACHE_VAR.compilationStarted) {
257 CACHE_VAR.compilationStarted = true;
258
259 return compile(psModule)
260 .then(() => {
261 CACHE_VAR.compilationFinished = true;
262 })
263 .then(() =>
264 Promise.map(CACHE_VAR.deferred, psModule =>
265 ide.load(psModule)
266 .then(() => toJavaScript(psModule))
267 .then(js => sourceMaps(psModule, js))
268 .then(psModule.load)
269 )
270 )
271 .catch(error => {
272 CACHE_VAR.deferred[0].reject(error);
273
274 CACHE_VAR.deferred.slice(1).forEach(psModule => {
275 psModule.reject(new Error('purs-loader failed'));
276 })
277 })
278 ;
279 }
280 else {
281 // The compilation has started. We must wait until it is
282 // done in order to ensure the module map contains all of
283 // the unknown modules.
284 }
285 }
286 else {
287 debug('ide rebuild failed due to an unhandled error: %o', error);
288
289 psModule.reject(error);
290 }
291 })
292 ;
293
294 connect().then(rebuild);
295 }
296 else if (CACHE_VAR.compilationFinished) {
297 debugVerbose('compilation is already finished, loading module %s', psModule.name);
298
299 toJavaScript(psModule)
300 .then(js => sourceMaps(psModule, js))
301 .then(psModule.load)
302 .catch(psModule.reject);
303 }
304 else {
305 // The compilation has not finished yet. We need to wait for
306 // compilation to finish before the loaders run so that references
307 // to compiled output are valid. Push the modules into the CACHE_VAR to
308 // be loaded once the complation is complete.
309
310 CACHE_VAR.deferred.push(psModule);
311
312 if (!CACHE_VAR.compilationStarted) {
313 CACHE_VAR.compilationStarted = true;
314
315 compile(psModule)
316 .then(() => {
317 CACHE_VAR.compilationFinished = true;
318 })
319 .then(() => {
320 if (options.bundle) {
321 return bundle(options, CACHE_VAR.bundleModules);
322 }
323 })
324 .then(() =>
325 Promise.map(CACHE_VAR.deferred, psModule =>
326 toJavaScript(psModule)
327 .then(js => sourceMaps(psModule, js))
328 .then(psModule.load)
329 )
330 )
331 .catch(error => {
332 CACHE_VAR.deferred[0].reject(error);
333
334 CACHE_VAR.deferred.slice(1).forEach(psModule => {
335 psModule.reject(new Error('purs-loader failed'));
336 })
337 })
338 ;
339 }
340 else {
341 // The complation has started. Nothing to do but wait until it is
342 // done before loading all of the modules.
343 }
344 }
345 }