diff --git a/docs/npm-debug.log.624783864 b/docs/npm-debug.log.624783864 new file mode 100644 index 0000000..e69de29 diff --git a/docs/package.json b/docs/package.json index 05ed33a..f01b9ab 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "build": "node build.js", + "build-dev": "node build.js dev", "dev": "concurrently \"npm run serve\" \"npm run watch\"", "serve": "http-server . -c-1 -s", "setup": "node setup.js", diff --git a/docs/src/App.fs b/docs/src/App.fs index c785db5..da257d5 100644 --- a/docs/src/App.fs +++ b/docs/src/App.fs @@ -189,6 +189,7 @@ module Main = runM (NavigateTo (Sample SampleApi.Counter)) (pStaticStr "/sample/counter" |> (drop >> _end)) runM (NavigateTo (Sample SampleApi.HelloWorld)) (pStaticStr "/sample/hello-world" |> (drop >> _end)) runM (NavigateTo (Sample SampleApi.NestedCounter)) (pStaticStr "/sample/nested-counter" |> (drop >> _end)) + runM (NavigateTo (Sample SampleApi.Calculator)) (pStaticStr "/sample/calculator" |> (drop >> _end)) runM (NavigateTo About) (pStaticStr "/about" |> (drop >> _end)) ] diff --git a/docs/src/Common.fs b/docs/src/Common.fs index f41897e..4df266a 100644 --- a/docs/src/Common.fs +++ b/docs/src/Common.fs @@ -25,6 +25,7 @@ module Common = | Counter | HelloWorld | NestedCounter + | Calculator type Route = Index @@ -45,6 +46,7 @@ module Common = | SampleApi.Counter -> Some "/sample/counter" | SampleApi.HelloWorld -> Some "/sample/hello-world" | SampleApi.NestedCounter -> Some "/sample/nested-counter" + | SampleApi.Calculator -> Some "/sample/calculator" | About -> Some "/about" let voidLinkAction<'T> : Attribute<'T> = property "href" "javascript:void(0)" diff --git a/docs/src/Pages/Sample/Sample_Calculator.fs b/docs/src/Pages/Sample/Sample_Calculator.fs new file mode 100644 index 0000000..d2eafbf --- /dev/null +++ b/docs/src/Pages/Sample/Sample_Calculator.fs @@ -0,0 +1,389 @@ +namespace WebApp.Pages.Sample + +open Fable.Core +open Fable.Import + +open Fable.Arch +open Fable.Arch.Html + +open WebApp +open WebApp.Common + +module Calculator = + + /// [BeginBlock:Model] + // Input of user, the buttons that he/she can click + type Input = + | Const of int + | Plus + | Minus + | Times + | Div + | Clear + | Equals + // the model or state of the app + // this is a list of the buttons the user has clicked so far + type Model = + | InputStack of Input list + /// [EndBlock] + + /// [BeginBlock:Actions] + // The action is what gets dispatched/sent to the update function + // then the update function gets both the current model/state + // and the action dispatched and decides how the next state + // should be computed + type Actions = + | PushInput of Input + /// [EndBlock] + + let (|Operation|_|) = function + | Plus -> Some Plus + | Minus -> Some Minus + | Times -> Some Times + | Div -> Some Div + | _ -> None + + let concatInts x y = int (sprintf "%d%d" x y) + + let opString = function + | Plus -> "+" + | Minus -> "-" + | Times -> "*" + | Div -> "/" + | Equals -> "=" + | Clear -> "CE" + | _ -> "" + + let inputString = function + | Operation op -> opString op + | Const n -> string n + | _ -> "" + + let modelString (InputStack xs) = + xs + |> List.map inputString + |> String.concat "" + + let solve (InputStack [Const x; Operation op; Const y]) = + match op with + | Plus -> x + y + | Minus -> x - y + | Times -> x * y + | Div -> x / y + | _ -> failwith "Will not happen" + /// [BeginBlock:Update] + // The update function: contains the logic of how to compute the next state or model + // based on the current state and the action dispatched by the User + // update : Model -> Action -> Model + let update (InputStack xs) (PushInput input) = + if input = Clear then InputStack [], [] + else + match xs with + | [] -> + match input with + | Operation op -> InputStack [], [] + | Equals -> InputStack [], [] + | _ -> InputStack [input], [] + | [Const x] -> + match input with + | Const y -> InputStack [Const (concatInts x y)], [] + | Operation op -> InputStack [Const x; op], [] + | _ -> InputStack xs, [] + | [Const x; Operation op] -> + match input with + | Const y -> InputStack [Const x; op; Const y], [] // push Const y to stack + | Operation otherOp -> InputStack [Const x; otherOp], [] // replace op with otheOp + | _ -> InputStack xs, [] // do nothing + | [Const x; Operation op; Const y] -> + match input with + | Const y' -> InputStack [Const x; op; Const (concatInts y y')], [] + | Equals -> InputStack [Const (solve (InputStack xs))], [] + | Operation op -> + let result = solve (InputStack xs) + InputStack [Const result; op], [] + | _ -> InputStack xs, [] + | _ -> InputStack xs, [] + /// [EndBlock] + + + /// [BeginBlock:View] + let digitStyle = + Style [ + ("height", "50px") + ("width", "55px") + ("font-size","20px") + ("cursor","pointer") + ("padding", "15px") + ("padding-top", "5px") + ("margin","5px") + ("text-align","center") + ("line-height","40px") + ("background-color","lightgreen") + ("box-shadow", "0 0 3px black") + ] + + let opButtonStyle = + Style [ + ("height", "50px") + ("width", "55px") + ("font-size","20px") + ("padding", "15px") + ("padding-top", "5px") + ("text-align","center") + ("line-height","40px") + ("cursor","pointer") + ("margin","5px") + ("background-color","lightblue") + ("box-shadow", "0 0 3px black") + ] + + let demoView model = + let digit n = + div + [ + digitStyle + onMouseClick (fun _ -> PushInput (Const n)) + ] + [ text (string n) ] + + let opBtn input = + let content = + match input with + | Operation op -> opString op + | Equals -> "=" + | Clear -> "CE" + | _ -> "" + div + [ + opButtonStyle + onMouseClick (fun _ -> PushInput input) + ] + [ text content ] + + let row xs = tr [] [ for x in xs -> td [] [x]] + + + div + [ Style [ ("width","320px"); ("border", "2px black solid"); ("border-radius", "15px"); ("padding", "10px")]] + [ + h1 [ Style [("font-size","24px");("padding-left", "20px"); ("height", "30px")] ] [ text (modelString model) ] + br [] + table + [] + [ + row [digit 1; digit 2; digit 3; opBtn Plus] + row [digit 4; digit 5; digit 6; opBtn Minus] + row [digit 7; digit 8; digit 9; opBtn Times] + row [opBtn Clear; digit 0; opBtn Equals; opBtn Div] + ] + ] + /// [EndBlock] + + let docs = new DocGen.Documentation(__SOURCE_FILE__) + + let view model = VDom.Html.sampleView "Calculator" (demoView model) docs.Html + + (* + [BeginDocs] + This sample is a simple calculator written using fable-arch. Created by [Zaid-Ajaj](https://github.com/Zaid-Ajaj). + + ## Model + + The first thing we do is define a *model*. Sometimes we call this the *state of the app*. The state is what we would like to keep track of when the user interacts with the app. + + In our case, the `Input` type represents what the user can click on in the calculator and `Model` represents what buttons the user had clicked so far. + + Not every click will be added to the model. It's up to the `update` function to decide how to compute the next state of the app once the user clicked something. + + You might ask, *How does the `update` function know what the user clicked if it only operates on the model?* It is the `view` function the sends or dispatches the actions to the `update` function. The `update` function computes the next state and returns the result to the `view`. The `view` function gets called and the UI gets re-rendered. + + ```fsharp + type Input = + | Const of int + | Plus + | Minus + | Times + | Div + | Clear + | Equals + + type Model = InputStack of Input list + ``` + ## Actions + The action is what gets dispatched/sent to the update function, then the update function gets both the current model/state and the action dispatched and decides how the next state should be computed. + ```fsharp + type Actions = PushInput of Input + ``` + ### Helper functions + Before we dive in the update function that has all the logic of the app, let us first define some helper functions that operate on our model and input. These should be self-explainatory for an F# developer. + + ```fsharp + // lets you concat two ints, i.e. concatInts 11 22 -> 1122 + // concatInts : int -> int -> int + let concatInts x y = int (sprintf "%d%d" x y) + + // Active pattern that matches with an operation + Operation : Input -> Input option + let (|Operation|_|) = function + | Plus -> Some Plus + | Minus -> Some Minus + | Times -> Some Times + | Div -> Some Div + | _ -> None + + // when the model has the shape `[Const a; operation; Const b]`, we reduce that to `(operation) a b` + // solve : Model -> int + let solve (InputStack [Const x; Operation op; Const y]) = + match op with + | Plus -> x + y + | Minus -> x - y + | Times -> x * y + | Div -> x / y + | _ -> failwith "Will not happen" + ``` + + ## Update + The update function: contains the logic of how to compute the next state or model based on the current state and the action dispatched by the user. + ```fsharp + // update : Model -> Action -> Model + let update (InputStack xs) (PushInput input) = + if input = Clear then InputStack [] + else + match xs with + | [] -> // model is empty + match input with + | Operation op -> InputStack [] // user clicks an operation -> model stays empty + | Equals -> InputStack [] // user clicks = -> model stays empty + | _ -> InputStack [input] // otherwise, add whatever input was clicked to model + | [Const x] -> // model contains a number + match input with + | Const y -> InputStack [Const (concatInts x y)] // user clikced on digit -> concat the two + | Operation op -> InputStack [Const x; op] // user clicked an operation -> add it to model + | _ -> InputStack xs // otherwise -> return the model unchanged + | [Const x; Operation op] -> // the model contains a number and an operation + match input with + | Const y -> InputStack [Const x; op; Const y] // user clicked another digit -> push the digit to model + | Operation otherOp -> InputStack [Const x; otherOp] // user clicked another operation -> replace op with otheOp + | _ -> InputStack xs // otherwise -> return model unchanged + | [Const x; Operation op; Const y] -> // now model contains the shape we want to send to the "solve" function + match input with + | Const y' -> InputStack [Const x; op; Const (concatInts y y')] // clicked on digit -> concat it with Const y + | Equals -> InputStack [Const (solve (InputStack xs))] // calculate result, reset model and push result to model + | Operation op -> + let result = solve (InputStack xs) + InputStack [Const result; op] + | _ -> InputStack xs + | _ -> InputStack xs + ``` + ## View + + Now the view. The view depends on the current state and dispatches actions to the update function, there by getting a new state. At this point the view will rerender itself. + + ### Helper functions + ```fsharp + let opString = function + | Plus -> "+" + | Minus -> "-" + | Times -> "*" + | Div -> "/" + | Equals -> "=" + | Clear -> "CE" + | _ -> "" + + let inputString = function + | Operation op -> opString op + | Const n -> string n + | _ -> "" + + let modelString (InputStack xs) = + xs + |> List.map inputString + |> String.concat "" + ``` + ### Styles for the buttons + ```fsharp + let digitStyle = + Style [ + ("height", "40px") + ("width", "55px") + ("font-size","24px") + ("cursor","pointer") + ("padding", "15px") + ("margin","5px") + ("text-align","center") + ("vertical-align","middle") + ("line-height","40px") + ("background-color","lightgreen") + ("box-shadow", "0 0 3px black") + ] + + let opButtonStyle = + Style [ + ("height", "40px") + ("width", "55px") + ("font-size","24px") + ("padding", "15px") + ("text-align","center") + ("vertical-align","middle") + ("line-height","40px") + ("cursor","pointer") + ("margin","5px") + ("background-color","lightblue") + ("box-shadow", "0 0 3px black") + ] + ``` + + ## The `view` function + ```fsharp + let view model = + let digit n = + div + [ + digitStyle + onMouseClick (fun _ -> PushInput (Const n)) + ] + [ text (string n) ] + + let opBtn input = + let content = + match input with + | Operation op -> opString op + | Equals -> "=" + | Clear -> "CE" + | _ -> "" + div + [ + opButtonStyle + onMouseClick (fun _ -> PushInput input) + ] + [ text content ] + + // table row + let row xs = tr [] [ for x in xs -> td [] [x]] + + div + [ Style [ ("width", "407px"); ("border", "2px black solid"); ("border-radius", "15px"); ("padding", "10px")]] + [ + h2 [ Style [("padding-left", "20px"); ("height", "30px")] ] [ text (modelString model) ] + br [] + table + [] + [ + row [digit 1; digit 2; digit 3; opBtn Plus] + row [digit 4; digit 5; digit 6; opBtn Minus] + row [digit 7; digit 8; digit 9; opBtn Times] + row [opBtn Clear; digit 0; opBtn Equals; opBtn Div] + ] + ] + ``` + + # Create the app + ```fsharp + let initialState = InputStack [] + createSimpleApp initialState view update Virtualdom.createRender + |> withStartNodeSelector "#calc" + |> start + ``` + + [EndDocs] + *) \ No newline at end of file diff --git a/docs/src/Pages/Sample/Sample_Dispatcher.fs b/docs/src/Pages/Sample/Sample_Dispatcher.fs index 6057157..5dbceae 100644 --- a/docs/src/Pages/Sample/Sample_Dispatcher.fs +++ b/docs/src/Pages/Sample/Sample_Dispatcher.fs @@ -16,13 +16,15 @@ module Dispatcher = Counter: Counter.Model option HelloWorld: HelloWorld.Model option NestedCounter: NestedCounter.Model option + Calculator: Calculator.Model option } - static member Generate (?index, ?counter, ?helloWorld, ?nestedCounter) = + static member Generate (?index, ?counter, ?helloWorld, ?nestedCounter, ?calc) = { Clock = index Counter = counter HelloWorld = helloWorld NestedCounter = nestedCounter + Calculator = calc } static member Initial (currentPage: SampleApi.Route) = @@ -31,6 +33,7 @@ module Dispatcher = | SampleApi.Counter -> Model.Generate (counter = Counter.Model.Initial) | SampleApi.HelloWorld -> Model.Generate (helloWorld = "" ) | SampleApi.NestedCounter -> Model.Generate (nestedCounter = NestedCounter.Model.Initial) + | SampleApi.Calculator -> Model.Generate (calc = Calculator.Model.InputStack []) type NavbarLink = { Text: string @@ -48,6 +51,7 @@ module Dispatcher = | CounterActions of Counter.Actions | HelloWorldActions of HelloWorld.Actions | NestedCounterActions of NestedCounter.Actions + | CalcActions of Calculator.Actions let update model action = match action with @@ -67,6 +71,10 @@ module Dispatcher = let (res, action) = NestedCounter.update model.NestedCounter.Value act let action' = mapActions NestedCounterActions action { model with NestedCounter = Some res}, action' + | CalcActions acts -> + let (res, action) = Calculator.update model.Calculator.Value acts + let action' = mapActions CalcActions action + { model with Calculator = Some res }, action' | NavigateTo route -> let message = [ fun h -> @@ -109,6 +117,8 @@ module Dispatcher = Html.map HelloWorldActions (HelloWorld.view model.HelloWorld.Value) | SampleApi.NestedCounter -> Html.map NestedCounterActions (NestedCounter.view model.NestedCounter.Value) + | SampleApi.Calculator -> + Html.map CalcActions (Calculator.view model.Calculator.Value) div [] @@ -121,6 +131,7 @@ module Dispatcher = NavbarLink.Create("Counter", SampleApi.Counter) NavbarLink.Create("Nested counter", SampleApi.NestedCounter) NavbarLink.Create("Clock", SampleApi.Clock) + NavbarLink.Create("Calculator", SampleApi.Calculator) ] subRoute ] diff --git a/docs/src/WebApp.fsproj b/docs/src/WebApp.fsproj index 4fcd523..c7279e9 100644 --- a/docs/src/WebApp.fsproj +++ b/docs/src/WebApp.fsproj @@ -67,6 +67,7 @@ + diff --git a/samples/arch-calculator/LICENSE b/samples/arch-calculator/LICENSE new file mode 100644 index 0000000..4c2e2bd --- /dev/null +++ b/samples/arch-calculator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Zaid Ajaj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/samples/arch-calculator/README.md b/samples/arch-calculator/README.md new file mode 100644 index 0000000..003c21c --- /dev/null +++ b/samples/arch-calculator/README.md @@ -0,0 +1,10 @@ +# Arch-Calculator [Demo!](https://zaidajaj.neocities.org/html/arch-calc/index.html) +F# stack-based calculator written with [Fable](https://github.com/fable-compiler/Fable) and [fable-arch](https://github.com/fable-compiler/fable-arch) + + +### Install +```shell +git clone https://github.com/Zaid-Ajaj/Arch-Calculator.git +cd arch-calculator +fable +``` diff --git a/samples/arch-calculator/calc.fsx b/samples/arch-calculator/calc.fsx new file mode 100644 index 0000000..370ccbf --- /dev/null +++ b/samples/arch-calculator/calc.fsx @@ -0,0 +1,177 @@ +#r "node_modules/fable-core/Fable.Core.dll" +#load "node_modules/fable-arch/Fable.Arch.Html.fs" +#load "node_modules/fable-arch/Fable.Arch.App.fs" +#load "node_modules/fable-arch/Fable.Arch.Virtualdom.fs" + +open Fable.Core +open Fable.Core.JsInterop + +open Fable.Arch +open Fable.Arch.App +open Fable.Arch.Html + + +type Input = + | Const of int + | Plus + | Minus + | Times + | Div + | Clear + | Equals + +type Model = + | InputStack of Input list + +type Actions = + | PushInput of Input + +let (|Operation|_|) = function + | Plus -> Some Plus + | Minus -> Some Minus + | Times -> Some Times + | Div -> Some Div + | _ -> None + +let concatInts x y = int (sprintf "%d%d" x y) + +let opString = function + | Plus -> "+" + | Minus -> "-" + | Times -> "*" + | Div -> "/" + | Equals -> "=" + | Clear -> "CE" + | _ -> "" + +let inputString = function + | Operation op -> opString op + | Const n -> string n + | _ -> "" + +let modelString (InputStack xs) = + xs + |> List.map inputString + |> String.concat "" + +let solve (InputStack [Const x; Operation op; Const y]) = + match op with + | Plus -> x + y + | Minus -> x - y + | Times -> x * y + | Div -> x / y + | _ -> failwith "Will not happen" + +// Update +let update (InputStack xs) (PushInput input) = + if input = Clear then InputStack [] + else + match xs with + | [] -> + match input with + | Operation op -> InputStack [] + | Equals -> InputStack [] + | _ -> InputStack [input] + | [Const x] -> + match input with + | Const y -> InputStack [Const (concatInts x y)] + | Operation op -> InputStack [Const x; op] + | _ -> InputStack xs + | [Const x; Operation op] -> + match input with + | Const y -> InputStack [Const x; op; Const y] // push Const y to stack + | Operation otherOp -> InputStack [Const x; otherOp] // replace op with otheOp + | _ -> InputStack xs // do nothing + | [Const x; Operation op; Const y] -> + match input with + | Const y' -> InputStack [Const x; op; Const (concatInts y y')] + | Equals -> InputStack [Const (solve (InputStack xs))] + | Operation op -> + let result = solve (InputStack xs) + InputStack [Const result; op] + | _ -> InputStack xs + | _ -> InputStack xs + + + +let digitStyle = + Style [ + ("height", "40px") + ("width", "55px") + ("font-size","24px") + ("cursor","pointer") + ("padding", "15px") + ("margin","5px") + ("text-align","center") + ("vertical-align","middle") + ("line-height","40px") + ("background-color","lightgreen") + ("box-shadow", "0 0 3px black") + ] + +let opButtonStyle = + Style [ + ("height", "40px") + ("width", "55px") + ("font-size","24px") + ("padding", "15px") + ("text-align","center") + ("vertical-align","middle") + ("line-height","40px") + ("cursor","pointer") + ("margin","5px") + ("background-color","lightblue") + ("box-shadow", "0 0 3px black") + ] + +let view model = + let digit n = + div + [ + digitStyle + onMouseClick (fun _ -> PushInput (Const n)) + ] + [ text (string n) ] + + let opBtn input = + let content = + match input with + | Operation op -> opString op + | Equals -> "=" + | Clear -> "CE" + | _ -> "" + div + [ + opButtonStyle + onMouseClick (fun _ -> PushInput input) + ] + [ text content ] + + let row xs = + tr [] + [ + for x in xs -> + td [] + [x] + ] + + div + [ Style [ ("width", "407px"); ("border", "2px black solid"); ("border-radius", "15px"); ("padding", "10px")]] + [ + h2 [] [text "Arch Calculator"] + h2 [ Style [("padding-left", "20px"); ("height", "30px")] ] [ text (modelString model) ] + br [] + table + [] + [ + row [digit 1; digit 2; digit 3; opBtn Plus] + row [digit 4; digit 5; digit 6; opBtn Minus] + row [digit 7; digit 8; digit 9; opBtn Times] + row [opBtn Clear; digit 0; opBtn Equals; opBtn Div] + ] + ] + + +createSimpleApp (InputStack []) view update Virtualdom.createRender +|> withStartNodeSelector "#calc" +|> start \ No newline at end of file diff --git a/samples/arch-calculator/fableconfig.json b/samples/arch-calculator/fableconfig.json new file mode 100644 index 0000000..8b04b88 --- /dev/null +++ b/samples/arch-calculator/fableconfig.json @@ -0,0 +1,17 @@ +{ + "module": "amd", + "sourceMaps": true, + "projFile": "./calc.fsx", + "outDir": "out", + "scripts": { + "prebuild": "npm install", + "postbuild": "node node_modules/webpack/bin/webpack" + }, + "targets": { + "watch": { + "scripts": { + "postbuild": "node node_modules/webpack/bin/webpack --watch" + } + } + } +} diff --git a/samples/arch-calculator/index.html b/samples/arch-calculator/index.html new file mode 100644 index 0000000..ca778d5 --- /dev/null +++ b/samples/arch-calculator/index.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + + + diff --git a/samples/arch-calculator/package.json b/samples/arch-calculator/package.json new file mode 100644 index 0000000..b981eda --- /dev/null +++ b/samples/arch-calculator/package.json @@ -0,0 +1,25 @@ +{ + "name": "arch-calculator", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "fable", + "watch": "fable -w --target watch" + }, + "dependencies": { + "core-js": "^2.4.0", + "fable-core": "^0.6.3", + "virtual-dom": "^2.1.1", + "todomvc-app-css": "^2.0.0", + "todomvc-common": "^1.0.1" + }, + "devDependencies": { + "fable-compiler": "^0.7.34", + "fable-arch": "^0.9.11", + "source-map-loader": "^0.1.5", + "webpack": "^1.13.1" + }, + "engines": { + "fable": "^0.6.3" + } +} \ No newline at end of file diff --git a/samples/arch-calculator/webpack.config.js b/samples/arch-calculator/webpack.config.js new file mode 100644 index 0000000..3e258b5 --- /dev/null +++ b/samples/arch-calculator/webpack.config.js @@ -0,0 +1,22 @@ +var path = require("path"); +var webpack = require("webpack"); + +var cfg = { + devtool: "source-map", + entry: "./out/calc.js", + output: { + path: path.join(__dirname, "public"), + filename: "bundle.js" + }, + module: { + preLoaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: "source-map-loader" + } + ] + } +}; + +module.exports = cfg; \ No newline at end of file