@@ -0,0 +1,287 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Dev
3+ ( Flags(..)
4+ , run
5+ )
6+ where
7+
8+
9+import Control.Concurrent (threadDelay)
10+import Control.Exception (finally)
11+import Control.Monad (when)
12+import qualified Data.List as List
13+import qualified Data.NonEmptyList as NE
14+import qualified Data.Time.Clock.POSIX as Time
15+import qualified System.Directory as Dir
16+import qualified System.Exit as SysExit
17+import System.FilePath ((</>), takeExtension, takeFileName)
18+import qualified System.Process as Process
19+
20+import qualified BackgroundWriter as BW
21+import qualified Build
22+import qualified Elm.Details as Details
23+import qualified File
24+import qualified Generate
25+import qualified Reporting
26+import qualified Reporting.Exit as Exit
27+import qualified Reporting.Task as Task
28+import qualified Stuff
29+
30+
31+
32+-- FLAGS
33+
34+
35+data Flags =
36+ Flags
37+ { _port :: Maybe Int
38+ }
39+
40+
41+
42+-- RUN
43+
44+
45+run :: FilePath -> Flags -> IO ()
46+run path (Flags maybePort) =
47+ do maybeRoot <- Stuff.findRoot
48+ case maybeRoot of
49+ Nothing ->
50+ putStrLn "I cannot find an elm.json file. Run `lynxjs-elm init` first."
51+
52+ Just root ->
53+ do home <- Stuff.getElmHome
54+ let bundlerDir = home </> "bundler"
55+ let srcDir = bundlerDir </> "src"
56+ let elmOutput = srcDir </> "elm.js"
57+ let nodeModules = bundlerDir </> "node_modules"
58+
59+ -- Set up bundler project
60+ let port = maybe 3000 id maybePort
61+ Dir.createDirectoryIfMissing True srcDir
62+ setupBundlerProject bundlerDir port
63+
64+ -- Initial compile
65+ putStrLn "Compiling..."
66+ ok <- compileAndWrite root path elmOutput
67+ when ok $
68+ do -- Install deps if needed
69+ let rspackDir = nodeModules </> "@rspack" </> "cli"
70+ hasCorrectDeps <- Dir.doesDirectoryExist rspackDir
71+ when (not hasCorrectDeps) $
72+ do putStrLn "Installing bundler dependencies..."
73+ callIn bundlerDir "npm" ["install"]
74+
75+ -- Assemble all.js and build bundle
76+ putStrLn "Bundling..."
77+ assembleAndBuild bundlerDir
78+
79+ -- Start static file server in background
80+ let distDir = bundlerDir </> "dist"
81+ let serveProc = (Process.proc "npx" ["serve", "-l", show port, "-n", distDir])
82+ { Process.cwd = Just bundlerDir }
83+ (_, _, _, serveHandle) <- Process.createProcess serveProc
84+
85+ -- Print bundle URL
86+ ip <- getLocalIp
87+ putStrLn ""
88+ putStrLn $ " LynxJS Explorer URL:"
89+ putStrLn $ " http://" ++ ip ++ ":" ++ show port ++ "/main.lynx.bundle"
90+ putStrLn ""
91+
92+ -- Watch for changes, recompile, and rebuild
93+ let cleanup = do Process.terminateProcess serveHandle
94+ _ <- Process.waitForProcess serveHandle
95+ return ()
96+ finally
97+ (watchAndRebuild root path elmOutput bundlerDir)
98+ cleanup
99+
100+
101+
102+-- COMPILE
103+
104+
105+compileAndWrite :: FilePath -> FilePath -> FilePath -> IO Bool
106+compileAndWrite root path elmOutput =
107+ BW.withScope $ \scope -> Stuff.withRootLock root $
108+ do result <- Task.run $
109+ do details <- Task.eio Exit.MakeBadDetails (Details.load Reporting.silent scope root)
110+ artifacts <- Task.eio Exit.MakeCannotBuild (Build.fromPaths Reporting.silent root details (NE.List path []))
111+ Task.mapError Exit.MakeBadGenerate (Generate.dev root details artifacts)
112+ case result of
113+ Left e ->
114+ do _ <- Reporting.attemptWithStyle Reporting.silent Exit.makeToReport (return (Left e))
115+ return False
116+
117+ Right javascript ->
118+ do File.writeBuilder elmOutput javascript
119+ putStrLn " Compiled successfully."
120+ return True
121+
122+
123+
124+-- WATCH
125+
126+
127+assembleAndBuild :: FilePath -> IO ()
128+assembleAndBuild bundlerDir =
129+ do let srcDir = bundlerDir </> "src"
130+ stubs <- readFile (srcDir </> "stubs.js")
131+ elm <- readFile (srcDir </> "elm.js")
132+ initCode <- readFile (srcDir </> "init.js")
133+ writeFile (srcDir </> "all.js") (stubs ++ elm ++ initCode)
134+ callIn bundlerDir "npx" ["rspack", "build"]
135+
136+
137+watchAndRebuild :: FilePath -> FilePath -> FilePath -> FilePath -> IO ()
138+watchAndRebuild root path elmOutput bundlerDir =
139+ do mtime <- getMaxMtime root
140+ loop mtime
141+ where
142+ loop lastMtime =
143+ do threadDelay 500000 -- 500ms
144+ mtime <- getMaxMtime root
145+ when (mtime /= lastMtime) $
146+ do putStrLn "\nRecompiling..."
147+ ok <- compileAndWrite root path elmOutput
148+ when ok $
149+ do putStrLn "Bundling..."
150+ assembleAndBuild bundlerDir
151+ loop mtime
152+
153+
154+getMaxMtime :: FilePath -> IO Double
155+getMaxMtime root =
156+ do files <- findElmFiles root
157+ case files of
158+ [] -> return 0
159+ _ ->
160+ do times <- mapM Dir.getModificationTime files
161+ return $ maximum $ map (realToFrac . Time.utcTimeToPOSIXSeconds) times
162+
163+
164+findElmFiles :: FilePath -> IO [FilePath]
165+findElmFiles dir =
166+ do entries <- Dir.listDirectory dir
167+ let paths = map (dir </>) entries
168+ files <- mapM classify paths
169+ return (concat files)
170+ where
171+ classify fp =
172+ do isDir <- Dir.doesDirectoryExist fp
173+ case (isDir, isHidden fp) of
174+ (True, False) -> findElmFiles fp
175+ (True, True) -> return []
176+ _ -> return [fp | takeExtension fp == ".elm"]
177+
178+ isHidden fp =
179+ case takeFileName fp of
180+ '.':_ -> True
181+ _ -> List.isPrefixOf "elm-stuff" (takeFileName fp)
182+
183+
184+
185+-- BUNDLER PROJECT
186+
187+
188+setupBundlerProject :: FilePath -> Int -> IO ()
189+setupBundlerProject dir _port =
190+ do let pkgFile = dir </> "package.json"
191+ let cfgFile = dir </> "rspack.config.js"
192+ let stubsFile = dir </> "src" </> "stubs.js"
193+ let initFile = dir </> "src" </> "init.js"
194+ writeFile pkgFile packageJson
195+ writeFile cfgFile rspackConfig
196+ writeFile stubsFile stubsJs
197+ writeFile initFile initJs
198+
199+
200+getLocalIp :: IO String
201+getLocalIp =
202+ do let proc = Process.proc "ipconfig" ["getifaddr", "en0"]
203+ result <- Process.readCreateProcess proc ""
204+ case lines result of
205+ (ip:_) -> return ip
206+ _ -> return "localhost"
207+
208+
209+callIn :: FilePath -> String -> [String] -> IO ()
210+callIn dir cmd cmdArgs =
211+ do let proc = (Process.proc cmd cmdArgs) { Process.cwd = Just dir }
212+ (_, _, _, ph) <- Process.createProcess proc
213+ exitCode <- Process.waitForProcess ph
214+ case exitCode of
215+ SysExit.ExitSuccess -> return ()
216+ SysExit.ExitFailure code ->
217+ error $ cmd ++ " failed with exit code " ++ show code
218+
219+
220+packageJson :: String
221+packageJson = unlines
222+ [ "{"
223+ , " \"name\": \"lynxjs-elm-bundler\","
224+ , " \"private\": true,"
225+ , " \"type\": \"module\","
226+ , " \"dependencies\": {"
227+ , " \"@lynx-js/template-webpack-plugin\": \"0.6.4\","
228+ , " \"@rspack/cli\": \"^1.7.0\","
229+ , " \"@rspack/core\": \"^1.7.0\""
230+ , " }"
231+ , "}"
232+ ]
233+
234+
235+rspackConfig :: String
236+rspackConfig = unlines
237+ [ "import { LynxEncodePlugin, LynxTemplatePlugin } from '@lynx-js/template-webpack-plugin';"
238+ , "import { defineConfig } from '@rspack/cli';"
239+ , ""
240+ , "export default defineConfig({"
241+ , " entry: { main: './src/all.js' },"
242+ , " plugins: ["
243+ , " new LynxEncodePlugin(),"
244+ , " new LynxTemplatePlugin({ filename: 'main.lynx.bundle', intermediate: 'main' }),"
245+ , " (compiler) => {"
246+ , " compiler.hooks.thisCompilation.tap('MarkMainThread', (compilation) => {"
247+ , " compilation.hooks.processAssets.tap('MarkMainThread', () => {"
248+ , " const asset = compilation.getAsset('main.js');"
249+ , " compilation.updateAsset(asset.name, asset.source, { ...asset.info, 'lynx:main-thread': true });"
250+ , " });"
251+ , " });"
252+ , " },"
253+ , " ]"
254+ , "});"
255+ ]
256+
257+
258+stubsJs :: String
259+stubsJs = unlines
260+ [ "globalThis['requestAnimationFrame'] = lynx['requestAnimationFrame'];"
261+ , "globalThis['cancelAnimationFrame'] = lynx['cancelAnimationFrame'];"
262+ , "globalThis['runWorklet'] = function(worklet, params) { return worklet(params); };"
263+ , "globalThis['native'] = {};"
264+ , "globalThis['processData'] = function() {};"
265+ , "globalThis['updatePage'] = function() {};"
266+ , "globalThis['updateGlobalProps'] = function() {};"
267+ ]
268+
269+
270+initJs :: String
271+initJs = unlines
272+ [ "globalThis['renderPage'] = function() {"
273+ , " var page = __CreatePage('0', 0);"
274+ , " var pageId = __GetElementUniqueID(page);"
275+ , " globalThis['native']['currentPageId'] = pageId;"
276+ , " globalThis['page'] = page;"
277+ , " var mods = globalThis.Elm;"
278+ , " if (mods) {"
279+ , " for (var k in mods) {"
280+ , " if (mods[k] && mods[k].init) {"
281+ , " mods[k].init({});"
282+ , " break;"
283+ , " }"
284+ , " }"
285+ , " }"
286+ , "};"
287+ ]