Skip to content

Commit 8e21ab0

Browse files
committed
Ensure that all imported files are watched
In order to handle the case where a new PureScript file is imported, but fails to compile, the purs-loader now tracks imports for each PureScript file in order to append any additional imports to the resulting JS. This ensures that webpack will watch the new file even before it successfully compiles.
1 parent a3c358f commit 8e21ab0

File tree

5 files changed

+1799
-65
lines changed

5 files changed

+1799
-65
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"globby": "^4.0.0",
4444
"js-string-escape": "^1.0.1",
4545
"loader-utils": "^0.2.14",
46+
"lodash.difference": "^4.5.0",
4647
"promise-retry": "^1.1.0"
4748
},
4849
"devDependencies": {

src/PsModuleMap.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@ const fs = Promise.promisifyAll(require('fs'));
88

99
const globby = require('globby');
1010

11-
const debug = require('debug')('purs-loader')
11+
const debug = require('debug')('purs-loader');
1212

13-
const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
13+
const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i;
1414

15-
function match(str) {
15+
const importModuleRegex = /(?:^|\n)\s*import\s+([\w\.]+)/ig;
16+
17+
function matchModule(str) {
1618
const matches = str.match(srcModuleRegex);
1719
return matches && matches[1];
1820
}
19-
module.exports.match = match;
21+
module.exports.match = matchModule;
22+
23+
function matchImports(str) {
24+
const matches = str.match(importModuleRegex);
25+
return (matches || []).map(a => a.replace(/\n?\s*import\s+/i, ''));
26+
}
27+
module.exports.matchImports = matchImports;
2028

2129
function makeMapEntry(filePurs) {
2230
const dirname = path.dirname(filePurs);
@@ -33,14 +41,18 @@ function makeMapEntry(filePurs) {
3341

3442
const sourceJs = fileMap.fileJs;
3543

36-
const moduleName = match(sourcePurs);
44+
const moduleName = matchModule(sourcePurs);
45+
46+
const imports = matchImports(sourcePurs);
3747

3848
const map = {};
3949

4050
map[moduleName] = map[moduleName] || {};
4151

4252
map[moduleName].src = path.resolve(filePurs);
4353

54+
map[moduleName].imports = imports;
55+
4456
if (sourceJs) {
4557
map[moduleName].ffi = path.resolve(fileJs);
4658
}

src/index.js

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@
33
const debug = require('debug')('purs-loader')
44
const loaderUtils = require('loader-utils')
55
const Promise = require('bluebird')
6-
const fs = Promise.promisifyAll(require('fs'))
76
const path = require('path')
8-
const jsStringEscape = require('js-string-escape')
97
const PsModuleMap = require('./PsModuleMap');
108
const Psc = require('./Psc');
119
const PscIde = require('./PscIde');
10+
const toJavaScript = require('./to-javascript');
1211
const dargs = require('./dargs');
1312
const spawn = require('cross-spawn').sync
1413
const eol = require('os').EOL
1514

16-
const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g
17-
1815
module.exports = function purescriptLoader(source, map) {
1916
const callback = this.async()
2017
const config = this.options
@@ -164,59 +161,3 @@ module.exports = function purescriptLoader(source, map) {
164161
})
165162
}
166163
}
167-
168-
function updatePsModuleMap(psModule) {
169-
const options = psModule.options
170-
const cache = psModule.cache
171-
const filePurs = psModule.srcPath
172-
if (!cache.psModuleMap) {
173-
debug('module mapping does not exist');
174-
return PsModuleMap.makeMap(options.src).then(map => {
175-
cache.psModuleMap = map;
176-
return cache.psModuleMap;
177-
});
178-
}
179-
else {
180-
return PsModuleMap.makeMapEntry(filePurs).then(result => {
181-
const map = Object.assign(cache.psModuleMap, result)
182-
cache.psModuleMap = map;
183-
return cache.psModuleMap;
184-
});
185-
}
186-
}
187-
188-
// The actual loader is executed *after* purescript compilation.
189-
function toJavaScript(psModule) {
190-
const options = psModule.options
191-
const cache = psModule.cache
192-
const bundlePath = path.resolve(options.bundleOutput)
193-
const jsPath = cache.bundle ? bundlePath : psModule.jsPath
194-
195-
debug('loading JavaScript for', psModule.name)
196-
197-
return Promise.props({
198-
js: fs.readFileAsync(jsPath, 'utf8'),
199-
psModuleMap: updatePsModuleMap(psModule)
200-
}).then(result => {
201-
let js = ''
202-
203-
if (options.bundle) {
204-
// if bundling, return a reference to the bundle
205-
js = 'module.exports = require("'
206-
+ jsStringEscape(path.relative(psModule.srcDir, options.bundleOutput))
207-
+ '")["' + psModule.name + '"]'
208-
} else {
209-
// replace require paths to output files generated by psc with paths
210-
// to purescript sources, which are then also run through this loader.
211-
js = result.js
212-
.replace(requireRegex, (m, p1) => {
213-
return 'require("' + jsStringEscape(result.psModuleMap[p1].src) + '")'
214-
})
215-
.replace(/require\(['"]\.\/foreign['"]\)/g, (m, p1) => {
216-
return 'require("' + jsStringEscape(result.psModuleMap[psModule.name].ffi) + '")'
217-
})
218-
}
219-
220-
return js
221-
})
222-
}

src/to-javascript.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
'use strict';
2+
3+
const Promise = require('bluebird');
4+
5+
const fs = Promise.promisifyAll(require('fs'));
6+
7+
const path = require('path');
8+
9+
const jsStringEscape = require('js-string-escape');
10+
11+
const difference = require('lodash.difference');
12+
13+
const debug = require('debug')('purs-loader');
14+
15+
const PsModuleMap = require('./PsModuleMap');
16+
17+
function updatePsModuleMap(psModule) {
18+
const options = psModule.options;
19+
20+
const cache = psModule.cache;
21+
22+
const filePurs = psModule.srcPath;
23+
24+
if (!cache.psModuleMap) {
25+
debug('module mapping does not exist');
26+
27+
return PsModuleMap.makeMap(options.src).then(map => {
28+
cache.psModuleMap = map;
29+
return cache.psModuleMap;
30+
});
31+
}
32+
else {
33+
return PsModuleMap.makeMapEntry(filePurs).then(result => {
34+
const map = Object.assign(cache.psModuleMap, result);
35+
36+
cache.psModuleMap = map;
37+
38+
return cache.psModuleMap;
39+
});
40+
}
41+
}
42+
43+
// Reference the bundle.
44+
function makeBundleJS(psModule) {
45+
const bundleOutput = psMoudle.options.bundleOutput;
46+
47+
const name = psModule.name;
48+
49+
const srcDir = psModule.srcDir;
50+
51+
const escaped = jsStringEscape(path.relative(srcDir, bundleOutput));
52+
53+
const result = `module.exports = require("${escaped}")["${name}"]`;
54+
55+
return result;
56+
}
57+
58+
// Replace require paths to output files generated by psc with paths
59+
// to purescript sources, which are then also run through this loader.
60+
// Additionally, the imports replaced are tracked so that in the event
61+
// the compiler fails to compile the PureScript source, we can tack on
62+
// any new imports in order to allow webpack to watch the new files
63+
// before they have been successfully compiled.
64+
function makeJS(psModule, psModuleMap, js) {
65+
const requireRE = /require\(['"]\.\.\/([\w\.]+)['"]\)/g;
66+
67+
const foreignRE = /require\(['"]\.\/foreign['"]\)/g;
68+
69+
const name = psModule.name;
70+
71+
const imports = psModuleMap[name].imports;
72+
73+
var replacedImports = [];
74+
75+
const result = js
76+
.replace(requireRE, (m, p1) => {
77+
const escapedPath = jsStringEscape(psModuleMap[p1].src);
78+
79+
replacedImports.push(p1);
80+
81+
return `require("${escapedPath}")`;
82+
})
83+
.replace(foreignRE, () => {
84+
const escapedPath = jsStringEscape(psModuleMap[name].ffi);
85+
86+
return `require("${escapedPath}")`;
87+
})
88+
;
89+
90+
debug('imports %o', imports);
91+
92+
debug('replaced imports %o', replacedImports);
93+
94+
const additionalImports = difference(imports, replacedImports);
95+
96+
debug('additional imports for %s: %o', name, additionalImports);
97+
98+
const additionalImportsResult = additionalImports.map(import_ => {
99+
const escapedPath = jsStringEscape(psModuleMap[import_].src);
100+
101+
return `var ${import_.replace(/\./g, '_')} = require("${escapedPath}")`;
102+
}).join('\n');
103+
104+
const result_ = result + (additionalImports.length ? '\n' + additionalImportsResult : '');
105+
106+
return result_;
107+
}
108+
109+
module.exports = function toJavaScript(psModule) {
110+
const options = psModule.options;
111+
112+
const cache = psModule.cache;
113+
114+
const bundlePath = path.resolve(options.bundleOutput);
115+
116+
const jsPath = cache.bundle ? bundlePath : psModule.jsPath;
117+
118+
const js = fs.readFileAsync(jsPath, 'utf8').catch(() => '');
119+
120+
const psModuleMap = updatePsModuleMap(psModule);
121+
122+
debug('loading JavaScript for %s', psModule.name);
123+
124+
return Promise.props({js: js, psModuleMap: psModuleMap}).then(result =>
125+
options.bundle ?
126+
makeBundleJS(psModule) :
127+
makeJS(psModule, result.psModuleMap, result.js)
128+
);
129+
};

0 commit comments

Comments
 (0)