]>
Commit | Line | Data |
---|---|---|
1 | 'use strict' | |
2 | ||
3 | const debug = require('debug')('purs-loader') | |
4 | const loaderUtils = require('loader-utils') | |
5 | const Promise = require('bluebird') | |
6 | const fs = Promise.promisifyAll(require('fs')) | |
7 | const path = require('path') | |
8 | const jsStringEscape = require('js-string-escape') | |
9 | const PsModuleMap = require('./PsModuleMap'); | |
10 | const Psc = require('./Psc'); | |
11 | const PscIde = require('./PscIde'); | |
12 | const dargs = require('./dargs'); | |
13 | ||
14 | const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g | |
15 | ||
16 | module.exports = function purescriptLoader(source, map) { | |
17 | const callback = this.async() | |
18 | const config = this.options | |
19 | const query = loaderUtils.parseQuery(this.query) | |
20 | const webpackOptions = this.options.purescriptLoader || {} | |
21 | ||
22 | const options = Object.assign({ | |
23 | context: config.context, | |
24 | psc: 'psc', | |
25 | pscArgs: {}, | |
26 | pscBundle: 'psc-bundle', | |
27 | pscBundleArgs: {}, | |
28 | pscIde: false, | |
29 | pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa', | |
30 | pscIdeArgs: {}, | |
31 | bundleOutput: 'output/bundle.js', | |
32 | bundleNamespace: 'PS', | |
33 | bundle: false, | |
34 | warnings: true, | |
35 | output: 'output', | |
36 | src: [ | |
37 | path.join('src', '**', '*.purs'), | |
38 | path.join('bower_components', 'purescript-*', 'src', '**', '*.purs') | |
39 | ] | |
40 | }, webpackOptions, query) | |
41 | ||
42 | this.cacheable && this.cacheable() | |
43 | ||
44 | let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { | |
45 | rebuild: false, | |
46 | deferred: [], | |
47 | bundleModules: [], | |
48 | warnings: [], | |
49 | errors: [] | |
50 | } | |
51 | ||
52 | if (!config.purescriptLoaderInstalled) { | |
53 | config.purescriptLoaderInstalled = true | |
54 | ||
55 | // invalidate loader cache when bundle is marked as invalid (in watch mode) | |
56 | this._compiler.plugin('invalid', () => { | |
57 | debug('invalidating loader cache'); | |
58 | ||
59 | cache = config.purescriptLoaderCache = { | |
60 | rebuild: options.pscIde, | |
61 | deferred: [], | |
62 | bundleModules: [], | |
63 | ideServer: cache.ideServer, | |
64 | psModuleMap: cache.psModuleMap, | |
65 | warnings: [], | |
66 | errors: [] | |
67 | } | |
68 | }); | |
69 | ||
70 | // add psc warnings to webpack compilation warnings | |
71 | this._compiler.plugin('after-compile', (compilation, callback) => { | |
72 | cache.warnings.forEach(warning => { | |
73 | compilation.warnings.push(warning); | |
74 | }); | |
75 | ||
76 | cache.errors.forEach(error => { | |
77 | compilation.errors.push(error); | |
78 | }); | |
79 | ||
80 | callback() | |
81 | }); | |
82 | } | |
83 | ||
84 | const psModuleName = PsModuleMap.match(source) | |
85 | const psModule = { | |
86 | name: psModuleName, | |
87 | load: js => callback(null, js), | |
88 | reject: error => callback(error), | |
89 | srcPath: this.resourcePath, | |
90 | srcDir: path.dirname(this.resourcePath), | |
91 | jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')), | |
92 | options: options, | |
93 | cache: cache, | |
94 | emitWarning: warning => { | |
95 | if (options.warnings && warning.length) { | |
96 | cache.warnings.push(warning); | |
97 | } | |
98 | }, | |
99 | emitError: error => { | |
100 | if (error.length) { | |
101 | cache.errors.push(error); | |
102 | } | |
103 | } | |
104 | } | |
105 | ||
106 | debug('loader called', psModule.name) | |
107 | ||
108 | if (options.bundle) { | |
109 | cache.bundleModules.push(psModule.name) | |
110 | } | |
111 | ||
112 | if (cache.rebuild) { | |
113 | return PscIde.connect(psModule) | |
114 | .then(PscIde.rebuild) | |
115 | .then(toJavaScript) | |
116 | .then(psModule.load) | |
117 | .catch(psModule.reject) | |
118 | } | |
119 | ||
120 | if (cache.compilationFinished) { | |
121 | return toJavaScript(psModule).then(psModule.load).catch(psModule.reject) | |
122 | } | |
123 | ||
124 | // We need to wait for compilation to finish before the loaders run so that | |
125 | // references to compiled output are valid. | |
126 | cache.deferred.push(psModule) | |
127 | ||
128 | if (!cache.compilationStarted) { | |
129 | return Psc.compile(psModule) | |
130 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | |
131 | debug('rebuilt module map'); | |
132 | cache.psModuleMap = map; | |
133 | })) | |
134 | .then(() => Promise.map(cache.deferred, psModule => { | |
135 | if (typeof cache.ideServer === 'object') cache.ideServer.kill() | |
136 | return toJavaScript(psModule).then(psModule.load) | |
137 | })) | |
138 | .catch(error => { | |
139 | cache.deferred[0].reject(error) | |
140 | cache.deferred.slice(1).forEach(psModule => psModule.reject(new Error('purs-loader failed'))) | |
141 | }) | |
142 | } | |
143 | } | |
144 | ||
145 | function updatePsModuleMap(psModule) { | |
146 | const options = psModule.options | |
147 | const cache = psModule.cache | |
148 | const filePurs = psModule.srcPath | |
149 | if (!cache.psModuleMap) { | |
150 | debug('module mapping does not exist'); | |
151 | return PsModuleMap.makeMap(options.src).then(map => { | |
152 | cache.psModuleMap = map; | |
153 | return cache.psModuleMap; | |
154 | }); | |
155 | } | |
156 | else { | |
157 | return PsModuleMap.makeMapEntry(filePurs).then(result => { | |
158 | const map = Object.assign(cache.psModuleMap, result) | |
159 | cache.psModuleMap = map; | |
160 | return cache.psModuleMap; | |
161 | }); | |
162 | } | |
163 | } | |
164 | ||
165 | // The actual loader is executed *after* purescript compilation. | |
166 | function toJavaScript(psModule) { | |
167 | const options = psModule.options | |
168 | const cache = psModule.cache | |
169 | const bundlePath = path.resolve(options.bundleOutput) | |
170 | const jsPath = cache.bundle ? bundlePath : psModule.jsPath | |
171 | ||
172 | debug('loading JavaScript for', psModule.name) | |
173 | ||
174 | return Promise.props({ | |
175 | js: fs.readFileAsync(jsPath, 'utf8'), | |
176 | psModuleMap: updatePsModuleMap(psModule) | |
177 | }).then(result => { | |
178 | let js = '' | |
179 | ||
180 | if (options.bundle) { | |
181 | // if bundling, return a reference to the bundle | |
182 | js = 'module.exports = require("' | |
183 | + jsStringEscape(path.relative(psModule.srcDir, options.bundleOutput)) | |
184 | + '")["' + psModule.name + '"]' | |
185 | } else { | |
186 | // replace require paths to output files generated by psc with paths | |
187 | // to purescript sources, which are then also run through this loader. | |
188 | js = result.js | |
189 | .replace(requireRegex, (m, p1) => { | |
190 | return 'require("' + jsStringEscape(result.psModuleMap[p1].src) + '")' | |
191 | }) | |
192 | .replace(/require\(['"]\.\/foreign['"]\)/g, (m, p1) => { | |
193 | return 'require("' + jsStringEscape(result.psModuleMap[psModule.name].ffi) + '")' | |
194 | }) | |
195 | } | |
196 | ||
197 | return js | |
198 | }) | |
199 | } |