├── .clj-kondo └── config.edn ├── .dir-locals.el ├── .github ├── FUNDING.yml ├── setup-gradle │ └── action.yml ├── setup │ └── action.yml └── workflows │ ├── clojure.yml │ └── ij-plugin.yaml ├── .gitignore ├── .joyride └── src │ └── portal │ └── bundle.cljs ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bb.edn ├── deps-clr.edn ├── deps.edn ├── dev ├── cljs │ └── user.cljs ├── hello.js ├── notebook │ ├── ci.clj │ ├── data.clj │ └── scittle.clj ├── portal │ ├── runtime │ │ └── debug.clj │ ├── setup.cljs │ └── share.clj ├── reveal.clj ├── tasks │ ├── bench.clj │ ├── build.clj │ ├── check.clj │ ├── ci.clj │ ├── clean.clj │ ├── cljr.clj │ ├── deploy.clj │ ├── deps.clj │ ├── dev.clj │ ├── docs.clj │ ├── e2e.clj │ ├── format.clj │ ├── ide.clj │ ├── ijverify.clj │ ├── info.clj │ ├── jar.clj │ ├── kondo.clj │ ├── load.clj │ ├── package.clj │ ├── parallel.clj │ ├── planck.clj │ ├── prepl.cljc │ ├── pwa.clj │ ├── test.clj │ ├── tools.clj │ └── version.clj ├── user.clj └── workspace.cljs ├── djblue.portal.csproj ├── doc ├── cli.md ├── cljdoc.edn ├── datafy.md ├── dev │ ├── deps.md │ ├── editors.md │ ├── index.md │ └── tasks.md ├── editors │ ├── emacs.md │ ├── intellij.md │ ├── vs-code-joyride.md │ ├── vs-code-notebook.md │ └── vs-code.md ├── guides │ ├── clerk.md │ ├── custom-taps.md │ ├── default-viewer.md │ ├── exceptions.md │ ├── git-deps.md │ ├── nrepl.md │ ├── portal-console.md │ ├── promises.md │ ├── shadow-cljs.md │ ├── standalone.md │ └── test-runner.md ├── inspiration.md ├── limitations.md ├── remote-api.md ├── ui │ ├── commands.md │ ├── filtering.md │ ├── history.md │ ├── index.md │ ├── selection.md │ ├── shortcuts.md │ ├── themes.md │ └── viewers.md └── videos.md ├── examples ├── bb │ ├── deps.edn │ └── edn ├── clr │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── deps.edn │ └── repl ├── git │ ├── deps.edn │ └── prep ├── jvm │ ├── .gitignore │ ├── deps.edn │ ├── dev │ │ └── user.clj │ ├── project.clj │ └── src │ │ └── example │ │ └── server.clj ├── mulog │ ├── README.md │ ├── deps.edn │ ├── screenshot.png │ └── src │ │ └── user.clj ├── nbb │ ├── .gitignore │ ├── README.md │ ├── dev │ │ └── start.cljs │ ├── nbb.edn │ ├── package-lock.json │ └── package.json ├── node │ ├── .gitignore │ ├── deps.edn │ ├── dev │ │ └── user.cljs │ └── src │ │ └── example │ │ └── server.cljs ├── parse-and-tap ├── plotly-viewer │ ├── .gitignore │ ├── deps.edn │ ├── dev │ │ └── user.clj │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── portal │ │ └── ui │ │ └── viewer │ │ └── plotly.cljs ├── portal-present │ ├── .gitignore │ ├── README.md │ ├── deps.edn │ └── src │ │ ├── portal_present │ │ └── viewer.cljs │ │ └── user.clj ├── remote │ ├── client │ └── server ├── timbre │ ├── README.md │ ├── deps.edn │ └── src │ │ └── user.clj ├── tufte │ ├── README.md │ ├── deps.edn │ └── src │ │ └── user.clj ├── web-figwheel-main │ ├── .gitignore │ ├── deps.edn │ ├── dev.cljs.edn │ ├── dev │ │ └── user.cljs │ ├── figwheel-main.edn │ ├── readme.md │ └── src │ │ └── example │ │ └── app.cljs ├── web-iframe │ ├── deps.edn │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── main │ │ └── demo.cljs └── web │ ├── .gitignore │ ├── deps.edn │ ├── dev │ └── user.cljs │ └── src │ └── example │ └── app.cljs ├── extension-electron ├── .gitignore ├── icon.png ├── package-lock.json └── package.json ├── extension-intellij ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ └── main │ ├── clojure │ └── portal │ │ └── extensions │ │ └── intellij │ │ ├── factory.clj │ │ ├── file.clj │ │ └── theme.clj │ ├── java │ ├── icons │ │ └── PortalIcons.java │ └── portal │ │ └── extensions │ │ └── intellij │ │ └── WithLoader.java │ └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ └── portal │ └── icon.svg ├── extension-vscode ├── .gitignore ├── .vscode │ ├── launch.json │ └── settings.json ├── .vscodeignore ├── icon.png ├── package-lock.json └── package.json ├── nbb.edn ├── package-lock.json ├── package.json ├── resources ├── icon.svg ├── runtime │ ├── babashka.svg │ ├── cljs.svg │ ├── clojure.svg │ ├── joyride.svg │ └── portal.svg ├── screenshot.png ├── splash.svg ├── sw.js └── viewers.edn ├── shadow-cljs.edn ├── src ├── examples │ ├── data.cljc │ ├── default_visualizer.clj │ ├── fetch.cljs │ ├── hacker_news.cljc │ └── macros.cljc └── portal │ ├── api.cljc │ ├── async.cljc │ ├── client │ ├── clr.clj │ ├── common.cljs │ ├── dart.cljd │ ├── jvm.clj │ ├── node.cljs │ ├── planck.cljs │ ├── py.lpy │ └── web.cljs │ ├── colors.cljc │ ├── console.cljc │ ├── extensions │ ├── electron.cljs │ ├── pwa.cljs │ ├── vs_code.cljs │ └── vs_code_notebook.cljs │ ├── main.clj │ ├── main.cljs │ ├── nrepl.clj │ ├── nrepl.cljr │ ├── resources.cljc │ ├── runtime.cljc │ ├── runtime │ ├── browser.cljc │ ├── clr │ │ ├── assembly.clj │ │ ├── client.clj │ │ ├── launcher.clj │ │ └── server.clj │ ├── cson.cljc │ ├── datafy.cljc │ ├── edn.cljc │ ├── fs.cljc │ ├── index.cljc │ ├── json.cljc │ ├── json_buffer.cljc │ ├── jvm │ │ ├── client.clj │ │ ├── commands.clj │ │ ├── editor.clj │ │ ├── launcher.clj │ │ └── server.clj │ ├── macros.cljc │ ├── node │ │ ├── client.cljs │ │ ├── launcher.cljs │ │ └── server.cljs │ ├── npm.cljc │ ├── rpc.cljc │ ├── shell.cljc │ ├── transit.cljc │ └── web │ │ ├── client.cljs │ │ └── launcher.cljs │ ├── shadow │ ├── preload.cljs │ ├── remote.clj │ └── remote.cljs │ ├── shortcuts.cljs │ ├── spec.cljc │ ├── sync.cljc │ ├── ui │ ├── api.cljs │ ├── app.cljs │ ├── cljs.cljs │ ├── commands.cljs │ ├── connection_status.cljs │ ├── core.cljs │ ├── drag_and_drop.cljs │ ├── embed.cljs │ ├── filter.cljc │ ├── html.cljs │ ├── icons.cljs │ ├── inspector.cljs │ ├── lazy.cljc │ ├── lazy.cljs │ ├── load.cljs │ ├── options.cljs │ ├── parsers.cljs │ ├── react.cljc │ ├── repl │ │ └── sci │ │ │ ├── eval.cljs │ │ │ ├── import.cljc │ │ │ └── libs.cljs │ ├── rpc.cljs │ ├── rpc │ │ └── runtime.cljs │ ├── select.cljs │ ├── state.cljs │ ├── styled.cljs │ ├── theme.cljs │ └── viewer │ │ ├── bin.cljs │ │ ├── bytes.cljs │ │ ├── charts.cljs │ │ ├── cljdoc.cljs │ │ ├── code.cljs │ │ ├── color.cljs │ │ ├── csv.cljs │ │ ├── date_time.cljs │ │ ├── deref.cljs │ │ ├── diff.cljs │ │ ├── diff_text.cljs │ │ ├── duration.cljs │ │ ├── edn.cljs │ │ ├── exception.cljs │ │ ├── hiccup.cljs │ │ ├── html.cljs │ │ ├── http.cljs │ │ ├── image.cljs │ │ ├── json.cljs │ │ ├── jwt.cljs │ │ ├── log.cljs │ │ ├── markdown.cljs │ │ ├── pprint.cljs │ │ ├── prepl.cljs │ │ ├── relative_time.cljs │ │ ├── source_location.cljs │ │ ├── spec.cljs │ │ ├── table.cljs │ │ ├── test_report.cljs │ │ ├── text.cljs │ │ ├── transit.cljs │ │ ├── tree.cljs │ │ ├── vega.cljs │ │ └── vega_lite.cljs │ ├── viewer.cljc │ └── web.cljs └── test └── portal ├── bench.cljc ├── client.cljc ├── client_test.cljc ├── e2e.clj ├── runtime ├── api_test.clj ├── api_test.cljs ├── bench_cson.cljc ├── cson_test.cljc ├── edn_test.cljc ├── fs_test.cljc ├── json_buffer_test.cljc ├── jvm │ └── editor_test.clj ├── npm_test.cljc └── shell_test.cljc ├── runtime_test.cljc ├── test_clr.clj ├── test_planck.cljs ├── test_runner.clj ├── test_runner.cljs ├── test_runtime_runner.cljs ├── test_ui_runner.cljs └── ui └── state_test.cljs /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:unsorted-required-namespaces {:level :fail} 2 | :unresolved-symbol 3 | {:exclude [(portal.ui.repl.sci.import/sci-import) 4 | (portal.ui.repl.sci.import/import-ns)]}} 5 | :lint-as 6 | {portal.async/let clojure.core/let 7 | portal.sync/let clojure.core/let 8 | portal.async/try clojure.core/try 9 | portal.sync/try clojure.core/try 10 | reagent.core/with-let clojure.core/let 11 | portal.bench/simple-benchmark clojure.core/let 12 | portal.runtime.macros/extend-type? clojure.core/extend-type}} 13 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-aliases . ":dev:cljs:shadow") 2 | (cider-custom-cljs-repl-init-form . "(user/cljs)") 3 | (cider-default-cljs-repl . custom) 4 | (cider-preferred-build-tool . clojure-cli) 5 | (cider-redirect-server-output-to-repl . t) 6 | (cider-repl-display-help-banner . nil) 7 | (clojure-toplevel-inside-comment-form . t) 8 | (eval . (progn 9 | (make-variable-buffer-local 'cider-jack-in-nrepl-middlewares) 10 | (add-to-list 'cider-jack-in-nrepl-middlewares 11 | "cider.nrepl/cider-middleware") 12 | (add-to-list 'cider-jack-in-nrepl-middlewares 13 | "portal.nrepl/wrap-portal") 14 | (add-to-list 'cider-jack-in-nrepl-middlewares 15 | "shadow.cljs.devtools.server.nrepl/middleware")))))) 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [djblue] 2 | -------------------------------------------------------------------------------- /.github/setup-gradle/action.yml: -------------------------------------------------------------------------------- 1 | runs: 2 | using: "composite" 3 | steps: 4 | - name: Gradle Cache 5 | uses: actions/cache@v3 6 | with: 7 | path: | 8 | ~/.gradle/caches 9 | ~/.gradle/wrapper 10 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 11 | restore-keys: | 12 | ${{ runner.os }}-gradle- -------------------------------------------------------------------------------- /.github/setup/action.yml: -------------------------------------------------------------------------------- 1 | runs: 2 | using: "composite" 3 | steps: 4 | - name: Maven Cache 5 | uses: actions/cache@v3 6 | with: 7 | path: | 8 | ~/.m2/repository 9 | ~/.gitlibs 10 | key: ${{ runner.os }}-deps-${{ hashFiles('**/deps.edn') }} 11 | restore-keys: | 12 | ${{ runner.os }}-deps- 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | - name: Node Cache 17 | uses: actions/cache@v3 18 | with: 19 | path: ~/.npm 20 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 21 | restore-keys: | 22 | ${{ runner.os }}-node- 23 | - name: Install Java 24 | uses: actions/setup-java@v3 25 | with: 26 | distribution: 'adopt' 27 | java-version: '11' 28 | - name: Install Babashka 29 | uses: turtlequeue/setup-babashka@v1.5.2 30 | with: 31 | babashka-version: 1.0.165 32 | -------------------------------------------------------------------------------- /.github/workflows/ij-plugin.yaml: -------------------------------------------------------------------------------- 1 | name: Intellij Plugin Verifier 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: extension-intellij/** 7 | pull_request: 8 | branches: [ master ] 9 | paths: extension-intellij/** 10 | 11 | jobs: 12 | ij-verify: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: ./.github/setup 17 | - uses: ./.github/setup-gradle 18 | - run: bb -m tasks.ijverify/verify 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | .cpcache/ 3 | .shadow-cljs/ 4 | target/ 5 | node_modules/ 6 | cljs-runtime/ 7 | resources/portal/ 8 | resources/portal-dev/ 9 | !.clj-kondo/config 10 | .clj-kondo/* 11 | .DS_Store 12 | .nrepl-port 13 | .bb-repl 14 | clojure.data.json* 15 | 16 | # vscode 17 | *.calva/ 18 | .lsp/ 19 | .portal/ 20 | 21 | # intellij files 22 | .idea/ 23 | *.iml 24 | 25 | obj/ 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeBackground": "#0065ff", 4 | "titleBar.activeForeground": "#272c36", 5 | "titleBar.inactiveBackground": "#00a2ff", 6 | "titleBar.inactiveForeground": "#272c36" 7 | }, 8 | "calva.replConnectSequences": [ 9 | { 10 | "name": "basic-clj", 11 | "projectType": "deps.edn", 12 | "projectRootPath": [ 13 | "." 14 | ], 15 | "extraNReplMiddleware": [ 16 | "portal.nrepl/middleware" 17 | ], 18 | "menuSelections": { 19 | "cljAliases": [ 20 | ":dev", 21 | ":cider" 22 | ] 23 | } 24 | }, 25 | { 26 | "name": "shadow-cljs", 27 | "projectType": "deps.edn", 28 | "projectRootPath": [ 29 | "." 30 | ], 31 | "customJackInCommandLine": "bb dev", 32 | "nReplPortFile": [ 33 | ".shadow-cljs", 34 | "nrepl.port" 35 | ], 36 | "shouldOpenUrl": false, 37 | "cljsType": "shadow-cljs", 38 | "menuSelections": { 39 | "cljAliases": [ 40 | ":dev", 41 | ":cider", 42 | ":cljs", 43 | ":shadow" 44 | ], 45 | "cljsLaunchBuilds": [ 46 | ":client", 47 | ":vs-code", 48 | ":vs-code-notebook" 49 | ], 50 | "cljsDefaultBuild": ":client" 51 | } 52 | }, 53 | { 54 | "name": "portal", 55 | "projectType": "deps.edn", 56 | "projectRootPath": [ 57 | "." 58 | ], 59 | "extraNReplMiddleware": [ 60 | "portal.nrepl/middleware" 61 | ], 62 | "shouldOpenUrl": false, 63 | "cljsType": { 64 | "dependsOn": "User provided", 65 | "isStarted": false, 66 | "connectCode": "((requiring-resolve 'portal.api/repl))", 67 | "isConnectedRegExp": ":repl" 68 | }, 69 | "menuSelections": { 70 | "cljAliases": [ 71 | ":dev", 72 | ":cider" 73 | ] 74 | } 75 | }, 76 | { 77 | "name": "clojure-clr", 78 | "projectType": "deps.edn", 79 | "projectRootPath": [ 80 | "." 81 | ], 82 | "customJackInCommandLine": "cljr -M:dev:test:nrepl -m portal.nrepl", 83 | "nReplPortFile": [ 84 | ".nrepl-port" 85 | ], 86 | "menuSelections": { 87 | "cljAliases": [ 88 | ":dev", 89 | ":test", 90 | ":nrepl" 91 | ] 92 | } 93 | } 94 | ] 95 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chris Badahdah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources" "dev" "test"] 2 | :deps {io.aviso/pretty {:mvn/version "1.4.4"}} 3 | :min-bb-version "0.6.4" 4 | :tasks {app tasks.pwa/pwa 5 | deps tasks.deps/fix-deps 6 | ci tasks.ci/ci 7 | clean tasks.clean/clean 8 | check tasks.check/check 9 | dev tasks.dev/-main 10 | e2e tasks.e2e/all 11 | fmt tasks.format/fix 12 | docs tasks.docs/-main 13 | jar tasks.package/jar 14 | test tasks.test/test 15 | tag tasks.version/tag 16 | ext tasks.build/extensions 17 | ide tasks.ide/open 18 | pkg tasks.package/all 19 | deploy tasks.deploy/all 20 | bench tasks.bench/-main}} 21 | -------------------------------------------------------------------------------- /deps-clr.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps 3 | {io.github.clojure/clr.data.json {:git/tag "v2.5.1" :git/sha "f84cb88"}} 4 | :aliases 5 | {:test 6 | {:extra-paths ["test"]} 7 | :dev 8 | {:extra-paths ["dev"]} 9 | :nrepl 10 | {:extra-deps 11 | {io.github.clojure/clr.tools.nrepl {:git/tag "v0.1.2-alpha2" :git/sha "a58009f"}}}}} -------------------------------------------------------------------------------- /dev/cljs/user.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.user) 2 | 3 | (comment 4 | (require '[portal.api :as p]) 5 | (add-tap #'p/submit) 6 | (remove-tap #'p/submit) 7 | 8 | (defn async-submit [value] 9 | (if-not (instance? js/Promise value) 10 | (p/submit value) 11 | (-> value 12 | (.then p/submit) 13 | (.catch p/submit)))) 14 | 15 | (add-tap #'async-submit) 16 | (tap> :hi) 17 | 18 | (p/clear) 19 | (p/close) 20 | (p/docs) 21 | 22 | (p/open) 23 | 24 | (require '[examples.data :refer [data]]) 25 | (dotimes [_i 25] (tap> data)) 26 | 27 | comment) -------------------------------------------------------------------------------- /dev/hello.js: -------------------------------------------------------------------------------- 1 | const react = require('react') 2 | 3 | console.log("hello-world:", react, __dirname, __filename) 4 | 5 | exports.world = 1234 6 | -------------------------------------------------------------------------------- /dev/notebook/ci.clj: -------------------------------------------------------------------------------- 1 | (ns notebook.ci 2 | (:require [tasks.build :refer [build install]] 3 | [tasks.check :as check] 4 | [tasks.format :as fmt] 5 | [tasks.parallel :refer [with-out-data]] 6 | [tasks.test :as test] 7 | [tasks.tools :as tool])) 8 | (check/cloc) 9 | (with-out-data (fmt/check)) 10 | (with-out-data (check/clj-kondo)) 11 | (with-out-data (check/clj-check)) 12 | (with-out-data (check/gradle-check)) 13 | (install) 14 | (with-out-data (test/cljs-runtime "1.10.773")) 15 | (with-out-data (test/cljs-runtime "1.10.844")) 16 | (with-out-data (test/cljs-nbb)) 17 | (with-out-data (test/cljs-ui)) 18 | (build) 19 | (with-out-data (tool/clj "-M:test" "-m" :portal.test-runner)) 20 | (with-out-data (tool/bb "-m" :portal.test-runner)) 21 | (with-out-data (test/cljr)) 22 | -------------------------------------------------------------------------------- /dev/notebook/data.clj: -------------------------------------------------------------------------------- 1 | (ns notebook.data 2 | (:require [examples.data :as d] 3 | [portal.colors :as c] 4 | [portal.viewer :as v])) 5 | 6 | (::d/hacker-news d/data) 7 | 8 | (-> d/basic-data) 9 | 10 | (-> d/platform-data) 11 | 12 | (:portal.colors/nord c/themes) 13 | 14 | (-> d/diff-data) 15 | 16 | (-> d/prepl-data) 17 | 18 | (-> d/log-data) 19 | 20 | (v/table d/log-data) 21 | 22 | (-> d/hiccup) 23 | 24 | (v/tree d/hiccup) 25 | 26 | (v/hiccup 27 | [:portal.viewer/markdown (::d/markdown d/string-data)]) 28 | 29 | (-> d/exception-data) 30 | 31 | (-> d/test-report) 32 | 33 | (-> d/line-chart) 34 | -------------------------------------------------------------------------------- /dev/portal/runtime/debug.clj: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.debug 2 | (:require [portal.api :as p] 3 | [portal.runtime :as rt])) 4 | 5 | (defn- section [title value] 6 | [(name title) 7 | {:hiccup 8 | [:div 9 | {:style {:padding 40}} 10 | [:h2 {:style {:margin-top 0}} 11 | (if (string? title) 12 | title 13 | [:portal.viewer/inspector title])] 14 | [:portal.viewer/inspector value]]}]) 15 | 16 | (defn- dashboard [session] 17 | {:cljdoc.doc/tree 18 | [["Portal Server" 19 | ["Runtime" 20 | (section "Sessions" rt/sessions) 21 | (section "Connections" rt/connections) 22 | (section "Commands" @#'rt/registry)] 23 | ["Session State" 24 | (section "Info" (dissoc session :options :value-cache :watch-registry :selected)) 25 | (section :options (:options session)) 26 | (section :value-cache (:value-cache session)) 27 | (section :watch-registry (:watch-registry session))]]]}) 28 | 29 | (defn open [session] 30 | (p/inspect 31 | (dashboard session) 32 | (-> (:options session) 33 | (dissoc :debug) 34 | (assoc :window-title "portal-debug-server")))) 35 | 36 | (defn close [instance] 37 | (when instance (p/close instance))) -------------------------------------------------------------------------------- /dev/portal/share.clj: -------------------------------------------------------------------------------- 1 | (ns portal.share 2 | (:require [clojure.pprint :as pp] 3 | [clojure.string :as str] 4 | [org.httpkit.client :as http] 5 | [portal.api :as p] 6 | [portal.runtime.json :as json]) 7 | (:import [java.net URL])) 8 | 9 | (defn- pprint [value] 10 | (binding [*print-meta* true] 11 | (with-out-str (pp/pprint value)))) 12 | 13 | (defn- create-gist [content] 14 | (-> @(http/post 15 | "https://api.github.com/gists" 16 | {:basic-auth [(System/getenv "GIT_USERNAME") 17 | (System/getenv "GIT_PASSWORD")] 18 | :headers {"Accept" "application/vnd.github.v3+json"} 19 | :body (json/write 20 | {:public false 21 | :description "Portal Share" 22 | :files {"data.clj" {:content content}}})}) 23 | :body 24 | json/read 25 | (get-in [:files :data.clj :raw_url]))) 26 | 27 | (defn- ->query-string [m] 28 | (str/join "&" (for [[k v] m] (str (name k) "=" v)))) 29 | 30 | (defn- create-url [gist-raw-url] 31 | (str "https://djblue.github.io/portal/?" 32 | (->query-string {:content-url gist-raw-url 33 | :content-type "application/edn"}))) 34 | 35 | (defn- shorten-url [url] 36 | (-> @(http/post 37 | "https://url.api.stdlib.com/temporary@0.3.0/create/" 38 | {:headers {"Content-Type" "application/json"} 39 | :body (json/write {:url url :ttl 86400})}) 40 | :body 41 | json/read 42 | :link_url)) 43 | 44 | (defn share 45 | "Create a shareable portal link for the supplied value." 46 | [value] 47 | (URL. (-> value pprint create-gist create-url shorten-url))) 48 | 49 | (p/register! #'share) 50 | -------------------------------------------------------------------------------- /dev/reveal.clj: -------------------------------------------------------------------------------- 1 | (ns reveal 2 | (:require [vlaaad.reveal :as r])) 3 | 4 | (comment (r/tap-log)) 5 | -------------------------------------------------------------------------------- /dev/tasks/bench.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.bench 2 | (:require 3 | [portal.api :as p] 4 | [portal.runtime.bench-cson :as bc] 5 | [tasks.parallel :refer [with-out-data]] 6 | [tasks.tools :as t])) 7 | 8 | (defn bench-cson [] 9 | (mapcat 10 | (fn [f] 11 | (some 12 | #(let [{:keys [tag val]} %] 13 | (when (= :tap tag) val)) 14 | @f)) 15 | (for [f [#(t/clj "-M:test" "-m" :portal.runtime.bench-cson) 16 | #(t/bb "-m" :portal.runtime.bench-cson) 17 | #(t/cljr "-m" :portal.runtime.bench-cson) 18 | #(t/nbb "-m" :portal.runtime.bench-cson)]] 19 | (future (with-out-data (f)))))) 20 | 21 | (def windows 22 | {{:encoding :cson :test :read} 23 | {:launcher :auto :window-title "cson-read"} 24 | {:encoding :cson :test :write} 25 | {:launcher :auto :window-title "cson-write"}}) 26 | 27 | (defn charts [data] 28 | (let [groups (group-by #(select-keys % [:encoding :test]) data)] 29 | (doseq [[group opts] windows] 30 | (p/inspect (bc/charts (get groups group groups)) opts)))) 31 | 32 | (defn -main 33 | "Run cson benchmarks and render results in portal" 34 | [] 35 | (.addShutdownHook 36 | (Runtime/getRuntime) 37 | (Thread. (fn [] (p/close)))) 38 | (try 39 | (charts (bench-cson)) 40 | (catch Exception e 41 | (p/inspect (Throwable->map e) {:launcher :auto}))) 42 | @(promise)) -------------------------------------------------------------------------------- /dev/tasks/build.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.build 2 | (:require [babashka.fs :as fs] 3 | [tasks.docs :refer [docs]] 4 | [tasks.info :refer [version]] 5 | [tasks.tools :refer [*cwd* gradle npm npx shadow]])) 6 | 7 | (defn install [] 8 | (when (seq 9 | (fs/modified-since 10 | "node_modules" 11 | ["package.json" "package-lock.json"])) 12 | (npm :ci))) 13 | 14 | (defn main-js [] 15 | (when (seq 16 | (fs/modified-since 17 | "resources/portal/main.js" 18 | (concat 19 | ["deps.edn" 20 | "package.json" 21 | "package-lock.json" 22 | "shadow-cljs.edn"] 23 | (fs/glob "src/portal/ui" "**.cljs")))) 24 | (install) 25 | (shadow :release :client)) 26 | (fs/copy "resources/icon.svg" 27 | "resources/portal/icon.svg" 28 | {:replace-existing true})) 29 | 30 | (defn ws-js [] 31 | (let [out "resources/portal/ws.js"] 32 | (when (seq 33 | (fs/modified-since 34 | out 35 | ["package.json" "package-lock.json"])) 36 | (install) 37 | (npx :browserify 38 | "--node" 39 | "--exclude" :bufferutil 40 | "--exclude" :utf-8-validate 41 | "--standalone" :Server 42 | "--outfile" out 43 | "node_modules/ws")))) 44 | 45 | (defn build [] (docs) (main-js) (ws-js)) 46 | 47 | (defn prep [_] (install) (build) (shutdown-agents)) 48 | 49 | (defn vs-code-extension 50 | "Build vs-code extension." 51 | [] 52 | (build) 53 | (shadow :release :vs-code :vs-code-notebook) 54 | (binding [*cwd* "extension-vscode"] 55 | (npm :ci) 56 | (fs/copy "README.md" "extension-vscode/" {:replace-existing true}) 57 | (fs/copy "LICENSE" "extension-vscode/LICENSE" {:replace-existing true}) 58 | (npx :vsce :package))) 59 | 60 | (defn intellij-extension 61 | "Build intellij extension." 62 | [] 63 | (binding [*cwd* "extension-intellij"] 64 | (gradle "buildPlugin") 65 | (println (str "extension-intellij/build/distributions/portal-extension-intellij-" version ".zip")))) 66 | 67 | (defn extensions 68 | "Build editor extensions." 69 | [] 70 | (vs-code-extension) 71 | (intellij-extension)) 72 | -------------------------------------------------------------------------------- /dev/tasks/check.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.check 2 | (:require [portal.runtime.json :as json] 3 | [portal.viewer :as v] 4 | [tasks.format :as fmt] 5 | [tasks.tools :refer [*cwd* clj gradle sh]])) 6 | 7 | (defn cloc [] 8 | (-> (sh :cloc "--json" :src :dev :test) 9 | (with-out-str) 10 | (json/read) 11 | (dissoc :header) 12 | (v/table {:columns [:nFiles :blank :comment :code]}))) 13 | 14 | (defn clj-kondo [] 15 | (clj "-M:kondo" 16 | "--lint" :dev :src :test 17 | "extension-intellij/src/main/clojure")) 18 | 19 | (defn clj-check [] (clj "-M:cider:check")) 20 | 21 | (defn gradle-check [] 22 | (binding [*cwd* "extension-intellij"] 23 | (gradle "checkClojure"))) 24 | 25 | (defn check* [] 26 | (future (fmt/check)) 27 | (future (clj-kondo)) 28 | (future (clj-check)) 29 | (future (gradle-check))) 30 | 31 | (defn check 32 | "Run all static analysis checks." 33 | [] 34 | (fmt/check) 35 | (clj-kondo) 36 | (clj-check) 37 | (gradle-check)) 38 | 39 | (defn -main [] (check)) 40 | -------------------------------------------------------------------------------- /dev/tasks/ci.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.ci 2 | (:require [tasks.check :refer [check check*]] 3 | [tasks.test :refer [test test*] :as test] 4 | [tasks.tools :refer [clj]])) 5 | 6 | (def ^:private commands 7 | ["-M:cljfmt" 8 | "-M:cljs" 9 | "-M:cljs:shadow" 10 | "-M:dev" 11 | "-M:kondo" 12 | "-M:test" 13 | "-X:deploy"]) 14 | 15 | (defn setup [] 16 | (test/setup) 17 | (doseq [command commands] (clj "-Sforce" "-Spath" command))) 18 | 19 | (defn ci 20 | "Run all CI Checks." 21 | [] 22 | (check) (test)) 23 | 24 | (defn -main [] (ci)) 25 | 26 | (comment 27 | (require '[tasks.parallel :refer [with-out-data]]) 28 | (with-out-data (check*) (test*))) 29 | -------------------------------------------------------------------------------- /dev/tasks/clean.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.clean 2 | (:require [babashka.fs :as fs] 3 | [io.aviso.ansi :as a] 4 | [tasks.info :refer [version]])) 5 | 6 | (defn rm [path] 7 | (when (fs/exists? path) 8 | (println (a/bold-blue "=>") (a/bold-green "rm") path) 9 | (fs/delete-tree path)) 10 | nil) 11 | 12 | (def ^:private clean-files 13 | ["extension-intellij/build/" 14 | "extension-vscode/vs-code.js" 15 | "extension-vscode/vs-code.js.map" 16 | "extension-vscode/notebook" 17 | "pom.xml" 18 | "resources/portal/" 19 | "resources/portal-dev/" 20 | "target/" 21 | (str "extension-vscode/portal-" version ".vsix")]) 22 | 23 | (defn clean 24 | "Remove target and resources/portal" 25 | [] 26 | (doseq [file clean-files] (rm file))) 27 | 28 | (defn -main [] (clean)) 29 | -------------------------------------------------------------------------------- /dev/tasks/cljr.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.cljr 2 | (:require [tasks.tools :refer [*opts* cljr]])) 3 | 4 | (defn repl [] 5 | (binding [*opts* {:inherit true}] 6 | (cljr "--eval" "((requiring-resolve 'tasks.prepl/prepl))" "--repl"))) 7 | 8 | (defn -main [] (repl)) 9 | -------------------------------------------------------------------------------- /dev/tasks/deploy.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.deploy 2 | (:require [tasks.ci :refer [ci]] 3 | [tasks.info :refer [options]] 4 | [tasks.package :as pkg] 5 | [tasks.tools :refer [*cwd* clj gradle npx]])) 6 | 7 | (defn- deploy-vscode [] 8 | (binding [*cwd* "extension-vscode"] 9 | (npx :vsce :publish))) 10 | 11 | (defn- deploy-open-vsx [] 12 | (binding [*cwd* "extension-vscode"] 13 | (npx :ovsx :publish))) 14 | 15 | (defn deploy-intellij [] 16 | (binding [*cwd* "extension-intellij"] 17 | (gradle :publishPlugin))) 18 | 19 | (defn- deploy-clojars [] 20 | (pkg/pom) 21 | (clj "-X:deploy" 22 | ":installer" ":remote" 23 | ":artifact" (-> options :jar-file str pr-str) 24 | ":pom-file" (-> options pkg/pom-file str pr-str))) 25 | 26 | (defn deploy [] 27 | (pkg/all) 28 | (deploy-clojars) 29 | (deploy-vscode) 30 | (deploy-open-vsx) 31 | (deploy-intellij)) 32 | 33 | (defn all 34 | "Deploy all artifacts." 35 | [] 36 | (ci) 37 | (deploy)) 38 | 39 | (defn -main [] (deploy)) 40 | -------------------------------------------------------------------------------- /dev/tasks/deps.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.deps 2 | (:require [tasks.tools :refer [*cwd* clj git npm]])) 3 | 4 | (defn check-deps [] 5 | (npm :outdated) 6 | (clj "-M:antq" "-m" :antq.core)) 7 | 8 | (defn assert-clean [] 9 | (git :diff "--exit-code")) 10 | 11 | (defn fix-deps 12 | "Update npm and clj dependencies." 13 | [] 14 | (assert-clean) 15 | (npm :update) 16 | (clj "-M:antq" "-m" :antq.core "--upgrade" "--force") 17 | (binding [*cwd* "extension-vscode"] 18 | (npm :update)) 19 | (binding [*cwd* "extension-electron"] 20 | (npm :update)) 21 | (git :add "-u") 22 | (git :commit "-m" "Bump deps")) 23 | 24 | (defn -main [] (check-deps)) 25 | -------------------------------------------------------------------------------- /dev/tasks/dev.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.dev 2 | (:require [clojure.core.server :as server] 3 | [clojure.java.io :as io] 4 | [org.httpkit.server :as http] 5 | [tasks.build :refer [build]] 6 | [tasks.tools :refer [*opts* clj]])) 7 | 8 | (defrecord Edn [edn]) 9 | 10 | (defmethod print-method Edn [v ^java.io.Writer w] 11 | (.write w ^String (:edn v))) 12 | 13 | (defn- proxy-tap> [request] 14 | (tap> (->Edn (slurp (:body request)))) 15 | {:status 200}) 16 | 17 | (defn- start-server [opts] 18 | (let [server (server/start-server opts) 19 | port (.getLocalPort server) 20 | host (-> server .getInetAddress .getCanonicalHostName) 21 | port-file (io/file ".bb-repl")] 22 | (.deleteOnExit port-file) 23 | (spit port-file (pr-str {:host host :port port :runtime :bb})) 24 | (printf "=> Babashka prepl listening on %s:%s\n" host port))) 25 | 26 | (defn- io-prepl [& args] 27 | (binding [*opts* {:extra-env {"PORTAL_PORT" (-> args first ::port str)}}] 28 | (apply server/io-prepl args))) 29 | 30 | (defmethod print-method (type #'var?) [v ^java.io.Writer w] 31 | (let [m (meta v)] 32 | (when *print-meta* 33 | (.write w "^") 34 | (.write w (pr-str m)) 35 | (.write w " ")) 36 | (.write w "#'") 37 | (.write w (str (symbol (str (:ns m)) 38 | (str (:name m))))))) 39 | 40 | (defn- pr-str* [v] 41 | (binding [*print-meta* true] (pr-str v))) 42 | 43 | (defn prepl [] 44 | (let [server (http/run-server #'proxy-tap> {:legacy-return-value? false}) 45 | port (http/server-port server)] 46 | (start-server 47 | {:name "bb" 48 | :port 0 49 | :server-daemon false 50 | :args [{:valf pr-str* ::port port}] 51 | :accept `io-prepl}))) 52 | 53 | (defn dev 54 | "Start dev server." 55 | [] 56 | (binding [*opts* {:inherit true}] 57 | (build) 58 | (clj "-M:dev:cider:cljs:shadow" 59 | "-m" "shadow.cljs.devtools.cli" 60 | :watch #_:pwa :client :vs-code :vs-code-notebook #_:electron))) 61 | 62 | (defn -main "Start dev server." [] (prepl) (dev)) 63 | -------------------------------------------------------------------------------- /dev/tasks/e2e.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.e2e 2 | (:require [babashka.process :as p] 3 | [clojure.java.io :as io] 4 | [portal.e2e :as e2e] 5 | [tasks.build :refer [build]])) 6 | 7 | (def e2e-envs 8 | {:jvm [:clojure "-M" "-e" "(set! *warn-on-reflection* true)" "-r"] 9 | :node [:clojure 10 | "-Sdeps" 11 | (pr-str 12 | {:deps 13 | {'org.clojure/clojurescript 14 | {:mvn/version "1.10.844"}}}) 15 | "-M" "-m" :cljs.main "-re" :node] 16 | :web [:clojure 17 | "-Sdeps" 18 | (pr-str 19 | {:deps 20 | {'org.clojure/clojurescript 21 | {:mvn/version "1.10.844"}}}) 22 | "-M" "-m" :cljs.main] 23 | :bb [:bb] 24 | :nbb [:npx :nbb] 25 | :clr [:bb "-m" "tasks.cljr/repl"]}) 26 | 27 | (defn e2e [env] 28 | (build) 29 | (let [env (if (keyword? env) env (read-string env)) 30 | ps (p/process (map name (get e2e-envs env)) {:out :inherit :err :inherit})] 31 | (println "running e2e tests for" env) 32 | (when (= env :web) 33 | (println "please wait for browser to open before proceeding")) 34 | (binding [*out* (io/writer (:in ps))] 35 | (e2e/-main (name env))) 36 | (.close (:in ps)) 37 | (p/check ps) 38 | nil)) 39 | 40 | (defn all 41 | "Run e2e tests in all envs." 42 | [] 43 | (dorun (map e2e (keys e2e-envs)))) 44 | 45 | (defn -main [] (all)) 46 | -------------------------------------------------------------------------------- /dev/tasks/format.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.format 2 | (:require [tasks.tools :refer [clj]])) 3 | 4 | (defn check [] 5 | (clj "-M:cljfmt" :check 6 | :dev :src :test 7 | "extension-intellij/src/main/clojure")) 8 | 9 | (defn fix 10 | "Run cljfmt formatter." 11 | [] 12 | (clj "-M:cljfmt" :fix 13 | :dev :src :test 14 | "extension-intellij/src/main/clojure")) 15 | -------------------------------------------------------------------------------- /dev/tasks/ide.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.ide 2 | (:require [clojure.string :as str] 3 | [tasks.tools :refer [*cwd* gradle]])) 4 | 5 | (def plugins 6 | {"2021.3" ["com.cursiveclojure.cursive:1.12.2-2021.3" "com.intellij.java"] 7 | "2022.3" ["com.cursiveclojure.cursive:1.13.1-2022.3" "com.intellij.java"] 8 | "2023.3" ["com.cursiveclojure.cursive:1.13.1-2023.3" "com.intellij.java"] 9 | "2024.3" ["com.cursiveclojure.cursive:1.14.0-2024.3" "com.intellij.java"]}) 10 | 11 | (defn open 12 | "Open dev extension for intellij." 13 | [& [version]] 14 | (binding [*cwd* "extension-intellij"] 15 | (let [platform (or version "2024.3")] 16 | (gradle "runIde" 17 | (str "-PplatformVersion=" platform) 18 | (str "-PplatformPlugins=" (str/join ", " (get plugins platform))))))) -------------------------------------------------------------------------------- /dev/tasks/ijverify.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.ijverify 2 | #_(:require [tasks.tools :refer [gradle *cwd*]])) 3 | 4 | (defn verify 5 | "Run Intellij Plugin Verification." 6 | [] 7 | #_(binding [*cwd* "extension-intellij"] 8 | (gradle "runPluginVerifier")) 9 | (println "Disabled for now")) 10 | -------------------------------------------------------------------------------- /dev/tasks/info.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.info 2 | (:require [clojure.java.shell :refer [sh]] 3 | [clojure.string :as str])) 4 | 5 | (def version "0.59.1") 6 | 7 | (defn git-hash [] 8 | (str/trim (:out (sh "git" "rev-parse" "HEAD")))) 9 | 10 | (defn- provided [deps] 11 | (reduce-kv 12 | (fn [m k v] 13 | (assoc m k (assoc v :scope "provided"))) 14 | {} 15 | deps)) 16 | 17 | (defn- get-deps [] 18 | (let [deps (read-string (slurp "deps.edn"))] 19 | (merge 20 | (:deps deps) 21 | (provided (get-in deps [:aliases :cider :extra-deps])) 22 | (provided (get-in deps [:aliases :cljs :extra-deps])) 23 | (provided (get-in deps [:aliases :plk :extra-deps]))))) 24 | 25 | (def options 26 | {:lib 'djblue/portal 27 | :description "A clojure tool to navigate through your data." 28 | :version version 29 | :url "https://github.com/djblue/portal" 30 | :src-dirs ["src"] 31 | :resource-dirs ["resources"] 32 | :jar-file (str "./target/portal-" version ".jar") 33 | :class-dir "target/classes" 34 | :repos {"clojars" {:url "https://repo.clojars.org/"}} 35 | :scm {:tag (git-hash) 36 | :url "https://github.com/djblue/portal"} 37 | :license 38 | {:name "MIT License" 39 | :url "https://opensource.org/licenses/MIT"} 40 | :deps (get-deps)}) 41 | 42 | (defn -main [] 43 | (println (str "::set-output name=version::" version))) 44 | -------------------------------------------------------------------------------- /dev/tasks/jar.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.jar 2 | (:require [clojure.tools.build.api :as b] 3 | [tasks.info :refer [options]])) 4 | 5 | (defn -main [] 6 | (let [{:keys [class-dir jar-file]} options] 7 | (b/copy-dir {:src-dirs ["src/portal"] 8 | :target-dir (str class-dir "/portal")}) 9 | (b/copy-dir {:src-dirs ["resources/portal"] 10 | :target-dir (str class-dir "/portal")}) 11 | (b/delete {:path (str class-dir "/portal/extensions/")}) 12 | (b/jar {:class-dir class-dir :jar-file jar-file}) 13 | (shutdown-agents))) 14 | -------------------------------------------------------------------------------- /dev/tasks/kondo.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.kondo 2 | (:require [clj-kondo.core :as core] 3 | [clj-kondo.main :as kondo] 4 | [clojure.string :as str] 5 | [portal.client.jvm :as p])) 6 | 7 | (defn- file->ns [file] 8 | (some-> file 9 | (str/replace #"src/|test/|dev/|.clj.?" "") 10 | (str/escape {\/ "." \_ "-"}) 11 | symbol)) 12 | 13 | (defn- print! [f {:keys [findings] :as data}] 14 | (if-let [port (System/getenv "PORTAL_PORT")] 15 | (let [submit (partial p/submit {:port port :encoding :cson})] 16 | (doseq [{:keys [col row filename level message type]} findings] 17 | (submit 18 | {:level (get {:warning :warn :fail :fatal} level level) 19 | :type type 20 | :column col 21 | :ns (file->ns filename) 22 | :line row 23 | :file filename 24 | :result message 25 | :time (java.util.Date.)}))) 26 | (f data))) 27 | 28 | (def ^:private config 29 | {:output 30 | {:pattern "::{{level}} file={{filename}},line={{row}},col={{col}}::clj-kondo: {{message}} {{type}}"}}) 31 | 32 | (defn -main [& args] 33 | (with-redefs [core/print! (partial print! core/print!)] 34 | (apply kondo/-main 35 | (concat args 36 | (when (System/getenv "GITHUB_ACTIONS") 37 | ["--config" (pr-str config)]))))) 38 | -------------------------------------------------------------------------------- /dev/tasks/load.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.load 2 | (:require [clojure.java.io :as io]) 3 | (:import [java.io File PushbackReader])) 4 | 5 | (defn- read-ns-form [file] 6 | (binding [*read-eval* false 7 | *default-data-reader-fn* tagged-literal] 8 | (with-open [reader (PushbackReader. (io/reader (io/file file)))] 9 | (read {:read-cond :preserve} reader)))) 10 | 11 | (defn- no-check [file] 12 | (:no-check (meta (second (read-ns-form file))))) 13 | 14 | (defn- check-ns [file] 15 | (when-not (no-check file) 16 | (binding [*out* *err*] 17 | (println "Compiling namespace" file)) 18 | (try 19 | (binding [*warn-on-reflection* true] 20 | (load-file file) 21 | nil) 22 | (catch ExceptionInInitializerError e 23 | (doto e .printStackTrace))))) 24 | 25 | (defn- get-failures [source-paths] 26 | (sequence 27 | (comp 28 | (mapcat (comp file-seq io/file)) 29 | (map #(.getAbsolutePath ^File %)) 30 | (filter #(re-matches #".*\.cljc?$" %)) 31 | (keep check-ns)) 32 | source-paths)) 33 | 34 | (defn check [source-paths] 35 | (let [failures (count (get-failures source-paths))] 36 | (shutdown-agents) 37 | (when-not (zero? failures) 38 | (System/exit failures)))) 39 | 40 | (defn -main [& source-paths] 41 | (check (or (seq source-paths) ["src"]))) 42 | -------------------------------------------------------------------------------- /dev/tasks/planck.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.planck 2 | (:require [tasks.tools :refer [sh]])) 3 | 4 | (defn setup [] 5 | (sh :sudo :add-apt-repository "ppa:mfikes/planck") 6 | (sh :sudo :apt-get :update) 7 | (sh :sudo :apt-get :install :planck)) 8 | 9 | (defn -main [] (setup)) 10 | -------------------------------------------------------------------------------- /dev/tasks/prepl.cljc: -------------------------------------------------------------------------------- 1 | (ns tasks.prepl 2 | (:require [clojure.core.server :as server] 3 | [portal.runtime.fs :as fs]) 4 | #?(:clj 5 | (:import 6 | (java.net Socket)) 7 | :cljr 8 | (:import 9 | (System.Net.Sockets TcpListener) 10 | (System.Threading Thread)))) 11 | 12 | (defn- get-runtime [] 13 | #?(:bb :bb :clj :clj :cljr :cljr)) 14 | 15 | (defn- get-server-info [server] 16 | #?(:clj (let [server ^Socket server] 17 | {:host (-> server .getInetAddress .getCanonicalHostName) 18 | :port (.getLocalPort server)}) 19 | :cljr (let [endpoint (.LocalEndpoint ^TcpListener server)] 20 | (while (zero? (.Port endpoint)) 21 | (Thread/Sleep 100)) 22 | {:host (.ToString (.Address endpoint)) :port (.Port endpoint)}) 23 | :cljs server)) 24 | 25 | (defn- start-server [opts] 26 | (let [server (server/start-server opts)] 27 | #_{:clj-kondo/ignore #?(:cljs [:unresolved-symbol] :default [])} 28 | (future 29 | (let [info (assoc (get-server-info server) :runtime (get-runtime)) 30 | port-file (str "." (name (:runtime info)) "-repl")] 31 | (fs/rm-exit port-file) 32 | (fs/spit port-file (pr-str info)) 33 | (println (str "=> " (:runtime info) " prepl listening on " (:host info) ":" (:port info))))) 34 | nil)) 35 | 36 | (defn- -pr-str [v] 37 | (binding [*print-meta* true] (pr-str v))) 38 | 39 | (defn prepl [] 40 | (start-server 41 | {:name (name (get-runtime)) 42 | :port 0 43 | :server-daemon false 44 | :args [{:valf -pr-str}] 45 | :accept `server/io-prepl})) 46 | -------------------------------------------------------------------------------- /dev/tasks/test.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.test 2 | (:refer-clojure :exclude [test]) 3 | (:require [babashka.fs :as fs] 4 | [tasks.build :refer [build install]] 5 | [tasks.tools :as t])) 6 | 7 | (defn cljs* [deps main] 8 | (let [version (get-in deps ['org.clojure/clojurescript :mvn/version]) 9 | out (str "target/" (name main) "." version ".js")] 10 | (when (seq 11 | (fs/modified-since 12 | out 13 | (concat 14 | (fs/glob "src" "**") 15 | (fs/glob "test" "**")))) 16 | (t/clj "-Sdeps" (pr-str {:deps deps}) 17 | "-M:test" 18 | "-m" :cljs.main 19 | "--output-dir" (str "target/cljs-output-" version) 20 | "--target" :node 21 | "--output-to" out 22 | "--compile" main)) 23 | (t/node out))) 24 | 25 | (defn cljs-runtime [version] 26 | (cljs* {'org.clojure/clojurescript {:mvn/version version}} :portal.test-runtime-runner)) 27 | 28 | (defn- get-cljs-deps [] 29 | (get-in (read-string (slurp "deps.edn")) [:aliases :cljs :extra-deps])) 30 | 31 | (defn cljs-ui [] 32 | (install) 33 | (cljs* (get-cljs-deps) :portal.test-ui-runner)) 34 | 35 | (defn cljs-nbb [] 36 | (t/nbb "-m" :portal.test-runtime-runner)) 37 | 38 | (defn- setup* [version] 39 | (t/clj 40 | "-Sforce" "-Spath" "-Sdeps" 41 | (pr-str {:deps {'org.clojure/clojurescript {:mvn/version version}}}))) 42 | 43 | (defn setup [] 44 | (setup* "1.10.773") 45 | (setup* "1.10.844")) 46 | 47 | (defn cljs [] 48 | (build) 49 | (cljs-runtime "1.10.773") 50 | (cljs-runtime "1.10.844") 51 | (cljs-nbb)) 52 | 53 | (defn clj 54 | [] 55 | (build) 56 | (t/clj "-M:test" "-m" :portal.test-runner) 57 | (t/bb "-m" :portal.test-runner)) 58 | 59 | (defn cljr [] 60 | (build) 61 | (t/cljr "-m" :portal.test-clr)) 62 | 63 | (defn test* [] 64 | (future (cljs-runtime "1.10.773")) 65 | (future (cljs-runtime "1.10.844")) 66 | (future 67 | (build) 68 | (future (t/clj "-M:test" "-m" :portal.test-runner)) 69 | (future (t/bb "-m" :portal.test-runner)) 70 | (future (cljr)))) 71 | 72 | (defn test "Run all clj/s tests." [] (cljs) (cljs-ui) (clj)) 73 | 74 | (defn -main [] (test)) 75 | -------------------------------------------------------------------------------- /dev/tasks/version.clj: -------------------------------------------------------------------------------- 1 | (ns tasks.version 2 | (:require [clojure.string :as str] 3 | [tasks.info :refer [version]] 4 | [tasks.tools :refer [git]]) 5 | (:import [java.time LocalDateTime] 6 | [java.time.format DateTimeFormatter])) 7 | 8 | (defn- find-version [file-name] 9 | (first (re-find #"(\d+)\.(\d+)\.(\d+)" (slurp file-name)))) 10 | 11 | (defn- get-date [] 12 | (.format 13 | (DateTimeFormatter/ofPattern "yyyy-MM-dd") 14 | (LocalDateTime/now))) 15 | 16 | (defn- changelog [version] 17 | (let [content (slurp "CHANGELOG.md")] 18 | (when-not (str/includes? content version) 19 | {"CHANGELOG.md" (str "## " version " - " (get-date) "\n\n" content)}))) 20 | 21 | (def files 22 | ["README.md" 23 | "extension-intellij/gradle.properties" 24 | "extension-vscode/package.json" 25 | "package.json" 26 | "src/portal/extensions/vs_code.cljs" 27 | "src/portal/runtime.cljc" 28 | "src/portal/runtime/index.cljc"]) 29 | 30 | (defn- version-updates [next-version] 31 | (let [current-version (find-version "src/portal/runtime/index.cljc")] 32 | (merge 33 | (changelog next-version) 34 | (zipmap 35 | files 36 | (for [file files] 37 | (str/replace (slurp file) current-version next-version)))))) 38 | 39 | (defn- set-version [version] 40 | (doseq [[file-name content] (version-updates version)] 41 | (spit file-name content))) 42 | 43 | (defn tag 44 | "Commit and tag a version." 45 | [] 46 | (set-version version) 47 | (git :add "-u") 48 | (git :commit "-m" (str "Release " version)) 49 | (git :tag version)) 50 | 51 | (defn -main [version] (set-version version)) 52 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user) 2 | 3 | (defn lazy-fn [symbol] 4 | (fn [& args] (apply (requiring-resolve symbol) args))) 5 | 6 | (def start! (lazy-fn 'shadow.cljs.devtools.server/start!)) 7 | (def watch (lazy-fn 'shadow.cljs.devtools.api/watch)) 8 | (def repl (lazy-fn 'shadow.cljs.devtools.api/repl)) 9 | 10 | (defn cljs 11 | ([] (cljs :client)) 12 | ([build-id] (start!) (watch build-id) (repl build-id))) 13 | 14 | (defn node [] (cljs :node)) 15 | 16 | (comment 17 | (require '[portal.api :as p]) 18 | (add-tap #'p/submit) 19 | (remove-tap #'p/submit) 20 | 21 | (watch :pwa) 22 | 23 | (p/clear) 24 | (p/close) 25 | (p/docs {:mode :dev}) 26 | 27 | (def portal (p/open {:launcher :auto})) 28 | (def dev (p/open {:mode :dev})) 29 | (def emacs (p/open {:mode :dev :launcher :emacs})) 30 | (def code (p/open {:mode :dev :launcher :vs-code})) 31 | (def idea (p/open {:mode :dev :launcher :intellij})) 32 | (def work (p/open {:mode :dev :main 'workspace/app})) 33 | (def tetris (p/open {:mode :dev :main 'tetris.core/app})) 34 | 35 | (p/repl portal) 36 | 37 | (require '[examples.data :refer [data]]) 38 | (dotimes [_i 25] (tap> data))) -------------------------------------------------------------------------------- /dev/workspace.cljs: -------------------------------------------------------------------------------- 1 | (ns workspace 2 | (:require ["./hello" :as hello] 3 | ["@fortawesome/free-solid-svg-icons/faArrowDown" :refer [faArrowDown]] 4 | ["react" :as react] 5 | [portal.colors :as c] 6 | [portal.ui.icons :as icons] 7 | [portal.ui.inspector :as ins] 8 | [portal.ui.styled :as s] 9 | [portal.ui.theme :as theme] 10 | [reagent.core :as r])) 11 | 12 | (.log js/console hello/world) 13 | 14 | (def counter (r/atom 0)) 15 | 16 | (defn button [props & children] 17 | (let [theme (theme/use-theme)] 18 | (into 19 | [s/button 20 | (merge 21 | {:style 22 | {:border :none 23 | :cursor :pointer 24 | :padding (:padding theme) 25 | :font-size (:font-size theme) 26 | :border-radius (:border-radius theme) 27 | :background (::c/boolean theme) 28 | :font-family (:font-family theme) 29 | :color (::c/text theme)}} 30 | props)] 31 | children))) 32 | 33 | (defn app [] 34 | (let [theme (theme/use-theme) 35 | [state set-state!] (react/useState 0)] 36 | [s/div 37 | {:style 38 | {:border-top [1 :solid (::c/border theme)] 39 | :padding (* 2 (:padding theme))}} 40 | [s/h1 {:style 41 | {:display :flex 42 | :color (::c/boolean theme)}} 43 | "Counter: " 44 | [ins/inspector @counter] 45 | [ins/inspector state]] 46 | [button {:on-click 47 | (fn [] 48 | (set-state! (inc state)) 49 | (tap> {:action :inc 50 | :current @counter 51 | :next (swap! counter inc)}))} 52 | "Click me!" [icons/icon faArrowDown {}]]])) 53 | -------------------------------------------------------------------------------- /djblue.portal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/cli.md: -------------------------------------------------------------------------------- 1 | # CLI Usage 2 | 3 | Add a portal alias in `~/.clojure/deps.edn` 4 | 5 | ```clojure 6 | :portal/cli 7 | {:main-opts ["-m" "portal.main"] 8 | :extra-deps 9 | {djblue/portal {:mvn/version "LATEST"} 10 | ;; optional yaml support 11 | clj-commons/clj-yaml {:mvn/version "0.7.0"}}} 12 | ``` 13 | 14 | Then do the following depending on your data format: 15 | 16 | ```bash 17 | cat data | clojure -M:portal/cli [edn|json|transit|yaml] 18 | # or with babashka for faster startup 19 | cat data | bb -cp `clojure -Spath -M:portal/cli` -m portal.main [edn|json|transit|yaml] 20 | ``` 21 | 22 | I keep the following bash aliases handy for easier CLI use: 23 | 24 | ```bash 25 | alias portal='bb -cp `clojure -Spath -M:portal/cli` -m portal.main' 26 | alias edn='portal edn' 27 | alias json='portal json' 28 | alias transit='portal transit' 29 | alias yaml='portal yaml' 30 | ``` 31 | 32 | and often use the `Copy as cURL` feature in the chrome network tab to do 33 | the following: 34 | 35 | ```bash 36 | curl ... | transit 37 | ``` 38 | 39 | There is also the ability to invoke a standalone http server to listen and 40 | display data from remote client 41 | 42 | ```bash 43 | bb -cp `clojure -Spath -Sdeps '{:deps {djblue/portal {:mvn/version "LATEST"}}}'` \ 44 | -e '(require (quote [portal.api])) (portal.api/open {:port 53755}) @(promise)' 45 | ``` 46 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree 2 | [["Readme" {:file "README.md"}] 3 | ["UI Concepts" 4 | {:file "doc/ui/index.md"} 5 | ["Selection" {:file "doc/ui/selection.md"}] 6 | ["Viewers" {:file "doc/ui/viewers.md"}] 7 | ["Commands" {:file "doc/ui/commands.md"}] 8 | ["History" {:file "doc/ui/history.md"}] 9 | ["Shortcuts" {:file "doc/ui/shortcuts.md"}] 10 | ["Filtering" {:file "doc/ui/filtering.md"}] 11 | ["Themes" {:file "doc/ui/themes.md"}]] 12 | ["Remote API" {:file "doc/remote-api.md"}] 13 | ["Guides" 14 | ["Node Babashka" {:file "examples/nbb/README.md"}] 15 | ["Exceptions" {:file "doc/guides/exceptions.md"}] 16 | ["Logging" 17 | ["Portal Console" {:file "doc/guides/portal-console.md"}] 18 | ["Timbre" {:file "examples/timbre/README.md"}] 19 | ["μ/log" {:file "examples/mulog/README.md"}]] 20 | ["Tufte Profiling" {:file "examples/tufte/README.md"}] 21 | ["JavaScript Promises" {:file "doc/guides/promises.md"}] 22 | ["Custom Tap List" {:file "doc/guides/custom-taps.md"}] 23 | ["Default Viewer" {:file "doc/guides/default-viewer.md"}] 24 | ["Test Runner" {:file "doc/guides/test-runner.md"}] 25 | ["Git Dep" {:file "doc/guides/git-deps.md"}] 26 | ["Shadow CLJS" {:file "doc/guides/shadow-cljs.md"}] 27 | ["nREPL" {:file "doc/guides/nrepl.md"}] 28 | ["Portal Standalone" {:file "doc/guides/standalone.md"}] 29 | ["Clerk" {:file "doc/guides/clerk.md"}] 30 | ["Custom Viewer" {:file "examples/portal-present/README.md"}]] 31 | ["Editors" 32 | ["Emacs" {:file "doc/editors/emacs.md"}] 33 | ["VS Code" {:file "doc/editors/vs-code.md"} 34 | ["Clojure Notebooks" {:file "doc/editors/vs-code-notebook.md"}] 35 | ["Joyride" {:file "doc/editors/vs-code-joyride.md"}]] 36 | ["Intellij" {:file "doc/editors/intellij.md"}]] 37 | ["CLI Usage" {:file "doc/cli.md"}] 38 | ["Datafy / Nav" {:file "doc/datafy.md"}] 39 | ["Video Presentations" {:file "doc/videos.md"}] 40 | ["Dev Guide" 41 | {:file "doc/dev/index.md"} 42 | ["System Deps" {:file "doc/dev/deps.md"}] 43 | ["Editor Setup" {:file "doc/dev/editors.md"}] 44 | ["Dev Tasks" {:file "doc/dev/tasks.md"}]] 45 | ["Inspiration" {:file "doc/inspiration.md"}] 46 | ["Limitations" {:file "doc/limitations.md"}]]} 47 | -------------------------------------------------------------------------------- /doc/dev/deps.md: -------------------------------------------------------------------------------- 1 | # System Dependencies 2 | 3 | To get started with development on Portal, make sure you have the following 4 | dependencies installed on your system: 5 | 6 | - [java](https://openjdk.java.net/) - for clojure runtime 7 | - for osx, do `brew install openjdk` 8 | - [babashka](https://babashka.org/) - for build scripting 9 | - for osx, do: `brew install borkdude/brew/babashka` 10 | - to list all build tasks, do: `bb tasks` 11 | - [node, npm](https://nodejs.org/) - for javascript dependencies 12 | - for osx, do: `brew install node` 13 | -------------------------------------------------------------------------------- /doc/dev/editors.md: -------------------------------------------------------------------------------- 1 | # Editor Setup 2 | 3 | For editor specific development tips, try the following: 4 | 5 | ## vim + [vim-fireplace](https://github.com/tpope/vim-fireplace) 6 | 7 | To start a dev server, do: 8 | 9 | ```bash 10 | bb dev 11 | ``` 12 | 13 | vim-fireplace should automatically connect upon evaluation, but this will 14 | only be for clj files, to get a cljs repl, do: 15 | 16 | ```vim 17 | :CljEval (user/cljs) 18 | ``` 19 | 20 | ## emacs + [cider](https://cider.mx/) 21 | 22 | The best way to get started via emacs is to have cider start the repl, do: 23 | 24 | ```bash 25 | M-x cider-jack-in-clj&cljs 26 | ``` 27 | 28 | [.dir-locals.el](../../.dir-locals.el) has all the configuration variables for 29 | cider. 30 | 31 | > [!NOTE] 32 | > When using cider-jack-in, you will need to manually `npm install` 33 | > when the `package.json` or `package-lock.json` changes. 34 | -------------------------------------------------------------------------------- /doc/dev/index.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | After [installing dependencies](./deps.md) and 4 | [setting up an editor](./editors.md), the following should be enough to get you 5 | started with Portal development. To kick off a dev server, do: 6 | 7 | ```bash 8 | bb dev 9 | bb tasks # List all bb tasks 10 | ``` 11 | 12 | For more info on dev tasks, see [tasks.md](./tasks.md). 13 | 14 | ## [`user.clj`](../../dev/user.clj) 15 | 16 | A good place to start poking around is in the [user](../../dev/user.clj) 17 | namespace. It has a bunch of useful example code for development. Take a peek to 18 | get going, but here are a few important bits. 19 | 20 | ## `:mode` `:dev` 21 | 22 | By default, anytime a Portal window is opened, it loads the production UI. If 23 | you intend to edit the [UI](../../src/portal/ui) code, you will need to start 24 | Portal with the following option: 25 | 26 | ```clojure 27 | (portal.api/open {:mode :dev}) 28 | ``` 29 | -------------------------------------------------------------------------------- /doc/dev/tasks.md: -------------------------------------------------------------------------------- 1 | # Dev Tasks 2 | 3 | ## Formatting 4 | 5 | To format source code, do: 6 | 7 | ```bash 8 | bb fmt 9 | ``` 10 | 11 | ## CI Checks 12 | 13 | To run ci checks, do: 14 | 15 | ```bash 16 | bb ci # run all ci check 17 | 18 | bb check # run just the static analysis 19 | bb test # run just the tests 20 | ``` 21 | 22 | ## E2E Testing 23 | 24 | To run the e2e tests in the jvm, node and web environments, do: 25 | 26 | ```bash 27 | bb e2e 28 | ``` 29 | 30 | > [!NOTE] 31 | > these aren't fully automated tests. They depend on a human for verification 32 | > and synchronization but it beats having to type everything out manually into 33 | > a repl. 34 | 35 | ## Extensions 36 | 37 | To build the [vs-code](./extension-vscode) and [intellij](./extension-intellij) extensions, do: 38 | 39 | ```bash 40 | bb ext 41 | ``` 42 | 43 | ### VS Code Dev 44 | 45 | To launch a dev version of the vs-code extension, open a new directory with 46 | [extension-vscode](../../extension-vscode/) as the root. Then under the `Run and 47 | Debug` tab you should see a `Run Portal` action which will launch a new vs-code 48 | dev instance, I like to open the [root](../../) Portal directory in this 49 | instance. You can also connect to shadow-cljs via the `:vs-code` or 50 | `:vs-code-notebook` builds to eval code at the repl. [Here][shadow-cljs] are the 51 | editor specific shadow-cljs guides. 52 | 53 | ## Deployment 54 | 55 | To deploy to a release to [clojars](https://clojars.org/djblue/portal), do: 56 | 57 | ```bash 58 | bb tag 59 | bb deploy 60 | ``` 61 | 62 | [shadow-cljs]: https://shadow-cljs.github.io/docs/UsersGuide.html#_editor_integration -------------------------------------------------------------------------------- /doc/editors/intellij.md: -------------------------------------------------------------------------------- 1 | # Intellij 2 | 3 | The Intellij plugin provides a way to embed the Portal UI directly into your IDE. 4 | 5 | [![Screenshot](https://user-images.githubusercontent.com/1986211/140680825-431459a8-02d5-40f8-b71c-42aa026cfe93.png)](https://plugins.jetbrains.com/plugin/18467-portal-inspector/) 6 | 7 | You can download the IntelliJ plugin from the [Jet Brains 8 | Marketplace](https://plugins.jetbrains.com/plugin/18467-portal-inspector/). 9 | "Portal" button will appear on the right-hand bar. The window area will be blank 10 | until portal is launched from the REPL. 11 | 12 | Add a dependency on Portal to your `deps.edn` or `project.clj`, start a Clojure 13 | REPL and inside that evaluate: 14 | 15 | ```clojure 16 | (do 17 | (def user/portal ((requiring-resolve 'portal.api/open) {:launcher :intellij})) 18 | (add-tap (requiring-resolve 'portal.api/submit))) 19 | ``` 20 | 21 | You can now `tap>` data and they will appear in the Portal tool window. 22 | 23 | ## Features 24 | 25 | The main benefits of using this plugin are: 26 | 27 | - Automatic font / theme discovery. 28 | - No window management. 29 | - Editor specific commands. See: goto-definition in the command palette. 30 | 31 | ## Debugging / Troubleshooting 32 | 33 | If after running `portal.api/open` at the REPL, the Portal UI does not open, it 34 | is most likely due to having a multi-module project. When the plugin is started, 35 | it writes a `.portal/intellij.edn` file, which the REPL process will try to 36 | find. If the REPL process is started outside of the root project, it will not be 37 | able to use the Intellij plugin. 38 | 39 | A quick hack to get around this problem is to symlink the `.portal` directory to 40 | the directory where the REPL process is started. 41 | 42 | Similarly, if you get the following error message, simply remove the `.portal` directory, and try calling `(p/open {:launcher :intellij}` again. 43 | (This might be caused by the portal intellij extension not being initialized after upgrading, but you have an existing .portal/intellij.edn -- the clj runtime is trying to connect to a server that is no longer running.) 44 | 45 | ``` 46 | Execution error (ConnectException) at sun.nio.ch.Net/pollConnect (Net.java:-2). 47 | Connection refused 48 | ``` 49 | -------------------------------------------------------------------------------- /doc/editors/vs-code-joyride.md: -------------------------------------------------------------------------------- 1 | # Joyride 2 | 3 | ![image](https://github.com/djblue/portal/assets/1986211/1fd37185-7304-4572-86c6-4207adc3f449) 4 | 5 | To leverage the `portal.api` namespace from your Joyride script, ensure you have 6 | the following VS Code extensions installed: 7 | 8 | - [Joyride](https://marketplace.visualstudio.com/items?itemName=betterthantomorrow.joyride) 9 | - [Calva](https://marketplace.visualstudio.com/items?itemName=betterthantomorrow.calva) 10 | - [Portal](https://marketplace.visualstudio.com/items?itemName=djblue.portal) 11 | 12 | Then after connection to a [Joyride REPL](https://calva.io/joyride/#how-to-connect), 13 | you should be able to follow the [API guide](../../README.md#api). 14 | -------------------------------------------------------------------------------- /doc/editors/vs-code-notebook.md: -------------------------------------------------------------------------------- 1 | # Clojure Notebooks 2 | 3 | If you are interested in using Portal as a renderer for [Calva's Clojure 4 | Notebooks][1], you've come to the right place. 5 | 6 | Before getting started, make sure you have both the [Calva][2] and [Portal][3] VS Code extensions installed. 7 | 8 | ## Usage 9 | 10 |

11 | 12 |

13 | 14 | With the above extensions installed, you should now be able to open any Clojure 15 | file by right clicking the file in the file explorer and selecting `Open 16 | With...` and selecting `Clojure Notebook`. The output cells have a three dot 17 | menu where you can change the presentation to Portal! 18 | 19 | > [!NOTE] 20 | > Calva gives Portal the evaluation results as an edn string therefore 21 | > this mode of Portal will not be the same as a process connected Portal 22 | > instance. 23 | 24 | [1]: https://calva.io/notebooks/ 25 | [2]: https://marketplace.visualstudio.com/items?itemName=betterthantomorrow.calva 26 | [3]: https://marketplace.visualstudio.com/items?itemName=djblue.portal 27 | 28 | ## `portal.nrepl/wrap-notebook` 29 | 30 | If you would like the same facilities provided by a process connected Portal 31 | instance directly in your Calva notebooks, simply add the 32 | `portal.nrepl/wrap-notebook` middleware to your nrepl stack. This will enable: 33 | 34 | - [Datafy + Nav](../datafy.md) 35 | - First class object support 36 | - Runtime registered commands 37 | - Larger values which are harder for edn to serialize and parse 38 | - Better error rendering 39 | -------------------------------------------------------------------------------- /doc/editors/vs-code.md: -------------------------------------------------------------------------------- 1 | # VS Code 2 | 3 | [![Screenshot](https://user-images.githubusercontent.com/1986211/140680881-497efd3b-da75-4220-9630-bf1af9d8bf37.png)](https://marketplace.visualstudio.com/items?itemName=djblue.portal) 4 | 5 | If you are using vs-code, try out the [vs-code-extension][extension]. It allows 6 | launching portal in an embedded [webview][webview] within vs-code. 7 | 8 | For a more in depth look at customizing vs-code for use with portal, 9 | particularly with [calva][calva], take a look at 10 | [seancorfield/vscode-calva-setup][calva-setup]. 11 | 12 | For a more complete workflow guide, checkout [Calva, Joyride, and 13 | Portal][guide] by [@seancorfield][seancorfield]. 14 | 15 | > [!NOTE] 16 | > The version of portal being run in the webview is still decided by 17 | > the runtime in which `(portal.api/open {:launcher :vs-code})` is run. 18 | 19 | [calva]: https://calva.io/ 20 | [calva-setup]: https://github.com/seancorfield/vscode-calva-setup 21 | [guide]: https://corfield.org/blog/2022/12/18/calva-joyride-portal/ 22 | [seancorfield]: https://github.com/seancorfield 23 | [extension]: https://marketplace.visualstudio.com/items?itemName=djblue.portal 24 | [webview]: https://code.visualstudio.com/api/extension-guides/webview 25 | -------------------------------------------------------------------------------- /doc/guides/clerk.md: -------------------------------------------------------------------------------- 1 | # [Clerk](https://github.com/nextjournal/clerk) 2 | 3 | If you would like to leverage Portal in the context of a clerk notebook, you can 4 | setup a viewer such as the following: 5 | 6 | > [!WARNING] 7 | > Every viewer is a new iframe so having too many on the page will 8 | > cause performance issues. 9 | 10 | ```clojure 11 | (ns portal.clerk 12 | (:require [nextjournal.clerk :as clerk] 13 | [portal.api :as p])) 14 | 15 | (def app-viewer 16 | {:name :portal/app 17 | :transform-fn 18 | (fn [value] 19 | (p/url 20 | (p/open {:launcher false 21 | :value (:nextjournal/value value) 22 | :theme :portal.colors/nord-light}))) 23 | :render-fn '#(v/html [:iframe 24 | {:src % 25 | :style {:width "100%" 26 | :height "50vh" 27 | :border-left "1px solid #d8dee9" 28 | :border-right "1px solid #d8dee9" 29 | :border-bottom "1px solid #d8dee9" 30 | :border-radius 2}}])}) 31 | 32 | (defn open 33 | "Open portal with the value of `x` in current notebook." 34 | ([x] (open {} x)) 35 | ([viewer-opts x] 36 | (clerk/with-viewer app-viewer viewer-opts x))) 37 | ``` 38 | -------------------------------------------------------------------------------- /doc/guides/custom-taps.md: -------------------------------------------------------------------------------- 1 | # Custom Tap List 2 | 3 | If the default behavior of Portal's tap list isn't exactly what you need, you 4 | can implement your own with any behavior your require. 5 | 6 | ## Constraints 7 | 8 | For this example, let's assume you have the following constraints: 9 | 10 | - The tap list should be displayed as a table 11 | - Every tapped value should have an `:id` and a `:time` 12 | - The order of columns should be `:id`, `:value`, and `:time` 13 | - The tap list should grow at the bottom (chronological) 14 | - The tap list should not exceed 25 items, dropping elements from the top 15 | 16 | ## Implementation 17 | 18 | Firstly, lets specify metadata to tell Portal how to render the tap-list: 19 | 20 | ```clojure 21 | (def viewer 22 | {:portal.viewer/default :portal.viewer/table 23 | :portal.viewer/table {:columns [:id :value :time]}}) 24 | 25 | (def tap-list (atom (with-meta [] viewer))) 26 | ``` 27 | 28 | Secondly, setup a custom submit function to implement the stated constraints: 29 | 30 | ```clojure 31 | (def ids (atom 0)) 32 | 33 | (defn submit [value] 34 | (let [id (swap! ids inc)] 35 | (swap! tap-list 36 | (fn [taps] 37 | (conj 38 | (if (< (count taps) 25) 39 | taps 40 | (subvec taps 1)) 41 | {:id id 42 | :value value 43 | :time (java.util.Date.)}))))) 44 | ``` 45 | 46 | Finally, you can wire all this up into Portal with the following: 47 | 48 | ```clojure 49 | (add-tap #'submit) 50 | 51 | (require '[portal.api :as p]) 52 | (p/open {:value tap-list}) 53 | 54 | (tap> :hello) 55 | ``` 56 | 57 | Which looks something like: 58 | 59 | ![Table Tap List](https://user-images.githubusercontent.com/1986211/164960811-b7ebfa17-05ab-4b6b-be5f-f3b9b0f742d7.png) 60 | -------------------------------------------------------------------------------- /doc/guides/default-viewer.md: -------------------------------------------------------------------------------- 1 | # Default Viewer 2 | 3 | If you would like to add some logic around which viewer to use by default in 4 | Portal, the easiest way is to provide a `:portal.viewer/default` key as part of 5 | a value's metadata. Unfortunately, not all values support metadata, such as 6 | strings. They way around this limitation is to wrap the value in a container 7 | that does support metadata, such as vectors. However, instead of managing this 8 | manually, we can leverage the `portal.viewer` ns to help provide default viewers 9 | for values. 10 | 11 | Here is one such default viewer selector: 12 | 13 | ```clojure 14 | (require '[portal.viewer :as v]) 15 | 16 | (def defaults 17 | {string? v/text 18 | bytes? v/bin}) 19 | 20 | (defn- get-viewer-f [value] 21 | (or (some (fn [[predicate viewer]] 22 | (when (predicate value) 23 | viewer)) 24 | defaults) 25 | v/tree)) 26 | ``` 27 | 28 | With a corresponding submit function: 29 | 30 | ```clojure 31 | (require '[portal.api :as p]) 32 | 33 | (defn submit [value] 34 | (let [f (get-viewer-f value)] 35 | (p/submit (f value)))) 36 | 37 | (add-tap #'submit) 38 | ``` 39 | 40 | Now by tapping the following values you can see the default viewer selector in 41 | action: 42 | 43 | ```clojure 44 | (tap> "hello, world") 45 | (tap> (byte-array [0 1 2 3])) 46 | (tap> (range 10)) 47 | ``` 48 | -------------------------------------------------------------------------------- /doc/guides/git-deps.md: -------------------------------------------------------------------------------- 1 | # Portal Git Dep 2 | 3 | To use Portal as a [git dep](https://clojure.org/news/2018/01/05/git-deps), you 4 | should add an alias like the following to your `deps.edn`: 5 | 6 | ```clojure 7 | :aliases 8 | {:dev 9 | {:extra-deps 10 | {djblue/portal 11 | {:git/url "https://github.com/djblue/portal.git" 12 | :sha "1645d580f7487657991ad112381104c133342faa"}}}} 13 | ``` 14 | 15 | Or using the [newer format](https://clojure.org/guides/deps_and_cli#_using_git_libraries): 16 | 17 | ```clojure 18 | :aliases 19 | {:dev 20 | {:extra-deps 21 | {io.github.djblue/portal 22 | {:git/tag "0.25.0" :git/sha "1645d58"}}}} 23 | ``` 24 | 25 | However, this will fail to resolve with the following error: 26 | 27 | ``` 28 | Checking out: https://github.com/djblue/portal.git at 1645d580f7487657991ad112381104c133342faa 29 | Error building classpath. The following libs must be prepared before use: [io.github.djblue/portal] 30 | 31 | ``` 32 | 33 | To use Portal from source, you need to perform a build step for the UI. The UI 34 | build assumes you have [node.js](https://nodejs.org/) available on your system. 35 | 36 | Now, to prepare Portal from source, use the following `clj` command: 37 | 38 | ```bash 39 | clj -X:deps prep :aliases '[:dev]' 40 | ``` 41 | 42 | That's it, you should be able to use Portal! 43 | -------------------------------------------------------------------------------- /doc/guides/portal-console.md: -------------------------------------------------------------------------------- 1 | # portal.console 2 | 3 | A major advantage of [`tap>`](https://clojuredocs.org/clojure.core/tap%3E) over 4 | [`println`](https://clojuredocs.org/clojure.core/println) is that it allows you 5 | to keep all values as data. However, as with `println`, if you `tap>` too many 6 | values, it is very easy to lose track where those values came from. Luckily, 7 | there is an easy fix, clojure macros. 8 | 9 | The `portal.console` namespace has a handful of macros that will not only `tap>` 10 | values, but capture the context in which those values are tapped. This includes 11 | source code, time and runtime information. Additionally, the data is formatted 12 | to work with the `:portal.viewer/log` viewer. 13 | 14 | ## Example 15 | 16 | ```clojure 17 | ;; user.clj 18 | (require '[portal.console :as log]) 19 | 20 | (log/trace ::trace) 21 | (log/debug ::debug) 22 | (log/info ::info) 23 | (log/warn ::warn) 24 | (log/error ::error) 25 | ``` 26 | 27 | Will produce the following: 28 | 29 | ![logs](https://user-images.githubusercontent.com/1986211/196558924-d07fa896-2550-427e-b437-9a6f83fba1fb.png) 30 | 31 | > [!NOTE] 32 | > The data produced by `portal.console` is compatible with the 33 | > `portal.runtime.jvm.editor/goto-definition` command which can automatically 34 | > open the file location via the `:launcher` / `:editor` option passed to 35 | > `portal.api/open`. 36 | 37 | ## Spec 38 | 39 | If you could like the generate data for the log viewer in another context, the 40 | following specs will be useful: 41 | 42 | ```clojure 43 | (def levels 44 | [:trace :debug :info :warn :error :fatal :report]) 45 | 46 | (s/def ::level (set levels)) 47 | 48 | (s/def ::ns symbol?) 49 | (s/def ::time inst?) 50 | 51 | (s/def ::column int?) 52 | (s/def ::line int?) 53 | 54 | (s/def ::log 55 | (s/keys :req-un 56 | [::level 57 | ::ns 58 | ::time 59 | ::line 60 | ::column])) 61 | ``` 62 | -------------------------------------------------------------------------------- /doc/guides/promises.md: -------------------------------------------------------------------------------- 1 | # JavaScript Promises 2 | 3 | In a JavaScript context, you often need to deal with asynchronicity and this 4 | often involves promises. However, when working with promises in ClojureScript, 5 | getting at the value of a promise can be a bit tedious. I often see the 6 | following pattern for `println` debugging. 7 | 8 | ```clojure 9 | (defn async-fn [] 10 | (.resolve js/Promise :hello)) 11 | 12 | (.then (async-fn) println) 13 | ``` 14 | 15 | I'm not a huge fan of this pattern, and worse yet, if you forget to wrap the 16 | value, you get `#object [Promise [object Promise]]` which isn't very helpful. 17 | 18 | Thankfully, in ClojureScript you have 19 | [`tap>`](https://clojuredocs.org/clojure.core/tap%3E) which can help you with 20 | this issue. 21 | 22 | With portal, you would typically do something like: 23 | 24 | ```clojure 25 | (require '[portal.api :as p]) 26 | (add-tap #'p/submit) 27 | 28 | (tap> (async-fn)) ;; #object [Promise [object Promise]] 29 | ``` 30 | 31 | But this has the same issue as before when you printed the promise value. To 32 | improve `tap>` usage around promises, you simply need to provide a specialized 33 | tap target, like the following: 34 | 35 | ```clojure 36 | (require '[portal.api :as p]) 37 | 38 | (defn async-submit [value] 39 | (if-not (instance? js/Promise value) 40 | (p/submit value) 41 | (-> value 42 | (.then p/submit) 43 | (.catch p/submit)))) 44 | 45 | (add-tap #'async-submit) 46 | 47 | (tap> (async-fn)) ;; :hello 48 | ``` 49 | -------------------------------------------------------------------------------- /doc/guides/test-runner.md: -------------------------------------------------------------------------------- 1 | # Test Runner 2 | 3 | With the addition of `:portal.viewer/test-report`, Portal can now render your 4 | `clojure.test` output! 5 | 6 | However, there is no automatic mechanism for delivering test report data 7 | directly to Portal, that integration is up to you. 8 | 9 | ## Example Integration 10 | 11 | To get started, let's assume you have the following tests: 12 | 13 | ```clojure 14 | (ns user 15 | (:require [clojure.test :refer [deftest is] :as t])) 16 | 17 | (deftest hello-world 18 | (is (= 0 0)) 19 | (is (= (name :hello) (name :world)))) 20 | ``` 21 | 22 | There are many ways to run clojure tests, but an easy way is via 23 | [clojure.test/run-tests](https://clojuredocs.org/clojure.test/run-tests), so 24 | let's go with that. 25 | 26 | ```clojure 27 | (defn run-test [ns] 28 | (let [report (atom [])] 29 | (tap> report) 30 | (with-redefs [t/do-report #(swap! report conj %)] 31 | (t/run-tests ns)))) 32 | 33 | (run-test 'user) 34 | ``` 35 | 36 | That's pretty much it. The key here is that you simply need to intercept values 37 | passed to [`clojure.test/do-report`](https://clojuredocs.org/clojure.test/do-report) 38 | and send them directly to Portal. 39 | 40 | With the code above, you should get something like: 41 | 42 | ![portal test runner](https://user-images.githubusercontent.com/1986211/165010389-96c610a4-7963-4343-863c-4a68fafdf40f.png) 43 | 44 | Although this example is trivial, the main advantage to getting test result out 45 | as data is particularly useful when dealing with large values in a test 46 | assertion. 47 | 48 | ### Tip 49 | 50 | You can [select](./ui/selection.md) two test output values and diff them via the 51 | [`lambdaisland.deep-diff2/diff` command](./ui/commands.md) 52 | 53 | ![portal test runner diff](https://user-images.githubusercontent.com/1986211/165010558-d5a86019-9a8b-4259-b808-dc0746853586.png) 54 | -------------------------------------------------------------------------------- /doc/inspiration.md: -------------------------------------------------------------------------------- 1 | # Inspiration 2 | 3 | - [Reveal](https://github.com/vlaaad/reveal) 4 | - [Talk](https://www.youtube.com/watch?v=jq-7aiXPRKs) 5 | - [Clouseau](https://common-lisp.net/project/mcclim/static/manual/mcclim.html) 6 | - [Demo Video](https://youtu.be/-1LzFxTbU9E) 7 | - [clojure.inspector](https://clojuredocs.org/clojure.inspector/inspect) 8 | - [REBL](https://github.com/cognitect-labs/REBL-distro) 9 | - [punk](https://github.com/Lokeh/punk) 10 | - [shadow-cljs inspect](https://clojureverse.org/t/introducing-shadow-cljs-inspect/5012) 11 | -------------------------------------------------------------------------------- /doc/limitations.md: -------------------------------------------------------------------------------- 1 | # Limitations 2 | 3 | Since the Portal UI is implemented in ClojureScript, it also adopts some of its 4 | platform limitations. 5 | 6 | ## Dates 7 | 8 | Dates captured from your host platform are interpreted as `js/Date` in the 9 | Portal UI. Therefore, as documents by this [ClojureScript issue][date-issue], 10 | they are potentially interpreted against a different calendar. 11 | 12 | ## Longs 13 | 14 | Since JavaScript doesn't support the full range of numbers provided by longs, 15 | integers outside of a certain range must be boxed and shipped as string to the 16 | UI. This precludes them from participating in viewers that expect `js/Number`. 17 | 18 | ## Doubles 19 | 20 | Since Portal serializes values as JSON, the treatment of doubles can become 21 | inexact. Any double value submitted to Portal with no decimal component can get 22 | "downcast" to a long when rendered or brought back into the REPL. This is 23 | further complicated since container values (maps, sets, vectors, lists) are 24 | captured and can be returned to the REPL exactly therefore avoiding this issue. 25 | In any case, Portal should not be replied upon to preserve exact numeric types. 26 | 27 | ## Lazy Values 28 | 29 | Lazy values that contain exceptions will cause Portal's internals to break if 30 | allowed to flow into the tap-list. By trying to partially realize lazy values 31 | before accepting them, and rejecting any exceptional values, Portal can keep 32 | working. However, this only applies to the default tap-list. Since Portal can be 33 | opened with any value, it is still possible to break that instance of Portal, 34 | but it should be contained to that specific instance. 35 | 36 | [date-issue]: https://github.com/clojure/clojurescript-site/issues/367 -------------------------------------------------------------------------------- /doc/remote-api.md: -------------------------------------------------------------------------------- 1 | # Remote API 2 | 3 | If running the Portal runtime in process is not supported or not desired, values 4 | can be sent over the wire to a remote instance. 5 | 6 | The following clients are currently implemented: 7 | 8 | - `portal.client.jvm` 9 | - clj on jvm 10 | - clj on [babashka](https://babashka.org/) 11 | - `portal.client.node` 12 | - cljs on [nbb](https://github.com/babashka/nbb) 13 | - cljs on [lumo](https://github.com/anmonteiro/lumo) 14 | - `portal.client.web` 15 | - cljs running in the browser 16 | - `portal.client.planck` 17 | - cljs running in [planck](https://planck-repl.org/) 18 | 19 | ## Usage 20 | 21 | In the process hosting the remote api, do: 22 | 23 | ``` clojure 24 | (require '[portal.api :as p]) 25 | (p/open {:port 5678}) 26 | ``` 27 | 28 | In the client process, do: 29 | 30 | ``` clojure 31 | (require '[portal.client.jvm :as p]) 32 | ;; (require '[portal.client.node :as p]) 33 | ;; (require '[portal.client.web :as p]) 34 | 35 | (def submit (partial p/submit {:port 5678})) ;; :encoding :edn is the default 36 | ;; (def submit (partial p/submit {:port 5678 :encoding :json})) 37 | ;; (def submit (partial p/submit {:port 5678 :encoding :transit})) 38 | 39 | (add-tap #'submit) 40 | ``` 41 | 42 | > [!NOTE] 43 | > `tap>`'d values must be serializable as edn, transit or json. 44 | 45 | ## Tips 46 | 47 | Platform specific tips. 48 | 49 | ### Planck 50 | 51 | Since Planck can load libraries from a jar, you can use the `clj` tool to 52 | generate the class path and pass it in via the `--classpath` cli argument. 53 | 54 | To start a Planck with Portal REPL, do: 55 | 56 | ```bash 57 | clj -Spath -Sdeps '{:deps {djblue/portal {:mvn/version "LATEST"}}}' > .classpath 58 | planck -c `cat .classpath` 59 | ``` 60 | 61 | Then at the REPL, do: 62 | 63 | ```clojure 64 | (require '[portal.client.planck :as p]) 65 | (def submit (partial p/submit {:port 5678})) 66 | (add-tap #'submit) 67 | (tap> :hello-from-planck) 68 | ``` -------------------------------------------------------------------------------- /doc/ui/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | ![portal command palette](https://user-images.githubusercontent.com/1986211/165007466-005c39ee-d4d2-4599-a58e-ad03445b22ec.png) 4 | 5 | The bottom-right yellow button will open the command palette. Commands can have 6 | a `:predicate` function like viewers, so only relevant commands will be visible 7 | which is based on the currently selected value. They will be sorted 8 | alphabetically by name and can quickly be filtered. The `(ctrl | ⌘) + shift + 9 | p` or `ctrl + j` shortcuts can also be used to open the command palette. 10 | 11 | The filter string is split by white space and all words must appear in a name to 12 | be considered a match. 13 | 14 | To register your own command, use the `portal.api/register!` function. For example: 15 | 16 | ``` clojure 17 | (portal.api/register! #'identity) 18 | ``` 19 | 20 | When multiple values are selected, commands will be applied as follows: 21 | 22 | ``` clojure 23 | (apply f [first-selected second-selected ...]) 24 | ``` 25 | 26 | **NOTES:** 27 | 28 | - A very useful command is `portal.ui.commands/copy` which will copy the 29 | currently selected value as an edn string to the clipboard. 30 | - [`lambdaisland.deep-diff2/diff`](https://github.com/lambdaisland/deep-diff2#use) 31 | is a useful command for diffing two selected values. 32 | - Commands manipulating the UI itself will live under the `portal.ui.commands` 33 | namespace. 34 | - The `cljs.core` namespace will be aliased as `clojure.core` when using a 35 | clojurescript runtime. 36 | -------------------------------------------------------------------------------- /doc/ui/filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | ![Screen Shot 2022-09-20 at 10 50 55 PM](https://user-images.githubusercontent.com/1986211/191424638-967fa72b-9932-4073-a5f4-b8050313a7d7.png) 4 | 5 | Like many concepts listed above, filtering is relative to the currently selected 6 | value. If no value is selected, filtering is disabled. When a collection is 7 | selected, the filter text will act to remove elements from that collection, 8 | similar to the command palette. 9 | 10 | Since multiple collections can be filtered at once, the input will change color 11 | and matched words in the collection and its children will be highlighted with 12 | that color. 13 | 14 | To remove the current filters, try the [`portal.ui.commands/clear-filter`](./commands.md) 15 | command. 16 | -------------------------------------------------------------------------------- /doc/ui/history.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | The top-left arrow buttons (`⭠` `⭢`) can be used to navigate through history. 4 | History is built up from commands pushing values into it. For example, anytime a 5 | value is double clicked, 6 | [`clojure.datafy/nav`](https://clojuredocs.org/clojure.datafy/nav) is applied to 7 | that value and the result is pushed onto the history stack. All commands that 8 | produce a new value will do the same. 9 | 10 | To quickly navigation through history, checkout the following commands: 11 | 12 | - `portal.ui.commands/history-back` 13 | - `portal.ui.commands/history-forward` 14 | - `portal.ui.commands/history-first` 15 | - `portal.ui.commands/history-last` 16 | 17 | ## Portal Atom 18 | 19 | In addition to getting the selected value back in the repl, the portal atom also 20 | allows direct manipulation of portal [history](#history). For example: 21 | 22 | ```clojure 23 | (def a (p/open)) 24 | 25 | ; push the value 0 into portal history 26 | (reset! a 0) 27 | 28 | @a ;=> 0 - get current value 29 | 30 | ; inc the current value in portal 31 | (swap! a inc) 32 | 33 | @a ;=> 1 - get current value 34 | ``` 35 | -------------------------------------------------------------------------------- /doc/ui/index.md: -------------------------------------------------------------------------------- 1 | # UI Concepts 2 | 3 | The UI concepts in Portal can be broken down into the following categories: 4 | 5 | - [Selection](./selection.md) 6 | - [Viewers](./viewers.md) 7 | - [Commands](./commands.md) 8 | - [History](./history.md) 9 | - [Shortcuts](./shortcuts.md) 10 | - [Filtering](./filtering.md) 11 | - [Themes](./themes.md) 12 | 13 | Take a look through them to get a feel for how the UI is organized. 14 | -------------------------------------------------------------------------------- /doc/ui/selection.md: -------------------------------------------------------------------------------- 1 | # Selection 2 | 3 | A single click will select a value. The arrow keys, (`⭠` `⭣` `⭡` `⭢`) or (`h` 4 | `j` `k` `l` for vim folks) will change the selection relative to the currently 5 | selected value. Relative selection is based on the [viewer](./viewers.md). 6 | 7 | Selection is an important concept in the Portal UI since it is the basis for a 8 | lot of the other concepts. 9 | 10 | ## Multi-Select 11 | 12 | To select multiple values, hold down either `⌘` or `alt` and single click a 13 | value. To un-select a value, simply click it again while holding down the 14 | multi-select key. The order of selection is important to the application of 15 | [commands](./commands.md). 16 | -------------------------------------------------------------------------------- /doc/ui/shortcuts.md: -------------------------------------------------------------------------------- 1 | # Shortcuts 2 | 3 | To run a command without having to open the command palette, you can use the 4 | commands shortcut. They will be listed in the command palette next to the 5 | command. When multiple shortcuts are available, they are separated by a vertical 6 | bar. 7 | 8 | > [!NOTE] 9 | > Shortcuts aren't currently user definable. 10 | -------------------------------------------------------------------------------- /doc/ui/themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | Portal has a handful of themes built-in that will be shown below. In the case of 4 | [editor extensions](../editors), Portal will try match the theme of your editor. 5 | 6 | The theme can be set via an option to `portal.api/open`: 7 | 8 | ```clojure 9 | (p/open {:theme :portal.colors/nord}) 10 | ``` 11 | 12 | Also, you can quickly toggle the theme via the 13 | [`portal.ui.commands/set-theme`](./commands.md) command. 14 | 15 | ## Screenshots 16 | 17 | ### [`:portal.colors/nord`](https://www.nordtheme.com/) (default) 18 | 19 | ![portal nord theme](https://user-images.githubusercontent.com/1986211/165007021-fb4acae1-0128-45cf-94cd-ce26b456d456.png) 20 | 21 | ### [`:portal.colors/solarized-dark`](https://ethanschoonover.com/solarized/) 22 | 23 | ![portal solarized-dark theme](https://user-images.githubusercontent.com/1986211/165007028-4b37889a-bae4-490f-91c8-508f1b297856.png) 24 | 25 | ### [`:portal.colors/solarized-light`](https://ethanschoonover.com/solarized/) 26 | 27 | ![portal solaized-light theme](https://user-images.githubusercontent.com/1986211/165007035-adb3b88c-f20f-4509-a64d-a0fd4b783f62.png) 28 | 29 | ### `:portal.colors/material-ui` 30 | 31 | ![portal material-ui theme](https://user-images.githubusercontent.com/1986211/165007045-6d2be1bb-2b39-40bc-b4b4-7d2d54cb2a74.png) 32 | 33 | ### [`:portal.colors/zerodark`](https://github.com/NicolasPetton/zerodark-theme) 34 | 35 | ![portal zerodark theme](https://user-images.githubusercontent.com/1986211/165007055-1b8a4b27-8e2d-4d67-b5eb-55fe06fcbc9b.png) 36 | 37 | ### [`:portal.colors/gruvbox`](https://github.com/morhetz/gruvbox) 38 | 39 | ![portal gruvbox theme](https://user-images.githubusercontent.com/1986211/165007060-d5618a30-685c-42d7-88bb-35bd1b77a15a.png) 40 | -------------------------------------------------------------------------------- /doc/ui/viewers.md: -------------------------------------------------------------------------------- 1 | # Viewers 2 | 3 | A viewer takes a value and renders it to the screen in a meaningful way. Since 4 | a value can be visualized in various ways, there are usually multiple viewers to 5 | choose from for a single value. 6 | 7 | Viewers can have a `:predicate` function to define what values they support, so 8 | viewers are automatically filtered based on the value. 9 | 10 | The bottom-left dropdown displays the viewer for the currently 11 | [selected](./selection.md) value and contains all supported viewers for that 12 | value. 13 | 14 | > [!NOTE] 15 | > You can also switch viewers via the `portal.ui.commands/select-viewer` 16 | > [command](./commands.md). 17 | 18 | ## Default Viewers 19 | 20 | Since the default priority order specified in Portal might not be desired, a 21 | default viewer can be set via [metadata](https://clojure.org/reference/metadata). 22 | 23 | For example: 24 | 25 | ```clojure 26 | (tap> ^{:portal.viewer/default :portal.viewer/hiccup} [:h1 "hello, world"]) 27 | ``` 28 | 29 | Or for a dynamic value: 30 | 31 | ```clojure 32 | (def hiccup [:h1 "hello, world"]) 33 | 34 | (tap> (with-meta hiccup {:portal.viewer/default :portal.viewer/hiccup})) 35 | ``` 36 | 37 | ## Tips 38 | 39 | ### String Values 40 | 41 | For values that don't support metadata, such as strings, you can leverage the 42 | hiccup viewer. 43 | 44 | For example, given a markdown string, you could do the following: 45 | 46 | ```clojure 47 | (def md-string "# hello, world") 48 | 49 | (tap> 50 | (with-meta 51 | [:portal.viewer/markdown md-string] 52 | {:portal.viewer/default :portal.viewer/hiccup})) 53 | ``` 54 | 55 | The also illustrates an important aspect about the hiccup viewer, it support 56 | traditional html and portal viewers as markup. 57 | -------------------------------------------------------------------------------- /doc/videos.md: -------------------------------------------------------------------------------- 1 | ## 2023-10-25 - Portal Internals (by Chris Badahdah) 2 | 3 | 4 | Portal Internals (by Chris Badahdah) 6 | 7 | 8 | ## 2023-04-06 - Clojure visual-tools meeting 17 - Various Updates 9 | 10 | 11 | Clojure visual-tools meeting 17 - Various Updates 13 | 14 | 15 | ## 2022-12-07 - Tapping into one of Clojure's superpowers with Portal by James Trunk 16 | 17 | 18 | Tapping into one of Clojure's superpowers with Portal by James Trunk 20 | 21 | 22 | ## 2022-12-01 - Clojure visual-tools meeting 16 - Calva Notebooks & Portal 23 | 24 | 25 | Clojure visual-tools meeting 16 - Calva Notebooks & Portal 27 | 28 | 29 | ## 2022-08-08 - JUXT Safari - Portal, Developing with Data with Chris Williams 30 | 31 | 32 | JUXT Safari - Portal, Developing with Data with Chris Williams 34 | 35 | 36 | ## 2022-04-13 - Meetup: Collaborative Learning - Portal 37 | 38 | 39 | Meetup: Collaborative Learning - Portal 41 | 42 | 43 | ## 2021-11-23 - Thinking with Portal (by Chris Badahdah) 44 | 45 | 46 | Thinking with Portal (by Chris Badahdah) 48 | 49 | 50 | ## 2020-10-15 - Apropos 14 51 | 52 | 53 | Apropos 14 55 | -------------------------------------------------------------------------------- /examples/bb/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {ring/ring {:mvn/version "1.8.2"}} 3 | :aliases 4 | {:dev 5 | {:extra-paths ["dev"] 6 | :extra-deps 7 | {djblue/portal {:mvn/version "0.19.0"}}}}} 8 | -------------------------------------------------------------------------------- /examples/bb/edn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | bb \ 4 | -Dbabashka.httpkit-server.warning=false \ 5 | -cp `clojure -Spath -Sdeps '{:deps {djblue/portal {:mvn/version "0.19.0"}}}'` \ 6 | -m portal.main edn 7 | 8 | -------------------------------------------------------------------------------- /examples/clr/.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .cpcache/ 3 | -------------------------------------------------------------------------------- /examples/clr/Makefile: -------------------------------------------------------------------------------- 1 | repl: .classpath 2 | @CLOJURE_LOAD_PATH=`cat .classpath` Clojure.Main 3 | 4 | .classpath: deps.edn 5 | clojure -X:deps prep 6 | clojure -Spath > .classpath 7 | 8 | deps/nuget: 9 | nuget install clojure.data.json -Version 2.4.0 10 | 11 | deps/clj: .classpath 12 | 13 | clean: 14 | rm .classpath 15 | -------------------------------------------------------------------------------- /examples/clr/README.md: -------------------------------------------------------------------------------- 1 | # Clojure CLR + Portal 2 | 3 | This project demonstrates how you can use Portal from Clojure CLR. 4 | 5 | ## Getting a REPL 6 | 7 | To get a REPL started with Portal as a dependency, do: 8 | 9 | ```bash 10 | make 11 | ``` 12 | 13 | or via a bash script, do: 14 | 15 | ```bash 16 | ./repl 17 | ``` 18 | 19 | This will pull do the following: 20 | 21 | - Pulls the Portal source from github via the [clojure cli][clojure-cli] 22 | - Builds Portal from source, which requires [node+npm][node+npm] 23 | - Builds a class path via the [clojure cli][clojure-cli] saved to `.classpath` 24 | - Starts `Clojure.Main` with `CLOJURE_LOAD_PATH` set to the class path from the 25 | previous step 26 | 27 | You should now be able to follow the [api guide][portal-api] from the main README. 28 | 29 | [clojure-cli]: https://clojure.org/guides/deps_and_cli 30 | [node+npm]: https://nodejs.org/ 31 | [portal-api]: ../../README.md#api 32 | -------------------------------------------------------------------------------- /examples/clr/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps 3 | {djblue/portal 4 | {:git/url "https://github.com/djblue/portal.git" 5 | :sha "52bb1d36b35f1abf025d8c64ccea2f164045c51a"}}} 6 | -------------------------------------------------------------------------------- /examples/clr/repl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clojure -X:deps prep 4 | clojure -Spath > .classpath 5 | 6 | export CLOJURE_LOAD_PATH=`cat .classpath` 7 | Clojure.Main 8 | -------------------------------------------------------------------------------- /examples/git/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {djblue/portal {:local/root "../../"}}} 2 | -------------------------------------------------------------------------------- /examples/git/prep: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf ../../resources/portal 4 | clj -X:deps prep 5 | -------------------------------------------------------------------------------- /examples/jvm/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .cpcache/ 3 | .lein-repl-history 4 | .nrepl-port 5 | -------------------------------------------------------------------------------- /examples/jvm/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {ring/ring {:mvn/version "1.8.2"}} 3 | :aliases 4 | {:dev 5 | {:extra-paths ["dev"] 6 | :extra-deps 7 | {djblue/portal {:mvn/version "0.19.0"}}}}} 8 | -------------------------------------------------------------------------------- /examples/jvm/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [example.server :refer [handler]] 3 | [portal.api :as p] 4 | [ring.adapter.jetty :refer [run-jetty]])) 5 | 6 | (def portal (p/open)) 7 | 8 | (add-tap #'p/submit) 9 | 10 | (def server (atom nil)) 11 | 12 | (defn go [] 13 | (println "starting server on http://localhost:3000") 14 | (reset! server (run-jetty #'handler {:port 3000 :join? false}))) 15 | 16 | (comment 17 | (tap> :hello) 18 | (-> @portal first) 19 | (swap! portal keys)) 20 | 21 | (go) 22 | -------------------------------------------------------------------------------- /examples/jvm/project.clj: -------------------------------------------------------------------------------- 1 | (defproject portal-example-lein "1.0.0" 2 | :dependencies [[ring "1.8.2"]] 3 | :profiles 4 | {:dev {:source-paths ["dev"] 5 | :dependencies [[djblue/portal "0.33.0"]] 6 | :repl-options 7 | {:nrepl-middleware [portal.nrepl/wrap-portal]}}}) 8 | -------------------------------------------------------------------------------- /examples/jvm/src/example/server.clj: -------------------------------------------------------------------------------- 1 | (ns example.server) 2 | 3 | (defn handler [req] 4 | (tap> req) 5 | {:status 200 :body "hello, world"}) 6 | -------------------------------------------------------------------------------- /examples/mulog/README.md: -------------------------------------------------------------------------------- 1 | # μ/log Setup Guide 2 | 3 | [μ/log](https://github.com/brunobonacci/mulog) logs events as data; Portal 4 | navigates data. Let's have them work together. 🤝 5 | 6 | ![](./screenshot.png) 7 | 8 | Using Portal to consume system logs is meant for **development** (and perhaps test) 9 | **environments only**. 10 | 11 | The μ/log publisher defined below can be used together with other publishers, 12 | by configuring a [μ/log multi publisher](https://github.com/BrunoBonacci/mulog/blob/master/doc/publishers/multi-publisher.md). 13 | 14 | ## Basic Setup 15 | 16 | Let's define a custom μ/log publisher that publishes log events with [`tap>`](https://clojuredocs.org/clojure.core/tap%3E): 17 | 18 | ```clojure 19 | (ns user 20 | (:require [com.brunobonacci.mulog :as mu] 21 | [com.brunobonacci.mulog.buffer :as rb] 22 | [portal.api :as p])) 23 | 24 | (deftype TapPublisher [buffer transform] 25 | com.brunobonacci.mulog.publisher.PPublisher 26 | (agent-buffer [_] buffer) 27 | 28 | (publish-delay [_] 200) 29 | 30 | (publish [_ buffer] 31 | (doseq [item (transform (map second (rb/items buffer)))] 32 | (tap> item)) 33 | (rb/clear buffer))) 34 | 35 | (defn tap-publisher 36 | [{:keys [transform] :as _config}] 37 | (TapPublisher. (rb/agent-buffer 10000) (or transform identity))) 38 | ``` 39 | 40 | Next we start μ/log with our custom publisher: 41 | 42 | ```clojure 43 | (def pub (mu/start-publisher! 44 | {:type :custom, :fqn-function "user/tap-publisher"})) 45 | ``` 46 | 47 | What about Portal? We just hook it up as a tap, as we normally do, and the 48 | μ/logs will start flowing in. 49 | 50 | ```clojure 51 | (def p (p/open)) 52 | (add-tap #'p/submit) 53 | 54 | (mu/log ::my-event ::ns (ns-publics *ns*)) 55 | ``` 56 | 57 | Find a working example of this setup [here](https://github.com/djblue/portal/tree/master/examples/mulog). 58 | -------------------------------------------------------------------------------- /examples/mulog/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {djblue/portal {:mvn/version "0.33.0"} 3 | com.brunobonacci/mulog {:mvn/version "0.9.0"}}} 4 | 5 | -------------------------------------------------------------------------------- /examples/mulog/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djblue/portal/82e070d4539f2799717c9110e634f1d18275b631/examples/mulog/screenshot.png -------------------------------------------------------------------------------- /examples/mulog/src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [com.brunobonacci.mulog :as mu] 3 | [com.brunobonacci.mulog.buffer :as rb] 4 | [portal.api :as p])) 5 | 6 | ;; μ/log publisher 7 | ;; Based on https://github.com/BrunoBonacci/mulog/blob/c417d67/mulog-core/src/com/brunobonacci/mulog/publisher.clj#L42-L66 8 | 9 | (deftype TapPublisher [buffer transform] 10 | com.brunobonacci.mulog.publisher.PPublisher 11 | (agent-buffer [_] 12 | buffer) 13 | 14 | (publish-delay [_] 15 | 200) 16 | 17 | (publish [_ buffer] 18 | (doseq [item (transform (map second (rb/items buffer)))] 19 | (tap> item)) 20 | (rb/clear buffer))) 21 | 22 | (defn tap-publisher 23 | [{:keys [transform] :as _config}] 24 | (TapPublisher. (rb/agent-buffer 10000) (or transform identity))) 25 | 26 | ;;; main 27 | 28 | (comment 29 | (def p (p/open)) 30 | (add-tap #'p/submit) 31 | 32 | (tap> (ns-publics *ns*)) 33 | 34 | (def pub! (mu/start-publisher! 35 | {:type :custom, :fqn-function "user/tap-publisher"})) 36 | 37 | (mu/log ::my-event ::ns (ns-publics *ns*)) 38 | ,) 39 | -------------------------------------------------------------------------------- /examples/nbb/.gitignore: -------------------------------------------------------------------------------- 1 | .nbb -------------------------------------------------------------------------------- /examples/nbb/README.md: -------------------------------------------------------------------------------- 1 | # nbb + portal 2 | 3 | To start using portal with [nbb][nbb], you just need to include the following in 4 | your `nbb.edn`: 5 | 6 | ```clojure 7 | ;; nbb.edn 8 | {:paths ["dev"] 9 | :deps {djblue/portal {:mvn/version "0.55.1"}}} 10 | ``` 11 | 12 | Then after connecting to a REPL, you should be able to follow the [API guide][api]. 13 | 14 | ## Auto Start 15 | 16 | Additionally, you can automatically start portal and nrepl with the following 17 | code: 18 | 19 | ```clojure 20 | ;; dev/start.cljs 21 | (ns start 22 | (:require [nbb.nrepl-server :as n] 23 | [nbb.repl :as r] 24 | [portal.api :as p])) 25 | 26 | (defn async-submit [value] 27 | (if-not (instance? js/Promise value) 28 | (p/submit value) 29 | (-> value 30 | (.then p/submit) 31 | (.catch p/submit)))) 32 | 33 | (defn -main [] 34 | (add-tap #'async-submit) 35 | (n/start-server! {:port 1337}) 36 | (p/open {:launcher :vs-code}) 37 | (r/repl)) 38 | ``` 39 | 40 | Which can then automatically be wired into your package.json: 41 | 42 | ```javascript 43 | // package.json 44 | { 45 | "scripts": { 46 | "start": "nbb -m start" 47 | }, 48 | "dependencies": { 49 | "nbb": "^1.2.187" 50 | } 51 | } 52 | ``` 53 | 54 | And then start everything via npm: 55 | 56 | ```bash 57 | npm start 58 | ``` 59 | 60 | ## Remote Client 61 | 62 | If you would like to send data to another remote Portal, you can use the 63 | `portal.client.node` client to leverage the [Remote API][remote]. 64 | 65 | [nbb]: https://github.com/babashka/nbb/ 66 | [api]: ../../README.md#api 67 | [remote]: ../../doc/remote-api.md#usage -------------------------------------------------------------------------------- /examples/nbb/dev/start.cljs: -------------------------------------------------------------------------------- 1 | (ns start 2 | (:require [nbb.nrepl-server :as n] 3 | [nbb.repl :as r] 4 | [portal.api :as p])) 5 | 6 | (defn async-submit [value] 7 | (if-not (instance? js/Promise value) 8 | (p/submit value) 9 | (-> value 10 | (.then p/submit) 11 | (.catch p/submit)))) 12 | 13 | (defn -main [] 14 | (add-tap #'async-submit) 15 | (n/start-server! {:port 1337}) 16 | (p/open {:launcher :vs-code}) 17 | (r/repl)) 18 | 19 | (comment 20 | (n/stop-server!) 21 | (require '[portal.console :as console]) 22 | (console/log :hi)) 23 | -------------------------------------------------------------------------------- /examples/nbb/nbb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["dev"] 2 | :deps {djblue/portal {:local/root "../../"}}} -------------------------------------------------------------------------------- /examples/nbb/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nbb", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "dependencies": { 9 | "nbb": "^1.2.187" 10 | } 11 | }, 12 | "node_modules/import-meta-resolve": { 13 | "version": "2.2.2", 14 | "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", 15 | "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", 16 | "funding": { 17 | "type": "github", 18 | "url": "https://github.com/sponsors/wooorm" 19 | } 20 | }, 21 | "node_modules/nbb": { 22 | "version": "1.2.187", 23 | "resolved": "https://registry.npmjs.org/nbb/-/nbb-1.2.187.tgz", 24 | "integrity": "sha512-pNJzlZ+wQtsOIvvOA1cp1o6rJzMk7wKTU2nAvNNx+xeKQgZEASv99r4jEB3wD+BYXtx1q7mvPI4O7WXMaIMWeg==", 25 | "dependencies": { 26 | "import-meta-resolve": "^2.1.0" 27 | }, 28 | "bin": { 29 | "nbb": "cli.js", 30 | "nbbun": "nbbun.js" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/nbb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "nbb -m start" 4 | }, 5 | "dependencies": { 6 | "nbb": "^1.2.187" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/node/.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache/ 2 | target/ 3 | -------------------------------------------------------------------------------- /examples/node/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps 3 | {org.clojure/clojurescript {:mvn/version "1.10.764"}} 4 | :aliases 5 | {:dev 6 | {:extra-paths ["dev"] 7 | :main-opts ["-m" "cljs.main" "-re" "node" "-d" "target" "-r"] 8 | :extra-deps 9 | {djblue/portal {:mvn/version "0.19.0"}}}}} 10 | -------------------------------------------------------------------------------- /examples/node/dev/user.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.user 2 | (:require [clojure.core.protocols :refer [Datafiable]] 3 | [clojure.datafy :refer [datafy nav]] 4 | [example.server :refer [handler]] 5 | [portal.api :as p] 6 | ["http" :as http])) 7 | 8 | (def portal (p/open)) 9 | 10 | (add-tap #'p/submit) 11 | 12 | (defn js->clj+ 13 | "For cases when built-in js->clj doesn't work. Source: https://stackoverflow.com/a/32583549/4839573" 14 | [x] 15 | (into {} (for [k (js-keys x)] [(keyword k) (aget x k)]))) 16 | 17 | (extend-protocol Datafiable 18 | http/IncomingMessage 19 | (datafy [this] (js->clj+ this))) 20 | 21 | (def server (atom nil)) 22 | 23 | (defn go [] 24 | (println "starting server on http://localhost:3000") 25 | (reset! server (http/createServer #(handler %1 %2))) 26 | (.listen @server 3000)) 27 | 28 | (go) 29 | -------------------------------------------------------------------------------- /examples/node/src/example/server.cljs: -------------------------------------------------------------------------------- 1 | (ns example.server) 2 | 3 | (defn handler [req res] 4 | (tap> req) 5 | (.end res "hello, world")) 6 | -------------------------------------------------------------------------------- /examples/parse-and-tap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[babashka.deps :as deps]) 4 | (require '[clojure.java.io :as io]) 5 | (require '[cheshire.core :as json]) 6 | 7 | (deps/add-deps '{:deps {djblue/portal {:mvn/version "0.19.0"}}}) 8 | 9 | (require '[portal.api :as p]) 10 | 11 | (p/open) 12 | 13 | (.addShutdownHook 14 | (Runtime/getRuntime) 15 | (Thread. (fn [] (p/close)))) 16 | 17 | (add-tap #'p/submit) 18 | 19 | (doseq [line (line-seq (io/reader *in*))] 20 | (tap> 21 | (try 22 | (json/parse-string line true) 23 | (catch Exception _ex 24 | line)))) 25 | -------------------------------------------------------------------------------- /examples/plotly-viewer/.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | node_modules 3 | -------------------------------------------------------------------------------- /examples/plotly-viewer/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["dev" "src" "resources"] 2 | :deps {djblue/portal {#_#_:mvn/version "0.37.0" 3 | :local/root "../../"}}} 4 | -------------------------------------------------------------------------------- /examples/plotly-viewer/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [portal.api :as p])) 3 | 4 | (defn open [] 5 | (p/open 6 | {:app false 7 | :value 8 | ^{:portal.viewer/default :portal.viewer/plotly} 9 | {:data 10 | [{:x [1 2 3] 11 | :y [2 6 3] 12 | :type "scatter" 13 | :mode "lines+markers" 14 | :marker {:color "red"}} 15 | {:type "bar" :x [1 2 3] :y [2 5 3]}]} 16 | :on-load 17 | (fn [] 18 | (p/eval-str "(require 'portal.ui.viewer.plotly)"))})) 19 | -------------------------------------------------------------------------------- /examples/plotly-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react-plotly.js": "^2.6.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/plotly-viewer/src/portal/ui/viewer/plotly.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.ui.viewer.plotly 2 | "Viewer for plotly 3 | For more information, see: 4 | https://github.com/plotly/plotly.js 5 | https://github.com/plotly/react-plotly.js" 6 | (:require 7 | ["react-plotly.js" :refer [default] :rename {default plotly}] 8 | #_[clojure.spec.alpha :as sp] 9 | [portal.ui.api :as p])) 10 | 11 | ;; sci in portal doesn't currently support spec 12 | ;; (sp/def ::name string?) 13 | ;; (sp/def ::plotly 14 | ;; (sp/keys :req-un [::data] 15 | ;; :opt-un [::name ::layout ::config ::style])) 16 | 17 | (defn plotly-viewer [value] 18 | ^{:key (hash value)} 19 | [:div {:style {:display :flex :justify-content :flex-end}} 20 | [:> plotly (merge {:style {:width 960 :height 540}} value)]]) 21 | 22 | (def viewer 23 | {#_#_:predicate (partial sp/valid? ::plotly) 24 | :predicate (constantly true) 25 | :component #'plotly-viewer 26 | :name :portal.viewer/plotly}) 27 | 28 | ;; (comment 29 | ;; (tap> {:data [{:x [0 2] :y [0 3]}] 30 | ;; :style {:width 960 :height 560}}) 31 | ;; (tap> {:data [{:values [0 2 3 4 5] 32 | ;; :type :pie}] 33 | ;; :style {:width "100%" :height "100%"}})) 34 | 35 | (p/register-viewer! viewer) 36 | -------------------------------------------------------------------------------- /examples/portal-present/.gitignore: -------------------------------------------------------------------------------- 1 | .clj-kondo/ 2 | .lsp/ 3 | .portal/ 4 | .cpcache/ -------------------------------------------------------------------------------- /examples/portal-present/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {djblue/portal {:mvn/version "0.34.2"}} 2 | :aliases 3 | {:repl 4 | {:extra-deps 5 | {cider/cider-nrepl {:mvn/version "0.28.5"}} 6 | :main-opts ["-m" "nrepl.cmdline" 7 | "--middleware" 8 | "[cider.nrepl/cider-middleware,portal.nrepl/wrap-repl]"]}}} -------------------------------------------------------------------------------- /examples/portal-present/src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.java.io :as io] 3 | [portal.api :as p] 4 | [portal.viewer :as-alias v])) 5 | 6 | (defn with-viewer [viewer] 7 | (with-meta 8 | [:<> 9 | [:h1 [::v/inspector viewer]] 10 | [:div 11 | {:style 12 | {:padding 40}} 13 | [viewer [:h1 "hello, world"]]]] 14 | {::v/default ::v/hiccup})) 15 | 16 | (def slides (atom nil)) 17 | 18 | (reset! 19 | slides 20 | (with-meta 21 | [(with-viewer ::v/inspector) 22 | (with-viewer ::v/hiccup) 23 | (with-viewer ::v/tree) 24 | (with-viewer ::v/pprint) 25 | (with-viewer ::v/pr-str)] 26 | {::v/default :portal-present.viewer/slides})) 27 | 28 | (declare presentation) 29 | 30 | (defn on-load [] 31 | (p/eval-str presentation 32 | (slurp "examples/portal-present/src/portal_present/viewer.cljs") 33 | #_(slurp (io/resource "portal_present/viewer.cljs")))) 34 | 35 | (def presentation 36 | (p/open {:mode :dev 37 | :value slides 38 | :on-load on-load 39 | :launcher :vs-code 40 | :window-title "Portal Present"})) 41 | 42 | (comment 43 | ;; for debugging 44 | (p/open {:mode :dev :launcher :vs-code}) 45 | (swap! slides empty) 46 | (add-tap p/submit) 47 | ;; start nrepl subrepl 48 | (p/repl presentation) 49 | (-> :cljs/quit)) 50 | -------------------------------------------------------------------------------- /examples/remote/client: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[babashka.deps :as deps]) 4 | (require '[clojure.main :as main]) 5 | 6 | (deps/add-deps '{:deps {djblue/portal {:mvn/version "0.43.0"}}}) 7 | 8 | (require '[portal.client.jvm :as p]) 9 | 10 | (def port 5678) 11 | 12 | (def submit (partial p/submit {:port port})) 13 | 14 | (add-tap #'submit) 15 | 16 | (comment 17 | (add-tap (partial p/submit {:port port :encoding :json})) 18 | (add-tap (partial p/submit {:port port :encoding :transit}))) 19 | 20 | (main/repl) 21 | -------------------------------------------------------------------------------- /examples/remote/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[babashka.deps :as deps]) 4 | (require '[clojure.main :as main]) 5 | 6 | (deps/add-deps '{:deps {djblue/portal {:mvn/version "0.43.0"}}}) 7 | 8 | (require '[portal.api :as p]) 9 | 10 | (def port 5678) 11 | 12 | (println "Server running on port:" port) 13 | (p/open {:port port}) 14 | 15 | (.addShutdownHook 16 | (Runtime/getRuntime) 17 | (Thread. (fn [] (p/close)))) 18 | 19 | (add-tap #'p/submit) 20 | 21 | (main/repl) 22 | -------------------------------------------------------------------------------- /examples/timbre/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {djblue/portal {:mvn/version "0.24.0"} 3 | com.taoensso/timbre {:mvn/version "5.2.1"}}} 4 | -------------------------------------------------------------------------------- /examples/timbre/src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.datafy :as d] 3 | [clojure.instant :as i] 4 | [portal.api :as p] 5 | [taoensso.timbre :as log])) 6 | 7 | (defn log->portal [{:keys [level ?err msg_ timestamp_ ?ns-str ?file context ?line]}] 8 | (merge 9 | (when ?err 10 | {:error (d/datafy ?err)}) 11 | (when-let [ts (force timestamp_)] 12 | {:time (i/read-instant-date ts)}) 13 | {:level level 14 | :ns (symbol (or ?ns-str ?file "?")) 15 | :line (or ?line 1) 16 | :column 1 17 | :result (force msg_) 18 | :runtime :clj} 19 | context)) 20 | 21 | (defonce logs (atom '())) 22 | 23 | (defn log 24 | "Accumulate a rolling log of 100 entries." 25 | [log] 26 | (swap! logs 27 | (fn [logs] 28 | (take 100 (conj logs (log->portal log)))))) 29 | 30 | (defn setup [] 31 | (reset! logs '()) 32 | (log/merge-config! 33 | {:appenders 34 | {:memory {:enabled? true :fn log}}})) 35 | 36 | (defn open [] 37 | (p/open {:window-title "Logs Viewer" :value logs})) 38 | 39 | (comment 40 | (log/info "my info log") 41 | (log/error (ex-info "my exception" {:hello :world}) "my error log") 42 | 43 | (log/with-context+ 44 | {:runtime :cljs} 45 | (log/info "my cljs log"))) 46 | -------------------------------------------------------------------------------- /examples/tufte/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {djblue/portal {:local/root "../../"} 3 | com.taoensso/tufte {:mvn/version "2.5.1"}}} 4 | -------------------------------------------------------------------------------- /examples/tufte/src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Fork of https://github.com/ptaoussanis/tufte#10-second-example for tap> and Portal" 3 | (:require [portal.api :as p] 4 | [taoensso.tufte :as tufte :refer (p profiled profile)])) 5 | 6 | (def columns 7 | (-> [:min :p25 :p50 :p75 :p90 :p95 :p99 :max :mean :mad :sum] 8 | (zipmap (repeat :portal.viewer/duration-ns)) 9 | (assoc :loc :portal.viewer/source-location))) 10 | 11 | (defn format-data [stats] 12 | (-> stats 13 | (update-in [:loc :ns] symbol) 14 | (vary-meta update :portal.viewer/for merge columns))) 15 | 16 | (defn format-pstats [pstats] 17 | (-> @pstats 18 | (:stats) 19 | (update-vals format-data) 20 | (with-meta 21 | {:portal.viewer/default :portal.viewer/table 22 | :portal.viewer/table 23 | {:columns [:n :min #_:p25 #_:p50 #_:p75 #_:p90 #_:p95 #_:p99 :max :mean #_:mad :sum :loc]}}))) 24 | 25 | (defn add-tap-handler! 26 | "Adds a simple handler that logs `profile` stats output with `tap>`." 27 | [{:keys [ns-pattern handler-id] 28 | :or {ns-pattern "*" 29 | handler-id :basic-tap}}] 30 | (tufte/add-handler! 31 | handler-id ns-pattern 32 | (fn [{:keys [?id ?data pstats]}] 33 | (tap> (vary-meta 34 | (format-pstats pstats) 35 | merge 36 | (cond-> {} 37 | ?id (assoc :id ?id) 38 | ?data (assoc :data ?data))))))) 39 | 40 | ;;; Let's define a couple dummy fns to simulate doing some expensive work 41 | (defn get-x [] (Thread/sleep 500) "x val") 42 | (defn get-y [] (Thread/sleep (rand-int 1000)) "y val") 43 | 44 | (defn do-work [] 45 | (dotimes [_ 5] 46 | (p :get-x (get-x)) 47 | (p :get-y (get-y)))) 48 | 49 | (defn run [] 50 | ;; CLI usage 51 | (println "Running profile...") 52 | (-> (profiled {} (do-work)) second format-pstats p/inspect)) 53 | 54 | (comment 55 | ;; REPL usage 56 | (p/open) 57 | (add-tap p/submit) 58 | (add-tap-handler! {}) 59 | (profile {} (do-work)) 60 | (remove-tap p/submit)) 61 | -------------------------------------------------------------------------------- /examples/web-figwheel-main/.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache/ 2 | target/ 3 | 4 | -------------------------------------------------------------------------------- /examples/web-figwheel-main/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps 3 | {org.clojure/clojurescript {:mvn/version "1.10.764"}} 4 | :aliases 5 | {:dev 6 | {:extra-paths ["dev"] 7 | :main-opts ["-m" "figwheel.main" "-b" "dev" "-r"] 8 | :extra-deps 9 | {com.bhauman/figwheel-main {:mvn/version "0.2.11"} 10 | djblue/portal {:mvn/version "0.19.0"}}}}} 11 | -------------------------------------------------------------------------------- /examples/web-figwheel-main/dev.cljs.edn: -------------------------------------------------------------------------------- 1 | {:output-to "resources/public/cljs-out/dev-main.js" 2 | :optimizations :none 3 | :pretty-print true 4 | :source-map true 5 | :source-map-timestamp true 6 | :main example.app} 7 | -------------------------------------------------------------------------------- /examples/web-figwheel-main/dev/user.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.user 2 | (:require [clojure.core.protocols :refer [Datafiable]] 3 | [example.app :refer [start]] 4 | [portal.web :as p])) 5 | 6 | (def portal (p/open)) 7 | 8 | (p/tap) 9 | 10 | (defn js->clj+ 11 | "For cases when built-in js->clj doesn't work. Source: https://stackoverflow.com/a/32583549/4839573" 12 | [x] 13 | (into {} (for [k (js-keys x)] [(keyword k) (aget x k)]))) 14 | 15 | (extend-protocol Datafiable 16 | js/KeyboardEvent 17 | (datafy [this] (js->clj+ this))) 18 | 19 | (defn go [] (start)) 20 | 21 | (go) 22 | -------------------------------------------------------------------------------- /examples/web-figwheel-main/figwheel-main.edn: -------------------------------------------------------------------------------- 1 | { 2 | :target-dir "resources" 3 | :watch-dirs ["dev" "src"] 4 | ;:css-dirs ["resources/public/css"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/web-figwheel-main/readme.md: -------------------------------------------------------------------------------- 1 | Launch the demo with 2 | 3 | clj -Adev 4 | 5 | Once launched eval the lines below which are located in dev/user.clj to open the portal window, I am not sure if you can make figwheel eval the contents of user.cljs on launch. 6 | 7 | (def portal (p/open)) 8 | (p/tap) 9 | 10 | -------------------------------------------------------------------------------- /examples/web-figwheel-main/src/example/app.cljs: -------------------------------------------------------------------------------- 1 | (ns example.app) 2 | 3 | (defn start [] 4 | (js/window.addEventListener "keydown" #(tap> %))) 5 | -------------------------------------------------------------------------------- /examples/web-iframe/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {thheller/shadow-cljs {:mvn/version "2.20.10"}} 2 | 3 | :paths ["src/main"] 4 | 5 | :aliases 6 | {:cljs 7 | {:extra-deps 8 | {org.clojure/clojurescript {:mvn/version "1.11.60"} 9 | reagent/reagent {:mvn/version "1.2.0"} 10 | djblue/portal {:local/root "../../"}}}}} 11 | -------------------------------------------------------------------------------- /examples/web-iframe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.2", 4 | "react-dom": "^17.0.2" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/web-iframe/resources/index.html: -------------------------------------------------------------------------------- 1 | portal iframe demo
2 | -------------------------------------------------------------------------------- /examples/web-iframe/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps {:aliases [:cljs]} 2 | 3 | :dev-http {4425 ["resources/"]} 4 | 5 | :nrepl {:port 4450} 6 | 7 | :builds 8 | {:demo 9 | {:target :browser 10 | :output-dir "resources/js" 11 | 12 | :modules 13 | {:main {:init-fn demo/init}} 14 | 15 | :dev 16 | {:compiler-options {:source-map true}}}}} 17 | -------------------------------------------------------------------------------- /examples/web-iframe/src/main/demo.cljs: -------------------------------------------------------------------------------- 1 | (ns demo 2 | (:require 3 | [reagent.dom :as rdom] 4 | [portal.web :as pw])) 5 | 6 | (defn- app [] 7 | [:div {:style {:display :flex, :height "100vh", :flex-direction "column"}} 8 | [:div {:style {:display :flex, :justify-content :center}} 9 | [:button {:on-click #(pw/submit {:hello :world, :n (rand-int 10)}) 10 | :style {:padding 8, :margin 8, :width "100%"}} 11 | "Test"]] 12 | [:div {:style {:flex-grow 1} 13 | :ref (fn [el] (when el (pw/open {:iframe-parent el})))}]]) 14 | 15 | (defn ^:dev/after-load init [] 16 | (rdom/render [app] (js/document.getElementById "root"))) 17 | -------------------------------------------------------------------------------- /examples/web/.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache/ 2 | target/ 3 | 4 | -------------------------------------------------------------------------------- /examples/web/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps 3 | {org.clojure/clojurescript {:mvn/version "1.10.764"}} 4 | :aliases 5 | {:dev 6 | {:extra-paths ["dev"] 7 | :main-opts ["-m" "cljs.main" "-d" "target" "-r"] 8 | :extra-deps 9 | {djblue/portal {:mvn/version "0.19.0"}}}}} 10 | -------------------------------------------------------------------------------- /examples/web/dev/user.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.user 2 | (:require [clojure.core.protocols :refer [Datafiable]] 3 | [example.app :refer [start]] 4 | [portal.web :as p])) 5 | 6 | (def portal (p/open)) 7 | 8 | (add-tap #'p/submit) 9 | 10 | (defn js->clj+ 11 | "For cases when built-in js->clj doesn't work. Source: https://stackoverflow.com/a/32583549/4839573" 12 | [x] 13 | (into {} (for [k (js-keys x)] [(keyword k) (aget x k)]))) 14 | 15 | (extend-protocol Datafiable 16 | js/KeyboardEvent 17 | (datafy [this] (js->clj+ this))) 18 | 19 | (defn go [] (start)) 20 | 21 | (go) 22 | -------------------------------------------------------------------------------- /examples/web/src/example/app.cljs: -------------------------------------------------------------------------------- 1 | (ns example.app) 2 | 3 | (defn start [] 4 | (js/window.addEventListener "keydown" #(tap> %))) 5 | -------------------------------------------------------------------------------- /extension-electron/.gitignore: -------------------------------------------------------------------------------- 1 | electron* 2 | node_modules/ 3 | out/ 4 | -------------------------------------------------------------------------------- /extension-electron/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djblue/portal/82e070d4539f2799717c9110e634f1d18275b631/extension-electron/icon.png -------------------------------------------------------------------------------- /extension-electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portal", 3 | "version": "0.23.0", 4 | "license": "MIT", 5 | "devDependencies": { 6 | "@electron-forge/cli": "^6.0.0-beta.63", 7 | "@electron-forge/maker-deb": "^6.0.0-beta.63", 8 | "@electron-forge/maker-rpm": "^6.0.0-beta.63", 9 | "@electron-forge/maker-squirrel": "^6.0.0-beta.63", 10 | "@electron-forge/maker-zip": "^6.0.0-beta.63", 11 | "electron": "^17.1.0", 12 | "electron-icon-builder": "^2.0.1" 13 | }, 14 | "main": "./electron.js", 15 | "scripts": { 16 | "start": "electron-forge start --app-path ./electron-dev.js", 17 | "prepackage": "electron-icon-builder --input=icon.png --output=./out", 18 | "make": "electron-forge make", 19 | "package": "electron-forge package" 20 | }, 21 | "dependencies": { 22 | "electron-squirrel-startup": "^1.0.0" 23 | }, 24 | "config": { 25 | "forge": { 26 | "packagerConfig": { 27 | "icon": "./out/icons/mac/icon.icns", 28 | "ignore": ["electron-dev.js", "icon.png"] 29 | }, 30 | "makers": [ 31 | { 32 | "name": "@electron-forge/maker-squirrel", 33 | "config": { 34 | "name": "portal_electron" 35 | } 36 | }, 37 | { 38 | "name": "@electron-forge/maker-zip", 39 | "platforms": [ 40 | "darwin" 41 | ] 42 | }, 43 | { 44 | "name": "@electron-forge/maker-deb", 45 | "config": {} 46 | }, 47 | { 48 | "name": "@electron-forge/maker-rpm", 49 | "config": {} 50 | } 51 | ] 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /extension-intellij/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /extension-intellij/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # portal-intellij Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.59.0] 6 | - Bumped to support Intellij 2025.1 7 | 8 | ## [0.58.3] 9 | - Bumped to support Intellij 2024.3 10 | 11 | ## [0.57.2] 12 | - Bumped to support Intellij 2024.2 and added background fill for plugin icon 13 | 14 | ## [0.54.2] 15 | - Bumped to support Intellij 2024.1 16 | 17 | ## [0.50.1] 18 | - Bumped to support Intellij 2023.3 19 | 20 | ## [0.44.1] 21 | - Bumped to support Intellij 2023.2 22 | 23 | ## [0.38.0] 24 | - Bumped to support Intellij 2023.1 25 | 26 | ## [0.34.3] 27 | - Bumped to support Intellij 2022.3 28 | - Upgrades Gradle IntelliJ Plugin -> 1.10.0 29 | - Upgrades Gradle -> 7.6 30 | 31 | ## 0.29.0 32 | - Bumped to support Intellij 2022.2 33 | -------------------------------------------------------------------------------- /extension-intellij/README.md: -------------------------------------------------------------------------------- 1 | # Portal Inspector Plugin 2 | 3 | 4 | 5 | A clojure tool to navigate through your data. 6 | 7 | This plugin acts as a companion to the main [Portal](https://github.com/djblue/portal) project. 8 | It enables you to host the web based Portal UI inside of Intellij. 9 | 10 | The main benefits of using this plugin are: 11 | * Automatic font / theme discovery 12 | * No window management 13 | * Editor specific commands. See: goto-definition in the command palette 14 | 15 | Portal itself allows you to pipe data from a clojure repl via [tap>](https://clojuredocs.org/clojure.core/tap%3E) into the various data visualization tools within the Portal UI. 16 | 17 | For a more in-depth look into how to use portal, take a look at the project [README](https://github.com/djblue/portal#api) 18 | 19 | 20 | -------------------------------------------------------------------------------- /extension-intellij/gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = djblue 5 | pluginName = portal 6 | pluginVersion = 0.59.1 7 | 8 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 9 | # for insight into build numbers and IntelliJ Platform versions. 10 | pluginSinceBuild = 231.* 11 | pluginUntilBuild = 251.* 12 | 13 | # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl 14 | # See https://jb.gg/intellij-platform-builds-list for available build versions. 15 | pluginVerifierIdeVersions = 2023.1, 2023.2, 2023.3, 2024.1, 2024.2, 2024.3, 2025.1 16 | 17 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 18 | platformType = IC 19 | platformVersion = 2021.3 20 | 21 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 22 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 23 | platformPlugins = com.cursiveclojure.cursive:1.12.2-2021.3, com.intellij.java 24 | 25 | # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 26 | javaVersion = 11 27 | 28 | # Gradle Releases -> https://github.com/gradle/gradle/releases 29 | gradleVersion = 7.5 30 | 31 | # Opt-out flag for bundling Kotlin standard library. 32 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 33 | # suppress inspection "UnusedProperty" 34 | kotlin.stdlib.default.dependency = false 35 | -------------------------------------------------------------------------------- /extension-intellij/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djblue/portal/82e070d4539f2799717c9110e634f1d18275b631/extension-intellij/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /extension-intellij/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /extension-intellij/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "portal-intellij" 2 | -------------------------------------------------------------------------------- /extension-intellij/src/main/clojure/portal/extensions/intellij/file.clj: -------------------------------------------------------------------------------- 1 | (ns portal.extensions.intellij.file 2 | (:import 3 | (com.intellij.openapi.application ApplicationManager) 4 | (com.intellij.openapi.fileEditor OpenFileDescriptor) 5 | (com.intellij.openapi.project Project) 6 | (com.intellij.openapi.vfs VirtualFileManager))) 7 | 8 | (defn- ->descriptor ^OpenFileDescriptor 9 | [^Project project {:keys [file line column]}] 10 | (OpenFileDescriptor. 11 | project 12 | (.findFileByUrl 13 | (VirtualFileManager/getInstance) 14 | (str "file://" file)) 15 | (if line (dec line) 0) 16 | (if column (dec column) 0))) 17 | 18 | (defn open [^Project project info] 19 | (.invokeLater 20 | (ApplicationManager/getApplication) 21 | (fn [] 22 | (.runReadAction 23 | (ApplicationManager/getApplication) 24 | ^Runnable 25 | (fn [] 26 | (.navigate (->descriptor project info) true)))))) 27 | -------------------------------------------------------------------------------- /extension-intellij/src/main/java/icons/PortalIcons.java: -------------------------------------------------------------------------------- 1 | package icons; 2 | 3 | import com.intellij.openapi.util.IconLoader; 4 | import javax.swing.Icon; 5 | 6 | public interface PortalIcons { 7 | Icon ToolWindow = IconLoader.getIcon("/portal/icon.svg", PortalIcons.class); 8 | } 9 | -------------------------------------------------------------------------------- /extension-intellij/src/main/java/portal/extensions/intellij/WithLoader.java: -------------------------------------------------------------------------------- 1 | package portal.extensions.intellij; 2 | 3 | public class WithLoader { 4 | static { 5 | bind(); 6 | } 7 | public static void bind() { 8 | Thread.currentThread().setContextClassLoader(WithLoader.class.getClassLoader()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /extension-intellij/src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | djblue.portal 3 | Portal Inspector 4 | djblue 5 | 6 | com.intellij.modules.platform 7 | 8 | 9 | 13 | 14 | 15 | 16 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /extension-vscode/.gitignore: -------------------------------------------------------------------------------- 1 | .clj-kondo/.cache/ 2 | README.md 3 | LICENSE 4 | vs-code.js 5 | vs-code.js.map 6 | notebook/ 7 | *.vsix 8 | -------------------------------------------------------------------------------- /extension-vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Portal", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /extension-vscode/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeBackground": "#ff5d00", 4 | "titleBar.activeForeground": "#272c36", 5 | "titleBar.inactiveBackground": "#ff9a00", 6 | "titleBar.inactiveForeground": "#272c36" 7 | } 8 | } -------------------------------------------------------------------------------- /extension-vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .gitignore 3 | -------------------------------------------------------------------------------- /extension-vscode/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djblue/portal/82e070d4539f2799717c9110e634f1d18275b631/extension-vscode/icon.png -------------------------------------------------------------------------------- /nbb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources" "test"]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@djblue/portal", 3 | "version": "0.59.1", 4 | "repository": "github:djblue/portal", 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 7 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 8 | "@fortawesome/react-fontawesome": "^0.1.16", 9 | "anser": "^2.1.1", 10 | "diff": "^5.2.0", 11 | "highlight.js": "^11.3.1", 12 | "marked": "^4.1.0", 13 | "papaparse": "^5.3.1", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "vega": "^5.23.0", 17 | "vega-embed": "^6.19.1", 18 | "vega-lite": "^5.1.1" 19 | }, 20 | "devDependencies": { 21 | "browserify": "^16.5.2", 22 | "nbb": "^1.2.187", 23 | "source-map-support": "^0.5.20", 24 | "ws": "^8.17.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djblue/portal/82e070d4539f2799717c9110e634f1d18275b631/resources/screenshot.png -------------------------------------------------------------------------------- /resources/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', function(event) { 2 | if ((event.request.url.indexOf('http') === 0)) { 3 | console.log('[Service Worker] Fetched resource ' + event.request.url) 4 | event.respondWith( 5 | caches.open('portal-cache').then(function (cache) { 6 | return fetch(event.request).then(function(response) { 7 | cache.put(event.request, response.clone()) 8 | return response 9 | }).catch(function() { 10 | return caches.match(event.request) 11 | }) 12 | }) 13 | ) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/examples/default_visualizer.clj: -------------------------------------------------------------------------------- 1 | (ns examples.default-visualizer 2 | (:require 3 | [clojure.core.protocols :refer [nav]] 4 | [clojure.string :as str])) 5 | 6 | (defn nav-dep-anno-tree [coll _ v] 7 | (let [{:keys [deps-map]} (meta coll)] 8 | (with-meta 9 | [:div "Depends on " 10 | (str/join ", " (map str (get deps-map v)))] 11 | {:portal.viewer/default :portal.viewer/hiccup}))) 12 | 13 | (comment 14 | (tap> 15 | (with-meta 16 | {:a 1 17 | :b 2 18 | :c 3} 19 | {`nav #'nav-dep-anno-tree 20 | :deps-map {:c #{:b :a}}}))) -------------------------------------------------------------------------------- /src/examples/fetch.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.fetch) 2 | 3 | (defn- node-fetch [url] 4 | (let [http (js/require "https")] 5 | (js/Promise. 6 | (fn [resolve reject] 7 | (.end 8 | (.request 9 | http 10 | url 11 | (fn [res] 12 | (let [body (atom "")] 13 | (.on res "data" #(swap! body str %)) 14 | (.on res "error" reject) 15 | (.on res "end" #(resolve @body)))))))))) 16 | 17 | (defn- web-fetch [url] 18 | (.then (js/fetch url) #(.text %))) 19 | 20 | (def fetch (if (exists? js/window) web-fetch node-fetch)) 21 | -------------------------------------------------------------------------------- /src/examples/macros.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc examples.macros 2 | #?(:portal (:import) :cljs (:require-macros examples.macros))) 3 | 4 | #?(:clj (defmacro read-file [file-name] (slurp file-name)) 5 | :cljs (defn read-file [_file-name] ::missing) 6 | :cljr (defmacro read-file [file-name] (slurp file-name :enc "utf8"))) 7 | -------------------------------------------------------------------------------- /src/portal/async.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.async 2 | (:refer-clojure :exclude [let try promise]) 3 | #?(:portal (:import) :cljs (:require-macros portal.async))) 4 | 5 | (defmacro do [& body] 6 | (reduce 7 | (fn [chain form] 8 | `(.then ~chain 9 | (fn [] (.resolve js/Promise ~form)))) 10 | `(.resolve js/Promise nil) 11 | body)) 12 | 13 | (defmacro let [bindings & body] 14 | (->> (partition-all 2 bindings) 15 | reverse 16 | (reduce (fn [body [n v]] 17 | `(.then (.resolve js/Promise ~v) 18 | (fn [~n] ~body))) 19 | `(portal.async/do ~@body)))) 20 | 21 | (defmacro try [& body] 22 | (reduce 23 | (fn [chain form] 24 | (cond 25 | (and (coll? form) (= 'finally (first form))) 26 | `(.finally ~chain 27 | (fn [] 28 | (portal.async/do ~@(rest form)))) 29 | 30 | (and (coll? form) (= 'catch (first form))) 31 | (clojure.core/let [[_ _type ex-binding & body] form] 32 | `(.catch ~chain 33 | (fn [~ex-binding] 34 | (portal.async/do ~@body)))) 35 | 36 | :else 37 | `(.then ~chain 38 | (fn [] 39 | (.resolve js/Promise ~form))))) 40 | `(.resolve js/Promise nil) 41 | body)) 42 | 43 | #?(:cljs (defn race [& args] (.race js/Promise args))) -------------------------------------------------------------------------------- /src/portal/client/clr.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc ^:no-check portal.client.clr 2 | (:require [portal.runtime] 3 | [portal.runtime.cson :as cson]) 4 | (:import (System.Net.Http 5 | HttpClient 6 | HttpMethod 7 | HttpRequestMessage 8 | StringContent) 9 | (System.Text Encoding))) 10 | 11 | (defn- serialize [encoding value] 12 | (try 13 | (case encoding 14 | :edn (binding [*print-meta* true] 15 | (pr-str value)) 16 | :cson (cson/write value)) 17 | (catch Exception ex 18 | (serialize encoding (Throwable->map ex))))) 19 | 20 | (def client (HttpClient.)) 21 | 22 | (defn submit 23 | {:added "0.36.0" :see-also ["portal.api/submit"]} 24 | ([value] (submit nil value)) 25 | ([{:keys [encoding port host] 26 | :or {encoding :edn 27 | host "localhost" 28 | port 53755}} 29 | value] 30 | (let [request (HttpRequestMessage. 31 | HttpMethod/Post 32 | (str "http://" host ":" port "/submit"))] 33 | (set! (.-Content request) 34 | (StringContent. 35 | (serialize encoding value) 36 | Encoding/UTF8 37 | (case encoding 38 | :edn "application/edn" 39 | :cson "application/cson"))) 40 | (.EnsureSuccessStatusCode (.Send ^HttpClient client request))))) 41 | -------------------------------------------------------------------------------- /src/portal/client/common.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.client.common 2 | (:require [portal.runtime] 3 | [portal.runtime.cson :as cson] 4 | [portal.runtime.json :as json] 5 | [portal.runtime.transit :as transit])) 6 | 7 | (defn- error->data [ex] 8 | (merge 9 | (when-let [data (ex-data ex)] 10 | {:data data}) 11 | {:runtime :cljs 12 | :cause (ex-message ex) 13 | :via [{:type (symbol (.-name (type ex))) 14 | :message (ex-message ex)}] 15 | :stack (.-stack ex)})) 16 | 17 | (defn- serialize [encoding value] 18 | (try 19 | (case encoding 20 | :json (json/write value) 21 | :transit (transit/write value) 22 | :cson (cson/write value) 23 | :edn (binding [*print-meta* true] 24 | (pr-str value))) 25 | (catch :default ex 26 | (serialize encoding (error->data ex))))) 27 | 28 | (defn ->submit [fetch] 29 | (fn submit 30 | ([value] (submit nil value)) 31 | ([{:keys [encoding port host] 32 | :or {encoding :edn 33 | host "localhost" 34 | port 53755}} 35 | value] 36 | (fetch 37 | (str "http://" host ":" port "/submit") 38 | {:method "POST" 39 | :headers 40 | {"content-type" 41 | (case encoding 42 | :json "application/json" 43 | :cson "application/cson" 44 | :transit "application/transit+json" 45 | :edn "application/edn")} 46 | :body (serialize encoding value)})))) 47 | -------------------------------------------------------------------------------- /src/portal/client/dart.cljd: -------------------------------------------------------------------------------- 1 | (ns portal.client.dart 2 | (:require ["dart:io" :as io])) 3 | 4 | (defn submit 5 | {:added "0.47.0" :see-also ["portal.api/submit"]} 6 | ([value] (submit nil value)) 7 | ([{:keys [encoding port host] 8 | :or {encoding :edn 9 | host "localhost" 10 | port 7891}} 11 | value] 12 | (let [client (io/HttpClient.) 13 | request (await (.post client host port "/submit"))] 14 | (.add (.-headers request) 15 | "content-type" 16 | (case encoding 17 | :edn "application/edn")) 18 | (.write request 19 | (case encoding 20 | :edn (binding [*print-meta* true] 21 | (pr-str value)))) 22 | (.close request)))) -------------------------------------------------------------------------------- /src/portal/client/jvm.clj: -------------------------------------------------------------------------------- 1 | (ns portal.client.jvm 2 | (:require [org.httpkit.client :as http] 3 | [portal.runtime] 4 | [portal.runtime.cson :as cson] 5 | [portal.runtime.json :as json] 6 | [portal.runtime.transit :as transit])) 7 | 8 | (defn- serialize [encoding value] 9 | (try 10 | (case encoding 11 | :json (json/write value) 12 | :transit (transit/write value) 13 | :cson (cson/write value) 14 | :edn (binding [*print-meta* true] 15 | (pr-str value))) 16 | (catch Exception ex 17 | (serialize encoding (Throwable->map ex))))) 18 | 19 | (defn submit 20 | "Tap target function. 21 | 22 | Will submit values to a remote portal. Tapped value must be serialiable with 23 | the encoding method provided. 24 | 25 | Usage: 26 | 27 | ```clojure 28 | (def submit (partial p/submit {:port 5678})) ;; :encoding :edn is the default 29 | ;; (def submit (partial p/submit {:port 5678 :encoding :json})) 30 | ;; (def submit (partial p/submit {:port 5678 :encoding :transit})) 31 | 32 | (add-tap #'submit) 33 | (remove-tap #'submit) 34 | ``` 35 | " 36 | {:added "0.18.0" :see-also ["portal.api/submit"]} 37 | ([value] (submit nil value)) 38 | ([{:keys [encoding port host] 39 | :or {encoding :edn 40 | host "localhost" 41 | port 53755}} 42 | value] 43 | @(http/post 44 | (str "http://" host ":" port "/submit") 45 | {:headers 46 | {"content-type" 47 | (case encoding 48 | :json "application/json" 49 | :cson "application/cson" 50 | :transit "application/transit+json" 51 | :edn "application/edn")} 52 | :body (serialize encoding value)}))) 53 | 54 | (comment 55 | (submit {:runtime :jvm :value "hello jvm"}) 56 | (submit {:port 1664} {:runtime :jvm :value "hello jvm"}) 57 | 58 | (add-tap submit) 59 | (tap> {:runtime :jvm :value "hello jvm"}) 60 | 61 | (add-tap (partial submit {:encoding :json})) 62 | (add-tap (partial submit {:encoding :transit}))) 63 | -------------------------------------------------------------------------------- /src/portal/client/node.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.client.node 2 | (:require 3 | [clojure.string :as str] 4 | [portal.client.common :refer (->submit)])) 5 | 6 | (defn fetch [url options] 7 | (let [https? (str/starts-with? url "https") 8 | http (js/require (str "http" (when https? "s")))] 9 | (js/Promise. 10 | (fn [resolve reject] 11 | (let [req (.request 12 | http 13 | url 14 | (clj->js options) 15 | (fn [^js res] 16 | (let [body (atom "")] 17 | (.on res "data" #(swap! body str %)) 18 | (.on res "error" reject) 19 | (.on res "end" #(resolve 20 | {:status (.-statusCode res) 21 | :body @body})))))] 22 | (.write req (:body options)) 23 | (.end req)))))) 24 | 25 | (def submit (->submit fetch)) 26 | 27 | (comment 28 | (submit {:runtime :node :value "hello node"}) 29 | (add-tap submit) 30 | (tap> {:runtime :node :value "hello node-tap"}) 31 | (add-tap submit)) 32 | -------------------------------------------------------------------------------- /src/portal/client/planck.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.client.planck 2 | (:require [planck.http :as http] 3 | [portal.client.common :refer (->submit)])) 4 | 5 | (def ^:private http-methods 6 | {"GET" http/get 7 | "HEAD" http/head 8 | "PATCH" http/patch 9 | "POST" http/post 10 | "PUT" http/put}) 11 | 12 | (defn- fetch [url options] 13 | (let [f (get http-methods (:method options))] 14 | (f url options))) 15 | 16 | (def ^{:see-also ["portal.api/submit"]} 17 | submit (->submit fetch)) 18 | 19 | (comment 20 | (require '[portal.client.planck :as c] :reload) 21 | (c/submit {:port 7777} ::hello) 22 | (add-tap (partial c/submit {:port 7777})) 23 | (tap> ::hello)) 24 | -------------------------------------------------------------------------------- /src/portal/client/py.lpy: -------------------------------------------------------------------------------- 1 | (ns portal.client.py 2 | (:require [basilisp.json :as json]) 3 | (:import [urllib.request :as request])) 4 | 5 | (defn- serialize [encoding value] 6 | (.encode 7 | (try 8 | (case encoding 9 | :json (json/write-str value) 10 | :edn (binding [*print-meta* true] 11 | (pr-str value))) 12 | (catch Exception ex 13 | (serialize encoding (pr-str ex)))) 14 | "utf-8")) 15 | 16 | (defn submit 17 | ([value] (submit nil value)) 18 | ([{:keys [encoding port host] 19 | :or {encoding :edn 20 | host "localhost" 21 | port 53755}} 22 | value] 23 | (let [req (request/Request 24 | (str "http://" host ":" port "/submit") 25 | ** :data (serialize encoding value))] 26 | (.add_header 27 | req "content-type" 28 | (case encoding 29 | :json "application/json" 30 | :cson "application/cson" 31 | :transit "application/transit+json" 32 | :edn "application/edn")) 33 | (request/urlopen req)))) -------------------------------------------------------------------------------- /src/portal/client/web.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.client.web 2 | (:require 3 | [portal.client.common :refer (->submit)])) 4 | 5 | (defn- fetch [url options] 6 | (js/fetch url (clj->js options))) 7 | 8 | (def submit (->submit fetch)) 9 | 10 | (comment 11 | (submit "Hello World") 12 | (add-tap submit) 13 | (tap> {:runtime :cljs :value "hello web"}) 14 | (add-tap submit) 15 | 16 | (add-tap (partial submit {:encoding :json})) 17 | (add-tap (partial submit {:encoding :transit}))) 18 | -------------------------------------------------------------------------------- /src/portal/console.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.console 2 | #?(:joyride (:import) 3 | :clj (:require [clojure.java.io :as io]) 4 | :portal (:import) 5 | :cljs (:require-macros portal.console))) 6 | 7 | (defn ^:no-doc now [] 8 | #?(:clj (java.util.Date.) :cljs (js/Date.) :cljr (DateTime/Now))) 9 | 10 | (defn ^:no-doc run [f] 11 | (try 12 | [nil (f)] 13 | (catch #?(:clj Exception :cljs :default :cljr Exception) ex 14 | [:throw ex]))) 15 | 16 | (defn ^:no-doc runtime [] 17 | #?(:portal :portal 18 | :org.babashka/nbb :nbb 19 | :joyride :joyride 20 | :bb :bb 21 | :clj :clj 22 | :cljs :cljs 23 | :cljr :cljr)) 24 | 25 | #?(:clj 26 | (defn ^:no-doc get-file [env file] 27 | (if (:ns env) ;; cljs target 28 | (if-let [classpath-file (io/resource file)] 29 | (.getPath (io/file classpath-file)) 30 | file) 31 | *file*))) 32 | 33 | #_{:clj-kondo/ignore #?(:cljs [:unused-binding] :default [])} 34 | (defn ^:no-doc capture [level form expr env] 35 | (let [{:keys [line column file]} (meta form)] 36 | `(let [[flow# result#] (run (fn [] ~expr))] 37 | (tap> 38 | (with-meta 39 | {:form (quote ~expr) 40 | :level (if (= flow# :throw) :fatal ~level) 41 | :result result# 42 | :ns (quote ~(symbol (str *ns*))) 43 | :file ~#?(:clj (get-file env file) 44 | :portal *file* 45 | :joyride '*file* 46 | :org.babashka/nbb *file* 47 | :cljs file 48 | :cljr *file*) 49 | :line ~line 50 | :column ~column 51 | :time (now) 52 | :runtime (runtime)} 53 | {:portal.viewer/default :portal.viewer/log 54 | :portal.viewer/for 55 | {:form :portal.viewer/pprint 56 | :time :portal.viewer/relative-time}})) 57 | (if (= flow# :throw) (throw result#) result#)))) 58 | 59 | (defmacro log [expr] (capture :info &form expr &env)) 60 | 61 | (defmacro trace [expr] (capture :trace &form expr &env)) 62 | (defmacro debug [expr] (capture :debug &form expr &env)) 63 | (defmacro info [expr] (capture :info &form expr &env)) 64 | (defmacro warn [expr] (capture :warn &form expr &env)) 65 | (defmacro error [expr] (capture :error &form expr &env)) 66 | (defmacro fatal [expr] (capture :fatal &form expr &env)) 67 | -------------------------------------------------------------------------------- /src/portal/main.clj: -------------------------------------------------------------------------------- 1 | (ns portal.main 2 | (:require [clojure.edn :as edn] 3 | [clojure.java.io :as io] 4 | [portal.api :as p]) 5 | (:import [java.io PushbackReader])) 6 | 7 | (defn- lazy-fn [symbol] 8 | (fn [& args] (apply (requiring-resolve symbol) args))) 9 | 10 | (def ^:private read-json (lazy-fn 'portal.runtime.json/read-stream)) 11 | (def ^:private read-yaml (lazy-fn 'clj-yaml.core/parse-string)) 12 | (def ^:private transit-read (lazy-fn 'cognitect.transit/read)) 13 | (def ^:private transit-reader (lazy-fn 'cognitect.transit/reader)) 14 | 15 | (defn- read-transit [in] 16 | (transit-read (transit-reader in :json))) 17 | 18 | (defn- read-edn [reader] 19 | (with-open [in (PushbackReader. reader)] (edn/read in))) 20 | 21 | (defn -main [& args] 22 | (let [[input-format] args 23 | in (case input-format 24 | "json" (-> System/in io/reader read-json) 25 | "yaml" (-> System/in io/reader slurp read-yaml) 26 | "edn" (-> System/in io/reader read-edn) 27 | "transit" (-> System/in read-transit))] 28 | (reset! (p/open) in) 29 | (.addShutdownHook 30 | (Runtime/getRuntime) 31 | (Thread. #(p/close))) 32 | (println "Press CTRL+C to exit") 33 | @(promise))) 34 | -------------------------------------------------------------------------------- /src/portal/main.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.main) 2 | 3 | (defn -main []) 4 | 5 | -------------------------------------------------------------------------------- /src/portal/resources.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.resources 2 | #?(:cljs (:require-macros portal.resources)) 3 | #?(:clj (:require [clojure.java.io :as io])) 4 | #?(:org.babashka/nbb (:require ["fs" :as fs] 5 | ["path" :as path]) 6 | :joyride (:require ["vscode" :as vscode] 7 | ["ext://djblue.portal$resources" :as resources]))) 8 | 9 | #?(:org.babashka/nbb (def file *file*)) 10 | 11 | (defn resource [_resource-name] 12 | #?(:org.babashka/nbb (some 13 | (fn [file] 14 | (when (fs/existsSync file) file)) 15 | [(path/join file "../../" _resource-name) 16 | (path/join file "../../../resources" _resource-name)]) 17 | :joyride 18 | (let [vscode (js/require "vscode") 19 | path (js/require "path") 20 | ^js uri (-> vscode .-workspace .-workspaceFolders (aget 0) .-uri) 21 | fs-path (.-fsPath uri)] 22 | (.join path (if-not (undefined? fs-path) fs-path (.-path uri)) 23 | "resources" 24 | _resource-name)))) 25 | 26 | (defonce ^:no-doc resources (atom {})) 27 | 28 | #?(:clj 29 | (defmacro inline 30 | "For runtime resources, ensure the inline call happens at the namespace 31 | top-level to ensure resources are pushed into `resources for use as part of 32 | the inline fn." 33 | [resource-name] 34 | (try 35 | `(-> resources 36 | (swap! assoc ~resource-name ~(slurp (io/resource resource-name))) 37 | (get ~resource-name)) 38 | (catch Exception e 39 | (println e)))) 40 | :joyride (defn inline [resourece-name] (resources/inline resourece-name)) 41 | :org.babashka/nbb (defn inline [resource-name] 42 | (fs/readFileSync (resource resource-name) "utf8")) 43 | :cljs (defn inline [resourece-name] (get @resources resourece-name))) -------------------------------------------------------------------------------- /src/portal/runtime/clr/assembly.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc ^:no-check portal.runtime.clr.assembly 2 | (:require [portal.runtime.fs :as fs])) 3 | 4 | (defn- find-dll [package version] 5 | (fs/exists 6 | (fs/join (fs/home) ".nuget/packages" 7 | package 8 | version 9 | "lib/netstandard2.1" 10 | (str package ".dll")))) 11 | 12 | (defn- resolve-dlls [deps] 13 | (reduce-kv 14 | (fn [out package info] 15 | (let [dll (find-dll (name package) (:nuget/version info))] 16 | (cond-> out dll (assoc package dll)))) 17 | {} 18 | deps)) 19 | 20 | (defn load-deps [deps] 21 | (doseq [[_package dll] (resolve-dlls deps)] 22 | #_{:clj-kondo/ignore [:unresolved-symbol]} 23 | (assembly-load-file dll))) 24 | 25 | #_{:clj-kondo/ignore [:unresolved-symbol]} 26 | (assembly-load "System.Text.Encoding") 27 | #_{:clj-kondo/ignore [:unresolved-symbol]} 28 | (assembly-load "System.Text.Json") 29 | #_{:clj-kondo/ignore [:unresolved-symbol]} 30 | (assembly-load "System.Net.WebSockets") 31 | #_{:clj-kondo/ignore [:unresolved-symbol]} 32 | (assembly-load "System.Net") 33 | #_{:clj-kondo/ignore [:unresolved-symbol]} 34 | (assembly-load "System.Net.HttpListener") 35 | #_{:clj-kondo/ignore [:unresolved-symbol]} 36 | (assembly-load "System.Net.Http") 37 | 38 | (def ^:private deps 39 | '{clojure.data.json {:nuget/version "2.4.0"}}) 40 | 41 | (load-deps deps) 42 | 43 | ; (assembly-load-from (str clojure.lang.RT/SystemRuntimeDirectory "System.Diagnostics.Process.dll")) -------------------------------------------------------------------------------- /src/portal/runtime/datafy.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.datafy) 2 | 3 | (defn datafy 4 | "Attempts to return x as data. 5 | datafy will return the value of clojure.core.protocols/datafy. If 6 | the value has been transformed and the result supports 7 | metadata, :clojure.datafy/obj will be set on the metadata to the 8 | original value of x, and :clojure.datafy/class to the name of the 9 | class of x, as a symbol." 10 | [x] 11 | x) 12 | 13 | (defn nav [_coll _k v] v) -------------------------------------------------------------------------------- /src/portal/runtime/index.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.index) 2 | 3 | (defn html [{:keys [name version host port session-id code-url platform mode] 4 | :or {name "portal" 5 | version "0.59.1" 6 | code-url "main.js" 7 | platform #?(:bb "bb" :clj "jvm" :cljs "node" :cljr "clr")}}] 8 | (str 9 | "" 10 | "" 11 | "" 12 | "" (str name " - " platform " - " version) "" 13 | "" 14 | "" 15 | "" 16 | "" 17 | "" 18 | "" 19 | "
" 20 | "" 26 | "" 27 | ;; wait.js will ensure headless chrome doesn't exit early 28 | (when (= mode :test) "") 29 | "" 30 | "")) 31 | -------------------------------------------------------------------------------- /src/portal/runtime/json.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.json 2 | (:refer-clojure :exclude [read]) 3 | #?(:bb (:require [cheshire.core :as json]) 4 | :clj (:require [clojure.data.json :as json]) 5 | :cljr (:require [portal.runtime.clr.assembly] 6 | [clojure.data.json :as json]))) 7 | 8 | (defn write [value] 9 | #?(:bb (json/generate-string value) 10 | :clj (json/write-str value) 11 | :cljr (json/write-str value) 12 | :cljs (.stringify js/JSON (clj->js value)))) 13 | 14 | (defn read 15 | ([string] 16 | (read string {:key-fn keyword})) 17 | ([string opts] 18 | #?(:bb (json/parse-string string (:key-fn opts)) 19 | :clj (json/read-str string :key-fn (:key-fn opts)) 20 | :cljr (json/read-str string :key-fn (:key-fn opts)) 21 | :cljs (js->clj (.parse js/JSON string) 22 | :keywordize-keys 23 | (= keyword (:key-fn opts)))))) 24 | 25 | (defn read-stream [stream] 26 | #?(:bb (json/parse-stream stream keyword) 27 | :clj (json/read stream :key-fn keyword) 28 | :cljs (throw (ex-info "Unsupported in cljs" {:stream stream})))) 29 | -------------------------------------------------------------------------------- /src/portal/runtime/jvm/commands.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.jvm.commands 2 | (:require [portal.runtime :as rt] 3 | [portal.runtime.jvm.editor :as editor]) 4 | (:import [java.io File] 5 | [java.net URI URL])) 6 | 7 | (defn- can-slurp? [value] 8 | (or (string? value) 9 | (instance? URI value) 10 | (instance? URL value) 11 | (and (instance? File value) 12 | (.isFile ^File value) 13 | (.canRead ^File value)))) 14 | 15 | (rt/register! #'bean) 16 | (rt/register! #'slurp {:predicate can-slurp?}) 17 | (rt/register! #'editor/goto-definition) 18 | 19 | (def ^:private in-bb? (some? (System/getProperty "babashka.version"))) 20 | 21 | (when-not in-bb? 22 | (try 23 | (rt/register! (requiring-resolve `clojure.spec.alpha/exercise)) 24 | (catch Exception _))) 25 | -------------------------------------------------------------------------------- /src/portal/runtime/macros.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.macros 2 | #?(:cljs (:require-macros portal.runtime.macros))) 3 | 4 | #?(:clj 5 | (defn- resolve-var [& args] 6 | (apply (requiring-resolve 'cljs.analyzer/resolve-var) args))) 7 | 8 | #?(:clj 9 | (defn- ->type [type] 10 | (if (and (= type 'js/BigInt) 11 | (contains? @(requiring-resolve 'cljs.core/base-type) 'bigint)) 12 | 'bigint 13 | type))) 14 | 15 | #?(:clj 16 | (defmacro extend-type? [type & args] 17 | (when (or (= (namespace type) "js") 18 | (:type (resolve-var &env type))) 19 | `(extend-type ~(->type type) ~@args))) 20 | :joyride 21 | (defmacro extend-type? [type & args] 22 | (when (contains? #{'js/BigInt 'js/URL} type) 23 | `(extend-type ~type ~@args))) 24 | :cljs 25 | (defmacro extend-type? [type & args] 26 | `(when (exists? ~type) 27 | (extend-type ~type ~@args)))) 28 | -------------------------------------------------------------------------------- /src/portal/runtime/npm.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.npm 2 | (:require #?(:clj [clojure.java.io :as io]) 3 | [clojure.string :as str] 4 | [portal.runtime.fs :as fs] 5 | [portal.runtime.json :as json])) 6 | 7 | (defn- package-resolve [module] 8 | (when-let [package (fs/exists (fs/join module "package.json"))] 9 | (fs/exists 10 | (fs/join 11 | module 12 | (let [json (json/read (fs/slurp package) {}) 13 | umd (get-in json ["exports" "umd"]) 14 | unpkg (get json "unpkg") 15 | browser (get json "browser") 16 | main (get json "main")] 17 | (or umd 18 | (when (string? browser) 19 | browser) 20 | (get browser main) 21 | (get browser (str "./" main)) 22 | main 23 | unpkg 24 | "index.js")))))) 25 | 26 | #_{:clj-kondo/ignore #?(:clj [] :default [:unused-binding])} 27 | (defn- resource-resolve [file] 28 | #?(:clj (some-> 29 | (io/file 30 | (or (io/resource file) 31 | (io/resource (str file ".js")))) 32 | str))) 33 | 34 | (defn- relative-resolve [module root] 35 | (let [path (fs/join root module)] 36 | (or (fs/is-file path) 37 | (fs/is-file (str path ".js")) 38 | (fs/is-file (fs/join path "index.js"))))) 39 | 40 | (defn- relative? [module] (str/starts-with? module ".")) 41 | 42 | (defn- get-parents [root] 43 | (->> root 44 | (iterate fs/dirname) 45 | (take-while some?) 46 | (map #(fs/join % "node_modules")))) 47 | 48 | (defn node-resolve 49 | ([module] 50 | (if (relative? module) 51 | (or (relative-resolve module (fs/cwd)) 52 | (resource-resolve module)) 53 | (node-resolve module (fs/cwd)))) 54 | ([module root] 55 | (if (relative? module) 56 | (relative-resolve module root) 57 | (let [search-paths (get-parents root)] 58 | (or 59 | (some 60 | (fn [root] 61 | (let [path (fs/join root module)] 62 | (or 63 | (fs/is-file path) 64 | (fs/is-file (str path ".js")) 65 | (package-resolve path) 66 | (fs/is-file (fs/join path "index.js"))))) 67 | search-paths) 68 | (throw 69 | (ex-info (str "Unable to find node module: " (pr-str module)) 70 | {:module module :search-paths search-paths}))))))) 71 | -------------------------------------------------------------------------------- /src/portal/runtime/rpc.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.rpc 2 | (:require #?(:clj [portal.runtime.jvm.client :as c] 3 | :cljs [portal.runtime.node.client :as c] 4 | :cljr [portal.runtime.clr.client :as c]) 5 | [portal.runtime :as rt])) 6 | 7 | (defn on-open [session send!] 8 | (swap! rt/connections 9 | assoc (:session-id session) 10 | (fn [message] 11 | (send! (rt/write message session)))) 12 | (when-let [f (get-in session [:options :on-load])] 13 | (try 14 | (f) 15 | (catch #?(:cljs :default :default Exception) e 16 | (tap> e)))) 17 | (when-let [f (get-in session [:options :on-load-1])] 18 | (try 19 | (f (c/make-atom (:session-id session))) 20 | (catch #?(:cljs :default :default Exception) e 21 | (tap> e))))) 22 | 23 | (def ^:private ops (merge c/ops rt/ops)) 24 | 25 | (defn- not-found [_request done] 26 | (done {:status :not-found})) 27 | 28 | (defn on-receive [session message] 29 | (let [send! (get @rt/connections (:session-id session)) 30 | body (rt/read message session) 31 | id (:portal.rpc/id body) 32 | op (get ops (:op body) not-found) 33 | done (fn on-done [response] 34 | (send! 35 | (assoc response 36 | :portal.rpc/id id 37 | :op :portal.rpc/response)))] 38 | (binding [rt/*session* session] 39 | (op body done)))) 40 | 41 | (defn on-close [session] 42 | (swap! rt/connections dissoc (:session-id session)) 43 | (rt/reset-session session)) -------------------------------------------------------------------------------- /src/portal/runtime/shell.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.shell 2 | #?(:clj (:require [clojure.java.shell :as shell]) 3 | :cljs (:require ["child_process" :as cp]) 4 | :cljr (:require [clojure.clr.shell :as shell]))) 5 | 6 | (defn spawn [bin & args] 7 | #?(:clj 8 | (future 9 | (let [{:keys [exit err out]} (apply shell/sh bin args)] 10 | (when-not (zero? exit) 11 | (prn (into [bin] args)) 12 | (println err out)))) 13 | :cljs 14 | (js/Promise. 15 | (fn [resolve reject] 16 | (let [ps (cp/spawn bin (clj->js args))] 17 | (.on ps "error" reject) 18 | (.on ps "close" resolve)))) 19 | :cljr 20 | (future 21 | (let [{:keys [exit err out]} (apply shell/sh bin args)] 22 | (when-not (zero? exit) 23 | (prn (into [bin] args)) 24 | (println err out)))))) 25 | 26 | (defn sh [bin & args] 27 | #?(:clj (apply shell/sh bin args) 28 | :cljs (let [result (cp/spawnSync bin (clj->js args))] 29 | {:exit (.-status result) 30 | :out (str (.-stdout result)) 31 | :err (str (.-stderr result))}) 32 | :cljr (apply shell/sh bin args))) -------------------------------------------------------------------------------- /src/portal/runtime/transit.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.transit 2 | (:refer-clojure :exclude [read]) 3 | #?(:org.babashka/nbb (:require) 4 | :joyride (:require [cljs.core]) 5 | :clj (:require [cognitect.transit :as transit]) 6 | :cljs (:require [cognitect.transit :as transit])) 7 | #?(:clj (:import [java.io ByteArrayOutputStream ByteArrayInputStream]))) 8 | 9 | (defn read [string] 10 | #?(:org.babashka/nbb 11 | (throw (ex-info "transit/read not supported in nbb" {:string string})) 12 | :joyride 13 | (throw (ex-info "transit/read not supported in joyride" {:string string})) 14 | 15 | :clj (-> ^String string 16 | .getBytes 17 | ByteArrayInputStream. 18 | (transit/reader :json) 19 | transit/read) 20 | :cljs (transit/read (transit/reader :json) string))) 21 | 22 | (defn write [value] 23 | #?(:org.babashka/nbb 24 | (throw (ex-info "transit/write not supported in nbb" {:value value})) 25 | :joyride 26 | (throw (ex-info "transit/write not supported in joyride" {:value value})) 27 | 28 | :clj (let [out (ByteArrayOutputStream. 1024)] 29 | (transit/write 30 | (transit/writer out :json {:transform transit/write-meta}) 31 | value) 32 | (.toString out)) 33 | :cljs (transit/write 34 | (transit/writer :json {:transform transit/write-meta}) 35 | value))) 36 | -------------------------------------------------------------------------------- /src/portal/runtime/web/client.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.runtime.web.client 2 | (:require [portal.runtime :as rt])) 3 | 4 | (defonce connection (atom nil)) 5 | 6 | (defonce session (atom {:session-id ::id})) 7 | 8 | (defn request 9 | ([message] 10 | (request ::id message)) 11 | ([_session-id message] 12 | (if-let [child-window @connection] 13 | (rt/read 14 | (.portal.ui.rpc.handler ^js child-window (rt/write message @session)) 15 | @session) 16 | (throw (ex-info "Portal not open" message))))) 17 | 18 | (defn- push-state [session-id new-value] 19 | (request session-id {:op :portal.rpc/push-state :state new-value}) 20 | (rt/update-selected session-id [new-value]) 21 | new-value) 22 | 23 | (deftype Portal [session-id] 24 | IDeref 25 | (-deref [_this] (first (get-in @rt/sessions [session-id :selected]))) 26 | IReset 27 | (-reset! [_this new-value] (push-state session-id new-value)) 28 | ISwap 29 | (-swap! [this f] 30 | (reset! this (f @this))) 31 | (-swap! [this f a] 32 | (reset! this (f @this a))) 33 | (-swap! [this f a b] 34 | (reset! this (f @this a b))) 35 | (-swap! [this f a b xs] 36 | (reset! this (apply f @this a b xs)))) 37 | 38 | (defn make-atom [session-id] (Portal. session-id)) 39 | 40 | (defn sessions [] (when @connection (list (make-atom ::id)))) 41 | -------------------------------------------------------------------------------- /src/portal/shadow/preload.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.shadow.preload 2 | (:require [portal.shadow.remote :as remote])) 3 | 4 | (add-tap remote/submit) 5 | -------------------------------------------------------------------------------- /src/portal/shadow/remote.clj: -------------------------------------------------------------------------------- 1 | (ns portal.shadow.remote 2 | (:require [portal.api :as p])) 3 | 4 | (defn hook 5 | {:shadow.build/stage :compile-prepare} 6 | ([build-state] 7 | (hook build-state nil)) 8 | ([build-state options] 9 | (cond-> build-state 10 | (= (:shadow.build/mode build-state) :dev) 11 | (assoc-in 12 | [:compiler-options :closure-defines `port] 13 | (:port (p/start options)))))) 14 | -------------------------------------------------------------------------------- /src/portal/shadow/remote.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.shadow.remote 2 | (:require [portal.client.web :as client])) 3 | 4 | (goog-define port 0) 5 | 6 | (defn get-port 7 | "Get portal server port." 8 | {:added "0.28.0"} 9 | [] 10 | (if-not (zero? port) 11 | port 12 | (let [error (js/Error. 13 | (str "Portal server port is missing. " 14 | "Did you add the portal.shadow.remote/hook to :build-hooks in shadow-cljs.edn? \n" 15 | "See https://shadow-cljs.github.io/docs/UsersGuide.html#build-hooks for more info."))] 16 | (.error js/console error) 17 | (throw error)))) 18 | 19 | (defn submit 20 | "Tap target function. 21 | 22 | Usage: 23 | (add-tap portal.shadow/submit) 24 | (remove-tap portal.shadow/submit)" 25 | {:added "0.28.0" 26 | :see-also ["portal.api/submit" 27 | "portal.client.web/submit"]} 28 | ([value] 29 | (submit {:encoding :edn} value)) 30 | ([option value] 31 | (client/submit (assoc option :port (get-port)) value))) 32 | -------------------------------------------------------------------------------- /src/portal/spec.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.spec 2 | (:require [clojure.spec.alpha :as s] 3 | [portal.colors :as c])) 4 | 5 | (s/def ::options (s/keys :opt [::c/theme])) 6 | 7 | (defn assert-options [options] 8 | (let [options (select-keys options [::c/theme]) 9 | parsed (s/conform ::options options)] 10 | (when (= parsed ::s/invalid) 11 | (throw (ex-info "Invalid options" (s/explain-data ::options options)))))) 12 | 13 | -------------------------------------------------------------------------------- /src/portal/sync.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.sync 2 | (:refer-clojure :exclude [let try])) 3 | 4 | (defmacro let [bindings & body] 5 | `(clojure.core/let ~bindings ~@body)) 6 | 7 | (defmacro try [& body] `(~'try ~@body)) 8 | -------------------------------------------------------------------------------- /src/portal/ui/api.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.ui.api 2 | (:require [portal.viewer :as v] 3 | [reagent.core :as r])) 4 | 5 | (defonce viewers (r/atom (v/table [] {:columns [:name :doc]}))) 6 | 7 | (defn register-viewer! [viewer-spec] 8 | (swap! viewers 9 | (fn [viewers] 10 | (assoc 11 | viewers 12 | (or 13 | (first 14 | (keep-indexed 15 | (fn [index {:keys [name]}] 16 | (when (= (:name viewer-spec) name) 17 | index)) 18 | viewers)) 19 | (count viewers)) 20 | viewer-spec)))) 21 | 22 | (def ^:no-doc portal-api "Portal API for JS interop." #js {}) 23 | 24 | (set! (.-portal_api js/window) portal-api) -------------------------------------------------------------------------------- /src/portal/ui/cljs.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.cljs) 2 | 3 | (def eval-fn (atom nil)) 4 | 5 | (defn eval-string [input] 6 | (let [f @eval-fn] 7 | (if (fn? f) 8 | (f input) 9 | (throw (ex-info "No eval-fn setup." {:input input}))))) 10 | 11 | (def init-fn (atom nil)) 12 | 13 | (defn init [] (when-let [f @init-fn] (f))) 14 | -------------------------------------------------------------------------------- /src/portal/ui/connection_status.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.connection-status 2 | (:require [portal.async :as a] 3 | [portal.ui.react :as react] 4 | [portal.ui.state :as state])) 5 | 6 | (defn- timeout [ms] 7 | (js/Promise. 8 | (fn [_resolve reject] 9 | (js/setTimeout 10 | #(reject (ex-info "Timeout reached" {:duration ms})) ms)))) 11 | 12 | (def ^:private poll-interval-ms 5000) 13 | 14 | (def ^:private disconnect-notification 15 | {:type :error 16 | :icon :exclamation-triangle 17 | :message "Runtime disconnected"}) 18 | 19 | (defn- use-conn-poll [] 20 | (let [state (state/use-state)] 21 | (react/use-effect 22 | #js [state] 23 | (let [last-poller (atom nil) 24 | poller (fn poller [] 25 | (a/try 26 | (a/race (state/invoke 'portal.runtime/ping) 27 | (timeout poll-interval-ms)) 28 | (state/dispatch! state state/dismiss disconnect-notification) 29 | (catch :default _ 30 | (state/dispatch! state state/notify disconnect-notification)) 31 | (finally 32 | (when @last-poller 33 | (reset! last-poller (js/setTimeout poller poll-interval-ms))))))] 34 | (reset! last-poller (js/setTimeout poller 0)) 35 | (fn [] 36 | (js/clearTimeout @last-poller) 37 | (reset! last-poller nil)))))) 38 | 39 | (defn poller [] (use-conn-poll) nil) -------------------------------------------------------------------------------- /src/portal/ui/core.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.core 2 | (:require [portal.async :as a] 3 | [portal.extensions.vs-code-notebook :as notebook] 4 | [portal.ui.api :as api] 5 | [portal.ui.app :as app] 6 | [portal.ui.cljs :as cljs] 7 | [portal.ui.inspector :as ins] 8 | [portal.ui.options :as opts] 9 | [portal.ui.react :as react] 10 | [portal.ui.rpc :as rpc] 11 | [portal.ui.state :as state] 12 | [reagent.core :as r] 13 | [reagent.dom :as dom])) 14 | 15 | (def functional-compiler (r/create-compiler {:function-components true})) 16 | 17 | (defn- custom-app [opts] 18 | (let [[app set-app!] (react/use-state nil)] 19 | (react/use-effect 20 | :once 21 | (a/let [_ (cljs/eval-string {:code (str "(require '" (namespace (:main opts)) ")")}) 22 | res (cljs/eval-string {:code (str (:main opts)) :context :expr})] 23 | (set-app! (fn [] (:value res))))) 24 | (when app 25 | [app/root [app]]))) 26 | 27 | (defn connected-app [] 28 | (let [opts (opts/use-options)] 29 | (cond 30 | (= opts ::opts/loading) nil 31 | (contains? opts :main) [app/root 32 | [:> ins/error-boundary 33 | [custom-app opts]]] 34 | :else [app/app (:value opts)]))) 35 | 36 | (defn with-cache [& children] 37 | (into [:<> (meta @state/value-cache)] children)) 38 | 39 | (defn render-app [] 40 | (when-let [el (.getElementById js/document "root")] 41 | (dom/render [with-cache 42 | [opts/with-options 43 | [connected-app]]] 44 | el 45 | functional-compiler))) 46 | 47 | (defn main! [] 48 | (cljs/init) 49 | (reset! state/sender rpc/request) 50 | (reset! state/render #'render-app) 51 | (render-app)) 52 | 53 | (defn reload! [] (render-app)) 54 | 55 | (set! (.-embed api/portal-api) notebook/activate) -------------------------------------------------------------------------------- /src/portal/ui/html.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.html 2 | (:require [clojure.string :as str] 3 | [portal.ui.inspector :as ins])) 4 | 5 | (defn- ->style [string] 6 | (persistent! 7 | (reduce 8 | (fn [style rule] 9 | (let [[k v] (str/split rule #":")] 10 | (assoc! style (keyword (str/trim k)) (str/trim v)))) 11 | (transient {}) 12 | (str/split string #";")))) 13 | 14 | (defn- dom->hiccup [opts ^js el] 15 | (let [{:keys [text-handler]} opts] 16 | (case (.-nodeType el) 17 | 3 (cond-> (.-wholeText el) 18 | text-handler 19 | text-handler) 20 | 1 (let [attrs (.-attributes el)] 21 | (into 22 | [(keyword (str/lower-case (.-tagName el))) 23 | (persistent! 24 | (reduce 25 | (fn [attrs ^js attr] 26 | (let [k (keyword (.-name attr))] 27 | (assoc! attrs k 28 | (case k 29 | :style (->style (.-value attr)) 30 | (.-value attr))))) 31 | (transient {}) 32 | attrs))] 33 | (map (partial dom->hiccup opts)) 34 | (.-childNodes el)))))) 35 | 36 | (defn- parse-dom [string] 37 | (-> (js/DOMParser.) 38 | (.parseFromString string "text/html") 39 | (.getElementsByTagName "body") 40 | (aget 0) 41 | (.-childNodes))) 42 | 43 | (defn parse-html 44 | ([html] 45 | (parse-html html nil)) 46 | ([html opts] 47 | (into [:<>] 48 | (map (partial dom->hiccup opts)) 49 | (parse-dom html)))) 50 | 51 | (def ^:private opts 52 | {:text-handler 53 | (fn [text] [ins/highlight-words text])}) 54 | 55 | (defn html+ [html] (parse-html html opts)) -------------------------------------------------------------------------------- /src/portal/ui/lazy.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.lazy) 2 | 3 | (defmacro use-lazy [k value] `(use-lazy* ~k (fn [] ~value))) 4 | -------------------------------------------------------------------------------- /src/portal/ui/load.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.load) 2 | 3 | (defn- module-wrapper 4 | "https://nodejs.org/api/modules.html#the-module-wrapper" 5 | [{:keys [source]}] 6 | (str "(function (exports, require, module, __filename, __dirname) { " source "\n });")) 7 | 8 | (def conn (atom nil)) 9 | 10 | (defn load-fn-sync [m] 11 | (let [_label (str "load-fn-sync: " (pr-str m)) 12 | xhr (js/XMLHttpRequest.)] 13 | #_(.time js/console _label) 14 | (.open xhr "POST" 15 | (if-let [{:keys [host port]} @conn] 16 | (str "http://" host ":" port "/load") 17 | (if (exists? js/PORTAL_HOST) 18 | (str "http://" js/PORTAL_HOST "/load") 19 | "/load")) 20 | false) 21 | (.setRequestHeader xhr "content-type" "application/edn") 22 | (.send xhr (pr-str m)) 23 | #_(.timeEnd js/console _label) 24 | (some-> 25 | (.parse js/JSON (.-responseText xhr)) 26 | (js->clj :keywordize-keys true) 27 | (update :lang keyword) 28 | (assoc :name (:name m))))) 29 | 30 | (def ^:private require-cache (atom {})) 31 | 32 | (deftype Module [exports]) 33 | 34 | (defn load-require-cache [modules] 35 | (swap! 36 | require-cache 37 | (fn [cache] 38 | (reduce-kv 39 | (fn [cache module-name export] 40 | (assoc cache module-name (Module. export))) 41 | cache 42 | modules)))) 43 | 44 | (defn node-require 45 | ([module] 46 | (node-require nil module)) 47 | ([parent module-name] 48 | (or 49 | (some-> ^Module (get @require-cache module-name) .-exports) 50 | (try 51 | (let [{:keys [file] :as value} (load-fn-sync {:npm true :name module-name :parent parent})] 52 | (if-let [^Module module (get @require-cache file)] 53 | (.-exports module) 54 | (let [exports #js {} 55 | module-obj (Module. exports)] 56 | (swap! require-cache assoc file module-obj) 57 | ((js/eval (module-wrapper value)) 58 | exports #(node-require (:dir value) %) module-obj (:file value) (:dir value)) 59 | (.-exports module-obj)))) 60 | (catch :default e 61 | (.error js/console e) 62 | (throw e)))))) 63 | 64 | (set! (.-require js/window) node-require) 65 | (set! (.-process js/window) 66 | #js {:env #js {:NODE_ENV 67 | (if js/goog.DEBUG 68 | "development" 69 | "production")}}) 70 | -------------------------------------------------------------------------------- /src/portal/ui/options.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.ui.options 2 | (:require [clojure.edn :as edn] 3 | [portal.ui.react :as react] 4 | [portal.ui.state :as state] 5 | [reagent.core :as r])) 6 | 7 | (defn- get-extension-options [] 8 | (when-let [options (.getItem js/sessionStorage "PORTAL_EXTENSION_OPTIONS")] 9 | (edn/read-string options))) 10 | 11 | (defonce ^:private extension-options (r/atom (get-extension-options))) 12 | 13 | (defn ^:export ^:no-doc patch 14 | "Function for extensions to patch options after init." 15 | [edn-string] 16 | (reset! extension-options (edn/read-string edn-string))) 17 | 18 | (defonce ^:private options-context (react/create-context nil)) 19 | 20 | (defn ^:no-doc with-options* [options & children] 21 | (into [:r> (.-Provider options-context) 22 | #js {:value (if (= options ::loading) 23 | options 24 | (merge options @extension-options))}] 25 | children)) 26 | 27 | (defn with-options [& children] 28 | (let [[options set-options!] (react/use-state ::loading)] 29 | (react/use-effect 30 | :once 31 | (-> (state/invoke `portal.runtime/get-options) 32 | (.then set-options!))) 33 | (into [with-options* options] children))) 34 | 35 | (defn use-options [] (react/use-context options-context)) 36 | -------------------------------------------------------------------------------- /src/portal/ui/parsers.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.parsers) 2 | 3 | (defmulti parse-string (fn [format _] format)) 4 | 5 | (defmethod parse-string :format/text [_ s] s) 6 | 7 | (defn formats [] (keys (methods parse-string))) -------------------------------------------------------------------------------- /src/portal/ui/react.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.react 2 | #?(:cljs (:require ["react" :as react])) 3 | #?(:cljs (:require-macros portal.ui.react))) 4 | 5 | (def ^:dynamic *static* false) 6 | 7 | #?(:cljs 8 | (defn use-effect* [f deps] 9 | (when-not *static* 10 | (case deps 11 | :always (react/useEffect f) 12 | :once (react/useEffect f #js []) 13 | (react/useEffect f deps))))) 14 | 15 | (defmacro use-effect [deps & body] 16 | `(when-not *static* 17 | (use-effect* 18 | (fn [] 19 | (let [result# (do ~@body)] 20 | (if (fn? result#) result# js/undefined))) 21 | ~deps))) 22 | 23 | #?(:cljs 24 | (defn use-state [initial-value] 25 | (if *static* 26 | #js [initial-value (constantly nil)] 27 | (react/useState initial-value)))) 28 | 29 | #?(:cljs 30 | (defn create-context [default-value] 31 | (react/createContext default-value))) 32 | 33 | #?(:cljs 34 | (defn use-context [context] 35 | (if *static* 36 | (.-_currentValue context) 37 | (react/useContext context)))) 38 | 39 | #?(:cljs 40 | (defn use-ref 41 | ([] 42 | (use-ref nil)) 43 | ([initial-value] 44 | (if *static* 45 | #js {:current js/undefined} 46 | (react/useRef initial-value))))) 47 | 48 | #?(:cljs 49 | (defn use-memo* [f deps] 50 | (if *static* 51 | (f) 52 | (case deps 53 | :always (react/useMemo f) 54 | :once (react/useMemo f #js []) 55 | (react/useMemo f deps))))) 56 | 57 | (defmacro use-memo [deps & body] 58 | `(use-memo* (fn [] ~@body) ~deps)) -------------------------------------------------------------------------------- /src/portal/ui/repl/sci/import.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.repl.sci.import 2 | (:refer-clojure :exclude [import]) 3 | #?(:cljs (:require-macros portal.ui.repl.sci.import)) 4 | #?(:cljs (:require [sci.core :as sci]))) 5 | 6 | (defn- sci-import [symbols] 7 | (let [ns (gensym)] 8 | (reduce-kv 9 | (fn [namespaces namespace var-symbols] 10 | (let [namespace (if (= namespace 'cljs.core) 'clojure.core namespace)] 11 | (assoc 12 | namespaces 13 | (list 'quote namespace) 14 | `(let [~ns (sci.core/create-ns '~namespace nil)] 15 | ~(reduce 16 | (fn [mapped var-symbol] 17 | (assoc mapped 18 | (list 'quote (symbol (name var-symbol))) 19 | `(sci.core/copy-var ~var-symbol ~ns))) 20 | {:obj ns} 21 | var-symbols))))) 22 | {} 23 | (group-by (comp symbol namespace) symbols)))) 24 | 25 | (defmacro import [& symbols] (sci-import symbols)) 26 | 27 | #?(:cljs 28 | (defn import-ns* [namespace publics] 29 | {namespace 30 | (let [ns (sci/create-ns namespace nil)] 31 | (reduce 32 | (fn [ns-map [var-name var]] 33 | (let [m (meta var)] 34 | (if (:private m) 35 | ns-map 36 | (assoc ns-map var-name 37 | (sci/new-var (symbol var-name) @var (assoc m :ns ns)))))) 38 | {:obj ns} 39 | publics))})) 40 | 41 | (defmacro import-ns [& namespaces] 42 | `(merge 43 | ~@(for [ns namespaces] 44 | `(import-ns* '~ns (ns-publics '~ns))))) 45 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/bin.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.bin 2 | (:require [portal.colors :as c] 3 | [portal.ui.inspector :as ins] 4 | [portal.ui.lazy :as l] 5 | [portal.ui.styled :as s] 6 | [portal.ui.theme :as theme])) 7 | 8 | (def ^:private indexed (partial map-indexed vector)) 9 | 10 | (defn- hex-value [[a b]] 11 | (let [theme (theme/use-theme)] 12 | [s/div 13 | {:style {:color (::c/number theme)}} 14 | (when a (.padStart (.toString a 16) 2 "0")) 15 | (when b (.padStart (.toString b 16) 2 "0"))])) 16 | 17 | (defn- pad-8 [row] 18 | (take 8 (concat row (repeat nil)))) 19 | 20 | (defn- inspect-hex [value] 21 | (for [[idx row] (indexed (partition-all 16 value))] 22 | [:<> 23 | {:key idx} 24 | (for [[idx hex] (indexed (pad-8 (partition-all 2 row)))] 25 | ^{:key idx} [hex-value hex])])) 26 | 27 | (defn- ascii-value [c] 28 | (let [theme (theme/use-theme)] 29 | (if-not (<= 32 c 127) 30 | [s/div {:style {:color (::c/border theme)}} "."] 31 | [s/div {:style {:color (::c/string theme)}} 32 | (String/fromCharCode c)]))) 33 | 34 | (defn- inspect-ascii [value] 35 | (for [[idx row] (indexed (partition-all 16 value))] 36 | [:<> 37 | {:key idx} 38 | (for [[idx c] (indexed row)] 39 | ^{:key idx} [ascii-value c])])) 40 | 41 | (defn inspect-bin [value] 42 | (let [theme (theme/use-theme) 43 | hex (inspect-hex value) 44 | ascii (inspect-ascii value) 45 | opts (ins/use-options)] 46 | [s/div 47 | {:style {:display :flex 48 | :overflow :auto 49 | :max-height (when-not (:expanded? opts) "24rem")}} 50 | [s/div 51 | {:style {:display :grid 52 | :grid-gap (:padding theme) 53 | :grid-template-columns 54 | (str "repeat(" (+ 1 1 8 1 16) ", auto)")}} 55 | (l/use-lazy 56 | value 57 | (for [[idx [hex ascii]] (indexed (map vector hex ascii))] 58 | [:<> {:key idx} 59 | [s/div 60 | {:style {:color (::c/border theme)}} 61 | (.padStart (.toString idx 16) 8 "0") ":"] 62 | [s/div] 63 | [:<> hex] 64 | [s/div] 65 | [:<> ascii]]))]])) 66 | 67 | (def viewer 68 | {:predicate ins/bin? 69 | :component #'inspect-bin 70 | :name :portal.viewer/bin 71 | :doc "View binary data as a hexdump."}) 72 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/color.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.color 2 | (:require [clojure.spec.alpha :as s] 3 | [portal.colors :as c] 4 | [portal.ui.inspector :as ins] 5 | [portal.ui.styled :as d] 6 | [portal.ui.theme :as theme])) 7 | 8 | ;;; :spec 9 | (defn- hex-short? [string] 10 | (re-matches #"#[0-9a-fA-F]{3}gi" string)) 11 | 12 | (defn- hex-full? [string] 13 | (re-matches #"#[0-9a-fA-F]{6}" string)) 14 | 15 | (defn- hex-alpha? [string] 16 | (re-matches #"#[0-9a-fA-F]{8}" string)) 17 | 18 | (defn- rgb-color? [string] 19 | (re-matches #"rgb\(\d+,\d+,\d+\)" string)) 20 | 21 | (defn- rgba-color? [string] 22 | (re-matches #"rgba\(.+,.+,.+,.+\)" string)) 23 | 24 | (s/def ::hex 25 | (s/or :short hex-short? 26 | :full hex-full? 27 | :alpha hex-alpha?)) 28 | 29 | (s/def ::rgb 30 | (s/or :rgb rgb-color? :rgba rgba-color?)) 31 | 32 | (s/def ::color 33 | (s/and string? (s/or :hex ::hex :rgb ::rgb))) 34 | ;;; 35 | 36 | (defn inspect-color [value] 37 | (let [theme (theme/use-theme)] 38 | [d/div 39 | {:style {:display :flex 40 | :gap (:padding theme) 41 | :align-items :center}} 42 | [d/div 43 | {:style {:width (:font-size theme) 44 | :height (:font-size theme) 45 | :border [1 :solid (::c/border theme)] 46 | :background value 47 | :border-radius (:border-radius theme)}}] 48 | [d/div [ins/highlight-words value]]])) 49 | 50 | (defn- color? [value] (s/valid? ::color value)) 51 | 52 | (def viewer 53 | {:predicate color? 54 | :component #'inspect-color 55 | :name :portal.viewer/color 56 | :doc "View hex / rgb / rgba colors"}) -------------------------------------------------------------------------------- /src/portal/ui/viewer/csv.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.csv 2 | (:require ["papaparse" :refer [parse]] 3 | [portal.ui.inspector :as ins] 4 | [portal.ui.parsers :as p])) 5 | 6 | (defn parse-csv [csv-string] 7 | (try 8 | (with-meta 9 | (js->clj (.-data (parse csv-string))) 10 | {:portal.viewer/default :portal.viewer/table}) 11 | (catch :default _e ::invalid))) 12 | 13 | (defmethod p/parse-string :format/csv [_ s] (parse-csv s)) 14 | 15 | (defn csv? [value] (string? value)) 16 | 17 | (defn inspect-csv [csv-string] 18 | [ins/tabs 19 | {:portal.viewer/csv (parse-csv csv-string) 20 | "..." csv-string}]) 21 | 22 | (def viewer 23 | {:predicate csv? 24 | :component #'inspect-csv 25 | :name :portal.viewer/csv 26 | :doc "Parse a string as a CSV and use the table viewer by default."}) 27 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/edn.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.edn 2 | (:require [portal.runtime.edn :as edn] 3 | [portal.ui.inspector :as ins] 4 | [portal.ui.parsers :as p])) 5 | 6 | (defn read-string [edn-string] 7 | (try (edn/read-string edn-string) 8 | (catch :default e (ins/error->data e)))) 9 | 10 | (defmethod p/parse-string :format/edn [_ s] (read-string s)) 11 | 12 | (defn edn? [value] (string? value)) 13 | 14 | (defn inspect-edn [edn-string] 15 | [ins/tabs 16 | {:portal.viewer/edn (read-string edn-string) 17 | "..." edn-string}]) 18 | 19 | (def viewer 20 | {:predicate edn? 21 | :component #'inspect-edn 22 | :name :portal.viewer/edn 23 | :doc "Parse a string as EDN. Will render error if parsing fails."}) 24 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/html.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.html 2 | (:require [portal.colors :as c] 3 | [portal.ui.styled :as s] 4 | [portal.ui.theme :as theme])) 5 | 6 | (defn inspect-html [value] 7 | (let [theme (theme/use-theme)] 8 | [s/iframe {:style {:width "100%" 9 | :height "75vh" 10 | :border-radius (:border-radius theme) 11 | :border [1 :solid (::c/border theme)]} 12 | :src-doc value}])) 13 | 14 | (def viewer 15 | {:predicate string? 16 | :component #'inspect-html 17 | :name :portal.viewer/html}) 18 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/image.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.image 2 | (:require [portal.colors :as c] 3 | [portal.ui.inspector :as ins] 4 | [portal.ui.styled :as s] 5 | [portal.ui.theme :as theme])) 6 | 7 | (defn inspect-image [value] 8 | (let [theme (theme/use-theme) 9 | blob (js/Blob. #js [value]) 10 | url (or js/window.URL js/window.webkitURL) 11 | src (.createObjectURL url blob)] 12 | [s/img 13 | {:src src 14 | :style 15 | {:max-height "100%" 16 | :max-width "100%" 17 | :user-select :none 18 | :background (ins/get-background) 19 | :border-radius (:border-radius theme) 20 | :border [1 :solid (::c/border theme)]}}])) 21 | 22 | (def viewer 23 | {:predicate ins/bin? 24 | :component #'inspect-image 25 | :name :portal.viewer/image 26 | :doc "View a binary value as an image."}) 27 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/json.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.json 2 | (:require [portal.ui.inspector :as ins] 3 | [portal.ui.parsers :as p])) 4 | 5 | (defn- parse-json [json-string] 6 | (try (js->clj (js/JSON.parse json-string) :keywordize-keys true) 7 | (catch :default e (ins/error->data e)))) 8 | 9 | (defmethod p/parse-string :format/json [_ s] (parse-json s)) 10 | 11 | (defn json? [value] (string? value)) 12 | 13 | (defn inspect-json [json-string] 14 | [ins/tabs 15 | {:portal.viewer/json (parse-json json-string) 16 | "..." json-string}]) 17 | 18 | (def viewer 19 | {:predicate json? 20 | :component #'inspect-json 21 | :name :portal.viewer/json 22 | :doc "Parse a string as JSON. Will render error if parsing fails."}) 23 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/jwt.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.jwt 2 | (:require [clojure.string :as str] 3 | [goog.crypt.base64 :as Base64] 4 | [portal.ui.inspector :as ins] 5 | [portal.ui.parsers :as p])) 6 | 7 | (defn- parse-json [value] 8 | (js->clj (.parse js/JSON (js/atob value)) :keywordize-keys true)) 9 | 10 | (defn parse-jwt [jwt] 11 | (try 12 | (let [[header payload signature] (str/split jwt ".")] 13 | (with-meta 14 | {:jwt/header (parse-json header) 15 | :jwt/payload 16 | (with-meta (parse-json payload) 17 | {:portal.viewer/for 18 | {:auth_time :portal.viewer/date-time 19 | :exp :portal.viewer/date-time 20 | :iat :portal.viewer/date-time 21 | :nbf :portal.viewer/date-time}}) 22 | :jwt/signature 23 | (Base64/decodeStringToUint8Array signature)} 24 | {:portal.viewer/for 25 | {:jwt/signature :portal.viewer/bin}})) 26 | (catch :default e (ins/error->data e)))) 27 | 28 | (defmethod p/parse-string :format/jwt [_ s] (parse-jwt s)) 29 | 30 | (defn inspect-jwt [jwt] 31 | [ins/tabs 32 | {:portal.viewer/jwt (parse-jwt jwt) 33 | "..." jwt}]) 34 | 35 | (def viewer 36 | {:predicate string? 37 | :component #'inspect-jwt 38 | :name :portal.viewer/jwt 39 | :doc "Parse a string as a JWT. Will render error if parsing fails."}) 40 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/source_location.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.source-location 2 | (:require [clojure.spec.alpha :as s] 3 | [portal.colors :as c] 4 | [portal.ui.inspector :as ins] 5 | [portal.ui.rpc :as rpc] 6 | [portal.ui.styled :as d] 7 | [portal.ui.theme :as theme])) 8 | 9 | ;;; :spec 10 | (s/def ::ns symbol?) 11 | (s/def ::column int?) 12 | (s/def ::line int?) 13 | (s/def ::file string?) 14 | 15 | (s/def ::source-location 16 | (s/keys :req-un [::line ::column] 17 | :opt-un [::ns ::file])) 18 | ;;; 19 | 20 | (defn- source-location? [value] 21 | (s/valid? ::source-location value)) 22 | 23 | (defn ->source-location [value] 24 | (when (source-location? value) 25 | (with-meta 26 | (select-keys value [:ns :column :line :file :label]) 27 | (assoc (meta value) 28 | :portal.viewer/default :portal.viewer/source-location)))) 29 | 30 | (defn inspect-source [value] 31 | (let [theme (theme/use-theme)] 32 | [d/div 33 | {:on-click 34 | (fn [e] 35 | (.stopPropagation e) 36 | (rpc/call 'portal.runtime.jvm.editor/goto-definition value)) 37 | :style/hover 38 | {:opacity 1 39 | :text-decoration :underline} 40 | :style 41 | {:opacity 0.75 42 | :cursor :pointer 43 | :color (::c/uri theme)}} 44 | [ins/highlight-words 45 | (str 46 | (or (:label value) 47 | (:ns value) 48 | (:file value)) 49 | ":" 50 | (:line value))]])) 51 | 52 | (def viewer 53 | {:predicate source-location? 54 | :component #'inspect-source 55 | :name :portal.viewer/source-location 56 | :doc "View a map as a source location, provides goto definition on click."}) 57 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/text.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.text 2 | (:require [clojure.string :as str] 3 | [portal.colors :as c] 4 | [portal.ui.inspector :as ins] 5 | [portal.ui.lazy :as l] 6 | [portal.ui.styled :as s] 7 | [portal.ui.theme :as theme])) 8 | 9 | (defn inspect-text [value] 10 | (let [theme (theme/use-theme) 11 | opts (ins/use-options) 12 | background (ins/get-background) 13 | search-text (ins/use-search-text)] 14 | [s/div 15 | {:style 16 | {:overflow :auto 17 | :background background 18 | :padding (:padding theme) 19 | :box-sizing :border-box 20 | :cursor :text 21 | :border-radius (:border-radius theme) 22 | :border [1 :solid (::c/border theme)] 23 | :max-height (when-not (:expanded? opts) "24rem")}} 24 | [s/table 25 | [s/tbody 26 | [l/lazy-seq 27 | (->> 28 | (str/split value #"\n") 29 | (map-indexed 30 | (fn [line line-content] 31 | [(inc line) line-content])) 32 | (filter 33 | (fn [[_ line-content]] 34 | (if search-text 35 | (some 36 | #(str/includes? line-content %) 37 | (str/split search-text #"\s+")) 38 | true))) 39 | (map 40 | (fn [[line line-content]] 41 | [s/tr 42 | {:key line} 43 | [s/td 44 | {:style 45 | {:color (::c/number theme) 46 | :background background 47 | :font-size (:font-size theme) 48 | :user-select :none 49 | :text-align :right 50 | :vertical-align :top 51 | :padding-right (* 2 (:padding theme))}} 52 | [s/span line]] 53 | [s/td 54 | {:style 55 | {:color (::c/text theme) 56 | :background background 57 | :text-align :left 58 | :font-size (:font-size theme)}} 59 | [:pre {:style {:margin 0 :white-space :pre-wrap}} 60 | [ins/highlight-words line-content]]]]))) 61 | {:default-take 100 :step 100}]]]])) 62 | 63 | (def viewer 64 | {:predicate string? 65 | :component #'inspect-text 66 | :name :portal.viewer/text 67 | :doc "View string as a text file."}) 68 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/transit.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.transit 2 | (:require [cognitect.transit :as t] 3 | [portal.ui.inspector :as ins] 4 | [portal.ui.parsers :as p])) 5 | 6 | (defn- parse-transit [transit-string] 7 | (try (t/read (t/reader :json) transit-string) 8 | (catch :default e (ins/error->data e)))) 9 | 10 | (defmethod p/parse-string :format/transit [_ s] (parse-transit s)) 11 | 12 | (defn transit? [value] (string? value)) 13 | 14 | (defn inspect-transit [transit-string] 15 | [ins/tabs 16 | {:portal.viewer/transit (parse-transit transit-string) 17 | "..." transit-string}]) 18 | 19 | (def viewer 20 | {:predicate transit? 21 | :component #'inspect-transit 22 | :name :portal.viewer/transit 23 | :doc "Parse a string as transit. Will render error if parsing fails."}) 24 | -------------------------------------------------------------------------------- /src/portal/ui/viewer/vega_lite.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc portal.ui.viewer.vega-lite 2 | "Viewer for the Vega-Lite specification 3 | https://vega.github.io/vega-lite/docs/spec.html" 4 | (:require [clojure.spec.alpha :as s] 5 | [portal.ui.viewer.vega :as vega])) 6 | 7 | ;;; :spec 8 | (def vega-lite-url #"https://vega\.github\.io/schema/vega-lite/v\d\.json") 9 | 10 | (s/def ::name string?) 11 | (s/def ::description string?) 12 | (s/def ::$schema 13 | (s/and string? #(re-matches vega-lite-url %))) 14 | 15 | (s/def ::vega-lite 16 | (s/keys :req-un [(or ::data ::datasets)] 17 | :opt-un [::name ::description ::$schema])) 18 | ;;; 19 | 20 | (defn vega-lite-viewer [value] 21 | [vega/vega-embed 22 | {:mode "vega-lite" :renderer :svg} 23 | value]) 24 | 25 | (def viewer 26 | {:predicate (partial s/valid? ::vega-lite) 27 | :component #'vega-lite-viewer 28 | :name :portal.viewer/vega-lite}) 29 | -------------------------------------------------------------------------------- /test/portal/bench.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.bench 2 | #?(:cljs (:refer-clojure :exclude [simple-benchmark])) 3 | #?(:cljs (:require-macros portal.bench))) 4 | 5 | (defn- now 6 | ([] 7 | #?(:clj (System/nanoTime) 8 | :cljr (.Ticks (System.DateTime/Now)) 9 | :cljs (if (exists? js/process) 10 | (.hrtime js/process) 11 | (.now js/Date)))) 12 | ([a] 13 | #?(:clj (/ (- (now) a) 1000000.0) 14 | :cljr (/ (- (now) a) 10000.0) 15 | :cljs (if (exists? js/process) 16 | (let [[a b] (.hrtime js/process a)] 17 | (+ (* a 1000.0) (/ b 1000000.0))) 18 | (- (.now js/Date) a))))) 19 | (defn floor [v] 20 | #?(:cljr (Math/Floor v) :default (Math/floor v))) 21 | 22 | (defn trunc [v] 23 | (/ (floor (* 100 v)) 100.0)) 24 | 25 | (defn- simple-stats [results] 26 | (let [n (count results) 27 | results (into [] (sort results)) 28 | median (nth results (quot n 2)) 29 | total (reduce + results)] 30 | ^{:portal.viewer/for 31 | {:min :portal.viewer/duration-ms 32 | :max :portal.viewer/duration-ms 33 | :avg :portal.viewer/duration-ms 34 | :med :portal.viewer/duration-ms 35 | :total :portal.viewer/duration-ms}} 36 | {:iter n 37 | :min (first results) 38 | :max (last results) 39 | :avg (trunc (/ total n)) 40 | :med median 41 | :total (trunc total)})) 42 | 43 | (defn run* [f ^long n] 44 | (dotimes [_ n] (f)) 45 | (simple-stats 46 | (loop [i 0 results (transient [])] 47 | (if (== i n) 48 | (persistent! results) 49 | (let [start (now) 50 | _ (f) 51 | end (now start)] 52 | (recur (unchecked-inc i) 53 | (conj! results (trunc end)))))))) 54 | 55 | (defmacro run [expr n] `(run* #(do ~expr) ~n)) 56 | -------------------------------------------------------------------------------- /test/portal/client.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.client 2 | #?(:clj (:require [portal.client.jvm :as p]) 3 | :cljr (:require [portal.client.clr :as p]) 4 | :cljs (:require [portal.client.node :as p]) 5 | :lpy (:require [portal.client.python :as p])) 6 | #?(:cljr (:import (System Environment)) 7 | :lpy (:import [os :as os]))) 8 | 9 | (def ^:private port 10 | #?(:clj (System/getenv "PORTAL_PORT") 11 | :cljr (Environment/GetEnvironmentVariable "PORTAL_PORT") 12 | :cljs (.. js/process -env -PORTAL_PORT) 13 | :lpy (.get os/environ "PORTAL_PORT"))) 14 | 15 | (defn enabled? [] (some? port)) 16 | 17 | (defn submit [value] 18 | (p/submit {:port port :encoding :cson} value)) 19 | 20 | ;; (defn table [value] 21 | ;; (if (enabled?) 22 | ;; (submit value) 23 | ;; (pp/print-table 24 | ;; (get-in (meta value) [:portal.viewer/table :columns]) 25 | ;; value))) -------------------------------------------------------------------------------- /test/portal/client_test.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.client-test 2 | #?(:clj 3 | (:require [clojure.test :refer [deftest is]] 4 | [portal.api :as p] 5 | [portal.client.jvm :as c] 6 | [portal.runtime :as rt] 7 | [portal.sync :as a]) 8 | :cljr 9 | (:require [clojure.test :refer [deftest is]] 10 | [portal.api :as p] 11 | [portal.client.clr :as c] 12 | [portal.runtime :as rt] 13 | [portal.sync :as a]) 14 | :cljs 15 | (:require [clojure.test :refer [async deftest is]] 16 | [portal.api :as p] 17 | [portal.async :as a] 18 | [portal.client.node :as c] 19 | [portal.runtime :as rt]))) 20 | 21 | (def ^:private bad-seq (map (fn [_] (throw (ex-info "Error" {}))) (range 10))) 22 | 23 | (defn- client-test* [done] 24 | (a/let [opts {:port 12345 :host "127.0.0.1"} 25 | tap-list @#'rt/tap-list] 26 | (swap! tap-list empty) 27 | (p/start opts) 28 | (c/submit opts ::value) 29 | (c/submit opts bad-seq) 30 | (p/stop) 31 | (is (= "Error" (:cause (first @tap-list)))) 32 | (is (= ::value (second @tap-list))) 33 | (done))) 34 | 35 | (deftest client-test 36 | #?(:cljs (async done (client-test* done)) 37 | :default (client-test* (constantly nil)))) -------------------------------------------------------------------------------- /test/portal/e2e.clj: -------------------------------------------------------------------------------- 1 | (ns portal.e2e 2 | (:require [portal.colors :as c])) 3 | 4 | (defn step [code] 5 | (binding [*out* *err*] 6 | (println "\n==> Enter to execute:" code "\n")) 7 | (read-line) 8 | (prn code)) 9 | 10 | (def pane-titles '("Alice" "Mad Hatter" "The Cake is a Lie")) 11 | 12 | (defn options [] 13 | {:app (rand-nth [true false]) 14 | :theme (rand-nth (keys (dissoc c/themes ::c/vs-code-embedded))) 15 | :on-load '(fn [] (tap> :loaded!)) 16 | :window-title (rand-nth pane-titles)}) 17 | 18 | (defn -main [& args] 19 | (if (= (first args) "web") 20 | (step '(require '[portal.web :as p])) 21 | (step '(require '[portal.api :as p]))) 22 | (step `(do (add-tap #'p/submit) 23 | (p/open ~(options)))) 24 | (step '(p/clear)) 25 | (step '(require '[examples.data :refer [data]])) 26 | (step '(tap> data)) 27 | (step '(p/clear)) 28 | (step '(remove-tap #'p/submit)) 29 | (step '(tap> :hello-world)) 30 | (step '(p/eval-str "(js/alert 1)")) 31 | (step '(p/close)) 32 | (when-not (= (first args) "web") 33 | (step '(p/stop)))) 34 | -------------------------------------------------------------------------------- /test/portal/runtime/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.api-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [portal.api :as p] 4 | [portal.runtime.browser :as browser])) 5 | 6 | (defn- headless-chrome-flags [url] 7 | ["--headless=new" "--disable-gpu" "--no-sandbox" url]) 8 | 9 | (defn- open [] 10 | (p/open {:mode :test 11 | ::browser/chrome-bin ["chromium"] 12 | ::browser/flags headless-chrome-flags})) 13 | 14 | (deftest e2e-jvm-test 15 | (let [portal (open)] 16 | (reset! portal 0) 17 | (is (= @portal 0)) 18 | (swap! portal inc) 19 | (is (= @portal 1)) 20 | (is (= 6 (p/eval-str portal "(+ 1 2 3)"))) 21 | (is (= 6 (p/eval-str portal "*1"))) 22 | (is (= :world (:hello (p/eval-str portal "{:hello :world}")))) 23 | (is (thrown? 24 | clojure.lang.ExceptionInfo 25 | (p/eval-str portal "(throw (ex-info \"error\" {:hello :world}))"))) 26 | (is (= :hi (p/eval-str portal "(.resolve js/Promise :hi)" {:await true}))) 27 | (is (some? (some #{portal} (p/sessions)))) 28 | (p/close portal))) 29 | -------------------------------------------------------------------------------- /test/portal/runtime/api_test.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.api-test 2 | (:require [clojure.test :refer [async deftest is]] 3 | [portal.api :as p] 4 | [portal.async :as a] 5 | [portal.runtime.browser :as browser])) 6 | 7 | (defn- headless-chrome-flags [url] 8 | ["--headless=new" "--disable-gpu" "--no-sandbox" url]) 9 | 10 | (defn- open [] 11 | (p/open {:mode :test 12 | ::browser/chrome-bin ["chromium"] 13 | ::browser/flags headless-chrome-flags})) 14 | 15 | (defn- is= [a b] 16 | (a/let [a' a b' b] (is (= a' b')))) 17 | 18 | (deftest e2e-node-test 19 | (async done 20 | (a/let [portal (open)] 21 | (reset! portal 0) 22 | (is (= @portal 0)) 23 | (swap! portal inc) 24 | (is (= @portal 1)) 25 | (is= 6 (p/eval-str portal "(+ 1 2 3)")) 26 | (is= 6 (p/eval-str portal "*1")) 27 | (is= {:hello :world} (p/eval-str portal "{:hello :world}")) 28 | (-> (p/eval-str portal "(throw (ex-info \"error\" {:hello :world}))") 29 | (.then (fn [] (throw (ex-info "Should throw error" {})))) 30 | (.catch (fn [_]))) 31 | (is= :hi (p/eval-str portal "(.resolve js/Promise :hi)" {:await true})) 32 | (is (some? (some #{portal} (p/sessions)))) 33 | (p/close portal) 34 | (done)))) 35 | -------------------------------------------------------------------------------- /test/portal/runtime/edn_test.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.edn-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [portal.runtime.edn :as edn])) 4 | 5 | (deftest read-string-test 6 | (let [tagged (edn/read-string "^{:my :meta} #'hi")] 7 | (is (= 'hi (:rep tagged))) 8 | (is (= "portal/var" (:tag tagged))) 9 | (is (= {:my :meta} (meta tagged)))) 10 | (let [tagged (edn/read-string "#\"hi\"")] 11 | (is (= "portal/re" (:tag tagged))) 12 | (is (= "hi" (:rep tagged)))) 13 | (let [tagged (edn/read-string (pr-str #"\\Qhi\\E"))] 14 | (is (= "portal/re" (:tag tagged))) 15 | (is (= "\\Qhi\\E" (:rep tagged)))) 16 | #?(:org.babashka/nbb nil 17 | :default 18 | (let [s "#function [clojure.core/constantly/fn--5740]"] 19 | (is (= s (pr-str (edn/read-string s))))))) -------------------------------------------------------------------------------- /test/portal/runtime/fs_test.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.fs-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [portal.runtime.fs :as fs])) 4 | 5 | (deftest fs-test 6 | (is (some? (fs/slurp "deps.edn"))) 7 | (let [deps (fs/join (fs/cwd) "deps.edn")] 8 | (is (= (fs/exists deps) deps))) 9 | (is (some? (fs/home))) 10 | (is (some? (seq (fs/paths)))) 11 | (is (some? (fs/is-file "deps.edn"))) 12 | (is (nil? (fs/is-file "deps.end"))) 13 | (is (contains? 14 | (into #{} (fs/list (fs/cwd))) 15 | (fs/join (fs/cwd) "deps.edn"))) 16 | (let [dir (str "target/" (gensym)) 17 | file (str dir "/" (gensym))] 18 | (fs/mkdir dir) 19 | (fs/spit file "hello") 20 | (is (= (fs/slurp file) "hello")) 21 | (fs/rm dir) 22 | (is (nil? (fs/exists file))) 23 | (is (nil? (fs/exists dir)))) 24 | (let [cwd (fs/cwd) 25 | path (fs/join cwd "deps.edn")] 26 | (is (= cwd (fs/dirname path)))) 27 | (is (nil? (fs/dirname "/"))) 28 | (is (= "deps.edn" (fs/basename "deps.edn"))) 29 | (is (= "portal" (fs/basename "src/portal"))) 30 | (is (= "runtime.cljc" (fs/basename "src/portal/runtime.cljc")))) 31 | 32 | (deftest modified 33 | (fs/spit "target/new" "hi") 34 | (is (< (fs/modified "deps.edn") 35 | (fs/modified "target/new")))) 36 | -------------------------------------------------------------------------------- /test/portal/runtime/json_buffer_test.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.json-buffer-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [portal.runtime.json-buffer :as b])) 4 | 5 | (defn- write-json [buffer _] 6 | (-> buffer 7 | (b/push-null) 8 | (b/push-long 0) 9 | (b/push-double 0.5) 10 | (b/push-bool true) 11 | (b/push-bool false) 12 | (b/push-string "hello"))) 13 | 14 | (deftest json-buffer 15 | (let [r (b/->reader (b/with-buffer write-json nil))] 16 | (is (nil? (b/next-null r))) 17 | (is (zero? (b/next-long r))) 18 | (is (= 0.5 (b/next-double r))) 19 | (is (true? (b/next-bool r))) 20 | (is (false? (b/next-bool r))) 21 | (is (= "hello" (b/next-string r)))) 22 | (let [r (b/->reader (b/with-buffer write-json nil))] 23 | (is (nil? (b/next-value r))) 24 | (is (zero? (b/next-value r))) 25 | (is (= 0.5 (b/next-value r))) 26 | (is (true? (b/next-value r))) 27 | (is (false? (b/next-value r))) 28 | (is (= "hello" (b/next-value r))))) 29 | -------------------------------------------------------------------------------- /test/portal/runtime/jvm/editor_test.clj: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.jvm.editor-test 2 | (:require [clojure.java.io :as io] 3 | [clojure.test :refer [are deftest]] 4 | [portal.runtime] 5 | [portal.runtime.fs :as fs] 6 | [portal.runtime.jvm.editor :as editor])) 7 | 8 | (deftest can-goto-test 9 | (are [value] 10 | (fs/exists (:file (editor/can-goto value))) 11 | ;; maps 12 | {:file "deps.edn"} 13 | {:ns 'portal.runtime} 14 | 15 | ;; vars 16 | #'portal.runtime/ops 17 | 18 | ;; namespace symbols 19 | 'portal.runtime 20 | 'portal.runtime/ops 21 | 22 | ;; urls 23 | (io/resource "portal/runtime.cljc") 24 | 25 | ;; files 26 | (io/file "deps.edn") 27 | 28 | ;; strings 29 | "deps.edn" 30 | "src/portal/runtime.cljc" 31 | 32 | ;; string on classpath 33 | "portal/runtime.cljc") 34 | 35 | (are [value] 36 | (not (fs/exists (:file (editor/can-goto value)))) 37 | ;; maps 38 | {:file "missing.edn"} 39 | {:ns 'ns.missing} 40 | {} 41 | 42 | ;; namespace symbols 43 | 'ns.missing 44 | 'ns.missing/conj 45 | 46 | ;; urls 47 | (io/resource "portal/missing.cljc") 48 | 49 | ;; files 50 | (io/file "missing.edn") 51 | 52 | ;; strings 53 | "missing.edn" 54 | "src/portal/missing.cljc" 55 | 56 | ;; string on classpath 57 | "portal/missing.cljc")) 58 | -------------------------------------------------------------------------------- /test/portal/runtime/npm_test.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.npm-test 2 | (:require [clojure.test :refer [are deftest]] 3 | [portal.runtime.npm :refer [node-resolve]])) 4 | 5 | (deftest valid-modules 6 | (are [module] 7 | (some? (node-resolve module)) 8 | "react/jsx-runtime.js" 9 | "react/index" 10 | "react")) 11 | 12 | (deftest invalid-modules 13 | (are [module] 14 | (thrown? 15 | #?(:clj Exception :cljr Exception :cljs js/Error) 16 | (node-resolve module)) 17 | "react/index.j")) 18 | -------------------------------------------------------------------------------- /test/portal/runtime/shell_test.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.runtime.shell-test 2 | (:require [clojure.string :as str] 3 | [clojure.test :refer [deftest is]] 4 | [portal.runtime.shell :as sh])) 5 | 6 | (deftest echo 7 | (is (= ":hi" (some-> (sh/sh "bb" "-e" ":hi") :out str/trim)))) -------------------------------------------------------------------------------- /test/portal/runtime_test.cljc: -------------------------------------------------------------------------------- 1 | (ns portal.runtime-test 2 | (:require [clojure.test :refer [are deftest is]] 3 | [portal.runtime :as rt])) 4 | 5 | (deftest un-hashable-values 6 | (let [value #?(:bb :skip 7 | :org.babashka/nbb :skip 8 | :clj (reify Object 9 | (hashCode [_] (throw (Exception. "test")))) 10 | :cljs (reify IHash 11 | (-hash [_] (throw (js/Error. "test")))) 12 | :default :skip) 13 | session {:id (atom 0) 14 | :value-cache (atom {})}] 15 | (when-not (= :skip value) 16 | (binding [rt/*session* session] 17 | (is (= 1 (#'rt/value->id value)) "un-hashable values should produce a mapping") 18 | (is (= 1 (count @(:value-cache session))) "un-hashable values only capture one-way mapping") 19 | (is (= 2 (#'rt/value->id value)) "future captures introduce a new mapping"))))) 20 | 21 | (deftest disambiguate-types 22 | (are [a b] 23 | (= (#'rt/value->key a) (#'rt/value->key b)) 24 | 25 | [] [] 26 | 27 | [1] [1] 28 | 29 | ^:one [1] ^:one [1] 30 | 31 | {:a ^:one [1]} 32 | {:a ^:one [1]}) 33 | 34 | (are [a b] 35 | (not= (#'rt/value->key a) (#'rt/value->key b)) 36 | 37 | [1] '(1) 38 | 39 | #{1 2 3} (sorted-set 1 2 3) 40 | 41 | ^{:one 1} [] ^{:two 2} [] 42 | 43 | {:a ^{:one 2} [1]} 44 | {:a ^{:two 2} [1]})) -------------------------------------------------------------------------------- /test/portal/test_clr.clj: -------------------------------------------------------------------------------- 1 | (ns portal.test-clr 2 | (:require [clojure.test :as t] 3 | [portal.client :as p] 4 | [portal.client-test] 5 | [portal.runtime-test] 6 | [portal.runtime.api-test] 7 | [portal.runtime.cson-test] 8 | [portal.runtime.edn-test] 9 | [portal.runtime.fs-test] 10 | [portal.runtime.json-buffer-test] 11 | [portal.runtime.npm-test] 12 | [portal.runtime.shell-test]) 13 | (:import (System Environment))) 14 | 15 | (defn run-tests [& tests] 16 | (if-not (p/enabled?) 17 | (apply t/run-tests tests) 18 | (let [report (atom []) 19 | counts 20 | (with-redefs [t/report #(swap! report conj %)] 21 | (apply t/run-tests tests))] 22 | (p/submit @report) 23 | counts))) 24 | 25 | (defn -main [] 26 | (let [{:keys [fail error]} 27 | (run-tests 28 | 'portal.client-test 29 | 'portal.runtime-test 30 | 'portal.runtime.api-test 31 | 'portal.runtime.cson-test 32 | 'portal.runtime.edn-test 33 | 'portal.runtime.fs-test 34 | 'portal.runtime.json-buffer-test 35 | 'portal.runtime.npm-test 36 | 'portal.runtime.shell-test)] 37 | (shutdown-agents) 38 | (Environment/Exit (+ fail error)))) 39 | -------------------------------------------------------------------------------- /test/portal/test_planck.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.test-planck 2 | (:require [cljs.test :refer [run-tests]] 3 | [clojure.pprint :as pp] 4 | [planck.core :refer [exit]] 5 | [planck.environ :refer [env]] 6 | [portal.client.planck :as p] 7 | [portal.runtime.bench-cson :as bench] 8 | [portal.runtime.cson-test])) 9 | 10 | (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] 11 | (when-not (cljs.test/successful? m) 12 | (exit 1))) 13 | 14 | (def port (:portal-port env)) 15 | 16 | (defn submit [value] (p/submit {:port port :encoding :cson} value)) 17 | 18 | (defn table [value] 19 | (if port 20 | (submit value) 21 | (pp/print-table 22 | (get-in (meta value) [:portal.viewer/table :columns]) 23 | value))) 24 | 25 | (defn -main [] 26 | (run-tests 'portal.runtime.cson-test) 27 | (table (bench/run))) 28 | -------------------------------------------------------------------------------- /test/portal/test_runner.clj: -------------------------------------------------------------------------------- 1 | (ns portal.test-runner 2 | (:require [clojure.test :as t] 3 | [portal.client :as p] 4 | [portal.client-test] 5 | [portal.runtime-test] 6 | [portal.runtime.api-test] 7 | [portal.runtime.cson-test] 8 | [portal.runtime.edn-test] 9 | [portal.runtime.fs-test] 10 | [portal.runtime.json-buffer-test] 11 | [portal.runtime.jvm.editor-test] 12 | [portal.runtime.npm-test] 13 | [portal.runtime.shell-test])) 14 | 15 | (defn run-tests [& tests] 16 | (if-not (p/enabled?) 17 | (apply t/run-tests tests) 18 | (let [report (atom []) 19 | counts 20 | (with-redefs [t/report #(swap! report conj %)] 21 | (apply t/run-tests tests))] 22 | (p/submit @report) 23 | counts))) 24 | 25 | (defn -main [] 26 | (let [{:keys [fail error]} 27 | (run-tests 'portal.client-test 28 | 'portal.runtime-test 29 | 'portal.runtime.api-test 30 | 'portal.runtime.cson-test 31 | 'portal.runtime.edn-test 32 | 'portal.runtime.fs-test 33 | 'portal.runtime.json-buffer-test 34 | 'portal.runtime.jvm.editor-test 35 | 'portal.runtime.npm-test 36 | 'portal.runtime.shell-test)] 37 | (shutdown-agents) 38 | (System/exit (+ fail error)))) 39 | -------------------------------------------------------------------------------- /test/portal/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.test-runner 2 | (:require [clojure.test :as t] 3 | [portal.async :as a] 4 | [portal.client :as p])) 5 | 6 | (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] 7 | (when-not (cljs.test/successful? m) 8 | (.exit js/process 1))) 9 | 10 | (defn run-tests [f] 11 | (if-not (p/enabled?) 12 | (f) 13 | (a/let [report (atom []) 14 | report' t/report] 15 | (set! t/report #(swap! report conj %)) 16 | (f) 17 | (set! t/report report') 18 | (p/submit @report) 19 | @report))) 20 | 21 | (defn run [f] 22 | (a/let [report (run-tests f) 23 | errors (count (filter (comp #{:fail} :type) report))] 24 | (.exit js/process errors))) 25 | -------------------------------------------------------------------------------- /test/portal/test_runtime_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.test-runtime-runner 2 | (:require [clojure.test :as t] 3 | [portal.client-test] 4 | [portal.runtime-test] 5 | [portal.runtime.api-test] 6 | [portal.runtime.cson-test] 7 | [portal.runtime.edn-test] 8 | [portal.runtime.fs-test] 9 | [portal.runtime.json-buffer-test] 10 | [portal.runtime.npm-test] 11 | [portal.runtime.shell-test] 12 | [portal.test-runner :as runner])) 13 | 14 | (defn -main []) 15 | 16 | (defn main! [] 17 | (runner/run 18 | #(t/run-tests 'portal.client-test 19 | 'portal.runtime-test 20 | 'portal.runtime.api-test 21 | 'portal.runtime.cson-test 22 | 'portal.runtime.edn-test 23 | 'portal.runtime.fs-test 24 | 'portal.runtime.json-buffer-test 25 | 'portal.runtime.npm-test 26 | 'portal.runtime.shell-test))) 27 | 28 | (main!) -------------------------------------------------------------------------------- /test/portal/test_ui_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.test-ui-runner 2 | (:require [clojure.test :as t] 3 | [portal.runtime.cson-test] 4 | [portal.runtime.edn-test] 5 | [portal.runtime.json-buffer-test] 6 | [portal.test-runner :as runner] 7 | [portal.ui.state-test])) 8 | 9 | (defn -main [] 10 | (runner/run 11 | #(t/run-tests 'portal.runtime.cson-test 12 | 'portal.runtime.edn-test 13 | 'portal.runtime.json-buffer-test 14 | 'portal.ui.state-test))) 15 | 16 | (-main) 17 | -------------------------------------------------------------------------------- /test/portal/ui/state_test.cljs: -------------------------------------------------------------------------------- 1 | (ns portal.ui.state-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [portal.ui.state :as state])) 4 | 5 | (deftest expanded-test 6 | (let [state nil 7 | context {:value :hi :stable-path [] :depth 1}] 8 | (is (nil? (state/expanded? state context)))) 9 | (let [context {:value :hi :stable-path [] :depth 1} 10 | location (state/get-location context) 11 | state {:expanded? {location 0}}] 12 | (is (false? (state/expanded? state context)))) 13 | (let [context {:value :hi :stable-path [] :depth 1} 14 | location (state/get-location context) 15 | state {:expanded? {location 1}}] 16 | (is (true? (state/expanded? state context)))) 17 | (let [parent {:value [:hi] :stable-path [] :depth 1} 18 | context {:value :hi :stable-path [0] :depth 2 :parent parent} 19 | location (state/get-location parent) 20 | state-a {:expanded? {location 1}} 21 | state-b {:expanded? {location 2}}] 22 | (is (nil? (state/expanded? state-a context))) 23 | (is (true? (state/expanded? state-b context))))) 24 | --------------------------------------------------------------------------------