X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=src%2Findex.js;h=ff1330232ec0b6d764f2828e85535357a55d75a4;hb=1029c0322c943313bcdd28145d518fbc83f661c7;hp=e09d247faea8db136443e4b11e2590d7fb3ad6a3;hpb=2d052df7f972f2a5fadf1711b13263a2e4dd6d33;p=github%2Ffretlink%2Fpurs-loader.git diff --git a/src/index.js b/src/index.js index e09d247..ff13302 100644 --- a/src/index.js +++ b/src/index.js @@ -1,472 +1,385 @@ 'use strict' -const colors = require('chalk') -const debug = require('debug')('purs-loader') +const debug_ = require('debug'); + +const debug = debug_('purs-loader'); + +const debugVerbose = debug_('purs-loader:verbose'); + const loaderUtils = require('loader-utils') -const globby = require('globby') + const Promise = require('bluebird') -const fs = Promise.promisifyAll(require('fs')) -const spawn = require('child_process').spawn + const path = require('path') -const retryPromise = require('promise-retry') -const psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i -const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g +const PsModuleMap = require('./purs-module-map'); -module.exports = function purescriptLoader(source, map) { - const callback = this.async() - const config = this.options - const query = loaderUtils.parseQuery(this.query) - const webpackOptions = this.options.purescriptLoader || {} +const compile = require('./compile'); - const options = Object.assign({ - context: config.context, - psc: 'psc', - pscArgs: {}, - pscBundle: 'psc-bundle', - pscBundleArgs: {}, - pscIde: false, - pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa', - pscIdeArgs: {}, - bundleOutput: 'output/bundle.js', - bundleNamespace: 'PS', - bundle: false, - warnings: true, - output: 'output', - src: [ - path.join('src', '**', '*.purs'), - path.join('bower_components', 'purescript-*', 'src', '**', '*.purs') - ], - ffi: [ - path.join('src', '**', '*.js'), - path.join('bower_components', 'purescript-*', 'src', '**', '*.js') - ], - }, webpackOptions, query) - - this.cacheable && this.cacheable() - - let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { - rebuild: false, - deferred: [], - bundleModules: [], - } +const bundle = require('./bundle'); - if (!config.purescriptLoaderInstalled) { - config.purescriptLoaderInstalled = true +const ide = require('./ide'); - // invalidate loader cache when bundle is marked as invalid (in watch mode) - this._compiler.plugin('invalid', () => { - cache = config.purescriptLoaderCache = { - rebuild: options.pscIde, - deferred: [], - ideServer: cache.ideServer - } - }) +const toJavaScript = require('./to-javascript'); - // add psc warnings to webpack compilation warnings - this._compiler.plugin('after-compile', (compilation, callback) => { - if (options.warnings && cache.warnings) { - compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings}`) - } +const sourceMaps = require('./source-maps'); - if (cache.errors) { - compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`) - } +const spawn = require('cross-spawn').sync - callback() - }) - } +const eol = require('os').EOL - const psModuleName = match(psModuleRegex, source) - const psModule = { - name: psModuleName, - load: js => callback(null, js), - reject: error => callback(error), - srcPath: this.resourcePath, - srcDir: path.dirname(this.resourcePath), - jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')), - options: options, - cache: cache, - } +var CACHE_VAR = { + rebuild: false, + deferred: [], + bundleModules: [], + ideServer: null, + psModuleMap: null, + warnings: [], + errors: [], + compilationStarted: false, + compilationFinished: false, + installed: false, + srcOption: [], + spagoOutputPath: null +}; - debug('loader called', psModule.name) +// include src files provided by psc-package or Spago +function requestDependencySources(packagerCommand, srcPath, loaderOptions) { + const packagerArgs = ['sources']; - if (options.bundle) { - cache.bundleModules.push(psModule.name) - } + const loaderSrc = loaderOptions.src || [ + srcPath + ]; - if (cache.rebuild) { - return connectIdeServer(psModule) - .then(rebuild) - .then(toJavaScript) - .then(psModule.load) - .catch(psModule.reject) - } - - if (cache.compilationFinished) { - return toJavaScript(psModule).then(psModule.load).catch(psModule.reject) - } + debug('%s %o', packagerCommand, packagerArgs); - // We need to wait for compilation to finish before the loaders run so that - // references to compiled output are valid. - cache.deferred.push(psModule) + const cmd = spawn(packagerCommand, packagerArgs); - if (!cache.compilationStarted) { - return compile(psModule) - .then(() => Promise.map(cache.deferred, psModule => { - if (typeof cache.ideServer === 'object') cache.ideServer.kill() - return toJavaScript(psModule).then(psModule.load) - })) - .catch(error => { - cache.deferred[0].reject(error) - cache.deferred.slice(1).forEach(psModule => psModule.reject(true)) - }) + if (cmd.error) { + throw new Error(cmd.error); } -} + else if (cmd.status !== 0) { + const error = cmd.stdout.toString(); -// The actual loader is executed *after* purescript compilation. -function toJavaScript(psModule) { - const options = psModule.options - const cache = psModule.cache - const bundlePath = path.resolve(options.bundleOutput) - const jsPath = cache.bundle ? bundlePath : psModule.jsPath - - debug('loading JavaScript for', psModule.name) - - return Promise.props({ - js: fs.readFileAsync(jsPath, 'utf8'), - psModuleMap: psModuleMap(options.src, cache) - }).then(result => { - let js = '' - - if (options.bundle) { - // if bundling, return a reference to the bundle - js = 'module.exports = require("' - + path.relative(psModule.srcDir, options.bundleOutput) - + '")["' + psModule.name + '"]' - } else { - // replace require paths to output files generated by psc with paths - // to purescript sources, which are then also run through this loader. - const foreignRequire = 'require("' + path.resolve( - path.join(psModule.options.output, psModule.name, 'foreign.js') - ) + '")' - - js = result.js - .replace(requireRegex, (m, p1) => { - return 'require("' + result.psModuleMap[p1] + '")' - }) - .replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire) - } + throw new Error(error); + } + else { + const result = cmd.stdout.toString().split(eol).filter(v => v != '').concat(loaderSrc); - return js - }) -} + debug('%s result: %o', packagerCommand, result); -function compile(psModule) { - const options = psModule.options - const cache = psModule.cache - const stderr = [] + CACHE_VAR.srcOption = result; - if (cache.compilationStarted) return Promise.resolve(psModule) + return result; + } +} - cache.compilationStarted = true +// 'spago output path' will return the output folder in a monorepo +function getSpagoSources() { + const cachedVal = CACHE_VAR.spagoOutputPath; + if (cachedVal) { + return cachedVal + } + const command = "spago" + const args = ["path", "output"] - const args = dargs(Object.assign({ - _: options.src, - ffi: options.ffi, - output: options.output, - }, options.pscArgs)) + const cmd = spawn(command, args); - debug('spawning compiler %s %o', options.psc, args) + if (cmd.error) { + throw new Error(cmd.error); + } + else if (cmd.status !== 0) { + const error = cmd.stdout.toString(); - return (new Promise((resolve, reject) => { - console.log('\nCompiling PureScript...') + throw new Error(error); + } + else { + const result = cmd.stdout.toString().split(eol)[0] - const compilation = spawn(options.psc, args) + debug('"spago path output" result: %o', result); - compilation.stdout.on('data', data => stderr.push(data.toString())) - compilation.stderr.on('data', data => stderr.push(data.toString())) + CACHE_VAR.spagoOutputPath = result; - compilation.on('close', code => { - console.log('Finished compiling PureScript.') - cache.compilationFinished = true - if (code !== 0) { - cache.errors = stderr.join('') - reject(true) - } else { - cache.warnings = stderr.join('') - resolve(psModule) - } - }) - })) - .then(compilerOutput => { - if (options.bundle) { - return bundle(options, cache).then(() => psModule) - } - return psModule - }) + return result; + } } -function rebuild(psModule) { - const options = psModule.options - const cache = psModule.cache +module.exports = function purescriptLoader(source, map) { + this.cacheable && this.cacheable(); - debug('attempting rebuild with psc-ide-client %s', psModule.srcPath) + const webpackContext = (this.options && this.options.context) || this.rootContext; - const request = (body) => new Promise((resolve, reject) => { - const args = dargs(options.pscIdeArgs) - const ideClient = spawn('psc-ide-client', args) + const callback = this.async(); - ideClient.stdout.once('data', data => { - let res = null + const loaderOptions = loaderUtils.getOptions(this) || {}; - try { - res = JSON.parse(data.toString()) - debug(res) - } catch (err) { - return reject(err) - } + const srcOption = ((pscPackage, spago) => { + const srcPath = path.join('src', '**', '*.purs'); - if (res && !Array.isArray(res.result)) { - return res.resultType === 'success' - ? resolve(psModule) - : reject('psc-ide rebuild failed') - } + const bowerPath = path.join('bower_components', 'purescript-*', 'src', '**', '*.purs'); - Promise.map(res.result, (item, i) => { - debug(item) - return formatIdeResult(item, options, i, res.result.length) - }) - .then(compileMessages => { - if (res.resultType === 'error') { - if (res.result.some(item => item.errorCode === 'UnknownModule')) { - console.log('Unknown module, attempting full recompile') - return compile(psModule) - .then(() => request({ command: 'load' })) - .then(resolve) - .catch(() => reject('psc-ide rebuild failed')) - } - cache.errors = compileMessages.join('\n') - reject('psc-ide rebuild failed') - } else { - cache.warnings = compileMessages.join('\n') - resolve(psModule) - } - }) - }) + if (CACHE_VAR.srcOption.length > 0) { + return CACHE_VAR.srcOption; + } + else if (pscPackage) { + return requestDependencySources('psc-package', srcPath, loaderOptions) + } + else if (spago) { + return requestDependencySources('spago', srcPath, loaderOptions) + } + else { + const result = loaderOptions.src || [ + bowerPath, + srcPath + ]; + + CACHE_VAR.srcOption = result; + + return result; + } + })(loaderOptions.pscPackage, loaderOptions.spago); + + const outputPath = loaderOptions.spago ? getSpagoSources() : 'output' - ideClient.stderr.once('data', data => reject(data.toString())) + const options = Object.assign({ + context: webpackContext, + psc: null, + pscArgs: {}, + pscBundle: null, + pscBundleArgs: {}, + pscIdeClient: null, + pscIdeClientArgs: {}, + pscIdeServer: null, + pscIdeServerArgs: {}, + pscIdeRebuildArgs: {}, + pscIde: false, + pscIdeColors: loaderOptions.psc === 'psa', + pscPackage: false, + spago: false, + bundleOutput: 'output/bundle.js', + bundleNamespace: 'PS', + bundle: false, + warnings: true, + watch: false, + output: outputPath, + src: [] + }, loaderOptions, { + src: srcOption + }); - ideClient.stdin.write(JSON.stringify(body)) - ideClient.stdin.write('\n') - }) + if (!CACHE_VAR.installed) { + debugVerbose('installing purs-loader with options: %O', options); - return request({ - command: 'rebuild', - params: { - file: psModule.srcPath, - } - }) -} + CACHE_VAR.installed = true; -function formatIdeResult(result, options, index, length) { - const srcPath = path.relative(options.context, result.filename) - const pos = result.position - const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` - let numAndErr = `[${index+1}/${length} ${result.errorCode}]` - numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr - - return fs.readFileAsync(result.filename, 'utf8').then(source => { - const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) - const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine - const up = options.pscIdeColors ? colors.red('^') : '^' - const down = options.pscIdeColors ? colors.red('v') : 'v' - let trimmed = lines.slice(0) - - if (endsOnNewline) { - lines.splice(lines.length - 1, 1) - pos.endLine = pos.endLine - 1 - pos.endColumn = lines[lines.length - 1].length || 1 - } + const invalidCb = () => { + debugVerbose('invalidating loader CACHE_VAR'); - // strip newlines at the end - if (endsOnNewline) { - trimmed = lines.reverse().reduce((trimmed, line, i) => { - if (i === 0 && line === '') trimmed.trimming = true - if (!trimmed.trimming) trimmed.push(line) - if (trimmed.trimming && line !== '') { - trimmed.trimming = false - trimmed.push(line) - } - return trimmed - }, []).reverse() - pos.endLine = pos.endLine - (lines.length - trimmed.length) - pos.endColumn = trimmed[trimmed.length - 1].length || 1 + CACHE_VAR = { + rebuild: options.pscIde, + deferred: [], + bundleModules: [], + ideServer: CACHE_VAR.ideServer, + psModuleMap: CACHE_VAR.psModuleMap, + warnings: [], + errors: [], + compilationStarted: false, + compilationFinished: false, + installed: CACHE_VAR.installed, + srcOption: [] + }; } - const spaces = ' '.repeat(String(pos.endLine).length) - let snippet = trimmed.map((line, i) => { - return ` ${pos.startLine + i} ${line}` - }).join('\n') - - if (trimmed.length === 1) { - snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` + // invalidate loader CACHE_VAR when bundle is marked as invalid (in watch mode) + if(this._compiler.hooks){ + this._compiler.hooks.invalid.tap('purs-loader', invalidCb); } else { - snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` - snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` + this._compiler.plugin('invalid', invalidCb); } - return Promise.resolve( - `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}` - ) - }) -} - -function bundle(options, cache) { - if (cache.bundle) return Promise.resolve(cache.bundle) + const afterCompileCb = (compilation, callback) => { + CACHE_VAR.warnings.forEach(warning => { + compilation.warnings.push(warning); + }); - const stdout = [] - const stderr = cache.bundle = [] + CACHE_VAR.errors.forEach(error => { + compilation.errors.push(error); + }); - const args = dargs(Object.assign({ - _: [path.join(options.output, '*', '*.js')], - output: options.bundleOutput, - namespace: options.bundleNamespace, - }, options.pscBundleArgs)) - - cache.bundleModules.forEach(name => args.push('--module', name)) - - debug('spawning bundler %s %o', options.pscBundle, args.join(' ')) + callback() + } - return (new Promise((resolve, reject) => { - console.log('Bundling PureScript...') + // add psc warnings to webpack compilation warnings + if(this._compiler.hooks) { + this._compiler.hooks.afterCompile.tapAsync('purs-loader', afterCompileCb); + } else { + this._compiler.plugin('after-compile', afterCompileCb); + } + } - const compilation = spawn(options.pscBundle, args) + const psModuleName = PsModuleMap.matchModule(source); - compilation.stdout.on('data', data => stdout.push(data.toString())) - compilation.stderr.on('data', data => stderr.push(data.toString())) - compilation.on('close', code => { - if (code !== 0) { - cache.errors = (cache.errors || '') + stderr.join('') - return reject(true) + const psModule = { + name: psModuleName, + source: source, + load: ({js, map}) => callback(null, js, map), + reject: error => callback(error), + srcPath: this.resourcePath, + remainingRequest: loaderUtils.getRemainingRequest(this), + srcDir: path.dirname(this.resourcePath), + jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')), + options: options, + cache: CACHE_VAR, + emitWarning: warning => { + if (options.warnings && warning.length) { + CACHE_VAR.warnings.push(warning); } - cache.bundle = stderr - resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`)) - }) - })) -} - -// map of PS module names to their source path -function psModuleMap(globs, cache) { - if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap) - - return globby(globs).then(paths => { - return Promise - .props(paths.reduce((map, file) => { - map[file] = fs.readFileAsync(file, 'utf8') - return map - }, {})) - .then(srcMap => { - cache.psModuleMap = Object.keys(srcMap).reduce((map, file) => { - const source = srcMap[file] - const psModuleName = match(psModuleRegex, source) - map[psModuleName] = path.resolve(file) - return map - }, {}) - return cache.psModuleMap - }) - }) -} + }, + emitError: error => { + if (error.length) { + CACHE_VAR.errors.push(error); + } + } + } -function connectIdeServer(psModule) { - const options = psModule.options - const cache = psModule.cache + debug('loading %s', psModule.name); - if (cache.ideServer) return Promise.resolve(psModule) + if (options.bundle) { + CACHE_VAR.bundleModules.push(psModule.name); + } - cache.ideServer = true + if (CACHE_VAR.rebuild) { + const connect = () => { + if (!CACHE_VAR.ideServer) { + CACHE_VAR.ideServer = true; + + return ide.connect(psModule) + .then(ideServer => { + CACHE_VAR.ideServer = ideServer; + return psModule; + }) + .then(ide.loadWithRetry) + .catch(error => { + if (CACHE_VAR.ideServer.kill) { + debug('ide failed to initially load modules, stopping the ide server process'); + + CACHE_VAR.ideServer.kill(); + } + + CACHE_VAR.ideServer = null; + + return Promise.reject(error); + }) + ; + } + else { + return Promise.resolve(psModule); + } + }; + + const rebuild = () => + ide.rebuild(psModule) + .then(() => + toJavaScript(psModule) + .then(js => sourceMaps(psModule, js)) + .then(psModule.load) + .catch(psModule.reject) + ) + .catch(error => { + if (error instanceof ide.UnknownModuleError) { + // Store the modules that trigger a recompile due to an + // unknown module error. We need to wait until compilation is + // done before loading these files. - const connect = () => new Promise((resolve, reject) => { - const args = dargs(options.pscIdeArgs) + CACHE_VAR.deferred.push(psModule); - debug('attempting to connect to psc-ide-server', args) + if (!CACHE_VAR.compilationStarted) { + CACHE_VAR.compilationStarted = true; - const ideClient = spawn('psc-ide-client', args) + return compile(psModule) + .then(() => { + CACHE_VAR.compilationFinished = true; + }) + .then(() => + Promise.map(CACHE_VAR.deferred, psModule => + ide.load(psModule) + .then(() => toJavaScript(psModule)) + .then(js => sourceMaps(psModule, js)) + .then(psModule.load) + ) + ) + .catch(error => { + CACHE_VAR.deferred[0].reject(error); + + CACHE_VAR.deferred.slice(1).forEach(psModule => { + psModule.reject(new Error('purs-loader failed')); + }) + }) + ; + } + else { + // The compilation has started. We must wait until it is + // done in order to ensure the module map contains all of + // the unknown modules. + } + } + else { + debug('ide rebuild failed due to an unhandled error: %o', error); - ideClient.stderr.on('data', data => { - debug(data.toString()) - cache.ideServer = false - reject(true) - }) - ideClient.stdout.once('data', data => { - debug(data.toString()) - if (data.toString()[0] === '{') { - const res = JSON.parse(data.toString()) - if (res.resultType === 'success') { - cache.ideServer = ideServer - resolve(psModule) - } else { - cache.ideServer = ideServer - reject(true) + psModule.reject(error); } - } else { - cache.ideServer = false - reject(true) - } - }) - ideClient.stdin.resume() - ideClient.stdin.write(JSON.stringify({ command: 'load' })) - ideClient.stdin.write('\n') - }) - - const args = dargs(Object.assign({ - outputDirectory: options.output, - }, options.pscIdeArgs)) - - debug('attempting to start psc-ide-server', args) - - const ideServer = cache.ideServer = spawn('psc-ide-server', []) - ideServer.stderr.on('data', data => { - debug(data.toString()) - }) - - return retryPromise((retry, number) => { - return connect().catch(error => { - if (!cache.ideServer && number === 9) { - debug(error) - - console.log( - 'failed to connect to or start psc-ide-server, ' + - 'full compilation will occur on rebuild' - ) + }) + ; - return Promise.resolve(psModule) - } + connect().then(rebuild); + } + else if (CACHE_VAR.compilationFinished) { + debugVerbose('compilation is already finished, loading module %s', psModule.name); - return retry(error) - }) - }, { - retries: 9, - factor: 1, - minTimeout: 333, - maxTimeout: 333, - }) -} + toJavaScript(psModule) + .then(js => sourceMaps(psModule, js)) + .then(psModule.load) + .catch(psModule.reject); + } + else { + // The compilation has not finished yet. We need to wait for + // compilation to finish before the loaders run so that references + // to compiled output are valid. Push the modules into the CACHE_VAR to + // be loaded once the complation is complete. -function match(regex, str) { - const matches = str.match(regex) - return matches && matches[1] -} + CACHE_VAR.deferred.push(psModule); -function dargs(obj) { - return Object.keys(obj).reduce((args, key) => { - const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase(); - const val = obj[key] + if (!CACHE_VAR.compilationStarted) { + CACHE_VAR.compilationStarted = true; - if (key === '_') val.forEach(v => args.push(v)) - else if (Array.isArray(val)) val.forEach(v => args.push(arg, v)) - else args.push(arg, obj[key]) + compile(psModule) + .then(() => { + CACHE_VAR.compilationFinished = true; + }) + .then(() => { + if (options.bundle) { + return bundle(options, CACHE_VAR.bundleModules); + } + }) + .then(() => + Promise.map(CACHE_VAR.deferred, psModule => + toJavaScript(psModule) + .then(js => sourceMaps(psModule, js)) + .then(psModule.load) + ) + ) + .catch(error => { + CACHE_VAR.deferred[0].reject(error); - return args.filter(arg => (typeof arg !== 'boolean')) - }, []) + CACHE_VAR.deferred.slice(1).forEach(psModule => { + psModule.reject(new Error('purs-loader failed')); + }) + }) + ; + } + else { + // The complation has started. Nothing to do but wait until it is + // done before loading all of the modules. + } + } }