aboutsummaryrefslogblamecommitdiffhomepage
path: root/src/index.js
blob: 0b4ecf8263c99d0ed1eb43912c65bbe1d8cc6f17 (plain) (tree)
1
2
3
4
5
6
7
8
9

            




                                                   
 
                                           
 
                                   
 
                            








                                                 
                                                
 

                                            
                                 
 

                                 
                                         
 
                             
 









                             
                           



                   
                                                         

                                     
                                                                                    


                                
                                                           
 
                                    



                                                                                           

                                         

                          
                                              
 

                                         



                                              

                                                                    

                                                           



                                   








                                                                                               
                                     


                      

          


                                           
        
 
                                   

                    
     
                               
 
                                 
                            
              
                
                    
                      



                         
                  
                                              
                      



                                     
                 
                     



                     
 
                             
                                                                     
 
                               
 
                                                                                   
                                            
                                                    
 
                   
                                
                     
                          

                                           
                     
                   

                                   
                                 
                                       
                     
        



                                                                       
                                             


                                           
                                         




                                       

   

                                                       

                       

                                                 

                                     
                                                            


                                                                              
                     

                                               
                                         

       

                              

                           


























                                                                                                         
 




                                    






                                                                                    

                             

         
                                                                                 

       

   
                                     
 
                       
                                                

   
                          
                           

                                   


                                    
                                            



                                  
                                           

                                                                                             
                                         

             
                                       










                                         


                              
                                               



                                 
                                                      



                                                                       
                                            
 

                                                


                                    
                                                     

                         
                                                           

                                                       
                                                         



                                        

                                                   
                                                    
 
                                                                 


                                                                   
             


                                                                             


                                                                     




                                                                           
                                 



         
                            
   
                                           
                                                                                      
 
                          
                                           

                              
   


                                                                      
                                                                           

                                                 
                                      
 

                                          


                       
                                               


                               
                                                            


                   
                                                     
                                  
                                                   
                                  


                         

                                             
                                              
 
                                                           



                                                             


                                                                       


                                                                       

   
'use strict'

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 path = require('path')

const PsModuleMap = require('./purs-module-map');

const compile = require('./compile');

const bundle = require('./bundle');

const ide = require('./ide');

const toJavaScript = require('./to-javascript');

const sourceMaps = require('./source-maps');

const dargs = require('./dargs');

const utils = require('./utils');

const spawn = require('cross-spawn').sync

const 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) {
  this.cacheable && this.cacheable();

  const webpackContext = (this.options && this.options.context) || this.rootContext;

  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_VAR.srcOption.length > 0) {
      return CACHE_VAR.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.error) {
        throw new Error(cmd.error);
      }
      else 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_VAR.srcOption = result;

        return result;
      }
    }
    else {
      const result = loaderOptions.src || [
        bowerPath,
        srcPath
      ];

      CACHE_VAR.srcOption = result;

      return result;
    }
  })(loaderOptions.pscPackage);

  const 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', () => {
      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', (compilation, callback) => {
      CACHE_VAR.warnings.forEach(warning => {
        compilation.warnings.push(warning);
      });

      CACHE_VAR.errors.forEach(error => {
        compilation.errors.push(error);
      });

      callback()
    });
  }

  const psModuleName = PsModuleMap.matchModule(source);

  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);
      }
    },
    emitError: pscMessage => {
      if (pscMessage.length) {
        const modules = [];

        const matchErrorsSeparator = /\n(?=Error)/;
        const errors = pscMessage.split(matchErrorsSeparator);
        for (const error of errors) {
          const matchErrLocation = /at (.+\.purs) line (\d+), column (\d+) - line (\d+), column (\d+)/;
          const [, filename] = matchErrLocation.exec(error) || [];
          if (!filename) continue;

          const baseModulePath = path.join(this.rootContext, filename);
          this.addDependency(baseModulePath);

          const matchErrModuleName = /in module ((?:\w+\.)*\w+)/;
          const [, baseModuleName] = matchErrModuleName.exec(error) || [];
          if (!baseModuleName) continue;

          const matchMissingModuleName = /Module ((?:\w+\.)*\w+) was not found/;
          const matchMissingImportFromModuleName = /Cannot import value \w+ from module ((?:\w+\.)*\w+)/;
          for (const re of [matchMissingModuleName, matchMissingImportFromModuleName]) {
            const [, targetModuleName] = re.exec(error) || [];
            if (targetModuleName) {
              const resolved = utils.resolvePursModule({
                baseModulePath,
                baseModuleName,
                targetModuleName
              });
              this.addDependency(resolved);
            }
          }

          const desc = {
            name: baseModuleName,
            filename: baseModulePath
          };

          if (typeof this.extractPursDependenciesFromError === 'function') {
            const dependencies = this.extractPursDependenciesFromError(error) || [];

            for (const dep of dependencies) {
              this.addDependency(dep);
            }
          }

          modules.push(desc);
        }

        CACHE_VAR.errors.push(Object.assign(new Error(pscMessage), { modules }));
      }
    }
  }

  debug('loading %s', psModule.name);

  if (options.bundle) {
    CACHE_VAR.bundleModules.push(psModule.name);
  }

  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.

          CACHE_VAR.deferred.push(psModule);

          if (!CACHE_VAR.compilationStarted) {
            CACHE_VAR.compilationStarted = true;

            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.compilationFailed = true;

                CACHE_VAR.deferred[0].reject(error);

                CACHE_VAR.deferred.slice(1).forEach(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(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.

    CACHE_VAR.deferred.push(psModule);

    if (!CACHE_VAR.compilationStarted) {
      CACHE_VAR.compilationStarted = true;

      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.compilationFailed = true;

          CACHE_VAR.deferred[0].reject(error);

          CACHE_VAR.deferred.slice(1).forEach(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.
    }
  }
}