]> git.immae.eu Git - github/fretlink/purs-loader.git/commitdiff
Refactor to compile independently of purescript-webpack-plugin.
authorAlex Mingoia <talk@alexmingoia.com>
Tue, 10 May 2016 07:09:28 +0000 (00:09 -0700)
committerAlex Mingoia <talk@alexmingoia.com>
Tue, 10 May 2016 07:09:28 +0000 (00:09 -0700)
- Remove dependence on purescript-webpack-plugin
- Fixes double-compilation issue by loading compiled JS instead of adding
  dependency.
- Uses `psc-ide-server` for fast rebuilds.

28 files changed:
.babelrc [new file with mode: 0644]
.gitignore
LICENSE
README.md
bower.json [deleted file]
docs/PursLoader/Debug.md [deleted file]
docs/PursLoader/JsStringEscape.md [deleted file]
docs/PursLoader/Loader.md [deleted file]
docs/PursLoader/LoaderRef.md [deleted file]
docs/PursLoader/LoaderUtil.md [deleted file]
docs/PursLoader/Options.md [deleted file]
docs/PursLoader/Path.md [deleted file]
docs/PursLoader/Plugin.md [deleted file]
entry.js [deleted file]
package.json
src/PursLoader/Debug.js [deleted file]
src/PursLoader/Debug.purs [deleted file]
src/PursLoader/JsStringEscape.js [deleted file]
src/PursLoader/JsStringEscape.purs [deleted file]
src/PursLoader/Loader.purs [deleted file]
src/PursLoader/LoaderRef.js [deleted file]
src/PursLoader/LoaderRef.purs [deleted file]
src/PursLoader/Path.js [deleted file]
src/PursLoader/Path.purs [deleted file]
src/PursLoader/Plugin.js [deleted file]
src/PursLoader/Plugin.purs [deleted file]
src/index.js [new file with mode: 0644]
webpack.config.js [deleted file]

diff --git a/.babelrc b/.babelrc
new file mode 100644 (file)
index 0000000..9d8d516
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1 @@
+{ "presets": ["es2015"] }
index 0a05586bece0b3937afc69258ecbc54b61405613..548b3c7fa9e5abe68b6c1713212988b69c889e9c 100644 (file)
@@ -1,9 +1,5 @@
-.psci
-.pulp-cache
-npm-debug.log
-index.json
-index.js
+**DS_Store*
+build/
 node_modules/
 bower_components/
