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 +8/−0
- README.md +6/−0
- jsaddle-wasm.cabal +20/−1
- src-wasm/Language/Javascript/JSaddle/Wasm/Internal.hs +23/−21
- src-wasm/Language/Javascript/JSaddle/Wasm/Internal/TH.hs +63/−0
- src/Language/Javascript/JSaddle/Wasm/TH.hs +48/−0
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