packages feed

jsaddle-wasm 0.1.1.0 → 0.1.2.0

raw patch · 6 files changed

+168/−22 lines, 6 filesdep +parser-regexdep +template-haskellPVP ok

version bump matches the API change (PVP)

Dependencies added: parser-regex, template-haskell

API changes (from Hackage documentation)

+ Language.Javascript.JSaddle.Wasm.TH: eval :: String -> Q Exp
+ Language.Javascript.JSaddle.Wasm.TH: evalFile :: FilePath -> Q Exp

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Revision history for jsaddle-wasm +## 0.1.2.0 -- 2025-06-27++ * Internally, stop using JS `eval`. This allows usage with a `Content-Security-Policy` without `unsafe-eval` (but still with `wasm-unsafe-eval`).++   For the same reason, expose `eval` and `evalFile` from `Language.Javascript.JSaddle.Wasm.TH` which allow to generate corresponding Wasm JSFFI imports for statically known strings.++   Useful as a replacement of JSaddle's `eval` in downstream libraries+ ## 0.1.1.0 -- 2025-05-01   * Bug fix: make GHCJS helpers globally available.
README.md view
@@ -103,6 +103,12 @@    runJSaddle(worker);    ``` +Additionally, when other packages use the `Language.Javascript.JSaddle.Wasm.TH` module, you need to disable the `eval-via-jsffi` flag, e.g. by adding the following to your `cabal.project`:+```cabal+package jsaddle-wasm+  flags: -eval-via-jsffi+```+ ## Potential future work   - Testing (e.g. via Selenium).
jsaddle-wasm.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: jsaddle-wasm-version: 0.1.1.0+version: 0.1.2.0 synopsis: Run JSaddle JSM with the GHC Wasm backend description: Run JSaddle @JSM@ with the GHC Wasm backend. category: Web, Javascript@@ -17,6 +17,14 @@   location: https://github.com/amesgen/jsaddle-wasm   type: git +flag eval-via-jsffi+  description:+    Generate Wasm JSFFI imports for the TemplateHaskell utilities in+    @Language.Javascript.JSaddle.Wasm.TH@.++  default: True+  manual: True+ common common   ghc-options:     -Wall@@ -26,7 +34,9 @@   default-language: GHC2021   default-extensions:     BlockArguments+    LambdaCase     OverloadedStrings+    TemplateHaskellQuotes  library js   import: common@@ -45,6 +55,7 @@   hs-source-dirs: src   exposed-modules:     Language.Javascript.JSaddle.Wasm+    Language.Javascript.JSaddle.Wasm.TH    other-modules:     Language.Javascript.JSaddle.Wasm.Internal@@ -53,14 +64,22 @@     base >=4.16 && <5,     jsaddle ^>=0.9,     jsaddle-wasm:js,+    template-haskell >=2.20 && <2.24,    if arch(wasm32)     build-depends:       aeson >=2 && <2.3,       bytestring >=0.11 && <0.13,       ghc-experimental ^>=0.1 || >=9.1000 && <9.1300,+      parser-regex ^>=0.3,       stm ^>=2.5, +    other-modules:+      Language.Javascript.JSaddle.Wasm.Internal.TH+     hs-source-dirs: src-wasm++    if flag(eval-via-jsffi)+      cpp-options: -DEVAL_VIA_JSFFI   else     hs-source-dirs: src-native
src-wasm/Language/Javascript/JSaddle/Wasm/Internal.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE TemplateHaskell #-}+ module Language.Javascript.JSaddle.Wasm.Internal   ( run,     runWorker,@@ -19,6 +21,7 @@ import Language.Javascript.JSaddle.Run (runJavaScript) import Language.Javascript.JSaddle.Run.Files qualified as JSaddle.Files import Language.Javascript.JSaddle.Types (Batch, JSM, Results)+import Language.Javascript.JSaddle.Wasm.Internal.TH qualified as JSaddle.Wasm.TH  -- Note: It is also possible to implement this succinctly on top of 'runWorker' -- and 'jsaddleScript' (using MessageChannel), but then e.g.@@ -37,27 +40,31 @@    let jsaddleRunner :: JSVal -> JSVal -> IO ()       jsaddleRunner processResultCallback processResultSyncCallback = do-        s <--          byteStringToJSString . BLC8.toStrict . BLC8.unlines $-            [ JSaddle.Files.initState,-              "(async () => {",-              "  while (true) {",-              "    const batch = JSON.parse(await readBatch());",-              JSaddle.Files.runBatch-                (\r -> "processResult(JSON.stringify(" <> r <> "));")-                (Just \r -> "JSON.parse(processResultSync(JSON.stringify(" <> r <> ")))"),-              "  }",-              "})();"-            ]+        let eval :: JSVal -> JSVal -> JSVal -> IO ()+            eval =+              $( do+                   let s =+                         BLC8.unlines $+                           [ JSaddle.Files.initState,+                             "var syncDepth = 0;",+                             "(async () => {",+                             "  while (true) {",+                             "    const batch = JSON.parse(await $3());",+                             JSaddle.Files.runBatch+                               (\r -> "$1(JSON.stringify(" <> r <> "));")+                               (Just \r -> "JSON.parse($2(JSON.stringify(" <> r <> ")))"),+                             "  }",+                             "})();"+                           ]+                   JSaddle.Wasm.TH.eval (BLC8.unpack s) (replicate 3 [t|JSVal|])+               )         evaluate-          =<< js_eval_jsaddle-            s+          =<< eval             processResultCallback             processResultSyncCallback             readBatchCallback -  -- The GHCJS helpers need to be available in the global scope.-  js_global_eval =<< byteStringToJSString (BLC8.toStrict JSaddle.Files.ghcjsHelpers)+  $(JSaddle.Wasm.TH.eval JSaddle.Wasm.TH.patchedGhcjsHelpers [])    runHelper entryPoint sendOutgoingMessage jsaddleRunner @@ -113,11 +120,6 @@ foreign import javascript "wrapper sync" mkSyncCallback :: (JSString -> IO JSString) -> IO JSVal  foreign import javascript "wrapper" mkPushCallback :: IO JSString -> IO JSVal--foreign import javascript unsafe "eval.call(globalThis, $1)" js_global_eval :: JSString -> IO ()--foreign import javascript safe "new Function('processResult','processResultSync','readBatch',`(()=>{${$1}})()`)($2, $3, $4)"-  js_eval_jsaddle :: JSString -> JSVal -> JSVal -> JSVal -> IO ()  -- Worker 
+ src-wasm/Language/Javascript/JSaddle/Wasm/Internal/TH.hs view
@@ -0,0 +1,63 @@+module Language.Javascript.JSaddle.Wasm.Internal.TH+  ( eval,+    patchedGhcjsHelpers,+  )+where++import Control.Applicative (asum, many)+import Data.ByteString.Lazy.Char8 qualified as BLC8+import Language.Haskell.TH qualified as TH+import Language.Haskell.TH.Syntax qualified as TH+import Language.Javascript.JSaddle.Run.Files qualified as JSaddle.Files+import Regex.List qualified as Re++eval :: String -> [TH.Q TH.Type] -> TH.Q TH.Exp+eval jsChunk argTys = do+  ffiImportName <- TH.newName . show =<< TH.newName "wasm_ffi_import_eval"+  sig <- mkSig argTys+  let ffiImport =+        TH.ForeignD $+          TH.ImportF+            TH.JavaScript+            TH.Safe+            jsChunk+            ffiImportName+            sig+  TH.addTopDecls [ffiImport]+  TH.varE ffiImportName+  where+    mkSig = \case+      [] -> [t|IO ()|]+      t : ts -> [t|$t -> $(mkSig ts)|]++-- | The JSaddle GHCJS helpers need to be available in the global scope.+-- Usually, this is done by evaluating them in a global scope; however, we want+-- to avoid JS eval (due to CSP, see+-- https://github.com/tweag/ghc-wasm-miso-examples/issues/33), so we instead use+-- a hack, namely transforming+--+-- > function foo(a) {+-- >   return a + 1;+-- > }+--+-- into+--+-- > globalThis["foo"] = function(a) {+-- >   return a + 1;+-- > }+--+-- Of course, this only works because of the very particular structure of+-- 'ghcjsHelpers'; but it changes very rarely (didn't change non-trivially in+-- the last 10 years), so this seems acceptable.+patchedGhcjsHelpers :: String+patchedGhcjsHelpers =+  Re.replaceAll re $ BLC8.unpack JSaddle.Files.ghcjsHelpers+  where+    re :: Re.RE Char String+    re =+      asum+        [ f <$> (Re.list "function " *> many (Re.satisfy (/= '('))),+          "\n};" <$ Re.list "\n}"+        ]+      where+        f name = "globalThis[" <> show name <> "] = function"
+ src/Language/Javascript/JSaddle/Wasm/TH.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE CPP #-}++-- | Utilities for evaluating (at runtime) JS code that is known at compile time+-- via TemplateHaskell /without/ relying on JS @eval@.+--+-- For niche use cases, generating JSaddle-based 'JSaddle.eval' instead is+-- possible by disabling the @eval-via-jsffi@ Cabal flag.+--+-- For convenience, on non-Wasm GHCs, the semantics of having @eval-via-jsffi@+-- disabled are used.+module Language.Javascript.JSaddle.Wasm.TH where++import Language.Haskell.TH qualified as TH+import Language.Javascript.JSaddle qualified as JSaddle+#ifdef EVAL_VIA_JSFFI+import Control.Monad.IO.Class (liftIO)+import Language.Javascript.JSaddle.Wasm.Internal.TH qualified as Internal+#else+import Data.Functor (void)+#endif++-- | Generate an expression that, when called, evaluates the given chunk of JS+-- code. Additionally, a list of argument types can be specified.+--+-- For example,+--+-- > $(eval "console.log('hi')") :: JSM ()+--+-- will print \"hi\" to the console when executed.+--+-- Internally, this generates the following code:+--+--  * If the Cabal flag @eval-via-jsffi@ is enabled (the default): An+--    appropriate safe Wasm JSFFI import.+--+--  * If @eval-via-jsffi@ is disabled: Use JSaddle's 'JSaddle.eval'.+eval :: String -> TH.Q TH.Exp+#if EVAL_VIA_JSFFI+eval chunk = [|liftIO $(Internal.eval chunk []) :: JSaddle.JSM ()|]+#else+eval chunk = [|void $ JSaddle.eval chunk|]+#endif++-- | Like 'eval', but read the JS code to evaluate from a file.+evalFile :: FilePath -> TH.Q TH.Exp+evalFile path = do+  chunk <- TH.runIO $ readFile path+  eval chunk