From 7de41f10b4ff0f0d6b45d59bee0f166c3cfe3f9f Mon Sep 17 00:00:00 2001 From: Alex Mingoia Date: Tue, 10 May 2016 00:09:28 -0700 Subject: [PATCH] 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. --- .babelrc | 1 + .gitignore | 10 +- LICENSE | 2 +- README.md | 52 +++- bower.json | 10 - docs/PursLoader/Debug.md | 9 - docs/PursLoader/JsStringEscape.md | 9 - docs/PursLoader/Loader.md | 27 -- docs/PursLoader/LoaderRef.md | 51 ---- docs/PursLoader/LoaderUtil.md | 9 - docs/PursLoader/Options.md | 21 -- docs/PursLoader/Path.md | 27 -- docs/PursLoader/Plugin.md | 33 -- entry.js | 11 - package.json | 59 ++-- src/PursLoader/Debug.js | 12 - src/PursLoader/Debug.purs | 9 - src/PursLoader/JsStringEscape.js | 7 - src/PursLoader/JsStringEscape.purs | 3 - src/PursLoader/Loader.purs | 108 ------- src/PursLoader/LoaderRef.js | 50 ---- src/PursLoader/LoaderRef.purs | 40 --- src/PursLoader/Path.js | 24 -- src/PursLoader/Path.purs | 14 - src/PursLoader/Plugin.js | 14 - src/PursLoader/Plugin.purs | 34 --- src/index.js | 465 +++++++++++++++++++++++++++++ webpack.config.js | 32 -- 28 files changed, 558 insertions(+), 585 deletions(-) create mode 100644 .babelrc delete mode 100644 bower.json delete mode 100644 docs/PursLoader/Debug.md delete mode 100644 docs/PursLoader/JsStringEscape.md delete mode 100644 docs/PursLoader/Loader.md delete mode 100644 docs/PursLoader/LoaderRef.md delete mode 100644 docs/PursLoader/LoaderUtil.md delete mode 100644 docs/PursLoader/Options.md delete mode 100644 docs/PursLoader/Path.md delete mode 100644 docs/PursLoader/Plugin.md delete mode 100644 entry.js delete mode 100644 src/PursLoader/Debug.js delete mode 100644 src/PursLoader/Debug.purs delete mode 100644 src/PursLoader/JsStringEscape.js delete mode 100644 src/PursLoader/JsStringEscape.purs delete mode 100644 src/PursLoader/Loader.purs delete mode 100644 src/PursLoader/LoaderRef.js delete mode 100644 src/PursLoader/LoaderRef.purs delete mode 100644 src/PursLoader/Path.js delete mode 100644 src/PursLoader/Path.purs delete mode 100644 src/PursLoader/Plugin.js delete mode 100644 src/PursLoader/Plugin.purs create mode 100644 src/index.js delete mode 100644 webpack.config.js 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 @@ -.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 05b0016..6fe3810 100644 --- 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 diff --git a/README.md b/README.md index df92e69..ed25296 100644 --- 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 index 761c24c..0000000 --- a/bower.json +++ /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 index 824a9f8..0000000 --- a/docs/PursLoader/Debug.md +++ /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 index 09f52aa..0000000 --- a/docs/PursLoader/JsStringEscape.md +++ /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 index d05e3b7..0000000 --- a/docs/PursLoader/Loader.md +++ /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 index 917db3a..0000000 --- a/docs/PursLoader/LoaderRef.md +++ /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 index 36d6879..0000000 --- a/docs/PursLoader/LoaderUtil.md +++ /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 index b3352fc..0000000 --- a/docs/PursLoader/Options.md +++ /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 index cc00436..0000000 --- a/docs/PursLoader/Path.md +++ /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 index 7a524da..0000000 --- a/docs/PursLoader/Plugin.md +++ /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 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; diff --git a/package.json b/package.json index 6ab55ec..4cbdd72 100644 --- a/package.json +++ b/package.json @@ -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 index 85eca10..0000000 --- a/src/PursLoader/Debug.js +++ /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 index 7a02f69..0000000 --- a/src/PursLoader/Debug.purs +++ /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 index ff0a1a6..0000000 --- a/src/PursLoader/JsStringEscape.js +++ /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 index 79590ae..0000000 --- a/src/PursLoader/JsStringEscape.purs +++ /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 index acb0993..0000000 --- a/src/PursLoader/Loader.purs +++ /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 index a5d8e1f..0000000 --- a/src/PursLoader/LoaderRef.js +++ /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 index 140d94a..0000000 --- a/src/PursLoader/LoaderRef.purs +++ /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 index 878f256..0000000 --- a/src/PursLoader/Path.js +++ /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 index 98cad5a..0000000 --- a/src/PursLoader/Path.purs +++ /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 index ded6df5..0000000 --- a/src/PursLoader/Plugin.js +++ /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 index c798c83..0000000 --- a/src/PursLoader/Plugin.purs +++ /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 index 0000000..0a25ccd --- /dev/null +++ b/src/index.js @@ -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 index a39832f..0000000 --- a/webpack.config.js +++ /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; -- 2.41.0