-build/
-tmp/
+/index.js
diff --git a/LICENSE b/LICENSE
index 05b0016faf19cfa0ecacc9a67747d4ff2497842a..6fe3810c61da61baa3bffa73582be505870cb0d5 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015 Eric Thul
+Copyright (c) 2016 Alexander Mingoia and Eric Thul
 
 Permission is hereby granted, free of charge, to any person obtaining a
 copy of this software and associated documentation files (the
index df92e6940ab4971af798cbde078b84656c3fa113..ed25296bea7ea348f8ad83b92d6a053fa6b2ba24 100644 (file)
--- a/README.md
+++ b/README.md
@@ -2,16 +2,62 @@
 
 > [PureScript](http://www.purescript.org) loader for [webpack](http://webpack.github.io)
 
+- Supports hot-reloading and rebuilding of single source files
+- Dead code elimination using the `bundle` option
+- Colorized build output using `purescript-psa` and the `psc: "psa"` option
+
 ## Install
 
 Install with [npm](https://npmjs.org/package/purs-loader).
 
-This loader works in conjunction with the [PureScript webpack plugin](https://npmjs.org/package/purescript-webpack-plugin). Ensure the plugin is installed and configured accordingly.
-
 ```
 npm install purs-loader --save-dev
 ```
 
 ## Example
 
-Refer to the [purescript-webpack-example](https://github.com/ethul/purescript-webpack-example) for an example.
+```javascript
+const webpackConfig = {
+  // ...
+  loaders: [
+    // ...
+    {
+      test: /\.purs$/,
+      loader: 'purs-loader',
+      exclude: /node_modules/,
+      query: {
+        psc: 'psa',
+        src: ['bower_components/purescript-*/src/**/*.purs', 'src/**/*.purs'],
+        ffi: ['bower_components/purescript-*/src/**/*.js', 'src/**/*.js'],
+      }
+    }
+    // ...
+  ]
+  // ...
+}
+```
+
+Default options:
+
+```javascript
+{
+  psc: 'psc',
+  pscArgs: {},
+  pscBundle: 'psc-bundle',
+  pscBundleArgs: {},
+  pscIdeColors: false, // defaults to true if psc === 'psa'
+  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')
+  ],
+}
+```
diff --git a/bower.json b/bower.json
deleted file mode 100644 (file)
index 761c24c..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "name": "purs-loader",
-  "private": true,
-  "dependencies": {
-    "purescript-aff": "^0.13.0",
-    "purescript-foreign": "^0.7.0",
-    "purescript-unsafe-coerce": "~0.1.0",
-    "purescript-nullable": "~0.2.1"
-  }
-}
diff --git a/docs/PursLoader/Debug.md b/docs/PursLoader/Debug.md
deleted file mode 100644 (file)
index 824a9f8..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-## Module PursLoader.Debug
-
-#### `debug`
-
-``` purescript
-debug :: forall eff. String -> Eff (loader :: Loader | eff) Unit
-```
-
-
diff --git a/docs/PursLoader/JsStringEscape.md b/docs/PursLoader/JsStringEscape.md
deleted file mode 100644 (file)
index 09f52aa..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-## Module PursLoader.JsStringEscape
-
-#### `jsStringEscape`
-
-``` purescript
-jsStringEscape :: String -> String
-```
-
-
diff --git a/docs/PursLoader/Loader.md b/docs/PursLoader/Loader.md
deleted file mode 100644 (file)
index d05e3b7..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-## Module PursLoader.Loader
-
-#### `Effects`
-
-``` purescript
-type Effects eff = (console :: CONSOLE, err :: EXCEPTION | eff)
-```
-
-#### `Effects_`
-
-``` purescript
-type Effects_ eff = Effects (loader :: Loader | eff)
-```
-
-#### `loader`
-
-``` purescript
-loader :: forall eff. LoaderRef -> String -> Eff (Effects_ eff) Unit
-```
-
-#### `loaderFn`
-
-``` purescript
-loaderFn :: forall eff. Fn2 LoaderRef String (Eff (Effects_ eff) Unit)
-```
-
-
diff --git a/docs/PursLoader/LoaderRef.md b/docs/PursLoader/LoaderRef.md
deleted file mode 100644 (file)
index 917db3a..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-## Module PursLoader.LoaderRef
-
-#### `AsyncCallback`
-
-``` purescript
-type AsyncCallback eff = Maybe Error -> String -> Eff (loader :: Loader | eff) Unit
-```
-
-#### `LoaderRef`
-
-``` purescript
-data LoaderRef
-```
-
-#### `Loader`
-
-``` purescript
-data Loader :: !
-```
-
-#### `async`
-
-``` purescript
-async :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) (Maybe Error -> String -> Eff (loader :: Loader | eff) Unit)
-```
-
-#### `cacheable`
-
-``` purescript
-cacheable :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
-```
-
-#### `clearDependencies`
-
-``` purescript
-clearDependencies :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
-```
-
-#### `resourcePath`
-
-``` purescript
-resourcePath :: LoaderRef -> String
-```
-
-#### `addDependency`
-
-``` purescript
-addDependency :: forall eff. LoaderRef -> String -> Eff (loader :: Loader | eff) Unit
-```
-
-
diff --git a/docs/PursLoader/LoaderUtil.md b/docs/PursLoader/LoaderUtil.md
deleted file mode 100644 (file)
index 36d6879..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-## Module PursLoader.LoaderUtil
-
-#### `parseQuery`
-
-``` purescript
-parseQuery :: String -> Foreign
-```
-
-
diff --git a/docs/PursLoader/Options.md b/docs/PursLoader/Options.md
deleted file mode 100644 (file)
index b3352fc..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-## Module PursLoader.Options
-
-#### `Options`
-
-``` purescript
-newtype Options
-  = Options { bundleOutput :: String }
-```
-
-##### Instances
-``` purescript
-IsForeign Options
-```
-
-#### `runOptions`
-
-``` purescript
-runOptions :: Options -> Options_
-```
-
-
diff --git a/docs/PursLoader/Path.md b/docs/PursLoader/Path.md
deleted file mode 100644 (file)
index cc00436..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-## Module PursLoader.Path
-
-#### `relative`
-
-``` purescript
-relative :: String -> String -> String
-```
-
-#### `resolve`
-
-``` purescript
-resolve :: String -> String
-```
-
-#### `dirname`
-
-``` purescript
-dirname :: String -> String
-```
-
-#### `joinPath`
-
-``` purescript
-joinPath :: String -> String -> String
-```
-
-
diff --git a/docs/PursLoader/Plugin.md b/docs/PursLoader/Plugin.md
deleted file mode 100644 (file)
index 7a524da..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-## Module PursLoader.Plugin
-
-#### `Compile`
-
-``` purescript
-type Compile eff = Nullable Error -> DependencyGraph -> Eff eff Unit
-```
-
-#### `Context`
-
-``` purescript
-type Context eff = { compile :: Compile eff -> Eff eff Unit, options :: Options }
-```
-
-#### `Options`
-
-``` purescript
-type Options = { bundle :: Boolean, output :: String, bundleOutput :: String }
-```
-
-#### `dependenciesOf`
-
-``` purescript
-dependenciesOf :: DependencyGraph -> String -> Either Error (Array String)
-```
-
-#### `DependencyGraph`
-
-``` purescript
-data DependencyGraph :: *
-```
-
-
diff --git a/entry.js b/entry.js
deleted file mode 100644 (file)
index 87f52d3..0000000
--- a/entry.js
+++ /dev/null
@@ -1,11 +0,0 @@
-'use strict';
-
-var pursLoader = require('PursLoader.Loader');
-
-function loader(source) {
-  var ref = this;
-  var result = pursLoader.loaderFn(ref, source);
-  return result();
-}
-
-module.exports = loader;
index 6ab55ec40602cf89bc51227eb2e6cb8e25ce3cc2..4cbdd72d6fa105bffb1f099281ce8baabf90db7b 100644 (file)
@@ -1,30 +1,49 @@
 {
   "name": "purs-loader",
-  "version": "0.6.0",
-  "description": "PureScript loader for webpack",
-  "license": "MIT",
-  "repository": "ethul/purs-loader",
-  "author": {
-    "name": "Eric Thul",
-    "email": "thul.eric@gmail.com"
-  },
-  "scripts": {
-    "build": "npm run-script build:compile && npm run-script build:docs && npm run-script build:package",
-    "build:compile": "pulp build -o build --force",
-    "build:docs": "pulp docs",
-    "build:package": "webpack --progress --colors --profile --bail",
-    "build:watch": "pulp -w build -o build --force",
-    "build:json": "webpack --progress --colors --profile --bail --json > index.json",
-    "prepublish": "npm run-script build"
-  },
+  "version": "1.0.0",
+  "description": "A webpack loader for PureScript.",
+  "main": "index.js",
   "files": [
     "index.js"
   ],
-  "devDependencies": {
-    "webpack": "^1.8.4"
+  "scripts": {
+    "build": "babel src/index.js -o index.js",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/alexmingoia/purs-loader.git"
+  },
+  "keywords": [
+    "loader",
+    "webpack",
+    "purescript",
+    "purs-loader",
+    "purs-loader"
+  ],
+  "author": "Alexander C. Mingoia",
+  "contributors": [
+    "Eric Thul"
+  ],
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/alexmingoia/purs-loader/issues"
+  },
+  "homepage": "https://github.com/alexmingoia/purs-loader#readme",
+  "peerDependencies": {
+    "webpack": ">=1.0.0 <3.0.0",
+    "purescript": ">=0.8.0"
   },
   "dependencies": {
+    "bluebird": "^3.3.5",
+    "chalk": "^1.1.3",
     "debug": "^2.2.0",
-    "js-string-escape": "^1.0.1"
+    "globby": "^4.0.0",
+    "loader-utils": "^0.2.14",
+    "promise-retry": "^1.1.0"
+  },
+  "devDependencies": {
+    "babel-cli": "^6.8.0",
+    "babel-preset-es2015": "^6.6.0"
   }
 }
diff --git a/src/PursLoader/Debug.js b/src/PursLoader/Debug.js
deleted file mode 100644 (file)
index 85eca10..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-'use strict';
-
-// module PursLoader.Debug
-
-var debug_ = require('debug')('purs-loader');
-
-function debug(message) {
-  return function(){
-    debug_(message);
-  };
-}
-exports.debug = debug;
diff --git a/src/PursLoader/Debug.purs b/src/PursLoader/Debug.purs
deleted file mode 100644 (file)
index 7a02f69..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-module PursLoader.Debug (debug) where
-
-import Prelude (Unit())
-
-import Control.Monad.Eff (Eff())
-
-import PursLoader.LoaderRef (Loader())
-
-foreign import debug :: forall eff. String -> Eff (loader :: Loader | eff) Unit
diff --git a/src/PursLoader/JsStringEscape.js b/src/PursLoader/JsStringEscape.js
deleted file mode 100644 (file)
index ff0a1a6..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-'use strict';
-
-// module PursLoader.JsStringEscape
-
-var jsStringEscape = require('js-string-escape');
-
-exports.jsStringEscape = jsStringEscape;
diff --git a/src/PursLoader/JsStringEscape.purs b/src/PursLoader/JsStringEscape.purs
deleted file mode 100644 (file)
index 79590ae..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-module PursLoader.JsStringEscape (jsStringEscape) where
-
-foreign import jsStringEscape :: String -> String
diff --git a/src/PursLoader/Loader.purs b/src/PursLoader/Loader.purs
deleted file mode 100644 (file)
index acb0993..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-module PursLoader.Loader
-  ( Effects()
-  , Effects_()
-  , loader
-  , loaderFn
-  ) where
-
-import Prelude (Unit(), ($), (>>=), (<$>), (<*>), (++), (<<<), bind, const, id, pure, unit)
-
-import Control.Bind (join)
-import Control.Monad.Eff (Eff(), foreachE)
-import Control.Monad.Eff.Console (CONSOLE())
-import Control.Monad.Eff.Exception (EXCEPTION(), Error(), error)
-
-import Data.Array ((!!))
-import Data.Either (Either(..), either)
-import Data.Function (Fn2(), mkFn2)
-import Data.Maybe (maybe)
-import Data.Nullable (toMaybe)
-import Data.String.Regex (Regex(), match, noFlags, regex)
-
-import Unsafe.Coerce (unsafeCoerce)
-
-import PursLoader.Debug (debug)
-import PursLoader.JsStringEscape (jsStringEscape)
-import PursLoader.LoaderRef
-  ( AsyncCallback()
-  , LoaderRef()
-  , Loader()
-  , async
-  , cacheable
-  , addDependency
-  , resourcePath
-  )
-import PursLoader.Path (dirname, joinPath, relative)
-import PursLoader.Plugin as Plugin
-
-type Effects eff = (console :: CONSOLE, err :: EXCEPTION | eff)
-
-type Effects_ eff = Effects (loader :: Loader | eff)
-
-loader :: forall eff. LoaderRef -> String -> Eff (Effects_ eff) Unit
-loader ref source = do
-  callback <- async ref
-
-  cacheable ref
-
-  debug "Invoke PureScript plugin compilation"
-
-  pluginContext.compile (compile callback)
-  where
-  pluginContext :: Plugin.Context (Effects_ eff)
-  pluginContext = (unsafeCoerce ref).purescriptWebpackPluginContext
-
-  compile :: AsyncCallback (Effects eff) -> Plugin.Compile (Effects_ eff)
-  compile callback error' graph = do
-    either (const $ pure unit) (\a -> debug ("Adding PureScript dependency " ++ a)) name
-
-    addDependency ref (resourcePath ref)
-
-    either (const $ callback (pure fixedError) "") id
-           (handle <$> name <*> dependencies <*> exports)
-    where
-    fixedError :: Error
-    fixedError = error "PureScript compilation has failed."
-
-    handle :: String -> Array String -> String -> Eff (Effects_ eff) Unit
-    handle name' deps res = do
-      debug ("Adding PureScript dependencies for " ++ name')
-      foreachE deps (addDependency ref)
-      debug "Generated loader result"
-      debug res
-      callback (const fixedError <$> toMaybe error') res
-
-    exports :: Either Error String
-    exports =
-      if pluginContext.options.bundle
-         then bundleExport <$> name
-         else moduleExport <<< modulePath <$> name
-      where
-      bundleExport :: String -> String
-      bundleExport name' = "module.exports = require('" ++ jsStringEscape path ++ "')['" ++ name' ++ "'];"
-        where
-        path :: String
-        path = relative resourceDir pluginContext.options.bundleOutput
-
-      moduleExport :: String -> String
-      moduleExport path = "module.exports = require('" ++ jsStringEscape path ++ "');"
-
-      modulePath :: String -> String
-      modulePath = relative resourceDir <<< joinPath pluginContext.options.output
-
-      resourceDir :: String
-      resourceDir = dirname (resourcePath ref)
-
-    dependencies :: Either Error (Array String)
-    dependencies = Plugin.dependenciesOf graph (resourcePath ref)
-
-    name :: Either Error String
-    name =
-      maybe (Left $ error "Failed to parse module name") Right
-            (join $ match re source >>= \as -> as !! 1)
-      where
-      re :: Regex
-      re = regex "(?:^|\\n)module\\s+([\\w\\.]+)" noFlags { ignoreCase = true }
-
-loaderFn :: forall eff. Fn2 LoaderRef String (Eff (Effects_ eff) Unit)
-loaderFn = mkFn2 loader
diff --git a/src/PursLoader/LoaderRef.js b/src/PursLoader/LoaderRef.js
deleted file mode 100644 (file)
index a5d8e1f..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-'use strict';
-
-// module PursLoader.LoaderRef
-
-function asyncFn(isJust, fromMaybe, ref){
-  return function(){
-    var callback = ref.async();
-    return function(error){
-      return function(value){
-        return function(){
-          return isJust(error) ? callback(fromMaybe(new Error())(error))
-                               : callback(null, value);
-        };
-      };
-    };
-  };
-}
-function cacheable(ref){
-  return function(){
-    return ref.cacheable && ref.cacheable();
-  };
-}
-
-function clearDependencies(ref){
-  return function(){
-    return ref.clearDependencies();
-  };
-}
-
-function resourcePath(ref){
-  return ref.resourcePath;
-}
-
-function addDependency(ref){
-  return function(dep){
-    return function(){
-      return ref.addDependency(dep);
-    };
-  };
-}
-
-exports.asyncFn = asyncFn;
-
-exports.cacheable = cacheable;
-
-exports.clearDependencies = clearDependencies;
-
-exports.resourcePath = resourcePath;
-
-exports.addDependency = addDependency;
diff --git a/src/PursLoader/LoaderRef.purs b/src/PursLoader/LoaderRef.purs
deleted file mode 100644 (file)
index 140d94a..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-module PursLoader.LoaderRef
-  ( LoaderRef()
-  , Loader()
-  , AsyncCallback()
-  , async
-  , cacheable
-  , clearDependencies
-  , addDependency
-  , resourcePath
-  ) where
-
-import Prelude (Unit())
-
-import Control.Monad.Eff (Eff())
-import Control.Monad.Eff.Exception (Error())
-
-import Data.Function (Fn3(), runFn3)
-import Data.Maybe (Maybe(), fromMaybe, isJust)
-
-type AsyncCallback eff = Maybe Error -> String -> Eff (loader :: Loader | eff) Unit
-
-data LoaderRef
-
-foreign import data Loader :: !
-
-foreign import asyncFn :: forall eff. Fn3 (Maybe Error -> Boolean)
-                                          (Error -> Maybe Error -> Error)
-                                          LoaderRef
-                                          (Eff (loader :: Loader | eff) (AsyncCallback eff))
-
-async :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) (Maybe Error -> String -> Eff (loader :: Loader | eff) Unit)
-async ref = runFn3 asyncFn isJust fromMaybe ref
-
-foreign import cacheable :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
-
-foreign import clearDependencies :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
-
-foreign import resourcePath :: LoaderRef -> String
-
-foreign import addDependency :: forall eff. LoaderRef -> String -> Eff (loader :: Loader | eff) Unit
diff --git a/src/PursLoader/Path.js b/src/PursLoader/Path.js
deleted file mode 100644 (file)
index 878f256..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-'use strict'
-
-// module PursLoader.Path
-
-var path = require('path');
-
-function relative(from) {
-  return function(to){
-    return path.relative(from, to);
-  };
-}
-exports.relative = relative;
-
-
-function joinPath(a) {
-  return function(b) {
-    return path.join(a, b);
-  };
-}
-exports.joinPath = joinPath;
-
-exports.resolve = path.resolve;
-
-exports.dirname = path.dirname;
diff --git a/src/PursLoader/Path.purs b/src/PursLoader/Path.purs
deleted file mode 100644 (file)
index 98cad5a..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-module PursLoader.Path
-  ( relative
-  , resolve
-  , dirname
-  , joinPath
-  ) where
-
-foreign import relative :: String -> String -> String
-
-foreign import resolve :: String -> String
-
-foreign import dirname :: String -> String
-
-foreign import joinPath :: String -> String -> String
diff --git a/src/PursLoader/Plugin.js b/src/PursLoader/Plugin.js
deleted file mode 100644 (file)
index ded6df5..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-'use strict';
-
-// module PursLoader.Plugin
-
-function dependenciesOfFn(left, right, graph, node) {
-  try {
-    var dependencies = graph.dependenciesOf(node);
-    return right(dependencies);
-  }
-  catch (error) {
-    return left(error);
-  }
-}
-exports.dependenciesOfFn = dependenciesOfFn;
diff --git a/src/PursLoader/Plugin.purs b/src/PursLoader/Plugin.purs
deleted file mode 100644 (file)
index c798c83..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-module PursLoader.Plugin
-  ( Compile()
-  , Context()
-  , Options()
-  , DependencyGraph()
-  , dependenciesOf
-  ) where
-
-import Prelude (Unit())
-
-import Control.Monad.Eff (Eff())
-import Control.Monad.Eff.Exception (Error())
-
-import Data.Either (Either(..))
-import Data.Function (Fn4(), runFn4)
-import Data.Nullable (Nullable())
-
-type Compile eff = Nullable Error -> DependencyGraph -> Eff eff Unit
-
-type Context eff = { compile :: Compile eff -> Eff eff Unit, options :: Options }
-
-type Options = { bundle :: Boolean, output :: String, bundleOutput :: String }
-
-dependenciesOf :: DependencyGraph -> String -> Either Error (Array String)
-dependenciesOf = runFn4 dependenciesOfFn Left Right
-
-foreign import data DependencyGraph :: *
-
-foreign import dependenciesOfFn
-  :: Fn4 (Error -> Either Error (Array String))
-         (Array String -> Either Error (Array String))
-         DependencyGraph
-         String
-         (Either Error (Array String))
diff --git a/src/index.js b/src/index.js
new file mode 100644 (file)
index 0000000..0a25ccd
--- /dev/null
@@ -0,0 +1,465 @@
+'use strict'
+
+const colors = require('chalk')
+const debug = require('debug')('purs-loader')
+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
+
+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 options = Object.assign({
+    context: config.context,
+    psc: 'psc',
+    pscArgs: {},
+    pscBundle: 'psc-bundle',
+    pscBundleArgs: {},
+    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: [],
+  }
+
+  if (!config.purescriptLoaderInstalled) {
+    config.purescriptLoaderInstalled = true
+
+    // invalidate loader cache when bundle is marked as invalid (in watch mode)
+    this._compiler.plugin('invalid', () => {
+      cache = config.purescriptLoaderCache = {
+        rebuild: true,
+        deferred: [],
+        ideServer: cache.ideServer
+      }
+    })
+
+    // add psc warnings to webpack compilation warnings
+    this._compiler.plugin('after-compile', (compilation, callback) => {
+      if (options.warnings && cache.warnings && cache.warnings.length) {
+        compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings.join('')}`)
+      }
+
+      if (cache.errors && cache.errors.length) {
+        compilation.errors.unshift(`PureScript compilation:\n${cache.errors.join('\n')}`)
+      }
+
+      callback()
+    })
+  }
+
+  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,
+  }
+
+  if (options.bundle) {
+    cache.bundleModules.push(psModule.name)
+  }
+
+  if (cache.rebuild) {
+    return connectIdeServer(psModule)
+      .then(rebuild)
+      .then(toJavaScript)
+      .then(psModule.load)
+      .catch(psModule.reject)
+  }
+
+  if (cache.compilation && cache.compilation.length) {
+    return toJavaScript(psModule).then(psModule.load).catch(psModule.reject)
+  }
+
+  // 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.compilation) {
+    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))
+      })
+  }
+}
+
+// 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.srcPath)
+
+  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)
+    }
+
+    return js
+  })
+}
+
+function compile(psModule) {
+  const options = psModule.options
+  const cache = psModule.cache
+  const stderr = []
+
+  if (cache.compilation) return Promise.resolve(cache.compilation)
+
+  cache.compilation = []
+  cache.warnings = []
+  cache.errors = []
+
+
+  const args = dargs(Object.assign({
+    _: options.src,
+    ffi: options.ffi,
+    output: options.output,
+  }, options.pscArgs))
+
+  debug('spawning compiler %s %o', options.psc, args)
+
+  return (new Promise((resolve, reject) => {
+    console.log('\nCompiling PureScript...')
+
+    const compilation = spawn(options.psc, args)
+
+    compilation.stderr.on('data', data => stderr.push(data.toString()))
+
+    compilation.on('close', code => {
+      console.log('Finished compiling PureScript.')
+      if (code !== 0) {
+        cache.compilation = cache.errors = stderr
+        reject(true)
+      } else {
+        cache.compilation = cache.warnings = stderr
+        resolve(psModule)
+      }
+    })
+  }))
+  .then(compilerOutput => {
+    if (options.bundle) {
+      return bundle(options, cache).then(() => psModule)
+    }
+    return psModule
+  })
+}
+
+function rebuild(psModule) {
+  const options = psModule.options
+  const cache = psModule.cache
+
+  debug('attempting rebuild with psc-ide-client %s', psModule.srcPath)
+
+  const request = (body) => new Promise((resolve, reject) => {
+    const args = dargs(options.pscIdeArgs)
+    const ideClient = spawn('psc-ide-client', args)
+
+    ideClient.stdout.once('data', data => {
+      const res = JSON.parse(data.toString())
+      debug(res)
+
+      if (!Array.isArray(res.result)) {
+        return res.resultType === 'success'
+               ? resolve(psModule)
+               : reject(res.result)
+      }
+
+      Promise.map(res.result, (item, i) => {
+        debug(item)
+        return formatIdeResult(item, options, i, res.result.length)
+      })
+      .then(compileMessages => {
+        if (res.resultType === 'error') {
+          cache.errors = compileMessages
+          reject(res.result)
+        } else {
+          cache.warnings = compileMessages
+          resolve(psModule)
+        }
+      })
+    })
+
+    ideClient.stderr.once('data', data => reject(data.toString()))
+
+    ideClient.stdin.write(JSON.stringify(body))
+    ideClient.stdin.write('\n')
+  })
+
+  return request({
+    command: 'rebuild',
+    params: {
+      file: psModule.srcPath,
+    }
+  }).catch(res => {
+    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' }))
+      }
+    }
+    return Promise.resolve(psModule)
+  })
+}
+
+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
+    }
+
+    // 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
+    }
+
+    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)}`
+    } 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}`
+    )
+  })
+}
+
+function bundle(options, cache) {
+  if (cache.bundle) return Promise.resolve(cache.bundle)
+
+  const stdout = []
+  const stderr = cache.bundle = []
+
+  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(' '))
+
+  return (new Promise((resolve, reject) => {
+    console.log('Bundling PureScript...')
+
+    const compilation = spawn(options.pscBundle, args)
+
+    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.concat(stderr)
+        return reject(true)
+      }
+      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
+      })
+  })
+}
+
+function connectIdeServer(psModule) {
+  const options = psModule.options
+  const cache = psModule.cache
+
+  if (cache.ideServer) return Promise.resolve(psModule)
+
+  cache.ideServer = true
+
+  const connect = () => new Promise((resolve, reject) => {
+    const args = dargs(options.pscIdeArgs)
+
+    debug('attempting to connect to psc-ide-server', args)
+
+    const ideClient = spawn('psc-ide-client', args)
+
+    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)
+        }
+      } 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)
+      }
+
+      return retry(error)
+    })
+  }, {
+    retries: 9,
+    factor: 1,
+    minTimeout: 333,
+    maxTimeout: 333,
+  })
+}
+
+function match(regex, str) {
+  const matches = str.match(regex)
+  return matches && matches[1]
+}
+
+function dargs(obj) {
+  return Object.keys(obj).reduce((args, key) => {
+    const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
+    const val = obj[key]
+
+    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])
+
+    return args
+  }, [])
+}
diff --git a/webpack.config.js b/webpack.config.js
deleted file mode 100644 (file)
index a39832f..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-'use strict';
-
-var path = require('path');
-
-var webpack = require('webpack');
-
-var packageJson = require('./package.json');
-
-var noErrorsPlugin = webpack.NoErrorsPlugin;
-
-var dedupePlugin = webpack.optimize.DedupePlugin;
-
-var config
-  = { cache: true
-    , target: 'node'
-    , entry: { index: './entry' }
-    , externals: Object.keys(packageJson.dependencies).reduce(function(b, a){
-                   b[a] = 'commonjs ' + a;
-                   return b;
-                 }, {})
-    , output: { path: __dirname
-              , filename: '[name].js'
-              , libraryTarget: 'commonjs2'
-              }
-    , plugins: [ new noErrorsPlugin()
-               , new dedupePlugin()
-               ]
-    , resolve: { modulesDirectories: [ 'build' ] }
-    }
-    ;
-
-module.exports = config;