diff options
author | eric <thul.eric@gmail.com> | 2016-06-12 16:06:34 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-06-12 16:06:34 -0400 |
commit | 27638f63256b6d38745eef251b3327536766e7c3 (patch) | |
tree | 75e135abd95c9fee60ef9d9ba7d35440fd2c1ddf | |
parent | 7243be70a2163be2230a5f2739768137305a24ef (diff) | |
parent | 0b853815ef14d35cedebc2c7806fd2f9ff9d5ab5 (diff) | |
download | purs-loader-27638f63256b6d38745eef251b3327536766e7c3.tar.gz purs-loader-27638f63256b6d38745eef251b3327536766e7c3.tar.zst purs-loader-27638f63256b6d38745eef251b3327536766e7c3.zip |
Merge pull request #61 from ethul/topic/issues
Reduce building of PureScript module map
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | package.json | 8 | ||||
-rw-r--r-- | src/PsModuleMap.js | 66 | ||||
-rw-r--r-- | src/Psc.js | 92 | ||||
-rw-r--r-- | src/PscIde.js | 237 | ||||
-rw-r--r-- | src/dargs.js | 16 | ||||
-rw-r--r-- | src/index.js | 396 |
7 files changed, 458 insertions, 360 deletions
@@ -1,3 +1,4 @@ | |||
1 | **DS_Store* | 1 | **DS_Store* |
2 | npm-debug.log | ||
2 | node_modules/ | 3 | node_modules/ |
3 | index.js | 4 | lib/ |
diff --git a/package.json b/package.json index 5fc5a7f..6c276ce 100644 --- a/package.json +++ b/package.json | |||
@@ -2,15 +2,15 @@ | |||
2 | "name": "purs-loader", | 2 | "name": "purs-loader", |
3 | "version": "2.0.0-rc.0", | 3 | "version": "2.0.0-rc.0", |
4 | "description": "A webpack loader for PureScript.", | 4 | "description": "A webpack loader for PureScript.", |
5 | "main": "index.js", | 5 | "main": "lib/index.js", |
6 | "files": [ | 6 | "files": [ |
7 | "LICENSE", | 7 | "LICENSE", |
8 | "README.md", | 8 | "README.md", |
9 | "index.js", | 9 | "src", |
10 | "src/index.js" | 10 | "lib" |
11 | ], | 11 | ], |
12 | "scripts": { | 12 | "scripts": { |
13 | "build": "babel src/index.js -o index.js", | 13 | "build": "babel src --out-dir lib", |
14 | "prepublish": "npm run build", | 14 | "prepublish": "npm run build", |
15 | "test": "echo \"Error: no test specified\" && exit 1" | 15 | "test": "echo \"Error: no test specified\" && exit 1" |
16 | }, | 16 | }, |
diff --git a/src/PsModuleMap.js b/src/PsModuleMap.js new file mode 100644 index 0000000..2193f02 --- /dev/null +++ b/src/PsModuleMap.js | |||
@@ -0,0 +1,66 @@ | |||
1 | 'use strict'; | ||
2 | |||
3 | const path = require('path'); | ||
4 | |||
5 | const Promise = require('bluebird'); | ||
6 | |||
7 | const fs = Promise.promisifyAll(require('fs')); | ||
8 | |||
9 | const globby = require('globby'); | ||
10 | |||
11 | const debug = require('debug')('purs-loader') | ||
12 | |||
13 | const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i | ||
14 | |||
15 | function match(str) { | ||
16 | const matches = str.match(srcModuleRegex); | ||
17 | return matches && matches[1]; | ||
18 | } | ||
19 | module.exports.match = match; | ||
20 | |||
21 | function makeMapEntry(filePurs) { | ||
22 | const dirname = path.dirname(filePurs); | ||
23 | |||
24 | const basename = path.basename(filePurs, '.purs'); | ||
25 | |||
26 | const fileJs = path.join(dirname, `${basename}.js`); | ||
27 | |||
28 | const result = Promise.props({ | ||
29 | filePurs: fs.readFileAsync(filePurs, 'utf8'), | ||
30 | fileJs: fs.readFileAsync(fileJs, 'utf8').catch(() => undefined) | ||
31 | }).then(fileMap => { | ||
32 | const sourcePurs = fileMap.filePurs; | ||
33 | |||
34 | const sourceJs = fileMap.fileJs; | ||
35 | |||
36 | const moduleName = match(sourcePurs); | ||
37 | |||
38 | const map = {}; | ||
39 | |||
40 | map[moduleName] = map[moduleName] || {}; | ||
41 | |||
42 | map[moduleName].src = path.resolve(filePurs); | ||
43 | |||
44 | if (sourceJs) { | ||
45 | map[moduleName].ffi = path.resolve(fileJs); | ||
46 | } | ||
47 | |||
48 | return map; | ||
49 | }); | ||
50 | |||
51 | return result; | ||
52 | } | ||
53 | module.exports.makeMapEntry = makeMapEntry; | ||
54 | |||
55 | function makeMap(src) { | ||
56 | debug('loading PureScript source and FFI files from %o', src); | ||
57 | |||
58 | const globs = [].concat(src); | ||
59 | |||
60 | return globby(globs).then(paths => | ||
61 | Promise.all(paths.map(makeMapEntry)).then(result => | ||
62 | result.reduce(Object.assign, {}) | ||
63 | ) | ||
64 | ); | ||
65 | } | ||
66 | module.exports.makeMap = makeMap; | ||
diff --git a/src/Psc.js b/src/Psc.js new file mode 100644 index 0000000..9269e0f --- /dev/null +++ b/src/Psc.js | |||
@@ -0,0 +1,92 @@ | |||
1 | 'use strict'; | ||
2 | |||
3 | const path = require('path'); | ||
4 | |||
5 | const Promise = require('bluebird') | ||
6 | |||
7 | const fs = Promise.promisifyAll(require('fs')) | ||
8 | |||
9 | const spawn = require('cross-spawn') | ||
10 | |||
11 | const debug = require('debug')('purs-loader'); | ||
12 | |||
13 | const dargs = require('./dargs'); | ||
14 | |||
15 | function compile(psModule) { | ||
16 | const options = psModule.options | ||
17 | const cache = psModule.cache | ||
18 | const stderr = [] | ||
19 | |||
20 | if (cache.compilationStarted) return Promise.resolve(psModule) | ||
21 | |||
22 | cache.compilationStarted = true | ||
23 | |||
24 | const args = dargs(Object.assign({ | ||
25 | _: options.src, | ||
26 | output: options.output, | ||
27 | }, options.pscArgs)) | ||
28 | |||
29 | debug('spawning compiler %s %o', options.psc, args) | ||
30 | |||
31 | return (new Promise((resolve, reject) => { | ||
32 | console.log('\nCompiling PureScript...') | ||
33 | |||
34 | const compilation = spawn(options.psc, args) | ||
35 | |||
36 | compilation.stdout.on('data', data => stderr.push(data.toString())) | ||
37 | compilation.stderr.on('data', data => stderr.push(data.toString())) | ||
38 | |||
39 | compilation.on('close', code => { | ||
40 | console.log('Finished compiling PureScript.') | ||
41 | cache.compilationFinished = true | ||
42 | if (code !== 0) { | ||
43 | cache.errors = stderr.join('') | ||
44 | reject(true) | ||
45 | } else { | ||
46 | cache.warnings = stderr.join('') | ||
47 | resolve(psModule) | ||
48 | } | ||
49 | }) | ||
50 | })) | ||
51 | .then(compilerOutput => { | ||
52 | if (options.bundle) { | ||
53 | return bundle(options, cache).then(() => psModule) | ||
54 | } | ||
55 | return psModule | ||
56 | }) | ||
57 | } | ||
58 | module.exports.compile = compile; | ||
59 | |||
60 | function bundle(options, cache) { | ||
61 | if (cache.bundle) return Promise.resolve(cache.bundle) | ||
62 | |||
63 | const stdout = [] | ||
64 | const stderr = cache.bundle = [] | ||
65 | |||
66 | const args = dargs(Object.assign({ | ||
67 | _: [path.join(options.output, '*', '*.js')], | ||
68 | output: options.bundleOutput, | ||
69 | namespace: options.bundleNamespace, | ||
70 | }, options.pscBundleArgs)) | ||
71 | |||
72 | cache.bundleModules.forEach(name => args.push('--module', name)) | ||
73 | |||
74 | debug('spawning bundler %s %o', options.pscBundle, args.join(' ')) | ||
75 | |||
76 | return (new Promise((resolve, reject) => { | ||
77 | console.log('Bundling PureScript...') | ||
78 | |||
79 | const compilation = spawn(options.pscBundle, args) | ||
80 | |||
81 | compilation.stdout.on('data', data => stdout.push(data.toString())) | ||
82 | compilation.stderr.on('data', data => stderr.push(data.toString())) | ||
83 | compilation.on('close', code => { | ||
84 | if (code !== 0) { | ||
85 | cache.errors = (cache.errors || '') + stderr.join('') | ||
86 | return reject(true) | ||
87 | } | ||
88 | cache.bundle = stderr | ||
89 | resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`)) | ||
90 | }) | ||
91 | })) | ||
92 | } | ||
diff --git a/src/PscIde.js b/src/PscIde.js new file mode 100644 index 0000000..9d6c1ff --- /dev/null +++ b/src/PscIde.js | |||
@@ -0,0 +1,237 @@ | |||
1 | 'use strict'; | ||
2 | |||
3 | const path = require('path'); | ||
4 | |||
5 | const Promise = require('bluebird'); | ||
6 | |||
7 | const fs = Promise.promisifyAll(require('fs')); | ||
8 | |||
9 | const retryPromise = require('promise-retry'); | ||
10 | |||
11 | const spawn = require('cross-spawn'); | ||
12 | |||
13 | const colors = require('chalk'); | ||
14 | |||
15 | const debug = require('debug')('purs-loader'); | ||
16 | |||
17 | const dargs = require('./dargs'); | ||
18 | |||
19 | const Psc = require('./Psc'); | ||
20 | |||
21 | const PsModuleMap = require('./PsModuleMap'); | ||
22 | |||
23 | function connect(psModule) { | ||
24 | const options = psModule.options | ||
25 | const cache = psModule.cache | ||
26 | |||
27 | if (cache.ideServer) return Promise.resolve(psModule) | ||
28 | |||
29 | cache.ideServer = true | ||
30 | |||
31 | const connect_ = () => new Promise((resolve, reject) => { | ||
32 | const args = dargs(options.pscIdeArgs) | ||
33 | |||
34 | debug('attempting to connect to psc-ide-server', args) | ||
35 | |||
36 | const ideClient = spawn('psc-ide-client', args) | ||
37 | |||
38 | ideClient.stderr.on('data', data => { | ||
39 | debug(data.toString()) | ||
40 | cache.ideServer = false | ||
41 | reject(true) | ||
42 | }) | ||
43 | ideClient.stdout.once('data', data => { | ||
44 | debug(data.toString()) | ||
45 | if (data.toString()[0] === '{') { | ||
46 | const res = JSON.parse(data.toString()) | ||
47 | if (res.resultType === 'success') { | ||
48 | cache.ideServer = ideServer | ||
49 | resolve(psModule) | ||
50 | } else { | ||
51 | cache.ideServer = ideServer | ||
52 | reject(true) | ||
53 | } | ||
54 | } else { | ||
55 | cache.ideServer = false | ||
56 | reject(true) | ||
57 | } | ||
58 | }) | ||
59 | ideClient.stdin.resume() | ||
60 | ideClient.stdin.write(JSON.stringify({ command: 'load' })) | ||
61 | ideClient.stdin.write('\n') | ||
62 | }) | ||
63 | |||
64 | const args = dargs(Object.assign({ | ||
65 | outputDirectory: options.output, | ||
66 | }, options.pscIdeArgs)) | ||
67 | |||
68 | debug('attempting to start psc-ide-server', args) | ||
69 | |||
70 | const ideServer = cache.ideServer = spawn('psc-ide-server', []) | ||
71 | ideServer.stderr.on('data', data => { | ||
72 | debug(data.toString()) | ||
73 | }) | ||
74 | |||
75 | return retryPromise((retry, number) => { | ||
76 | return connect_().catch(error => { | ||
77 | if (!cache.ideServer && number === 9) { | ||
78 | debug(error) | ||
79 | |||
80 | console.log( | ||
81 | 'failed to connect to or start psc-ide-server, ' + | ||
82 | 'full compilation will occur on rebuild' | ||
83 | ) | ||
84 | |||
85 | return Promise.resolve(psModule) | ||
86 | } | ||
87 | |||
88 | return retry(error) | ||
89 | }) | ||
90 | }, { | ||
91 | retries: 9, | ||
92 | factor: 1, | ||
93 | minTimeout: 333, | ||
94 | maxTimeout: 333, | ||
95 | }) | ||
96 | } | ||
97 | module.exports.connect = connect; | ||
98 | |||
99 | function rebuild(psModule) { | ||
100 | const options = psModule.options | ||
101 | const cache = psModule.cache | ||
102 | |||
103 | debug('attempting rebuild with psc-ide-client %s', psModule.srcPath) | ||
104 | |||
105 | const request = (body) => new Promise((resolve, reject) => { | ||
106 | const args = dargs(options.pscIdeArgs) | ||
107 | const ideClient = spawn('psc-ide-client', args) | ||
108 | |||
109 | var stdout = '' | ||
110 | var stderr = '' | ||
111 | |||
112 | ideClient.stdout.on('data', data => { | ||
113 | stdout = stdout + data.toString() | ||
114 | }) | ||
115 | |||
116 | ideClient.stderr.on('data', data => { | ||
117 | stderr = stderr + data.toString() | ||
118 | }) | ||
119 | |||
120 | ideClient.on('close', code => { | ||
121 | if (code !== 0) { | ||
122 | const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr | ||
123 | return reject(new Error(error)) | ||
124 | } | ||
125 | |||
126 | let res = null | ||
127 | |||
128 | try { | ||
129 | res = JSON.parse(stdout.toString()) | ||
130 | debug(res) | ||
131 | } catch (err) { | ||
132 | return reject(err) | ||
133 | } | ||
134 | |||
135 | if (res && !Array.isArray(res.result)) { | ||
136 | return res.resultType === 'success' | ||
137 | ? resolve(psModule) | ||
138 | : reject('psc-ide rebuild failed') | ||
139 | } | ||
140 | |||
141 | Promise.map(res.result, (item, i) => { | ||
142 | debug(item) | ||
143 | return formatIdeResult(item, options, i, res.result.length) | ||
144 | }) | ||
145 | .then(compileMessages => { | ||
146 | if (res.resultType === 'error') { | ||
147 | if (res.result.some(item => item.errorCode === 'UnknownModule' || item.errorCode === 'UnknownName')) { | ||
148 | debug('unknown module, attempting full recompile') | ||
149 | return Psc.compile(psModule) | ||
150 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | ||
151 | debug('rebuilt module map'); | ||
152 | cache.psModuleMap = map; | ||
153 | })) | ||
154 | .then(() => request({ command: 'load' })) | ||
155 | .then(resolve) | ||
156 | .catch(() => reject('psc-ide rebuild failed')) | ||
157 | } | ||
158 | cache.errors = compileMessages.join('\n') | ||
159 | reject('psc-ide rebuild failed') | ||
160 | } else { | ||
161 | cache.warnings = compileMessages.join('\n') | ||
162 | resolve(psModule) | ||
163 | } | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | ideClient.stdin.write(JSON.stringify(body)) | ||
168 | ideClient.stdin.write('\n') | ||
169 | }) | ||
170 | |||
171 | return request({ | ||
172 | command: 'rebuild', | ||
173 | params: { | ||
174 | file: psModule.srcPath, | ||
175 | } | ||
176 | }) | ||
177 | } | ||
178 | module.exports.rebuild = rebuild; | ||
179 | |||
180 | function formatIdeResult(result, options, index, length) { | ||
181 | let numAndErr = `[${index+1}/${length} ${result.errorCode}]` | ||
182 | numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr | ||
183 | |||
184 | function makeResult() { | ||
185 | return Promise.resolve(`\n${numAndErr} ${result.message}`) | ||
186 | } | ||
187 | |||
188 | function makeResultSnippet(filename, pos) { | ||
189 | const srcPath = path.relative(options.context, filename); | ||
190 | const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` | ||
191 | |||
192 | return fs.readFileAsync(filename, 'utf8').then(source => { | ||
193 | const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) | ||
194 | const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine | ||
195 | const up = options.pscIdeColors ? colors.red('^') : '^' | ||
196 | const down = options.pscIdeColors ? colors.red('v') : 'v' | ||
197 | let trimmed = lines.slice(0) | ||
198 | |||
199 | if (endsOnNewline) { | ||
200 | lines.splice(lines.length - 1, 1) | ||
201 | pos.endLine = pos.endLine - 1 | ||
202 | pos.endColumn = lines[lines.length - 1].length || 1 | ||
203 | } | ||
204 | |||
205 | // strip newlines at the end | ||
206 | if (endsOnNewline) { | ||
207 | trimmed = lines.reverse().reduce((trimmed, line, i) => { | ||
208 | if (i === 0 && line === '') trimmed.trimming = true | ||
209 | if (!trimmed.trimming) trimmed.push(line) | ||
210 | if (trimmed.trimming && line !== '') { | ||
211 | trimmed.trimming = false | ||
212 | trimmed.push(line) | ||
213 | } | ||
214 | return trimmed | ||
215 | }, []).reverse() | ||
216 | pos.endLine = pos.endLine - (lines.length - trimmed.length) | ||
217 | pos.endColumn = trimmed[trimmed.length - 1].length || 1 | ||
218 | } | ||
219 | |||
220 | const spaces = ' '.repeat(String(pos.endLine).length) | ||
221 | let snippet = trimmed.map((line, i) => { | ||
222 | return ` ${pos.startLine + i} ${line}` | ||
223 | }).join('\n') | ||
224 | |||
225 | if (trimmed.length === 1) { | ||
226 | snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` | ||
227 | } else { | ||
228 | snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` | ||
229 | snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` | ||
230 | } | ||
231 | |||
232 | return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`) | ||
233 | }) | ||
234 | } | ||
235 | |||
236 | return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult(); | ||
237 | } | ||
diff --git a/src/dargs.js b/src/dargs.js new file mode 100644 index 0000000..e5c574c --- /dev/null +++ b/src/dargs.js | |||
@@ -0,0 +1,16 @@ | |||
1 | 'use strict'; | ||
2 | |||
3 | function dargs(obj) { | ||
4 | return Object.keys(obj).reduce((args, key) => { | ||
5 | const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase(); | ||
6 | const val = obj[key] | ||
7 | |||
8 | if (key === '_') val.forEach(v => args.push(v)) | ||
9 | else if (Array.isArray(val)) val.forEach(v => args.push(arg, v)) | ||
10 | else args.push(arg, obj[key]) | ||
11 | |||
12 | return args.filter(arg => (typeof arg !== 'boolean')) | ||
13 | }, []) | ||
14 | } | ||
15 | |||
16 | module.exports = dargs; | ||
diff --git a/src/index.js b/src/index.js index cfba1e2..c73fdd5 100644 --- a/src/index.js +++ b/src/index.js | |||
@@ -1,17 +1,16 @@ | |||
1 | 'use strict' | 1 | 'use strict' |
2 | 2 | ||
3 | const colors = require('chalk') | ||
4 | const debug = require('debug')('purs-loader') | 3 | const debug = require('debug')('purs-loader') |
5 | const loaderUtils = require('loader-utils') | 4 | const loaderUtils = require('loader-utils') |
6 | const globby = require('globby') | ||
7 | const Promise = require('bluebird') | 5 | const Promise = require('bluebird') |
8 | const fs = Promise.promisifyAll(require('fs')) | 6 | const fs = Promise.promisifyAll(require('fs')) |
9 | const spawn = require('cross-spawn') | ||
10 | const path = require('path') | 7 | const path = require('path') |
11 | const retryPromise = require('promise-retry') | ||
12 | const jsStringEscape = require('js-string-escape') | 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 | 13 | ||
14 | const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i | ||
15 | const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g | 14 | const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g |
16 | 15 | ||
17 | module.exports = function purescriptLoader(source, map) { | 16 | module.exports = function purescriptLoader(source, map) { |
@@ -45,7 +44,7 @@ module.exports = function purescriptLoader(source, map) { | |||
45 | let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { | 44 | let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { |
46 | rebuild: false, | 45 | rebuild: false, |
47 | deferred: [], | 46 | deferred: [], |
48 | bundleModules: [], | 47 | bundleModules: [] |
49 | } | 48 | } |
50 | 49 | ||
51 | if (!config.purescriptLoaderInstalled) { | 50 | if (!config.purescriptLoaderInstalled) { |
@@ -53,10 +52,14 @@ module.exports = function purescriptLoader(source, map) { | |||
53 | 52 | ||
54 | // invalidate loader cache when bundle is marked as invalid (in watch mode) | 53 | // invalidate loader cache when bundle is marked as invalid (in watch mode) |
55 | this._compiler.plugin('invalid', () => { | 54 | this._compiler.plugin('invalid', () => { |
55 | debug('invalidating loader cache'); | ||
56 | |||
56 | cache = config.purescriptLoaderCache = { | 57 | cache = config.purescriptLoaderCache = { |
57 | rebuild: options.pscIde, | 58 | rebuild: options.pscIde, |
58 | deferred: [], | 59 | deferred: [], |
59 | ideServer: cache.ideServer | 60 | bundleModules: [], |
61 | ideServer: cache.ideServer, | ||
62 | psModuleMap: cache.psModuleMap | ||
60 | } | 63 | } |
61 | }) | 64 | }) |
62 | 65 | ||
@@ -64,17 +67,19 @@ module.exports = function purescriptLoader(source, map) { | |||
64 | this._compiler.plugin('after-compile', (compilation, callback) => { | 67 | this._compiler.plugin('after-compile', (compilation, callback) => { |
65 | if (options.warnings && cache.warnings) { | 68 | if (options.warnings && cache.warnings) { |
66 | compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings}`) | 69 | compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings}`) |
70 | cache.warnings = null; | ||
67 | } | 71 | } |
68 | 72 | ||
69 | if (cache.errors) { | 73 | if (cache.errors) { |
70 | compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`) | 74 | compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`) |
75 | cache.errors = null; | ||
71 | } | 76 | } |
72 | 77 | ||
73 | callback() | 78 | callback() |
74 | }) | 79 | }) |
75 | } | 80 | } |
76 | 81 | ||
77 | const psModuleName = match(srcModuleRegex, source) | 82 | const psModuleName = PsModuleMap.match(source) |
78 | const psModule = { | 83 | const psModule = { |
79 | name: psModuleName, | 84 | name: psModuleName, |
80 | load: js => callback(null, js), | 85 | load: js => callback(null, js), |
@@ -93,8 +98,8 @@ module.exports = function purescriptLoader(source, map) { | |||
93 | } | 98 | } |
94 | 99 | ||
95 | if (cache.rebuild) { | 100 | if (cache.rebuild) { |
96 | return connectIdeServer(psModule) | 101 | return PscIde.connect(psModule) |
97 | .then(rebuild) | 102 | .then(PscIde.rebuild) |
98 | .then(toJavaScript) | 103 | .then(toJavaScript) |
99 | .then(psModule.load) | 104 | .then(psModule.load) |
100 | .catch(psModule.reject) | 105 | .catch(psModule.reject) |
@@ -109,7 +114,11 @@ module.exports = function purescriptLoader(source, map) { | |||
109 | cache.deferred.push(psModule) | 114 | cache.deferred.push(psModule) |
110 | 115 | ||
111 | if (!cache.compilationStarted) { | 116 | if (!cache.compilationStarted) { |
112 | return compile(psModule) | 117 | return Psc.compile(psModule) |
118 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | ||
119 | debug('rebuilt module map'); | ||
120 | cache.psModuleMap = map; | ||
121 | })) | ||
113 | .then(() => Promise.map(cache.deferred, psModule => { | 122 | .then(() => Promise.map(cache.deferred, psModule => { |
114 | if (typeof cache.ideServer === 'object') cache.ideServer.kill() | 123 | if (typeof cache.ideServer === 'object') cache.ideServer.kill() |
115 | return toJavaScript(psModule).then(psModule.load) | 124 | return toJavaScript(psModule).then(psModule.load) |
@@ -121,6 +130,26 @@ module.exports = function purescriptLoader(source, map) { | |||
121 | } | 130 | } |
122 | } | 131 | } |
123 | 132 | ||
133 | function updatePsModuleMap(psModule) { | ||
134 | const options = psModule.options | ||
135 | const cache = psModule.cache | ||
136 | const filePurs = psModule.srcPath | ||
137 | if (!cache.psModuleMap) { | ||
138 | debug('module mapping does not exist'); | ||
139 | return PsModuleMap.makeMap(options.src).then(map => { | ||
140 | cache.psModuleMap = map; | ||
141 | return cache.psModuleMap; | ||
142 | }); | ||
143 | } | ||
144 | else { | ||
145 | return PsModuleMap.makeMapEntry(filePurs).then(result => { | ||
146 | const map = Object.assign(cache.psModuleMap, result) | ||
147 | cache.psModuleMap = map; | ||
148 | return cache.psModuleMap; | ||
149 | }); | ||
150 | } | ||
151 | } | ||
152 | |||
124 | // The actual loader is executed *after* purescript compilation. | 153 | // The actual loader is executed *after* purescript compilation. |
125 | function toJavaScript(psModule) { | 154 | function toJavaScript(psModule) { |
126 | const options = psModule.options | 155 | const options = psModule.options |
@@ -132,7 +161,7 @@ function toJavaScript(psModule) { | |||
132 | 161 | ||
133 | return Promise.props({ | 162 | return Promise.props({ |
134 | js: fs.readFileAsync(jsPath, 'utf8'), | 163 | js: fs.readFileAsync(jsPath, 'utf8'), |
135 | psModuleMap: psModuleMap(options, cache) | 164 | psModuleMap: updatePsModuleMap(psModule) |
136 | }).then(result => { | 165 | }).then(result => { |
137 | let js = '' | 166 | let js = '' |
138 | 167 | ||
@@ -156,346 +185,3 @@ function toJavaScript(psModule) { | |||
156 | return js | 185 | return js |
157 | }) | 186 | }) |
158 | } | 187 | } |
159 | |||
160 | function compile(psModule) { | ||
161 | const options = psModule.options | ||
162 | const cache = psModule.cache | ||
163 | const stderr = [] | ||
164 | |||
165 | if (cache.compilationStarted) return Promise.resolve(psModule) | ||
166 | |||
167 | cache.compilationStarted = true | ||
168 | |||
169 | const args = dargs(Object.assign({ | ||
170 | _: options.src, | ||
171 | output: options.output, | ||
172 | }, options.pscArgs)) | ||
173 | |||
174 | debug('spawning compiler %s %o', options.psc, args) | ||
175 | |||
176 | return (new Promise((resolve, reject) => { | ||
177 | console.log('\nCompiling PureScript...') | ||
178 | |||
179 | const compilation = spawn(options.psc, args) | ||
180 | |||
181 | compilation.stdout.on('data', data => stderr.push(data.toString())) | ||
182 | compilation.stderr.on('data', data => stderr.push(data.toString())) | ||
183 | |||
184 | compilation.on('close', code => { | ||
185 | console.log('Finished compiling PureScript.') | ||
186 | cache.compilationFinished = true | ||
187 | if (code !== 0) { | ||
188 | cache.errors = stderr.join('') | ||
189 | reject(true) | ||
190 | } else { | ||
191 | cache.warnings = stderr.join('') | ||
192 | resolve(psModule) | ||
193 | } | ||
194 | }) | ||
195 | })) | ||
196 | .then(compilerOutput => { | ||
197 | if (options.bundle) { | ||
198 | return bundle(options, cache).then(() => psModule) | ||
199 | } | ||
200 | return psModule | ||
201 | }) | ||
202 | } | ||
203 | |||
204 | function rebuild(psModule) { | ||
205 | const options = psModule.options | ||
206 | const cache = psModule.cache | ||
207 | |||
208 | debug('attempting rebuild with psc-ide-client %s', psModule.srcPath) | ||
209 | |||
210 | const request = (body) => new Promise((resolve, reject) => { | ||
211 | const args = dargs(options.pscIdeArgs) | ||
212 | const ideClient = spawn('psc-ide-client', args) | ||
213 | |||
214 | var stdout = '' | ||
215 | var stderr = '' | ||
216 | |||
217 | ideClient.stdout.on('data', data => { | ||
218 | stdout = stdout + data.toString() | ||
219 | }) | ||
220 | |||
221 | ideClient.stderr.on('data', data => { | ||
222 | stderr = stderr + data.toString() | ||
223 | }) | ||
224 | |||
225 | ideClient.on('close', code => { | ||
226 | if (code !== 0) { | ||
227 | const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr | ||
228 | return reject(new Error(error)) | ||
229 | } | ||
230 | |||
231 | let res = null | ||
232 | |||
233 | try { | ||
234 | res = JSON.parse(stdout.toString()) | ||
235 | debug(res) | ||
236 | } catch (err) { | ||
237 | return reject(err) | ||
238 | } | ||
239 | |||
240 | if (res && !Array.isArray(res.result)) { | ||
241 | return res.resultType === 'success' | ||
242 | ? resolve(psModule) | ||
243 | : reject('psc-ide rebuild failed') | ||
244 | } | ||
245 | |||
246 | Promise.map(res.result, (item, i) => { | ||
247 | debug(item) | ||
248 | return formatIdeResult(item, options, i, res.result.length) | ||
249 | }) | ||
250 | .then(compileMessages => { | ||
251 | if (res.resultType === 'error') { | ||
252 | if (res.result.some(item => item.errorCode === 'UnknownModule')) { | ||
253 | console.log('Unknown module, attempting full recompile') | ||
254 | return compile(psModule) | ||
255 | .then(() => request({ command: 'load' })) | ||
256 | .then(resolve) | ||
257 | .catch(() => reject('psc-ide rebuild failed')) | ||
258 | } | ||
259 | cache.errors = compileMessages.join('\n') | ||
260 | reject('psc-ide rebuild failed') | ||
261 | } else { | ||
262 | cache.warnings = compileMessages.join('\n') | ||
263 | resolve(psModule) | ||
264 | } | ||
265 | }) | ||
266 | }) | ||
267 | |||
268 | ideClient.stdin.write(JSON.stringify(body)) | ||
269 | ideClient.stdin.write('\n') | ||
270 | }) | ||
271 | |||
272 | return request({ | ||
273 | command: 'rebuild', | ||
274 | params: { | ||
275 | file: psModule.srcPath, | ||
276 | } | ||
277 | }) | ||
278 | } | ||
279 | |||
280 | function formatIdeResult(result, options, index, length) { | ||
281 | const srcPath = path.relative(options.context, result.filename) | ||
282 | const pos = result.position | ||
283 | const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` | ||
284 | let numAndErr = `[${index+1}/${length} ${result.errorCode}]` | ||
285 | numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr | ||
286 | |||
287 | return fs.readFileAsync(result.filename, 'utf8').then(source => { | ||
288 | const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) | ||
289 | const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine | ||
290 | const up = options.pscIdeColors ? colors.red('^') : '^' | ||
291 | const down = options.pscIdeColors ? colors.red('v') : 'v' | ||
292 | let trimmed = lines.slice(0) | ||
293 | |||
294 | if (endsOnNewline) { | ||
295 | lines.splice(lines.length - 1, 1) | ||
296 | pos.endLine = pos.endLine - 1 | ||
297 | pos.endColumn = lines[lines.length - 1].length || 1 | ||
298 | } | ||
299 | |||
300 | // strip newlines at the end | ||
301 | if (endsOnNewline) { | ||
302 | trimmed = lines.reverse().reduce((trimmed, line, i) => { | ||
303 | if (i === 0 && line === '') trimmed.trimming = true | ||
304 | if (!trimmed.trimming) trimmed.push(line) | ||
305 | if (trimmed.trimming && line !== '') { | ||
306 | trimmed.trimming = false | ||
307 | trimmed.push(line) | ||
308 | } | ||
309 | return trimmed | ||
310 | }, []).reverse() | ||
311 | pos.endLine = pos.endLine - (lines.length - trimmed.length) | ||
312 | pos.endColumn = trimmed[trimmed.length - 1].length || 1 | ||
313 | } | ||
314 | |||
315 | const spaces = ' '.repeat(String(pos.endLine).length) | ||
316 | let snippet = trimmed.map((line, i) => { | ||
317 | return ` ${pos.startLine + i} ${line}` | ||
318 | }).join('\n') | ||
319 | |||
320 | if (trimmed.length === 1) { | ||
321 | snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` | ||
322 | } else { | ||
323 | snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` | ||
324 | snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` | ||
325 | } | ||
326 | |||
327 | return Promise.resolve( | ||
328 | `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}` | ||
329 | ) | ||
330 | }) | ||
331 | } | ||
332 | |||
333 | function bundle(options, cache) { | ||
334 | if (cache.bundle) return Promise.resolve(cache.bundle) | ||
335 | |||
336 | const stdout = [] | ||
337 | const stderr = cache.bundle = [] | ||
338 | |||
339 | const args = dargs(Object.assign({ | ||
340 | _: [path.join(options.output, '*', '*.js')], | ||
341 | output: options.bundleOutput, | ||
342 | namespace: options.bundleNamespace, | ||
343 | }, options.pscBundleArgs)) | ||
344 | |||
345 | cache.bundleModules.forEach(name => args.push('--module', name)) | ||
346 | |||
347 | debug('spawning bundler %s %o', options.pscBundle, args.join(' ')) | ||
348 | |||
349 | return (new Promise((resolve, reject) => { | ||
350 | console.log('Bundling PureScript...') | ||
351 | |||
352 | const compilation = spawn(options.pscBundle, args) | ||
353 | |||
354 | compilation.stdout.on('data', data => stdout.push(data.toString())) | ||
355 | compilation.stderr.on('data', data => stderr.push(data.toString())) | ||
356 | compilation.on('close', code => { | ||
357 | if (code !== 0) { | ||
358 | cache.errors = (cache.errors || '') + stderr.join('') | ||
359 | return reject(true) | ||
360 | } | ||
361 | cache.bundle = stderr | ||
362 | resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`)) | ||
363 | }) | ||
364 | })) | ||
365 | } | ||
366 | |||
367 | // map of PS module names to their source path | ||
368 | function psModuleMap(options, cache) { | ||
369 | if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap) | ||
370 | |||
371 | const globs = [].concat(options.src); | ||
372 | |||
373 | function pursToJs(file){ | ||
374 | const dirname = path.dirname(file) | ||
375 | const basename = path.basename(file, '.purs') | ||
376 | const fileJS = path.join(dirname, `${basename}.js`) | ||
377 | return fileJS | ||
378 | } | ||
379 | |||
380 | return globby(globs).then(paths => { | ||
381 | return Promise | ||
382 | .props(paths.reduce((map, file) => { | ||
383 | const fileJS = pursToJs(file) | ||
384 | map[file] = fs.readFileAsync(file, 'utf8') | ||
385 | map[fileJS] = fs.readFileAsync(fileJS, 'utf8').catch(() => undefined) | ||
386 | return map | ||
387 | }, {})) | ||
388 | .then(fileMap => { | ||
389 | cache.psModuleMap = Object.keys(fileMap).reduce((map, file) => { | ||
390 | const ext = path.extname(file) | ||
391 | const isPurs = ext.match(/purs$/i) | ||
392 | if (isPurs) { | ||
393 | const fileJs = pursToJs(file) | ||
394 | const source = fileMap[file] | ||
395 | const ffi = fileMap[fileJs] | ||
396 | const moduleName = match(srcModuleRegex, source) | ||
397 | map[moduleName] = map[moduleName] || {} | ||
398 | map[moduleName].src = path.resolve(file) | ||
399 | if (ffi) { | ||
400 | map[moduleName].ffi = path.resolve(fileJs) | ||
401 | } | ||
402 | } | ||
403 | return map | ||
404 | }, {}) | ||
405 | return cache.psModuleMap | ||
406 | }) | ||
407 | }) | ||
408 | } | ||
409 | |||
410 | function connectIdeServer(psModule) { | ||
411 | const options = psModule.options | ||
412 | const cache = psModule.cache | ||
413 | |||
414 | if (cache.ideServer) return Promise.resolve(psModule) | ||
415 | |||
416 | cache.ideServer = true | ||
417 | |||
418 | const connect = () => new Promise((resolve, reject) => { | ||
419 | const args = dargs(options.pscIdeArgs) | ||
420 | |||
421 | debug('attempting to connect to psc-ide-server', args) | ||
422 | |||
423 | const ideClient = spawn('psc-ide-client', args) | ||
424 | |||
425 | ideClient.stderr.on('data', data => { | ||
426 | debug(data.toString()) | ||
427 | cache.ideServer = false | ||
428 | reject(true) | ||
429 | }) | ||
430 | ideClient.stdout.once('data', data => { | ||
431 | debug(data.toString()) | ||
432 | if (data.toString()[0] === '{') { | ||
433 | const res = JSON.parse(data.toString()) | ||
434 | if (res.resultType === 'success') { | ||
435 | cache.ideServer = ideServer | ||
436 | resolve(psModule) | ||
437 | } else { | ||
438 | cache.ideServer = ideServer | ||
439 | reject(true) | ||
440 | } | ||
441 | } else { | ||
442 | cache.ideServer = false | ||
443 | reject(true) | ||
444 | } | ||
445 | }) | ||
446 | ideClient.stdin.resume() | ||
447 | ideClient.stdin.write(JSON.stringify({ command: 'load' })) | ||
448 | ideClient.stdin.write('\n') | ||
449 | }) | ||
450 | |||
451 | const args = dargs(Object.assign({ | ||
452 | outputDirectory: options.output, | ||
453 | }, options.pscIdeArgs)) | ||
454 | |||
455 | debug('attempting to start psc-ide-server', args) | ||
456 | |||
457 | const ideServer = cache.ideServer = spawn('psc-ide-server', []) | ||
458 | ideServer.stderr.on('data', data => { | ||
459 | debug(data.toString()) | ||
460 | }) | ||
461 | |||
462 | return retryPromise((retry, number) => { | ||
463 | return connect().catch(error => { | ||
464 | if (!cache.ideServer && number === 9) { | ||
465 | debug(error) | ||
466 | |||
467 | console.log( | ||
468 | 'failed to connect to or start psc-ide-server, ' + | ||
469 | 'full compilation will occur on rebuild' | ||
470 | ) | ||
471 | |||
472 | return Promise.resolve(psModule) | ||
473 | } | ||
474 | |||
475 | return retry(error) | ||
476 | }) | ||
477 | }, { | ||
478 | retries: 9, | ||
479 | factor: 1, | ||
480 | minTimeout: 333, | ||
481 | maxTimeout: 333, | ||
482 | }) | ||
483 | } | ||
484 | |||
485 | function match(regex, str) { | ||
486 | const matches = str.match(regex) | ||
487 | return matches && matches[1] | ||
488 | } | ||
489 | |||
490 | function dargs(obj) { | ||
491 | return Object.keys(obj).reduce((args, key) => { | ||
492 | const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase(); | ||
493 | const val = obj[key] | ||
494 | |||
495 | if (key === '_') val.forEach(v => args.push(v)) | ||
496 | else if (Array.isArray(val)) val.forEach(v => args.push(arg, v)) | ||
497 | else args.push(arg, obj[key]) | ||
498 | |||
499 | return args.filter(arg => (typeof arg !== 'boolean')) | ||
500 | }, []) | ||
501 | } | ||