X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=src%2Findex.js;h=19a549e3aa06b8bbf15b1f87991c6260ebabdfdc;hb=e17196589a380eed3ffc47705f8c6c87f99b58c2;hp=f3b2fdec2205ed0ac88679bd8ee94e8e918d1fa9;hpb=4b99e432d63e7a0ce8ad8d2184232417853bfd2b;p=github%2Ffretlink%2Fpurs-loader.git diff --git a/src/index.js b/src/index.js index f3b2fde..19a549e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,85 +1,165 @@ 'use strict' -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 Promise = require('bluebird') -const fs = Promise.promisifyAll(require('fs')) + const path = require('path') -const jsStringEscape = require('js-string-escape') -const PsModuleMap = require('./PsModuleMap'); -const Psc = require('./Psc'); -const PscIde = require('./PscIde'); + +const PsModuleMap = require('./purs-module-map'); + +const compile = require('./compile'); + +const bundle = require('./bundle'); + +const ide = require('./ide'); + +const toJavaScript = require('./to-javascript'); + const dargs = require('./dargs'); -const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g +const spawn = require('cross-spawn').sync + +const eol = require('os').EOL 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 || {} + this.cacheable && this.cacheable(); + + const webpackConfig = this.options; + + var cache = webpackConfig.purescriptLoaderCache = webpackConfig.purescriptLoaderCache || { + rebuild: false, + deferred: [], + bundleModules: [], + ideServer: null, + psModuleMap: null, + warnings: [], + errors: [], + compilationStarted: false, + compilationFinished: false, + installed: false, + srcOption: [] + }; + + const callback = this.async(); + + const loaderOptions = loaderUtils.getOptions(this) || {}; + + const srcOption = (pscPackage => { + const srcPath = path.join('src', '**', '*.purs'); + + const bowerPath = path.join('bower_components', 'purescript-*', 'src', '**', '*.purs'); + + if (cache.srcOption.length > 0) { + return cache.srcOption; + } + else if (pscPackage) { + const pscPackageCommand = 'psc-package'; + + const pscPackageArgs = ['sources']; + + const loaderSrc = loaderOptions.src || [ + srcPath + ]; + + debug('psc-package %s %o', pscPackageCommand, pscPackageArgs); + + const cmd = spawn(pscPackageCommand, pscPackageArgs); + + if (cmd.status !== 0) { + const error = cmd.stdout.toString(); + + throw new Error(error); + } + else { + const result = cmd.stdout.toString().split(eol).filter(v => v != '').concat(loaderSrc); + + debug('psc-package result: %o', result); + + cache.srcOption = result; + + return result; + } + } + else { + const result = loaderOptions.src || [ + bowerPath, + srcPath + ]; + + cache.srcOption = result; + + return result; + } + })(loaderOptions.pscPackage); const options = Object.assign({ - context: config.context, - psc: 'psc', + context: webpackConfig.context, + psc: null, pscArgs: {}, - pscBundle: 'psc-bundle', + pscBundle: null, pscBundleArgs: {}, pscIde: false, - pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa', + pscIdeColors: loaderOptions.psc === 'psa', pscIdeArgs: {}, + pscPackage: false, bundleOutput: 'output/bundle.js', bundleNamespace: 'PS', bundle: false, warnings: true, + watch: false, output: 'output', - src: [ - path.join('src', '**', '*.purs'), - path.join('bower_components', 'purescript-*', 'src', '**', '*.purs') - ] - }, webpackOptions, query) + src: [] + }, loaderOptions, { + src: srcOption + }); - this.cacheable && this.cacheable() + if (!cache.installed) { + debugVerbose('installing purs-loader with options: %O', options); - let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { - rebuild: false, - deferred: [], - bundleModules: [] - } - - if (!config.purescriptLoaderInstalled) { - config.purescriptLoaderInstalled = true + cache.installed = true; // invalidate loader cache when bundle is marked as invalid (in watch mode) this._compiler.plugin('invalid', () => { - debug('invalidating loader cache'); + debugVerbose('invalidating loader cache'); - cache = config.purescriptLoaderCache = { + cache = webpackConfig.purescriptLoaderCache = { rebuild: options.pscIde, deferred: [], bundleModules: [], ideServer: cache.ideServer, - psModuleMap: cache.psModuleMap - } - }) + psModuleMap: cache.psModuleMap, + warnings: [], + errors: [], + compilationStarted: cache.compilationStarted, + compilationFinished: cache.compilationFinished, + installed: cache.installed, + srcOption: cache.srcOption + }; + }); // 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}`) - cache.warnings = null; - } + cache.warnings.forEach(warning => { + compilation.warnings.push(warning); + }); - if (cache.errors) { - compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`) - cache.errors = null; - } + cache.errors.forEach(error => { + compilation.errors.push(error); + }); callback() - }) + }); } - const psModuleName = PsModuleMap.match(source) + const psModuleName = PsModuleMap.matchModule(source); + const psModule = { name: psModuleName, load: js => callback(null, js), @@ -89,99 +169,144 @@ module.exports = function purescriptLoader(source, map) { jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')), options: options, cache: cache, + emitWarning: warning => { + if (options.warnings && warning.length) { + cache.warnings.push(warning); + } + }, + emitError: error => { + if (error.length) { + cache.errors.push(error); + } + } } - debug('loader called', psModule.name) + debug('loading %s', psModule.name); if (options.bundle) { - cache.bundleModules.push(psModule.name) + cache.bundleModules.push(psModule.name); } if (cache.rebuild) { - return PscIde.connect(psModule) - .then(PscIde.rebuild) + const connect = () => { + if (!cache.ideServer) { + cache.ideServer = true; + + return ide.connect(psModule) + .then(ideServer => { + cache.ideServer = ideServer; + return psModule; + }) + .then(ide.loadWithRetry) + .catch(error => { + if (cache.ideServer.kill) { + debug('ide failed to initially load modules, stopping the ide server process'); + + cache.ideServer.kill(); + } + + cache.ideServer = null; + + return Promise.reject(error); + }) + ; + } + else { + return Promise.resolve(psModule); + } + }; + + const rebuild = () => + ide.rebuild(psModule).catch(error => { + if (error instanceof ide.UnknownModuleError) { + if (!cache.compilationStarted) { + cache.compilationStarted = true; + + return compile(psModule) + .then(() => { + cache.compilationFinished = true; + }) + .then(() => + PsModuleMap.makeMap(options.src).then(map => { + debug('rebuilt module map after unknown module forced a recompilation'); + + cache.psModuleMap = map; + }) + ) + .then(() => ide.load(psModule)) + .then(() => psModule) + ; + } + else { + return Promise.resolve(psModule); + } + } + else { + debug('ide rebuild failed due to an unhandled error: %o', error); + + return Promise.reject(error); + } + }) + ; + + connect() + .then(rebuild) .then(toJavaScript) .then(psModule.load) .catch(psModule.reject) + ; } + else if (cache.compilationFinished) { + debugVerbose('compilation is already finished, loading module %s', psModule.name); - if (cache.compilationFinished) { - return toJavaScript(psModule).then(psModule.load).catch(psModule.reject) + toJavaScript(psModule) + .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 to + // be loaded once the complation is complete. - // We need to wait for compilation to finish before the loaders run so that - // references to compiled output are valid. - cache.deferred.push(psModule) - - if (!cache.compilationStarted) { - return Psc.compile(psModule) - .then(() => PsModuleMap.makeMap(options.src).then(map => { - debug('rebuilt module map'); - cache.psModuleMap = map; - })) - .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(new Error('purs-loader failed'))) - }) - } -} + cache.deferred.push(psModule); -function updatePsModuleMap(psModule) { - const options = psModule.options - const cache = psModule.cache - const filePurs = psModule.srcPath - if (!cache.psModuleMap) { - debug('module mapping does not exist'); - return PsModuleMap.makeMap(options.src).then(map => { - cache.psModuleMap = map; - return cache.psModuleMap; - }); - } - else { - return PsModuleMap.makeMapEntry(filePurs).then(result => { - const map = Object.assign(cache.psModuleMap, result) - cache.psModuleMap = map; - return cache.psModuleMap; - }); - } -} + if (!cache.compilationStarted) { + cache.compilationStarted = true; -// 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: updatePsModuleMap(psModule) - }).then(result => { - let js = '' - - if (options.bundle) { - // if bundling, return a reference to the bundle - js = 'module.exports = require("' - + jsStringEscape(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. - js = result.js - .replace(requireRegex, (m, p1) => { - return 'require("' + jsStringEscape(result.psModuleMap[p1].src) + '")' + compile(psModule) + .then(() => { + cache.compilationFinished = true; }) - .replace(/require\(['"]\.\/foreign['"]\)/g, (m, p1) => { - return 'require("' + jsStringEscape(result.psModuleMap[psModule.name].ffi) + '")' + .then(() => { + if (options.bundle) { + return bundle(options, cache.bundleModules); + } }) - } + .then(() => + PsModuleMap.makeMap(options.src).then(map => { + debug('rebuilt module map after compilation'); + + cache.psModuleMap = map; + }) + ) + .then(() => + Promise.map(cache.deferred, psModule => + toJavaScript(psModule).then(psModule.load) + ) + ) + .catch(error => { + cache.deferred[0].reject(error); - return js - }) + cache.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. + } + } }