├── .github ├── settings.yml └── workflows │ └── pipeline.yml ├── .gitignore ├── NOTES.md ├── README.md ├── cabal.project ├── inline-js-core ├── CHANGELOG.md ├── LICENSE ├── README.md ├── inline-js-core.cabal ├── jsbits │ └── index.js └── src │ └── Language │ └── JavaScript │ └── Inline │ ├── Core.hs │ └── Core │ ├── Class.hs │ ├── Dict.hs │ ├── Exception.hs │ ├── Export.hs │ ├── IPC.hs │ ├── Import.hs │ ├── Instruction.hs │ ├── JSVal.hs │ ├── Message.hs │ ├── NodePath.hs │ ├── NodeVersion.hs │ ├── Session.hs │ └── Utils.hs ├── inline-js-examples ├── CHANGELOG.md ├── LICENSE ├── README.md ├── inline-js-examples.cabal └── src │ └── Language │ └── JavaScript │ └── Inline │ └── Examples │ ├── Stream.hs │ ├── Utils.hs │ ├── Utils │ └── LazyIO.hs │ └── Wasm.hs ├── inline-js-parser ├── index.mjs ├── package-lock.json └── package.json ├── inline-js-tests ├── CHANGELOG.md ├── LICENSE ├── README.md ├── inline-js-tests.cabal ├── jsbits │ └── node_modules │ │ └── left-pad │ │ └── index.js └── test │ ├── Language │ └── JavaScript │ │ └── Inline │ │ └── Tests │ │ └── Utils │ │ └── Aeson.hs │ └── inline-js-tests.hs ├── inline-js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── inline-js.cabal ├── jsbits │ └── main.js └── src │ └── Language │ └── JavaScript │ ├── Inline.hs │ └── Inline │ ├── Aeson.hs │ ├── JSParse.hs │ └── TH.hs ├── nix ├── ci.nix ├── jsbits.nix ├── pkg-set.nix ├── sources.json └── sources.nix ├── shell.nix └── stack.yaml /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | has_wiki: false 3 | 4 | labels: 5 | - name: "duplicate" 6 | color: cfd3d7 7 | - name: "good first issue" 8 | color: 7057ff 9 | - name: "invalid" 10 | color: cfd3d7 11 | - name: "more data needed" 12 | color: bfdadc 13 | - name: "P0" 14 | color: b60205 15 | description: "blocker: fix immediately!" 16 | - name: "P1" 17 | color: d93f0b 18 | description: "critical: next release" 19 | - name: "P2" 20 | color: e99695 21 | description: "major: an upcoming release" 22 | - name: "P3" 23 | color: fbca04 24 | description: "minor: not priorized" 25 | - name: "P4" 26 | color: fef2c0 27 | description: "unimportant: consider wontfix or other priority" 28 | - name: "question" 29 | color: d876e3 30 | - name: "type: bug" 31 | color: 0052cc 32 | - name: "type: documentation" 33 | color: 0052cc 34 | - name: "type: feature request" 35 | color: 0052cc 36 | - name: "wontfix" 37 | color: ffffff 38 | - name: "merge-queue" 39 | color: 0e8a16 40 | description: "merge on green CI" 41 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: pipeline 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | 9 | nix: 10 | name: nix-${{ matrix.os }}-${{ matrix.ghc }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - macos-11 18 | ghc: 19 | - ghc8107 20 | - ghc884 21 | - ghc865 22 | steps: 23 | 24 | - name: checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: setup-nix 28 | uses: cachix/install-nix-action@v14.1 29 | with: 30 | extra_nix_config: | 31 | substituters = https://cache.nixos.org https://hydra.iohk.io 32 | trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= 33 | 34 | - name: setup-cachix 35 | uses: cachix/cachix-action@v10 36 | with: 37 | name: asterius 38 | signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' 39 | 40 | - name: cachix-watch-store 41 | run: | 42 | cachix watch-store -c9 asterius & 43 | 44 | - name: test 45 | run: | 46 | nix-build --keep-going --no-out-link --arg ghcs '["${{ matrix.ghc }}"]' nix/ci.nix 47 | 48 | cabal: 49 | name: cabal-${{ matrix.os }}-${{ matrix.ghc }} 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | os: 55 | - windows-2022 56 | ghc: 57 | - 9.0.1 58 | - 8.10.7 59 | - 8.8.4 60 | - 8.6.5 61 | steps: 62 | 63 | - name: checkout 64 | uses: actions/checkout@v2 65 | 66 | - name: setup-haskell 67 | id: setup-haskell 68 | uses: haskell/actions/setup@v1 69 | with: 70 | ghc-version: ${{ matrix.ghc }} 71 | 72 | - name: cache 73 | uses: actions/cache@v2 74 | with: 75 | key: cabal-${{ matrix.os }}-${{ matrix.ghc }}-${{ github.run_id }} 76 | restore-keys: cabal-${{ matrix.os }}-${{ matrix.ghc }}- 77 | path: | 78 | ${{ steps.setup-haskell.outputs.cabal-store }} 79 | dist-newstyle 80 | 81 | - name: build 82 | run: | 83 | C:\msys64\msys2_shell.cmd -mingw64 -defterm -full-path -here -no-start -c "cabal v2-build all" 84 | 85 | - name: setup-node-16 86 | uses: actions/setup-node@v2 87 | with: 88 | node-version: 16 89 | check-latest: true 90 | 91 | - name: test-node-16 92 | run: | 93 | node --version 94 | C:\msys64\msys2_shell.cmd -mingw64 -defterm -full-path -here -no-start -c "cabal v2-run inline-js-tests -- -j2" 95 | 96 | - name: setup-node-15 97 | uses: actions/setup-node@v2 98 | with: 99 | node-version: 15 100 | check-latest: true 101 | 102 | - name: test-node-15 103 | run: | 104 | node --version 105 | C:\msys64\msys2_shell.cmd -mingw64 -defterm -full-path -here -no-start -c "cabal v2-run inline-js-tests -- -j2" 106 | 107 | - name: setup-node-14 108 | uses: actions/setup-node@v2 109 | with: 110 | node-version: 14 111 | check-latest: true 112 | 113 | - name: test-node-14 114 | run: | 115 | node --version 116 | C:\msys64\msys2_shell.cmd -mingw64 -defterm -full-path -here -no-start -c "cabal v2-run inline-js-tests -- -j2" 117 | 118 | - name: setup-node-13 119 | uses: actions/setup-node@v2 120 | with: 121 | node-version: 13 122 | check-latest: true 123 | 124 | - name: test-node-13 125 | run: | 126 | node --version 127 | C:\msys64\msys2_shell.cmd -mingw64 -defterm -full-path -here -no-start -c "cabal v2-run inline-js-tests -- -j2" 128 | 129 | - name: setup-node-12 130 | uses: actions/setup-node@v2 131 | with: 132 | node-version: 12 133 | check-latest: true 134 | 135 | - name: test-node-12 136 | run: | 137 | node --version 138 | C:\msys64\msys2_shell.cmd -mingw64 -defterm -full-path -here -no-start -c "cabal v2-run inline-js-tests -- -j2" 139 | 140 | - name: setup-node-10 141 | uses: actions/setup-node@v2 142 | with: 143 | node-version: 10 144 | check-latest: true 145 | 146 | - name: test-node-10 147 | run: | 148 | node --version 149 | C:\msys64\msys2_shell.cmd -mingw64 -defterm -full-path -here -no-start -c "cabal v2-run inline-js-tests -- -j2" 150 | 151 | sdist-haddock: 152 | name: sdist-haddock 153 | runs-on: ubuntu-latest 154 | steps: 155 | 156 | - name: checkout 157 | uses: actions/checkout@v2 158 | 159 | - name: setup-haskell 160 | id: setup-haskell 161 | uses: haskell/actions/setup@v1 162 | with: 163 | ghc-version: 8.10.7 164 | 165 | - name: cache 166 | uses: actions/cache@v2 167 | with: 168 | key: sdist-haddock-${{ github.run_id }} 169 | restore-keys: sdist-haddock- 170 | path: | 171 | ${{ steps.setup-haskell.outputs.cabal-store }} 172 | dist-newstyle 173 | 174 | - name: check 175 | run: | 176 | for pkg in inline-js inline-js-core inline-js-examples inline-js-tests; do 177 | pushd $pkg 178 | cabal check 179 | popd 180 | done 181 | 182 | - name: sdist 183 | run: | 184 | cabal v2-sdist all 185 | 186 | - name: sdist-artifact 187 | uses: actions/upload-artifact@v2 188 | with: 189 | name: sdist 190 | path: dist-newstyle/sdist/*.tar.gz 191 | 192 | - name: haddock 193 | run: | 194 | cabal v2-haddock \ 195 | --enable-documentation \ 196 | --haddock-for-hackage \ 197 | --haddock-hyperlink-source \ 198 | --haddock-quickjump \ 199 | all 200 | 201 | - name: haddock-artifact 202 | uses: actions/upload-artifact@v2 203 | with: 204 | name: haddock 205 | path: dist-newstyle/*-docs.tar.gz 206 | 207 | docs: 208 | name: docs 209 | runs-on: ubuntu-latest 210 | env: 211 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 212 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 213 | steps: 214 | 215 | - name: checkout 216 | uses: actions/checkout@v2 217 | 218 | - name: setup-haskell 219 | id: setup-haskell 220 | uses: haskell/actions/setup@v1 221 | with: 222 | enable-stack: true 223 | stack-no-global: true 224 | 225 | - name: cache 226 | uses: actions/cache@v2 227 | with: 228 | key: docs-${{ github.run_id }} 229 | restore-keys: docs- 230 | path: | 231 | ${{ steps.setup-haskell.outputs.stack-root }} 232 | **/.stack-work 233 | 234 | - name: build 235 | run: | 236 | stack build --haddock 237 | 238 | if [ ! -z "$NETLIFY_AUTH_TOKEN" ] 239 | then 240 | if [ $(git rev-parse --abbrev-ref HEAD) = "master" ] 241 | then 242 | netlify deploy --dir $(stack path --local-doc-root) --message "$GITHUB_REF-$GITHUB_SHA" --prod 243 | else 244 | netlify deploy --dir $(stack path --local-doc-root) --message "$GITHUB_REF-$GITHUB_SHA" 245 | fi 246 | fi 247 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv/ 2 | .ghc.environment* 3 | .stack-work/ 4 | .vscode/ 5 | dist/ 6 | dist-newstyle/ 7 | node_modules 8 | stack.yaml.lock 9 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # inline-js implementation notes 2 | 3 | ## Rationale 4 | 5 | Asterius implements Template Haskell in a similar manner to GHCJS: running 6 | compiled code in a `node` process. Simplest way to do it would be calling `node` 7 | with each TH splice evaluation, passing input/output via `stdin` / `stdout`,but 8 | we thought that an `inline-*` style library that enables more principled 9 | interaction with the JavaScript world is a nice yak to shave, hence the creation 10 | of `inline-js`. 11 | 12 | The primary user of `inline-js` is Asterius, which will be refactored as a 13 | proper GHC cross compilation backend in the future. This restricts our technical 14 | choices: 15 | 16 | - To link against `node`, or perform IPC? The former has much higher efficiency, 17 | and makes it easier for Haskell & JavaScript heap to coordinate, but will 18 | significantly complicate the build system of `inline-js`. So we favor IPC 19 | here. 20 | - How is IPC performed? Over sockets or what? Do we conform to certain 21 | standards, like protobuf or anything like that? Since we need to guarantee 22 | simplicity of implementation, we can't use a well-established IPC standard 23 | here, since they often involve code generation and further complicates the 24 | build system. We only use `stdin` / `stdout` of the `node` process, since it's 25 | the only portable way that works with all major platforms; a very simple 26 | custom binary serialization protocol is used, since the messages to be 27 | exchanged is fully known beforehand, we don't need to address some common 28 | issues related to IPC messages. 29 | - What Haskell dependencies can we bring in? They'll become "boot libs" of GHC, 30 | so `aeson` and friends would increase dependency footprint of Asterius, making 31 | it much harder to be merged back to GHC HQ. So we split the project into two 32 | layers: the `inline-js-core` package, which only depends on existing GHC boot 33 | libs, have basic `node` interop logic to be used by Asterius, and the 34 | `inline-js` package which re-exports `inline-js-core`, while adding `aeson` 35 | support and is intended to be used by non-Asterius Haskell projects. 36 | - How to lex/parse JavaScript? This is needed when implementing the TH 37 | quasiquoters in `inline-js`. We used to use `language-javascript`, but 38 | JavaScript is a complex and evolving language, and the Haskell parser has a 39 | lot of rough edges. So now we use a JavaScript parser which is already 40 | battle-tested in the JavaScript ecosystem; there's plenty to choose from, and 41 | our current choice is `acorn`. 42 | 43 | ## Package structure 44 | 45 | - `inline-js-core`: ships a hand-written JavaScript file to be executed by 46 | `node`, and contains `Language.JavaScript.Inline.Core*` which enables `node` 47 | interop 48 | - `inline-js`: ships a compiled JavaScript file which implements parsing logic, 49 | and contains `Language.JavaScript.Inline` to be used by regular projects 50 | - `inline-js-examples`: they're supposed to be self-documenting modules to 51 | demonstrate various ways of using `inline-js` 52 | - `inline-js-tests`: a unified test suite implementing all unit tests for the 53 | project. 54 | 55 | ## The nodejs eval server 56 | 57 | `inline-js-core/jsbits/index.js` is a self-contained JavaScript file to be run 58 | by `node` when a `Session` starts. It parses messages from `stdin`, handles 59 | them, and when reply messages are generated (there isn't a 1-to-1 mapping from 60 | input to output messages!), the messages are written to `stdout`. It exits the 61 | process when a shutdown message is received. 62 | 63 | The message format is defined at `Language.JavaScript.Inline.Core.Message`. The 64 | `MessageHS` type is sent to `node` from Haskell, and `MessageJS` the other way. 65 | The primary `MessageHS` request variants are: 66 | 67 | - `JSEvalRequest`: we need to run some JavaScript code, so send the "code" 68 | (we'll explain what "code" means later) tagged with a unique id and its type 69 | information. 70 | - `JSValFree`: when we detect that a `JSVal` (Haskell reference to a JavaScript 71 | value) is unreachable, we send this to inform the eval server to drop its 72 | reference on the JavaScript side as well. This doesn't necessarily result in 73 | removal of that value on the JavaScript heap though, since it may be 74 | referenced elsewhere. 75 | - `HSExportRequest`: we support exporting Haskell functions to the JavaScript 76 | world, and they appear as ordinary JavaScript functions on the JavaScript 77 | side! `JSVal`s are normally eval results of `JSEvalRequest`, but reference to 78 | an exported Haskell function needs special handling, hence this request. 79 | - `HSEvalResponse`: the JavaScript world may attempt to call Haskell functions, 80 | which will send `HSEvalRequest` messages back (it's a `MessageJS` variant). 81 | When Haskell evaluation completes, this message will be sent back to the 82 | JavaScript world. 83 | 84 | ## The type system 85 | 86 | In `Language.JavaScript.Inline.Core.Class`, we define two type classes: 87 | 88 | - `ToJS`: whatever that can be sent to the JavaScript world, be it a raw buffer, 89 | a string, a JSON value, or a `JSVal`, they are `ToJS` instances. 90 | - `FromJS`: whatever that can be sent from the JavaScript world to Haskell, is a 91 | `FromJS` instance. 92 | 93 | `ToJS` is fairly simple: it marshals the input to a `JSExpr`. Conceptually, the 94 | `JSExpr` is a JavaScript expression that can be concated, and it is the 95 | `JSEvalRequest` payload. It's not simply a UTF-8 string though, since we want to 96 | efficiently embed raw buffers and other stuff, so `JSExpr` is a list of 97 | `JSExprSegment` which can be different things; the `toJS()` method in the eval 98 | server script will perform decoding of the serialized `JSExpr`. 99 | 100 | Before we talk about `FromJS`, we need to classify what kinds of info we want to 101 | retrieve from the JavaScript world as eval results: 102 | 103 | - Values: buffers, strings, JSONs, etc. These things can be fully serialized to 104 | pass back, no need to care about lifetime issues or whatever. As a special 105 | case, `undefined` or `()` also belongs to this category, which means only 106 | execute side effects on the JavaScript side and don't pass anything back. 107 | - References: it can be an arbitrary JavaScript value, so it may reference 108 | something which can't be serialized, like handles to system resources. They 109 | need special handling: 110 | - The eval server maintains a mapping from integers to JavaScript values. 111 | The key is the unique identifier to be sent between Haskell/JavaScript. 112 | Since the JavaScript side doesn't know about the Haskell heap, it has to 113 | explicitly maintain these references unless explicitly told to drop one. 114 | - The Haskell side has a special `JSVal` type which models these references. 115 | They have attached finalizers that sends `JSValFree` messages back when 116 | being garbage collected. 117 | 118 | `Language.JavaScript.Inline.Core.Message` implements `RawJSType` which 119 | classifies different kinds of JavaScript things, and it is used by the `FromJS` 120 | class. The eval server script also uses this simple type system to decide how to 121 | decode/encode things. 122 | 123 | ## Evaluating JavaScript from Haskell 124 | 125 | What happens when we evaluate a piece of JavaScript? 126 | 127 | - Given a `Session`, we can `eval` a `JSExpr` (remember the `ToJS` class yet?) 128 | It'll create the `JSEvalRequest` payload, assign it with a unique id, and send 129 | it to `node`. 130 | - In the eval server, the `MainContext` class runs in the main thread, and the 131 | `recvLoop` method will intercept the message, then redirect it to the worker. 132 | - The `WorkerContext` class runs in a worker thread, and the parent thread sends 133 | it a message, to be handled by the `handleParentMessage` method. 134 | - The evaluation may be asynchronous; so we attach handlers to the `Promise` 135 | that represents eval result, and when eval completes either successfully or 136 | with error, the handlers will construct the reply message, to be sent back to 137 | the main thread, then sent to `stdout` of the `node` process. 138 | - On the Haskell side, there's a forked thread which polls the `stdout` of the 139 | `node` process and when it's `JSEvalResponse`, the response message will 140 | contain the unique id of the eval request. The id is in fact a `StablePtr` of 141 | a Haskell `TMVar`, and it's used to get the `TMVar` back and fulfill it. The 142 | `eval` call in the beginning returns a `IO r` continuation, which attempts to 143 | fetch result from that `TMVar`, and it'll block when the result isn't yet 144 | available. 145 | - The `IO r` continuation uses lazy I/O; the returned `r` value is a thunk and 146 | the blocking behavior happens only when forcing the thunk. Normally lazy I/O 147 | is an antipattern, but here it allows us to utilize JavaScript concurrency 148 | while simplifying the eval API: 149 | - We want to be able to issue a bunch of eval requests first, and collect 150 | the results later, without needing to fork a Haskell thread for each 151 | request. 152 | - If we don't use laziness here, we need two distinct APIs: one for 153 | returning the actual result value, another one for returning a handle for 154 | later retrieval of the result. 155 | - It's possible that evaluation will fail, resulting in a rejected `Promise`. In 156 | that case, the serialized JavaScript error message is sent back, and the `IO 157 | r` continuation will rethrow it as a Haskell exception. 158 | - It's also possible that a critical error happens at the `node` side. We do our 159 | best effort to send a `FatalError` message back, and the Haskell receive 160 | thread will fulfill a special `TMVar` with the fatal error. All pending `IO r` 161 | continuations will throw, since their STM computation will first attempt to 162 | poll the fatal error inbox, and then the individual message inbox. 163 | 164 | ## Evaluating Haskell from JavaScript 165 | 166 | What happens when we attempt to export Haskell functions to JavaScript: 167 | 168 | - Given a `Session`, we can `export` a Haskell function and return the 169 | corresponding `JSVal`, if the function type is a `Export` instance. Users 170 | shouldn't handwrite `Export` instances; for the function type `a -> b -> .. -> 171 | IO r`, if arguments `a`, `b`, etc are `FromJS` instances, and `r` is `ToJS` 172 | instance, then it can be exported. 173 | - An `HSExportRequest` is constructed and sent to the `node` side. In addition 174 | to the id associated with the request, there's also an id associated with the 175 | exported Haskell function, which will be used by the JavaScript side as a 176 | reference to the Haskell heap. 177 | - The eval server receives the request. It uses the type information in the 178 | request to generate a JavaScript function that, when called, will send 179 | `HSEvalRequest` back to Haskell and waits for Haskell's eval response. The 180 | function is then encapsulated as a `JSVal` to be sent back to Haskell. 181 | 182 | What happens when that JavaScript function is called: 183 | 184 | - The Haskell function can be exported as either an async or sync JavaScript 185 | function. The async way is recommended. When the JavaScript function is 186 | called, it queries the previously cached type information to decide how to 187 | marshal the arguments and construct the `HSEvalRequest` payload. The request 188 | is sent, and the function returns a `Promise` which fulfills or rejects when 189 | the corresponding `HSEvalResponse` is sent back. 190 | - In some cases, you really really want the JavaScript function to be a sync 191 | one: for instance, WebAssembly import functions can't be async. In that case, 192 | the JavaScript function will block until the result is available. The blocking 193 | mechanism is implemented by shared buffers and atomics, and that's the only 194 | reason why we need the main/worker thread distinction in the eval server. It's 195 | a very heavy hammer, slow and non-reentrant. 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `inline-js`: Call JavaScript from Haskell, and vice versa! 2 | 3 | [![GitHub Actions](https://github.com/tweag/inline-js/workflows/pipeline/badge.svg?branch=master)](https://github.com/tweag/inline-js/actions?query=branch%3Amaster) 4 | [![Gitter](https://img.shields.io/gitter/room/tweag/inline-js)](https://gitter.im/tweag/inline-js) 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/b2320ec2-8feb-44d6-886a-8cd4728d92ad/deploy-status)](https://inline-js.netlify.app) 6 | 7 | ## Documentation 8 | 9 | ### Haddock 10 | 11 | [Haddock documentation](https://inline-js.netlify.app) for HEAD is available. 12 | 13 | ### Adding `inline-js` as a dependency 14 | 15 | #### `cabal` 16 | 17 | Add a `source-repository-package` in the `cabal.project` file, see 18 | [documentation](https://cabal.readthedocs.io/en/latest/cabal-project.html#specifying-packages-from-remote-version-control-locations) 19 | for details. 20 | 21 | #### `stack` 22 | 23 | Add an `extra-deps` entry in the `stack.yaml` file, see 24 | [documentation](https://docs.haskellstack.org/en/stable/yaml_configuration/#extra-deps) 25 | for details. 26 | 27 | #### `nix` 28 | 29 | Here's an example overlay. 30 | 31 | ```nix 32 | pkgsSelf: pkgsSuper: 33 | let 34 | src = pkgsSelf.fetchFromGitHub { 35 | owner = "tweag"; 36 | repo = "inline-js"; 37 | rev = ""; 38 | sha256 = pkgsSelf.lib.fakeSha256; 39 | }; 40 | in 41 | { 42 | haskellPackages = pkgsSuper.haskellPackages.override { 43 | overrides = self: _: { 44 | inline-js-core = (self.callCabal2nixWithOptions "inline-js-core" src 45 | "--subpath inline-js-core" 46 | { }).overrideAttrs (_: { 47 | preBuild = '' 48 | substituteInPlace src/Language/JavaScript/Inline/Core/NodePath.hs --replace '"node"' '"${pkgsSelf.nodejs-16_x}/bin/node"' 49 | ''; 50 | }); 51 | inline-js = 52 | self.callCabal2nixWithOptions "inline-js" src "--subpath inline-js" { }; 53 | }; 54 | }; 55 | } 56 | ``` 57 | 58 | #### `haskell.nix` 59 | 60 | See 61 | [documentation](https://input-output-hk.github.io/haskell.nix/tutorials/source-repository-hashes) 62 | for details. 63 | 64 | ## Implemented features 65 | 66 | - Manage `node` sessions which run the eval server script for Haskell/JavaScript 67 | interop 68 | - Evaluate expressions with `require()`/`import()` support 69 | - Export Haskell functions to async/sync JavaScript functions 70 | - Load third party libraries in user-specified `node_modules` 71 | - Garbage-collected `JSVal` references in Haskell 72 | - Support `Promise`-based async evaluation 73 | - Type classes for Haskell/JavaScript data marshaling, with `aeson` support 74 | - Template Haskell QuasiQuoters for lifting Haskell variables into JavaScript 75 | code 76 | - Doesn't require `node` native addon or third party libraries 77 | 78 | ## Planned features 79 | 80 | - Integrate with TypeScript compiler, generate Haskell code from TypeScript 81 | `.d.ts` code 82 | - Integrate with headless browser testing frameworks like 83 | `playwright`/`puppeteer` for running JavaScript in browsers 84 | 85 | ## Supported versions 86 | 87 | Supported GHC versions: 88 | 89 | - `ghc-8.6`, tested with `ghc-8.6.5` 90 | - `ghc-8.8`, tested with `ghc-8.8.4` 91 | - `ghc-8.10`, tested with `ghc-8.10.7` 92 | - `ghc-9.0`, tested with `ghc-9.0.1` 93 | 94 | Supported platforms: 95 | 96 | - Windows 10 x64, tested with Windows Server 2019 97 | - Linux x64, tested with Ubuntu 20.04 98 | - macOS x64, tested with macOS 10.15 99 | 100 | Supported `node` versions: 101 | 102 | - `node-v10`, minimum `v10.20.0` 103 | - `node-v12` and later 104 | 105 | See the [CI 106 | config](https://github.com/tweag/inline-js/blob/master/.github/workflows/pipeline.yml) 107 | for details. 108 | 109 | ## Contributors 110 | 111 | [](https://tweag.io) 112 | 113 | `inline-js` is maintained by [Tweag I/O](https://tweag.io/). 114 | 115 | Have questions? Need help? Tweet at [@tweagio](https://twitter.com/tweagio). 116 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | inline-js 3 | inline-js-core 4 | inline-js-examples 5 | inline-js-tests 6 | 7 | tests: True 8 | 9 | package aeson 10 | flags: +cffi 11 | 12 | package hashable 13 | flags: +random-initial-seed 14 | 15 | index-state: 2021-10-15T00:00:00Z 16 | -------------------------------------------------------------------------------- /inline-js-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for `inline-js-core` 2 | -------------------------------------------------------------------------------- /inline-js-core/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 EURL Tweag 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of Tweag I/O nor the names of other contributors may be 16 | used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /inline-js-core/README.md: -------------------------------------------------------------------------------- 1 | # `inline-js-core` 2 | -------------------------------------------------------------------------------- /inline-js-core/inline-js-core.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: inline-js-core 3 | version: 0.0.1.0 4 | synopsis: Call JavaScript from Haskell. 5 | description: 6 | Please see for details. 7 | 8 | category: Web 9 | homepage: https://github.com/tweag/inline-js#readme 10 | bug-reports: https://github.com/tweag/inline-js/issues 11 | maintainer: Cheng Shao 12 | copyright: (c) 2018 Tweag I/O 13 | license: BSD-3-Clause 14 | license-file: LICENSE 15 | build-type: Simple 16 | extra-source-files: 17 | CHANGELOG.md 18 | jsbits/index.js 19 | LICENSE 20 | README.md 21 | 22 | source-repository head 23 | type: git 24 | location: https://github.com/tweag/inline-js 25 | 26 | library 27 | exposed-modules: Language.JavaScript.Inline.Core 28 | other-modules: 29 | Language.JavaScript.Inline.Core.Class 30 | Language.JavaScript.Inline.Core.Dict 31 | Language.JavaScript.Inline.Core.Exception 32 | Language.JavaScript.Inline.Core.Export 33 | Language.JavaScript.Inline.Core.Import 34 | Language.JavaScript.Inline.Core.Instruction 35 | Language.JavaScript.Inline.Core.IPC 36 | Language.JavaScript.Inline.Core.JSVal 37 | Language.JavaScript.Inline.Core.Message 38 | Language.JavaScript.Inline.Core.NodePath 39 | Language.JavaScript.Inline.Core.NodeVersion 40 | Language.JavaScript.Inline.Core.Session 41 | Language.JavaScript.Inline.Core.Utils 42 | 43 | hs-source-dirs: src 44 | ghc-options: -Wall 45 | build-depends: 46 | , base >=4.12 && <5 47 | , binary 48 | , bytestring 49 | , Cabal 50 | , containers 51 | , directory 52 | , filepath 53 | , ghc-prim 54 | , process 55 | , stm 56 | , template-haskell 57 | , text 58 | 59 | default-language: Haskell2010 60 | -------------------------------------------------------------------------------- /inline-js-core/jsbits/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs").promises; 4 | const path = require("path"); 5 | const string_decoder = require("string_decoder"); 6 | const util = require("util"); 7 | const vm = require("vm"); 8 | const worker_threads = require("worker_threads"); 9 | 10 | class JSValContext { 11 | constructor() { 12 | this.jsvalMap = new Map(); 13 | this.jsvalLast = 0n; 14 | } 15 | 16 | new(x) { 17 | const i = ++this.jsvalLast; 18 | this.jsvalMap.set(i, x); 19 | return i; 20 | } 21 | 22 | get(i) { 23 | if (!this.jsvalMap.has(i)) { 24 | throw new Error(`jsval.get(${i}): invalid key`); 25 | } 26 | return this.jsvalMap.get(i); 27 | } 28 | 29 | free(i) { 30 | if (!this.jsvalMap.delete(i)) { 31 | throw new Error(`jsval.free(${i}): invalid key`); 32 | } 33 | 34 | if (i === this.jsvalLast) { 35 | if (this.jsvalMap.size > 0) { 36 | --this.jsvalLast; 37 | while (!this.jsvalMap.has(this.jsvalLast)) { 38 | --this.jsvalLast; 39 | } 40 | } else { 41 | this.jsvalLast = 0n; 42 | } 43 | } 44 | } 45 | 46 | clear() { 47 | this.jsvalMap.clear(); 48 | this.jsvalLast = 0n; 49 | } 50 | } 51 | 52 | class MainContext { 53 | constructor() { 54 | process.on("uncaughtException", (err) => this.onUncaughtException(err)); 55 | const exportSyncArrayBuffer = new SharedArrayBuffer( 56 | 8 + 57 | Number.parseInt(process.env.INLINE_JS_EXPORT_SYNC_BUFFER_SIZE, 10) * 58 | 0x100000 59 | ); 60 | this.exportSyncFlag = new Int32Array(exportSyncArrayBuffer, 0, 1); 61 | this.exportSyncBuffer = Buffer.from(exportSyncArrayBuffer, 4); 62 | this.worker = new worker_threads.Worker(__filename, { 63 | stdout: true, 64 | workerData: exportSyncArrayBuffer, 65 | }); 66 | this.worker.on("message", (buf_msg) => this.onWorkerMessage(buf_msg)); 67 | this.recvLoop(); 68 | } 69 | 70 | recvLoop() { 71 | let buf = Buffer.allocUnsafe(0); 72 | process.stdin.on("data", (c) => { 73 | buf = Buffer.concat([buf, c]); 74 | while (true) { 75 | if (buf.length < 8) return; 76 | const len = Number(buf.readBigUInt64LE(0)); 77 | if (buf.length < 8 + len) return; 78 | const buf_msg = buf.slice(8, 8 + len); 79 | buf = buf.slice(8 + len); 80 | 81 | if (buf_msg.readUInt8(0) === 4) { 82 | process.stdin.unref(); 83 | } 84 | 85 | Atomics.wait(this.exportSyncFlag, 0, 2); 86 | const export_sync_flag = Atomics.load(this.exportSyncFlag, 0); 87 | 88 | if (export_sync_flag === 1) { 89 | this.exportSyncBuffer.writeUInt32LE(buf_msg.length, 0); 90 | buf_msg.copy(this.exportSyncBuffer, 4); 91 | Atomics.store(this.exportSyncFlag, 0, 2); 92 | Atomics.notify(this.exportSyncFlag, 0, 1); 93 | continue; 94 | } 95 | 96 | this.worker.postMessage(buf_msg); 97 | } 98 | }); 99 | } 100 | 101 | send(buf_msg) { 102 | return new Promise((resolve, reject) => { 103 | const buf_send = Buffer.allocUnsafe(8 + buf_msg.length); 104 | buf_send.writeBigUInt64LE(BigInt(buf_msg.length)); 105 | buf_msg.copy(buf_send, 8); 106 | process.stdout.write(buf_send, (err) => (err ? reject(err) : resolve())); 107 | }); 108 | } 109 | 110 | onWorkerMessage(buf_msg) { 111 | this.send(bufferFromArrayBufferView(buf_msg)); 112 | } 113 | 114 | onUncaughtException(err) { 115 | const err_str = `${err.stack ? err.stack : err}`; 116 | const err_buf = Buffer.from(err_str, "utf-8"); 117 | const resp_buf = Buffer.allocUnsafe(9 + err_buf.length); 118 | resp_buf.writeUInt8(2, 0); 119 | resp_buf.writeBigUInt64LE(BigInt(err_buf.length), 1); 120 | err_buf.copy(resp_buf, 9); 121 | this.send(resp_buf).finally(() => process.exit(1)); 122 | } 123 | } 124 | 125 | class WorkerContext { 126 | constructor() { 127 | const exportSyncArrayBuffer = worker_threads.workerData; 128 | this.exportSyncFlag = new Int32Array(exportSyncArrayBuffer, 0, 1); 129 | this.exportSyncBuffer = Buffer.from(exportSyncArrayBuffer, 4); 130 | this.decoder = new string_decoder.StringDecoder("utf-8"); 131 | this.jsval = new JSValContext(); 132 | this.hsCtx = new JSValContext(); 133 | (async () => { 134 | if (process.env.INLINE_JS_NODE_MODULES) { 135 | await fs.symlink( 136 | process.env.INLINE_JS_NODE_MODULES, 137 | path.join(__dirname, "node_modules"), 138 | "dir" 139 | ); 140 | } 141 | worker_threads.parentPort.on("message", (buf_msg) => 142 | this.onParentMessage(buf_msg) 143 | ); 144 | })(); 145 | } 146 | 147 | toJS(buf, p) { 148 | const jsval_tmp = []; 149 | const expr_segs_len = Number(buf.readBigUInt64LE(p)); 150 | p += 8; 151 | let expr = ""; 152 | let has_code = false; 153 | for (let i = 0; i < expr_segs_len; ++i) { 154 | const expr_seg_type = buf.readUInt8(p); 155 | p += 1; 156 | switch (expr_seg_type) { 157 | case 0: { 158 | // Code 159 | const expr_seg_len = Number(buf.readBigUInt64LE(p)); 160 | p += 8; 161 | expr = `${expr}${this.decoder.end(buf.slice(p, p + expr_seg_len))}`; 162 | p += expr_seg_len; 163 | has_code = has_code || Boolean(expr_seg_len); 164 | break; 165 | } 166 | 167 | case 1: { 168 | // BufferLiteral 169 | const buf_len = Number(buf.readBigUInt64LE(p)); 170 | p += 8; 171 | const buf_id = jsval_tmp.push(buf.slice(p, p + buf_len)) - 1; 172 | expr = `${expr}__${buf_id}`; 173 | p += buf_len; 174 | break; 175 | } 176 | 177 | case 2: { 178 | // StringLiteral 179 | const buf_len = Number(buf.readBigUInt64LE(p)); 180 | p += 8; 181 | const str_id = 182 | jsval_tmp.push(this.decoder.end(buf.slice(p, p + buf_len))) - 1; 183 | expr = `${expr}__${str_id}`; 184 | p += buf_len; 185 | break; 186 | } 187 | 188 | case 3: { 189 | // JSONLiteral 190 | const buf_len = Number(buf.readBigUInt64LE(p)); 191 | p += 8; 192 | const json_id = 193 | jsval_tmp.push( 194 | JSON.parse(this.decoder.end(buf.slice(p, p + buf_len))) 195 | ) - 1; 196 | expr = `${expr}__${json_id}`; 197 | p += buf_len; 198 | break; 199 | } 200 | 201 | case 4: { 202 | // JSValLiteral 203 | const jsval_id = 204 | jsval_tmp.push(this.jsval.get(buf.readBigUInt64LE(p))) - 1; 205 | expr = `${expr}__${jsval_id}`; 206 | p += 8; 207 | break; 208 | } 209 | 210 | default: { 211 | throw new Error(`toJS failed: ${buf}`); 212 | } 213 | } 214 | } 215 | 216 | let result; 217 | 218 | if (!has_code && jsval_tmp.length === 1) { 219 | result = jsval_tmp[0]; 220 | } else { 221 | let expr_params = "require"; 222 | for (let i = 0; i < jsval_tmp.length; ++i) { 223 | expr_params = `${expr_params}, __${i}`; 224 | } 225 | expr = `(${expr_params}) => (\n${expr}\n)`; 226 | result = vm.runInThisContext(expr, { 227 | lineOffset: -1, 228 | importModuleDynamically: (spec) => import(spec), 229 | })(require, ...jsval_tmp); 230 | } 231 | 232 | return { p: p, result: result }; 233 | } 234 | 235 | fromJS(val, val_type) { 236 | switch (val_type) { 237 | case 0: { 238 | // RawNone 239 | return Buffer.allocUnsafe(0); 240 | } 241 | 242 | case 1: { 243 | // RawBuffer 244 | return Buffer.isBuffer(val) 245 | ? val 246 | : util.types.isArrayBufferView(val) 247 | ? bufferFromArrayBufferView(val) 248 | : Buffer.from(val); 249 | } 250 | 251 | case 2: { 252 | // RawJSON 253 | return Buffer.from(JSON.stringify(val), "utf-8"); 254 | } 255 | 256 | case 3: { 257 | // RawJSVal 258 | const val_buf = Buffer.allocUnsafe(8); 259 | val_buf.writeBigUInt64LE(this.jsval.new(val), 0); 260 | return val_buf; 261 | } 262 | 263 | default: { 264 | throw new Error(`fromJS: invalid type ${val_type}`); 265 | } 266 | } 267 | } 268 | 269 | onParentMessage(buf_msg) { 270 | const r = this.handleParentMessage(buf_msg); 271 | if (isPromise(r)) { 272 | r.then((resp_buf) => { 273 | if (resp_buf) { 274 | worker_threads.parentPort.postMessage(resp_buf); 275 | } 276 | }); 277 | } else { 278 | if (r) { 279 | worker_threads.parentPort.postMessage(r); 280 | } 281 | } 282 | } 283 | 284 | handleParentMessage(buf_msg) { 285 | buf_msg = bufferFromArrayBufferView(buf_msg); 286 | let p = 0; 287 | const msg_tag = buf_msg.readUInt8(p); 288 | p += 1; 289 | switch (msg_tag) { 290 | case 0: { 291 | // JSEvalRequest 292 | const req_id = buf_msg.readBigUInt64LE(p); 293 | p += 8; 294 | try { 295 | const r = this.toJS(buf_msg, p); 296 | p = r.p; 297 | 298 | const on_eval_result = (eval_result) => { 299 | const return_type = buf_msg.readUInt8(p); 300 | p += 1; 301 | const eval_result_buf = this.fromJS(eval_result, return_type); 302 | const resp_buf = Buffer.allocUnsafe(18 + eval_result_buf.length); 303 | resp_buf.writeUInt8(0, 0); 304 | resp_buf.writeBigUInt64LE(req_id, 1); 305 | resp_buf.writeUInt8(1, 9); 306 | resp_buf.writeBigUInt64LE(BigInt(eval_result_buf.length), 10); 307 | eval_result_buf.copy(resp_buf, 18); 308 | return resp_buf; 309 | }; 310 | 311 | return isPromise(r.result) 312 | ? r.result 313 | .then(on_eval_result) 314 | .catch((err) => this.onEvalError(req_id, err)) 315 | : on_eval_result(r.result); 316 | } catch (err) { 317 | // EvalError 318 | return this.onEvalError(req_id, err); 319 | } 320 | } 321 | 322 | case 1: { 323 | // HSExportRequest 324 | const is_sync = Boolean(buf_msg.readUInt8(p)); 325 | p += 1; 326 | const req_id = buf_msg.readBigUInt64LE(p); 327 | p += 8; 328 | try { 329 | const hs_func_id = buf_msg.readBigUInt64LE(p); 330 | p += 8; 331 | const hs_func_args_len = Number(buf_msg.readBigUInt64LE(p)); 332 | p += 8; 333 | const hs_func_args_type = []; 334 | for (let i = 0; i < hs_func_args_len; ++i) { 335 | const r = this.toJS(buf_msg, p); 336 | p = r.p; 337 | const raw_type = buf_msg.readUInt8(p); 338 | p += 1; 339 | hs_func_args_type.push([r.result, raw_type]); 340 | } 341 | 342 | const js_func = (...js_args) => { 343 | if (js_args.length !== hs_func_args_len) { 344 | throw new Error( 345 | `inline-js export function error: arity mismatch, expected ${hs_func_args_len} arguments, got ${js_args.length}` 346 | ); 347 | } 348 | 349 | const hs_args_buf = []; 350 | for (let i = 0; i < hs_func_args_len; ++i) { 351 | hs_args_buf.push( 352 | this.fromJS( 353 | hs_func_args_type[i][0](js_args[i]), 354 | hs_func_args_type[i][1] 355 | ) 356 | ); 357 | } 358 | 359 | const req_buf = Buffer.allocUnsafe( 360 | 26 + 361 | hs_func_args_len * 8 + 362 | hs_args_buf.reduce((acc, hs_arg) => acc + hs_arg.length, 0) 363 | ); 364 | 365 | const hs_eval_req_promise = newPromise(); 366 | const hs_eval_req_id = this.hsCtx.new(hs_eval_req_promise); 367 | if (is_sync) { 368 | hs_eval_req_promise.catch(() => {}); 369 | } 370 | 371 | req_buf.writeUInt8(1, 0); 372 | req_buf.writeUInt8(Number(is_sync), 1); 373 | req_buf.writeBigUInt64LE(hs_eval_req_id, 2); 374 | req_buf.writeBigUInt64LE(hs_func_id, 10); 375 | req_buf.writeBigUInt64LE(BigInt(hs_func_args_len), 18); 376 | let p = 26; 377 | for (const hs_arg of hs_args_buf) { 378 | req_buf.writeBigUInt64LE(BigInt(hs_arg.length), p); 379 | p += 8; 380 | hs_arg.copy(req_buf, p); 381 | p += hs_arg.length; 382 | } 383 | 384 | if (is_sync) { 385 | Atomics.store(this.exportSyncFlag, 0, 1); 386 | } 387 | 388 | worker_threads.parentPort.postMessage(req_buf); 389 | 390 | if (is_sync) { 391 | while (true) { 392 | Atomics.wait(this.exportSyncFlag, 0, 1); 393 | const buf_msg_len = this.exportSyncBuffer.readUInt32LE(0); 394 | const buf_msg = Buffer.from( 395 | this.exportSyncBuffer.slice(4, 4 + buf_msg_len) 396 | ); 397 | 398 | this.onParentMessage(buf_msg); 399 | 400 | Atomics.store( 401 | this.exportSyncFlag, 402 | 0, 403 | hs_eval_req_promise.fulfilled || hs_eval_req_promise.rejected 404 | ? 0 405 | : 1 406 | ); 407 | Atomics.notify(this.exportSyncFlag, 0, 1); 408 | 409 | if (hs_eval_req_promise.fulfilled) { 410 | return hs_eval_req_promise.value; 411 | } 412 | if (hs_eval_req_promise.rejected) { 413 | throw hs_eval_req_promise.reason; 414 | } 415 | } 416 | } else { 417 | return hs_eval_req_promise; 418 | } 419 | }; 420 | 421 | const js_func_buf = this.fromJS(js_func, 3); 422 | const resp_buf = Buffer.allocUnsafe(18 + js_func_buf.length); 423 | resp_buf.writeUInt8(0, 0); 424 | resp_buf.writeBigUInt64LE(req_id, 1); 425 | resp_buf.writeUInt8(1, 9); 426 | resp_buf.writeBigUInt64LE(BigInt(js_func_buf.length), 10); 427 | js_func_buf.copy(resp_buf, 18); 428 | return resp_buf; 429 | } catch (err) { 430 | // EvalError 431 | return this.onEvalError(req_id, err); 432 | } 433 | } 434 | 435 | case 2: { 436 | // HSEvalResponse 437 | const is_sync = Boolean(buf_msg.readUInt8(p)); 438 | p += 1; 439 | const hs_eval_resp_id = buf_msg.readBigUInt64LE(p); 440 | p += 8; 441 | const hs_eval_resp_promise = this.hsCtx.get(hs_eval_resp_id); 442 | this.hsCtx.free(hs_eval_resp_id); 443 | const hs_eval_resp_tag = buf_msg.readUInt8(p); 444 | p += 1; 445 | switch (hs_eval_resp_tag) { 446 | case 0: { 447 | const err_len = Number(buf_msg.readBigUInt64LE(p)); 448 | p += 8; 449 | err = new Error(this.decoder.end(buf_msg.slice(p, p + err_len))); 450 | p += err_len; 451 | hs_eval_resp_promise.reject(err); 452 | break; 453 | } 454 | 455 | case 1: { 456 | const r = this.toJS(buf_msg, p); 457 | p = r.p; 458 | hs_eval_resp_promise.resolve(r.result); 459 | break; 460 | } 461 | 462 | default: { 463 | throw new Error(`inline-js invalid message ${buf_msg}`); 464 | } 465 | } 466 | return; 467 | } 468 | 469 | case 3: { 470 | // JSValFree 471 | const jsval_id = buf_msg.readBigUInt64LE(p); 472 | p += 8; 473 | this.jsval.free(jsval_id); 474 | return; 475 | } 476 | 477 | case 4: { 478 | // Close 479 | this.jsval.clear(); 480 | worker_threads.parentPort.unref(); 481 | return; 482 | } 483 | 484 | default: { 485 | throw new Error(`inline-js invalid message ${buf_msg}`); 486 | } 487 | } 488 | } 489 | 490 | onEvalError(req_id, err) { 491 | const err_str = `${err.stack ? err.stack : err}`; 492 | const err_buf = Buffer.from(err_str, "utf-8"); 493 | const resp_buf = Buffer.allocUnsafe(18 + err_buf.length); 494 | resp_buf.writeUInt8(0, 0); 495 | resp_buf.writeBigUInt64LE(req_id, 1); 496 | resp_buf.writeUInt8(0, 9); 497 | resp_buf.writeBigUInt64LE(BigInt(err_buf.length), 10); 498 | err_buf.copy(resp_buf, 18); 499 | return resp_buf; 500 | } 501 | } 502 | 503 | function newPromise() { 504 | let promise_resolve, promise_reject; 505 | const p = new Promise((resolve, reject) => { 506 | promise_resolve = resolve; 507 | promise_reject = reject; 508 | }); 509 | p.resolve = (v) => { 510 | p.fulfilled = true; 511 | p.value = v; 512 | promise_resolve(v); 513 | }; 514 | p.reject = (err) => { 515 | p.rejected = true; 516 | p.reason = err; 517 | promise_reject(err); 518 | }; 519 | return p; 520 | } 521 | 522 | function isPromise(obj) { 523 | return Boolean(obj) && typeof obj.then === "function"; 524 | } 525 | 526 | function bufferFromArrayBufferView(a) { 527 | return Buffer.from(a.buffer, a.byteOffset, a.byteLength); 528 | } 529 | 530 | if (worker_threads.isMainThread) { 531 | new MainContext(); 532 | } else { 533 | new WorkerContext(); 534 | } 535 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core.hs: -------------------------------------------------------------------------------- 1 | module Language.JavaScript.Inline.Core 2 | ( -- * Session management 3 | Config (..), 4 | defaultConfig, 5 | Session, 6 | newSession, 7 | closeSession, 8 | killSession, 9 | withSession, 10 | 11 | -- * Haskell/JavaScript data marshaling 12 | JSExpr, 13 | JSVal, 14 | EncodedString (..), 15 | EncodedJSON (..), 16 | RawJSType (..), 17 | ToJS (..), 18 | FromJS (..), 19 | 20 | -- * Performing evaluation 21 | eval, 22 | importCJS, 23 | importMJS, 24 | 25 | -- * Importing JavaScript functions to Haskell 26 | Import, 27 | importJSFunc, 28 | 29 | -- * Exporting Haskell functions to JavaScript 30 | Export, 31 | export, 32 | exportSync, 33 | 34 | -- * Manual resource management 35 | freeJSVal, 36 | 37 | -- * Exceptions 38 | NodeVersionUnsupported (..), 39 | EvalError (..), 40 | SessionClosed (..), 41 | ) 42 | where 43 | 44 | import Language.JavaScript.Inline.Core.Class 45 | import Language.JavaScript.Inline.Core.Exception 46 | import Language.JavaScript.Inline.Core.Export 47 | import Language.JavaScript.Inline.Core.Import 48 | import Language.JavaScript.Inline.Core.Instruction 49 | import Language.JavaScript.Inline.Core.JSVal 50 | import Language.JavaScript.Inline.Core.Message 51 | import Language.JavaScript.Inline.Core.Session 52 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Class.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TypeFamilies #-} 5 | 6 | module Language.JavaScript.Inline.Core.Class where 7 | 8 | import Data.Binary.Get 9 | import qualified Data.ByteString.Lazy as LBS 10 | import Data.Proxy 11 | import Data.String 12 | import Language.JavaScript.Inline.Core.JSVal 13 | import Language.JavaScript.Inline.Core.Message 14 | import Language.JavaScript.Inline.Core.Session 15 | import Language.JavaScript.Inline.Core.Utils 16 | 17 | -- | UTF-8 encoded string. 18 | newtype EncodedString = EncodedString 19 | { unEncodedString :: LBS.ByteString 20 | } 21 | deriving (Show, IsString) 22 | 23 | -- | UTF-8 encoded JSON. 24 | newtype EncodedJSON = EncodedJSON 25 | { unEncodedJSON :: LBS.ByteString 26 | } 27 | deriving (Show, IsString) 28 | 29 | -- | Haskell types which can be converted to JavaScript. 30 | class ToJS a where 31 | -- | Encodes a Haskell value to 'JSExpr'. 32 | toJS :: a -> JSExpr 33 | 34 | instance ToJS () where 35 | toJS _ = "undefined" 36 | 37 | instance ToJS LBS.ByteString where 38 | toJS = JSExpr . pure . BufferLiteral 39 | 40 | instance ToJS EncodedString where 41 | toJS = JSExpr . pure . StringLiteral . unEncodedString 42 | 43 | instance ToJS EncodedJSON where 44 | toJS = JSExpr . pure . JSONLiteral . unEncodedJSON 45 | 46 | instance ToJS JSVal where 47 | toJS = JSExpr . pure . JSValLiteral 48 | 49 | -- | Haskell types which can be converted from JavaScript. 50 | class FromJS a where 51 | -- | The JavaScript value's 'RawJSType'. 52 | rawJSType :: Proxy a -> RawJSType 53 | 54 | -- | A synchronous JavaScript function which encodes a value to its 55 | -- 'RawJSType'. 56 | toRawJSType :: Proxy a -> JSExpr 57 | 58 | -- | A Haskell function which decodes the Haskell value from the serialized 59 | -- 'RawJSType'. 60 | fromJS :: Session -> LBS.ByteString -> IO a 61 | 62 | instance FromJS () where 63 | rawJSType _ = RawNone 64 | toRawJSType _ = "() => undefined" 65 | fromJS _ _ = pure () 66 | 67 | instance FromJS LBS.ByteString where 68 | rawJSType _ = RawBuffer 69 | toRawJSType _ = "a => a" 70 | fromJS _ = pure 71 | 72 | instance FromJS EncodedString where 73 | rawJSType _ = RawBuffer 74 | toRawJSType _ = "a => a" 75 | fromJS _ = pure . EncodedString 76 | 77 | instance FromJS EncodedJSON where 78 | rawJSType _ = RawJSON 79 | toRawJSType _ = "a => a" 80 | fromJS _ = pure . EncodedJSON 81 | 82 | instance FromJS JSVal where 83 | rawJSType _ = RawJSVal 84 | toRawJSType _ = "a => a" 85 | fromJS _session _jsval_id_buf = do 86 | _jsval_id <- runGetExact getWord64host _jsval_id_buf 87 | newJSVal True _jsval_id (sessionSend _session $ JSValFree _jsval_id) 88 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Dict.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ConstraintKinds #-} 2 | {-# LANGUAGE GADTs #-} 3 | 4 | module Language.JavaScript.Inline.Core.Dict where 5 | 6 | import Data.Proxy 7 | 8 | data Dict c where 9 | Dict :: c a => Proxy a -> Dict c 10 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Exception.hs: -------------------------------------------------------------------------------- 1 | module Language.JavaScript.Inline.Core.Exception where 2 | 3 | import Control.Exception 4 | 5 | data NotThreadedRTS 6 | = NotThreadedRTS 7 | deriving (Show) 8 | 9 | instance Exception NotThreadedRTS 10 | 11 | data NodeVersionUnsupported 12 | = NodeVersionUnsupported 13 | deriving (Show) 14 | 15 | instance Exception NodeVersionUnsupported 16 | 17 | newtype EvalError = EvalError 18 | { evalErrorMessage :: String 19 | } 20 | deriving (Show) 21 | 22 | instance Exception EvalError 23 | 24 | data SessionClosed 25 | = SessionClosed 26 | deriving (Show) 27 | 28 | instance Exception SessionClosed 29 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Export.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | {-# LANGUAGE TypeApplications #-} 3 | 4 | module Language.JavaScript.Inline.Core.Export where 5 | 6 | import qualified Data.ByteString.Lazy as LBS 7 | import Data.Proxy 8 | import Language.JavaScript.Inline.Core.Class 9 | import Language.JavaScript.Inline.Core.Dict 10 | import Language.JavaScript.Inline.Core.Message 11 | import Language.JavaScript.Inline.Core.Session 12 | 13 | -- | The class of Haskell functions which can be exported as JavaScript 14 | -- functions. The Haskell function type should be @a -> b -> .. -> IO r@, where 15 | -- the arguments @a@, @b@, etc are 'FromJS' instances, and the result @r@ is 16 | -- 'ToJS' instance. 17 | class Export f where 18 | exportArgsFromJS :: Proxy f -> [Dict FromJS] 19 | exportMonomorphize :: Session -> f -> [LBS.ByteString] -> IO JSExpr 20 | 21 | instance ToJS r => Export (IO r) where 22 | exportArgsFromJS _ = [] 23 | exportMonomorphize _ m [] = toJS <$> m 24 | exportMonomorphize _ _ _ = fail "Language.JavaScript.Inline.Core.Export: impossible" 25 | 26 | instance (FromJS a, Export b) => Export (a -> b) where 27 | exportArgsFromJS _ = 28 | Dict (Proxy @a) : exportArgsFromJS (Proxy @b) 29 | exportMonomorphize s f (x : xs) = do 30 | a <- fromJS s x 31 | exportMonomorphize s (f a) xs 32 | exportMonomorphize _ _ _ = fail "Language.JavaScript.Inline.Core.Export: impossible" 33 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/IPC.hs: -------------------------------------------------------------------------------- 1 | module Language.JavaScript.Inline.Core.IPC where 2 | 3 | import Control.Concurrent 4 | import Control.Monad 5 | import Data.Binary.Get 6 | import qualified Data.ByteString as BS 7 | import Data.ByteString.Builder 8 | import qualified Data.ByteString.Lazy as LBS 9 | import Foreign 10 | import Language.JavaScript.Inline.Core.Utils 11 | import System.IO 12 | 13 | type Msg = LBS.ByteString 14 | 15 | -- | An 'IPC' represents an opaque bi-directional message port. There are 16 | -- middleware functions in this module which modify 'IPC' fields. 17 | data IPC = IPC 18 | { -- | Send a 'Msg'. 19 | send :: Msg -> IO (), 20 | -- | Receive a 'Msg'. Should block when there's no incoming 'Msg' yet, and 21 | -- throw when the 'IPC' is closed. 22 | recv :: IO Msg, 23 | -- | Callback for each incoming 'Msg'. 24 | onRecv :: Msg -> IO (), 25 | -- | The callback to be called when 'recv' throws, which indicates the 26 | -- remote device has closed. Will only be called once. 27 | postClose :: IO () 28 | } 29 | 30 | instance Show IPC where 31 | show IPC {} = "IPC" 32 | 33 | -- | Given the 'Handle's for send/recv, this function creates the 'send' / 34 | -- 'recv' fields of an 'IPC' value. 35 | -- 36 | -- The protocol for preserving message boundaries is simple: first comes the 37 | -- message header, which is just a little-endian 64-bit unsigned integer, 38 | -- representing the message byte length (sans header). Then follows the actual 39 | -- message. 40 | 41 | -- The 'Handle' for send is flushed after each 'send' call to allow the remote 42 | -- device to get the 'Msg' immediately. 43 | ipcFromHandles :: Handle -> Handle -> IPC -> IPC 44 | ipcFromHandles h_send h_recv ipc = 45 | ipc 46 | { send = \msg -> do 47 | BS.hPut h_send $ 48 | LBS.toStrict $ 49 | toLazyByteString $ 50 | storablePut (LBS.length msg) 51 | <> lazyByteString msg 52 | hFlush h_send, 53 | recv = do 54 | len <- runGet storableGet <$> hGetExact h_recv 8 55 | hGetExact h_recv $ fromIntegral (len :: Word64) 56 | } 57 | 58 | -- | This function forks the recv thread. In the result 'IPC' value, only the 59 | -- 'send' field remain valid and can be used by the user. 60 | -- 61 | -- The recv thread repeatedly fetches incoming 'Msg's and invokes the 'onRecv' 62 | -- callback on them. When an exception is raised, it invokes the 'postClose' 63 | -- callback and exits. Since 'onRecv' is run in the recv thread, the user should 64 | -- ensure it doesn't throw and doesn't take too long to complete, otherwise 65 | -- it'll block later incoming messages. 66 | ipcFork :: IPC -> IO IPC 67 | ipcFork ipc = do 68 | let io_recv_loop = forever $ do 69 | msg <- recv ipc 70 | onRecv ipc msg 71 | _ <- forkFinally io_recv_loop $ \_ -> postClose ipc 72 | pure 73 | ipc 74 | { recv = error "fork: recv", 75 | onRecv = error "fork: onRecv", 76 | postClose = error "fork: postClose" 77 | } 78 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Import.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings, ScopedTypeVariables, TypeApplications #-} 2 | 3 | module Language.JavaScript.Inline.Core.Import where 4 | 5 | import Language.JavaScript.Inline.Core.Class 6 | import Language.JavaScript.Inline.Core.Session 7 | import Language.JavaScript.Inline.Core.JSVal 8 | import Language.JavaScript.Inline.Core.Message 9 | import Language.JavaScript.Inline.Core.Instruction 10 | import Data.List 11 | 12 | -- | The class of Haskell functions which can be imported from JavaScript 13 | -- function 'JSVal's. The Haskell function type should be @a -> b -> .. -> IO 14 | -- r@, where the arguments @a@, @b@, etc are 'ToJS' instances, and the result 15 | -- @r@ is 'FromJS' instance. 16 | class Import f where 17 | importMonomorphize :: Session -> JSVal -> [JSExpr] -> f 18 | 19 | instance FromJS r => Import (IO r) where 20 | importMonomorphize s v xs = eval s $ toJS v <> "(...[" <> mconcat (intersperse "," xs) <> "])" 21 | 22 | instance (ToJS a, Import b) => Import (a -> b) where 23 | importMonomorphize s v xs = \a -> importMonomorphize @b s v (toJS a:xs) 24 | 25 | -- | Import a JavaScript function to a Haskell function. 26 | importJSFunc :: Import f => Session -> JSVal -> f 27 | importJSFunc s v = importMonomorphize s v [] 28 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Instruction.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | 6 | module Language.JavaScript.Inline.Core.Instruction where 7 | 8 | import Control.Concurrent.STM 9 | import Control.Exception 10 | import Data.Binary.Get 11 | import qualified Data.ByteString.Lazy as LBS 12 | import Data.Proxy 13 | import Foreign 14 | import Language.JavaScript.Inline.Core.Class 15 | import Language.JavaScript.Inline.Core.Dict 16 | import Language.JavaScript.Inline.Core.Export 17 | import Language.JavaScript.Inline.Core.JSVal 18 | import Language.JavaScript.Inline.Core.Message 19 | import Language.JavaScript.Inline.Core.Session 20 | import Language.JavaScript.Inline.Core.Utils 21 | import System.Directory 22 | import System.IO.Unsafe 23 | 24 | evalWithDecoder :: 25 | RawJSType -> 26 | (Session -> LBS.ByteString -> IO a) -> 27 | Session -> 28 | JSExpr -> 29 | IO a 30 | evalWithDecoder _return_type _decoder _session@Session {..} _code = do 31 | _inbox <- newEmptyTMVarIO 32 | _sp <- newStablePtr _inbox 33 | let _id = word64FromStablePtr _sp 34 | sessionSend 35 | _session 36 | JSEvalRequest 37 | { jsEvalRequestId = _id, 38 | code = _code, 39 | returnType = _return_type 40 | } 41 | touch _code 42 | unsafeInterleaveIO $ do 43 | _resp <- atomically $ takeTMVar _inbox `orElse` readTMVar fatalErrorInbox 44 | freeStablePtr _sp 45 | case _resp of 46 | Left err -> throwIO err 47 | Right _result_buf -> _decoder _session _result_buf 48 | 49 | -- | Evaluate a 'JSExpr' and return the result. Evaluation is /asynchronous/. 50 | -- When this function returns, the eval request has been sent to the eval 51 | -- server, but the result may not be sent back yet. The returned value is a 52 | -- thunk, and forcing it to WHNF will block until the result is sent back. 53 | -- 54 | -- The caveats of lazy I/O apply here as well. For instance, returning 55 | -- evaluation result from a @with@-style function may cause a use-after-free 56 | -- problem. In case when it's desirable to ensure the evaluation has completed 57 | -- at a certain point, use 'Control.Exception.evaluate' or @BangPatterns@ to 58 | -- force the result value. 59 | -- 60 | -- Modeling asynchronousity with laziness enables us to simplify API. Users can 61 | -- easily regain strictness from the lazy API; if we do it the other way around 62 | -- and provide strict-by-default eval functions, we'll need to explicitly 63 | -- decouple sending of eval requests and receiving of eval results, which 64 | -- complicates the API. 65 | -- 66 | -- On the eval server side, the result value is @await@ed before further 67 | -- processing. Therefore if it's a @Promise@, the eval result will be the 68 | -- resolved value instead of the @Promise@ itself. If the @Promise@ value needs 69 | -- to be returned, wrap it in another object (e.g. a single-element array). 70 | eval :: forall a. FromJS a => Session -> JSExpr -> IO a 71 | eval s c = 72 | evalWithDecoder (rawJSType (Proxy @a)) fromJS s $ 73 | "((x, f) => (Boolean(x) && typeof x.then === 'function') ? x.then(f) : f(x))(" 74 | <> c 75 | <> ", " 76 | <> toRawJSType (Proxy @a) 77 | <> ")" 78 | 79 | -- | Import a CommonJS module file and return its @module.exports@ object. The 80 | -- module file path can be absolute, or relative to the current Haskell process. 81 | -- The imported module will be retained in the @require()@ loader cache. 82 | importCJS :: Session -> FilePath -> IO JSVal 83 | importCJS s p = do 84 | p' <- makeAbsolute p 85 | eval s $ "require(" <> string p' <> ")" 86 | 87 | -- | Import an ECMAScript module file and return its module namespace object. 88 | -- The module file path can be absolute, or relative to the current Haskell 89 | -- process. The imported module will be retained in the ESM loader cache. 90 | importMJS :: Session -> FilePath -> IO JSVal 91 | importMJS s p = do 92 | p' <- makeAbsolute p 93 | eval s $ 94 | "import('url').then(url => import(url.pathToFileURL(" 95 | <> string p' 96 | <> ")))" 97 | 98 | string :: String -> JSExpr 99 | string = toJS . EncodedString . stringToLBS 100 | 101 | exportAsyncOrSync :: forall f. Export f => Bool -> Session -> f -> IO JSVal 102 | exportAsyncOrSync _is_sync _session@Session {..} f = do 103 | _inbox <- newEmptyTMVarIO 104 | let args_type = 105 | map (\(Dict p) -> (toRawJSType p, rawJSType p)) $ 106 | exportArgsFromJS (Proxy @f) 107 | f' = exportMonomorphize _session f 108 | _sp_inbox <- newStablePtr _inbox 109 | _sp_f <- newStablePtr f' 110 | let _id_inbox = word64FromStablePtr _sp_inbox 111 | _id_f = word64FromStablePtr _sp_f 112 | sessionSend 113 | _session 114 | HSExportRequest 115 | { exportIsSync = _is_sync, 116 | exportRequestId = _id_inbox, 117 | exportFuncId = _id_f, 118 | argsType = args_type 119 | } 120 | unsafeInterleaveIO $ do 121 | _resp <- atomically $ takeTMVar _inbox `orElse` readTMVar fatalErrorInbox 122 | freeStablePtr _sp_inbox 123 | case _resp of 124 | Left err -> throwIO err 125 | Right _jsval_id_buf -> do 126 | _jsval_id <- runGetExact getWord64host _jsval_id_buf 127 | newJSVal False _jsval_id $ do 128 | freeStablePtr _sp_f 129 | sessionSend _session $ JSValFree _jsval_id 130 | 131 | -- | Export a Haskell function as a JavaScript async function, and return its 132 | -- 'JSVal'. Some points to keep in mind: 133 | -- 134 | -- * The Haskell function itself can call into JavaScript again via 'eval', and 135 | -- vice versa. 136 | -- * When called in JavaScript, the Haskell function is run in a forked thread. 137 | -- * If the Haskell function throws, the JavaScript function will reject with an 138 | -- @Error@ with the exception string. 139 | -- * Unlike 'JSVal's returned by 'eval', 'JSVal's returned by 'export' are not 140 | -- garbage collected, since we don't know when a function is garbage collected 141 | -- on the @node@ side. These 'JSVal's need to be manually freed using 142 | -- 'freeJSVal'. 143 | export :: Export f => Session -> f -> IO JSVal 144 | export = exportAsyncOrSync False 145 | 146 | -- | Export a Haskell function as a JavaScript sync function. This is quite 147 | -- heavyweight and in most cases, 'export' is preferrable. 'exportSync' can be 148 | -- useful in certain scenarios when a sync function is desired, e.g. converting 149 | -- a Haskell function to a WebAssembly import. 150 | -- 151 | -- Unlike 'export', 'exportSync' has limited reentrancy: 152 | -- 153 | -- * The Haskell function may calculate the return value based on the result of 154 | -- calling into JavaScript again, but only synchronous code is supported in 155 | -- this case. 156 | -- * The exported JavaScript sync function must not invoke other exported 157 | -- JavaScript sync functions, either directly or indirectly(Haskell calling 158 | -- into JavaScript again). 159 | exportSync :: Export f => Session -> f -> IO JSVal 160 | exportSync = exportAsyncOrSync True 161 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/JSVal.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MagicHash #-} 2 | {-# LANGUAGE StrictData #-} 3 | {-# LANGUAGE UnboxedTuples #-} 4 | 5 | module Language.JavaScript.Inline.Core.JSVal where 6 | 7 | import Foreign 8 | import GHC.Exts 9 | import GHC.Types 10 | 11 | -- $jsval-notes 12 | -- 13 | -- Lifecycle of a 'JSVal': 14 | -- 1. If the 'returnType' is specified as 'RawJSVal', the eval server makes 15 | -- a new 'JSVal' out of the return value and sends it back. 16 | -- 2. The 'JSVal' values can be passed around and later put into a 'JSExpr' 17 | -- for evaluation. 18 | -- 3. When a 'JSVal' is garbage collected on the client side, the finalizer is 19 | -- called which frees it on the eval server side. 20 | -- 21 | -- Notes to keep in mind: 22 | -- 1. When putting 'JSVal's into a 'JSExpr', ensure the value is used 23 | -- synchronously, as opposed to being used in the function body of a 24 | -- callback. Otherwise, by the time it's actually used, it may have already 25 | -- been freed. 26 | -- 2. The finalizer sends a 'JSVal' free message to the eval server. Ensure 27 | -- the free message doesn't appear before the eval message which uses the 28 | -- 'JSVal' in the send queue. 29 | -- 3. There is no response message for the 'JSVal' free message. Freeing or 30 | -- using a non-existent 'JSVal' should result in a fatal error. 31 | 32 | -- | An opaque reference of a JavaScript value. Each 'JSVal' is registered with 33 | -- a finalizer which frees the reference on the @node@ side when it's garbage 34 | -- collected in Haskell. It's also possible to manually free a 'JSVal' using 35 | -- 'freeJSVal'. 36 | data JSVal 37 | = JSVal Word64 (MutVar# RealWorld ()) (Weak# ()) 38 | 39 | instance Show JSVal where 40 | show (JSVal i _ _) = "JSVal " <> show i 41 | 42 | newJSVal :: Bool -> Word64 -> IO () -> IO JSVal 43 | newJSVal gc i (IO f) = IO $ \s0 -> case newMutVar# () s0 of 44 | (# s1, v #) -> 45 | if gc 46 | then case mkWeak# v () f s1 of 47 | (# s2, w #) -> (# s2, JSVal i v w #) 48 | else case mkWeak# () () f s1 of 49 | (# s2, w #) -> (# s2, JSVal i v w #) 50 | 51 | unsafeUseJSVal :: JSVal -> Word64 52 | unsafeUseJSVal (JSVal i _ _) = i 53 | 54 | freeJSVal :: JSVal -> IO () 55 | freeJSVal (JSVal _ _ w) = IO $ \s0 -> case finalizeWeak# w s0 of 56 | (# s1, 0#, _ #) -> (# s1, () #) 57 | (# s1, _, f #) -> f s1 58 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Message.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE StrictData #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | 6 | module Language.JavaScript.Inline.Core.Message where 7 | 8 | import Control.Monad 9 | import Data.Binary 10 | import Data.Binary.Get 11 | import Data.ByteString.Builder 12 | import qualified Data.ByteString.Lazy as LBS 13 | import Data.String 14 | import Language.JavaScript.Inline.Core.JSVal 15 | import Language.JavaScript.Inline.Core.Utils 16 | 17 | data JSExprSegment 18 | = Code LBS.ByteString 19 | | BufferLiteral LBS.ByteString 20 | | StringLiteral LBS.ByteString 21 | | JSONLiteral LBS.ByteString 22 | | JSValLiteral JSVal 23 | deriving (Show) 24 | 25 | -- | Represents a JavaScript expression. 26 | -- 27 | -- Use the 'IsString' instance to convert a 'String' to 'JSExpr', and the 28 | -- 'Semigroup' instance for concating 'JSExpr'. It's also possible to embed 29 | -- other things into 'JSExpr', e.g. a buffer literal, JSON value or a 'JSVal'. 30 | newtype JSExpr = JSExpr 31 | { unJSExpr :: [JSExprSegment] 32 | } 33 | deriving (Semigroup, Monoid, Show) 34 | 35 | instance IsString JSExpr where 36 | fromString = JSExpr . pure . Code . stringToLBS 37 | 38 | -- | To convert a JavaScript value to Haskell, we need to specify its "raw 39 | -- type", which can be one of the following: 40 | data RawJSType 41 | = -- | The JavaScript value is discarded. 42 | RawNone 43 | | -- | The JavaScript value is an @ArrayBufferView@, @ArrayBuffer@ or 44 | -- @string@. 45 | RawBuffer 46 | | -- | The JavaScript value can be JSON-encoded via @JSON.stringify()@. 47 | RawJSON 48 | | -- | The JavaScript value should be managed as a 'JSVal'. 49 | RawJSVal 50 | deriving (Show) 51 | 52 | data MessageHS 53 | = JSEvalRequest 54 | { jsEvalRequestId :: Word64, 55 | code :: JSExpr, 56 | returnType :: RawJSType 57 | } 58 | | HSExportRequest 59 | { exportIsSync :: Bool, 60 | exportRequestId :: Word64, 61 | exportFuncId :: Word64, 62 | argsType :: [(JSExpr, RawJSType)] 63 | } 64 | | HSEvalResponse 65 | { hsEvalResponseIsSync :: Bool, 66 | hsEvalResponseId :: Word64, 67 | hsEvalResponseContent :: Either LBS.ByteString JSExpr 68 | } 69 | | JSValFree Word64 70 | | Close 71 | deriving (Show) 72 | 73 | data MessageJS 74 | = JSEvalResponse 75 | { jsEvalResponseId :: Word64, 76 | jsEvalResponseContent :: Either LBS.ByteString LBS.ByteString 77 | } 78 | | HSEvalRequest 79 | { hsEvalRequestIsSync :: Bool, 80 | hsEvalRequestId :: Word64, 81 | hsEvalRequestFunc :: Word64, 82 | args :: [LBS.ByteString] 83 | } 84 | | FatalError LBS.ByteString 85 | deriving (Show) 86 | 87 | messageHSPut :: MessageHS -> Builder 88 | messageHSPut msg = case msg of 89 | JSEvalRequest {..} -> 90 | word8Put 0 91 | <> word64Put jsEvalRequestId 92 | <> exprPut code 93 | <> rawTypePut returnType 94 | HSExportRequest {..} -> 95 | word8Put 1 96 | <> boolPut exportIsSync 97 | <> word64Put exportRequestId 98 | <> word64Put exportFuncId 99 | <> word64Put (fromIntegral (length argsType)) 100 | <> foldMap 101 | (\(code, raw_type) -> exprPut code <> rawTypePut raw_type) 102 | argsType 103 | HSEvalResponse {..} -> 104 | word8Put 2 105 | <> boolPut hsEvalResponseIsSync 106 | <> word64Put hsEvalResponseId 107 | <> ( case hsEvalResponseContent of 108 | Left err -> word8Put 0 <> lbsPut err 109 | Right r -> word8Put 1 <> exprPut r 110 | ) 111 | JSValFree v -> word8Put 3 <> word64Put v 112 | Close -> word8Put 4 113 | where 114 | boolPut = word8Put . fromIntegral . fromEnum 115 | word8Put = storablePut @Word8 116 | word64Put = storablePut @Word64 117 | lbsPut s = storablePut (LBS.length s) <> lazyByteString s 118 | exprPut code = 119 | word64Put (fromIntegral (length (unJSExpr code)) :: Word64) 120 | <> foldMap exprSegmentPut (unJSExpr code) 121 | exprSegmentPut (Code s) = word8Put 0 <> lbsPut s 122 | exprSegmentPut (BufferLiteral s) = word8Put 1 <> lbsPut s 123 | exprSegmentPut (StringLiteral s) = word8Put 2 <> lbsPut s 124 | exprSegmentPut (JSONLiteral s) = word8Put 3 <> lbsPut s 125 | exprSegmentPut (JSValLiteral v) = word8Put 4 <> word64Put (unsafeUseJSVal v) 126 | rawTypePut RawNone = word8Put 0 127 | rawTypePut RawBuffer = word8Put 1 128 | rawTypePut RawJSON = word8Put 2 129 | rawTypePut RawJSVal = word8Put 3 130 | 131 | messageJSGet :: Get MessageJS 132 | messageJSGet = do 133 | t <- getWord8 134 | case t of 135 | 0 -> do 136 | _id <- getWord64host 137 | _tag <- getWord8 138 | case _tag of 139 | 0 -> do 140 | _err_buf <- lbsGet 141 | pure 142 | JSEvalResponse 143 | { jsEvalResponseId = _id, 144 | jsEvalResponseContent = Left _err_buf 145 | } 146 | 1 -> do 147 | _result_buf <- lbsGet 148 | pure 149 | JSEvalResponse 150 | { jsEvalResponseId = _id, 151 | jsEvalResponseContent = Right _result_buf 152 | } 153 | _ -> fail $ "messageJSGet: invalid _tag " <> show _tag 154 | 1 -> do 155 | _is_sync <- toEnum . fromIntegral <$> getWord8 156 | _id <- getWord64host 157 | _func <- getWord64host 158 | l <- fromIntegral <$> getWord64host 159 | _args <- replicateM l lbsGet 160 | pure 161 | HSEvalRequest 162 | { hsEvalRequestIsSync = _is_sync, 163 | hsEvalRequestId = _id, 164 | hsEvalRequestFunc = _func, 165 | args = _args 166 | } 167 | 2 -> FatalError <$> lbsGet 168 | _ -> fail $ "messageJSGet: invalid tag " <> show t 169 | where 170 | lbsGet = do 171 | l <- fromIntegral <$> getWord64host 172 | getLazyByteString l 173 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/NodePath.hs: -------------------------------------------------------------------------------- 1 | module Language.JavaScript.Inline.Core.NodePath 2 | ( defNodePath, 3 | ) 4 | where 5 | 6 | {-# INLINE defNodePath #-} 7 | defNodePath :: FilePath 8 | defNodePath = "node" 9 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/NodeVersion.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ViewPatterns #-} 2 | 3 | module Language.JavaScript.Inline.Core.NodeVersion where 4 | 5 | import Control.Exception 6 | import Control.Monad 7 | import Data.List 8 | import Data.Version 9 | import Language.JavaScript.Inline.Core.Exception 10 | import Language.JavaScript.Inline.Core.Utils 11 | import System.Process 12 | 13 | parseNodeVersion :: String -> Version 14 | parseNodeVersion s0 = Version vs $ case tag of 15 | "" -> [] 16 | _ -> [tag] 17 | where 18 | vs_ts = split (== '-') s0 19 | vs = map read $ split (== '.') $ head vs_ts 20 | tag = intercalate "-" $ tail vs_ts 21 | 22 | isSupportedVersion :: Version -> Bool 23 | isSupportedVersion (versionBranch -> v) = 24 | (v >= [12, 0, 0]) || (v >= [10, 20, 0] && v < [11]) 25 | 26 | checkNodeVersion :: FilePath -> IO () 27 | checkNodeVersion p = do 28 | v <- 29 | parseNodeVersion 30 | <$> readProcess 31 | p 32 | ["--eval", "process.stdout.write(process.versions.node)"] 33 | "" 34 | unless (isSupportedVersion v) $ throwIO NodeVersionUnsupported 35 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Session.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | {-# LANGUAGE RecursiveDo #-} 5 | {-# LANGUAGE StrictData #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | {-# LANGUAGE TupleSections #-} 8 | {-# LANGUAGE ViewPatterns #-} 9 | 10 | module Language.JavaScript.Inline.Core.Session where 11 | 12 | import Control.Concurrent 13 | import Control.Concurrent.STM 14 | import Control.Exception 15 | import Control.Monad 16 | import qualified Data.ByteString as BS 17 | import Data.ByteString.Builder 18 | import qualified Data.ByteString.Lazy as LBS 19 | import Data.Maybe 20 | import Distribution.Simple.Utils 21 | import Foreign 22 | import Language.JavaScript.Inline.Core.Exception 23 | import Language.JavaScript.Inline.Core.IPC 24 | import Language.JavaScript.Inline.Core.Message 25 | import Language.JavaScript.Inline.Core.NodePath 26 | import Language.JavaScript.Inline.Core.NodeVersion 27 | import Language.JavaScript.Inline.Core.Utils 28 | import System.Directory 29 | import System.Environment.Blank 30 | import System.FilePath 31 | import System.Process 32 | 33 | -- $session-todos 34 | -- 35 | -- * Using closed sessions throw immediately 36 | -- * Handle errors in send/recv thread 37 | 38 | {-# NOINLINE evalServerSrc #-} 39 | evalServerSrc :: BS.ByteString 40 | evalServerSrc = $(embedFile $ "jsbits" "index.js") 41 | 42 | data Config = Config 43 | { -- | Path to the @node@ executable. Defaults to @node@. 44 | nodePath :: FilePath, 45 | -- | Extra @node@ arguments that appear before the eval server script's file 46 | -- path. These arguments won't show up in @process.argv@. 47 | nodeExtraArgs :: [String], 48 | -- | Extra environment variables to pass to @node@. Will shadow already 49 | -- existing ones. 50 | nodeExtraEnv :: [(String, String)], 51 | -- | To @require()@ or @import()@ third-party packages, set this to the 52 | -- @node_modules@ directory path. 53 | nodeModules :: Maybe FilePath, 54 | -- | Size in MiBs of the buffer for passing results of functions exported by 55 | -- 'exportSync'. Most users don't need to care about this. Defaults to 1. 56 | nodeExportSyncBufferSize :: Int 57 | } 58 | deriving (Show) 59 | 60 | defaultConfig :: Config 61 | defaultConfig = 62 | Config 63 | { nodePath = defNodePath, 64 | nodeExtraArgs = 65 | [ "--experimental-modules", 66 | "--experimental-worker", 67 | "--no-warnings", 68 | "--unhandled-rejections=strict" 69 | ], 70 | nodeExtraEnv = [], 71 | nodeModules = Nothing, 72 | nodeExportSyncBufferSize = 1 73 | } 74 | 75 | data Session = Session 76 | { ipc :: IPC, 77 | fatalErrorInbox :: TMVar (Either SomeException LBS.ByteString), 78 | -- | After a 'Session' is closed, no more messages can be sent to @node@. 79 | -- Use this to close the 'Session' if @node@ should still run for some time 80 | -- to allow previous evaluation results to be sent back. Blocks until @node@ 81 | -- process exits. 82 | closeSession :: IO (), 83 | -- | Terminate the @node@ process immediately. Use this to close the 84 | -- 'Session' if @node@ doesn't need to run any more. Blocks until @node@ 85 | -- process exits. 86 | killSession :: IO () 87 | } 88 | 89 | instance Show Session where 90 | show Session {} = "Session" 91 | 92 | newSession :: Config -> IO Session 93 | newSession Config {..} = do 94 | unless rtsSupportsBoundThreads $ throwIO NotThreadedRTS 95 | checkNodeVersion nodePath 96 | (_root, _p) <- do 97 | _tmp <- getTemporaryDirectory 98 | _root <- createTempDirectory _tmp "inline-js" 99 | let _p = _root "index.js" 100 | BS.writeFile _p evalServerSrc 101 | pure (_root, _p) 102 | _env <- getEnvironment 103 | (Just _wh, Just _rh, _, _ph) <- 104 | createProcess 105 | (proc nodePath $ nodeExtraArgs <> [_p]) 106 | { env = 107 | Just $ 108 | kvDedup $ 109 | [ ( "INLINE_JS_EXPORT_SYNC_BUFFER_SIZE", 110 | show nodeExportSyncBufferSize 111 | ) 112 | ] 113 | <> map ("INLINE_JS_NODE_MODULES",) (maybeToList nodeModules) 114 | <> nodeExtraEnv 115 | <> _env, 116 | std_in = CreatePipe, 117 | std_out = CreatePipe 118 | } 119 | _err_inbox <- newEmptyTMVarIO 120 | _exit_inbox <- newEmptyTMVarIO 121 | mdo 122 | let on_recv msg_buf = do 123 | msg <- runGetExact messageJSGet msg_buf 124 | case msg of 125 | JSEvalResponse {..} -> do 126 | let _sp = word64ToStablePtr jsEvalResponseId 127 | _inbox <- deRefStablePtr _sp 128 | atomically $ putTMVar _inbox jsEvalResponseContent 129 | HSEvalRequest {..} -> do 130 | _ <- 131 | forkFinally 132 | ( do 133 | let sp = word64ToStablePtr hsEvalRequestFunc 134 | f <- deRefStablePtr sp 135 | r <- f args 136 | sessionSend 137 | _session 138 | HSEvalResponse 139 | { hsEvalResponseIsSync = hsEvalRequestIsSync, 140 | hsEvalResponseId = hsEvalRequestId, 141 | hsEvalResponseContent = Right r 142 | } 143 | ) 144 | ( \case 145 | Left (SomeException err) -> do 146 | let err_buf = stringToLBS $ show err 147 | sessionSend 148 | _session 149 | HSEvalResponse 150 | { hsEvalResponseIsSync = hsEvalRequestIsSync, 151 | hsEvalResponseId = hsEvalRequestId, 152 | hsEvalResponseContent = Left err_buf 153 | } 154 | Right () -> pure () 155 | ) 156 | pure () 157 | FatalError err_buf -> 158 | atomically $ 159 | putTMVar _err_inbox $ 160 | Left $ 161 | toException 162 | EvalError 163 | { evalErrorMessage = stringFromLBS err_buf 164 | } 165 | ipc_post_close = do 166 | ec <- waitForProcess _ph 167 | atomically $ do 168 | _ <- tryPutTMVar _err_inbox $ Left $ toException SessionClosed 169 | putTMVar _exit_inbox ec 170 | removePathForcibly _root 171 | _ipc <- 172 | ipcFork $ 173 | ipcFromHandles 174 | _wh 175 | _rh 176 | IPC 177 | { send = error "newSession: send", 178 | recv = error "newSession: recv", 179 | onRecv = on_recv, 180 | postClose = ipc_post_close 181 | } 182 | let wait_for_exit = atomically $ () <$ readTMVar _exit_inbox 183 | session_close = do 184 | send _ipc $ toLazyByteString $ messageHSPut Close 185 | wait_for_exit 186 | session_kill = do 187 | terminateProcess _ph 188 | wait_for_exit 189 | _session = 190 | Session 191 | { ipc = _ipc, 192 | fatalErrorInbox = _err_inbox, 193 | closeSession = session_close, 194 | killSession = session_kill 195 | } 196 | pure _session 197 | 198 | sessionSend :: Session -> MessageHS -> IO () 199 | sessionSend Session {..} msg = send ipc $ toLazyByteString $ messageHSPut msg 200 | 201 | -- | Create a 'Session' with 'newSession', run the passed computation, then free 202 | -- the 'Session' with 'killSession'. The return value is forced to WHNF before 203 | -- freeing the 'Session' to reduce the likelihood of use-after-free errors. 204 | withSession :: Config -> (Session -> IO r) -> IO r 205 | withSession c m = bracket (newSession c) killSession (evaluate <=< m) 206 | -------------------------------------------------------------------------------- /inline-js-core/src/Language/JavaScript/Inline/Core/Utils.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MagicHash #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | {-# LANGUAGE UnboxedTuples #-} 6 | {-# LANGUAGE ViewPatterns #-} 7 | 8 | module Language.JavaScript.Inline.Core.Utils where 9 | 10 | import Data.Binary.Get 11 | import Data.Binary.Get.Internal 12 | import qualified Data.ByteString as BS 13 | import Data.ByteString.Builder 14 | import Data.ByteString.Builder.Prim 15 | import Data.ByteString.Builder.Prim.Internal 16 | import qualified Data.ByteString.Lazy as LBS 17 | import qualified Data.ByteString.Unsafe as BS 18 | import Data.Coerce 19 | import qualified Data.Map.Strict as M 20 | import qualified Data.Text.Lazy as LT 21 | import qualified Data.Text.Lazy.Encoding as LT 22 | import Foreign 23 | import GHC.Exts 24 | import GHC.Types 25 | import Language.Haskell.TH.Lib 26 | import Language.Haskell.TH.Syntax 27 | import System.Directory 28 | import System.FilePath 29 | import System.IO 30 | import System.IO.Unsafe 31 | import Type.Reflection 32 | 33 | -- | 'embedFile' takes a file path, and generates a 'BS.ByteString' at 34 | -- compile-time containing the file content. The file path is relative to the 35 | -- package root directory, and should be included as a part of 36 | -- @extra-source-files@ of that package's @.cabal@ metadata. 37 | -- 38 | -- We embed the eval server JavaScript source via TH, instead of using 39 | -- @data-files@ at runtime, so that standalone executables using this package 40 | -- should still work fine if the build directory is no longer present. 41 | -- 42 | -- A more space-efficient implementation of 'embedFile' which avoids 43 | -- 'StringPrimL' is possible with GHC 8.10+. We stay with 'StringPrimL' to avoid 44 | -- breaking GHC 8.8. 45 | embedFile :: FilePath -> Q Exp 46 | embedFile p' = do 47 | src <- loc_filename <$> location 48 | pkg <- runIO $ pkgDir =<< makeAbsolute src 49 | let p = pkg p' 50 | addDependentFile p 51 | s <- runIO $ BS.readFile p 52 | let len = BS.length s 53 | [| 54 | unsafePerformIO $ 55 | BS.unsafePackAddressLen len $(litE $ stringPrimL $ BS.unpack s) 56 | |] 57 | where 58 | pkgDir p = do 59 | let d = takeDirectory p 60 | fs <- getDirectoryContents d 61 | if any ((== ".cabal") . takeExtension) fs 62 | then pure d 63 | else pkgDir d 64 | 65 | -- | Deduplicate an association list in a left-biased manner; if the same key 66 | -- appears more than once, the left-most key/value pair is preserved. 67 | kvDedup :: Ord k => [(k, a)] -> [(k, a)] 68 | kvDedup = M.toList . M.fromListWith (\_ a -> a) 69 | 70 | storableGet :: 71 | forall a. 72 | Storable a => 73 | Get a 74 | storableGet = 75 | readN (sizeOf (undefined :: a)) $ \bs -> 76 | unsafeDupablePerformIO $ BS.unsafeUseAsCString bs $ peek . castPtr 77 | 78 | storablePut :: Storable a => a -> Builder 79 | storablePut = primFixed storableToF 80 | 81 | intFromStablePtr :: StablePtr a -> Int 82 | intFromStablePtr = coerce . ptrToIntPtr . castStablePtrToPtr 83 | 84 | intToStablePtr :: Int -> StablePtr a 85 | intToStablePtr = castPtrToStablePtr . intPtrToPtr . coerce 86 | 87 | word64FromStablePtr :: StablePtr a -> Word64 88 | word64FromStablePtr = fromIntegral . ptrToWordPtr . castStablePtrToPtr 89 | 90 | word64ToStablePtr :: Word64 -> StablePtr a 91 | word64ToStablePtr = castPtrToStablePtr . wordPtrToPtr . fromIntegral 92 | 93 | stringFromLBS :: LBS.ByteString -> String 94 | stringFromLBS = LT.unpack . LT.decodeUtf8 95 | 96 | stringToLBS :: String -> LBS.ByteString 97 | stringToLBS = LT.encodeUtf8 . LT.pack 98 | 99 | hGetExact :: Handle -> Int -> IO LBS.ByteString 100 | hGetExact h len_expected = do 101 | r <- LBS.hGet h len_expected 102 | let len_actual = fromIntegral $ LBS.length r 103 | if len_actual == len_expected 104 | then pure r 105 | else 106 | fail $ 107 | "hGetExact: expected " 108 | <> show len_expected 109 | <> " bytes, got " 110 | <> show len_actual 111 | 112 | runGetExact :: 113 | forall a. 114 | Typeable a => 115 | Get a -> 116 | LBS.ByteString -> 117 | IO a 118 | runGetExact g buf = 119 | case runGetOrFail g buf of 120 | Right (LBS.null -> True, _, r) -> pure r 121 | _ -> fail $ "runGetExact failed on type " <> show (typeRep @a) 122 | 123 | {-# NOINLINE touch #-} 124 | touch :: a -> IO () 125 | touch a = 126 | IO $ \s0 -> 127 | case touch# a s0 of 128 | s1 -> (# s1, () #) 129 | 130 | split :: (a -> Bool) -> [a] -> [[a]] 131 | split f l = 132 | case foldr w [] l of 133 | [] : r -> r 134 | r -> r 135 | where 136 | w x acc 137 | | f x = 138 | case acc of 139 | (_ : _) : _ -> [] : acc 140 | _ -> acc 141 | | otherwise = 142 | case acc of 143 | [] -> [[x]] 144 | xs : acc' -> (x : xs) : acc' 145 | -------------------------------------------------------------------------------- /inline-js-examples/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for `inline-js-core` 2 | -------------------------------------------------------------------------------- /inline-js-examples/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 EURL Tweag 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of Tweag I/O nor the names of other contributors may be 16 | used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /inline-js-examples/README.md: -------------------------------------------------------------------------------- 1 | # `inline-js-core` 2 | -------------------------------------------------------------------------------- /inline-js-examples/inline-js-examples.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: inline-js-examples 3 | version: 0.0.1.0 4 | synopsis: Call JavaScript from Haskell. 5 | description: 6 | Please see for details. 7 | 8 | category: Web 9 | homepage: https://github.com/tweag/inline-js#readme 10 | bug-reports: https://github.com/tweag/inline-js/issues 11 | maintainer: Cheng Shao 12 | copyright: (c) 2018 Tweag I/O 13 | license: BSD-3-Clause 14 | license-file: LICENSE 15 | build-type: Simple 16 | extra-source-files: 17 | CHANGELOG.md 18 | LICENSE 19 | README.md 20 | 21 | source-repository head 22 | type: git 23 | location: https://github.com/tweag/inline-js 24 | 25 | library 26 | exposed-modules: 27 | Language.JavaScript.Inline.Examples.Stream 28 | Language.JavaScript.Inline.Examples.Utils 29 | Language.JavaScript.Inline.Examples.Utils.LazyIO 30 | Language.JavaScript.Inline.Examples.Wasm 31 | 32 | hs-source-dirs: src 33 | ghc-options: -Wall 34 | build-depends: 35 | , base >=4.12 && <5 36 | , bytestring 37 | , inline-js 38 | , stm 39 | , stm-chans 40 | 41 | default-language: Haskell2010 42 | -------------------------------------------------------------------------------- /inline-js-examples/src/Language/JavaScript/Inline/Examples/Stream.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | 6 | module Language.JavaScript.Inline.Examples.Stream where 7 | 8 | import Control.Exception 9 | import qualified Data.ByteString.Lazy as LBS 10 | import Data.Foldable 11 | import Language.JavaScript.Inline 12 | import Language.JavaScript.Inline.Examples.Utils.LazyIO 13 | 14 | lazyStream :: Session -> JSVal -> IO LBS.ByteString 15 | lazyStream _session _stream = do 16 | LazyIO {..} <- newLazyIO 17 | _on_data <- export _session (lazyOnData . LBS.toStrict) 18 | _on_end <- export _session lazyOnEnd 19 | _on_error <- 20 | export 21 | _session 22 | (lazyOnError . toException . userError . show @EncodedString) 23 | lazySetFinalizer $ for_ [_on_data, _on_end, _on_error] freeJSVal 24 | eval @() 25 | _session 26 | [js| 27 | $_stream.on("data", $_on_data); 28 | $_stream.on("end", $_on_end); 29 | $_stream.on("error", $_on_error); 30 | |] 31 | pure lazyContent 32 | -------------------------------------------------------------------------------- /inline-js-examples/src/Language/JavaScript/Inline/Examples/Utils.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | 3 | module Language.JavaScript.Inline.Examples.Utils where 4 | 5 | import qualified Data.ByteString.Internal as BS 6 | import qualified Data.ByteString.Lazy as LBS 7 | import qualified Data.ByteString.Unsafe as BS 8 | import Foreign 9 | import System.IO.Unsafe 10 | 11 | storableToLBS :: Storable a => a -> LBS.ByteString 12 | storableToLBS a = 13 | unsafeDupablePerformIO $ 14 | fmap LBS.fromStrict $ 15 | BS.create (sizeOf a) $ \p -> 16 | poke (castPtr p) a 17 | 18 | storableFromLBS :: forall a. Storable a => LBS.ByteString -> IO a 19 | storableFromLBS s = BS.unsafeUseAsCStringLen (LBS.toStrict s) $ \(p, len) -> 20 | if len == sizeOf (undefined :: a) 21 | then peek (castPtr p) 22 | else fail "Language.JavaScript.Inline.Examples.Utils.storableFromLBS" 23 | -------------------------------------------------------------------------------- /inline-js-examples/src/Language/JavaScript/Inline/Examples/Utils/LazyIO.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE StrictData #-} 2 | {-# LANGUAGE TupleSections #-} 3 | 4 | module Language.JavaScript.Inline.Examples.Utils.LazyIO where 5 | 6 | import Control.Concurrent.STM 7 | import Control.Concurrent.STM.TMQueue 8 | import Control.Exception 9 | import Control.Monad 10 | import qualified Data.ByteString as BS 11 | import qualified Data.ByteString.Lazy.Internal as LBS 12 | import Data.IORef 13 | import System.IO.Unsafe 14 | 15 | data LazyIO = LazyIO 16 | { lazyContent :: ~LBS.ByteString, 17 | lazyOnData :: BS.ByteString -> IO (), 18 | lazyOnEnd :: IO (), 19 | lazyOnError :: SomeException -> IO (), 20 | lazySetFinalizer :: IO () -> IO () 21 | } 22 | 23 | newLazyIO :: IO LazyIO 24 | newLazyIO = do 25 | q <- newTMQueueIO 26 | let w = do 27 | r <- atomically $ readTMQueue q 28 | case r of 29 | Just c -> do 30 | cs <- unsafeInterleaveIO w 31 | pure $ LBS.Chunk c cs 32 | _ -> pure LBS.Empty 33 | s <- unsafeInterleaveIO w 34 | fin_ref <- newIORef (pure ()) 35 | let fin = join $ atomicModifyIORef' fin_ref (pure (),) 36 | pure 37 | LazyIO 38 | { lazyContent = s, 39 | lazyOnData = atomically . writeTMQueue q, 40 | lazyOnEnd = do 41 | atomically $ closeTMQueue q 42 | fin, 43 | lazyOnError = \(SomeException err) -> do 44 | atomically $ do 45 | writeTMQueue q (throw err) 46 | closeTMQueue q 47 | fin, 48 | lazySetFinalizer = atomicWriteIORef fin_ref 49 | } 50 | -------------------------------------------------------------------------------- /inline-js-examples/src/Language/JavaScript/Inline/Examples/Wasm.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingVia #-} 2 | {-# LANGUAGE QuasiQuotes #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | {-# LANGUAGE ViewPatterns #-} 5 | 6 | module Language.JavaScript.Inline.Examples.Wasm where 7 | 8 | import Control.Exception 9 | import qualified Data.ByteString.Lazy as LBS 10 | import Foreign 11 | import Language.JavaScript.Inline 12 | import Language.JavaScript.Inline.Examples.Utils 13 | 14 | newtype I32 = I32 Word32 15 | deriving (Show, Storable) via Word32 16 | 17 | newtype I64 = I64 Word64 18 | deriving (Show, Storable) via Word64 19 | 20 | newtype F32 = F32 Float 21 | deriving (Show, Storable) via Float 22 | 23 | newtype F64 = F64 Double 24 | deriving (Show, Storable) via Double 25 | 26 | instance ToJS I32 where 27 | toJS x = [js| $buf.readUInt32LE() |] where buf = storableToLBS x 28 | 29 | instance FromJS I32 where 30 | rawJSType _ = RawBuffer 31 | toRawJSType _ = 32 | [js| x => { const buf = Buffer.allocUnsafe(4); buf.writeUInt32LE(x); return buf; } |] 33 | fromJS _ = storableFromLBS 34 | 35 | instance ToJS I64 where 36 | toJS x = [js| $buf.readBigUInt64LE() |] where buf = storableToLBS x 37 | 38 | instance FromJS I64 where 39 | rawJSType _ = RawBuffer 40 | toRawJSType _ = 41 | [js| x => { const buf = Buffer.allocUnsafe(8); buf.writeBigUInt64LE(x); return buf; } |] 42 | fromJS _ = storableFromLBS 43 | 44 | instance ToJS F32 where 45 | toJS x = [js| $buf.readFloatLE() |] where buf = storableToLBS x 46 | 47 | instance FromJS F32 where 48 | rawJSType _ = RawBuffer 49 | toRawJSType _ = 50 | [js| x => { const buf = Buffer.allocUnsafe(4); buf.writeFloatLE(x); return buf; } |] 51 | fromJS _ = storableFromLBS 52 | 53 | instance ToJS F64 where 54 | toJS x = [js| $buf.readDoubleLE() |] where buf = storableToLBS x 55 | 56 | instance FromJS F64 where 57 | rawJSType _ = RawBuffer 58 | toRawJSType _ = 59 | [js| x => { const buf = Buffer.allocUnsafe(8); buf.writeDoubleLE(x); return buf; } |] 60 | fromJS _ = storableFromLBS 61 | 62 | importNew :: Session -> IO JSVal 63 | importNew _session = eval _session [js| {} |] 64 | 65 | importAdd :: Export f => Session -> JSVal -> String -> String -> f -> IO () 66 | importAdd _session _import_obj (Aeson -> _import_module) (Aeson -> _import_name) _import_hs_func = 67 | do 68 | _import_js_func <- exportSync _session _import_hs_func 69 | evaluate 70 | =<< eval 71 | _session 72 | [js| 73 | if (!($_import_obj[$_import_module])) { 74 | $_import_obj[$_import_module] = {}; 75 | } 76 | $_import_obj[$_import_module][$_import_name] = $_import_js_func; 77 | |] 78 | 79 | wasmCompile :: Session -> LBS.ByteString -> IO JSVal 80 | wasmCompile _session _module_buf = 81 | eval _session [js| WebAssembly.compile($_module_buf) |] 82 | 83 | wasmInstantiate :: Session -> JSVal -> JSVal -> IO JSVal 84 | wasmInstantiate _session _module _import_obj = 85 | eval _session [js| WebAssembly.instantiate($_module, $_import_obj) |] 86 | 87 | exportGet :: Import f => Session -> JSVal -> String -> IO f 88 | exportGet _session _instance (Aeson -> _export_name) = do 89 | _export_js_func <- eval _session [js| $_instance.exports[$_export_name] |] 90 | pure $ importJSFunc _session _export_js_func 91 | -------------------------------------------------------------------------------- /inline-js-parser/index.mjs: -------------------------------------------------------------------------------- 1 | import * as acorn from "acorn"; 2 | import getStdin from "get-stdin"; 3 | 4 | function tokenize(s) { 5 | const toks = new Set(); 6 | acorn.parse(s, { 7 | ecmaVersion: "latest", 8 | onToken: (tok) => { 9 | if (tok.type.label === "name" && tok.value.startsWith("$")) { 10 | toks.add(tok.value.slice(1)); 11 | } 12 | }, 13 | }); 14 | return toks; 15 | } 16 | 17 | const s = await getStdin(); 18 | 19 | let toks, is_sync, is_expr; 20 | 21 | try { 22 | toks = tokenize(`() => (${s})`); 23 | is_sync = true; 24 | is_expr = true; 25 | } catch (_) { 26 | try { 27 | toks = tokenize(`() => {${s}}`); 28 | is_sync = true; 29 | is_expr = false; 30 | } catch (_) { 31 | try { 32 | toks = tokenize(`async () => (${s})`); 33 | is_sync = false; 34 | is_expr = true; 35 | } catch (_) { 36 | toks = tokenize(`async () => {${s}}`); 37 | is_sync = false; 38 | is_expr = false; 39 | } 40 | } 41 | } 42 | 43 | let o = `${is_sync ? "True" : "False"}\n${is_expr ? "True" : "False"}\n`; 44 | toks.forEach((tok) => { 45 | o = `${o}${tok}\n`; 46 | }); 47 | process.stdout.write(o); 48 | -------------------------------------------------------------------------------- /inline-js-parser/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inline-js-parser", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@discoveryjs/json-ext": { 8 | "version": "0.5.3", 9 | "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", 10 | "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==", 11 | "dev": true 12 | }, 13 | "@types/eslint": { 14 | "version": "7.28.0", 15 | "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz", 16 | "integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==", 17 | "dev": true, 18 | "requires": { 19 | "@types/estree": "*", 20 | "@types/json-schema": "*" 21 | } 22 | }, 23 | "@types/eslint-scope": { 24 | "version": "3.7.1", 25 | "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz", 26 | "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==", 27 | "dev": true, 28 | "requires": { 29 | "@types/eslint": "*", 30 | "@types/estree": "*" 31 | } 32 | }, 33 | "@types/estree": { 34 | "version": "0.0.50", 35 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", 36 | "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", 37 | "dev": true 38 | }, 39 | "@types/json-schema": { 40 | "version": "7.0.9", 41 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", 42 | "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", 43 | "dev": true 44 | }, 45 | "@types/node": { 46 | "version": "16.9.1", 47 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", 48 | "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", 49 | "dev": true 50 | }, 51 | "@webassemblyjs/ast": { 52 | "version": "1.11.1", 53 | "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", 54 | "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", 55 | "dev": true, 56 | "requires": { 57 | "@webassemblyjs/helper-numbers": "1.11.1", 58 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1" 59 | } 60 | }, 61 | "@webassemblyjs/floating-point-hex-parser": { 62 | "version": "1.11.1", 63 | "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", 64 | "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", 65 | "dev": true 66 | }, 67 | "@webassemblyjs/helper-api-error": { 68 | "version": "1.11.1", 69 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", 70 | "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", 71 | "dev": true 72 | }, 73 | "@webassemblyjs/helper-buffer": { 74 | "version": "1.11.1", 75 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", 76 | "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", 77 | "dev": true 78 | }, 79 | "@webassemblyjs/helper-numbers": { 80 | "version": "1.11.1", 81 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", 82 | "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", 83 | "dev": true, 84 | "requires": { 85 | "@webassemblyjs/floating-point-hex-parser": "1.11.1", 86 | "@webassemblyjs/helper-api-error": "1.11.1", 87 | "@xtuc/long": "4.2.2" 88 | } 89 | }, 90 | "@webassemblyjs/helper-wasm-bytecode": { 91 | "version": "1.11.1", 92 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", 93 | "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", 94 | "dev": true 95 | }, 96 | "@webassemblyjs/helper-wasm-section": { 97 | "version": "1.11.1", 98 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", 99 | "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", 100 | "dev": true, 101 | "requires": { 102 | "@webassemblyjs/ast": "1.11.1", 103 | "@webassemblyjs/helper-buffer": "1.11.1", 104 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1", 105 | "@webassemblyjs/wasm-gen": "1.11.1" 106 | } 107 | }, 108 | "@webassemblyjs/ieee754": { 109 | "version": "1.11.1", 110 | "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", 111 | "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", 112 | "dev": true, 113 | "requires": { 114 | "@xtuc/ieee754": "^1.2.0" 115 | } 116 | }, 117 | "@webassemblyjs/leb128": { 118 | "version": "1.11.1", 119 | "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", 120 | "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", 121 | "dev": true, 122 | "requires": { 123 | "@xtuc/long": "4.2.2" 124 | } 125 | }, 126 | "@webassemblyjs/utf8": { 127 | "version": "1.11.1", 128 | "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", 129 | "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", 130 | "dev": true 131 | }, 132 | "@webassemblyjs/wasm-edit": { 133 | "version": "1.11.1", 134 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", 135 | "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", 136 | "dev": true, 137 | "requires": { 138 | "@webassemblyjs/ast": "1.11.1", 139 | "@webassemblyjs/helper-buffer": "1.11.1", 140 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1", 141 | "@webassemblyjs/helper-wasm-section": "1.11.1", 142 | "@webassemblyjs/wasm-gen": "1.11.1", 143 | "@webassemblyjs/wasm-opt": "1.11.1", 144 | "@webassemblyjs/wasm-parser": "1.11.1", 145 | "@webassemblyjs/wast-printer": "1.11.1" 146 | } 147 | }, 148 | "@webassemblyjs/wasm-gen": { 149 | "version": "1.11.1", 150 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", 151 | "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", 152 | "dev": true, 153 | "requires": { 154 | "@webassemblyjs/ast": "1.11.1", 155 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1", 156 | "@webassemblyjs/ieee754": "1.11.1", 157 | "@webassemblyjs/leb128": "1.11.1", 158 | "@webassemblyjs/utf8": "1.11.1" 159 | } 160 | }, 161 | "@webassemblyjs/wasm-opt": { 162 | "version": "1.11.1", 163 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", 164 | "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", 165 | "dev": true, 166 | "requires": { 167 | "@webassemblyjs/ast": "1.11.1", 168 | "@webassemblyjs/helper-buffer": "1.11.1", 169 | "@webassemblyjs/wasm-gen": "1.11.1", 170 | "@webassemblyjs/wasm-parser": "1.11.1" 171 | } 172 | }, 173 | "@webassemblyjs/wasm-parser": { 174 | "version": "1.11.1", 175 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", 176 | "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", 177 | "dev": true, 178 | "requires": { 179 | "@webassemblyjs/ast": "1.11.1", 180 | "@webassemblyjs/helper-api-error": "1.11.1", 181 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1", 182 | "@webassemblyjs/ieee754": "1.11.1", 183 | "@webassemblyjs/leb128": "1.11.1", 184 | "@webassemblyjs/utf8": "1.11.1" 185 | } 186 | }, 187 | "@webassemblyjs/wast-printer": { 188 | "version": "1.11.1", 189 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", 190 | "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", 191 | "dev": true, 192 | "requires": { 193 | "@webassemblyjs/ast": "1.11.1", 194 | "@xtuc/long": "4.2.2" 195 | } 196 | }, 197 | "@webpack-cli/configtest": { 198 | "version": "1.0.4", 199 | "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.4.tgz", 200 | "integrity": "sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==", 201 | "dev": true 202 | }, 203 | "@webpack-cli/info": { 204 | "version": "1.3.0", 205 | "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.3.0.tgz", 206 | "integrity": "sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w==", 207 | "dev": true, 208 | "requires": { 209 | "envinfo": "^7.7.3" 210 | } 211 | }, 212 | "@webpack-cli/serve": { 213 | "version": "1.5.2", 214 | "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.2.tgz", 215 | "integrity": "sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==", 216 | "dev": true 217 | }, 218 | "@xtuc/ieee754": { 219 | "version": "1.2.0", 220 | "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", 221 | "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", 222 | "dev": true 223 | }, 224 | "@xtuc/long": { 225 | "version": "4.2.2", 226 | "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", 227 | "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", 228 | "dev": true 229 | }, 230 | "acorn": { 231 | "version": "8.5.0", 232 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", 233 | "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==" 234 | }, 235 | "acorn-import-assertions": { 236 | "version": "1.7.6", 237 | "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz", 238 | "integrity": "sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==", 239 | "dev": true 240 | }, 241 | "ajv": { 242 | "version": "6.12.6", 243 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 244 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 245 | "dev": true, 246 | "requires": { 247 | "fast-deep-equal": "^3.1.1", 248 | "fast-json-stable-stringify": "^2.0.0", 249 | "json-schema-traverse": "^0.4.1", 250 | "uri-js": "^4.2.2" 251 | } 252 | }, 253 | "ajv-keywords": { 254 | "version": "3.5.2", 255 | "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", 256 | "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", 257 | "dev": true 258 | }, 259 | "browserslist": { 260 | "version": "4.17.0", 261 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.0.tgz", 262 | "integrity": "sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==", 263 | "dev": true, 264 | "requires": { 265 | "caniuse-lite": "^1.0.30001254", 266 | "colorette": "^1.3.0", 267 | "electron-to-chromium": "^1.3.830", 268 | "escalade": "^3.1.1", 269 | "node-releases": "^1.1.75" 270 | } 271 | }, 272 | "buffer-from": { 273 | "version": "1.1.2", 274 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 275 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 276 | "dev": true 277 | }, 278 | "caniuse-lite": { 279 | "version": "1.0.30001257", 280 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz", 281 | "integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==", 282 | "dev": true 283 | }, 284 | "chrome-trace-event": { 285 | "version": "1.0.3", 286 | "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", 287 | "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", 288 | "dev": true 289 | }, 290 | "clone-deep": { 291 | "version": "4.0.1", 292 | "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", 293 | "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", 294 | "dev": true, 295 | "requires": { 296 | "is-plain-object": "^2.0.4", 297 | "kind-of": "^6.0.2", 298 | "shallow-clone": "^3.0.0" 299 | } 300 | }, 301 | "colorette": { 302 | "version": "1.4.0", 303 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", 304 | "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", 305 | "dev": true 306 | }, 307 | "commander": { 308 | "version": "2.20.3", 309 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 310 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 311 | "dev": true 312 | }, 313 | "cross-spawn": { 314 | "version": "7.0.3", 315 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 316 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 317 | "dev": true, 318 | "requires": { 319 | "path-key": "^3.1.0", 320 | "shebang-command": "^2.0.0", 321 | "which": "^2.0.1" 322 | } 323 | }, 324 | "electron-to-chromium": { 325 | "version": "1.3.836", 326 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.836.tgz", 327 | "integrity": "sha512-Ney3pHOJBWkG/AqYjrW0hr2AUCsao+2uvq9HUlRP8OlpSdk/zOHOUJP7eu0icDvePC9DlgffuelP4TnOJmMRUg==", 328 | "dev": true 329 | }, 330 | "enhanced-resolve": { 331 | "version": "5.8.2", 332 | "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz", 333 | "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==", 334 | "dev": true, 335 | "requires": { 336 | "graceful-fs": "^4.2.4", 337 | "tapable": "^2.2.0" 338 | } 339 | }, 340 | "envinfo": { 341 | "version": "7.8.1", 342 | "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", 343 | "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", 344 | "dev": true 345 | }, 346 | "es-module-lexer": { 347 | "version": "0.7.1", 348 | "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz", 349 | "integrity": "sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==", 350 | "dev": true 351 | }, 352 | "escalade": { 353 | "version": "3.1.1", 354 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 355 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 356 | "dev": true 357 | }, 358 | "eslint-scope": { 359 | "version": "5.1.1", 360 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", 361 | "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", 362 | "dev": true, 363 | "requires": { 364 | "esrecurse": "^4.3.0", 365 | "estraverse": "^4.1.1" 366 | } 367 | }, 368 | "esrecurse": { 369 | "version": "4.3.0", 370 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 371 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 372 | "dev": true, 373 | "requires": { 374 | "estraverse": "^5.2.0" 375 | }, 376 | "dependencies": { 377 | "estraverse": { 378 | "version": "5.2.0", 379 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", 380 | "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", 381 | "dev": true 382 | } 383 | } 384 | }, 385 | "estraverse": { 386 | "version": "4.3.0", 387 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 388 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 389 | "dev": true 390 | }, 391 | "events": { 392 | "version": "3.3.0", 393 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 394 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 395 | "dev": true 396 | }, 397 | "execa": { 398 | "version": "5.1.1", 399 | "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", 400 | "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", 401 | "dev": true, 402 | "requires": { 403 | "cross-spawn": "^7.0.3", 404 | "get-stream": "^6.0.0", 405 | "human-signals": "^2.1.0", 406 | "is-stream": "^2.0.0", 407 | "merge-stream": "^2.0.0", 408 | "npm-run-path": "^4.0.1", 409 | "onetime": "^5.1.2", 410 | "signal-exit": "^3.0.3", 411 | "strip-final-newline": "^2.0.0" 412 | } 413 | }, 414 | "fast-deep-equal": { 415 | "version": "3.1.3", 416 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 417 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 418 | "dev": true 419 | }, 420 | "fast-json-stable-stringify": { 421 | "version": "2.1.0", 422 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 423 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 424 | "dev": true 425 | }, 426 | "fastest-levenshtein": { 427 | "version": "1.0.12", 428 | "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", 429 | "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", 430 | "dev": true 431 | }, 432 | "find-up": { 433 | "version": "4.1.0", 434 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 435 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 436 | "dev": true, 437 | "requires": { 438 | "locate-path": "^5.0.0", 439 | "path-exists": "^4.0.0" 440 | } 441 | }, 442 | "function-bind": { 443 | "version": "1.1.1", 444 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 445 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 446 | "dev": true 447 | }, 448 | "get-stdin": { 449 | "version": "9.0.0", 450 | "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", 451 | "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==" 452 | }, 453 | "get-stream": { 454 | "version": "6.0.1", 455 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", 456 | "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", 457 | "dev": true 458 | }, 459 | "glob-to-regexp": { 460 | "version": "0.4.1", 461 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 462 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 463 | "dev": true 464 | }, 465 | "graceful-fs": { 466 | "version": "4.2.8", 467 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", 468 | "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", 469 | "dev": true 470 | }, 471 | "has": { 472 | "version": "1.0.3", 473 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 474 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 475 | "dev": true, 476 | "requires": { 477 | "function-bind": "^1.1.1" 478 | } 479 | }, 480 | "has-flag": { 481 | "version": "4.0.0", 482 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 483 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 484 | "dev": true 485 | }, 486 | "human-signals": { 487 | "version": "2.1.0", 488 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", 489 | "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", 490 | "dev": true 491 | }, 492 | "import-local": { 493 | "version": "3.0.2", 494 | "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", 495 | "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", 496 | "dev": true, 497 | "requires": { 498 | "pkg-dir": "^4.2.0", 499 | "resolve-cwd": "^3.0.0" 500 | } 501 | }, 502 | "interpret": { 503 | "version": "2.2.0", 504 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", 505 | "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", 506 | "dev": true 507 | }, 508 | "is-core-module": { 509 | "version": "2.6.0", 510 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", 511 | "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", 512 | "dev": true, 513 | "requires": { 514 | "has": "^1.0.3" 515 | } 516 | }, 517 | "is-plain-object": { 518 | "version": "2.0.4", 519 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 520 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 521 | "dev": true, 522 | "requires": { 523 | "isobject": "^3.0.1" 524 | } 525 | }, 526 | "is-stream": { 527 | "version": "2.0.1", 528 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", 529 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", 530 | "dev": true 531 | }, 532 | "isexe": { 533 | "version": "2.0.0", 534 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 535 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 536 | "dev": true 537 | }, 538 | "isobject": { 539 | "version": "3.0.1", 540 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 541 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 542 | "dev": true 543 | }, 544 | "jest-worker": { 545 | "version": "27.2.0", 546 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.0.tgz", 547 | "integrity": "sha512-laB0ZVIBz+voh/QQy9dmUuuDsadixeerrKqyVpgPz+CCWiOYjOBabUXHIXZhsdvkWbLqSHbgkAHWl5cg24Q6RA==", 548 | "dev": true, 549 | "requires": { 550 | "@types/node": "*", 551 | "merge-stream": "^2.0.0", 552 | "supports-color": "^8.0.0" 553 | } 554 | }, 555 | "json-parse-better-errors": { 556 | "version": "1.0.2", 557 | "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", 558 | "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", 559 | "dev": true 560 | }, 561 | "json-schema-traverse": { 562 | "version": "0.4.1", 563 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 564 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 565 | "dev": true 566 | }, 567 | "kind-of": { 568 | "version": "6.0.3", 569 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 570 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 571 | "dev": true 572 | }, 573 | "loader-runner": { 574 | "version": "4.2.0", 575 | "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", 576 | "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", 577 | "dev": true 578 | }, 579 | "locate-path": { 580 | "version": "5.0.0", 581 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 582 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 583 | "dev": true, 584 | "requires": { 585 | "p-locate": "^4.1.0" 586 | } 587 | }, 588 | "merge-stream": { 589 | "version": "2.0.0", 590 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 591 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 592 | "dev": true 593 | }, 594 | "mime-db": { 595 | "version": "1.49.0", 596 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", 597 | "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", 598 | "dev": true 599 | }, 600 | "mime-types": { 601 | "version": "2.1.32", 602 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", 603 | "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", 604 | "dev": true, 605 | "requires": { 606 | "mime-db": "1.49.0" 607 | } 608 | }, 609 | "mimic-fn": { 610 | "version": "2.1.0", 611 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 612 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 613 | "dev": true 614 | }, 615 | "neo-async": { 616 | "version": "2.6.2", 617 | "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", 618 | "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", 619 | "dev": true 620 | }, 621 | "node-releases": { 622 | "version": "1.1.75", 623 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", 624 | "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", 625 | "dev": true 626 | }, 627 | "npm-run-path": { 628 | "version": "4.0.1", 629 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 630 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 631 | "dev": true, 632 | "requires": { 633 | "path-key": "^3.0.0" 634 | } 635 | }, 636 | "onetime": { 637 | "version": "5.1.2", 638 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 639 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 640 | "dev": true, 641 | "requires": { 642 | "mimic-fn": "^2.1.0" 643 | } 644 | }, 645 | "p-limit": { 646 | "version": "3.1.0", 647 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 648 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 649 | "dev": true, 650 | "requires": { 651 | "yocto-queue": "^0.1.0" 652 | } 653 | }, 654 | "p-locate": { 655 | "version": "4.1.0", 656 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 657 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 658 | "dev": true, 659 | "requires": { 660 | "p-limit": "^2.2.0" 661 | }, 662 | "dependencies": { 663 | "p-limit": { 664 | "version": "2.3.0", 665 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 666 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 667 | "dev": true, 668 | "requires": { 669 | "p-try": "^2.0.0" 670 | } 671 | } 672 | } 673 | }, 674 | "p-try": { 675 | "version": "2.2.0", 676 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 677 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 678 | "dev": true 679 | }, 680 | "path-exists": { 681 | "version": "4.0.0", 682 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 683 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 684 | "dev": true 685 | }, 686 | "path-key": { 687 | "version": "3.1.1", 688 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 689 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 690 | "dev": true 691 | }, 692 | "path-parse": { 693 | "version": "1.0.7", 694 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 695 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 696 | "dev": true 697 | }, 698 | "pkg-dir": { 699 | "version": "4.2.0", 700 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 701 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 702 | "dev": true, 703 | "requires": { 704 | "find-up": "^4.0.0" 705 | } 706 | }, 707 | "punycode": { 708 | "version": "2.1.1", 709 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 710 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 711 | "dev": true 712 | }, 713 | "randombytes": { 714 | "version": "2.1.0", 715 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 716 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 717 | "dev": true, 718 | "requires": { 719 | "safe-buffer": "^5.1.0" 720 | } 721 | }, 722 | "rechoir": { 723 | "version": "0.7.1", 724 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", 725 | "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", 726 | "dev": true, 727 | "requires": { 728 | "resolve": "^1.9.0" 729 | } 730 | }, 731 | "resolve": { 732 | "version": "1.20.0", 733 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 734 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 735 | "dev": true, 736 | "requires": { 737 | "is-core-module": "^2.2.0", 738 | "path-parse": "^1.0.6" 739 | } 740 | }, 741 | "resolve-cwd": { 742 | "version": "3.0.0", 743 | "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", 744 | "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", 745 | "dev": true, 746 | "requires": { 747 | "resolve-from": "^5.0.0" 748 | } 749 | }, 750 | "resolve-from": { 751 | "version": "5.0.0", 752 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", 753 | "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", 754 | "dev": true 755 | }, 756 | "safe-buffer": { 757 | "version": "5.2.1", 758 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 759 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 760 | "dev": true 761 | }, 762 | "schema-utils": { 763 | "version": "3.1.1", 764 | "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", 765 | "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", 766 | "dev": true, 767 | "requires": { 768 | "@types/json-schema": "^7.0.8", 769 | "ajv": "^6.12.5", 770 | "ajv-keywords": "^3.5.2" 771 | } 772 | }, 773 | "serialize-javascript": { 774 | "version": "6.0.0", 775 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 776 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 777 | "dev": true, 778 | "requires": { 779 | "randombytes": "^2.1.0" 780 | } 781 | }, 782 | "shallow-clone": { 783 | "version": "3.0.1", 784 | "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", 785 | "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", 786 | "dev": true, 787 | "requires": { 788 | "kind-of": "^6.0.2" 789 | } 790 | }, 791 | "shebang-command": { 792 | "version": "2.0.0", 793 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 794 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 795 | "dev": true, 796 | "requires": { 797 | "shebang-regex": "^3.0.0" 798 | } 799 | }, 800 | "shebang-regex": { 801 | "version": "3.0.0", 802 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 803 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 804 | "dev": true 805 | }, 806 | "signal-exit": { 807 | "version": "3.0.3", 808 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 809 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 810 | "dev": true 811 | }, 812 | "source-map": { 813 | "version": "0.6.1", 814 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 815 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 816 | "dev": true 817 | }, 818 | "source-map-support": { 819 | "version": "0.5.20", 820 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", 821 | "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", 822 | "dev": true, 823 | "requires": { 824 | "buffer-from": "^1.0.0", 825 | "source-map": "^0.6.0" 826 | } 827 | }, 828 | "strip-final-newline": { 829 | "version": "2.0.0", 830 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 831 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", 832 | "dev": true 833 | }, 834 | "supports-color": { 835 | "version": "8.1.1", 836 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 837 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 838 | "dev": true, 839 | "requires": { 840 | "has-flag": "^4.0.0" 841 | } 842 | }, 843 | "tapable": { 844 | "version": "2.2.1", 845 | "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", 846 | "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", 847 | "dev": true 848 | }, 849 | "terser": { 850 | "version": "5.8.0", 851 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.8.0.tgz", 852 | "integrity": "sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==", 853 | "dev": true, 854 | "requires": { 855 | "commander": "^2.20.0", 856 | "source-map": "~0.7.2", 857 | "source-map-support": "~0.5.20" 858 | }, 859 | "dependencies": { 860 | "source-map": { 861 | "version": "0.7.3", 862 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 863 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 864 | "dev": true 865 | } 866 | } 867 | }, 868 | "terser-webpack-plugin": { 869 | "version": "5.2.4", 870 | "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz", 871 | "integrity": "sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA==", 872 | "dev": true, 873 | "requires": { 874 | "jest-worker": "^27.0.6", 875 | "p-limit": "^3.1.0", 876 | "schema-utils": "^3.1.1", 877 | "serialize-javascript": "^6.0.0", 878 | "source-map": "^0.6.1", 879 | "terser": "^5.7.2" 880 | } 881 | }, 882 | "uri-js": { 883 | "version": "4.4.1", 884 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 885 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 886 | "dev": true, 887 | "requires": { 888 | "punycode": "^2.1.0" 889 | } 890 | }, 891 | "v8-compile-cache": { 892 | "version": "2.3.0", 893 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", 894 | "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", 895 | "dev": true 896 | }, 897 | "watchpack": { 898 | "version": "2.2.0", 899 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", 900 | "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", 901 | "dev": true, 902 | "requires": { 903 | "glob-to-regexp": "^0.4.1", 904 | "graceful-fs": "^4.1.2" 905 | } 906 | }, 907 | "webpack": { 908 | "version": "5.52.1", 909 | "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.52.1.tgz", 910 | "integrity": "sha512-wkGb0hLfrS7ML3n2xIKfUIwHbjB6gxwQHyLmVHoAqEQBw+nWo+G6LoHL098FEXqahqximsntjBLuewStrnJk0g==", 911 | "dev": true, 912 | "requires": { 913 | "@types/eslint-scope": "^3.7.0", 914 | "@types/estree": "^0.0.50", 915 | "@webassemblyjs/ast": "1.11.1", 916 | "@webassemblyjs/wasm-edit": "1.11.1", 917 | "@webassemblyjs/wasm-parser": "1.11.1", 918 | "acorn": "^8.4.1", 919 | "acorn-import-assertions": "^1.7.6", 920 | "browserslist": "^4.14.5", 921 | "chrome-trace-event": "^1.0.2", 922 | "enhanced-resolve": "^5.8.0", 923 | "es-module-lexer": "^0.7.1", 924 | "eslint-scope": "5.1.1", 925 | "events": "^3.2.0", 926 | "glob-to-regexp": "^0.4.1", 927 | "graceful-fs": "^4.2.4", 928 | "json-parse-better-errors": "^1.0.2", 929 | "loader-runner": "^4.2.0", 930 | "mime-types": "^2.1.27", 931 | "neo-async": "^2.6.2", 932 | "schema-utils": "^3.1.0", 933 | "tapable": "^2.1.1", 934 | "terser-webpack-plugin": "^5.1.3", 935 | "watchpack": "^2.2.0", 936 | "webpack-sources": "^3.2.0" 937 | } 938 | }, 939 | "webpack-cli": { 940 | "version": "4.8.0", 941 | "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.8.0.tgz", 942 | "integrity": "sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==", 943 | "dev": true, 944 | "requires": { 945 | "@discoveryjs/json-ext": "^0.5.0", 946 | "@webpack-cli/configtest": "^1.0.4", 947 | "@webpack-cli/info": "^1.3.0", 948 | "@webpack-cli/serve": "^1.5.2", 949 | "colorette": "^1.2.1", 950 | "commander": "^7.0.0", 951 | "execa": "^5.0.0", 952 | "fastest-levenshtein": "^1.0.12", 953 | "import-local": "^3.0.2", 954 | "interpret": "^2.2.0", 955 | "rechoir": "^0.7.0", 956 | "v8-compile-cache": "^2.2.0", 957 | "webpack-merge": "^5.7.3" 958 | }, 959 | "dependencies": { 960 | "commander": { 961 | "version": "7.2.0", 962 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 963 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", 964 | "dev": true 965 | } 966 | } 967 | }, 968 | "webpack-merge": { 969 | "version": "5.8.0", 970 | "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", 971 | "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", 972 | "dev": true, 973 | "requires": { 974 | "clone-deep": "^4.0.1", 975 | "wildcard": "^2.0.0" 976 | } 977 | }, 978 | "webpack-sources": { 979 | "version": "3.2.0", 980 | "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.0.tgz", 981 | "integrity": "sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==", 982 | "dev": true 983 | }, 984 | "which": { 985 | "version": "2.0.2", 986 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 987 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 988 | "dev": true, 989 | "requires": { 990 | "isexe": "^2.0.0" 991 | } 992 | }, 993 | "wildcard": { 994 | "version": "2.0.0", 995 | "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", 996 | "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", 997 | "dev": true 998 | }, 999 | "yocto-queue": { 1000 | "version": "0.1.0", 1001 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1002 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1003 | "dev": true 1004 | } 1005 | } 1006 | } 1007 | -------------------------------------------------------------------------------- /inline-js-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inline-js-parser", 3 | "version": "0.0.1", 4 | "main": "./index.mjs", 5 | "type": "module", 6 | "scripts": { 7 | "build": "webpack --entry ./index.mjs --experiments-top-level-await --mode production --progress --target node" 8 | }, 9 | "files": [ 10 | "index.mjs" 11 | ], 12 | "dependencies": { 13 | "acorn": "^8.5.0", 14 | "get-stdin": "^9.0.0" 15 | }, 16 | "devDependencies": { 17 | "webpack": "^5.52.1", 18 | "webpack-cli": "^4.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /inline-js-tests/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for `inline-js-tests` 2 | -------------------------------------------------------------------------------- /inline-js-tests/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 EURL Tweag 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of Tweag I/O nor the names of other contributors may be 16 | used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /inline-js-tests/README.md: -------------------------------------------------------------------------------- 1 | # `inline-js-tests` 2 | -------------------------------------------------------------------------------- /inline-js-tests/inline-js-tests.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: inline-js-tests 3 | version: 0.0.1.0 4 | synopsis: Call JavaScript from Haskell. 5 | description: 6 | Please see for details. 7 | 8 | category: Web 9 | homepage: https://github.com/tweag/inline-js#readme 10 | bug-reports: https://github.com/tweag/inline-js/issues 11 | maintainer: Cheng Shao 12 | copyright: (c) 2018 Tweag I/O 13 | license: BSD-3-Clause 14 | license-file: LICENSE 15 | build-type: Simple 16 | extra-source-files: 17 | CHANGELOG.md 18 | LICENSE 19 | README.md 20 | 21 | data-files: jsbits/node_modules/left-pad/index.js 22 | 23 | source-repository head 24 | type: git 25 | location: https://github.com/tweag/inline-js 26 | 27 | library 28 | build-depends: base 29 | default-language: Haskell2010 30 | 31 | test-suite inline-js-tests 32 | type: exitcode-stdio-1.0 33 | main-is: inline-js-tests.hs 34 | other-modules: 35 | Language.JavaScript.Inline.Tests.Utils.Aeson 36 | Paths_inline_js_tests 37 | 38 | autogen-modules: Paths_inline_js_tests 39 | hs-source-dirs: test 40 | ghc-options: -Wall -threaded -rtsopts 41 | build-depends: 42 | , aeson 43 | , base >=4.12 && <5 44 | , bytestring 45 | , directory 46 | , filepath 47 | , inline-js 48 | , inline-js-examples 49 | , process 50 | , QuickCheck 51 | , quickcheck-instances 52 | , splitmix 53 | , tasty 54 | , tasty-hunit 55 | , tasty-quickcheck 56 | , temporary 57 | 58 | default-language: Haskell2010 59 | -------------------------------------------------------------------------------- /inline-js-tests/jsbits/node_modules/left-pad/index.js: -------------------------------------------------------------------------------- 1 | /* This program is free software. It comes without any warranty, to 2 | * the extent permitted by applicable law. You can redistribute it 3 | * and/or modify it under the terms of the Do What The Fuck You Want 4 | * To Public License, Version 2, as published by Sam Hocevar. See 5 | * http://www.wtfpl.net/ for more details. */ 6 | 'use strict'; 7 | module.exports = leftPad; 8 | 9 | var cache = [ 10 | '', 11 | ' ', 12 | ' ', 13 | ' ', 14 | ' ', 15 | ' ', 16 | ' ', 17 | ' ', 18 | ' ', 19 | ' ' 20 | ]; 21 | 22 | function leftPad (str, len, ch) { 23 | // convert `str` to a `string` 24 | str = str + ''; 25 | // `len` is the `pad`'s length now 26 | len = len - str.length; 27 | // doesn't need to pad 28 | if (len <= 0) return str; 29 | // `ch` defaults to `' '` 30 | if (!ch && ch !== 0) ch = ' '; 31 | // convert `ch` to a `string` cuz it could be a number 32 | ch = ch + ''; 33 | // cache common use cases 34 | if (ch === ' ' && len < 10) return cache[len] + str; 35 | // `pad` starts with an empty string 36 | var pad = ''; 37 | // loop 38 | while (true) { 39 | // add `ch` to `pad` if `len` is odd 40 | if (len & 1) pad += ch; 41 | // divide `len` by 2, ditch the remainder 42 | len >>= 1; 43 | // "double" the `ch` so this operation count grows logarithmically on `len` 44 | // each time `ch` is "doubled", the `len` would need to be "doubled" too 45 | // similar to finding a value in binary search tree, hence O(log(n)) 46 | if (len) ch += ch; 47 | // `len` is 0, exit the loop 48 | else break; 49 | } 50 | // pad `str`! 51 | return pad + str; 52 | } 53 | -------------------------------------------------------------------------------- /inline-js-tests/test/Language/JavaScript/Inline/Tests/Utils/Aeson.hs: -------------------------------------------------------------------------------- 1 | module Language.JavaScript.Inline.Tests.Utils.Aeson 2 | ( genValueWithSize, 3 | ) 4 | where 5 | 6 | import qualified Data.Aeson as A 7 | import GHC.Exts 8 | import qualified Test.QuickCheck as Q 9 | import Test.QuickCheck.Instances () 10 | 11 | genValueWithSize :: Int -> Q.Gen A.Value 12 | genValueWithSize n 13 | | n > 1 = do 14 | ns <- genSplitSizes n 15 | Q.oneof 16 | [ A.Object . fromList 17 | <$> sequence 18 | [(,) <$> Q.arbitrary <*> genValueWithSize n' | n' <- ns], 19 | A.Array . fromList <$> sequence [genValueWithSize n' | n' <- ns] 20 | ] 21 | | n == 1 = 22 | Q.oneof 23 | [ A.String <$> Q.arbitrary, 24 | A.Number <$> Q.arbitrary, 25 | A.Bool <$> Q.arbitrary, 26 | pure A.Null 27 | ] 28 | | otherwise = error $ "genValueWithSize: invalid size " <> show n 29 | 30 | genSplitSizes :: Int -> Q.Gen [Int] 31 | genSplitSizes = w [] 32 | where 33 | w acc r = do 34 | n' <- Q.choose (1, r) 35 | if n' == r then pure (n' : acc) else w (n' : acc) (r - n') 36 | -------------------------------------------------------------------------------- /inline-js-tests/test/inline-js-tests.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingVia #-} 2 | {-# LANGUAGE OverloadedLists #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE QuasiQuotes #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | {-# LANGUAGE UndecidableInstances #-} 9 | 10 | import Control.Exception hiding (assert) 11 | import Control.Monad 12 | import qualified Data.Aeson as A 13 | import qualified Data.ByteString.Lazy as LBS 14 | import Data.Foldable 15 | import Data.String 16 | import Foreign 17 | import Language.JavaScript.Inline 18 | import Language.JavaScript.Inline.Examples.Stream 19 | import Language.JavaScript.Inline.Tests.Utils.Aeson 20 | import Paths_inline_js_tests 21 | import System.Directory 22 | import System.FilePath 23 | import System.IO 24 | import System.IO.Temp 25 | import System.Random.SplitMix 26 | import Test.Tasty 27 | import Test.Tasty.HUnit hiding (assert) 28 | import Test.Tasty.QuickCheck 29 | 30 | main :: IO () 31 | main = do 32 | data_dir <- getDataDir 33 | defaultMain $ 34 | testGroup 35 | "kitchen-sink" 36 | [ testCase "Session: new/close" $ withDefaultSession $ \_ -> pure (), 37 | testCase "Session: delay" $ 38 | withDefaultSession $ \s -> do 39 | vs <- 40 | replicateM 0x1000 $ 41 | (eval @()) s "new Promise((resolve) => setTimeout(resolve, 1000))" 42 | for_ vs evaluate, 43 | testCase "Session: roundtrip" $ 44 | withDefaultSession $ \s -> 45 | replicateM_ 0x10 $ do 46 | let buf = "asdf" 47 | buf' <- (eval @LBS.ByteString) s $ toJS buf 48 | buf' @?= buf, 49 | withResource (newSession defaultConfig) killSession $ \m -> 50 | testProperty "JSON" $ 51 | forAll (V <$> genValueWithSize 0x400) $ \v -> 52 | ioProperty $ do 53 | s <- m 54 | v' <- eval s [js| $v |] 55 | pure $ v' == v, 56 | testCase "catch-use-after-free" $ do 57 | (err :: Aeson String) <- 58 | bracket (newSession defaultConfig) killSession $ 59 | \s -> 60 | eval 61 | s 62 | [js| new Promise(resolve => setTimeout(resolve, 8000, "asdf")) |] 63 | result <- catch (False <$ evaluate err) $ \SessionClosed -> pure True 64 | assertBool "" result, 65 | withResource 66 | ( newSession 67 | defaultConfig 68 | { nodeModules = Just $ data_dir "jsbits" "node_modules" 69 | } 70 | ) 71 | killSession 72 | $ \m -> testCase "left-pad" $ do 73 | s <- m 74 | (Aeson r :: Aeson String) <- 75 | eval 76 | s 77 | [js| require('left-pad')('foo', 5) |] 78 | r @?= " foo", 79 | testCase "expr" $ 80 | withDefaultSession $ \s -> replicateM_ 0x10 $ do 81 | let x = I 6 82 | y = I 7 83 | r <- eval s [js| $x * $y |] 84 | r @?= I 42, 85 | testCase "import" $ 86 | withDefaultSession $ \s -> replicateM_ 0x10 $ do 87 | v <- eval s [js| (x, y) => x * y |] 88 | let f = importJSFunc s v 89 | r <- f (I 6) (I 7) 90 | r @?= I 42, 91 | testCase "export" $ 92 | withDefaultSession $ \s -> replicateM_ 0x10 $ do 93 | let f :: V -> V -> IO V 94 | f (V x) (V y) = pure $ V $ A.Array [x, y] 95 | v <- export s f 96 | let x = V $ A.String "asdf" 97 | y = V $ A.String "233" 98 | r <- eval s [js| $v($x, $y) |] 99 | r @?= V (A.Array [A.String "asdf", A.String "233"]) 100 | freeJSVal v, 101 | testCase "exportSync" $ 102 | withDefaultSession $ \s -> replicateM_ 0x10 $ do 103 | let f :: V -> V -> IO V 104 | f x y = eval s [js| [$x, $y] |] 105 | v <- exportSync s f 106 | let x = V $ A.String "asdf" 107 | y = V $ A.String "233" 108 | r <- eval s [js| $v($x, $y) |] 109 | r @?= V (A.Array [A.String "asdf", A.String "233"]) 110 | freeJSVal v, 111 | testCase "stream" $ 112 | withDefaultSession $ \s -> 113 | bracket (randomFile 0x100000) removeFile $ \p -> do 114 | let js_path = fromString @EncodedString p 115 | js_stream <- 116 | eval 117 | s 118 | [js| 119 | const fs = require("fs"); 120 | return fs.createReadStream($js_path); 121 | |] 122 | js_content <- lazyStream s js_stream 123 | hs_content <- LBS.readFile p 124 | js_content @?= hs_content 125 | ] 126 | 127 | newtype I = I Int 128 | deriving (Eq, Show) 129 | deriving (ToJS, FromJS) via (Aeson Int) 130 | 131 | newtype V = V A.Value 132 | deriving (Eq, Show) 133 | deriving (ToJS, FromJS) via (Aeson A.Value) 134 | 135 | withDefaultSession :: (Session -> IO a) -> IO a 136 | withDefaultSession = withSession defaultConfig 137 | 138 | randomFile :: Int -> IO FilePath 139 | randomFile size = do 140 | tmpdir <- getCanonicalTemporaryDirectory 141 | (p, h) <- openBinaryTempFile tmpdir "inline-js" 142 | gen <- newSMGen 143 | alloca $ \ptr -> 144 | let w _gen _size 145 | | _size >= 8 = do 146 | let (x, _gen') = nextWord64 _gen 147 | poke ptr x 148 | hPutBuf h ptr 8 149 | w _gen' (_size - 8) 150 | | otherwise = pure () 151 | in w gen size 152 | hClose h 153 | pure p 154 | -------------------------------------------------------------------------------- /inline-js/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for `inline-js` 2 | -------------------------------------------------------------------------------- /inline-js/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 EURL Tweag 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of Tweag I/O nor the names of other contributors may be 16 | used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /inline-js/README.md: -------------------------------------------------------------------------------- 1 | # `inline-js` 2 | -------------------------------------------------------------------------------- /inline-js/inline-js.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: inline-js 3 | version: 0.0.1.0 4 | synopsis: Call JavaScript from Haskell. 5 | description: 6 | Please see for details. 7 | 8 | category: Web 9 | homepage: https://github.com/tweag/inline-js#readme 10 | bug-reports: https://github.com/tweag/inline-js/issues 11 | maintainer: Cheng Shao 12 | copyright: (c) 2018 Tweag I/O 13 | license: BSD-3-Clause 14 | license-file: LICENSE 15 | build-type: Simple 16 | extra-source-files: 17 | CHANGELOG.md 18 | jsbits/main.js 19 | LICENSE 20 | README.md 21 | 22 | source-repository head 23 | type: git 24 | location: https://github.com/tweag/inline-js 25 | 26 | library 27 | exposed-modules: Language.JavaScript.Inline 28 | other-modules: 29 | Language.JavaScript.Inline.Aeson 30 | Language.JavaScript.Inline.JSParse 31 | Language.JavaScript.Inline.TH 32 | 33 | hs-source-dirs: src 34 | ghc-options: -Wall 35 | build-depends: 36 | , aeson <2 37 | , base >=4.12 && <5 38 | , bytestring 39 | , file-embed 40 | , inline-js-core 41 | , template-haskell 42 | , unliftio 43 | 44 | default-language: Haskell2010 45 | -------------------------------------------------------------------------------- /inline-js/src/Language/JavaScript/Inline.hs: -------------------------------------------------------------------------------- 1 | module Language.JavaScript.Inline 2 | ( -- * Core functionalities 3 | module Language.JavaScript.Inline.Core, 4 | 5 | -- * @aeson@ support 6 | Aeson (..), 7 | 8 | -- * QuasiQuoters for inline JavaScript 9 | js, 10 | ) 11 | where 12 | 13 | import Language.JavaScript.Inline.Aeson 14 | import Language.JavaScript.Inline.Core 15 | import Language.JavaScript.Inline.TH 16 | -------------------------------------------------------------------------------- /inline-js/src/Language/JavaScript/Inline/Aeson.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE TypeFamilies #-} 3 | 4 | module Language.JavaScript.Inline.Aeson where 5 | 6 | import qualified Data.Aeson as A 7 | import Language.JavaScript.Inline.Core 8 | 9 | -- | If a Haskell type @a@ has 'A.ToJSON' and 'A.FromJSON' instances, then we 10 | -- can derive 'ToJS' and 'FromJS' instances for it using: 11 | -- 12 | -- 1. @deriving (ToJS, FromJS) via (Aeson a)@, using the @DerivingVia@ extension 13 | -- 2. @deriving (ToJS, FromJS)@, using the @GeneralizedNewtypeDeriving@ 14 | -- extension 15 | newtype Aeson a = Aeson 16 | { unAeson :: a 17 | } 18 | deriving (Show) 19 | 20 | instance A.ToJSON a => ToJS (Aeson a) where 21 | toJS = toJS . EncodedJSON . A.encode . unAeson 22 | 23 | instance A.FromJSON a => FromJS (Aeson a) where 24 | rawJSType _ = RawJSON 25 | toRawJSType _ = "a => a" 26 | fromJS _ s = case A.eitherDecode' s of 27 | Left err -> fail err 28 | Right a -> pure $ Aeson a 29 | -------------------------------------------------------------------------------- /inline-js/src/Language/JavaScript/Inline/JSParse.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | 4 | module Language.JavaScript.Inline.JSParse (jsParse) where 5 | 6 | import Data.ByteString (ByteString) 7 | import qualified Data.ByteString as BS 8 | import Data.FileEmbed 9 | import Language.JavaScript.Inline.Core 10 | import UnliftIO 11 | import UnliftIO.Process 12 | 13 | jsParse :: String -> IO (Bool, Bool, [String]) 14 | jsParse src = 15 | do 16 | let node_path = nodePath defaultConfig 17 | o <- withSystemTempFile "main.js" $ 18 | \p h -> do 19 | hClose h 20 | BS.writeFile p parserSrc 21 | readProcess node_path [p] src 22 | let is_sync_str : is_expr_str : toks = lines o 23 | !is_sync = read is_sync_str 24 | !is_expr = read is_expr_str 25 | pure (is_sync, is_expr, toks) 26 | 27 | parserSrc :: ByteString 28 | parserSrc = $(makeRelativeToProject "jsbits/main.js" >>= embedFile) 29 | -------------------------------------------------------------------------------- /inline-js/src/Language/JavaScript/Inline/TH.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | module Language.JavaScript.Inline.TH (js) where 4 | 5 | import Data.List 6 | import Data.String 7 | import Language.Haskell.TH 8 | import Language.Haskell.TH.Quote 9 | import Language.JavaScript.Inline.Core 10 | import Language.JavaScript.Inline.JSParse 11 | 12 | -- | Generate a 'JSExpr' from inline JavaScript code. The code should be a 13 | -- single expression or a code block with potentially multiple statements (use 14 | -- @return@ to specify the result value in which case). Top-level @await@ is 15 | -- supported. 16 | -- 17 | -- Use @$var@ to refer to a Haskell variable @var@. @var@ should be an instance 18 | -- of 'ToJS'. 19 | -- 20 | -- Important: when using 'js', GHC calls the @node@ process at compile-time in 21 | -- order to use a JavaScript-based JavaScript parser to extract necessary info. 22 | -- Don't forget to ensure @node@ is available in @PATH@ at compile-time. 23 | js :: QuasiQuoter 24 | js = fromQuoteExp inlineJS 25 | 26 | fromQuoteExp :: (String -> Q Exp) -> QuasiQuoter 27 | fromQuoteExp q = 28 | QuasiQuoter 29 | { quoteExp = q, 30 | quotePat = error "Language.JavaScript.Inline.TH: quotePat", 31 | quoteType = error "Language.JavaScript.Inline.TH: quoteType", 32 | quoteDec = error "Language.JavaScript.Inline.TH: quoteDec" 33 | } 34 | 35 | inlineJS :: String -> Q Exp 36 | inlineJS js_code = 37 | do 38 | (is_sync, is_expr, hs_vars) <- runIO $ jsParse js_code 39 | [| 40 | mconcat 41 | $( listE 42 | ( [ [| 43 | fromString 44 | $( litE 45 | ( stringL 46 | ( ( if is_sync 47 | then "((" 48 | else "(async (" 49 | ) 50 | <> intercalate 51 | "," 52 | ['$' : v | v <- hs_vars] 53 | <> ") => {" 54 | <> ( if is_expr 55 | then 56 | "return " 57 | <> js_code 58 | <> ";" 59 | else js_code 60 | ) 61 | <> "})(" 62 | ) 63 | ) 64 | ) 65 | |] 66 | ] 67 | <> intersperse 68 | [|fromString ","|] 69 | [ [|toJS $(varE (mkName v))|] 70 | | v <- hs_vars 71 | ] 72 | <> [[|fromString ")"|]] 73 | ) 74 | ) 75 | |] 76 | -------------------------------------------------------------------------------- /nix/ci.nix: -------------------------------------------------------------------------------- 1 | { sources ? import ./sources.nix { } 2 | , haskellNix ? import sources.haskell-nix { } 3 | , pkgs ? import sources.nixpkgs haskellNix.nixpkgsArgs 4 | , ghcs ? [ "ghc865" "ghc884" "ghc8107" ] 5 | }: 6 | pkgs.callPackage 7 | ({ callPackage, haskell-nix, lib, runCommand, stdenvNoCC }: 8 | let 9 | src = haskell-nix.haskellLib.cleanGit { 10 | name = "inline-js-src"; 11 | src = ../.; 12 | }; 13 | in 14 | runCommand "inline-js-ci" 15 | { 16 | paths = [ (callPackage ./jsbits.nix { inherit pkgs; }) ] ++ lib.concatMap 17 | (ghc: 18 | lib.concatMap 19 | (node: [ 20 | (callPackage ./pkg-set.nix { 21 | inherit pkgs ghc node; 22 | }).inline-js-tests.checks.inline-js-tests 23 | ((callPackage ../shell.nix { inherit pkgs ghc node; }).overrideAttrs 24 | (_: { 25 | name = "inline-js-ci-${ghc}-${node}"; 26 | phases = [ "unpackPhase" "buildPhase" ]; 27 | inherit src; 28 | buildPhase = '' 29 | export HOME=$(mktemp -d) 30 | cabal v2-build all -j$NIX_BUILD_CORES --ghc-option=-j$(($NIX_BUILD_CORES > 4 ? 4 : $NIX_BUILD_CORES)) 31 | cabal v2-run inline-js-tests -- -j$NIX_BUILD_CORES 32 | cabal-docspec 33 | echo :q | cabal v2-repl inline-js 34 | export > $out 35 | ''; 36 | })) 37 | ]) [ "nodejs-16_x" "nodejs-14_x" "nodejs-12_x" "nodejs-10_x" ]) 38 | ghcs; 39 | } "export > $out") 40 | { } 41 | -------------------------------------------------------------------------------- /nix/jsbits.nix: -------------------------------------------------------------------------------- 1 | { sources ? import ./sources.nix { } 2 | , haskellNix ? import sources.haskell-nix { } 3 | , pkgs ? import sources.nixpkgs haskellNix.nixpkgsArgs 4 | }: 5 | pkgs.callPackage 6 | ({ haskell-nix, nodePackages, nodejs-14_x, stdenvNoCC }: 7 | let 8 | src = haskell-nix.haskellLib.cleanGit { 9 | name = "inline-js-parser-src"; 10 | src = ../.; 11 | subDir = "inline-js-parser"; 12 | }; 13 | src_configured = stdenvNoCC.mkDerivation { 14 | name = "inline-js-parser-src-configured"; 15 | inherit src; 16 | nativeBuildInputs = [ nodePackages.node2nix ]; 17 | buildPhase = "node2nix -l package-lock.json -d -14"; 18 | installPhase = "cp -R ./ $out"; 19 | }; 20 | node_dependencies = 21 | (import src_configured { inherit pkgs; }).nodeDependencies; 22 | jsbits = stdenvNoCC.mkDerivation { 23 | name = "inline-js-jsbits"; 24 | inherit src; 25 | nativeBuildInputs = [ nodejs-14_x ]; 26 | buildPhase = '' 27 | ln -s ${node_dependencies}/lib/node_modules 28 | npm run-script build 29 | ''; 30 | installPhase = "mv dist/main.js $out"; 31 | allowedReferences = [ ]; 32 | }; 33 | in 34 | jsbits) 35 | { } 36 | -------------------------------------------------------------------------------- /nix/pkg-set.nix: -------------------------------------------------------------------------------- 1 | { sources ? import ./sources.nix { } 2 | , haskellNix ? import sources.haskell-nix { } 3 | , pkgs ? import sources.nixpkgs haskellNix.nixpkgsArgs 4 | , ghc ? "ghc8107" 5 | , node ? "nodejs_latest" 6 | }: 7 | pkgs.haskell-nix.cabalProject { 8 | src = pkgs.haskell-nix.haskellLib.cleanGit { 9 | name = "inline-js"; 10 | src = ../.; 11 | }; 12 | compiler-nix-name = ghc; 13 | modules = [ 14 | { configureFlags = [ "-O2" ]; } 15 | { dontPatchELF = false; } 16 | { dontStrip = false; } 17 | { hardeningDisable = [ "all" ]; } 18 | { 19 | packages.inline-js-core.preConfigure = 20 | let nodeSrc = pkgs."${node}"; 21 | in 22 | '' 23 | substituteInPlace src/Language/JavaScript/Inline/Core/NodePath.hs --replace '"node"' '"${nodeSrc}/bin/node"' 24 | ''; 25 | } 26 | { packages.inline-js-tests.testFlags = [ "-j$NIX_BUILD_CORES" ]; } 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "haskell-nix": { 3 | "branch": "master", 4 | "description": "Alternative Haskell Infrastructure for Nixpkgs", 5 | "homepage": "https://input-output-hk.github.io/haskell.nix", 6 | "owner": "input-output-hk", 7 | "repo": "haskell.nix", 8 | "rev": "e6a0d20e06aa16134446468c3f3f59ea92eb745b", 9 | "sha256": "07fyygw5sbzzq3j96imn9imparfsaymvbbfxaq4wj6h8kng76clp", 10 | "type": "tarball", 11 | "url": "https://github.com/input-output-hk/haskell.nix/archive/e6a0d20e06aa16134446468c3f3f59ea92eb745b.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "hs-nix-tools": { 15 | "branch": "master", 16 | "description": null, 17 | "homepage": null, 18 | "owner": "TerrorJack", 19 | "repo": "hs-nix-tools", 20 | "rev": "a922abb7ecffa678c830dd3198a422787d156f0f", 21 | "sha256": "1waj70r475kha8wyk6fk729ai21hqykc1phw0vsvyjy5h3l43ywx", 22 | "type": "tarball", 23 | "url": "https://github.com/TerrorJack/hs-nix-tools/archive/a922abb7ecffa678c830dd3198a422787d156f0f.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | }, 26 | "nixpkgs": { 27 | "branch": "nixos-unstable", 28 | "description": "Nix Packages collection", 29 | "homepage": "https://github.com/NixOS/nixpkgs", 30 | "owner": "NixOS", 31 | "repo": "nixpkgs", 32 | "rev": "2cf9db0e3d45b9d00f16f2836cb1297bcadc475e", 33 | "sha256": "0sij1a5hlbigwcgx10dkw6mdbjva40wzz4scn0wchv7yyi9ph48l", 34 | "type": "tarball", 35 | "url": "https://github.com/NixOS/nixpkgs/archive/2cf9db0e3d45b9d00f16f2836cb1297bcadc475e.tar.gz", 36 | "url_template": "https://github.com///archive/.tar.gz" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: name: spec: 10 | let 11 | name' = sanitizeName name + "-src"; 12 | in 13 | if spec.builtin or true then 14 | builtins_fetchurl { inherit (spec) url sha256; name = name'; } 15 | else 16 | pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; 17 | 18 | fetch_tarball = pkgs: name: spec: 19 | let 20 | name' = sanitizeName name + "-src"; 21 | in 22 | if spec.builtin or true then 23 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 24 | else 25 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 26 | 27 | fetch_git = pkgs: spec: 28 | pkgs.fetchgit { url = spec.repo; inherit (spec) rev sha256; fetchSubmodules = true; }; 29 | 30 | fetch_local = spec: spec.path; 31 | 32 | fetch_builtin-tarball = name: throw 33 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 34 | $ niv modify ${name} -a type=tarball -a builtin=true''; 35 | 36 | fetch_builtin-url = name: throw 37 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 38 | $ niv modify ${name} -a type=file -a builtin=true''; 39 | 40 | # 41 | # Various helpers 42 | # 43 | 44 | # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 45 | sanitizeName = name: 46 | ( 47 | concatMapStrings (s: if builtins.isList s then "-" else s) 48 | ( 49 | builtins.split "[^[:alnum:]+._?=-]+" 50 | ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) 51 | ) 52 | ); 53 | 54 | # The set of packages used when specs are fetched using non-builtins. 55 | mkPkgs = sources: system: 56 | let 57 | sourcesNixpkgs = 58 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; 59 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 60 | hasThisAsNixpkgsPath = == ./.; 61 | in 62 | if builtins.hasAttr "nixpkgs" sources 63 | then sourcesNixpkgs 64 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 65 | import {} 66 | else 67 | abort 68 | '' 69 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 70 | add a package called "nixpkgs" to your sources.json. 71 | ''; 72 | 73 | # The actual fetching function. 74 | fetch = pkgs: name: spec: 75 | 76 | if ! builtins.hasAttr "type" spec then 77 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 78 | else if spec.type == "file" then fetch_file pkgs name spec 79 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 80 | else if spec.type == "git" then fetch_git pkgs spec 81 | else if spec.type == "local" then fetch_local spec 82 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 83 | else if spec.type == "builtin-url" then fetch_builtin-url name 84 | else 85 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 86 | 87 | # If the environment variable NIV_OVERRIDE_${name} is set, then use 88 | # the path directly as opposed to the fetched source. 89 | replace = name: drv: 90 | let 91 | saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; 92 | ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; 93 | in 94 | if ersatz == "" then drv else 95 | # this turns the string into an actual Nix path (for both absolute and 96 | # relative paths) 97 | if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; 98 | 99 | # Ports of functions for older nix versions 100 | 101 | # a Nix version of mapAttrs if the built-in doesn't exist 102 | mapAttrs = builtins.mapAttrs or ( 103 | f: set: with builtins; 104 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 105 | ); 106 | 107 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 108 | range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); 109 | 110 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 111 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 112 | 113 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 114 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 115 | concatMapStrings = f: list: concatStrings (map f list); 116 | concatStrings = builtins.concatStringsSep ""; 117 | 118 | # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 119 | optionalAttrs = cond: as: if cond then as else {}; 120 | 121 | # fetchTarball version that is compatible between all the versions of Nix 122 | builtins_fetchTarball = { url, name ? null, sha256 }@attrs: 123 | let 124 | inherit (builtins) lessThan nixVersion fetchTarball; 125 | in 126 | if lessThan nixVersion "1.12" then 127 | fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 128 | else 129 | fetchTarball attrs; 130 | 131 | # fetchurl version that is compatible between all the versions of Nix 132 | builtins_fetchurl = { url, name ? null, sha256 }@attrs: 133 | let 134 | inherit (builtins) lessThan nixVersion fetchurl; 135 | in 136 | if lessThan nixVersion "1.12" then 137 | fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 138 | else 139 | fetchurl attrs; 140 | 141 | # Create the final "sources" from the config 142 | mkSources = config: 143 | mapAttrs ( 144 | name: spec: 145 | if builtins.hasAttr "outPath" spec 146 | then abort 147 | "The values in sources.json should not have an 'outPath' attribute" 148 | else 149 | spec // { outPath = replace name (fetch config.pkgs name spec); } 150 | ) config.sources; 151 | 152 | # The "config" used by the fetchers 153 | mkConfig = 154 | { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null 155 | , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) 156 | , system ? builtins.currentSystem 157 | , pkgs ? mkPkgs sources system 158 | }: rec { 159 | # The sources, i.e. the attribute set of spec name to spec 160 | inherit sources; 161 | 162 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 163 | inherit pkgs; 164 | }; 165 | 166 | in 167 | mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } 168 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { sources ? import ./nix/sources.nix { } 2 | , haskellNix ? import sources.haskell-nix { } 3 | , pkgs ? import sources.nixpkgs haskellNix.nixpkgsArgs 4 | , ghc ? "ghc8107" 5 | , node ? "nodejs_latest" 6 | , hsPkgs ? import ./nix/pkg-set.nix { inherit pkgs ghc node; } 7 | }: 8 | hsPkgs.shellFor { 9 | packages = ps: 10 | with ps; [ 11 | inline-js 12 | inline-js-core 13 | inline-js-examples 14 | inline-js-tests 15 | ]; 16 | 17 | withHoogle = true; 18 | 19 | nativeBuildInputs = 20 | pkgs.lib.attrValues (import sources.hs-nix-tools { inherit ghc; }) 21 | ++ [ pkgs."${node}" pkgs.util-linux ]; 22 | 23 | exactDeps = true; 24 | 25 | shellHook = "taskset -pc 0-1000 $$"; 26 | } 27 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-18.13 2 | packages: 3 | - inline-js 4 | - inline-js-core 5 | - inline-js-examples 6 | - inline-js-tests 7 | --------------------------------------------------------------------------------