├── .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 | [](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 | 
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 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
20 |
21 | ### [`:portal.colors/solarized-dark`](https://ethanschoonover.com/solarized/)
22 |
23 | 
24 |
25 | ### [`:portal.colors/solarized-light`](https://ethanschoonover.com/solarized/)
26 |
27 | 
28 |
29 | ### `:portal.colors/material-ui`
30 |
31 | 
32 |
33 | ### [`:portal.colors/zerodark`](https://github.com/NicolasPetton/zerodark-theme)
34 |
35 | 
36 |
37 | ### [`:portal.colors/gruvbox`](https://github.com/morhetz/gruvbox)
38 |
39 | 
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 |
6 |
7 |
8 | ## 2023-04-06 - Clojure visual-tools meeting 17 - Various Updates
9 |
10 |
11 |
13 |
14 |
15 | ## 2022-12-07 - Tapping into one of Clojure's superpowers with Portal by James Trunk
16 |
17 |
18 |
20 |
21 |
22 | ## 2022-12-01 - Clojure visual-tools meeting 16 - Calva Notebooks & Portal
23 |
24 |
25 |
27 |
28 |
29 | ## 2022-08-08 - JUXT Safari - Portal, Developing with Data with Chris Williams
30 |
31 |
32 |
34 |
35 |
36 | ## 2022-04-13 - Meetup: Collaborative Learning - Portal
37 |
38 |
39 |
41 |
42 |
43 | ## 2021-11-23 - Thinking with Portal (by Chris Badahdah)
44 |
45 |
46 |
48 |
49 |
50 | ## 2020-10-15 - Apropos 14
51 |
52 |
53 |
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 | 
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 |
--------------------------------------------------------------------------------