aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAlex Mingoia <talk@alexmingoia.com>2016-05-10 00:09:28 -0700
committerAlex Mingoia <talk@alexmingoia.com>2016-05-10 00:09:28 -0700
commit7de41f10b4ff0f0d6b45d59bee0f166c3cfe3f9f (patch)
tree9ee160ba5b7dab900ccd1cfa657760e4103a175e
parent777472b3830cb3d2ff3390003ea422c6d4522715 (diff)
downloadpurs-loader-7de41f10b4ff0f0d6b45d59bee0f166c3cfe3f9f.tar.gz
purs-loader-7de41f10b4ff0f0d6b45d59bee0f166c3cfe3f9f.tar.zst
purs-loader-7de41f10b4ff0f0d6b45d59bee0f166c3cfe3f9f.zip
Refactor to compile independently of purescript-webpack-plugin.
- 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.
-rw-r--r--.babelrc1
-rw-r--r--.gitignore10
-rw-r--r--LICENSE2
-rw-r--r--README.md52
-rw-r--r--bower.json10
-rw-r--r--docs/PursLoader/Debug.md9
-rw-r--r--docs/PursLoader/JsStringEscape.md9
-rw-r--r--docs/PursLoader/Loader.md27
-rw-r--r--docs/PursLoader/LoaderRef.md51
-rw-r--r--docs/PursLoader/LoaderUtil.md9
-rw-r--r--docs/PursLoader/Options.md21
-rw-r--r--docs/PursLoader/Path.md27
-rw-r--r--docs/PursLoader/Plugin.md33
-rw-r--r--entry.js11
-rw-r--r--package.json59
-rw-r--r--src/PursLoader/Debug.js12
-rw-r--r--src/PursLoader/Debug.purs9
-rw-r--r--src/PursLoader/JsStringEscape.js7
-rw-r--r--src/PursLoader/JsStringEscape.purs3
-rw-r--r--src/PursLoader/Loader.purs108
-rw-r--r--src/PursLoader/LoaderRef.js50
-rw-r--r--src/PursLoader/LoaderRef.purs40
-rw-r--r--src/PursLoader/Path.js24
-rw-r--r--src/PursLoader/Path.purs14
-rw-r--r--src/PursLoader/Plugin.js14
-rw-r--r--src/PursLoader/Plugin.purs34
-rw-r--r--src/index.js465
-rw-r--r--webpack.config.js32
28 files changed, 558 insertions, 585 deletions
diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..9d8d516
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1 @@
{ "presets": ["es2015"] }
diff --git a/.gitignore b/.gitignore
index 0a05586..548b3c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,5 @@
1.psci 1**DS_Store*
2.pulp-cache 2build/
3npm-debug.log
4index.json
5index.js
6node_modules/ 3node_modules/
7bower_components/ 4bower_components/
8build/ 5/index.js
9tmp/
diff --git a/LICENSE b/LICENSE
index 05b0016..6fe3810 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
1Copyright (c) 2015 Eric Thul 1Copyright (c) 2016 Alexander Mingoia and Eric Thul
2 2
3Permission is hereby granted, free of charge, to any person obtaining a 3Permission is hereby granted, free of charge, to any person obtaining a
4copy of this software and associated documentation files (the 4copy of this software and associated documentation files (the
diff --git a/README.md b/README.md
index df92e69..ed25296 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,62 @@
2 2
3> [PureScript](http://www.purescript.org) loader for [webpack](http://webpack.github.io) 3> [PureScript](http://www.purescript.org) loader for [webpack](http://webpack.github.io)
4 4
5- Supports hot-reloading and rebuilding of single source files
6- Dead code elimination using the `bundle` option
7- Colorized build output using `purescript-psa` and the `psc: "psa"` option
8
5## Install 9## Install
6 10
7Install with [npm](https://npmjs.org/package/purs-loader). 11Install with [npm](https://npmjs.org/package/purs-loader).
8 12
9This loader works in conjunction with the [PureScript webpack plugin](https://npmjs.org/package/purescript-webpack-plugin). Ensure the plugin is installed and configured accordingly.
10
11``` 13```
12npm install purs-loader --save-dev 14npm install purs-loader --save-dev
13``` 15```
14 16
15## Example 17## Example
16 18
17Refer to the [purescript-webpack-example](https://github.com/ethul/purescript-webpack-example) for an example. 19```javascript
20const webpackConfig = {
21 // ...
22 loaders: [
23 // ...
24 {
25 test: /\.purs$/,
26 loader: 'purs-loader',
27 exclude: /node_modules/,
28 query: {
29 psc: 'psa',
30 src: ['bower_components/purescript-*/src/**/*.purs', 'src/**/*.purs'],
31 ffi: ['bower_components/purescript-*/src/**/*.js', 'src/**/*.js'],
32 }
33 }
34 // ...
35 ]
36 // ...
37}
38```
39
40Default options:
41
42```javascript
43{
44 psc: 'psc',
45 pscArgs: {},
46 pscBundle: 'psc-bundle',
47 pscBundleArgs: {},
48 pscIdeColors: false, // defaults to true if psc === 'psa'
49 bundleOutput: 'output/bundle.js',
50 bundleNamespace: 'PS',
51 bundle: false,
52 warnings: true,
53 output: 'output',
54 src: [
55 path.join('src', '**', '*.purs'),
56 path.join('bower_components', 'purescript-*', 'src', '**', '*.purs')
57 ],
58 ffi: [
59 path.join('src', '**', '*.js'),
60 path.join('bower_components', 'purescript-*', 'src', '**', '*.js')
61 ],
62}
63```
diff --git a/bower.json b/bower.json
deleted file mode 100644
index 761c24c..0000000
--- a/bower.json
+++ /dev/null
@@ -1,10 +0,0 @@
1{
2 "name": "purs-loader",
3 "private": true,
4 "dependencies": {
5 "purescript-aff": "^0.13.0",
6 "purescript-foreign": "^0.7.0",
7 "purescript-unsafe-coerce": "~0.1.0",
8 "purescript-nullable": "~0.2.1"
9 }
10}
diff --git a/docs/PursLoader/Debug.md b/docs/PursLoader/Debug.md
deleted file mode 100644
index 824a9f8..0000000
--- a/docs/PursLoader/Debug.md
+++ /dev/null
@@ -1,9 +0,0 @@
1## Module PursLoader.Debug
2
3#### `debug`
4
5``` purescript
6debug :: forall eff. String -> Eff (loader :: Loader | eff) Unit
7```
8
9
diff --git a/docs/PursLoader/JsStringEscape.md b/docs/PursLoader/JsStringEscape.md
deleted file mode 100644
index 09f52aa..0000000
--- a/docs/PursLoader/JsStringEscape.md
+++ /dev/null
@@ -1,9 +0,0 @@
1## Module PursLoader.JsStringEscape
2
3#### `jsStringEscape`
4
5``` purescript
6jsStringEscape :: String -> String
7```
8
9
diff --git a/docs/PursLoader/Loader.md b/docs/PursLoader/Loader.md
deleted file mode 100644
index d05e3b7..0000000
--- a/docs/PursLoader/Loader.md
+++ /dev/null
@@ -1,27 +0,0 @@
1## Module PursLoader.Loader
2
3#### `Effects`
4
5``` purescript
6type Effects eff = (console :: CONSOLE, err :: EXCEPTION | eff)
7```
8
9#### `Effects_`
10
11``` purescript
12type Effects_ eff = Effects (loader :: Loader | eff)
13```
14
15#### `loader`
16
17``` purescript
18loader :: forall eff. LoaderRef -> String -> Eff (Effects_ eff) Unit
19```
20
21#### `loaderFn`
22
23``` purescript
24loaderFn :: forall eff. Fn2 LoaderRef String (Eff (Effects_ eff) Unit)
25```
26
27
diff --git a/docs/PursLoader/LoaderRef.md b/docs/PursLoader/LoaderRef.md
deleted file mode 100644
index 917db3a..0000000
--- a/docs/PursLoader/LoaderRef.md
+++ /dev/null
@@ -1,51 +0,0 @@
1## Module PursLoader.LoaderRef
2
3#### `AsyncCallback`
4
5``` purescript
6type AsyncCallback eff = Maybe Error -> String -> Eff (loader :: Loader | eff) Unit
7```
8
9#### `LoaderRef`
10
11``` purescript
12data LoaderRef
13```
14
15#### `Loader`
16
17``` purescript
18data Loader :: !
19```
20
21#### `async`
22
23``` purescript
24async :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) (Maybe Error -> String -> Eff (loader :: Loader | eff) Unit)
25```
26
27#### `cacheable`
28
29``` purescript
30cacheable :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
31```
32
33#### `clearDependencies`
34
35``` purescript
36clearDependencies :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
37```
38
39#### `resourcePath`
40
41``` purescript
42resourcePath :: LoaderRef -> String
43```
44
45#### `addDependency`
46
47``` purescript
48addDependency :: forall eff. LoaderRef -> String -> Eff (loader :: Loader | eff) Unit
49```
50
51
diff --git a/docs/PursLoader/LoaderUtil.md b/docs/PursLoader/LoaderUtil.md
deleted file mode 100644
index 36d6879..0000000
--- a/docs/PursLoader/LoaderUtil.md
+++ /dev/null
@@ -1,9 +0,0 @@
1## Module PursLoader.LoaderUtil
2
3#### `parseQuery`
4
5``` purescript
6parseQuery :: String -> Foreign
7```
8
9
diff --git a/docs/PursLoader/Options.md b/docs/PursLoader/Options.md
deleted file mode 100644
index b3352fc..0000000
--- a/docs/PursLoader/Options.md
+++ /dev/null
@@ -1,21 +0,0 @@
1## Module PursLoader.Options
2
3#### `Options`
4
5``` purescript
6newtype Options
7 = Options { bundleOutput :: String }
8```
9
10##### Instances
11``` purescript
12IsForeign Options
13```
14
15#### `runOptions`
16
17``` purescript
18runOptions :: Options -> Options_
19```
20
21
diff --git a/docs/PursLoader/Path.md b/docs/PursLoader/Path.md
deleted file mode 100644
index cc00436..0000000
--- a/docs/PursLoader/Path.md
+++ /dev/null
@@ -1,27 +0,0 @@
1## Module PursLoader.Path
2
3#### `relative`
4
5``` purescript
6relative :: String -> String -> String
7```
8
9#### `resolve`
10
11``` purescript
12resolve :: String -> String
13```
14
15#### `dirname`
16
17``` purescript
18dirname :: String -> String
19```
20
21#### `joinPath`
22
23``` purescript
24joinPath :: String -> String -> String
25```
26
27
diff --git a/docs/PursLoader/Plugin.md b/docs/PursLoader/Plugin.md
deleted file mode 100644
index 7a524da..0000000
--- a/docs/PursLoader/Plugin.md
+++ /dev/null
@@ -1,33 +0,0 @@
1## Module PursLoader.Plugin
2
3#### `Compile`
4
5``` purescript
6type Compile eff = Nullable Error -> DependencyGraph -> Eff eff Unit
7```
8
9#### `Context`
10
11``` purescript
12type Context eff = { compile :: Compile eff -> Eff eff Unit, options :: Options }
13```
14
15#### `Options`
16
17``` purescript
18type Options = { bundle :: Boolean, output :: String, bundleOutput :: String }
19```
20
21#### `dependenciesOf`
22
23``` purescript
24dependenciesOf :: DependencyGraph -> String -> Either Error (Array String)
25```
26
27#### `DependencyGraph`
28
29``` purescript
30data DependencyGraph :: *
31```
32
33
diff --git a/entry.js b/entry.js
deleted file mode 100644
index 87f52d3..0000000
--- a/entry.js
+++ /dev/null
@@ -1,11 +0,0 @@
1'use strict';
2
3var pursLoader = require('PursLoader.Loader');
4
5function loader(source) {
6 var ref = this;
7 var result = pursLoader.loaderFn(ref, source);
8 return result();
9}
10
11module.exports = loader;
diff --git a/package.json b/package.json
index 6ab55ec..4cbdd72 100644
--- a/package.json
+++ b/package.json
@@ -1,30 +1,49 @@
1{ 1{
2 "name": "purs-loader", 2 "name": "purs-loader",
3 "version": "0.6.0", 3 "version": "1.0.0",
4 "description": "PureScript loader for webpack", 4 "description": "A webpack loader for PureScript.",
5 "license": "MIT", 5 "main": "index.js",
6 "repository": "ethul/purs-loader",
7 "author": {
8 "name": "Eric Thul",
9 "email": "thul.eric@gmail.com"
10 },
11 "scripts": {
12 "build": "npm run-script build:compile && npm run-script build:docs && npm run-script build:package",
13 "build:compile": "pulp build -o build --force",
14 "build:docs": "pulp docs",
15 "build:package": "webpack --progress --colors --profile --bail",
16 "build:watch": "pulp -w build -o build --force",
17 "build:json": "webpack --progress --colors --profile --bail --json > index.json",
18 "prepublish": "npm run-script build"
19 },
20 "files": [ 6 "files": [
21 "index.js" 7 "index.js"
22 ], 8 ],
23 "devDependencies": { 9 "scripts": {
24 "webpack": "^1.8.4" 10 "build": "babel src/index.js -o index.js",
11 "test": "echo \"Error: no test specified\" && exit 1"
12 },
13 "repository": {
14 "type": "git",
15 "url": "git://github.com/alexmingoia/purs-loader.git"
16 },
17 "keywords": [
18 "loader",
19 "webpack",
20 "purescript",
21 "purs-loader",
22 "purs-loader"
23 ],
24 "author": "Alexander C. Mingoia",
25 "contributors": [
26 "Eric Thul"
27 ],
28 "license": "MIT",
29 "bugs": {
30 "url": "https://github.com/alexmingoia/purs-loader/issues"
31 },
32 "homepage": "https://github.com/alexmingoia/purs-loader#readme",
33 "peerDependencies": {
34 "webpack": ">=1.0.0 <3.0.0",
35 "purescript": ">=0.8.0"
25 }, 36 },
26 "dependencies": { 37 "dependencies": {
38 "bluebird": "^3.3.5",
39 "chalk": "^1.1.3",
27 "debug": "^2.2.0", 40 "debug": "^2.2.0",
28 "js-string-escape": "^1.0.1" 41 "globby": "^4.0.0",
42 "loader-utils": "^0.2.14",
43 "promise-retry": "^1.1.0"
44 },
45 "devDependencies": {
46 "babel-cli": "^6.8.0",
47 "babel-preset-es2015": "^6.6.0"
29 } 48 }
30} 49}
diff --git a/src/PursLoader/Debug.js b/src/PursLoader/Debug.js
deleted file mode 100644
index 85eca10..0000000
--- a/src/PursLoader/Debug.js
+++ /dev/null
@@ -1,12 +0,0 @@
1'use strict';
2
3// module PursLoader.Debug
4
5var debug_ = require('debug')('purs-loader');
6
7function debug(message) {
8 return function(){
9 debug_(message);
10 };
11}
12exports.debug = debug;
diff --git a/src/PursLoader/Debug.purs b/src/PursLoader/Debug.purs
deleted file mode 100644
index 7a02f69..0000000
--- a/src/PursLoader/Debug.purs
+++ /dev/null
@@ -1,9 +0,0 @@
1module PursLoader.Debug (debug) where
2
3import Prelude (Unit())
4
5import Control.Monad.Eff (Eff())
6
7import PursLoader.LoaderRef (Loader())
8
9foreign 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
index ff0a1a6..0000000
--- a/src/PursLoader/JsStringEscape.js
+++ /dev/null
@@ -1,7 +0,0 @@
1'use strict';
2
3// module PursLoader.JsStringEscape
4
5var jsStringEscape = require('js-string-escape');
6
7exports.jsStringEscape = jsStringEscape;
diff --git a/src/PursLoader/JsStringEscape.purs b/src/PursLoader/JsStringEscape.purs
deleted file mode 100644
index 79590ae..0000000
--- a/src/PursLoader/JsStringEscape.purs
+++ /dev/null
@@ -1,3 +0,0 @@
1module PursLoader.JsStringEscape (jsStringEscape) where
2
3foreign import jsStringEscape :: String -> String
diff --git a/src/PursLoader/Loader.purs b/src/PursLoader/Loader.purs
deleted file mode 100644
index acb0993..0000000
--- a/src/PursLoader/Loader.purs
+++ /dev/null
@@ -1,108 +0,0 @@
1module PursLoader.Loader
2 ( Effects()
3 , Effects_()
4 , loader
5 , loaderFn
6 ) where
7
8import Prelude (Unit(), ($), (>>=), (<$>), (<*>), (++), (<<<), bind, const, id, pure, unit)
9
10import Control.Bind (join)
11import Control.Monad.Eff (Eff(), foreachE)
12import Control.Monad.Eff.Console (CONSOLE())
13import Control.Monad.Eff.Exception (EXCEPTION(), Error(), error)
14
15import Data.Array ((!!))
16import Data.Either (Either(..), either)
17import Data.Function (Fn2(), mkFn2)
18import Data.Maybe (maybe)
19import Data.Nullable (toMaybe)
20import Data.String.Regex (Regex(), match, noFlags, regex)
21
22import Unsafe.Coerce (unsafeCoerce)
23
24import PursLoader.Debug (debug)
25import PursLoader.JsStringEscape (jsStringEscape)
26import PursLoader.LoaderRef
27 ( AsyncCallback()
28 , LoaderRef()
29 , Loader()
30 , async
31 , cacheable
32 , addDependency
33 , resourcePath
34 )
35import PursLoader.Path (dirname, joinPath, relative)
36import PursLoader.Plugin as Plugin
37
38type Effects eff = (console :: CONSOLE, err :: EXCEPTION | eff)
39
40type Effects_ eff = Effects (loader :: Loader | eff)
41
42loader :: forall eff. LoaderRef -> String -> Eff (Effects_ eff) Unit
43loader ref source = do
44 callback <- async ref
45
46 cacheable ref
47
48 debug "Invoke PureScript plugin compilation"
49
50 pluginContext.compile (compile callback)
51 where
52 pluginContext :: Plugin.Context (Effects_ eff)
53 pluginContext = (unsafeCoerce ref).purescriptWebpackPluginContext
54
55 compile :: AsyncCallback (Effects eff) -> Plugin.Compile (Effects_ eff)
56 compile callback error' graph = do
57 either (const $ pure unit) (\a -> debug ("Adding PureScript dependency " ++ a)) name
58
59 addDependency ref (resourcePath ref)
60
61 either (const $ callback (pure fixedError) "") id
62 (handle <$> name <*> dependencies <*> exports)
63 where
64 fixedError :: Error
65 fixedError = error "PureScript compilation has failed."
66
67 handle :: String -> Array String -> String -> Eff (Effects_ eff) Unit
68 handle name' deps res = do
69 debug ("Adding PureScript dependencies for " ++ name')
70 foreachE deps (addDependency ref)
71 debug "Generated loader result"
72 debug res
73 callback (const fixedError <$> toMaybe error') res
74
75 exports :: Either Error String
76 exports =
77 if pluginContext.options.bundle
78 then bundleExport <$> name
79 else moduleExport <<< modulePath <$> name
80 where
81 bundleExport :: String -> String
82 bundleExport name' = "module.exports = require('" ++ jsStringEscape path ++ "')['" ++ name' ++ "'];"
83 where
84 path :: String
85 path = relative resourceDir pluginContext.options.bundleOutput
86
87 moduleExport :: String -> String
88 moduleExport path = "module.exports = require('" ++ jsStringEscape path ++ "');"
89
90 modulePath :: String -> String
91 modulePath = relative resourceDir <<< joinPath pluginContext.options.output
92
93 resourceDir :: String
94 resourceDir = dirname (resourcePath ref)
95
96 dependencies :: Either Error (Array String)
97 dependencies = Plugin.dependenciesOf graph (resourcePath ref)
98
99 name :: Either Error String
100 name =
101 maybe (Left $ error "Failed to parse module name") Right
102 (join $ match re source >>= \as -> as !! 1)
103 where
104 re :: Regex
105 re = regex "(?:^|\\n)module\\s+([\\w\\.]+)" noFlags { ignoreCase = true }
106
107loaderFn :: forall eff. Fn2 LoaderRef String (Eff (Effects_ eff) Unit)
108loaderFn = mkFn2 loader
diff --git a/src/PursLoader/LoaderRef.js b/src/PursLoader/LoaderRef.js
deleted file mode 100644
index a5d8e1f..0000000
--- a/src/PursLoader/LoaderRef.js
+++ /dev/null
@@ -1,50 +0,0 @@
1'use strict';
2
3// module PursLoader.LoaderRef
4
5function asyncFn(isJust, fromMaybe, ref){
6 return function(){
7 var callback = ref.async();
8 return function(error){
9 return function(value){
10 return function(){
11 return isJust(error) ? callback(fromMaybe(new Error())(error))
12 : callback(null, value);
13 };
14 };
15 };
16 };
17}
18function cacheable(ref){
19 return function(){
20 return ref.cacheable && ref.cacheable();
21 };
22}
23
24function clearDependencies(ref){
25 return function(){
26 return ref.clearDependencies();
27 };
28}
29
30function resourcePath(ref){
31 return ref.resourcePath;
32}
33
34function addDependency(ref){
35 return function(dep){
36 return function(){
37 return ref.addDependency(dep);
38 };
39 };
40}
41
42exports.asyncFn = asyncFn;
43
44exports.cacheable = cacheable;
45
46exports.clearDependencies = clearDependencies;
47
48exports.resourcePath = resourcePath;
49
50exports.addDependency = addDependency;
diff --git a/src/PursLoader/LoaderRef.purs b/src/PursLoader/LoaderRef.purs
deleted file mode 100644
index 140d94a..0000000
--- a/src/PursLoader/LoaderRef.purs
+++ /dev/null
@@ -1,40 +0,0 @@
1module PursLoader.LoaderRef
2 ( LoaderRef()
3 , Loader()
4 , AsyncCallback()
5 , async
6 , cacheable
7 , clearDependencies
8 , addDependency
9 , resourcePath
10 ) where
11
12import Prelude (Unit())
13
14import Control.Monad.Eff (Eff())
15import Control.Monad.Eff.Exception (Error())
16
17import Data.Function (Fn3(), runFn3)
18import Data.Maybe (Maybe(), fromMaybe, isJust)
19
20type AsyncCallback eff = Maybe Error -> String -> Eff (loader :: Loader | eff) Unit
21
22data LoaderRef
23
24foreign import data Loader :: !
25
26foreign import asyncFn :: forall eff. Fn3 (Maybe Error -> Boolean)
27 (Error -> Maybe Error -> Error)
28 LoaderRef
29 (Eff (loader :: Loader | eff) (AsyncCallback eff))
30
31async :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) (Maybe Error -> String -> Eff (loader :: Loader | eff) Unit)
32async ref = runFn3 asyncFn isJust fromMaybe ref
33
34foreign import cacheable :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
35
36foreign import clearDependencies :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
37
38foreign import resourcePath :: LoaderRef -> String
39
40foreign 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
index 878f256..0000000
--- a/src/PursLoader/Path.js
+++ /dev/null
@@ -1,24 +0,0 @@
1'use strict'
2
3// module PursLoader.Path
4
5var path = require('path');
6
7function relative(from) {
8 return function(to){
9 return path.relative(from, to);
10 };
11}
12exports.relative = relative;
13
14
15function joinPath(a) {
16 return function(b) {
17 return path.join(a, b);
18 };
19}
20exports.joinPath = joinPath;
21
22exports.resolve = path.resolve;
23
24exports.dirname = path.dirname;
diff --git a/src/PursLoader/Path.purs b/src/PursLoader/Path.purs
deleted file mode 100644
index 98cad5a..0000000
--- a/src/PursLoader/Path.purs
+++ /dev/null
@@ -1,14 +0,0 @@
1module PursLoader.Path
2 ( relative
3 , resolve
4 , dirname
5 , joinPath
6 ) where
7
8foreign import relative :: String -> String -> String
9
10foreign import resolve :: String -> String
11
12foreign import dirname :: String -> String
13
14foreign import joinPath :: String -> String -> String
diff --git a/src/PursLoader/Plugin.js b/src/PursLoader/Plugin.js
deleted file mode 100644
index ded6df5..0000000
--- a/src/PursLoader/Plugin.js
+++ /dev/null
@@ -1,14 +0,0 @@
1'use strict';
2
3// module PursLoader.Plugin
4
5function dependenciesOfFn(left, right, graph, node) {
6 try {
7 var dependencies = graph.dependenciesOf(node);
8 return right(dependencies);
9 }
10 catch (error) {
11 return left(error);
12 }
13}
14exports.dependenciesOfFn = dependenciesOfFn;
diff --git a/src/PursLoader/Plugin.purs b/src/PursLoader/Plugin.purs
deleted file mode 100644
index c798c83..0000000
--- a/src/PursLoader/Plugin.purs
+++ /dev/null
@@ -1,34 +0,0 @@
1module PursLoader.Plugin
2 ( Compile()
3 , Context()
4 , Options()
5 , DependencyGraph()
6 , dependenciesOf
7 ) where
8
9import Prelude (Unit())
10
11import Control.Monad.Eff (Eff())
12import Control.Monad.Eff.Exception (Error())
13
14import Data.Either (Either(..))
15import Data.Function (Fn4(), runFn4)
16import Data.Nullable (Nullable())
17
18type Compile eff = Nullable Error -> DependencyGraph -> Eff eff Unit
19
20type Context eff = { compile :: Compile eff -> Eff eff Unit, options :: Options }
21
22type Options = { bundle :: Boolean, output :: String, bundleOutput :: String }
23
24dependenciesOf :: DependencyGraph -> String -> Either Error (Array String)
25dependenciesOf = runFn4 dependenciesOfFn Left Right
26
27foreign import data DependencyGraph :: *
28
29foreign import dependenciesOfFn
30 :: Fn4 (Error -> Either Error (Array String))
31 (Array String -> Either Error (Array String))
32 DependencyGraph
33 String
34 (Either Error (Array String))
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..0a25ccd
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,465 @@
1'use strict'
2
3const colors = require('chalk')
4const debug = require('debug')('purs-loader')
5const loaderUtils = require('loader-utils')
6const globby = require('globby')
7const Promise = require('bluebird')
8const fs = Promise.promisifyAll(require('fs'))
9const spawn = require('child_process').spawn
10const path = require('path')
11const retryPromise = require('promise-retry')
12
13const psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
14const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g
15
16module.exports = function purescriptLoader(source, map) {
17 const callback = this.async()
18 const config = this.options
19 const query = loaderUtils.parseQuery(this.query)
20 const webpackOptions = this.options.purescriptLoader || {}
21
22 const options = Object.assign({
23 context: config.context,
24 psc: 'psc',
25 pscArgs: {},
26 pscBundle: 'psc-bundle',
27 pscBundleArgs: {},
28 pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa',
29 pscIdeArgs: {},
30 bundleOutput: 'output/bundle.js',
31 bundleNamespace: 'PS',
32 bundle: false,
33 warnings: true,
34 output: 'output',
35 src: [
36 path.join('src', '**', '*.purs'),
37 path.join('bower_components', 'purescript-*', 'src', '**', '*.purs')
38 ],
39 ffi: [
40 path.join('src', '**', '*.js'),
41 path.join('bower_components', 'purescript-*', 'src', '**', '*.js')
42 ],
43 }, webpackOptions, query)
44
45 this.cacheable && this.cacheable()
46
47 let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || {
48 rebuild: false,
49 deferred: [],
50 bundleModules: [],
51 }
52
53 if (!config.purescriptLoaderInstalled) {
54 config.purescriptLoaderInstalled = true
55
56 // invalidate loader cache when bundle is marked as invalid (in watch mode)
57 this._compiler.plugin('invalid', () => {
58 cache = config.purescriptLoaderCache = {
59 rebuild: true,
60 deferred: [],
61 ideServer: cache.ideServer
62 }
63 })
64
65 // add psc warnings to webpack compilation warnings
66 this._compiler.plugin('after-compile', (compilation, callback) => {
67 if (options.warnings && cache.warnings && cache.warnings.length) {
68 compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings.join('')}`)
69 }
70
71 if (cache.errors && cache.errors.length) {
72 compilation.errors.unshift(`PureScript compilation:\n${cache.errors.join('\n')}`)
73 }
74
75 callback()
76 })
77 }
78
79 const psModuleName = match(psModuleRegex, source)
80 const psModule = {
81 name: psModuleName,
82 load: js => callback(null, js),
83 reject: error => callback(error),
84 srcPath: this.resourcePath,
85 srcDir: path.dirname(this.resourcePath),
86 jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')),
87 options: options,
88 cache: cache,
89 }
90
91 if (options.bundle) {
92 cache.bundleModules.push(psModule.name)
93 }
94
95 if (cache.rebuild) {
96 return connectIdeServer(psModule)
97 .then(rebuild)
98 .then(toJavaScript)
99 .then(psModule.load)
100 .catch(psModule.reject)
101 }
102
103 if (cache.compilation && cache.compilation.length) {
104 return toJavaScript(psModule).then(psModule.load).catch(psModule.reject)
105 }
106
107 // We need to wait for compilation to finish before the loaders run so that
108 // references to compiled output are valid.
109 cache.deferred.push(psModule)
110
111 if (!cache.compilation) {
112 return compile(psModule)
113 .then(() => Promise.map(cache.deferred, psModule => {
114 if (typeof cache.ideServer === 'object') cache.ideServer.kill()
115 return toJavaScript(psModule).then(psModule.load)
116 }))
117 .catch(error => {
118 cache.deferred[0].reject(error)
119 cache.deferred.slice(1).forEach(psModule => psModule.reject(true))
120 })
121 }
122}
123
124// The actual loader is executed *after* purescript compilation.
125function toJavaScript(psModule) {
126 const options = psModule.options
127 const cache = psModule.cache
128 const bundlePath = path.resolve(options.bundleOutput)
129 const jsPath = cache.bundle ? bundlePath : psModule.jsPath
130
131 debug('loading JavaScript for', psModule.srcPath)
132
133 return Promise.props({
134 js: fs.readFileAsync(jsPath, 'utf8'),
135 psModuleMap: psModuleMap(options.src, cache)
136 }).then(result => {
137 let js = ''
138
139 if (options.bundle) {
140 // if bundling, return a reference to the bundle
141 js = 'module.exports = require("'
142 + path.relative(psModule.srcDir, options.bundleOutput)
143 + '")["' + psModule.name + '"]'
144 } else {
145 // replace require paths to output files generated by psc with paths
146 // to purescript sources, which are then also run through this loader.
147 const foreignRequire = 'require("' + path.resolve(
148 path.join(psModule.options.output, psModule.name, 'foreign.js')
149 ) + '")'
150
151 js = result.js
152 .replace(requireRegex, (m, p1) => {
153 return 'require("' + result.psModuleMap[p1] + '")'
154 })
155 .replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire)
156 }
157
158 return js
159 })
160}
161
162function compile(psModule) {
163 const options = psModule.options
164 const cache = psModule.cache
165 const stderr = []
166
167 if (cache.compilation) return Promise.resolve(cache.compilation)
168
169 cache.compilation = []
170 cache.warnings = []
171 cache.errors = []
172
173
174 const args = dargs(Object.assign({
175 _: options.src,
176 ffi: options.ffi,
177 output: options.output,
178 }, options.pscArgs))
179
180 debug('spawning compiler %s %o', options.psc, args)
181
182 return (new Promise((resolve, reject) => {
183 console.log('\nCompiling PureScript...')
184
185 const compilation = spawn(options.psc, args)
186
187 compilation.stderr.on('data', data => stderr.push(data.toString()))
188
189 compilation.on('close', code => {
190 console.log('Finished compiling PureScript.')
191 if (code !== 0) {
192 cache.compilation = cache.errors = stderr
193 reject(true)
194 } else {
195 cache.compilation = cache.warnings = stderr
196 resolve(psModule)
197 }
198 })
199 }))
200 .then(compilerOutput => {
201 if (options.bundle) {
202 return bundle(options, cache).then(() => psModule)
203 }
204 return psModule
205 })
206}
207
208function rebuild(psModule) {
209 const options = psModule.options
210 const cache = psModule.cache
211
212 debug('attempting rebuild with psc-ide-client %s', psModule.srcPath)
213
214 const request = (body) => new Promise((resolve, reject) => {
215 const args = dargs(options.pscIdeArgs)
216 const ideClient = spawn('psc-ide-client', args)
217
218 ideClient.stdout.once('data', data => {
219 const res = JSON.parse(data.toString())
220 debug(res)
221
222 if (!Array.isArray(res.result)) {
223 return res.resultType === 'success'
224 ? resolve(psModule)
225 : reject(res.result)
226 }
227
228 Promise.map(res.result, (item, i) => {
229 debug(item)
230 return formatIdeResult(item, options, i, res.result.length)
231 })
232 .then(compileMessages => {
233 if (res.resultType === 'error') {
234 cache.errors = compileMessages
235 reject(res.result)
236 } else {
237 cache.warnings = compileMessages
238 resolve(psModule)
239 }
240 })
241 })
242
243 ideClient.stderr.once('data', data => reject(data.toString()))
244
245 ideClient.stdin.write(JSON.stringify(body))
246 ideClient.stdin.write('\n')
247 })
248
249 return request({
250 command: 'rebuild',
251 params: {
252 file: psModule.srcPath,
253 }
254 }).catch(res => {
255 if (res.resultType === 'error') {
256 if (res.result.some(item => item.errorCode === 'UnknownModule')) {
257 console.log('Unknown module, attempting full recompile')
258 return compile(psModule).then(() => request({ command: 'load' }))
259 }
260 }
261 return Promise.resolve(psModule)
262 })
263}
264
265function formatIdeResult(result, options, index, length) {
266 const srcPath = path.relative(options.context, result.filename)
267 const pos = result.position
268 const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}`
269 let numAndErr = `[${index+1}/${length} ${result.errorCode}]`
270 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr
271
272 return fs.readFileAsync(result.filename, 'utf8').then(source => {
273 const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine)
274 const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine
275 const up = options.pscIdeColors ? colors.red('^') : '^'
276 const down = options.pscIdeColors ? colors.red('v') : 'v'
277 let trimmed = lines.slice(0)
278
279 if (endsOnNewline) {
280 lines.splice(lines.length - 1, 1)
281 pos.endLine = pos.endLine - 1
282 pos.endColumn = lines[lines.length - 1].length || 1
283 }
284
285 // strip newlines at the end
286 if (endsOnNewline) {
287 trimmed = lines.reverse().reduce((trimmed, line, i) => {
288 if (i === 0 && line === '') trimmed.trimming = true
289 if (!trimmed.trimming) trimmed.push(line)
290 if (trimmed.trimming && line !== '') {
291 trimmed.trimming = false
292 trimmed.push(line)
293 }
294 return trimmed
295 }, []).reverse()
296 pos.endLine = pos.endLine - (lines.length - trimmed.length)
297 pos.endColumn = trimmed[trimmed.length - 1].length || 1
298 }
299
300 const spaces = ' '.repeat(String(pos.endLine).length)
301 let snippet = trimmed.map((line, i) => {
302 return ` ${pos.startLine + i} ${line}`
303 }).join('\n')
304
305 if (trimmed.length === 1) {
306 snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
307 } else {
308 snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
309 snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
310 }
311
312 return Promise.resolve(
313 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
314 )
315 })
316}
317
318function bundle(options, cache) {
319 if (cache.bundle) return Promise.resolve(cache.bundle)
320
321 const stdout = []
322 const stderr = cache.bundle = []
323
324 const args = dargs(Object.assign({
325 _: [path.join(options.output, '*', '*.js')],
326 output: options.bundleOutput,
327 namespace: options.bundleNamespace,
328 }, options.pscBundleArgs))
329
330 cache.bundleModules.forEach(name => args.push('--module', name))
331
332 debug('spawning bundler %s %o', options.pscBundle, args.join(' '))
333
334 return (new Promise((resolve, reject) => {
335 console.log('Bundling PureScript...')
336
337 const compilation = spawn(options.pscBundle, args)
338
339 compilation.stdout.on('data', data => stdout.push(data.toString()))
340 compilation.stderr.on('data', data => stderr.push(data.toString()))
341 compilation.on('close', code => {
342 if (code !== 0) {
343 cache.errors.concat(stderr)
344 return reject(true)
345 }
346 cache.bundle = stderr
347 resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
348 })
349 }))
350}
351
352// map of PS module names to their source path
353function psModuleMap(globs, cache) {
354 if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap)
355
356 return globby(globs).then(paths => {
357 return Promise
358 .props(paths.reduce((map, file) => {
359 map[file] = fs.readFileAsync(file, 'utf8')
360 return map
361 }, {}))
362 .then(srcMap => {
363 cache.psModuleMap = Object.keys(srcMap).reduce((map, file) => {
364 const source = srcMap[file]
365 const psModuleName = match(psModuleRegex, source)
366 map[psModuleName] = path.resolve(file)
367 return map
368 }, {})
369 return cache.psModuleMap
370 })
371 })
372}
373
374function connectIdeServer(psModule) {
375 const options = psModule.options
376 const cache = psModule.cache
377
378 if (cache.ideServer) return Promise.resolve(psModule)
379
380 cache.ideServer = true
381
382 const connect = () => new Promise((resolve, reject) => {
383 const args = dargs(options.pscIdeArgs)
384
385 debug('attempting to connect to psc-ide-server', args)
386
387 const ideClient = spawn('psc-ide-client', args)
388
389 ideClient.stderr.on('data', data => {
390 debug(data.toString())
391 cache.ideServer = false
392 reject(true)
393 })
394 ideClient.stdout.once('data', data => {
395 debug(data.toString())
396 if (data.toString()[0] === '{') {
397 const res = JSON.parse(data.toString())
398 if (res.resultType === 'success') {
399 cache.ideServer = ideServer
400 resolve(psModule)
401 } else {
402 cache.ideServer = ideServer
403 reject(true)
404 }
405 } else {
406 cache.ideServer = false
407 reject(true)
408 }
409 })
410 ideClient.stdin.resume()
411 ideClient.stdin.write(JSON.stringify({ command: 'load' }))
412 ideClient.stdin.write('\n')
413 })
414
415 const args = dargs(Object.assign({
416 outputDirectory: options.output,
417 }, options.pscIdeArgs))
418
419 debug('attempting to start psc-ide-server', args)
420
421 const ideServer = cache.ideServer = spawn('psc-ide-server', [])
422 ideServer.stderr.on('data', data => {
423 debug(data.toString())
424 })
425
426 return retryPromise((retry, number) => {
427 return connect().catch(error => {
428 if (!cache.ideServer && number === 9) {
429 debug(error)
430
431 console.log(
432 'failed to connect to or start psc-ide-server, ' +
433 'full compilation will occur on rebuild'
434 )
435
436 return Promise.resolve(psModule)
437 }
438
439 return retry(error)
440 })
441 }, {
442 retries: 9,
443 factor: 1,
444 minTimeout: 333,
445 maxTimeout: 333,
446 })
447}
448
449function match(regex, str) {
450 const matches = str.match(regex)
451 return matches && matches[1]
452}
453
454function dargs(obj) {
455 return Object.keys(obj).reduce((args, key) => {
456 const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
457 const val = obj[key]
458
459 if (key === '_') val.forEach(v => args.push(v))
460 else if (Array.isArray(val)) val.forEach(v => args.push(arg, v))
461 else args.push(arg, obj[key])
462
463 return args
464 }, [])
465}
diff --git a/webpack.config.js b/webpack.config.js
deleted file mode 100644
index a39832f..0000000
--- a/webpack.config.js
+++ /dev/null
@@ -1,32 +0,0 @@
1'use strict';
2
3var path = require('path');
4
5var webpack = require('webpack');
6
7var packageJson = require('./package.json');
8
9var noErrorsPlugin = webpack.NoErrorsPlugin;
10
11var dedupePlugin = webpack.optimize.DedupePlugin;
12
13var config
14 = { cache: true
15 , target: 'node'
16 , entry: { index: './entry' }
17 , externals: Object.keys(packageJson.dependencies).reduce(function(b, a){
18 b[a] = 'commonjs ' + a;
19 return b;
20 }, {})
21 , output: { path: __dirname
22 , filename: '[name].js'
23 , libraryTarget: 'commonjs2'
24 }
25 , plugins: [ new noErrorsPlugin()
26 , new dedupePlugin()
27 ]
28 , resolve: { modulesDirectories: [ 'build' ] }
29 }
30 ;
31
32module.exports = config;