diff --git a/.github/workflows/documenter.yml b/.github/workflows/documenter.yml index bd7b4d99..04c31ac1 100644 --- a/.github/workflows/documenter.yml +++ b/.github/workflows/documenter.yml @@ -14,11 +14,13 @@ jobs: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@latest with: - version: '1.6' + version: '1.10' + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 - name: Install dependencies run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key - run: julia --project=docs/ docs/make.jl \ No newline at end of file + run: julia --project=docs/ docs/make.jl diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index faac48f2..d1cafcd8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.6' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. + - '1.10' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. - '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia. - 'nightly' os: @@ -23,24 +23,15 @@ jobs: arch: - x64 steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: node-version: '16' - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 with: diff --git a/.gitignore b/.gitignore index cac0f566..dc8930f2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ coverage/ *.jl.cov *.jl.*.cov *.jl.mem +docs/src/assets/trivial_import.js # JavaScript dist diff --git a/Project.toml b/Project.toml index dea941d5..238495e5 100644 --- a/Project.toml +++ b/Project.toml @@ -13,33 +13,28 @@ Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Requires = "ae029012-a4dd-5104-9daa-d747884805df" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -WebSockets = "104b5d7c-a370-577a-8038-80a2059c5097" Widgets = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62" +[weakdeps] +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" +Mux = "a975b10e-0019-58db-a62f-e48ff68538c9" +WebSockets = "104b5d7c-a370-577a-8038-80a2059c5097" + +[extensions] +IJuliaExt = "IJulia" +MuxExt = "Mux" +WebSocketsExt = "WebSockets" + [compat] AssetRegistry = "0.1.0" FunctionalCollections = "0.5.0" -JSExpr = "0.5" +IJulia = "1.13" JSON = "0.18, 0.19, 0.20, 0.21" +Mux = "1" Observables = "0.5" -Requires = "0.4.4, 0.5, 1.0.0" WebSockets = "1.5.0, 1.6.0" Widgets = "0.6.2" -julia = "1.6" - -[extras] -Blink = "ad839575-38b3-5650-b840-f874b8c74a25" -Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" -DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" -JSExpr = "97c1335a-c9c5-57fe-bc5d-ec35cebe8660" -Mux = "a975b10e-0019-58db-a62f-e48ff68538c9" -NBInclude = "0db19996-df87-5ea3-a455-e3a50d440464" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +julia = "1.10" -[targets] -test = ["Blink", "Conda", "DataStructures", "Dates", "IJulia", "JSExpr", "Mux", "NBInclude", "Test"] diff --git a/deps/jupyter.jl b/deps/jupyter.jl deleted file mode 100644 index 73827031..00000000 --- a/deps/jupyter.jl +++ /dev/null @@ -1,11 +0,0 @@ -_error() = error( - "The WebIO Jupyter extension must be installed using Python or Conda. " * - "See https://juliagizmos.github.io/WebIO.jl/latest/providers/ijulia/ for more information." -) - - -find_jupyter_cmd(args...; kwargs...) = _error() -find_condajl_jupyter_cmd(args...; kwargs...) = _error() -install_jupyter_labextension(args...; kwargs...) = _error() -install_jupyter_nbextension(args...; kwargs...) = _error() -install_jupyter_serverextension(args...; kwargs...) = _error() diff --git a/docs/Project.toml b/docs/Project.toml index a7addacf..f6793099 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -3,3 +3,10 @@ Blink = "ad839575-38b3-5650-b840-f874b8c74a25" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" Mux = "a975b10e-0019-58db-a62f-e48ff68538c9" +WebIO = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29" + +[compat] +Blink = "0.12" +Documenter = "1.8" +IJulia = "1.13" +Mux = "1" diff --git a/docs/make.jl b/docs/make.jl index ab0ad020..57096cdf 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,12 +1,33 @@ -using Documenter +using Documenter, SHA using WebIO +# Copy or update trivial_import.js +docs_trivial_path = joinpath(@__DIR__, "src/assets/trivial_import.js") +test_trivial_path = joinpath(@__DIR__, "../test/assets/trivial_import.js") +if isfile(docs_trivial_path) + test_sha = open(test_trivial_path) do io + sha1(io) + end + docs_sha = open(docs_trivial_path) do io + sha1(io) + end + if test_sha != docs_sha + cp(test_trivial_path, docs_trivial_path; force=true) + end +else + mkdir(dirname(docs_trivial_path)) + cp(test_trivial_path, docs_trivial_path) +end + # We have to ensure that these modules are loaded because some functions are # defined behind @require guards. using IJulia, Mux, Blink +DocMeta.setdocmeta!(WebIO, :DocTestSetup, :(using WebIO); recursive=true) + makedocs( sitename="WebIO", + warnonly=true, format=Documenter.HTML(), modules=[WebIO], pages=[ diff --git a/docs/src/assets/trivial_import.js b/docs/src/assets/trivial_import.js new file mode 100644 index 00000000..62774217 --- /dev/null +++ b/docs/src/assets/trivial_import.js @@ -0,0 +1,14 @@ +// A trivial importable module that simply produces "ok". +// Used for testing the WebIO Scope imports machinery. + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.amdWeb = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + return {x: "ok"} +})); diff --git a/src/providers/ijulia.jl b/ext/IJuliaExt.jl similarity index 86% rename from src/providers/ijulia.jl rename to ext/IJuliaExt.jl index c64eb40e..9e09bdb3 100644 --- a/src/providers/ijulia.jl +++ b/ext/IJuliaExt.jl @@ -1,6 +1,8 @@ -using .AssetRegistry -using .Sockets -using .WebIO +module IJuliaExt + +using WebIO +using WebIO: WEBIO_NODE_MIME +using IJulia, Sockets struct IJuliaConnection <: AbstractConnection comm::IJulia.CommManager.Comm @@ -68,16 +70,18 @@ function main() return end - # https://github.com/JuliaLang/IJulia.jl/pull/755 - if isdefined(IJulia, :register_jsonmime) - IJulia.register_jsonmime(WEBIO_NODE_MIME()) - else - @warn "IJulia doesn't have register_mime; WebIO may not work as expected. Please upgrade to IJulia v1.13.0 or greater." - end + IJulia.register_jsonmime(WEBIO_NODE_MIME()) # See comment on _IJuliaInit for what this does display(_IJuliaInit()) + + return nothing end WebIO.setup_provider(::Val{:ijulia}) = main() # calling setup_provider(Val(:ijulia)) will display the setup javascript -WebIO.setup(:ijulia) + +function __init__() + WebIO.setup(:ijulia) +end + +end diff --git a/src/providers/mux.jl b/ext/MuxExt.jl similarity index 81% rename from src/providers/mux.jl rename to ext/MuxExt.jl index eaa9e90e..00f42f20 100644 --- a/src/providers/mux.jl +++ b/ext/MuxExt.jl @@ -1,23 +1,26 @@ -using .JSON -using .AssetRegistry -using .Sockets -using .Base64: stringmime -export webio_serve +module MuxExt + +using WebIO +using WebIO: MUX_BUNDLE_PATH + +using Mux, JSON, AssetRegistry +using Sockets +using Base64: stringmime """ webio_serve(app, port=8000) Serve a Mux app which might return a WebIO node. """ -function webio_serve(app, args...) +function WebIO.webio_serve(app, args...) http = Mux.App(Mux.mux( Mux.defaults, app, Mux.notfound() )) - webio_serve(http, args...) + WebIO.webio_serve(http, args...) end -function webio_serve(app::Mux.App, args...) +function WebIO.webio_serve(app::Mux.App, args...) websock = Mux.App(Mux.mux( Mux.wdefaults, Mux.route("/webio-socket", create_socket), @@ -53,7 +56,9 @@ end Base.isopen(p::WebSockConnection) = isopen(p.sock) -Mux.Response(o::AbstractWidget) = Mux.Response(Widgets.render(o)) +# TYPE-PIRACY +# Mux.Response(o::AbstractWidget) = Mux.Response(Widgets.render(o)) + function Mux.Response(content::Union{Node, Scope}) script_url = try AssetRegistry.register(MUX_BUNDLE_PATH) @@ -86,4 +91,9 @@ function WebIO.register_renderable(::Type{T}, ::Val{:mux}) where {T} end WebIO.setup_provider(::Val{:mux}) = nothing # Mux setup has no side-effects -WebIO.setup(:mux) + +function __init__() + WebIO.setup(:mux) +end + +end diff --git a/src/providers/generic_http.jl b/ext/WebSocketsExt.jl similarity index 84% rename from src/providers/generic_http.jl rename to ext/WebSocketsExt.jl index def98ed9..f7a42174 100644 --- a/src/providers/generic_http.jl +++ b/ext/WebSocketsExt.jl @@ -1,9 +1,12 @@ -using .Sockets -import .AssetRegistry, .JSON -using .WebIO -using .WebSockets: is_upgrade, upgrade, writeguarded -using .WebSockets: HTTP +module WebSocketsExt +using WebIO, JSON, AssetRegistry +using Sockets + +using WebIO: WEBIO_APPLICATION_MIME, GENERIC_HTTP_BUNDLE_PATH, bundle_key, + global_server_config, WebIOServer, singleton_instance, routing_callback, + webio_server_config +using WebSockets: WebSockets, HTTP, is_upgrade, upgrade, writeguarded struct WSConnection{T} <: WebIO.AbstractConnection sock::T @@ -12,15 +15,7 @@ end Sockets.send(p::WSConnection, data) = writeguarded(p.sock, JSON.json(data)) Base.isopen(p::WSConnection) = isopen(p.sock) -if !isfile(GENERIC_HTTP_BUNDLE_PATH) - error( - "Unable to find WebIO JavaScript bundle for generic HTTP provider; " - * "try rebuilding WebIO (via `Pkg.build(\"WebIO\")`)." - ) -end -const bundle_key = AssetRegistry.register(GENERIC_HTTP_BUNDLE_PATH) - -include(joinpath(@__DIR__, "..", "..", "deps", "mimetypes.jl")) +include(joinpath(@__DIR__, "..", "deps", "mimetypes.jl")) """ Serve an asset from the asset registry. @@ -52,17 +47,8 @@ function websocket_handler(ws) end end -struct WebIOServer{S} - server::S - serve_task::Task -end - kill!(server::WebIOServer) = put!(server.server.in, HTTP.Servers.KILL) -const singleton_instance = Ref{WebIOServer}() - -const routing_callback = Ref{Any}((req)-> missing) - """ Run the WebIO server. @@ -84,7 +70,7 @@ end server = WebIO.WebIOServer(serve_app, verbose = true) ``` """ -function WebIOServer( +function WebIO.WebIOServer( default_response::Function = (req)-> missing; baseurl::String = "127.0.0.1", http_port::Int = 8081, verbose = false, singleton = true, @@ -111,7 +97,7 @@ function WebIOServer( else # relative url string("http://", baseurl, ":", http_port, WebIO.baseurl[]) end - string(base, bundle_key) + string(base, bundle_key[]) end wait_time = 5; start = time() # wait for max 5 s while time() - start < wait_time @@ -127,24 +113,22 @@ function WebIOServer( return singleton_instance[] end -const webio_server_config = Ref{typeof((url = "", bundle_url = "", http_port = 0, ws_url = ""))}() - """ Fetches the global configuration for our http + websocket server from environment variables. It will memoise the result, so after a first call, any update to the environment will get ignored. """ -function global_server_config() +function WebIO.global_server_config() if !isassigned(webio_server_config) - setbaseurl!(get(ENV, "JULIA_WEBIO_BASEURL", "")) + WebIO.setbaseurl!(get(ENV, "JULIA_WEBIO_BASEURL", "")) url = get(ENV, "WEBIO_SERVER_HOST_URL", "127.0.0.1") http_port = parse(Int, get(ENV, "WEBIO_HTTP_PORT", "8081")) ws_default = string("ws://", url, ":", http_port, "/webio_websocket/") ws_url = get(ENV, "WEBIO_WEBSOCKT_URL", ws_default) # make it possible, to e.g. host the bundle online - bundle_url = get(ENV, "WEBIO_BUNDLE_URL", string(WebIO.baseurl[], bundle_key)) + bundle_url = get(ENV, "WEBIO_BUNDLE_URL", string(WebIO.baseurl[], bundle_key[])) webio_server_config[] = ( url = url, bundle_url = bundle_url, http_port = http_port, ws_url = ws_url @@ -176,3 +160,15 @@ function Base.show(io::IO, m::WEBIO_APPLICATION_MIME, app::Application) show(io, "text/html", app) return end + +function __init__() + if !isfile(GENERIC_HTTP_BUNDLE_PATH) + error( + "Unable to find WebIO JavaScript bundle for generic HTTP provider; " + * "try rebuilding WebIO (via `Pkg.build(\"WebIO\")`)." + ) + end + bundle_key[] = AssetRegistry.register(GENERIC_HTTP_BUNDLE_PATH) +end + +end diff --git a/src/WebIO.jl b/src/WebIO.jl index 35fd2441..d09e8c10 100644 --- a/src/WebIO.jl +++ b/src/WebIO.jl @@ -1,7 +1,6 @@ module WebIO using Observables -using Requires using AssetRegistry using Base64: stringmime import Widgets @@ -49,8 +48,6 @@ include("rpc.jl") # Extra "non-core" functionality include("devsetup.jl") -include("../deps/jupyter.jl") - """ setup_provider(s::Union{Symbol, AbstractString}) @@ -88,41 +85,30 @@ function setup(provider::Symbol) end setup(provider::AbstractString) = setup(Symbol(provider)) -function prefetch_provider_file(basename) - filepath = joinpath(@__DIR__, "providers", basename) - code = read(filepath, String) - (file = filepath, code = code) +struct WebIOServer{S} + server::S + serve_task::Task + + function WebIOServer(server::S, serve_task::Task) where S + # based on assert_extension from longemen3000/ExtensionsExt + ext = Base.get_extension(@__MODULE__, :WebSocketsExt) + if isnothing(ext) + throw(error("Extension `WebSocketsExt` must be loaded to construct the type `WebIOServer`.")) + end + return new{S}(server, serve_task) + end end -provider_mux = prefetch_provider_file("mux.jl") -provider_blink = prefetch_provider_file("blink.jl") -provider_ijulia = prefetch_provider_file("ijulia.jl") -provider_generic_http = prefetch_provider_file("generic_http.jl") +function webio_serve end +function global_server_config end + +const bundle_key = Ref{String}() +const singleton_instance = Ref{WebIOServer}() +const routing_callback = Ref{Any}((req) -> missing) +const webio_server_config = Ref{typeof((url = "", bundle_url = "", http_port = 0, ws_url = ""))}() function __init__() push!(Observables.addhandler_callbacks, WebIO.setup_comm) - @require Mux="a975b10e-0019-58db-a62f-e48ff68538c9" begin - include_string(@__MODULE__, provider_mux.code, provider_mux.file) - end - @require Blink="ad839575-38b3-5650-b840-f874b8c74a25" begin - # The latest version of Blink defines their own WebIO integration - # (after https://github.com/JunoLab/Blink.jl/pull/201). - if isdefined(Blink.AtomShell, :initwebio!) - return - end - Base.depwarn( - "Please upgrade Blink for a smoother integration with WebIO.", - :webio_blink_upgrade, - ) - include_string(@__MODULE__, provider_blink.code, provider_blink.file) - end - @require IJulia="7073ff75-c697-5162-941a-fcdaad2a7d2a" begin - include_string(@__MODULE__, provider_ijulia.code, provider_ijulia.file) - end - @require WebSockets="104b5d7c-a370-577a-8038-80a2059c5097" begin - include_string(@__MODULE__, provider_generic_http.code, provider_generic_http.file) - end - end end # module diff --git a/src/devsetup.jl b/src/devsetup.jl index 11297b80..43191425 100644 --- a/src/devsetup.jl +++ b/src/devsetup.jl @@ -1,7 +1,3 @@ -function devsetup() - Base.depwarn("WebIO.devsetup() is no longer required. You can simply run WebIO.bundlejs().", :webio_devesetup) -end - function bundlejs() include(joinpath(@__DIR__, "..", "deps", "_bundlejs.jl")) end diff --git a/src/providers/blink.jl b/src/providers/blink.jl deleted file mode 100644 index a8e49d0d..00000000 --- a/src/providers/blink.jl +++ /dev/null @@ -1,46 +0,0 @@ -# This is deprecated and will be removed soon. -# WebIO+Blink integration is now implemented in Blink.jl. -# https://github.com/JunoLab/Blink.jl/pull/201 - -using .AssetRegistry -using .Base64: stringmime - -using .Sockets - -struct BlinkConnection <: WebIO.AbstractConnection - page::Blink.Page -end - -Blink.body!(p::Blink.Page, x::AbstractWidget) = Blink.body!(p, Widgets.render(x)) -function Blink.body!(p::Blink.Page, x::Union{Node, Scope}) - wait(p) - - bs = AssetRegistry.register(BLINK_BUNDLE_PATH) - Blink.loadjs!(p, bs) - - conn = BlinkConnection(p) - Blink.handle(p, "webio") do msg - WebIO.dispatch(conn, msg) - end - - Blink.body!(p, stringmime(MIME"text/html"(), x)) -end - -Blink.body!(p::Blink.Window, x::AbstractWidget) = Blink.body!(p, Widgets.render(x)) -function Blink.body!(p::Blink.Window, x::Union{Node, Scope}) - Blink.body!(p.content, x) -end - -function Sockets.send(b::BlinkConnection, data) - Blink.msg(b.page, Dict(:type=>"webio", :data=>data)) -end - -Base.isopen(b::BlinkConnection) = Blink.active(b.page) - -function WebIO.register_renderable(T::Type, ::Val{:blink}) - eval(:(Blink.body!(p::Union{Blink.Window, Blink.Page}, x::$T) = - Blink.body!(p, WebIO.render(x)))) -end - -WebIO.setup_provider(::Val{:blink}) = nothing # blink setup has no side-effects -WebIO.setup(:blink) diff --git a/src/syntax.jl b/src/syntax.jl index e91fe37b..193ff811 100644 --- a/src/syntax.jl +++ b/src/syntax.jl @@ -1,6 +1,6 @@ using JSON -export @dom_str, @js, @js_str +export @dom_str, @js_str # adapted from Hiccup.jl function cssparse(s) diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 00000000..7f83262d --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,14 @@ +[deps] +Blink = "ad839575-38b3-5650-b840-f874b8c74a25" +Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" +JSExpr = "97c1335a-c9c5-57fe-bc5d-ec35cebe8660" +Mux = "a975b10e-0019-58db-a62f-e48ff68538c9" +NBInclude = "0db19996-df87-5ea3-a455-e3a50d440464" +Observables = "510215fc-4207-5dde-b226-833fc4488ee2" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +WebSockets = "104b5d7c-a370-577a-8038-80a2059c5097" diff --git a/test/blink-tests.jl b/test/blink-tests.jl index c673f233..ed3e33ff 100644 --- a/test/blink-tests.jl +++ b/test/blink-tests.jl @@ -90,15 +90,11 @@ w = open_window() end @testset "global URL, no http:" begin - # TODO: change this to a permanent URL because this CSAIL account - # will eventually expire. - @test scope_import(w, "//people.csail.mit.edu/rdeits/webio_tests/trivial_import.js", use_iframe) == "ok" + @test scope_import(w, "//juliagizmos.github.io/WebIO.jl/dev/assets/trivial_import.js", use_iframe) == "ok" end @testset "global URL, with http:" begin - # TODO: change this to a permanent URL because this CSAIL account - # will eventually expire. - @test scope_import(w, "http://people.csail.mit.edu/rdeits/webio_tests/trivial_import.js", use_iframe) == "ok" + @test scope_import(w, "http://juliagizmos.github.io/WebIO.jl/dev/assets/trivial_import.js", use_iframe) == "ok" end end end diff --git a/test/http-tests.jl b/test/http-tests.jl index bd6ea73f..1c85ee23 100644 --- a/test/http-tests.jl +++ b/test/http-tests.jl @@ -7,6 +7,6 @@ using Test @test occursin("_webIOWebSocketURL = ", output) @test occursin("ws://127.0.0.1:8081/webio_websocket/", output) @test occursin("""hello, world""", output) - @test WebIO.webio_server_config[] == (url = "127.0.0.1", bundle_url = WebIO.bundle_key, http_port = 8081, ws_url = "ws://127.0.0.1:8081/webio_websocket/") + @test WebIO.webio_server_config[] == (url = "127.0.0.1", bundle_url = WebIO.bundle_key[], http_port = 8081, ws_url = "ws://127.0.0.1:8081/webio_websocket/") @test isassigned(WebIO.singleton_instance) end