git.iko.soy/lynxjs-elm/compiler
on master help sign in
Improve developer experience: init scaffolding, build feedback, QR codes, verbose mode, uninstall command, and new examples
commit51621f2d347586698ceea9d887fb8440aac33f91 authoriko <mail@iko.soy> dateFri 20 Mar 2026, 03:18 (6 months ago) parente576dea
12 files changed+1269−76
examples/scroll-view/elm.json+24 examples/scroll-view/src/Main.elm+205 examples/timer/elm.json+24 examples/timer/src/Main.elm+221 examples/todo-list/elm.json+24 examples/todo-list/src/Main.elm+229 lynxjs-elm.cabal+1 terminal/src/Dev.hs+142−29 terminal/src/Init.hs+129−11 terminal/src/Main.hs+41−17 terminal/src/Make.hs+75−19 terminal/src/Uninstall.hs+154
examples/scroll-view/elm.jsonnew file+24
@@ -0,0 +1,24 @@
1+{
2+ "type": "application",
3+ "source-directories": [
4+ "src"
5+ ],
6+ "elm-version": "0.19.1",
7+ "dependencies": {
8+ "direct": {
9+ "elm/core": "1.0.5",
10+ "elm/json": "1.1.3",
11+ "lynxjs-elm/browser": "1.0.0",
12+ "lynxjs-elm/ui": "1.0.0",
13+ "lynxjs-elm/virtual-dom": "1.0.0"
14+ },
15+ "indirect": {
16+ "elm/time": "1.0.0",
17+ "elm/url": "1.0.0"
18+ }
19+ },
20+ "test-dependencies": {
21+ "direct": {},
22+ "indirect": {}
23+ }
24+}
examples/scroll-view/src/Main.elmnew file+205
@@ -0,0 +1,205 @@
1+module Main exposing (main)
2+
3+{-| Scroll view with a long list of styled items.
4+Demonstrates scrollView, list rendering, and layout patterns.
5+-}
6+
7+import Browser
8+import Lynx
9+import Lynx.Attributes as Attr
10+import Lynx.Events exposing (onTap)
11+import VirtualDom exposing (Node)
12+
13+
14+
15+-- MAIN
16+
17+
18+main : Program () Model Msg
19+main =
20+ Browser.sandbox
21+ { init = init
22+ , view = view
23+ , update = update
24+ }
25+
26+
27+
28+-- MODEL
29+
30+
31+type alias Model =
32+ { selected : Maybe Int
33+ }
34+
35+
36+init : Model
37+init =
38+ { selected = Nothing
39+ }
40+
41+
42+colors : List String
43+colors =
44+ [ "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7"
45+ , "#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9"
46+ , "#F0B27A", "#82E0AA", "#F1948A", "#85929E", "#D7BDE2"
47+ , "#A3E4D7", "#FAD7A0", "#AED6F1", "#D5DBDB", "#EDBB99"
48+ ]
49+
50+
51+
52+-- UPDATE
53+
54+
55+type Msg
56+ = Select Int
57+
58+
59+update : Msg -> Model -> Model
60+update msg model =
61+ case msg of
62+ Select idx ->
63+ { model
64+ | selected =
65+ if model.selected == Just idx then
66+ Nothing
67+ else
68+ Just idx
69+ }
70+
71+
72+
73+-- VIEW
74+
75+
76+view : Model -> Node Msg
77+view model =
78+ Lynx.view
79+ [ Attr.flexDirection "column"
80+ , Attr.flex 1
81+ ]
82+ [ -- Header
83+ Lynx.view
84+ [ Attr.backgroundColor "#2C3E50"
85+ , Attr.paddingTop 16
86+ , Attr.paddingBottom 16
87+ , Attr.paddingLeft 20
88+ , Attr.paddingRight 20
89+ ]
90+ [ Lynx.text
91+ [ Attr.fontSize 22
92+ , Attr.fontWeight "bold"
93+ , Attr.color "#ffffff"
94+ ]
95+ [ Lynx.textContent "Color Palette" ]
96+ , Lynx.text
97+ [ Attr.fontSize 14
98+ , Attr.color "#95A5A6"
99+ , Attr.marginTop 4
100+ ]
101+ [ Lynx.textContent "Tap a color to select it" ]
102+ ]
103+
104+ -- Scrollable content
105+ , Lynx.scrollView
106+ [ Attr.flex 1
107+ , Attr.scrollDirection "vertical"
108+ , Attr.padding 12
109+ ]
110+ (List.indexedMap (viewCard model.selected) colors)
111+
112+ -- Footer showing selection
113+ , Lynx.view
114+ [ Attr.backgroundColor "#34495E"
115+ , Attr.paddingTop 12
116+ , Attr.paddingBottom 12
117+ , Attr.paddingLeft 20
118+ , Attr.paddingRight 20
119+ , Attr.flexDirection "row"
120+ , Attr.alignItems "center"
121+ ]
122+ [ case model.selected of
123+ Nothing ->
124+ Lynx.text [ Attr.color "#95A5A6", Attr.fontSize 14 ]
125+ [ Lynx.textContent "No color selected" ]
126+
127+ Just idx ->
128+ let
129+ colorHex =
130+ List.drop idx colors
131+ |> List.head
132+ |> Maybe.withDefault "#000000"
133+ in
134+ Lynx.view
135+ [ Attr.flexDirection "row"
136+ , Attr.alignItems "center"
137+ ]
138+ [ Lynx.view
139+ [ Attr.width 20
140+ , Attr.height 20
141+ , Attr.borderRadius 10
142+ , Attr.backgroundColor colorHex
143+ , Attr.marginRight 8
144+ ]
145+ []
146+ , Lynx.text [ Attr.color "#ffffff", Attr.fontSize 14 ]
147+ [ Lynx.textContent ("Selected: " ++ colorHex) ]
148+ ]
149+ ]
150+ ]
151+
152+
153+viewCard : Maybe Int -> Int -> String -> Node Msg
154+viewCard selected idx colorHex =
155+ let
156+ isSelected =
157+ selected == Just idx
158+ in
159+ Lynx.view
160+ [ onTap (Select idx)
161+ , Attr.flexDirection "row"
162+ , Attr.alignItems "center"
163+ , Attr.marginBottom 8
164+ , Attr.borderRadius 12
165+ , Attr.backgroundColor "#ffffff"
166+ , Attr.borderWidth (if isSelected then 2 else 0)
167+ , Attr.borderColor colorHex
168+ , Attr.paddingTop 14
169+ , Attr.paddingBottom 14
170+ , Attr.paddingLeft 16
171+ , Attr.paddingRight 16
172+ ]
173+ [ -- Color swatch
174+ Lynx.view
175+ [ Attr.width 48
176+ , Attr.height 48
177+ , Attr.borderRadius 12
178+ , Attr.backgroundColor colorHex
179+ , Attr.marginRight 16
180+ ]
181+ []
182+
183+ -- Info
184+ , Lynx.view [ Attr.flex 1 ]
185+ [ Lynx.text
186+ [ Attr.fontSize 16
187+ , Attr.fontWeight "bold"
188+ , Attr.color "#2C3E50"
189+ ]
190+ [ Lynx.textContent ("Color #" ++ String.fromInt (idx + 1)) ]
191+ , Lynx.text
192+ [ Attr.fontSize 13
193+ , Attr.color "#7F8C8D"
194+ , Attr.marginTop 2
195+ ]
196+ [ Lynx.textContent colorHex ]
197+ ]
198+
199+ -- Selection indicator
200+ , if isSelected then
201+ Lynx.text [ Attr.fontSize 20, Attr.color colorHex ]
202+ [ Lynx.textContent "●" ]
203+ else
204+ Lynx.text [] []
205+ ]
examples/timer/elm.jsonnew file+24
@@ -0,0 +1,24 @@
1+{
2+ "type": "application",
3+ "source-directories": [
4+ "src"
5+ ],
6+ "elm-version": "0.19.1",
7+ "dependencies": {
8+ "direct": {
9+ "elm/core": "1.0.5",
10+ "elm/json": "1.1.3",
11+ "elm/time": "1.0.0",
12+ "lynxjs-elm/browser": "1.0.0",
13+ "lynxjs-elm/ui": "1.0.0",
14+ "lynxjs-elm/virtual-dom": "1.0.0"
15+ },
16+ "indirect": {
17+ "elm/url": "1.0.0"
18+ }
19+ },
20+ "test-dependencies": {
21+ "direct": {},
22+ "indirect": {}
23+ }
24+}
examples/timer/src/Main.elmnew file+221
@@ -0,0 +1,221 @@
1+module Main exposing (main)
2+
3+{-| A timer app demonstrating subscriptions with Time.every.
4+-}
5+
6+import Browser
7+import Lynx
8+import Lynx.Attributes as Attr
9+import Lynx.Events exposing (onTap)
10+import Time
11+import VirtualDom exposing (Node)
12+
13+
14+
15+-- MAIN
16+
17+
18+main : Program () Model Msg
19+main =
20+ Browser.element
21+ { init = init
22+ , view = view
23+ , update = update
24+ , subscriptions = subscriptions
25+ }
26+
27+
28+
29+-- MODEL
30+
31+
32+type alias Model =
33+ { elapsed : Int -- tenths of a second
34+ , running : Bool
35+ , laps : List Int
36+ }
37+
38+
39+init : () -> ( Model, Cmd Msg )
40+init _ =
41+ ( { elapsed = 0
42+ , running = False
43+ , laps = []
44+ }
45+ , Cmd.none
46+ )
47+
48+
49+
50+-- UPDATE
51+
52+
53+type Msg
54+ = Tick Time.Posix
55+ | ToggleRunning
56+ | RecordLap
57+ | Reset
58+
59+
60+update : Msg -> Model -> ( Model, Cmd Msg )
61+update msg model =
62+ case msg of
63+ Tick _ ->
64+ ( { model | elapsed = model.elapsed + 1 }
65+ , Cmd.none
66+ )
67+
68+ ToggleRunning ->
69+ ( { model | running = not model.running }
70+ , Cmd.none
71+ )
72+
73+ RecordLap ->
74+ ( { model | laps = model.elapsed :: model.laps }
75+ , Cmd.none
76+ )
77+
78+ Reset ->
79+ ( { model | elapsed = 0, running = False, laps = [] }
80+ , Cmd.none
81+ )
82+
83+
84+
85+-- SUBSCRIPTIONS
86+
87+
88+subscriptions : Model -> Sub Msg
89+subscriptions model =
90+ if model.running then
91+ Time.every 100 Tick
92+ else
93+ Sub.none
94+
95+
96+
97+-- VIEW
98+
99+
100+view : Model -> Node Msg
101+view model =
102+ Lynx.view
103+ [ Attr.flexDirection "column"
104+ , Attr.alignItems "center"
105+ , Attr.flex 1
106+ , Attr.backgroundColor "#1a1a2e"
107+ , Attr.paddingTop 40
108+ ]
109+ [ -- Timer display
110+ Lynx.text
111+ [ Attr.fontSize 64
112+ , Attr.fontWeight "bold"
113+ , Attr.color "#e94560"
114+ ]
115+ [ Lynx.textContent (formatTime model.elapsed) ]
116+
117+ -- Controls
118+ , Lynx.view
119+ [ Attr.flexDirection "row"
120+ , Attr.marginTop 30
121+ ]
122+ [ -- Start/Stop
123+ timerButton
124+ (if model.running then "#e94560" else "#16c79a")
125+ ToggleRunning
126+ (if model.running then "Stop" else "Start")
127+
128+ -- Lap (only when running)
129+ , if model.running then
130+ timerButton "#0f3460" RecordLap "Lap"
131+ else
132+ Lynx.view [] []
133+
134+ -- Reset (only when stopped and has time)
135+ , if not model.running && model.elapsed > 0 then
136+ timerButton "#533483" Reset "Reset"
137+ else
138+ Lynx.view [] []
139+ ]
140+
141+ -- Laps
142+ , if List.isEmpty model.laps then
143+ Lynx.view [] []
144+ else
145+ Lynx.view
146+ [ Attr.marginTop 30
147+ , Attr.flexDirection "column"
148+ , Attr.alignItems "center"
149+ ]
150+ [ Lynx.text
151+ [ Attr.fontSize 18
152+ , Attr.color "#e94560"
153+ , Attr.marginBottom 12
154+ ]
155+ [ Lynx.textContent "Laps" ]
156+ , Lynx.view [ Attr.flexDirection "column" ]
157+ (List.indexedMap viewLap (List.reverse model.laps))
158+ ]
159+ ]
160+
161+
162+timerButton : String -> Msg -> String -> Node Msg
163+timerButton bgColor msg label =
164+ Lynx.view
165+ [ onTap msg
166+ , Attr.backgroundColor bgColor
167+ , Attr.borderRadius 30
168+ , Attr.paddingTop 14
169+ , Attr.paddingBottom 14
170+ , Attr.paddingLeft 28
171+ , Attr.paddingRight 28
172+ , Attr.marginLeft 8
173+ , Attr.marginRight 8
174+ ]
175+ [ Lynx.text
176+ [ Attr.color "#ffffff"
177+ , Attr.fontSize 18
178+ , Attr.fontWeight "bold"
179+ ]
180+ [ Lynx.textContent label ]
181+ ]
182+
183+
184+viewLap : Int -> Int -> Node Msg
185+viewLap index tenths =
186+ Lynx.view
187+ [ Attr.flexDirection "row"
188+ , Attr.paddingTop 8
189+ , Attr.paddingBottom 8
190+ ]
191+ [ Lynx.text
192+ [ Attr.fontSize 16
193+ , Attr.color "#95A5A6"
194+ , Attr.marginRight 16
195+ ]
196+ [ Lynx.textContent ("Lap " ++ String.fromInt (index + 1)) ]
197+ , Lynx.text
198+ [ Attr.fontSize 16
199+ , Attr.color "#ffffff"
200+ ]
201+ [ Lynx.textContent (formatTime tenths) ]
202+ ]
203+
204+
205+formatTime : Int -> String
206+formatTime tenths =
207+ let
208+ totalSeconds = tenths // 10
209+ minutes = totalSeconds // 60
210+ seconds = modBy 60 totalSeconds
211+ deciseconds = modBy 10 tenths
212+ in
213+ padZero minutes ++ ":" ++ padZero seconds ++ "." ++ String.fromInt deciseconds
214+
215+
216+padZero : Int -> String
217+padZero n =
218+ if n < 10 then
219+ "0" ++ String.fromInt n
220+ else
221+ String.fromInt n
examples/todo-list/elm.jsonnew file+24
@@ -0,0 +1,24 @@
1+{
2+ "type": "application",
3+ "source-directories": [
4+ "src"
5+ ],
6+ "elm-version": "0.19.1",
7+ "dependencies": {
8+ "direct": {
9+ "elm/core": "1.0.5",
10+ "elm/json": "1.1.3",
11+ "lynxjs-elm/browser": "1.0.0",
12+ "lynxjs-elm/ui": "1.0.0",
13+ "lynxjs-elm/virtual-dom": "1.0.0"
14+ },
15+ "indirect": {
16+ "elm/time": "1.0.0",
17+ "elm/url": "1.0.0"
18+ }
19+ },
20+ "test-dependencies": {
21+ "direct": {},
22+ "indirect": {}
23+ }
24+}
examples/todo-list/src/Main.elmnew file+229
@@ -0,0 +1,229 @@
1+module Main exposing (main)
2+
3+{-| A simple todo list showing list rendering and text input.
4+-}
5+
6+import Browser
7+import Lynx
8+import Lynx.Attributes as Attr
9+import Lynx.Events exposing (onTap, onInput)
10+import VirtualDom exposing (Node)
11+
12+
13+
14+-- MAIN
15+
16+
17+main : Program () Model Msg
18+main =
19+ Browser.sandbox
20+ { init = init
21+ , view = view
22+ , update = update
23+ }
24+
25+
26+
27+-- MODEL
28+
29+
30+type alias Model =
31+ { items : List TodoItem
32+ , nextId : Int
33+ , draft : String
34+ }
35+
36+
37+type alias TodoItem =
38+ { id : Int
39+ , text : String
40+ , done : Bool
41+ }
42+
43+
44+init : Model
45+init =
46+ { items =
47+ [ { id = 1, text = "Learn Elm", done = True }
48+ , { id = 2, text = "Build a LynxJS app", done = False }
49+ , { id = 3, text = "Ship to production", done = False }
50+ ]
51+ , nextId = 4
52+ , draft = ""
53+ }
54+
55+
56+
57+-- UPDATE
58+
59+
60+type Msg
61+ = Toggle Int
62+ | Remove Int
63+ | UpdateDraft String
64+ | AddItem
65+
66+
67+update : Msg -> Model -> Model
68+update msg model =
69+ case msg of
70+ Toggle targetId ->
71+ { model
72+ | items =
73+ List.map
74+ (\item ->
75+ if item.id == targetId then
76+ { item | done = not item.done }
77+ else
78+ item
79+ )
80+ model.items
81+ }
82+
83+ Remove targetId ->
84+ { model | items = List.filter (\item -> item.id /= targetId) model.items }
85+
86+ UpdateDraft text ->
87+ { model | draft = text }
88+
89+ AddItem ->
90+ if String.isEmpty (String.trim model.draft) then
91+ model
92+ else
93+ { model
94+ | items = model.items ++ [ { id = model.nextId, text = model.draft, done = False } ]
95+ , nextId = model.nextId + 1
96+ , draft = ""
97+ }
98+
99+
100+
101+-- VIEW
102+
103+
104+view : Model -> Node Msg
105+view model =
106+ Lynx.view
107+ [ Attr.flexDirection "column"
108+ , Attr.padding 20
109+ , Attr.flex 1
110+ ]
111+ [ -- Header
112+ Lynx.text
113+ [ Attr.fontSize 28
114+ , Attr.fontWeight "bold"
115+ , Attr.marginBottom 16
116+ ]
117+ [ Lynx.textContent "Todo List" ]
118+
119+ -- Input row
120+ , Lynx.view
121+ [ Attr.flexDirection "row"
122+ , Attr.marginBottom 16
123+ , Attr.alignItems "center"
124+ ]
125+ [ Lynx.input
126+ [ onInput UpdateDraft
127+ , Attr.value model.draft
128+ , Attr.placeholder "What needs to be done?"
129+ , Attr.flex 1
130+ , Attr.height 44
131+ , Attr.fontSize 16
132+ , Attr.borderWidth 1
133+ , Attr.borderColor "#cccccc"
134+ , Attr.borderRadius 8
135+ , Attr.paddingLeft 12
136+ ]
137+ []
138+ , Lynx.view
139+ [ onTap AddItem
140+ , Attr.backgroundColor "#4a90d9"
141+ , Attr.borderRadius 8
142+ , Attr.paddingTop 10
143+ , Attr.paddingBottom 10
144+ , Attr.paddingLeft 16
145+ , Attr.paddingRight 16
146+ , Attr.marginLeft 8
147+ ]
148+ [ Lynx.text [ Attr.color "#ffffff", Attr.fontSize 18 ]
149+ [ Lynx.textContent "Add" ]
150+ ]
151+ ]
152+
153+ -- Summary
154+ , Lynx.text
155+ [ Attr.fontSize 14
156+ , Attr.color "#888888"
157+ , Attr.marginBottom 12
158+ ]
159+ [ Lynx.textContent (summaryText model.items) ]
160+
161+ -- Items
162+ , Lynx.scrollView
163+ [ Attr.flex 1
164+ , Attr.scrollDirection "vertical"
165+ ]
166+ (List.map viewItem model.items)
167+ ]
168+
169+
170+summaryText : List TodoItem -> String
171+summaryText items =
172+ let
173+ total = List.length items
174+ done = List.length (List.filter .done items)
175+ in
176+ String.fromInt done ++ "/" ++ String.fromInt total ++ " completed"
177+
178+
179+viewItem : TodoItem -> Node Msg
180+viewItem item =
181+ Lynx.view
182+ [ Attr.flexDirection "row"
183+ , Attr.alignItems "center"
184+ , Attr.paddingTop 12
185+ , Attr.paddingBottom 12
186+ , Attr.borderWidth 1
187+ , Attr.borderColor "#eeeeee"
188+ ]
189+ [ -- Checkbox area
190+ Lynx.view
191+ [ onTap (Toggle item.id)
192+ , Attr.width 28
193+ , Attr.height 28
194+ , Attr.borderRadius 14
195+ , Attr.borderWidth 2
196+ , Attr.borderColor
197+ (if item.done then "#4a90d9" else "#cccccc")
198+ , Attr.backgroundColor
199+ (if item.done then "#4a90d9" else "#ffffff")
200+ , Attr.alignItems "center"
201+ , Attr.justifyContent "center"
202+ , Attr.marginRight 12
203+ ]
204+ [ if item.done then
205+ Lynx.text [ Attr.color "#ffffff", Attr.fontSize 14 ]
206+ [ Lynx.textContent "✓" ]
207+ else
208+ Lynx.text [] []
209+ ]
210+
211+ -- Text
212+ , Lynx.text
213+ [ Attr.fontSize 16
214+ , Attr.flex 1
215+ , Attr.color
216+ (if item.done then "#aaaaaa" else "#333333")
217+ ]
218+ [ Lynx.textContent item.text ]
219+
220+ -- Remove button
221+ , Lynx.view
222+ [ onTap (Remove item.id)
223+ , Attr.paddingLeft 8
224+ , Attr.paddingRight 8
225+ ]
226+ [ Lynx.text [ Attr.fontSize 18, Attr.color "#cc0000" ]
227+ [ Lynx.textContent "✕" ]
228+ ]
229+ ]
lynxjs-elm.cabal+1
@@ -61,6 +61,7 @@ Executable lynxjs-elm
6161 Make
6262 Publish
6363 Repl
64+ Uninstall
6465
6566 -- terminal args
6667 Terminal
terminal/src/Dev.hs+142−29
@@ -1,4 +1,4 @@
1−{-# LANGUAGE OverloadedStrings #-}
1+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
22module Dev
33 ( Flags(..)
44 , run
@@ -7,15 +7,16 @@ module Dev
77
88
99import Control.Concurrent (threadDelay)
10−import Control.Exception (finally)
11−import Control.Monad (when)
10+import Control.Exception (finally, catch, IOException)
11+import Control.Monad (when, unless)
1212import qualified Data.List as List
1313import qualified Data.NonEmptyList as NE
1414import qualified Data.Time.Clock.POSIX as Time
1515import qualified System.Directory as Dir
1616import qualified System.Exit as SysExit
1717import System.FilePath ((</>), takeExtension, takeFileName)
18−import System.IO (hSetBuffering, stdout, BufferMode(..))
18+import qualified System.Info as Info
19+import System.IO (hSetBuffering, stdout, BufferMode(..), hFlush, hPutStr, hGetContents)
1920import qualified System.Process as Process
2021
2122import qualified BackgroundWriter as BW
@@ -36,6 +37,7 @@ import qualified Stuff
3637data Flags =
3738 Flags
3839 { _port :: Maybe Int
40+ , _verbose :: Bool
3941 }
4042
4143
@@ -44,7 +46,7 @@ data Flags =
4446
4547
4648run :: FilePath -> Flags -> IO ()
47−run path (Flags maybePort) =
49+run path (Flags maybePort verbose) =
4850 do hSetBuffering stdout LineBuffering
4951 maybeRoot <- Stuff.findRoot
5052 case maybeRoot of
@@ -65,18 +67,23 @@ run path (Flags maybePort) =
6567
6668 -- Initial compile
6769 putStrLn "Compiling..."
70+ startTime <- Time.getPOSIXTime
6871 ok <- compileAndWrite root path elmOutput
6972 when ok $
70− do -- Install deps if needed
73+ do endTime <- Time.getPOSIXTime
74+ let elapsed = realToFrac (endTime - startTime) :: Double
75+ putStrFlush $ " (" ++ showTime elapsed ++ ")\n"
76+
77+ -- Install deps if needed
7178 let rspackDir = nodeModules </> "@rspack" </> "cli"
7279 hasCorrectDeps <- Dir.doesDirectoryExist rspackDir
73− when (not hasCorrectDeps) $
80+ unless hasCorrectDeps $
7481 do putStrLn "Installing bundler dependencies..."
75− callIn bundlerDir "npm" ["install"]
82+ callIn bundlerDir "npm" ["install"] verbose
7683
7784 -- Assemble all.js and build bundle
7885 putStrLn "Bundling..."
79− assembleAndBuild bundlerDir
86+ assembleAndBuild bundlerDir verbose
8087
8188 -- Start static file server in background
8289 let distDir = bundlerDir </> "dist"
@@ -89,17 +96,29 @@ run path (Flags maybePort) =
8996
9097 -- Print bundle URL
9198 ip <- getLocalIp
99+ let url = "http://" ++ ip ++ ":" ++ show port ++ "/main.lynx.bundle"
92100 putStrLn ""
93101 putStrLn $ " LynxJS Explorer URL:"
94− putStrLn $ " http://" ++ ip ++ ":" ++ show port ++ "/main.lynx.bundle"
102+ putStrLn $ " " ++ url
95103 putStrLn ""
96104
105+ -- Print QR code
106+ printQrCode bundlerDir url
107+
108+ -- Report bundle size
109+ let bundlePath = distDir </> "main.lynx.bundle"
110+ bundleExists <- Dir.doesFileExist bundlePath
111+ when bundleExists $
112+ do size <- Dir.getFileSize bundlePath
113+ putStrLn $ " Bundle size: " ++ formatSize size
114+ putStrLn ""
115+
97116 -- Watch for changes, recompile, and rebuild
98117 let cleanup = do Process.terminateProcess serveHandle
99118 _ <- Process.waitForProcess serveHandle
100119 return ()
101120 finally
102− (watchAndRebuild root path elmOutput bundlerDir)
121+ (watchAndRebuild root path elmOutput bundlerDir verbose)
103122 cleanup
104123
105124
@@ -129,18 +148,18 @@ compileAndWrite root path elmOutput =
129148-- WATCH
130149
131150
132−assembleAndBuild :: FilePath -> IO ()
133−assembleAndBuild bundlerDir =
151+assembleAndBuild :: FilePath -> Bool -> IO ()
152+assembleAndBuild bundlerDir verbose =
134153 do let srcDir = bundlerDir </> "src"
135154 stubs <- readFile (srcDir </> "stubs.js")
136155 elm <- readFile (srcDir </> "elm.js")
137156 initCode <- readFile (srcDir </> "init.js")
138157 writeFile (srcDir </> "all.js") (stubs ++ elm ++ initCode)
139− callIn bundlerDir "npx" ["rspack", "build"]
158+ callIn bundlerDir "npx" ["rspack", "build"] verbose
140159
141160
142−watchAndRebuild :: FilePath -> FilePath -> FilePath -> FilePath -> IO ()
143−watchAndRebuild root path elmOutput bundlerDir =
161+watchAndRebuild :: FilePath -> FilePath -> FilePath -> FilePath -> Bool -> IO ()
162+watchAndRebuild root path elmOutput bundlerDir verbose =
144163 do mtime <- getMaxMtime root
145164 loop mtime
146165 where
@@ -149,10 +168,21 @@ watchAndRebuild root path elmOutput bundlerDir =
149168 mtime <- getMaxMtime root
150169 when (mtime /= lastMtime) $
151170 do putStrLn "\nRecompiling..."
171+ startTime <- Time.getPOSIXTime
152172 ok <- compileAndWrite root path elmOutput
153173 when ok $
154− do putStrLn "Bundling..."
155− assembleAndBuild bundlerDir
174+ do endTime <- Time.getPOSIXTime
175+ let elapsed = realToFrac (endTime - startTime) :: Double
176+ putStrFlush $ " (" ++ showTime elapsed ++ ")\n"
177+ putStrLn "Bundling..."
178+ assembleAndBuild bundlerDir verbose
179+
180+ -- Report bundle size
181+ let bundlePath = bundlerDir </> "dist" </> "main.lynx.bundle"
182+ bundleExists <- Dir.doesFileExist bundlePath
183+ when bundleExists $
184+ do size <- Dir.getFileSize bundlePath
185+ putStrLn $ " Bundle size: " ++ formatSize size
156186 loop mtime
157187
158188
@@ -204,22 +234,104 @@ setupBundlerProject dir _port =
204234
205235getLocalIp :: IO String
206236getLocalIp =
207− do let proc = Process.proc "ipconfig" ["getifaddr", "en0"]
208− result <- Process.readCreateProcess proc ""
209− case lines result of
210− (ip:_) -> return ip
211− _ -> return "localhost"
237+ tryGetIp `catch` (\(_ :: IOException) -> return "localhost")
238+ where
239+ tryGetIp =
240+ case Info.os of
241+ "darwin" ->
242+ do let proc = Process.proc "ipconfig" ["getifaddr", "en0"]
243+ result <- Process.readCreateProcess proc ""
244+ case words result of
245+ (ip:_) -> return ip
246+ _ -> return "localhost"
247+
248+ "linux" ->
249+ do let proc = Process.proc "hostname" ["-I"]
250+ result <- Process.readCreateProcess proc ""
251+ case words result of
252+ (ip:_) -> return ip
253+ _ -> return "localhost"
254+
255+ _ ->
256+ return "localhost"
257+
258+
259+callIn :: FilePath -> String -> [String] -> Bool -> IO ()
260+callIn dir cmd cmdArgs verbose =
261+ do let outStream = if verbose then Process.Inherit else Process.NoStream
262+ let proc = (Process.proc cmd cmdArgs)
263+ { Process.cwd = Just dir
264+ , Process.std_out = outStream
265+ , Process.std_err = Process.CreatePipe
266+ }
267+ (_, _, mStderr, ph) <- Process.createProcess proc
268+ exitCode <- Process.waitForProcess ph
269+ case exitCode of
270+ SysExit.ExitSuccess -> return ()
271+ SysExit.ExitFailure code ->
272+ do case mStderr of
273+ Just h ->
274+ do errOutput <- readAll h
275+ unless (null errOutput) $
276+ do putStrLn $ "\n-- " ++ cmd ++ " ERROR " ++ replicate 60 '-'
277+ putStrLn errOutput
278+ Nothing -> return ()
279+ error $ cmd ++ " failed with exit code " ++ show code
280+ where
281+ readAll h =
282+ do content <- hGetContents h
283+ length content `seq` return content
284+
285+
286+-- QR CODE
212287
213288
214−callIn :: FilePath -> String -> [String] -> IO ()
215−callIn dir cmd cmdArgs =
216− do let proc = (Process.proc cmd cmdArgs) { Process.cwd = Just dir }
289+printQrCode :: FilePath -> String -> IO ()
290+printQrCode bundlerDir url =
291+ do let proc = (Process.proc "npx" ["qrcode-terminal", url])
292+ { Process.cwd = Just bundlerDir
293+ , Process.std_err = Process.NoStream
294+ }
217295 (_, _, _, ph) <- Process.createProcess proc
218296 exitCode <- Process.waitForProcess ph
219297 case exitCode of
220298 SysExit.ExitSuccess -> return ()
221− SysExit.ExitFailure code ->
222− error $ cmd ++ " failed with exit code " ++ show code
299+ SysExit.ExitFailure _ -> return () -- silently skip if not available
300+
301+
302+
303+-- HELPERS
304+
305+
306+showTime :: Double -> String
307+showTime seconds
308+ | seconds < 1.0 = show (round (seconds * 1000) :: Int) ++ "ms"
309+ | otherwise = showFFloat 2 seconds ++ "s"
310+ where
311+ showFFloat n x =
312+ let whole = floor x :: Int
313+ frac = round ((x - fromIntegral whole) * (10 ^ n)) :: Int
314+ fracStr = show frac
315+ padded = replicate (n - length fracStr) '0' ++ fracStr
316+ in show whole ++ "." ++ padded
317+
318+
319+formatSize :: Integer -> String
320+formatSize bytes
321+ | bytes < 1024 = show bytes ++ " B"
322+ | bytes < 1048576 = showF (fromIntegral bytes / 1024) ++ " KB"
323+ | otherwise = showF (fromIntegral bytes / 1048576) ++ " MB"
324+ where
325+ showF :: Double -> String
326+ showF x =
327+ let whole = floor x :: Int
328+ frac = round ((x - fromIntegral whole) * 10) :: Int
329+ in if frac == 0 then show whole else show whole ++ "." ++ show frac
330+
331+
332+putStrFlush :: String -> IO ()
333+putStrFlush str =
334+ hPutStr stdout str >> hFlush stdout
223335
224336
225337packageJson :: String
@@ -231,7 +343,8 @@ packageJson = unlines
231343 , " \"dependencies\": {"
232344 , " \"@lynx-js/template-webpack-plugin\": \"0.6.4\","
233345 , " \"@rspack/cli\": \"^1.7.0\","
234− , " \"@rspack/core\": \"^1.7.0\""
346+ , " \"@rspack/core\": \"^1.7.0\","
347+ , " \"qrcode-terminal\": \"^0.12.0\""
235348 , " }"
236349 , "}"
237350 ]
terminal/src/Init.hs+129−11
@@ -44,18 +44,14 @@ question =
4444 D.stack
4545 [ D.fillSep
4646 ["Hello!"
47− ,"Elm","projects","always","start","with","an",D.green "elm.json","file."
47+ ,"LynxJS","Elm","projects","start","with","an",D.green "elm.json","file"
48+ ,"and","a","starter",D.green "src/Main.elm" <> "."
4849 ,"I","can","create","them!"
4950 ]
5051 , D.reflow
51− "Now you may be wondering, what will be in this file? How do I add Elm files to\
52− \ my project? How do I see it in the browser? How will my code grow? Do I need\
53− \ more directories? What about tests? Etc."
54− , D.fillSep
55− ["Check","out",D.cyan (D.fromChars (D.makeLink "init"))
56− ,"for","all","the","answers!"
57− ]
58− , "Knowing all that, would you like me to create an elm.json file now? [Y/n]: "
52+ "The project will include lynxjs-elm/ui and lynxjs-elm/browser as dependencies,\
53+ \ plus a simple counter app to get you started."
54+ , "Would you like me to create a new LynxJS Elm project here? [Y/n]: "
5955 ]
6056
6157
@@ -91,7 +87,17 @@ init =
9187 do Dir.createDirectoryIfMissing True "src"
9288 Outline.write "." $ Outline.App $
9389 Outline.AppOutline V.compiler (NE.List (Outline.RelativeSrcDir "src") []) directs indirects Map.empty Map.empty
94− putStrLn "Okay, I created it. Now read that link!"
90+ let mainElm = "src/Main.elm"
91+ mainExists <- Dir.doesFileExist mainElm
92+ if mainExists
93+ then return ()
94+ else writeFile mainElm starterMain
95+ putStrLn ""
96+ putStrLn " Created elm.json and src/Main.elm"
97+ putStrLn ""
98+ putStrLn " To build: lynxjs-elm make src/Main.elm"
99+ putStrLn " To dev: lynxjs-elm dev src/Main.elm"
100+ putStrLn ""
95101 return (Right ())
96102
97103
@@ -100,5 +106,117 @@ defaults =
100106 Map.fromList
101107 [ (Pkg.core, Con.anything)
102108 , (Pkg.browser, Con.anything)
103− , (Pkg.html, Con.anything)
109+ , (Pkg.ui, Con.anything)
104110 ]
111+
112+
113+starterMain :: String
114+starterMain = unlines
115+ [ "module Main exposing (main)"
116+ , ""
117+ , ""
118+ , "import Browser"
119+ , "import Lynx"
120+ , "import Lynx.Attributes as Attr"
121+ , "import Lynx.Events exposing (onTap)"
122+ , ""
123+ , ""
124+ , ""
125+ , ""
126+ , "-- MAIN"
127+ , ""
128+ , ""
129+ , "main : Program () Model Msg"
130+ , "main ="
131+ , " Browser.sandbox"
132+ , " { init = init"
133+ , " , view = view"
134+ , " , update = update"
135+ , " }"
136+ , ""
137+ , ""
138+ , ""
139+ , "-- MODEL"
140+ , ""
141+ , ""
142+ , "type alias Model ="
143+ , " { count : Int"
144+ , " }"
145+ , ""
146+ , ""
147+ , "init : Model"
148+ , "init ="
149+ , " { count = 0"
150+ , " }"
151+ , ""
152+ , ""
153+ , ""
154+ , "-- UPDATE"
155+ , ""
156+ , ""
157+ , "type Msg"
158+ , " = Increment"
159+ , " | Decrement"
160+ , ""
161+ , ""
162+ , "update : Msg -> Model -> Model"
163+ , "update msg model ="
164+ , " case msg of"
165+ , " Increment ->"
166+ , " { model | count = model.count + 1 }"
167+ , ""
168+ , " Decrement ->"
169+ , " { model | count = model.count - 1 }"
170+ , ""
171+ , ""
172+ , ""
173+ , "-- VIEW"
174+ , ""
175+ , ""
176+ , "view : Model -> Lynx.Node Msg"
177+ , "view model ="
178+ , " Lynx.view"
179+ , " [ Attr.flexDirection \"column\""
180+ , " , Attr.alignItems \"center\""
181+ , " , Attr.justifyContent \"center\""
182+ , " , Attr.flex 1"
183+ , " ]"
184+ , " [ Lynx.text"
185+ , " [ Attr.fontSize 24"
186+ , " , Attr.marginBottom 20"
187+ , " ]"
188+ , " [ Lynx.textContent \"LynxJS Elm App\" ]"
189+ , " , Lynx.view"
190+ , " [ Attr.flexDirection \"row\""
191+ , " , Attr.alignItems \"center\""
192+ , " ]"
193+ , " [ button Decrement \"-\""
194+ , " , Lynx.text"
195+ , " [ Attr.fontSize 32"
196+ , " , Attr.marginLeft 20"
197+ , " , Attr.marginRight 20"
198+ , " ]"
199+ , " [ Lynx.textContent (String.fromInt model.count) ]"
200+ , " , button Increment \"+\""
201+ , " ]"
202+ , " ]"
203+ , ""
204+ , ""
205+ , "button : Msg -> String -> Lynx.Node Msg"
206+ , "button msg label ="
207+ , " Lynx.view"
208+ , " [ onTap msg"
209+ , " , Attr.backgroundColor \"#4a90d9\""
210+ , " , Attr.borderRadius 8"
211+ , " , Attr.paddingTop 12"
212+ , " , Attr.paddingBottom 12"
213+ , " , Attr.paddingLeft 24"
214+ , " , Attr.paddingRight 24"
215+ , " ]"
216+ , " [ Lynx.text"
217+ , " [ Attr.color \"#ffffff\""
218+ , " , Attr.fontSize 24"
219+ , " ]"
220+ , " [ Lynx.textContent label ]"
221+ , " ]"
222+ ]
terminal/src/Main.hs+41−17
@@ -23,6 +23,7 @@ import qualified Install
2323import qualified Make
2424import qualified Publish
2525import qualified Repl
26+import qualified Uninstall
2627
2728
2829
@@ -38,6 +39,7 @@ main =
3839 , dev
3940 , make
4041 , install
42+ , uninstall
4143 , bump
4244 , diff
4345 , publish
@@ -77,16 +79,17 @@ init :: Terminal.Command
7779init =
7880 let
7981 summary =
80− "Start an Elm project. It creates a starter elm.json file and\
81− \ provides a link explaining what to do from there."
82+ "Start a LynxJS Elm project. It creates an elm.json file with LynxJS\
83+ \ dependencies and a starter src/Main.elm with a counter app."
8284
8385 details =
84− "The `init` command helps start Elm projects:"
86+ "The `init` command helps start LynxJS Elm projects:"
8587
8688 example =
8789 reflow
88− "It will ask permission to create an elm.json file, the one thing common\
89− \ to all Elm projects. It also provides a link explaining what to do from there."
90+ "It will ask permission to create an elm.json file and a starter src/Main.elm\
91+ \ file. The project will include lynxjs-elm/ui and lynxjs-elm/browser as\
92+ \ dependencies so you can start building native UIs right away."
9093 in
9194 Terminal.Command "init" (Common summary) details example noArgs noFlags Init.run
9295
@@ -178,10 +181,10 @@ dev =
178181 let
179182 summary =
180183 "Start a LynxJS dev server with live reloading. It compiles your Elm code,\
181− \ bundles it with Rspeedy, and watches for changes."
184+ \ bundles it with rspack, and watches for changes."
182185
183186 details =
184− "The `dev` command starts an Rspeedy dev server for LynxJS:"
187+ "The `dev` command starts a dev server for LynxJS:"
185188
186189 example =
187190 reflow
@@ -192,6 +195,7 @@ dev =
192195 devFlags =
193196 flags Dev.Flags
194197 |-- flag "port" port_ "The port of the dev server (default: 3000)"
198+ |-- onOff "verbose" "Show detailed output from npm and rspack during builds."
195199 in
196200 Terminal.Command "dev" (Common summary) details example (required elmFile) devFlags Dev.run
197201
@@ -210,10 +214,10 @@ make =
210214 stack
211215 [ reflow
212216 "For example:"
213− , P.indent 4 $ P.green "elm make src/Main.elm"
217+ , P.indent 4 $ P.green "lynxjs-elm make src/Main.elm"
214218 , reflow
215− "This tries to compile an Elm file named src/Main.elm, generating an index.html\
216− \ file if possible."
219+ "This tries to compile an Elm file named src/Main.elm, generating a\
220+ \ main.lynx.bundle file by default."
217221 ]
218222
219223 makeFlags =
@@ -223,6 +227,7 @@ make =
223227 |-- flag "output" Make.output "Specify the name of the resulting JS file. For example --output=assets/elm.js to generate the JS at assets/elm.js or --output=/dev/null to generate no output at all!"
224228 |-- flag "report" Make.reportType "You can say --report=json to get error messages as JSON. This is only really useful if you are an editor plugin. Humans should avoid it!"
225229 |-- flag "docs" Make.docsFile "Generate a JSON file of documentation for a package. Eventually it will be possible to preview docs with `reactor` because it is quite hard to deal with these JSON files directly."
230+ |-- onOff "verbose" "Show detailed output from npm and rspack during bundling."
226231 in
227232 Terminal.Command "make" Uncommon details example (zeroOrMore elmFile) makeFlags Make.run
228233
@@ -235,23 +240,19 @@ install :: Terminal.Command
235240install =
236241 let
237242 details =
238− "The `install` command fetches packages from <https://package.elm-lang.org> for\
239− \ use in your project:"
243+ "The `install` command fetches packages for use in your project:"
240244
241245 example =
242246 stack
243247 [ reflow
244248 "For example, if you want to get packages for HTTP and JSON, you would say:"
245249 , P.indent 4 $ P.green $ P.vcat $
246− [ "elm install elm/http"
247− , "elm install elm/json"
250+ [ "lynxjs-elm install lynxjs-elm/http"
251+ , "lynxjs-elm install elm/json"
248252 ]
249253 , reflow
250254 "Notice that you must say the AUTHOR name and PROJECT name! After running those\
251255 \ commands, you could say `import Http` or `import Json.Decode` in your code."
252− , reflow
253− "What if two projects use different versions of the same package? No problem!\
254− \ Each project is independent, so there cannot be conflicts like that!"
255256 ]
256257
257258 installArgs =
@@ -264,6 +265,29 @@ install =
264265
265266
266267
268+-- UNINSTALL
269+
270+
271+uninstall :: Terminal.Command
272+uninstall =
273+ let
274+ details =
275+ "The `uninstall` command removes packages from your project:"
276+
277+ example =
278+ stack
279+ [ reflow
280+ "For example, if you want to remove a package, you would say:"
281+ , P.indent 4 $ P.green "lynxjs-elm uninstall elm/http"
282+ , reflow
283+ "I will remove it from your elm.json and verify that everything\
284+ \ still compiles without it."
285+ ]
286+ in
287+ Terminal.Command "uninstall" Uncommon details example (required package) noFlags Uninstall.run
288+
289+
290+
267291-- PUBLISH
268292
269293
terminal/src/Make.hs+75−19
@@ -11,14 +11,16 @@ module Make
1111 where
1212
1313
14−import Control.Monad (when)
14+import Control.Monad (unless)
1515import qualified Data.ByteString.Builder as B
1616import qualified Data.Maybe as Maybe
1717import qualified Data.NonEmptyList as NE
18+import qualified Data.Time.Clock.POSIX as Time
1819import qualified System.Directory as Dir
1920import qualified System.FilePath as FP
2021import System.FilePath ((</>))
2122import qualified System.Exit as SysExit
23+import System.IO (hGetContents)
2224import qualified System.Process as Process
2325
2426import qualified AST.Optimized as Opt
@@ -47,6 +49,7 @@ data Flags =
4749 , _output :: Maybe Output
4850 , _report :: Maybe ReportType
4951 , _docs :: Maybe FilePath
52+ , _verbose :: Bool
5053 }
5154
5255
@@ -69,7 +72,7 @@ type Task a = Task.Task Exit.Make a
6972
7073
7174run :: [FilePath] -> Flags -> IO ()
72−run paths flags@(Flags _ _ _ report _) =
75+run paths flags@(Flags _ _ _ report _ _) =
7376 do style <- getStyle report
7477 maybeRoot <- Stuff.findRoot
7578 Reporting.attemptWithStyle style Exit.makeToReport $
@@ -79,7 +82,7 @@ run paths flags@(Flags _ _ _ report _) =
7982
8083
8184runHelp :: FilePath -> [FilePath] -> Reporting.Style -> Flags -> IO (Either Exit.Make ())
82−runHelp root paths style (Flags debug optimize maybeOutput _ maybeDocs) =
85+runHelp root paths style (Flags debug optimize maybeOutput _ maybeDocs verbose) =
8386 BW.withScope $ \scope ->
8487 Stuff.withRootLock root $ Task.run $
8588 do desiredMode <- getMode debug optimize
@@ -100,7 +103,7 @@ runHelp root paths style (Flags debug optimize maybeOutput _ maybeDocs) =
100103 _ ->
101104 do _name <- hasOneMain artifacts
102105 builder <- toBuilder root details desiredMode artifacts
103− Task.io $ bundle style "main.lynx.bundle" builder (Build.getRootNames artifacts)
106+ Task.io $ bundle style "main.lynx.bundle" builder (Build.getRootNames artifacts) verbose
104107
105108 Just DevNull ->
106109 return ()
@@ -122,7 +125,7 @@ runHelp root paths style (Flags debug optimize maybeOutput _ maybeDocs) =
122125 Just (Bundle target) ->
123126 do _name <- hasOneMain artifacts
124127 builder <- toBuilder root details desiredMode artifacts
125− Task.io $ bundle style target builder (Build.getRootNames artifacts)
128+ Task.io $ bundle style target builder (Build.getRootNames artifacts) verbose
126129
127130
128131
@@ -336,9 +339,10 @@ isDevNull name =
336339-- BUNDLE
337340
338341
339−bundle :: Reporting.Style -> FilePath -> B.Builder -> NE.List ModuleName.Raw -> IO ()
340−bundle style target js names =
341− do home <- Stuff.getElmHome
342+bundle :: Reporting.Style -> FilePath -> B.Builder -> NE.List ModuleName.Raw -> Bool -> IO ()
343+bundle style target js names verbose =
344+ do startTime <- Time.getPOSIXTime
345+ home <- Stuff.getElmHome
342346 let bundlerDir = home </> "bundler"
343347 let srcDir = bundlerDir </> "src"
344348 let elmFile = srcDir </> "elm.js"
@@ -354,29 +358,35 @@ bundle style target js names =
354358 -- Install deps if needed
355359 let rspackDir = nodeModules </> "@rspack" </> "cli"
356360 hasCorrectDeps <- Dir.doesDirectoryExist rspackDir
357− when (not hasCorrectDeps) $
361+ unless hasCorrectDeps $
358362 do putStrLn "Installing bundler dependencies..."
359− callIn bundlerDir "npm" ["install"]
363+ callIn bundlerDir "npm" ["install"] verbose
360364
361365 -- Assemble all.js and build bundle
362366 putStrLn "Bundling..."
363− assembleAndBuild bundlerDir
367+ assembleAndBuild bundlerDir verbose
364368
365369 -- Copy the output bundle
366370 let distBundle = bundlerDir </> "dist" </> "main.lynx.bundle"
367371 Dir.createDirectoryIfMissing True (FP.takeDirectory target)
368372 Dir.copyFile distBundle target
373+
374+ -- Report timing and size
375+ endTime <- Time.getPOSIXTime
376+ let elapsed = realToFrac (endTime - startTime) :: Double
377+ size <- Dir.getFileSize target
369378 Reporting.reportGenerate style names target
379+ putStrLn $ " Bundled in " ++ showTime elapsed ++ " (" ++ formatSize size ++ ")"
370380
371381
372−assembleAndBuild :: FilePath -> IO ()
373−assembleAndBuild bundlerDir =
382+assembleAndBuild :: FilePath -> Bool -> IO ()
383+assembleAndBuild bundlerDir verbose =
374384 do let srcDir = bundlerDir </> "src"
375385 stubs <- readFile (srcDir </> "stubs.js")
376386 elm <- readFile (srcDir </> "elm.js")
377387 initCode <- readFile (srcDir </> "init.js")
378388 writeFile (srcDir </> "all.js") (stubs ++ elm ++ initCode)
379− callIn bundlerDir "npx" ["rspack", "build"]
389+ callIn bundlerDir "npx" ["rspack", "build"] verbose
380390
381391
382392setupBundlerProject :: FilePath -> IO ()
@@ -391,15 +401,61 @@ setupBundlerProject dir =
391401 writeFile initFile initJs
392402
393403
394−callIn :: FilePath -> String -> [String] -> IO ()
395−callIn dir cmd args =
396− do let proc = (Process.proc cmd args) { Process.cwd = Just dir }
397− (_, _, _, ph) <- Process.createProcess proc
404+callIn :: FilePath -> String -> [String] -> Bool -> IO ()
405+callIn dir cmd args verbose =
406+ do let outStream = if verbose then Process.Inherit else Process.NoStream
407+ let proc = (Process.proc cmd args)
408+ { Process.cwd = Just dir
409+ , Process.std_out = outStream
410+ , Process.std_err = Process.CreatePipe
411+ }
412+ (_, _, mStderr, ph) <- Process.createProcess proc
398413 exitCode <- Process.waitForProcess ph
399414 case exitCode of
400415 SysExit.ExitSuccess -> return ()
401416 SysExit.ExitFailure code ->
402− error $ cmd ++ " failed with exit code " ++ show code
417+ do case mStderr of
418+ Just h ->
419+ do errOutput <- readAll h
420+ unless (null errOutput) $
421+ do putStrLn $ "\n-- " ++ cmd ++ " ERROR " ++ replicate 60 '-'
422+ putStrLn errOutput
423+ Nothing -> return ()
424+ error $ cmd ++ " failed with exit code " ++ show code
425+ where
426+ readAll h =
427+ do content <- hGetContents h
428+ length content `seq` return content
429+
430+
431+
432+-- HELPERS
433+
434+
435+showTime :: Double -> String
436+showTime seconds
437+ | seconds < 1.0 = show (round (seconds * 1000) :: Int) ++ "ms"
438+ | otherwise = showFFloat 2 seconds ++ "s"
439+ where
440+ showFFloat n x =
441+ let whole = floor x :: Int
442+ frac = round ((x - fromIntegral whole) * (10 ^ n)) :: Int
443+ fracStr = show frac
444+ padded = replicate (n - length fracStr) '0' ++ fracStr
445+ in show whole ++ "." ++ padded
446+
447+
448+formatSize :: Integer -> String
449+formatSize bytes
450+ | bytes < 1024 = show bytes ++ " B"
451+ | bytes < 1048576 = showF (fromIntegral bytes / 1024) ++ " KB"
452+ | otherwise = showF (fromIntegral bytes / 1048576) ++ " MB"
453+ where
454+ showF :: Double -> String
455+ showF x =
456+ let whole = floor x :: Int
457+ frac = round ((x - fromIntegral whole) * 10) :: Int
458+ in if frac == 0 then show whole else show whole ++ "." ++ show frac
403459
404460
405461packageJson :: String
terminal/src/Uninstall.hsnew file+154
@@ -0,0 +1,154 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Uninstall
3+ ( run
4+ )
5+ where
6+
7+
8+import qualified Data.Map as Map
9+
10+import qualified BackgroundWriter as BW
11+import qualified Deps.Solver as Solver
12+import qualified Elm.Constraint as C
13+import qualified Elm.Details as Details
14+import qualified Elm.Outline as Outline
15+import qualified Elm.Package as Pkg
16+import qualified Reporting
17+import qualified Reporting.Doc as D
18+import qualified Reporting.Exit as Exit
19+import qualified Reporting.Task as Task
20+import qualified Stuff
21+
22+
23+
24+-- RUN
25+
26+
27+run :: Pkg.Name -> () -> IO ()
28+run pkg () =
29+ Reporting.attempt Exit.installToReport $
30+ do maybeRoot <- Stuff.findRoot
31+ case maybeRoot of
32+ Nothing ->
33+ return (Left Exit.InstallNoOutline)
34+
35+ Just root ->
36+ Task.run $
37+ do env <- Task.eio Exit.InstallBadRegistry $ Solver.initEnv
38+ oldOutline <- Task.eio Exit.InstallBadOutline $ Outline.read root
39+ case oldOutline of
40+ Outline.App outline ->
41+ uninstallApp root env oldOutline pkg outline
42+
43+ Outline.Pkg outline ->
44+ uninstallPkg root env oldOutline pkg outline
45+
46+
47+type Task = Task.Task Exit.Install
48+
49+
50+
51+-- UNINSTALL FROM APP
52+
53+
54+uninstallApp :: FilePath -> Solver.Env -> Outline.Outline -> Pkg.Name -> Outline.AppOutline -> Task ()
55+uninstallApp root env oldOutline pkg outline@(Outline.AppOutline _ _ direct _indirect testDirect _testIndirect) =
56+ if Map.member pkg direct then
57+ do let newDirect = Map.delete pkg direct
58+ -- Re-solve dependencies to get correct indirect deps
59+ let constraints = Map.map C.exactly newDirect
60+ let (Solver.Env cache _ connection registry) = env
61+ result <- Task.io $ Solver.verify cache connection registry constraints
62+ case result of
63+ Solver.Ok details ->
64+ let
65+ solution = Map.map (\(Solver.Details vsn _) -> vsn) details
66+ solvedDirects = Map.intersection solution constraints
67+ solvedIndirects = Map.difference solution constraints
68+ newOutline = Outline.App $
69+ outline
70+ { Outline._app_deps_direct = solvedDirects
71+ , Outline._app_deps_indirect = solvedIndirects
72+ }
73+ in
74+ attemptUninstall root env oldOutline newOutline pkg
75+
76+ Solver.NoSolution ->
77+ Task.io $
78+ putStrLn $ "I cannot safely remove " ++ Pkg.toChars pkg ++ " because other packages depend on it."
79+
80+ Solver.NoOfflineSolution ->
81+ Task.io $
82+ putStrLn $ "I cannot safely remove " ++ Pkg.toChars pkg ++ " while offline."
83+
84+ Solver.Err exit ->
85+ Task.throw (Exit.InstallHadSolverTrouble exit)
86+
87+ else if Map.member pkg testDirect then
88+ do let newOutline = Outline.App $
89+ outline
90+ { Outline._app_test_direct = Map.delete pkg testDirect
91+ }
92+ attemptUninstall root env oldOutline newOutline pkg
93+
94+ else
95+ Task.io $ putStrLn $ "The package " ++ Pkg.toChars pkg ++ " is not a direct dependency."
96+
97+
98+
99+-- UNINSTALL FROM PACKAGE
100+
101+
102+uninstallPkg :: FilePath -> Solver.Env -> Outline.Outline -> Pkg.Name -> Outline.PkgOutline -> Task ()
103+uninstallPkg root env oldOutline pkg outline@(Outline.PkgOutline _ _ _ _ _ deps test _) =
104+ if Map.member pkg deps then
105+ do let newOutline = Outline.Pkg $
106+ outline
107+ { Outline._pkg_deps = Map.delete pkg deps
108+ }
109+ attemptUninstall root env oldOutline newOutline pkg
110+
111+ else if Map.member pkg test then
112+ do let newOutline = Outline.Pkg $
113+ outline
114+ { Outline._pkg_test_deps = Map.delete pkg test
115+ }
116+ attemptUninstall root env oldOutline newOutline pkg
117+
118+ else
119+ Task.io $ putStrLn $ "The package " ++ Pkg.toChars pkg ++ " is not a dependency."
120+
121+
122+
123+-- ATTEMPT UNINSTALL
124+
125+
126+attemptUninstall :: FilePath -> Solver.Env -> Outline.Outline -> Outline.Outline -> Pkg.Name -> Task ()
127+attemptUninstall root env oldOutline newOutline pkg =
128+ Task.eio Exit.InstallBadDetails $
129+ BW.withScope $ \scope ->
130+ do let question = D.vcat
131+ [ D.fillSep
132+ ["I","found",D.red (D.fromPackage pkg),"in","your","dependencies."
133+ ]
134+ , ""
135+ , D.fillSep
136+ ["Should","I","remove","it?","[Y/n]: "
137+ ]
138+ ]
139+ approved <- Reporting.ask question
140+ if approved
141+ then
142+ do Outline.write root newOutline
143+ result <- Details.verifyInstall scope root env newOutline
144+ case result of
145+ Left exit ->
146+ do Outline.write root oldOutline
147+ return (Left exit)
148+
149+ Right () ->
150+ do putStrLn "Success!"
151+ return (Right ())
152+ else
153+ do putStrLn "Okay, I did not change anything!"
154+ return (Right ())