From d3f40b6f0b0f507308f8dfd91e9cf6d4745dbce8 Mon Sep 17 00:00:00 2001 From: Cyril Sobierajewicz Date: Mon, 3 Dec 2018 16:07:05 +0100 Subject: [PATCH] Build v3.3.1 --- lib/bundle.js | 63 ++++++ lib/compile.js | 64 ++++++ lib/dargs.js | 7 + lib/ide.js | 270 +++++++++++++++++++++++++ lib/index.js | 434 +++++++++++++++++++++++++++++++++++++++++ lib/purs-module-map.js | 78 ++++++++ lib/source-maps.js | 61 ++++++ lib/to-javascript.js | 152 +++++++++++++++ lib/utils.js | 63 ++++++ 9 files changed, 1192 insertions(+) create mode 100644 lib/bundle.js create mode 100644 lib/compile.js create mode 100644 lib/dargs.js create mode 100644 lib/ide.js create mode 100644 lib/index.js create mode 100644 lib/purs-module-map.js create mode 100644 lib/source-maps.js create mode 100644 lib/to-javascript.js create mode 100644 lib/utils.js diff --git a/lib/bundle.js b/lib/bundle.js new file mode 100644 index 0000000..943e08b --- /dev/null +++ b/lib/bundle.js @@ -0,0 +1,63 @@ +'use strict'; + +var path = require('path'); + +var Promise = require('bluebird'); + +var fs = Promise.promisifyAll(require('fs')); + +var spawn = require('cross-spawn'); + +var debug = require('debug')('purs-loader'); + +var dargs = require('./dargs'); + +module.exports = function bundle(options, bundleModules) { + var stdout = []; + + var stderr = []; + + var bundleCommand = options.pscBundle || 'purs'; + + var bundleArgs = (options.pscBundle ? [] : ['bundle']).concat(dargs(Object.assign({ + _: [path.join(options.output, '*', '*.js')], + output: options.bundleOutput, + namespace: options.bundleNamespace + }, options.pscBundleArgs))); + + bundleModules.forEach(function (name) { + return bundleArgs.push('--module', name); + }); + + debug('bundle: %s %O', bundleCommand, bundleArgs); + + return new Promise(function (resolve, reject) { + debug('bundling PureScript...'); + + var compilation = spawn(bundleCommand, bundleArgs); + + compilation.stdout.on('data', function (data) { + return stdout.push(data.toString()); + }); + + compilation.stderr.on('data', function (data) { + return stderr.push(data.toString()); + }); + + compilation.on('close', function (code) { + debug('finished bundling PureScript.'); + + if (code !== 0) { + var errorMessage = stderr.join(''); + + if (errorMessage.length) { + psModule.emitError(errorMessage); + } + + reject(new Error('bundling failed')); + } else { + resolve(fs.appendFileAsync(options.bundleOutput, 'module.exports = ' + options.bundleNamespace)); + } + }); + }); +}; \ No newline at end of file diff --git a/lib/compile.js b/lib/compile.js new file mode 100644 index 0000000..a1bf211 --- /dev/null +++ b/lib/compile.js @@ -0,0 +1,64 @@ +'use strict'; + +var Promise = require('bluebird'); + +var spawn = require('cross-spawn'); + +var debug_ = require('debug'); + +var debug = debug_('purs-loader'); + +var debugVerbose = debug_('purs-loader:verbose'); + +var dargs = require('./dargs'); + +module.exports = function compile(psModule) { + var options = psModule.options; + + var compileCommand = options.psc || 'purs'; + + var compileArgs = (options.psc ? [] : ['compile']).concat(dargs(Object.assign({ + _: options.src, + output: options.output + }, options.pscArgs))); + + var stderr = []; + + debug('compile %s %O', compileCommand, compileArgs); + + return new Promise(function (resolve, reject) { + debug('compiling PureScript...'); + + var compilation = spawn(compileCommand, compileArgs); + + compilation.stderr.on('data', function (data) { + stderr.push(data.toString()); + }); + + compilation.stdout.on('data', function (data) { + debugVerbose(data.toString()); + }); + + compilation.on('close', function (code) { + debug('finished compiling PureScript.'); + + if (code !== 0) { + var errorMessage = stderr.join(''); + if (errorMessage.length) { + psModule.emitError(errorMessage); + } + if (options.watch) { + resolve(psModule); + } else { + reject(new Error('compilation failed')); + } + } else { + var warningMessage = stderr.join(''); + if (options.warnings && warningMessage.length) { + psModule.emitWarning(warningMessage); + } + resolve(psModule); + } + }); + }); +}; \ No newline at end of file diff --git a/lib/dargs.js b/lib/dargs.js new file mode 100644 index 0000000..191ab44 --- /dev/null +++ b/lib/dargs.js @@ -0,0 +1,7 @@ +'use strict'; + +var dargs = require('dargs'); + +module.exports = function (obj) { + return dargs(obj, { ignoreFalse: true }); +}; \ No newline at end of file diff --git a/lib/ide.js b/lib/ide.js new file mode 100644 index 0000000..6087e1c --- /dev/null +++ b/lib/ide.js @@ -0,0 +1,270 @@ +'use strict'; + +var path = require('path'); + +var Promise = require('bluebird'); + +var fs = Promise.promisifyAll(require('fs')); + +var retryPromise = require('promise-retry'); + +var spawn = require('cross-spawn'); + +var colors = require('chalk'); + +var debug_ = require('debug'); + +var debug = debug_('purs-loader'); + +var debugVerbose = debug_('purs-loader:verbose'); + +var dargs = require('./dargs'); + +var compile = require('./compile'); + +var PsModuleMap = require('./purs-module-map'); + +function UnknownModuleError() { + this.name = 'UnknownModuleError'; + this.stack = new Error().stack; +} + +UnknownModuleError.prototype = Object.create(Error.prototype); + +UnknownModuleError.prototype.constructor = UnknownModuleError; + +module.exports.UnknownModuleError = UnknownModuleError; + +function spawnIdeClient(body, options) { + var ideClientCommand = options.pscIdeClient || 'purs'; + + var ideClientArgs = (options.pscIdeClient ? [] : ['ide', 'client']).concat(dargs(options.pscIdeClientArgs)); + + var stderr = []; + + var stdout = []; + + debug('ide client %s %o %O', ideClientCommand, ideClientArgs, body); + + return new Promise(function (resolve, reject) { + var ideClient = spawn(ideClientCommand, ideClientArgs); + + ideClient.stderr.on('data', function (data) { + stderr.push(data.toString()); + }); + + ideClient.stdout.on('data', function (data) { + stdout.push(data.toString()); + }); + + ideClient.on('close', function (code) { + if (code !== 0) { + var errorMessage = stderr.join(''); + + reject(new Error('ide client failed: ' + errorMessage)); + } else { + var result = stdout.join(''); + + resolve(result); + } + }); + + ideClient.stdin.resume(); + + ideClient.stdin.write(JSON.stringify(body)); + + ideClient.stdin.write('\n'); + }); +} + +function formatIdeResult(result, options, index, length) { + var numAndErr = '[' + (index + 1) + '/' + length + ' ' + result.errorCode + ']'; + numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr; + + function makeResult() { + return Promise.resolve('\n' + numAndErr + ' ' + result.message); + } + + function makeResultSnippet(filename, pos) { + var srcPath = path.relative(options.context, filename); + var fileAndPos = srcPath + ':' + pos.startLine + ':' + pos.startColumn; + + return fs.readFileAsync(filename, 'utf8').then(function (source) { + var lines = source.split('\n').slice(pos.startLine - 1, pos.endLine); + var endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine; + var up = options.pscIdeColors ? colors.red('^') : '^'; + var down = options.pscIdeColors ? colors.red('v') : 'v'; + var 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; + } + + // strip newlines at the end + if (endsOnNewline) { + trimmed = lines.reverse().reduce(function (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; + } + + var spaces = ' '.repeat(String(pos.endLine).length); + var snippet = trimmed.map(function (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); + } else { + snippet = ' ' + spaces + ' ' + ' '.repeat(pos.startColumn - 1) + down + '\n' + snippet; + snippet += '\n ' + spaces + ' ' + ' '.repeat(pos.endColumn - 1) + up; + } + + return Promise.resolve('\n' + numAndErr + ' ' + fileAndPos + '\n\n' + snippet + '\n\n' + result.message); + }).catch(function (error) { + debug('failed to format ide result: %o', error); + + return Promise.resolve(''); + }); + } + + return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult(); +} + +module.exports.connect = function connect(psModule) { + var options = psModule.options; + + var serverCommand = options.pscIdeServer || 'purs'; + + var serverArgs = (options.pscIdeServer ? [] : ['ide', 'server']).concat(dargs(Object.assign({ + outputDirectory: options.output, + '_': options.src + }, options.pscIdeServerArgs))); + + debug('ide server: %s %o', serverCommand, serverArgs); + + var ideServer = spawn(serverCommand, serverArgs); + + ideServer.stdout.on('data', function (data) { + debugVerbose('ide server stdout: %s', data.toString()); + }); + + ideServer.stderr.on('data', function (data) { + debugVerbose('ide server stderr: %s', data.toString()); + }); + + ideServer.on('error', function (error) { + debugVerbose('ide server error: %o', error); + }); + + ideServer.on('close', function (code, signal) { + debugVerbose('ide server close: %s %s', code, signal); + }); + + return Promise.resolve(ideServer); +}; + +module.exports.load = function load(psModule) { + var options = psModule.options; + + var body = { command: 'load' }; + + return spawnIdeClient(body, options); +}; + +module.exports.loadWithRetry = function loadWithRetry(psModule) { + var retries = 9; + + return retryPromise(function (retry, number) { + debugVerbose('attempting to load modules (%d out of %d attempts)', number, retries); + + return module.exports.load(psModule).catch(retry); + }, { + retries: retries, + factor: 1, + minTimeout: 333, + maxTimeout: 333 + }).then(function () { + return psModule; + }); +}; + +module.exports.rebuild = function rebuild(psModule) { + var options = psModule.options; + + var body = { + command: 'rebuild', + params: { + file: psModule.srcPath + } + }; + + var parseResponse = function parseResponse(response) { + try { + var parsed = JSON.parse(response); + + debugVerbose('parsed JSON response: %O', parsed); + + return Promise.resolve(parsed); + } catch (error) { + return Promise.reject(error); + } + }; + + var formatResponse = function formatResponse(parsed) { + var result = Array.isArray(parsed.result) ? parsed.result : []; + + return Promise.map(result, function (item, i) { + debugVerbose('formatting result %O', item); + + return formatIdeResult(item, options, i, result.length); + }).then(function (formatted) { + return { + parsed: parsed, + formatted: formatted, + formattedMessage: formatted.join('\n') + }; + }); + }; + + return spawnIdeClient(body, options).then(parseResponse).then(formatResponse).then(function (_ref) { + var parsed = _ref.parsed, + formatted = _ref.formatted, + formattedMessage = _ref.formattedMessage; + + if (parsed.resultType === 'success') { + if (options.warnings && formattedMessage.length) { + psModule.emitWarning(formattedMessage); + } + + return psModule; + } else if ((parsed.result || []).some(function (item) { + var isModuleNotFound = item.errorCode === 'ModuleNotFound'; + + var isUnknownModule = item.errorCode === 'UnknownModule'; + + var isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message); + + return isModuleNotFound || isUnknownModule || isUnknownModuleImport; + })) { + debug('module %s was not rebuilt because the module is unknown', psModule.name); + + return Promise.reject(new UnknownModuleError()); + } else { + if (formattedMessage.length) { + psModule.emitError(formattedMessage); + } + + return psModule; + } + }); +}; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..e648869 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,434 @@ +'use strict'; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var debug_ = require('debug'); + +var debug = debug_('purs-loader'); + +var debugVerbose = debug_('purs-loader:verbose'); + +var loaderUtils = require('loader-utils'); + +var Promise = require('bluebird'); + +var path = require('path'); + +var PsModuleMap = require('./purs-module-map'); + +var compile = require('./compile'); + +var bundle = require('./bundle'); + +var ide = require('./ide'); + +var toJavaScript = require('./to-javascript'); + +var sourceMaps = require('./source-maps'); + +var dargs = require('./dargs'); + +var utils = require('./utils'); + +var spawn = require('cross-spawn').sync; + +var eol = require('os').EOL; + +var CACHE_VAR = { + rebuild: false, + deferred: [], + bundleModules: [], + ideServer: null, + psModuleMap: null, + warnings: [], + errors: [], + compilationStarted: false, + compilationFinished: false, + compilationFailed: false, + installed: false, + srcOption: [] +}; + +module.exports = function purescriptLoader(source, map) { + var _this = this; + + this.cacheable && this.cacheable(); + + var webpackContext = this.options && this.options.context || this.rootContext; + + var callback = this.async(); + + var loaderOptions = loaderUtils.getOptions(this) || {}; + + var srcOption = function (pscPackage) { + var srcPath = path.join('src', '**', '*.purs'); + + var bowerPath = path.join('bower_components', 'purescript-*', 'src', '**', '*.purs'); + + if (CACHE_VAR.srcOption.length > 0) { + return CACHE_VAR.srcOption; + } else if (pscPackage) { + var pscPackageCommand = 'psc-package'; + + var pscPackageArgs = ['sources']; + + var loaderSrc = loaderOptions.src || [srcPath]; + + debug('psc-package %s %o', pscPackageCommand, pscPackageArgs); + + var cmd = spawn(pscPackageCommand, pscPackageArgs); + + if (cmd.error) { + throw new Error(cmd.error); + } else if (cmd.status !== 0) { + var error = cmd.stdout.toString(); + + throw new Error(error); + } else { + var result = cmd.stdout.toString().split(eol).filter(function (v) { + return v != ''; + }).concat(loaderSrc); + + debug('psc-package result: %o', result); + + CACHE_VAR.srcOption = result; + + return result; + } + } else { + var _result = loaderOptions.src || [bowerPath, srcPath]; + + CACHE_VAR.srcOption = _result; + + return _result; + } + }(loaderOptions.pscPackage); + + var options = Object.assign({ + context: webpackContext, + psc: null, + pscArgs: {}, + pscBundle: null, + pscBundleArgs: {}, + pscIdeClient: null, + pscIdeClientArgs: {}, + pscIdeServer: null, + pscIdeServerArgs: {}, + pscIde: false, + pscIdeColors: loaderOptions.psc === 'psa', + pscPackage: false, + bundleOutput: 'output/bundle.js', + bundleNamespace: 'PS', + bundle: false, + warnings: true, + watch: false, + output: 'output', + src: [] + }, loaderOptions, { + src: srcOption + }); + + if (!CACHE_VAR.installed) { + debugVerbose('installing purs-loader with options: %O', options); + + CACHE_VAR.installed = true; + + // invalidate loader CACHE_VAR when bundle is marked as invalid (in watch mode) + this._compiler.plugin('invalid', function () { + debugVerbose('invalidating loader CACHE_VAR'); + + CACHE_VAR = { + rebuild: options.pscIde, + deferred: [], + bundleModules: [], + ideServer: CACHE_VAR.ideServer, + psModuleMap: CACHE_VAR.psModuleMap, + warnings: [], + errors: [], + compilationStarted: false, + compilationFinished: false, + compilationFailed: false, + installed: CACHE_VAR.installed, + srcOption: [] + }; + }); + + // add psc warnings to webpack compilation warnings + this._compiler.plugin('after-compile', function (compilation, callback) { + CACHE_VAR.warnings.forEach(function (warning) { + compilation.warnings.push(warning); + }); + + CACHE_VAR.errors.forEach(function (error) { + compilation.errors.push(error); + }); + + callback(); + }); + } + + var psModuleName = PsModuleMap.matchModule(source); + + var psModule = { + name: psModuleName, + source: source, + load: function load(_ref) { + var js = _ref.js, + map = _ref.map; + return callback(null, js, map); + }, + reject: function reject(error) { + return 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: function emitWarning(warning) { + if (options.warnings && warning.length) { + CACHE_VAR.warnings.push(warning); + } + }, + emitError: function emitError(pscMessage) { + if (pscMessage.length) { + var modules = []; + + var matchErrorsSeparator = /\n(?=Error)/; + var errors = pscMessage.split(matchErrorsSeparator); + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = errors[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var error = _step.value; + + var matchErrLocation = /at (.+\.purs) line (\d+), column (\d+) - line (\d+), column (\d+)/; + + var _ref2 = matchErrLocation.exec(error) || [], + _ref3 = _slicedToArray(_ref2, 2), + filename = _ref3[1]; + + if (!filename) continue; + + var baseModulePath = path.join(_this.rootContext, filename); + _this.addDependency(baseModulePath); + + var matchErrModuleName = /in module ((?:\w+\.)*\w+)/; + + var _ref4 = matchErrModuleName.exec(error) || [], + _ref5 = _slicedToArray(_ref4, 2), + baseModuleName = _ref5[1]; + + if (!baseModuleName) continue; + + var matchMissingModuleName = /Module ((?:\w+\.)*\w+) was not found/; + var matchMissingImportFromModuleName = /Cannot import value \w+ from module ((?:\w+\.)*\w+)/; + var _arr = [matchMissingModuleName, matchMissingImportFromModuleName]; + for (var _i = 0; _i < _arr.length; _i++) { + var re = _arr[_i]; + var _ref6 = re.exec(error) || [], + _ref7 = _slicedToArray(_ref6, 2), + targetModuleName = _ref7[1]; + + if (targetModuleName) { + var resolved = utils.resolvePursModule({ + baseModulePath: baseModulePath, + baseModuleName: baseModuleName, + targetModuleName: targetModuleName + }); + _this.addDependency(resolved); + } + } + + var desc = { + name: baseModuleName, + filename: baseModulePath + }; + + if (typeof _this.describePscError === 'function') { + var _describePscError = _this.describePscError(error, desc), + _describePscError$dep = _describePscError.dependencies, + dependencies = _describePscError$dep === undefined ? [] : _describePscError$dep, + details = _describePscError.details; + + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + + for (var _iterator2 = dependencies[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var dep = _step2.value; + + _this.addDependency(dep); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + Object.assign(desc, details); + } + + modules.push(desc); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + CACHE_VAR.errors.push(new utils.PscError(pscMessage, modules)); + } + } + }; + + debug('loading %s', psModule.name); + + if (options.bundle) { + CACHE_VAR.bundleModules.push(psModule.name); + } + + if (CACHE_VAR.rebuild) { + var connect = function connect() { + if (!CACHE_VAR.ideServer) { + CACHE_VAR.ideServer = true; + + return ide.connect(psModule).then(function (ideServer) { + CACHE_VAR.ideServer = ideServer; + return psModule; + }).then(ide.loadWithRetry).catch(function (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); + } + }; + + var rebuild = function rebuild() { + return ide.rebuild(psModule).then(function () { + return toJavaScript(psModule).then(function (js) { + return sourceMaps(psModule, js); + }).then(psModule.load).catch(psModule.reject); + }).catch(function (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. + + CACHE_VAR.deferred.push(psModule); + + if (!CACHE_VAR.compilationStarted) { + CACHE_VAR.compilationStarted = true; + + return compile(psModule).then(function () { + CACHE_VAR.compilationFinished = true; + }).then(function () { + return Promise.map(CACHE_VAR.deferred, function (psModule) { + return ide.load(psModule).then(function () { + return toJavaScript(psModule); + }).then(function (js) { + return sourceMaps(psModule, js); + }).then(psModule.load); + }); + }).catch(function (error) { + CACHE_VAR.compilationFailed = true; + + CACHE_VAR.deferred[0].reject(error); + + CACHE_VAR.deferred.slice(1).forEach(function (psModule) { + psModule.reject(new Error('purs-loader failed')); + }); + }); + } else if (CACHE_VAR.compilationFailed) { + CACHE_VAR.deferred.pop().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); + + psModule.reject(error); + } + }); + }; + + connect().then(rebuild); + } else if (CACHE_VAR.compilationFinished) { + debugVerbose('compilation is already finished, loading module %s', psModule.name); + + toJavaScript(psModule).then(function (js) { + return 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. + + CACHE_VAR.deferred.push(psModule); + + if (!CACHE_VAR.compilationStarted) { + CACHE_VAR.compilationStarted = true; + + compile(psModule).then(function () { + CACHE_VAR.compilationFinished = true; + }).then(function () { + if (options.bundle) { + return bundle(options, CACHE_VAR.bundleModules); + } + }).then(function () { + return Promise.map(CACHE_VAR.deferred, function (psModule) { + return toJavaScript(psModule).then(function (js) { + return sourceMaps(psModule, js); + }).then(psModule.load); + }); + }).catch(function (error) { + CACHE_VAR.compilationFailed = true; + + CACHE_VAR.deferred[0].reject(error); + + CACHE_VAR.deferred.slice(1).forEach(function (psModule) { + psModule.reject(new Error('purs-loader failed')); + }); + }); + } else if (CACHE_VAR.compilationFailed) { + CACHE_VAR.deferred.pop().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. + } + } +}; \ No newline at end of file diff --git a/lib/purs-module-map.js b/lib/purs-module-map.js new file mode 100644 index 0000000..cb06322 --- /dev/null +++ b/lib/purs-module-map.js @@ -0,0 +1,78 @@ +'use strict'; + +var path = require('path'); + +var Promise = require('bluebird'); + +var fs = Promise.promisifyAll(require('fs')); + +var globby = require('globby'); + +var debug = require('debug')('purs-loader'); + +var srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i; + +var importModuleRegex = /(?:^|\n)\s*import\s+([\w\.]+)/ig; + +module.exports.matchModule = function matchModule(str) { + var matches = str.match(srcModuleRegex); + return matches && matches[1]; +}; + +module.exports.matchImports = function matchImports(str) { + var matches = str.match(importModuleRegex); + return (matches || []).map(function (a) { + return a.replace(/\n?\s*import\s+/i, ''); + }); +}; + +module.exports.makeMapEntry = function makeMapEntry(filePurs) { + var dirname = path.dirname(filePurs); + + var basename = path.basename(filePurs, '.purs'); + + var fileJs = path.join(dirname, basename + '.js'); + + var result = Promise.props({ + filePurs: fs.readFileAsync(filePurs, 'utf8'), + fileJs: fs.readFileAsync(fileJs, 'utf8').catch(function () { + return undefined; + }) + }).then(function (fileMap) { + var sourcePurs = fileMap.filePurs; + + var sourceJs = fileMap.fileJs; + + var moduleName = module.exports.matchModule(sourcePurs); + + var imports = module.exports.matchImports(sourcePurs); + + var map = {}; + + map[moduleName] = map[moduleName] || {}; + + map[moduleName].src = path.resolve(filePurs); + + map[moduleName].imports = imports; + + if (sourceJs) { + map[moduleName].ffi = path.resolve(fileJs); + } + + return map; + }); + + return result; +}; + +module.exports.makeMap = function makeMap(src) { + debug('loading PureScript source and FFI files from %o', src); + + var globs = [].concat(src); + + return globby(globs).then(function (paths) { + return Promise.all(paths.map(module.exports.makeMapEntry)).then(function (result) { + return result.reduce(Object.assign, {}); + }); + }); +}; \ No newline at end of file diff --git a/lib/source-maps.js b/lib/source-maps.js new file mode 100644 index 0000000..3ad70dd --- /dev/null +++ b/lib/source-maps.js @@ -0,0 +1,61 @@ +'use strict'; + +var Promise = require('bluebird'); + +var fs = require('fs'); + +var path = require('path'); + +var debug_ = require('debug'); + +var debugVerbose = debug_('purs-loader:verbose'); + +module.exports = function sourceMap(psModule, js) { + var options = psModule.options; + + var jsPath = psModule.jsPath; + + var srcPath = psModule.srcPath; + + var source = psModule.source; + + var remainingRequest = psModule.remainingRequest; + + var sourceMapPath = path.join(path.dirname(jsPath), 'index.js.map'); + + var isSourceMapsEnabled = options.pscArgs && options.pscArgs.sourceMaps; + + return new Promise(function (resolve, reject) { + if (!isSourceMapsEnabled) { + resolve({ + js: js, + map: undefined + }); + } else { + debugVerbose('loading source map %s', sourceMapPath); + + fs.readFile(sourceMapPath, 'utf-8', function (error, result) { + if (error) { + reject(error); + } else { + try { + var map = Object.assign(JSON.parse(result), { + sources: [remainingRequest], + file: path.normalize(srcPath), + sourcesContent: [source] + }); + + var jsRemovedMapUrl = js.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); + + resolve({ + js: jsRemovedMapUrl, + map: map + }); + } catch (error) { + reject(error); + } + } + }); + } + }); +}; \ No newline at end of file diff --git a/lib/to-javascript.js b/lib/to-javascript.js new file mode 100644 index 0000000..ce49704 --- /dev/null +++ b/lib/to-javascript.js @@ -0,0 +1,152 @@ +'use strict'; + +var Promise = require('bluebird'); + +var fs = Promise.promisifyAll(require('fs')); + +var path = require('path'); + +var jsStringEscape = require('js-string-escape'); + +var difference = require('lodash.difference'); + +var debug_ = require('debug'); + +var debug = debug_('purs-loader'); + +var debugVerbose = debug_('purs-loader:verbose'); + +var PsModuleMap = require('./purs-module-map'); + +function updatePsModuleMap(psModule) { + var options = psModule.options; + + var cache = psModule.cache; + + var filePurs = psModule.srcPath; + + if (!cache.psModuleMap) { + debugVerbose('module mapping does not exist - making a new module map'); + + cache.psModuleMap = PsModuleMap.makeMap(options.src); + + return cache.psModuleMap; + } else { + debugVerbose('module mapping exists - updating module map for %s', filePurs); + + cache.psModuleMap = cache.psModuleMap.then(function (psModuleMap) { + return PsModuleMap.makeMapEntry(filePurs).then(function (result) { + var map = Object.assign(psModuleMap, result); + + return map; + }); + }); + + return cache.psModuleMap; + } +} + +// Reference the bundle. +function makeBundleJS(psModule) { + var bundleOutput = psModule.options.bundleOutput; + + var name = psModule.name; + + var srcDir = psModule.srcDir; + + var escaped = jsStringEscape(path.relative(srcDir, bundleOutput)); + + var result = 'module.exports = require("' + escaped + '")["' + name + '"]'; + + return Promise.resolve(result); +} + +// Replace require paths to output files generated by psc with paths +// to purescript sources, which are then also run through this loader. +// Additionally, the imports replaced are tracked so that in the event +// the compiler fails to compile the PureScript source, we can tack on +// any new imports in order to allow webpack to watch the new files +// before they have been successfully compiled. +function makeJS(psModule, psModuleMap, js) { + var requireRE = /require\(['"]\.\.\/([\w\.]+)(?:\/index\.js)?['"]\)/g; + + var foreignRE = /require\(['"]\.\/foreign(?:\.js)?['"]\)/g; + + var name = psModule.name; + + var imports = psModuleMap[name].imports; + + var replacedImports = []; + + var result = js.replace(requireRE, function (m, p1) { + var moduleValue = psModuleMap[p1]; + + if (!moduleValue) { + debug('module %s was not found in the map, replacing require with null', p1); + + return 'null'; + } else { + var escapedPath = jsStringEscape(moduleValue.src); + + replacedImports.push(p1); + + return 'require("' + escapedPath + '")'; + } + }).replace(foreignRE, function () { + var escapedPath = jsStringEscape(psModuleMap[name].ffi); + + return 'require("' + escapedPath + '")'; + }); + + var additionalImports = difference(imports, replacedImports); + + if (!additionalImports.length) { + return Promise.resolve(result); + } else { + debug('rebuilding module map due to additional imports for %s: %o', name, additionalImports); + + psModule.cache.psModuleMap = null; + + return updatePsModuleMap(psModule).then(function (updatedPsModuleMap) { + var additionalImportsResult = additionalImports.map(function (import_) { + var moduleValue = updatedPsModuleMap[import_]; + + if (!moduleValue) { + debug('module %s was not found in the map, skipping require', import_); + + return null; + } else { + var escapedPath = jsStringEscape(moduleValue.src); + + return 'var ' + import_.replace(/\./g, '_') + ' = require("' + escapedPath + '")'; + } + }).filter(function (a) { + return a !== null; + }).join('\n'); + + return result + '\n' + additionalImportsResult; + }); + } +} + +module.exports = function toJavaScript(psModule) { + var options = psModule.options; + + var cache = psModule.cache; + + var bundlePath = path.resolve(options.bundleOutput); + + var jsPath = options.bundle ? bundlePath : psModule.jsPath; + + var js = fs.readFileAsync(jsPath, 'utf8').catch(function () { + return ''; + }); + + var psModuleMap = updatePsModuleMap(psModule); + + debugVerbose('loading JavaScript for %s', psModule.name); + + return Promise.props({ js: js, psModuleMap: psModuleMap }).then(function (result) { + return options.bundle ? makeBundleJS(psModule) : makeJS(psModule, result.psModuleMap, result.js); + }); +}; \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..ebee1d1 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,63 @@ +'use strict'; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var path = require('path'); + +exports.PscError = function (_Error) { + _inherits(PscError, _Error); + + function PscError(message, modules) { + _classCallCheck(this, PscError); + + var _this = _possibleConstructorReturn(this, (PscError.__proto__ || Object.getPrototypeOf(PscError)).call(this, message)); + + _this.modules = modules; + return _this; + } + + _createClass(PscError, null, [{ + key: 'name', + get: function get() { + return 'PscError'; + } + }]); + + return PscError; +}(Error); + +var repeat = function repeat(value, times) { + return times <= 0 ? [] : [value].concat(_toConsumableArray(repeat(value, times - 1))); +}; +var diffPursModuleNames = function diffPursModuleNames(from, target, parts) { + if (!from.length) return parts.concat(target); + if (!target.length) return parts.concat(repeat('..', from.length)); + + var _from = _toArray(from), + head_from = _from[0], + tail_from = _from.slice(1); + + var _target = _toArray(target), + head_target = _target[0], + tail_target = _target.slice(1); + + return head_from === head_target ? diffPursModuleNames(tail_from, tail_target, parts) : parts.concat(repeat('..', from.length), target); +}; +exports.resolvePursModule = function (_ref) { + var baseModulePath = _ref.baseModulePath, + baseModuleName = _ref.baseModuleName, + targetModuleName = _ref.targetModuleName; + + var parts = diffPursModuleNames(baseModuleName.split('.'), targetModuleName.split('.'), []); + return parts.length ? path.resolve(baseModulePath, path.join.apply(path, _toConsumableArray(parts)) + '.purs') : baseModulePath; +}; \ No newline at end of file -- 2.41.0