├── .cargo └── config.toml ├── .github ├── dependabot.yaml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── Makefile.toml ├── Package.swift ├── README.md ├── crates ├── core │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ └── streaming.rs │ ├── liveview-native-core-jetpack │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ ├── core │ │ │ ├── .gitignore │ │ │ ├── build.gradle.kts │ │ │ ├── lint.xml │ │ │ ├── proguard-rules.pro │ │ │ └── src │ │ │ │ ├── androidTest │ │ │ │ └── java │ │ │ │ │ └── org │ │ │ │ │ └── phoenixframework │ │ │ │ │ └── liveview_jetpack │ │ │ │ │ └── ExampleInstrumentedTest.kt │ │ │ │ ├── main │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── phoenixframework │ │ │ │ └── liveview_jetpack │ │ │ │ └── DocumentTest.kt │ │ ├── gradle.properties │ │ ├── gradle │ │ │ ├── libs.versions.toml │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── scripts │ │ │ └── prepareJitpackEnvironment.sh │ │ └── settings.gradle.kts │ ├── liveview-native-core-swift │ │ ├── Benchmarks │ │ │ └── LiveViewNativeCore │ │ │ │ └── Merging.swift │ │ ├── Sources │ │ │ └── LiveViewNativeCore │ │ │ │ ├── .gitignore │ │ │ │ └── Support.swift │ │ └── Tests │ │ │ └── LiveViewNativeCoreTests │ │ │ ├── LiveViewNativeCoreSocketTests.swift │ │ │ └── LiveViewNativeCoreTests.swift │ ├── src │ │ ├── callbacks.rs │ │ ├── client │ │ │ ├── config.rs │ │ │ ├── inner │ │ │ │ ├── connected_client.rs │ │ │ │ ├── cookie_store.rs │ │ │ │ ├── event_loop.rs │ │ │ │ ├── logging.rs │ │ │ │ ├── mod.rs │ │ │ │ └── navigation.rs │ │ │ ├── mod.rs │ │ │ └── tests │ │ │ │ ├── lifecycle.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── streaming.rs │ │ │ │ └── upload.rs │ │ ├── diff │ │ │ ├── fragment │ │ │ │ ├── error.rs │ │ │ │ ├── merge.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── render.rs │ │ │ │ ├── tests │ │ │ │ │ ├── flow-1-change-0.html │ │ │ │ │ ├── flow-1-change-0.json │ │ │ │ │ ├── flow-1-change-1.html │ │ │ │ │ ├── flow-1-change-1.json │ │ │ │ │ ├── flow-1-change-2.html │ │ │ │ │ ├── flow-1-change-2.json │ │ │ │ │ ├── flow-1-change-3.html │ │ │ │ │ ├── flow-1-change-3.json │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── stream.rs │ │ │ │ └── wasm.rs │ │ │ ├── mod.rs │ │ │ ├── morph.rs │ │ │ ├── patch.rs │ │ │ └── traversal.rs │ │ ├── dom │ │ │ ├── attribute.rs │ │ │ ├── ffi.rs │ │ │ ├── mod.rs │ │ │ ├── node.rs │ │ │ ├── parser.rs │ │ │ ├── printer.rs │ │ │ └── select.rs │ │ ├── error │ │ │ ├── mod.rs │ │ │ └── socket.rs │ │ ├── lib.rs │ │ ├── live_socket │ │ │ ├── channel.rs │ │ │ ├── mod.rs │ │ │ ├── navigation │ │ │ │ ├── ffi.rs │ │ │ │ └── mod.rs │ │ │ ├── socket.rs │ │ │ └── tests │ │ │ │ ├── error.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── navigation.rs │ │ │ │ ├── streaming.rs │ │ │ │ └── upload.rs │ │ └── protocol.rs │ ├── tests │ │ ├── bindings │ │ │ ├── simple.kts │ │ │ └── simple.swift │ │ ├── diff.rs │ │ ├── dom.rs │ │ ├── fixtures │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── attr-value-empty-string │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── change-tagname-ids │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── change-tagname │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── change-types │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── data-table │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── data-table2 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── equal │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── id-change-tag-name │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-nested-2 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-nested-3 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-nested-4 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-nested-5 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-nested-6 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-nested-7 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-nested │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── ids-prepend │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── input-element-disabled │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── input-element-enabled │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── input-element │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── large │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── lengthen │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── one │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── reverse-ids │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── reverse │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── select-element-default │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── select-element │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── shorten │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── simple-ids │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── simple-text-el │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── simple │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── svg-append-new │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── svg-append │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── svg-no-default-namespace │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── svg-xlink │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── svg │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── tag-to-text │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── text-to-tag │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── text-to-text │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── textarea │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── todomvc │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ ├── todomvc2 │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ │ └── two │ │ │ │ ├── from.html │ │ │ │ └── to.html │ │ ├── parser.rs │ │ ├── support │ │ │ └── tinycross.png │ │ └── uniffi.rs │ └── uniffi.toml ├── uniffi-bindgen │ ├── Cargo.toml │ └── src │ │ └── main.rs └── wasm │ ├── Cargo.toml │ ├── npm_shims │ └── jest_mock.js │ ├── scripts │ ├── build.sh │ └── jest_tests.sh │ └── src │ └── lib.rs ├── jitpack.yml ├── rustfmt.toml └── tests └── support └── test_server ├── .formatter.exs ├── .gitignore ├── README.md ├── action.yml ├── assets ├── css │ └── app.css ├── js │ └── app.js └── tailwind.config.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── lib ├── test_server.ex ├── test_server │ ├── application.ex │ ├── mailer.ex │ ├── song_publisher.ex │ └── songs.ex ├── test_server_native.ex ├── test_server_web.ex └── test_server_web │ ├── components │ ├── core_components.ex │ ├── core_components.swiftui.ex │ ├── layouts.ex │ ├── layouts.jetpack.ex │ ├── layouts.swiftui.ex │ ├── layouts │ │ ├── app.html.heex │ │ └── root.html.heex │ ├── layouts_jetpack │ │ ├── app.jetpack.neex │ │ └── root.jetpack.neex │ └── layouts_swiftui │ │ ├── app.swiftui.neex │ │ └── root.swiftui.neex │ ├── controllers │ ├── error_html.ex │ ├── error_json.ex │ ├── page_controller.ex │ ├── page_html.ex │ └── page_html │ │ └── home.html.heex │ ├── endpoint.ex │ ├── gettext.ex │ ├── live │ ├── cookie.ex │ ├── hello.ex │ ├── nav.ex │ ├── push_navigation.ex │ ├── redirect_from.ex │ ├── redirect_to.ex │ ├── simple_stream.ex │ ├── thermostat_live.ex │ └── upload_live.ex │ ├── router.ex │ ├── styles │ ├── app.jetpack.ex │ └── app.swiftui.ex │ └── telemetry.ex ├── mix.exs ├── mix.lock ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── static │ ├── favicon.ico │ ├── images │ └── logo.svg │ ├── robots.txt │ └── uploads │ └── .gitignore └── test ├── support └── conn_case.ex ├── test_helper.exs └── test_server_web └── controllers ├── error_html_test.exs ├── error_json_test.exs └── page_controller_test.exs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-apple-ios-sim] 2 | runner = "cargo dinghy -p auto-ios-aarch64-sim runner --" 3 | 4 | [target.aarch64-apple-visionos-sim] 5 | runner = "cargo dinghy -p auto-visionos-aarch64-sim runner --" 6 | 7 | [target.x86_64-apple-ios] 8 | runner = "cargo dinghy -p auto-ios-x86_64 runner --" 9 | 10 | # Running on device requires doing the steps in 11 | # https://github.com/sonos/dinghy/blob/main/docs/ios.md#additional-requirements 12 | [target.aarch64-apple-ios] 13 | runner = "cargo dinghy -p auto-ios-aarch64 runner --" 14 | 15 | # These target runners are added in https://github.com/sonos/dinghy/pull/203 16 | [target.aarch64-apple-tvos-sim] 17 | runner = "cargo dinghy -p auto-tvos-aarch64-sim runner --" 18 | 19 | [target.x86_64-apple-tvos] 20 | runner = "cargo dinghy -p auto-tvos-x86_64 runner --" 21 | 22 | [target.x86_64-apple-watchos-sim] 23 | runner = "cargo dinghy -p auto-watchos-x86_64-sim runner --" 24 | 25 | [target.aarch64-apple-watchos-sim] 26 | runner = "cargo dinghy -p auto-watchos-aarch64-sim runner --" 27 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | ignore: 10 | - dependency-name: "*" 11 | update-types: ["version-update:semver-patch"] # ignore patch updates 12 | 13 | # Maintain dependencies for Cargo 14 | - package-ecosystem: "cargo" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | ignore: 19 | - dependency-name: "*" 20 | update-types: ["version-update:semver-patch"] # ignore patch updates 21 | 22 | - package-ecosystem: "mix" 23 | directory: "/tests/support/test_server/" 24 | schedule: 25 | interval: "daily" 26 | 27 | - package-ecosystem: "swift" 28 | directory: "/" 29 | schedule: 30 | interval: "daily" 31 | ignore: 32 | - dependency-name: "*" 33 | update-types: ["version-update:semver-patch"] # ignore patch updates 34 | 35 | - package-ecosystem: "gradle" 36 | directory: "/crates/core/liveview-native-core-jetpack/" 37 | schedule: 38 | interval: "daily" 39 | ignore: 40 | - dependency-name: "*" 41 | update-types: ["version-update:semver-patch"] # ignore patch updates 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # will have compiled files and executables 2 | /target/ 3 | 4 | ### These are backup files generated by rustfmt 5 | **/*.rs.bk 6 | 7 | ### VisualStudioCode ### 8 | .vscode/* 9 | !.vscode/settings.json 10 | !.vscode/tasks.json 11 | !.vscode/launch.json 12 | !.vscode/extensions.json 13 | 14 | ### CLion ### 15 | .idea 16 | 17 | ### OSX ### 18 | .DS_Store 19 | 20 | ### Elixir 21 | /.elixir_ls 22 | 23 | ### From `cargo make uniffi-swift-test-visionos` 24 | xcode-build/ 25 | 26 | ### From `cargo make uniffi-swift-tests 27 | Package.resolved 28 | .build 29 | 30 | ### from wasm builds and tests 31 | crates/wasm/phoenix_live_view 32 | crates/wasm/liveview-native-core-wasm-* 33 | crates/wasm/liveview-native-core-wasm-*.tgz 34 | 35 | ### When vendoring in as a swift package 36 | .swiftpm 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceFolder}/", 12 | "args": [], 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "./crates/core/Cargo.toml" 4 | ] 5 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liveview-native/liveview-native-core/2f0e874960ee93b5b76daff2ecfb6d8e445e15bc/CHANGELOG.md -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crates/core", "crates/uniffi-bindgen", "crates/wasm"] 4 | 5 | [workspace.package] 6 | version = "0.4.1-rc-3" 7 | rust-version = "1.64" 8 | authors = [ 9 | "Paul Schoenfelder ", 10 | "Thomas Sieverding ", 11 | "Elle Imhoff ", 12 | ] 13 | description = "Provides core, cross-platform functionality for LiveView Native implementations" 14 | repository = "https://github.com/liveview-native/liveview-native-core" 15 | homepage = "https://github.com/liveview-native/liveview-native-core" 16 | documentation = "https://github.com/liveview-native/liveview-native-core" 17 | categories = ["API bindings", "GUI"] 18 | keywords = ["liveview", "phoenix"] 19 | license = "MIT" 20 | license-file = "LICENSE.md" 21 | readme = "README.md" 22 | edition = "2021" 23 | publish = false 24 | 25 | [workspace.dependencies] 26 | uniffi = "0.28.3" 27 | 28 | [profile.dev] 29 | split-debuginfo = "unpacked" 30 | debug = 2 31 | 32 | [profile.release] 33 | opt-level = 'z' # Optimize for size 34 | lto = true # Enable link-time optimization 35 | codegen-units = 1 # Reduce number of codegen units to increase optimizations 36 | strip = 'debuginfo' # Strip symbols from binary* 37 | split-debuginfo = "packed" 38 | debug = 1 39 | 40 | [patch.crates-io] 41 | # https://github.com/rust-lang/socket2/pull/503 42 | socket2 = { git = "https://github.com/rust-lang/socket2.git", rev = "3a938932829ea6ee3025d2d7a86c7b095c76e6c3" } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 DockYard, Inc. 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | import class Foundation.ProcessInfo 7 | 8 | let liveview_native_core_framework: Target 9 | 10 | // To release, toggle this to `false` 11 | let useLocalFramework = true 12 | if useLocalFramework { 13 | liveview_native_core_framework = .binaryTarget( 14 | name: "liveview_native_core", 15 | path: "./target/uniffi/swift/liveview_native_core.xcframework" 16 | ) 17 | } else { 18 | let releaseTag = "0.4.1-rc-3" 19 | let releaseChecksum = "f3972f4d40732c884c98426b28550376abaff20a3490b73367ad170f1f0bcca9" 20 | liveview_native_core_framework = .binaryTarget( 21 | name: "liveview_native_core", 22 | url: 23 | "https://github.com/liveview-native/liveview-native-core/releases/download/\(releaseTag)/liveview_native_core.xcframework.zip", 24 | checksum: releaseChecksum 25 | ) 26 | } 27 | 28 | let package = Package( 29 | name: "LiveViewNativeCore", 30 | platforms: [ 31 | .iOS("16.0"), 32 | .macOS("13.0"), 33 | .watchOS("9.0"), 34 | .tvOS("16.0"), 35 | ], 36 | products: [ 37 | .library( 38 | name: "LiveViewNativeCore", 39 | targets: [ 40 | "liveview_native_core", 41 | "LiveViewNativeCore", 42 | ] 43 | ) 44 | ], 45 | dependencies: [ 46 | // This is used to generate documentation vio `swift package generate-documentation` 47 | // This doesn't work because of: 48 | // https://github.com/apple/swift-docc-plugin/issues/50 will hopefully resolve it 49 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") 50 | ], 51 | targets: [ 52 | liveview_native_core_framework, 53 | .target( 54 | name: "LiveViewNativeCore", 55 | dependencies: [ 56 | .target(name: "liveview_native_core") 57 | ], 58 | path: "./crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/" 59 | ), 60 | .testTarget( 61 | name: "LiveViewNativeCoreTests", 62 | dependencies: [ 63 | "LiveViewNativeCore" 64 | ], 65 | path: "./crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/" 66 | ), 67 | ] 68 | ) 69 | 70 | let environment = ProcessInfo.processInfo.environment 71 | let runBenchmarks = environment["RUN_BENCHMARKS"] != nil 72 | if runBenchmarks { 73 | package.dependencies.append( 74 | .package( 75 | url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.0.0"))) 76 | package.targets.append( 77 | .executableTarget( 78 | name: "LiveViewNativeCoreBenchmarks", 79 | dependencies: [ 80 | .product(name: "Benchmark", package: "package-benchmark"), 81 | "LiveViewNativeCore", 82 | ], 83 | path: "./crates/core/liveview-native-core-swift/Benchmarks/LiveViewNativeCore", 84 | plugins: [ 85 | .plugin(name: "BenchmarkPlugin", package: "package-benchmark") 86 | ]) 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /crates/core/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "liveview-native-core" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | repository.workspace = true 8 | homepage.workspace = true 9 | documentation.workspace = true 10 | categories.workspace = true 11 | keywords.workspace = true 12 | license.workspace = true 13 | readme.workspace = true 14 | publish.workspace = true 15 | 16 | 17 | [lib] 18 | crate-type = ["staticlib", "rlib", "cdylib"] 19 | name = "liveview_native_core" 20 | 21 | [features] 22 | default = ["liveview-channels-tls"] 23 | liveview-channels = [ 24 | "phoenix_channels_client", 25 | "reqwest", 26 | "uniffi/tokio", 27 | "cookie_store", 28 | "reqwest_cookie_store", 29 | "tokio", 30 | "openssl", 31 | ] 32 | liveview-channels-tls = [ 33 | "liveview-channels", 34 | "reqwest/native-tls-vendored", 35 | "phoenix_channels_client/native-tls", 36 | "phoenix_channels_client/default", 37 | ] 38 | 39 | # This is for support of phoenix-channnels-client in for wasm. 40 | browser = [ 41 | #"liveview-channels", 42 | #"phoenix_channels_client/browser", 43 | ] 44 | 45 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 46 | # 47 | [target.'cfg(target_os = "android")'.dependencies] 48 | android_logger = "0.15" 49 | openssl = { version = "=0.10.71", features = ["vendored"], optional = true } 50 | 51 | [target.'cfg(target_vendor = "apple")'.dependencies] 52 | oslog = "0.2.0" 53 | 54 | [dependencies] 55 | serde_urlencoded = "0.7.1" 56 | cranelift-entity = { version = "0.118" } 57 | fixedbitset = { version = "0.5.7" } 58 | fxhash = { version = "0.2" } 59 | html5gum = { git = "https://github.com/liveviewnative/html5gum", branch = "lvn" } 60 | petgraph = { version = "0.7.1", default-features = false, features = [ 61 | "graphmap", 62 | ] } 63 | futures = "0.3.31" 64 | serde = { version = "1.0", features = ["derive"] } 65 | serde_json = { version = "1.0" } 66 | smallstr = { version = "0.3", features = ["union"] } 67 | smallvec = { version = "1.14", features = ["union", "const_generics"] } 68 | thiserror = "2.0" 69 | log = "0.4" 70 | tokio-util = "0.7.3" 71 | reqwest = { version = "0.12.12", default-features = false, optional = true, features = [ 72 | "cookies", 73 | ] } 74 | cookie_store = { version = "0.21.1", default-features = false, optional = true } 75 | reqwest_cookie_store = { version = "0.8.0", default-features = false, optional = true } 76 | env_logger = "0.11.1" 77 | uniffi = { workspace = true } 78 | 79 | tokio = { version = "1.44.2", features = ["full"], optional = true } 80 | phoenix_channels_client = { git = "https://github.com/liveview-native/phoenix-channels-client.git", branch = "main", optional = true, default-features = false } 81 | # This is for wasm support on phoenix-channels-client 82 | #phoenix_channels_client = { git = "https://github.com/liveview-native/phoenix-channels-client.git", branch = "simlay/webassembly-support", optional = true, default-features = false } 83 | 84 | [build-dependencies] 85 | Inflector = "0.11" 86 | 87 | [dev-dependencies] 88 | paste = { version = "1.0" } 89 | pretty_assertions = { version = "1.4.0" } 90 | text-diff = { version = "0.4.0" } 91 | uniffi = { workspace = true, features = ["bindgen-tests", "tokio"] } 92 | 93 | # For image generation for tests 94 | image = "0.25.1" 95 | tempfile = "3.19.1" 96 | -------------------------------------------------------------------------------- /crates/core/README.md: -------------------------------------------------------------------------------- 1 | # LiveView Core 2 | 3 | ## Terminology 4 | 5 | - "LV", Phoenix LiveView 6 | - "LVN", LiveView Native 7 | - "host platform", the native UI framework and associated code for rendering via that framework (e.g. SwiftUI) 8 | - "host bindings", the layer that sits between the host platform and Core and handles the details of initializing Core and calling Core APIs 9 | - "server", the Phoenix LiveView server/backend 10 | - "client", the LVN client, i.e. Core + host bindings 11 | - "DOM", the abstract element tree maintained by Core, derived from a server-provided template + LV updates 12 | - "view tree", the native UI view/element tree, derived from the DOM 13 | 14 | ## Architecture 15 | 16 | Core will consist of two main user-facing components: 17 | 18 | - A "DOM", an abstract tree of nodes, each of which has one or more attributes. These can be serialized/deserialized from strings (in either XML or JSON form), diffed, merged, traversed and either destructively or non-destructively modified. Many of these may exist in memory at the same time. 19 | - A "client", which is instantiated per-view, and holds a DOM internally to which it applies updates as they are received. Callbacks may be registered with the client to be notified on various events; such as when the DOM is loaded, when it is updated, and more. 20 | 21 | To use Core, a set of native bindings must be implemented for each host platform that expose the functionality provided by Core in a way that is best suited for the host language. 22 | 23 | ## Usage 24 | 25 | ### Version 1 26 | 27 | In the near-term, we are punting on a few things to allow integrating with our existing SwiftUI client as quickly as possible. 28 | As a result, while it has some deficiencies, this will allow us to validate Core as soon as possible and ultimately iterate more quickly. 29 | 30 | In this version, Core is used like so: 31 | 32 | 1. Host creates a Core client, providing the client with whatever configuration it needs, including registering any callbacks for events the Host cares about. 33 | To be useful though, the Host must register a callback which will be invoked when the DOM is updated, which it can then use to (re)render the native UI view tree 34 | 2. Host establishes a connection to an LV server, receiving the initial server-rendered template as a string 35 | 3. Host calls an API on the client that tells it to initialize its internal DOM from a given template string, setting aside that template for its own use if desired 36 | 4. As the LV server continues to periodically send updates over the wire, to be applied against the template it originally sent, 37 | the Host will parse these updates as it sees fit, and when ready to apply them, will generate a new template representing the updated document. 38 | 5. Host calls a Core API to create a fresh DOM from the updated template it generated 39 | 6. Host calls an API on the client with the new DOM that tells it to perform a diff+merge of its internal DOM against the given DOM 40 | 7. The Core client will invoke its registered callbacks any time its internal DOM changes, causing the Host to re-render its view tree 41 | 42 | There are a few awkward bits here: 43 | 44 | - The responsibility for communicating with the LV server should really be owned by the Core client, but in this version is owned by the Host 45 | - The Host must perform redundant serialization/deserialization of LV updates multiple times 46 | - The Host must maintain additional state so that it can generate templates for the Core client to parse and apply as diffs to its DOM 47 | 48 | So, in a future version, we plan to address these deficiencies as described below. 49 | 50 | ### Version 2 51 | 52 | There is a lot of overlap between this version and the previous, but it is considerably simpler and resolves all of the issues raised above: 53 | 54 | 1. Host creates a Core client, as described in Version 1, but the configuration is extended to include things necessary for connecting to the LV server 55 | 2. Core client establishes a connection to the LV server, receives the initial server-rendered template, and initializes its internal DOM using it 56 | 3. Core client invokes the registered callback to tell the Host that it should render now 57 | 4. LV server continues to periodically send updates over the wire, which the Core client receives, decodes, and applies to its DOM. Each time a batch of 58 | these updates are applied, the Host-provided callback is invoked again with a handle to the latest DOM 59 | 60 | Since the client owns the LV server connection in this version, we would want to expose the ability to register callbacks for additional events, such as 61 | server disconnects/reconnects, errors decoding the DOM/updates, and anything else deemed worth propagating to the host. 62 | -------------------------------------------------------------------------------- /crates/core/examples/streaming.rs: -------------------------------------------------------------------------------- 1 | use liveview_native_core::live_socket::LiveSocket; 2 | 3 | #[cfg(target_os = "android")] 4 | const HOST: &str = "10.0.2.2:4001"; 5 | 6 | #[cfg(not(target_os = "android"))] 7 | const HOST: &str = "127.0.0.1:4001"; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let _ = env_logger::builder().parse_default_env().try_init(); 12 | 13 | let url = format!("http://{HOST}/stream"); 14 | 15 | let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default(), None) 16 | .await 17 | .expect("Failed to get liveview socket"); 18 | let live_channel = live_socket 19 | .join_liveview_channel(None, None) 20 | .await 21 | .expect("Failed to join the liveview channel"); 22 | live_channel 23 | .merge_diffs() 24 | .await 25 | .expect("Failed to merge diffs"); 26 | } 27 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | 17 | # will have compiled files and executables 18 | jni_bindings/target/ 19 | 20 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 21 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 22 | Cargo.lock 23 | 24 | # These are backup files generated by rustfmt 25 | **/*.rs.bk 26 | 27 | github.properties -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/README.md: -------------------------------------------------------------------------------- 1 | # LiveView Native Core Jetpack 2 | 3 | This library provides an abstraction layer on top of the [LiveView Native Core](https://github.com/liveview-native/liveview-native-core) library. 4 | 5 | ## Pre-requisites 6 | 7 | In order to build this library, it's necessary to do the following steps: 8 | - Use the latest version of [Android Studio](https://developer.android.com/studio) with [NDK](https://developer.android.com/studio/projects/install-ndk). 9 | - This project contains Rust files which depends on **LiveView Core library** and exposes functionality to the Kotlin layer via [JNI](https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html). Therefore, you need to [install Rust](https://www.rust-lang.org/tools/install). 10 | - After installing Rust, you'll need to install the toolchains for each platform which the library will be generated *(arm, arm64, x86, x86_64, darwin-x86-64, darwin-aarch64)*. This project is using [Rust Gradle Plugin](https://github.com/mozilla/rust-android-gradle), therefore follow the steps described in the corresponding section in their website. For instance: 11 | - Ensure your classpath points to both the proper NDK and kotlinx-coroutines e.g. `export CLASSPATH = "../path/to/jna-5.14.0.jar:/..path/to/kotlinx-coroutines-core-jvm.jar"`, typically on MacOs the kotlinx coroutines are located in `opt/homebrew/opt/kotlin/libexec/lib` if you installed with homebrew. 12 | - Ensure that you have a python version installed between 3.10 and 3.12 accessible as `python` in your environment, or use the override below. 13 | ``` 14 | rustup target add armv7-linux-androideabi # for arm 15 | rustup target add i686-linux-android # for x86 16 | rustup target add aarch64-linux-android # for aarch64 17 | rustup target add x86_64-linux-android # for x86 18 | export RUST_ANDROID_GRADLE_PYTHON_COMMAND=python3.12 # or some other version less than 3.13 19 | ``` 20 | 21 | ## Building the library 22 | 23 | In order to generate the [Android Archive](https://developer.android.com/studio/projects/android-library) (`*.aar`) file, use the command below: 24 | ``` 25 | ./gradlew assembleRelease 26 | ``` 27 | 28 | ## Releasing a new version of the library 29 | 30 | This library is hosted in [Jitpack](https://jitpack.io/) and the whole build process is automated. 31 | In order to generate a new version of the library, you just need to open a PR containing the changes and update the library version in the [build.gradle.kts](core/build.gradle.kts) file (see the `version` field in `publishing` task). 32 | After approved and merged, [create a new release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) here in GitHub. 33 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | configurations.all { 3 | resolutionStrategy.eachDependency { 4 | if (this.requested.group == "org.jetbrains.kotlin") { 5 | this.useVersion("1.9.21") 6 | because("compatibility with client version") 7 | } 8 | } 9 | } 10 | } 11 | 12 | plugins { 13 | alias(libs.plugins.android.library) apply false 14 | alias(libs.plugins.android.application) apply false 15 | alias(libs.plugins.kotlin.android) apply false 16 | alias(libs.plugins.rust.android.gradle) apply false 17 | alias(libs.plugins.dokka) apply true 18 | } 19 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /property\(* 3 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/core/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 13 | 14 | 15 | 21 | 25 | 26 | 27 | 33 | 37 | 38 | 39 | 45 | 49 | 50 | 51 | 57 | 61 | 62 | 63 | 69 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/core/src/androidTest/java/org/phoenixframework/liveview_jetpack/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package org.phoenixframework.liveview_jetpack 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import org.phoenixframework.liveviewnative.core.Document; 6 | 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | import org.junit.Assert.* 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * See [testing documentation](http://d.android.com/tools/testing). 16 | */ 17 | @RunWith(AndroidJUnit4::class) 18 | class ExampleInstrumentedTest { 19 | @Test 20 | fun useAppContext() { 21 | // Context of the app under test. 22 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 23 | assertEquals("org.phoenixframework.liveview_native_core_jetpack.test", appContext.packageName) 24 | } 25 | 26 | @Test 27 | fun parseDocument() { 28 | var doc = Document.parse(""" 29 | 30 | 31 | 32 | 33 | Email 34 | 35 | 36 | Enter 37 | 38 | 39 | 40 | 41 | """); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | android-gradle-plugin = "8.8.0" 3 | jna = "5.16.0" 4 | kotlin-stdlib = "1.9.21" 5 | kotlinx-coroutines = "1.7.3" 6 | androidx-test-ext = "1.2.1" 7 | espresso-core = "3.6.1" 8 | junit = "4.13.2" 9 | desugar_jdk_libs = "2.1.0" 10 | jetbrains-kotlin-android = "1.9.21" 11 | rust-android-gradle = "0.9.3" 12 | dokka = "2.0.0" 13 | 14 | [libraries] 15 | net-java-dev-jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna"} 16 | org-jetbrains-kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 17 | org-jetbrains-kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 18 | androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext" } 19 | androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } 20 | junit = { group = "junit", name = "junit", version.ref = "junit" } 21 | com-android-tools-desugar = {group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar_jdk_libs" } 22 | 23 | [plugins] 24 | android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } 25 | android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } 26 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "jetbrains-kotlin-android" } 27 | rust-android-gradle = { id = "org.mozilla.rust-android-gradle.rust-android", version.ref = "rust-android-gradle" } 28 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 29 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liveview-native/liveview-native-core/2f0e874960ee93b5b76daff2ecfb6d8e445e15bc/crates/core/liveview-native-core-jetpack/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/scripts/prepareJitpackEnvironment.sh: -------------------------------------------------------------------------------- 1 | # Installing Rust 2 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 3 | # After install Rust, need to the environment variables 4 | source "$HOME/.cargo/env" 5 | # Rust code is using nightly channel 6 | rustup install nightly 7 | rustup default nightly 8 | # Adding the target architectures to compile Rust code 9 | rustup target add armv7-linux-androideabi 10 | rustup target add i686-linux-android 11 | rustup target add aarch64-linux-android 12 | rustup target add x86_64-linux-android 13 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-jetpack/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "liveview-native-core-jetpack" 17 | include(":core") 18 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-swift/Benchmarks/LiveViewNativeCore/Merging.swift: -------------------------------------------------------------------------------- 1 | // Benchmark boilerplate generated by Benchmark 2 | 3 | import Benchmark 4 | import Foundation 5 | import LiveViewNativeCore 6 | 7 | let benchmarks = { 8 | Benchmark("Large Merge") { benchmark in 9 | for _ in benchmark.scaledIterations { 10 | blackHole(try MultiMergeJson()) // replace this line with your own benchmark 11 | } 12 | } 13 | Benchmark("Large Rendering") { benchmark in 14 | let doc = try MultiMergeJson() 15 | for _ in benchmark.scaledIterations { 16 | blackHole(try MultiMergeRender(doc)) // replace this line with your own benchmark 17 | } 18 | } 19 | Benchmark("Tree Traversal") { benchmark in 20 | let doc = try Document.parse(html_input) 21 | for _ in benchmark.scaledIterations { 22 | blackHole(TreeTraversal(doc)) // replace this line with your own benchmark 23 | } 24 | } 25 | Benchmark("Tree Traversal - Depth First") { benchmark in 26 | let doc = try Document.parse(html_input) 27 | for _ in benchmark.scaledIterations { 28 | blackHole(DepthFirstTreeTraversal(doc)) // replace this line with your own benchmark 29 | } 30 | } 31 | Benchmark("Parse Json") { benchmark in 32 | for _ in benchmark.scaledIterations { 33 | blackHole(try ParseJson()) // replace this line with your own benchmark 34 | } 35 | } 36 | Benchmark("Parse HTML") { benchmark in 37 | for _ in benchmark.scaledIterations { 38 | blackHole(try ParseHtml()) // replace this line with your own benchmark 39 | } 40 | } 41 | // Add additional benchmarks here 42 | } 43 | let initial_json = """ 44 | { 45 | "0":"0", 46 | "1":"0", 47 | "2":"", 48 | "s":[ 49 | "\\n \\n \\n Static Text \\n Counter 1: ", 50 | " \\n Counter 2: ", 51 | " \\n", 52 | "\\n" 53 | ] 54 | } 55 | """ 56 | let first_increment = """ 57 | { 58 | "0":"1", 59 | "1":"1", 60 | "2":{ 61 | "0":{ 62 | "s":[ 63 | "\\n Item ", 64 | "!!!\\n", 65 | "\\n", 66 | "\\n" 67 | ], 68 | "p":{ 69 | "0":[ 70 | 71 | "\\n Number = ", 72 | 73 | " + 3 is even\\n" 74 | ], 75 | "1":[ 76 | "\\n Number + 4 = ", 77 | " is odd\\n" 78 | ] 79 | }, 80 | "d":[["1",{"0":"1","s":0},{"0":"5","s":1}]] 81 | }, 82 | "1":"101", 83 | "s":[ 84 | "\\n", 85 | "\\n Number + 100 is ","\\n" 86 | ] 87 | } 88 | } 89 | """ 90 | let second_increment = """ 91 | { 92 | "0":"2", 93 | "1":"2", 94 | "2":{ 95 | "0":{ 96 | "p":{ 97 | "0":[ 98 | "\\n Number = ", 99 | " + 3 is even\\n" 100 | ], 101 | "1":[ 102 | "\\n Number + 4 = ", 103 | " is odd\\n" 104 | ], 105 | "2":[ 106 | "\\n Number = ", 107 | " + 3 is odd\\n" 108 | ], 109 | "3":[ 110 | "\\n Number + 4 = ", 111 | " is even\\n" 112 | ] 113 | }, 114 | "d":[ 115 | ["1",{"0":"1","s":0},{"0":"5","s":1}], 116 | ["2",{"0":"2","s":2},{"0":"6","s":3}] 117 | ] 118 | }, 119 | "1":"102" 120 | } 121 | } 122 | """ 123 | 124 | func ParseJson() throws -> Document { 125 | return try Document.parseFragmentJson(initial_json) 126 | } 127 | // This is the code for the large integration test from: 128 | // https://github.com/liveview-native/liveview-native-core/blob/4b7d303f98be3325c94575bfb6a7a317e2eee717/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift#L121 129 | func MultiMergeJson() throws -> Document { 130 | let doc = try Document.parseFragmentJson(initial_json) 131 | try doc.mergeFragmentJson(first_increment) 132 | try doc.mergeFragmentJson(second_increment) 133 | return doc 134 | } 135 | 136 | 137 | func MultiMergeRender(_ doc: Document) { 138 | let third_render = doc.toString() 139 | } 140 | func TreeTraversal(_ doc: Document) { 141 | let root_node = doc[doc.root()] 142 | for i in root_node.children() { 143 | } 144 | } 145 | func DepthFirstTreeTraversal(_ doc: Document) { 146 | let root_node = doc[doc.root()] 147 | for i in root_node.depthFirstChildren() { 148 | } 149 | } 150 | let html_input = """ 151 | 152 | 157 | 162 | 163 | Static Text 164 | 165 | 166 | Counter 1: 2 167 | 168 | 169 | Counter 2: 2 170 | 171 | 172 | Item 1!!! 173 | 174 | 175 | Number = 1 + 3 is even 176 | 177 | 178 | Number + 4 = 5 is odd 179 | 180 | 181 | Item 2!!! 182 | 183 | 184 | Number = 2 + 3 is odd 185 | 186 | 187 | Number + 4 = 6 is even 188 | 189 | 190 | Number + 100 is 102 191 | 192 | 193 | """ 194 | func ParseHtml() throws -> Document { 195 | return try Document.parse(html_input) 196 | } 197 | -------------------------------------------------------------------------------- /crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/.gitignore: -------------------------------------------------------------------------------- 1 | LiveViewNativeCore.swift 2 | PhoenixChannelsClient.swift 3 | -------------------------------------------------------------------------------- /crates/core/src/client/inner/cookie_store.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use log::{error, warn}; 4 | use reqwest::{cookie::CookieStore, header::HeaderValue, Url}; 5 | 6 | use crate::callbacks::SecurePersistentStore; 7 | 8 | const COOKIE_STORE_KEY: &str = "COOKIE_CACHE"; 9 | 10 | pub struct PersistentCookieStore { 11 | store: Arc, 12 | persistent_store: Option>, 13 | } 14 | 15 | impl PersistentCookieStore { 16 | pub fn new(persistent_store: Option>) -> Self { 17 | let cookie_store = if let Some(store) = &persistent_store { 18 | if let Some(binary_json) = store.get(COOKIE_STORE_KEY.to_owned()) { 19 | match cookie_store::serde::json::load(binary_json.as_slice()) { 20 | Ok(store) => store, 21 | Err(e) => { 22 | error!( 23 | "Failed to load cookie store: {} - defaulting to empty store", 24 | e 25 | ); 26 | reqwest_cookie_store::CookieStore::default() 27 | } 28 | } 29 | } else { 30 | reqwest_cookie_store::CookieStore::default() 31 | } 32 | } else { 33 | reqwest_cookie_store::CookieStore::default() 34 | }; 35 | 36 | let store = Arc::new(reqwest_cookie_store::CookieStoreMutex::new(cookie_store)); 37 | 38 | Self { 39 | store, 40 | persistent_store, 41 | } 42 | } 43 | pub fn get_cookie_list(&self, url: &Url) -> Option> { 44 | self.cookies(url).map(|header| { 45 | header 46 | .to_str() 47 | .unwrap_or_default() 48 | .split("; ") 49 | .map(|s| s.to_string()) 50 | .collect::>() 51 | }) 52 | } 53 | 54 | pub fn save(&self) { 55 | let Some(store) = &self.persistent_store else { 56 | warn!("No persistence provider while attempting to save, Cookies will not persist"); 57 | return; 58 | }; 59 | 60 | let mut buffer = Vec::new(); 61 | let store_guard = self.store.lock().unwrap(); 62 | 63 | if let Err(e) = cookie_store::serde::json::save(&store_guard, &mut buffer) { 64 | warn!("Failed to serialize cookie store: {}", e); 65 | return; 66 | } 67 | 68 | store.set(COOKIE_STORE_KEY.to_owned(), buffer) 69 | } 70 | } 71 | 72 | impl CookieStore for PersistentCookieStore { 73 | fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &Url) { 74 | CookieStore::set_cookies(self.store.as_ref(), cookie_headers, url); 75 | self.save(); 76 | } 77 | 78 | fn cookies(&self, url: &Url) -> Option { 79 | CookieStore::cookies(self.store.as_ref(), url) 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use std::{collections::HashMap, sync::Mutex}; 86 | 87 | use super::*; 88 | 89 | #[derive(Default, Debug)] 90 | struct InMemoryStore(Mutex>>); 91 | 92 | impl SecurePersistentStore for InMemoryStore { 93 | fn remove_entry(&self, key: String) { 94 | self.0.lock().unwrap().remove(&key); 95 | } 96 | 97 | fn get(&self, key: String) -> Option> { 98 | self.0.lock().unwrap().get(&key).cloned() 99 | } 100 | 101 | fn set(&self, key: String, value: Vec) { 102 | self.0.lock().unwrap().insert(key, value); 103 | } 104 | } 105 | 106 | #[test] 107 | fn test_cookie_persistence() { 108 | let _ = env_logger::builder() 109 | .parse_default_env() 110 | .is_test(true) 111 | .try_init(); 112 | 113 | let store = Arc::new(InMemoryStore::default()); 114 | let cookie_store = PersistentCookieStore::new(Some(store.clone())); 115 | 116 | let url = "https://example.com".parse().unwrap(); 117 | 118 | let persistent_cookie = 119 | "session=123; Domain=example.com; Expires=Fri, 31 Dec 9999 23:59:59 GMT"; 120 | 121 | let headers = [HeaderValue::from_static(persistent_cookie)]; 122 | 123 | cookie_store.set_cookies(&mut headers.iter(), &url); 124 | cookie_store.save(); 125 | 126 | let new_store = PersistentCookieStore::new(Some(store)); 127 | assert!(new_store.cookies(&url).is_some()); 128 | } 129 | 130 | #[test] 131 | fn test_no_persistence() { 132 | let _ = env_logger::builder() 133 | .parse_default_env() 134 | .is_test(true) 135 | .try_init(); 136 | 137 | let store = PersistentCookieStore::new(None); 138 | let url = "https://example.com".parse().unwrap(); 139 | let headers = [HeaderValue::from_static("session=123")]; 140 | 141 | store.set_cookies(&mut headers.iter(), &url); 142 | store.save(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/core/src/client/inner/logging.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Once; 2 | 3 | use log::LevelFilter; 4 | 5 | use crate::client::LogLevel; 6 | 7 | static INIT_LOG: Once = Once::new(); 8 | 9 | impl From for LevelFilter { 10 | fn from(level: LogLevel) -> Self { 11 | match level { 12 | LogLevel::Trace => LevelFilter::Trace, 13 | LogLevel::Debug => LevelFilter::Debug, 14 | LogLevel::Info => LevelFilter::Info, 15 | LogLevel::Warn => LevelFilter::Warn, 16 | LogLevel::Error => LevelFilter::Error, 17 | } 18 | } 19 | } 20 | 21 | pub fn init_log(level: LogLevel) { 22 | INIT_LOG.call_once(|| { 23 | platform::init_log(level); 24 | }); 25 | } 26 | 27 | pub fn set_log_level(level: LogLevel) { 28 | log::set_max_level(level.into()) 29 | } 30 | 31 | #[cfg(all(target_os = "android", not(test)))] 32 | mod platform { 33 | use super::*; 34 | 35 | pub fn init_log(level: LogLevel) { 36 | android_logger::init_once( 37 | android_logger::Config::default() 38 | .with_max_level(level.into()) 39 | .with_tag("LiveViewNative") 40 | .format(|f, record| { 41 | if record.level() == log::Level::Error { 42 | writeln!( 43 | f, 44 | "[{}] {} {}:{} - {}", 45 | record.level(), 46 | record.target(), 47 | record.file().unwrap_or("unknown"), 48 | record 49 | .line() 50 | .map(|line| line.to_string()) 51 | .as_deref() 52 | .unwrap_or("unknown"), 53 | record.args() 54 | ) 55 | } else { 56 | writeln!( 57 | f, 58 | "[{}] {} - {}", 59 | record.level(), 60 | record.target(), 61 | record.args() 62 | ) 63 | } 64 | }), 65 | ); 66 | } 67 | } 68 | 69 | #[cfg(all(target_vendor = "apple", not(test)))] 70 | mod platform { 71 | use super::*; 72 | 73 | pub fn init_log(level: LogLevel) { 74 | if let Err(e) = oslog::OsLogger::new("com.liveview.core.lib") 75 | .level_filter(level.into()) 76 | // For some reason uniffi really loves printing every fn call, for a dom, that sucks 77 | .category_level_filter("liveview_native_core::dom::node", LevelFilter::Warn) 78 | .category_level_filter("liveview_native_core::dom::ffi", LevelFilter::Warn) 79 | .init() 80 | { 81 | eprintln!("{e}"); 82 | } 83 | } 84 | } 85 | 86 | #[cfg(any(test, not(any(target_os = "android", target_vendor = "apple"))))] 87 | mod platform { 88 | use std::io::Write; 89 | 90 | use env_logger::{Builder, Env}; 91 | 92 | use super::*; 93 | 94 | pub fn init_log(level: LogLevel) { 95 | let env = Env::default(); 96 | let mut builder = Builder::from_env(env); 97 | let _ = builder 98 | .is_test(cfg!(test)) 99 | .format(|formatter, record| { 100 | if record.level() == log::Level::Error { 101 | writeln!( 102 | formatter, 103 | "[{}] {} {}:{} - {}", 104 | record.level(), 105 | record.target(), 106 | record.file().unwrap_or("unknown"), 107 | record 108 | .line() 109 | .map(|line| line.to_string()) 110 | .as_deref() 111 | .unwrap_or("unknown"), 112 | record.args() 113 | ) 114 | } else { 115 | writeln!( 116 | formatter, 117 | "[{}] {} - {}", 118 | record.level(), 119 | record.target(), 120 | record.args() 121 | ) 122 | } 123 | }) 124 | .filter(None, level.into()) 125 | .filter(Some("liveview_native_core::dom::node"), LevelFilter::Warn) 126 | .filter(Some("liveview_native_core::dom::ffi"), LevelFilter::Warn) 127 | .try_init(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /crates/core/src/client/tests/streaming.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Duration}; 2 | 3 | use tokio::{ 4 | sync::mpsc::{error, *}, 5 | time, 6 | }; 7 | 8 | use super::*; 9 | use crate::{ 10 | client::{ChangeType, DocumentChangeHandler}, 11 | dom::{NodeData, NodeRef}, 12 | }; 13 | 14 | const MAX_TRIES: u64 = 10; 15 | const MS_DELAY: u64 = 1500; 16 | 17 | struct Inspector { 18 | tx: UnboundedSender<(ChangeType, NodeData)>, 19 | } 20 | 21 | impl Inspector { 22 | pub fn new() -> (Self, UnboundedReceiver<(ChangeType, NodeData)>) { 23 | let (tx, rx) = unbounded_channel(); 24 | let out = Self { tx }; 25 | (out, rx) 26 | } 27 | } 28 | 29 | /// An extremely simple change handler that reports diffs in order 30 | /// over an unbounded channel 31 | impl DocumentChangeHandler for Inspector { 32 | fn handle_document_change( 33 | &self, 34 | change_type: ChangeType, 35 | _node_ref: Arc, 36 | node_data: NodeData, 37 | _parent: Option>, 38 | ) { 39 | self.tx 40 | .send((change_type, node_data)) 41 | .expect("Message Never Received."); 42 | } 43 | } 44 | 45 | // Tests that streaming connects, and succeeds at parsing at least one delta. 46 | #[tokio::test] 47 | async fn streaming_connect() -> Result<(), String> { 48 | let url = format!("http://{HOST}/stream"); 49 | 50 | let (inspector, mut rx) = Inspector::new(); 51 | 52 | let mut config = LiveViewClientConfiguration::default(); 53 | config.patch_handler = Some(Arc::new(inspector)); 54 | config.format = Platform::Swiftui; 55 | let _client = LiveViewClient::initial_connect(config, url, Default::default()) 56 | .await 57 | .expect("Failed to create client"); 58 | 59 | for _ in 0..MAX_TRIES { 60 | match rx.try_recv() { 61 | Ok(_) => { 62 | return Ok(()); 63 | } 64 | Err(error::TryRecvError::Empty) => { 65 | time::sleep(Duration::from_millis(MS_DELAY)).await; 66 | } 67 | Err(_) => { 68 | return Err(String::from("Merging Panicked")); 69 | } 70 | } 71 | } 72 | 73 | Err(format!( 74 | "Exceeded {MAX_TRIES} Max tries, waited {} ms", 75 | MAX_TRIES * MS_DELAY 76 | )) 77 | } 78 | -------------------------------------------------------------------------------- /crates/core/src/diff/fragment/error.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(Debug, Clone, thiserror::Error, uniffi::Error)] 4 | pub enum MergeError { 5 | #[error("Component not resolved after merging")] 6 | UnresolvedComponent, 7 | #[error("Missing component id {0}")] 8 | MissingComponent(i32), 9 | #[error("Fragment type mismatch")] 10 | FragmentTypeMismatch, 11 | #[error("Create component from update")] 12 | CreateComponentFromUpdate, 13 | #[error("Create child from update fragment")] 14 | CreateChildFromUpdateFragment, 15 | #[error("Add child to existing")] 16 | AddChildToExisting, 17 | #[error("There was a id mismatch when merging a stream")] 18 | StreamIDMismatch, 19 | #[error("Stream Error {error}")] 20 | Stream { 21 | #[from] 22 | error: StreamConversionError, 23 | }, 24 | } 25 | 26 | #[derive(Debug, Clone, thiserror::Error, uniffi::Error)] 27 | #[uniffi(flat_error)] 28 | pub enum RenderError { 29 | #[error("No components found when needed")] 30 | NoComponents, 31 | #[error("No templates found when needed")] 32 | NoTemplates, 33 | #[error("Templated ID {0} not found in templates")] 34 | TemplateNotFound(i32), 35 | #[error("Component ID {0} not found in components")] 36 | ComponentNotFound(i32), 37 | #[error("Merge Error {0}")] 38 | MergeError(#[from] MergeError), 39 | #[error("Child {0} for template")] 40 | ChildNotFoundForTemplate(i32), 41 | #[error("Child {0} not found for static")] 42 | ChildNotFoundForStatic(i32), 43 | #[error("Cousin not found for {0}")] 44 | CousinNotFound(i32), 45 | #[error("Serde Error {0}")] 46 | SerdeError(Arc), 47 | #[error("Parse Error {0}")] 48 | ParseError(#[from] crate::dom::ParseError), 49 | } 50 | 51 | impl From for RenderError { 52 | fn from(value: serde_json::Error) -> Self { 53 | Self::SerdeError(Arc::new(value)) 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone, thiserror::Error, uniffi::Error)] 58 | #[uniffi(flat_error)] 59 | pub enum StreamConversionError { 60 | #[error("There was no stream ID for this ")] 61 | NoStreamID, 62 | } 63 | -------------------------------------------------------------------------------- /crates/core/src/diff/fragment/tests/flow-1-change-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "1": { 4 | "stream": [ 5 | "0", 6 | [ 7 | [ 8 | "songs-0", 9 | -1, 10 | null 11 | ] 12 | ], 13 | [ 14 | "songs-0" 15 | ] 16 | ], 17 | "d": [ 18 | [ 19 | " id=\"songs-0\"", 20 | "song 1", 21 | " phx-value-id=\"0\"", 22 | " phx-value-id=\"0\"" 23 | ] 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/core/src/diff/fragment/tests/flow-1-change-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "1": { 4 | "stream": [ 5 | "0", 6 | [], 7 | [ 8 | "songs-1" 9 | ] 10 | ], 11 | "d": [] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/core/src/diff/fragment/tests/flow-1-change-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "1": { 4 | "stream": [ 5 | "0", 6 | [ 7 | [ 8 | "songs-0", 9 | -1, 10 | null 11 | ], 12 | [ 13 | "songs-1", 14 | -1, 15 | null 16 | ] 17 | ], 18 | [], 19 | true 20 | ], 21 | "d": [ 22 | [ 23 | " id=\"songs-0\"", 24 | "reset base song 0", 25 | " phx-value-id=\"0\"", 26 | " phx-value-id=\"0\"" 27 | ], 28 | [ 29 | " id=\"songs-1\"", 30 | "reset base song 1", 31 | " phx-value-id=\"1\"", 32 | " phx-value-id=\"1\"" 33 | ] 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/core/src/diff/fragment/tests/stream.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use super::*; 4 | 5 | #[test] 6 | fn recorded_stream_test() { 7 | let initial = include_str!("flow-1-change-0.json"); 8 | let root: RootDiff = serde_json::from_str(initial).expect("Failed to deserialize fragment"); 9 | let root: Root = root.try_into().expect("Failed to convert RootDiff to Root"); 10 | let out: String = root 11 | .clone() 12 | .try_into() 13 | .expect("Failed to convert Root into string"); 14 | assert_eq!(out, include_str!("flow-1-change-0.html")); 15 | 16 | let diff: RootDiff = serde_json::from_str(include_str!("flow-1-change-1.json")) 17 | .expect("Failed to deserialize fragment"); 18 | 19 | let root = root.merge(diff).expect("Failed to merge diff"); 20 | 21 | let out: String = root 22 | .clone() 23 | .try_into() 24 | .expect("Failed to convert Root into string"); 25 | assert_eq!(format!("{out}\n"), include_str!("flow-1-change-1.html")); 26 | 27 | let diff: RootDiff = serde_json::from_str(include_str!("flow-1-change-2.json")) 28 | .expect("Failed to deserialize fragment"); 29 | 30 | let root = root.merge(diff).expect("Failed to merge diff"); 31 | 32 | let out: String = root 33 | .clone() 34 | .try_into() 35 | .expect("Failed to convert Root into string"); 36 | assert_eq!(format!("{out}\n"), include_str!("flow-1-change-2.html")); 37 | 38 | let diff: RootDiff = serde_json::from_str(include_str!("flow-1-change-3.json")) 39 | .expect("Failed to deserialize fragment"); 40 | 41 | let root = root.merge(diff).expect("Failed to merge diff"); 42 | 43 | let out: String = root 44 | .clone() 45 | .try_into() 46 | .expect("Failed to convert Root into string"); 47 | assert_eq!(format!("{out}\n"), include_str!("flow-1-change-3.html")); 48 | } 49 | -------------------------------------------------------------------------------- /crates/core/src/diff/fragment/wasm.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | // These are used in the wasm build. 4 | impl Root { 5 | pub fn set_new_render(&mut self, new: bool) { 6 | self.new_render = Some(new); 7 | } 8 | 9 | pub fn is_component_only_diff(&self) -> bool { 10 | !self.components.is_empty() && self.fragment.is_empty() 11 | } 12 | pub fn is_new_fingerprint(&self) -> bool { 13 | self.fragment.is_new_fingerprint() 14 | } 15 | pub fn get_component(&self, cid: i32) -> Option { 16 | self.components.get(&format!("{cid}")).cloned() 17 | } 18 | pub fn component_cids(&self) -> Vec { 19 | let keys: Vec = self 20 | .components 21 | .keys() 22 | .filter_map(|key| key.parse::().ok()) 23 | .collect(); 24 | 25 | keys 26 | } 27 | } 28 | 29 | impl Fragment { 30 | pub fn is_new_fingerprint(&self) -> bool { 31 | match self { 32 | Fragment::Regular { statics, .. } | Fragment::Comprehension { statics, .. } => { 33 | statics.is_some() 34 | } 35 | } 36 | } 37 | 38 | pub fn is_empty(&self) -> bool { 39 | match self { 40 | Fragment::Comprehension { 41 | dynamics, 42 | statics: None, 43 | is_root: None, 44 | templates: None, 45 | stream: None, 46 | new_render: None, 47 | } => dynamics.is_empty(), 48 | _ => false, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/core/src/diff/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fragment; 2 | mod morph; 3 | mod patch; 4 | mod traversal; 5 | 6 | pub use morph::{diff, Morph}; 7 | pub use patch::{Patch, PatchResult}; 8 | pub use traversal::MoveTo; 9 | -------------------------------------------------------------------------------- /crates/core/src/diff/traversal.rs: -------------------------------------------------------------------------------- 1 | use crate::dom::NodeRef; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 4 | pub enum MoveTo { 5 | Node(NodeRef), 6 | 7 | /// Move from the current node up to its parent. 8 | Parent, 9 | 10 | /// Move to the current node's n^th child. 11 | Child(u32), 12 | 13 | /// Move to the current node's n^th from last child. 14 | ReverseChild(u32), 15 | 16 | /// Move to the n^th sibling. Not relative from the current 17 | /// location. Absolute indexed within all of the current siblings. 18 | Sibling(u32), 19 | 20 | /// Move to the n^th from last sibling. Not relative from the current 21 | /// location. Absolute indexed within all of the current siblings. 22 | ReverseSibling(u32), 23 | } 24 | -------------------------------------------------------------------------------- /crates/core/src/dom/attribute.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Write}; 2 | 3 | use smallstr::SmallString; 4 | 5 | /// Represents the fully-qualified name of an attribute 6 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, uniffi::Record)] 7 | pub struct AttributeName { 8 | /// This is used by svg attributes, e.g. `xlink-href` 9 | pub namespace: Option, 10 | pub name: String, 11 | } 12 | impl fmt::Display for AttributeName { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | if let Some(ref ns) = self.namespace { 15 | write!(f, "{}:{}", ns, &self.name) 16 | } else { 17 | write!(f, "{}", &self.name) 18 | } 19 | } 20 | } 21 | impl AttributeName { 22 | #[inline] 23 | pub fn new>(name: N) -> Self { 24 | Self { 25 | namespace: None, 26 | name: name.into(), 27 | } 28 | } 29 | 30 | #[inline] 31 | pub fn new_with_namespace, N: Into>(namespace: NS, name: N) -> Self { 32 | Self { 33 | namespace: Some(namespace.into()), 34 | name: name.into(), 35 | } 36 | } 37 | } 38 | impl From<&str> for AttributeName { 39 | fn from(s: &str) -> Self { 40 | match s.split_once(':') { 41 | None => AttributeName::new(s), 42 | Some((ns, name)) => AttributeName::new_with_namespace(ns, name), 43 | } 44 | } 45 | } 46 | impl From for AttributeName { 47 | fn from(s: String) -> Self { 48 | AttributeName::new(s) 49 | } 50 | } 51 | 52 | impl PartialEq for AttributeName { 53 | fn eq(&self, other: &str) -> bool { 54 | self.name.as_str().eq(other) 55 | } 56 | } 57 | 58 | #[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] 59 | pub struct Attribute { 60 | pub name: AttributeName, 61 | pub value: Option, 62 | } 63 | impl Attribute { 64 | /// Creates a new attribute with the given name and value 65 | /// 66 | /// If the attribute you wish to create is namespaced, make sure to set the 67 | /// namespace with `set_namespace` after creating the attribute. 68 | pub fn new>(name: K, value: Option) -> Self { 69 | Self { 70 | name: AttributeName::new(name), 71 | value, 72 | } 73 | } 74 | 75 | #[inline] 76 | pub fn set_value(&mut self, value: Option) { 77 | self.value = value; 78 | } 79 | } 80 | 81 | #[derive(Debug, Clone)] 82 | pub enum AttributeValue { 83 | None, 84 | String(SmallString<[u8; 16]>), 85 | } 86 | impl AttributeValue { 87 | pub fn name(&self) -> String { 88 | match self { 89 | Self::None => String::new(), 90 | Self::String(s) => s.to_string(), 91 | } 92 | } 93 | 94 | pub fn as_str(&self) -> Option<&str> { 95 | match self { 96 | Self::None => None, 97 | Self::String(s) => Some(s.as_str()), 98 | } 99 | } 100 | } 101 | impl From<&str> for AttributeValue { 102 | #[inline] 103 | fn from(s: &str) -> Self { 104 | Self::String(SmallString::from_str(s)) 105 | } 106 | } 107 | impl From for AttributeValue { 108 | #[inline] 109 | fn from(s: String) -> Self { 110 | Self::String(SmallString::from_string(s)) 111 | } 112 | } 113 | impl From> for AttributeValue { 114 | #[inline] 115 | fn from(s: SmallString<[u8; 16]>) -> Self { 116 | Self::String(s) 117 | } 118 | } 119 | impl Eq for AttributeValue {} 120 | impl PartialEq for AttributeValue { 121 | fn eq(&self, other: &Self) -> bool { 122 | match (self, other) { 123 | (Self::None, Self::None) => true, 124 | (Self::None, Self::String(s)) | (Self::String(s), Self::None) => s.is_empty(), 125 | (Self::String(x), Self::String(y)) => x == y, 126 | } 127 | } 128 | } 129 | impl PartialEq for AttributeValue { 130 | fn eq(&self, other: &str) -> bool { 131 | match (self, other) { 132 | (Self::None, y) => y.is_empty(), 133 | (Self::String(x), y) => x == y, 134 | } 135 | } 136 | } 137 | impl fmt::Display for AttributeValue { 138 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 139 | match self { 140 | Self::None => Ok(()), 141 | Self::String(s) => { 142 | // Ensure we print strings quoted, with proper escaping of inner quotes 143 | f.write_char('"')?; 144 | let mut escaped = false; 145 | for c in s.chars() { 146 | match c { 147 | // Force unescaped quotes to be escaped 148 | '"' if !escaped => write!(f, "{}", '"'.escape_default())?, 149 | // Handle escape transitions 150 | c if escaped => { 151 | f.write_char('\\')?; 152 | f.write_char(c)?; 153 | escaped = false; 154 | } 155 | '\\' => { 156 | escaped = true; 157 | continue; 158 | } 159 | // Otherwise print characters normally 160 | c => f.write_char(c)?, 161 | } 162 | } 163 | f.write_char('"') 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /crates/core/src/dom/printer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::{Document, NodeData, NodeRef}; 4 | 5 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 6 | pub enum PrintOptions { 7 | /// Prints a document/fragment without any extra whitespace (indentation/whitespace) 8 | Minified, 9 | /// Prints a document/fragment with each element open/closed on it's own line, 10 | /// and indented based on the level of nesting in the document. 11 | Pretty, 12 | } 13 | impl PrintOptions { 14 | #[inline(always)] 15 | pub fn pretty(&self) -> bool { 16 | self == &Self::Pretty 17 | } 18 | } 19 | 20 | pub struct Printer<'a> { 21 | doc: &'a Document, 22 | root: NodeRef, 23 | options: PrintOptions, 24 | indent: usize, 25 | } 26 | impl<'a> Printer<'a> { 27 | pub fn new(doc: &'a Document, root: NodeRef, options: PrintOptions) -> Self { 28 | Self { 29 | doc, 30 | root, 31 | options, 32 | indent: 0, 33 | } 34 | } 35 | 36 | pub fn print(mut self, writer: &mut dyn fmt::Write) -> fmt::Result { 37 | use petgraph::visit::{depth_first_search, DfsEvent}; 38 | 39 | let mut first = true; 40 | depth_first_search(self.doc, Some(self.root), |event| { 41 | match event { 42 | DfsEvent::Discover(node, _) => { 43 | // We're encountering `node` for the first time 44 | match &self.doc.nodes[node] { 45 | NodeData::NodeElement { element: elem } => { 46 | let pretty = self.options.pretty(); 47 | let self_closing = self.doc.children[node].is_empty(); 48 | if pretty { 49 | if !first { 50 | writer.write_char('\n')?; 51 | } else { 52 | first = false; 53 | } 54 | indent(self.indent, writer)?; 55 | } 56 | write!(writer, "<{}", &elem.name)?; 57 | let attrs = elem.attributes(); 58 | if !attrs.is_empty() { 59 | for attr in attrs.iter() { 60 | write!( 61 | writer, 62 | " {}=\"{}\"", 63 | &attr.name, 64 | &attr.value.clone().unwrap_or_default() 65 | )? 66 | } 67 | } 68 | if self_closing { 69 | writer.write_str(" />") 70 | } else { 71 | if pretty { 72 | self.indent += 1; 73 | } 74 | writer.write_str(">") 75 | } 76 | } 77 | NodeData::Leaf { value: content } => { 78 | if self.options.pretty() { 79 | if !first { 80 | writer.write_char('\n')?; 81 | } else { 82 | first = false; 83 | } 84 | indent(self.indent, writer)?; 85 | } 86 | writer.write_str(content.as_str()) 87 | } 88 | NodeData::Root => Ok(()), 89 | } 90 | } 91 | DfsEvent::Finish(node, _) => { 92 | // We've visited all the children of `node` 93 | if let NodeData::NodeElement { element: elem } = &self.doc.nodes[node] { 94 | let self_closing = self.doc.children[node].is_empty(); 95 | if self_closing { 96 | return Ok(()); 97 | } 98 | if self.options.pretty() { 99 | writer.write_char('\n')?; 100 | self.indent -= 1; 101 | indent(self.indent, writer)?; 102 | } 103 | write!(writer, "", &elem.name)? 104 | } 105 | Ok(()) 106 | } 107 | _ => Ok(()), 108 | } 109 | }) 110 | } 111 | } 112 | 113 | fn indent(mut n: usize, writer: &mut dyn fmt::Write) -> fmt::Result { 114 | const INDENT: &str = " "; 115 | while n > 0 { 116 | writer.write_str(INDENT)?; 117 | n -= 1; 118 | } 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /crates/core/src/error/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "liveview-channels")] 2 | mod socket; 3 | 4 | #[cfg(feature = "liveview-channels")] 5 | pub use socket::*; 6 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "liveview-channels")] 2 | pub mod client; 3 | 4 | #[cfg(feature = "liveview-channels")] 5 | pub use client::inner::LiveViewClientInner as LiveViewClient; 6 | #[cfg(feature = "liveview-channels")] 7 | pub use client::LiveViewClientBuilder; 8 | 9 | mod callbacks; 10 | pub mod diff; 11 | pub mod dom; 12 | mod protocol; 13 | 14 | mod error; 15 | 16 | #[cfg(feature = "liveview-channels")] 17 | pub mod live_socket; 18 | 19 | #[cfg(feature = "liveview-channels")] 20 | phoenix_channels_client::uniffi_reexport_scaffolding!(); 21 | 22 | uniffi::setup_scaffolding!("liveview_native_core"); 23 | -------------------------------------------------------------------------------- /crates/core/src/live_socket/mod.rs: -------------------------------------------------------------------------------- 1 | mod channel; 2 | pub mod navigation; 3 | mod socket; 4 | 5 | #[cfg(test)] 6 | mod tests; 7 | 8 | pub use channel::{LiveChannel, LiveFile}; 9 | use serde::Deserialize; 10 | pub use socket::{ConnectOpts, LiveSocket, Method, SessionData}; 11 | 12 | #[derive(Deserialize, Debug)] 13 | pub struct UploadConfig { 14 | chunk_size: u64, 15 | max_file_size: u64, 16 | max_entries: u64, 17 | } 18 | 19 | /// Defaults from https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#allow_upload/3 20 | impl Default for UploadConfig { 21 | fn default() -> Self { 22 | Self { 23 | chunk_size: 64_000, 24 | max_file_size: 8000000, 25 | max_entries: 1, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/core/src/live_socket/tests/error.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::error::*; 3 | 4 | #[tokio::test] 5 | async fn dead_render_error() { 6 | let _ = env_logger::builder() 7 | .parse_default_env() 8 | .is_test(true) 9 | .try_init(); 10 | 11 | let url = format!("http://{HOST}/doesnt-exist"); 12 | let live_socket_err = 13 | LiveSocket::new(url.to_string(), "swiftui".into(), Default::default(), None).await; 14 | assert!(live_socket_err.is_err()); 15 | let live_socket_err = live_socket_err.err().unwrap(); 16 | assert!(matches!( 17 | live_socket_err, 18 | LiveSocketError::ConnectionError { .. } 19 | )); 20 | log::debug!("ERROR HTML: {live_socket_err}"); 21 | } 22 | -------------------------------------------------------------------------------- /crates/core/src/live_socket/tests/streaming.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc::error::TryRecvError::Empty; 2 | 3 | use super::*; 4 | 5 | // As of this commit the server sends a 6 | // stream even every 10_000 ms 7 | // This sampling interval should catch one 8 | const MAX_TRIES: u64 = 120; 9 | const MS_DELAY: u64 = 100; 10 | 11 | // Tests that streaming connects, and succeeds at parsing at least one delta. 12 | #[tokio::test] 13 | async fn streaming_connect() -> Result<(), String> { 14 | let _ = env_logger::builder() 15 | .parse_default_env() 16 | .is_test(true) 17 | .try_init(); 18 | 19 | let url = format!("http://{HOST}/stream"); 20 | 21 | let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default(), None) 22 | .await 23 | .map_err(|e| format!("Failed to get liveview socket {e}"))?; 24 | 25 | let live_channel = live_socket 26 | .join_liveview_channel(None, None) 27 | .await 28 | .map_err(|e| format!("Failed to join the liveview channel {e}"))?; 29 | 30 | let doc = live_channel.document(); 31 | let (inspector, mut rx) = Inspector::new(doc); 32 | live_channel.set_event_handler(Box::new(inspector)); 33 | 34 | let chan_clone = live_channel.channel().clone(); 35 | tokio::spawn(async move { 36 | live_channel 37 | .merge_diffs() 38 | .await 39 | .expect("Failed to merge diffs"); 40 | }); 41 | 42 | for _ in 0..MAX_TRIES { 43 | match rx.try_recv() { 44 | Ok(_) => { 45 | chan_clone 46 | .leave() 47 | .await 48 | .map_err(|e| format!("Failed to leave channel {e}"))?; 49 | 50 | return Ok(()); 51 | } 52 | Err(Empty) => { 53 | tokio::time::sleep(Duration::from_millis(MS_DELAY)).await; 54 | } 55 | Err(_) => { 56 | return Err(String::from("Merging Panicked")); 57 | } 58 | } 59 | } 60 | 61 | Err(format!( 62 | "Exceeded {MAX_TRIES} Max tries, waited {} ms", 63 | MAX_TRIES * MS_DELAY 64 | )) 65 | } 66 | -------------------------------------------------------------------------------- /crates/core/src/protocol.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Replies can contain redirect information either in a 4 | /// live_redirect (new channel) or a full redirect (new socket) 5 | #[derive(Clone, Debug, Serialize, Deserialize)] 6 | pub struct Redirect { 7 | pub kind: Option, 8 | pub to: String, 9 | } 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | #[serde(rename_all = "lowercase")] 13 | pub enum RedirectKind { 14 | Push, 15 | Replace, 16 | } 17 | -------------------------------------------------------------------------------- /crates/core/tests/bindings/simple.kts: -------------------------------------------------------------------------------- 1 | import org.phoenixframework.liveviewnative.core.*; 2 | import org.phoenixframework.liveviewnative.channel.*; 3 | -------------------------------------------------------------------------------- /crates/core/tests/bindings/simple.swift: -------------------------------------------------------------------------------- 1 | import LiveViewNativeCore 2 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Patrick Steele-Idem (psteeleidem.com) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /crates/core/tests/fixtures/README.md: -------------------------------------------------------------------------------- 1 | Fixtures sourced from https://github.com/patrick-steele-idem/morphdom -------------------------------------------------------------------------------- /crates/core/tests/fixtures/attr-value-empty-string/from.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/attr-value-empty-string/to.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/change-tagname-ids/from.html: -------------------------------------------------------------------------------- 1 |

2 | italics 3 |

-------------------------------------------------------------------------------- /crates/core/tests/fixtures/change-tagname-ids/to.html: -------------------------------------------------------------------------------- 1 |
2 | italics 3 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/change-tagname/from.html: -------------------------------------------------------------------------------- 1 | italics -------------------------------------------------------------------------------- /crates/core/tests/fixtures/change-tagname/to.html: -------------------------------------------------------------------------------- 1 | bold -------------------------------------------------------------------------------- /crates/core/tests/fixtures/change-types/from.html: -------------------------------------------------------------------------------- 1 | italics -------------------------------------------------------------------------------- /crates/core/tests/fixtures/change-types/to.html: -------------------------------------------------------------------------------- 1 | bold -------------------------------------------------------------------------------- /crates/core/tests/fixtures/data-table/from.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 24 | 25 | 26 | 29 | 32 | 35 | 36 | 37 | 40 | 43 | 46 | 47 | 48 | 51 | 54 | 57 | 58 | 59 | 62 | 65 | 68 | 69 | 70 | 73 | 76 | 79 | 80 | 81 | 84 | 87 | 90 | 91 | 92 | 95 | 98 | 101 | 102 | 103 | 106 | 109 | 112 | 113 | 114 | 117 | 120 | 123 | 124 | 125 | 128 | 131 | 134 | 135 | 136 | 139 | 142 | 145 | 146 | 147 | 150 | 153 | 156 | 157 | 158 | 161 | 164 | 167 | 168 | 169 | 172 | 175 | 178 | 179 | 180 | 183 | 186 | 189 | 190 | 191 | 194 | 197 | 200 | 201 | 202 | 205 | 208 | 211 | 212 | 213 | 216 | 219 | 222 | 223 | 224 | 227 | 230 | 233 | 234 | 235 | 238 | 241 | 244 | 245 | 246 | 249 | 252 | 255 | 256 | 257 | 260 | 263 | 266 | 267 | 268 | 271 | 274 | 277 | 278 | 279 | 282 | 285 | 288 | 289 | 290 |
6 | virtual-dom 7 | 9 | morphdom 10 |
16 | change-tagname 17 | 19 | 0.01ms 20 | 22 | 0.01ms 23 |
27 | change-tagname-ids 28 | 30 | 0.01ms 31 | 33 | 0.01ms 34 |
38 | ids-nested 39 | 41 | 0.01ms 42 | 44 | 0.01ms 45 |
49 | ids-nested-2 50 | 52 | 0.01ms 53 | 55 | 0.02ms 56 |
60 | ids-nested-3 61 | 63 | 0.02ms 64 | 66 | 0.01ms 67 |
71 | ids-nested-4 72 | 74 | 0.11ms 75 | 77 | 0.02ms 78 |
82 | ids-nested-5 83 | 85 | 0.02ms 86 | 88 | 0.02ms 89 |
93 | ids-nested-6 94 | 96 | 0.01ms 97 | 99 | 0.01ms 100 |
104 | ids-prepend 105 | 107 | 0.01ms 108 | 110 | 0.01ms 111 |
115 | input-element 116 | 118 | 0.01ms 119 | 121 | 0.01ms 122 |
126 | input-element-disabled 127 | 129 | 0.01ms 130 | 132 | 0.01ms 133 |
137 | input-element-enabled 138 | 140 | 0.01ms 141 | 143 | 0.01ms 144 |
148 | large 149 | 151 | 1.15ms 152 | 154 | 2.42ms 155 |
159 | lengthen 160 | 162 | 0.02ms 163 | 165 | 0.02ms 166 |
170 | one 171 | 173 | 0.01ms 174 | 176 | 0.01ms 177 |
181 | reverse 182 | 184 | 0.02ms 185 | 187 | 0.01ms 188 |
192 | reverse-ids 193 | 195 | 0.02ms 196 | 198 | 0.03ms 199 |
203 | select-element 204 | 206 | 0.04ms 207 | 209 | 0.36ms 210 |
214 | shorten 215 | 217 | 0.01ms 218 | 220 | 0.02ms 221 |
225 | simple 226 | 228 | 0.01ms 229 | 231 | 0.00ms 232 |
236 | simple-ids 237 | 239 | 0.02ms 240 | 242 | 0.03ms 243 |
247 | simple-text-el 248 | 250 | 0.01ms 251 | 253 | 0.01ms 254 |
258 | svg 259 | 261 | 0.01ms 262 | 264 | 0.02ms 265 |
269 | todomvc 270 | 272 | 0.25ms 273 | 275 | 0.50ms 276 |
280 | two 281 | 283 | 0.02ms 284 | 286 | 0.01ms 287 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/equal/from.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 | 11 | 12 |
    13 |
  • 14 |
    15 | 16 | 17 | 18 |
    19 | 20 |
  • 21 |
  • 22 |
    23 | 24 | 25 | 26 |
    27 | 28 |
  • 29 |
  • 30 |
    31 | 32 | 33 | 34 |
    35 | 36 |
  • 37 |
  • 38 |
    39 | 40 | 41 | 42 |
    43 | 44 |
  • 45 |
  • 46 |
    47 | 48 | 49 | 50 |
    51 | 52 |
  • 53 |
  • 54 |
    55 | 56 | 57 | 58 |
    59 | 60 |
  • 61 |
62 |
63 |
64 | 65 | 5 items left 66 | 71 | 72 |
73 |
74 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/equal/to.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 | 11 | 12 |
    13 |
  • 14 |
    15 | 16 | 17 | 18 |
    19 | 20 |
  • 21 |
  • 22 |
    23 | 24 | 25 | 26 |
    27 | 28 |
  • 29 |
  • 30 |
    31 | 32 | 33 | 34 |
    35 | 36 |
  • 37 |
  • 38 |
    39 | 40 | 41 | 42 |
    43 | 44 |
  • 45 |
  • 46 |
    47 | 48 | 49 | 50 |
    51 | 52 |
  • 53 |
  • 54 |
    55 | 56 | 57 | 58 |
    59 | 60 |
  • 61 |
62 |
63 |
64 | 65 | 5 items left 66 | 71 | 72 |
73 |
74 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/id-change-tag-name/from.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/id-change-tag-name/to.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-2/from.html: -------------------------------------------------------------------------------- 1 |
strongtestbar
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-2/to.html: -------------------------------------------------------------------------------- 1 |
strikethrough
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-3/from.html: -------------------------------------------------------------------------------- 1 |
strongtestbar
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-3/to.html: -------------------------------------------------------------------------------- 1 |
strikethrough
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-4/from.html: -------------------------------------------------------------------------------- 1 |
bold
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-4/to.html: -------------------------------------------------------------------------------- 1 |
foo
bar
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-5/from.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | bold 4 |
5 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-5/to.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | foo 4 |
5 |
6 | bar 7 |
8 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-6/from.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | foo 4 |
5 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-6/to.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | foo 5 |
6 |
7 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-7/from.html: -------------------------------------------------------------------------------- 1 |
foo
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested-7/to.html: -------------------------------------------------------------------------------- 1 |
foobar
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested/from.html: -------------------------------------------------------------------------------- 1 |
foobar
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-nested/to.html: -------------------------------------------------------------------------------- 1 |
foo
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-prepend/from.html: -------------------------------------------------------------------------------- 1 |
W1
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/ids-prepend/to.html: -------------------------------------------------------------------------------- 1 |
W1
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/input-element-disabled/from.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/input-element-disabled/to.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/input-element-enabled/from.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/input-element-enabled/to.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/input-element/from.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/input-element/to.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/lengthen/from.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 0 4 |
5 |
6 | 1 7 |
8 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/lengthen/to.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 0 4 |
5 |
6 | 1 7 |
8 | After Bar 9 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/one/from.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/one/to.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/reverse-ids/from.html: -------------------------------------------------------------------------------- 1 |
w1
w2
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/reverse-ids/to.html: -------------------------------------------------------------------------------- 1 |
w2
w1
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/reverse/from.html: -------------------------------------------------------------------------------- 1 |
2 | 0 3 | bold 4 | 1 5 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/reverse/to.html: -------------------------------------------------------------------------------- 1 |
2 | 0 3 | italics 4 | 1 5 | span 6 | 2 7 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/select-element-default/from.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/select-element-default/to.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/select-element/from.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/select-element/to.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/shorten/from.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 0 4 |
5 |
6 | 1 7 |
8 | After Bar 9 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/shorten/to.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 0 4 |
5 |
6 | 1 7 |
8 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/simple-ids/from.html: -------------------------------------------------------------------------------- 1 |
2 | 1 3 |
4 | 2 5 |
6 | 3 7 | 8 | 4 9 | 10 | 5 11 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/simple-ids/to.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | before 4 | 5 | 1 6 |
7 | 2 8 |
9 | 3 10 | 11 | 4 12 | 13 | 5 14 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/simple-text-el/from.html: -------------------------------------------------------------------------------- 1 |
2 | 0 3 | bold 4 | 1 5 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/simple-text-el/to.html: -------------------------------------------------------------------------------- 1 |
2 | italics 3 | bold 4 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/simple/from.html: -------------------------------------------------------------------------------- 1 |
bold
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/simple/to.html: -------------------------------------------------------------------------------- 1 |
italicsbold
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-append-new/from.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-append-new/to.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | /svg/index.html 7 | 8 | 9 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-append/from.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-append/to.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | /svg/index.html 7 | 8 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-no-default-namespace/from.html: -------------------------------------------------------------------------------- 1 |
2 |

SVG embedded inline in XHTML

3 | 4 | 5 | 6 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-no-default-namespace/to.html: -------------------------------------------------------------------------------- 1 |
2 |

SVG embedded inline in XHTML

3 | 4 | 5 | 6 | 7 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-xlink/from.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg-xlink/to.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg/from.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/svg/to.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/tag-to-text/from.html: -------------------------------------------------------------------------------- 1 |
hello
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/tag-to-text/to.html: -------------------------------------------------------------------------------- 1 | world -------------------------------------------------------------------------------- /crates/core/tests/fixtures/text-to-tag/from.html: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /crates/core/tests/fixtures/text-to-tag/to.html: -------------------------------------------------------------------------------- 1 |
world
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/text-to-text/from.html: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /crates/core/tests/fixtures/text-to-text/to.html: -------------------------------------------------------------------------------- 1 | world -------------------------------------------------------------------------------- /crates/core/tests/fixtures/textarea/from.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/textarea/to.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/core/tests/fixtures/todomvc/from.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 | 11 | 12 |
    13 |
  • 14 |
    15 | 16 | 17 | 18 |
    19 | 20 |
  • 21 |
  • 22 |
    23 | 24 | 25 | 26 |
    27 | 28 |
  • 29 |
  • 30 |
    31 | 32 | 33 | 34 |
    35 | 36 |
  • 37 |
  • 38 |
    39 | 40 | 41 | 42 |
    43 | 44 |
  • 45 |
  • 46 |
    47 | 48 | 49 | 50 |
    51 | 52 |
  • 53 |
  • 54 |
    55 | 56 | 57 | 58 |
    59 | 60 |
  • 61 |
62 |
63 |
64 | 65 | 5 items left 66 | 71 | 72 |
73 |
74 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/todomvc2/from.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | 4 |
  • 5 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/todomvc2/to.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | 4 |
  • 5 |
  • 6 | 7 |
  • 8 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/two/from.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /crates/core/tests/fixtures/two/to.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /crates/core/tests/parser.rs: -------------------------------------------------------------------------------- 1 | use liveview_native_core::dom::{AttributeName, Document, NodeData}; 2 | 3 | #[test] 4 | fn parser_simple() { 5 | let result = Document::parse("Hello World!"); 6 | assert!(result.is_ok()); 7 | let document = result.unwrap(); 8 | let root = document.root(); 9 | let html = document.children(root)[0]; 10 | let attrs = document 11 | .attributes(html) 12 | .iter() 13 | .map(|a| (a.name.clone(), a.value.clone())) 14 | .collect::>(); 15 | let lang: AttributeName = "lang".into(); 16 | let en = Some("en".to_string()); 17 | assert_eq!(attrs, vec![(lang, en)]); 18 | } 19 | 20 | #[test] 21 | fn parser_whitespace_handling() { 22 | let result = Document::parse( 23 | r#" 24 | 25 | 26 | 27 | 28 | 29 | some content 30 | 31 | 32 | "#, 33 | ); 34 | assert!(result.is_ok()); 35 | let document = result.unwrap(); 36 | let root = document.root(); 37 | let children = document.children(root); 38 | assert_eq!(children.len(), 1); 39 | let html = children[0]; 40 | let children = document.children(html); 41 | assert_eq!(children.len(), 2); 42 | let body = children[1]; 43 | let children = document.children(body); 44 | assert_eq!(children.len(), 1); 45 | let content = document.get(children[0]); 46 | assert!(matches!(content, NodeData::Leaf { .. })); 47 | let NodeData::Leaf { value: content } = content else { 48 | unreachable!() 49 | }; 50 | assert_eq!(content.as_str(), "some content"); 51 | } 52 | 53 | #[test] 54 | fn parser_preserve_upcase() { 55 | let result = Document::parse("Hello World!"); 56 | assert!(result.is_ok()); 57 | let document = result.unwrap(); 58 | let root = document.root(); 59 | let component = document.children(root)[0]; 60 | let element = document.get(component); 61 | let NodeData::NodeElement { element } = element else { 62 | panic!("expected element"); 63 | }; 64 | let expected_name: String = "Component".into(); 65 | assert_eq!(element.name, expected_name); 66 | } 67 | -------------------------------------------------------------------------------- /crates/core/tests/support/tinycross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liveview-native/liveview-native-core/2f0e874960ee93b5b76daff2ecfb6d8e445e15bc/crates/core/tests/support/tinycross.png -------------------------------------------------------------------------------- /crates/core/tests/uniffi.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(target_os = "macos", target_os = "linux"))] 2 | uniffi::build_foreign_language_testcases!( 3 | "tests/bindings/simple.kts", 4 | "tests/bindings/simple.swift", 5 | ); 6 | -------------------------------------------------------------------------------- /crates/core/uniffi.toml: -------------------------------------------------------------------------------- 1 | [bindings.kotlin] 2 | cdylib_name = "liveview_native_core" 3 | package_name = "org.phoenixframework.liveviewnative.core" 4 | android_cleaner = false 5 | android = true 6 | kotlin_target_version = "1.9.0" 7 | 8 | [bindings.swift] 9 | omit_argument_labels = true 10 | module_name = "LiveViewNativeCore" 11 | 12 | [bindings.kotlin.custom_types.Url] 13 | type_name = "URL" 14 | imports = [ "java.net.URI", "java.net.URL" ] 15 | into_custom = "URI({}).toURL()" 16 | from_custom = "{}.toString()" 17 | 18 | [bindings.swift.custom_types.Url] 19 | type_name = "URL" 20 | # Modules that need to be imported 21 | imports = ["Foundation"] 22 | # Functions to convert between strings and URLs 23 | into_custom = "URL(string: {})!" 24 | from_custom = "String(describing: {})" 25 | 26 | [bindings.kotlin.external_packages] 27 | phoenix_channels_client = "org.phoenixframework.liveviewnative.channel" 28 | -------------------------------------------------------------------------------- /crates/uniffi-bindgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uniffi-bindgen" 3 | version.workspace = true 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | repository.workspace = true 8 | homepage.workspace = true 9 | documentation.workspace = true 10 | categories.workspace = true 11 | keywords.workspace = true 12 | readme.workspace = true 13 | edition.workspace = true 14 | license.workspace = true 15 | publish = false 16 | 17 | [dependencies] 18 | uniffi = { workspace = true, features = ["cli"] } 19 | -------------------------------------------------------------------------------- /crates/uniffi-bindgen/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | uniffi::uniffi_bindgen_main() 3 | } 4 | -------------------------------------------------------------------------------- /crates/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "liveview_native_core_wasm" 3 | version.workspace = true 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | repository.workspace = true 8 | homepage.workspace = true 9 | documentation.workspace = true 10 | categories.workspace = true 11 | keywords.workspace = true 12 | license.workspace = true 13 | readme.workspace = true 14 | edition.workspace = true 15 | publish.workspace = true 16 | 17 | [lints.rust] 18 | unexpected_cfgs = { level = "warn", check-cfg = [ 19 | 'cfg(wasm_bindgen_unstable_test_coverage)', 20 | ] } 21 | 22 | [lib] 23 | crate-type = ["cdylib"] 24 | 25 | [dependencies] 26 | wasm-bindgen = "0.2.93" 27 | serde-wasm-bindgen = "0.6.5" 28 | serde = { version = "1.0", features = ["derive"] } 29 | liveview-native-core = { path = "../core/", default-features = false, features = [ 30 | "browser", 31 | ] } 32 | console_error_panic_hook = { version = "0.1.7" } 33 | console_log = { version = "1", features = ["color"] } 34 | log = "0.4" 35 | js-sys = "0.3.69" 36 | 37 | # This is needed just for the js feature flag to be enabled on getrandom. 38 | getrandom = { version = "0.2", features = ["js"] } 39 | 40 | [dev-dependencies] 41 | url = "2.5" 42 | uuid = { version = "1.15.0", features = ["v4"] } 43 | wasm-bindgen-test = "0.3.42" 44 | wasm-bindgen-futures = { version = "0.4.31", features = [ 45 | "futures-core-03-stream", 46 | ] } 47 | -------------------------------------------------------------------------------- /crates/wasm/npm_shims/jest_mock.js: -------------------------------------------------------------------------------- 1 | /// Substitute live view webs classes for our own during jest tests. 2 | jest.mock("phoenix_live_view/rendered", () => { 3 | const actualModule = jest.requireActual("phoenix_live_view/rendered"); 4 | const wasmProxy = require("liveview_native_core_wasm_nodejs"); 5 | 6 | // free function that we are not concerned with. 7 | wasmProxy.Rendered.modifyRoot = actualModule.modifyRoot; 8 | 9 | return wasmProxy.Rendered; 10 | }); 11 | -------------------------------------------------------------------------------- /crates/wasm/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | TARGET=$1 5 | 6 | # move to the root of the wasm directory 7 | script_dir=$(dirname "$0") 8 | cd "$script_dir/.." 9 | 10 | SED=sed 11 | if [ $(uname) = "Darwin" ]; then 12 | SED=gsed 13 | fi 14 | 15 | if [ $TARGET = "web" ] ; then 16 | wasm-pack build --no-typescript --out-dir ./liveview-native-core-wasm-web 17 | wasm2es6js --base64 liveview-native-core-wasm-web/liveview_native_core_wasm_bg.wasm -o ./liveview-native-core-wasm-web/liveview_native_core_wasm_bg.wasm.js 18 | 19 | # Update our JS shim to require the JS file instead 20 | ${SED} -i 's/liveview_native_core_wasm_bg.wasm/liveview_native_core_wasm_bg.wasm.js/' liveview-native-core-wasm-web/liveview_native_core_wasm.js 21 | ${SED} -i 's/liveview_native_core_wasm_bg.wasm/liveview_native_core_wasm_bg.wasm.js/' liveview-native-core-wasm-web/liveview_native_core_wasm_bg.wasm.js 22 | ${SED} -i 's/liveview_native_core_wasm_bg.wasm/liveview_native_core_wasm_bg.wasm.js/' liveview-native-core-wasm-web/package.json 23 | 24 | jq '.name = "liveview_native_core_wasm_web"' liveview-native-core-wasm-web/package.json > tmp.json && mv tmp.json ./liveview-native-core-wasm-web/package.json 25 | npm pack ./liveview-native-core-wasm-web 26 | mv liveview_native_core_wasm*tgz ./liveview-native-core-wasm-web.tgz 27 | elif [ $TARGET = "nodejs" ] ; then 28 | wasm-pack build --no-typescript --target nodejs --out-dir ./liveview-native-core-wasm-nodejs 29 | jq '.name = "liveview_native_core_wasm_nodejs"' liveview-native-core-wasm-nodejs/package.json > tmp.json && mv tmp.json ./liveview-native-core-wasm-nodejs/package.json 30 | npm pack ./liveview-native-core-wasm-nodejs/ 31 | mv liveview_native_core_wasm*tgz ./liveview-native-core-wasm-nodejs.tgz 32 | else 33 | echo "Either `web` or `nodejs` must be specified as the first argument" 34 | exit 1 35 | fi 36 | -------------------------------------------------------------------------------- /crates/wasm/scripts/jest_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # move to the root of the wasm directory 5 | script_dir=$(dirname "$0") 6 | cd "$script_dir/.." 7 | 8 | # The first argument is interpreted as a quoted jest filter 9 | # for example: "merges the latter" 10 | if [ -z "$1" ]; then 11 | filter_arg="-t .*" 12 | else 13 | filter_arg="-t $1" 14 | fi 15 | 16 | # set up a deferred cleanup hook 17 | initial_dir=$(pwd) 18 | cleanup() { 19 | cd "$initial_dir" 20 | echo "Cleaning up..." 21 | rm -rf phoenix_live_view 22 | } 23 | 24 | checkout_latest_tag() { 25 | git fetch --tags 26 | git checkout "$(git describe --tags "$(git rev-list --tags --max-count=1)")" 27 | } 28 | 29 | if [ $(uname) == "Darwin" ]; then 30 | trap cleanup ERR 31 | fi 32 | 33 | if [ ! -d "phoenix_live_view" ]; then 34 | git clone https://github.com/phoenixframework/phoenix_live_view 35 | fi 36 | 37 | cd phoenix_live_view/assets && checkout_latest_tag 38 | npm install ../../liveview-native-core-wasm-nodejs 39 | cp ../../npm_shims/jest_mock.js . 40 | 41 | # shim our classes into the jest tests 42 | npm test -- --setupFilesAfterEnv='./jest_mock.js' "$filter_arg" 43 | cleanup 44 | -------------------------------------------------------------------------------- /crates/wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use liveview_native_core::diff::fragment::{FragmentMerge, Root, RootDiff}; 4 | use serde::Serialize; 5 | use wasm_bindgen::prelude::*; 6 | 7 | #[wasm_bindgen] 8 | pub struct Rendered { 9 | inner: Root, 10 | view_id: i32, 11 | } 12 | 13 | // These are the only thing that ever showed up 14 | // in the tests. 15 | #[derive(serde::Deserialize, serde::Serialize)] 16 | #[serde(untagged)] 17 | enum Event { 18 | String(String), 19 | Object(HashMap>), 20 | List(Vec), 21 | } 22 | 23 | #[derive(serde::Deserialize)] 24 | pub struct RenderedExtractedInput { 25 | #[serde(rename = "r")] 26 | reply: Option>, 27 | #[serde(rename = "t")] 28 | title: Option, 29 | #[serde(flatten)] 30 | diff: RootDiff, 31 | } 32 | 33 | #[derive(serde::Serialize)] 34 | pub struct RenderedExtractedOutput { 35 | reply: Option>, 36 | title: Option, 37 | events: Vec, 38 | diff: RootDiff, 39 | } 40 | 41 | impl From for RenderedExtractedOutput { 42 | fn from(value: RenderedExtractedInput) -> Self { 43 | Self { 44 | reply: value.reply, 45 | title: value.title, 46 | events: value 47 | .diff 48 | .events() 49 | .expect("Parse Error") 50 | .unwrap_or_default(), 51 | diff: value.diff, 52 | } 53 | } 54 | } 55 | 56 | #[wasm_bindgen] 57 | impl Rendered { 58 | #[wasm_bindgen(constructor)] 59 | pub fn new(view_id: i32, rendered: JsValue) -> Result { 60 | console_error_panic_hook::set_once(); 61 | let _ = console_log::init_with_level(log::Level::Info); 62 | log::debug!("RAW INITIAL DIFF: {rendered:#?}"); 63 | let root_diff: RootDiff = serde_wasm_bindgen::from_value(rendered)?; 64 | let root: Root = root_diff.try_into()?; 65 | 66 | Ok(Rendered { 67 | inner: root, 68 | view_id, 69 | }) 70 | } 71 | 72 | #[wasm_bindgen(js_name = "mergeDiff")] 73 | pub fn merge_diff(&mut self, diff: JsValue) -> Result<(), JsError> { 74 | log::debug!("RAW MERGE DIFF: {diff:#?}"); 75 | let diff: RootDiff = serde_wasm_bindgen::from_value(diff)?; 76 | log::debug!("DIFF: {diff:#?}"); 77 | self.inner = self.inner.clone().merge(diff)?; 78 | log::debug!("MERGED: {:#?}", self.inner); 79 | 80 | Ok(()) 81 | } 82 | 83 | #[wasm_bindgen(js_name = "parentViewId")] 84 | pub fn parent_view_id(&self) -> String { 85 | format!("{}", self.view_id) 86 | } 87 | 88 | #[wasm_bindgen(js_name = "isComponentOnlyDiff")] 89 | pub fn is_component_only_diff(&self, diff: JsValue) -> Result { 90 | let diff: RootDiff = serde_wasm_bindgen::from_value(diff)?; 91 | let root: Root = diff.try_into()?; 92 | 93 | Ok(root.is_component_only_diff()) 94 | } 95 | #[wasm_bindgen(js_name = "componentCIDs")] 96 | pub fn component_cids(&self, diff: JsValue) -> Result, JsError> { 97 | let diff: RootDiff = serde_wasm_bindgen::from_value(diff)?; 98 | let root: Root = diff.try_into()?; 99 | Ok(root.component_cids()) 100 | } 101 | #[wasm_bindgen(js_name = "getComponent")] 102 | pub fn get_component(&self, diff: JsValue, cid: i32) -> Result { 103 | let diff: RootDiff = serde_wasm_bindgen::from_value(diff)?; 104 | let root: Root = diff.try_into()?; 105 | let component = if let Some(component) = root.get_component(cid) { 106 | component 107 | } else { 108 | return Ok(JsValue::null()); 109 | }; 110 | 111 | let serializer = serde_wasm_bindgen::Serializer::json_compatible(); 112 | 113 | let component = component 114 | .serialize(&serializer) 115 | .expect("Failed to serialize"); 116 | 117 | Ok(component) 118 | } 119 | 120 | #[wasm_bindgen(js_name = "isNewFingerprint")] 121 | pub fn is_new_fingerprint(&self, diff: JsValue) -> bool { 122 | let diff: RootDiff = if let Ok(diff) = serde_wasm_bindgen::from_value(diff) { 123 | diff 124 | } else { 125 | return false; 126 | }; 127 | let root: Root = if let Ok(root) = diff.try_into() { 128 | root 129 | } else { 130 | return false; 131 | }; 132 | root.is_new_fingerprint() 133 | } 134 | 135 | pub fn get(&self) -> Result { 136 | let serializer = serde_wasm_bindgen::Serializer::json_compatible(); 137 | 138 | let map = self 139 | .inner 140 | .serialize(&serializer) 141 | .expect("Failed to serialize"); 142 | Ok(map) 143 | } 144 | #[wasm_bindgen(js_name = "toString")] 145 | pub fn to_string(&self) -> Result { 146 | let out = js_sys::Array::new(); 147 | let rendered: String = self.inner.clone().try_into()?; 148 | out.push(&rendered.into()); 149 | let streams = js_sys::Set::default(); 150 | out.push(&streams); 151 | 152 | Ok(out.into()) 153 | } 154 | 155 | pub fn extract(diff: JsValue) -> Result { 156 | let extracted: RenderedExtractedInput = serde_wasm_bindgen::from_value(diff)?; 157 | let extracted: RenderedExtractedOutput = extracted.into(); 158 | // This is needed because various fields in RootDiff won't be included. 159 | // The json compatible serializer is a bit more costly. 160 | // https://github.com/RReverser/serde-wasm-bindgen?tab=readme-ov-file#supported-types 161 | let serializer = serde_wasm_bindgen::Serializer::json_compatible(); 162 | 163 | let out = extracted 164 | .serialize(&serializer) 165 | .expect("Failed to serialize"); 166 | Ok(out) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - ls ./ 5 | # Running preparation script 6 | - ./crates/core/liveview-native-core-jetpack/scripts/prepareJitpackEnvironment.sh 7 | # Installing Java 8 because Android SDK manager needs XML Binds which not exists in JDK 17 8 | - sdk install java 8.0.265-open 9 | - sdk use java 8.0.265-open 10 | # Installing the necessary Android SDK packages (accepting licenses, NDK, CMake) 11 | - yes | sdkmanager --licenses 12 | - yes | sdkmanager --update 13 | - yes | sdkmanager --uninstall "ndk-bundle" 14 | - yes | sdkmanager --install "ndk;26.3.11579264" 15 | - yes | sdkmanager --install "cmake;3.22.1" 16 | # Install JDK 17 and using it for the rest of the build 17 | - sdk install java 17.0.3-tem 18 | - sdk use java 17.0.3-tem 19 | install: 20 | - echo "Running a custom install command" 21 | - uname -a 22 | - ls $ANDROID_HOME/ndk/26.3.11579264/toolchains/llvm/prebuilt/*/bin/* 23 | - cd ./crates/core/liveview-native-core-jetpack/ 24 | - source "$HOME/.cargo/env" 25 | - ./gradlew buildDebugStaticLib 26 | - ls ../../../target/debug/* 27 | - ./gradlew build assembleRelease publishToMavenLocal -x test 28 | env: 29 | # These environment variables are used by the Rust/Gradle plugin 30 | RUST_ANDROID_GRADLE_RUSTC_COMMAND: "/home/jitpack/.cargo/bin/rustc" 31 | RUST_ANDROID_GRADLE_CARGO_COMMAND: "/home/jitpack/.cargo/bin/cargo" 32 | RUST_ANDROID_GRADLE_RUSTUP_CHANNEL: "nightly" 33 | RANLIB: "/opt/android-sdk-linux/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib" 34 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Crate" 3 | newline_style = "Unix" 4 | tab_spaces = 4 5 | -------------------------------------------------------------------------------- /tests/support/test_server/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /tests/support/test_server/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | test_server-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /tests/support/test_server/README.md: -------------------------------------------------------------------------------- 1 | # TestServer 2 | 3 | To start your Phoenix server: 4 | 5 | * Run `mix setup` to install and setup dependencies 6 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | 10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 11 | 12 | ## Learn more 13 | 14 | * Official website: https://www.phoenixframework.org/ 15 | * Guides: https://hexdocs.pm/phoenix/overview.html 16 | * Docs: https://hexdocs.pm/phoenix 17 | * Forum: https://elixirforum.com/c/phoenix-forum 18 | * Source: https://github.com/phoenixframework/phoenix 19 | -------------------------------------------------------------------------------- /tests/support/test_server/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Test Server' 2 | description: 'An action for setting up the test server' 3 | runs: 4 | using: "composite" 5 | steps: 6 | 7 | - name: Set up Elixir 8 | if: ${{ runner.os == 'macOS' }} 9 | run: brew install elixir 10 | shell: bash 11 | 12 | - uses: erlef/setup-beam@v1 13 | if: ${{ runner.os == 'Linux' }} 14 | with: 15 | elixir-version: 1.17 16 | otp-version: 27 17 | 18 | - name: Set up inotify-tools 19 | if: ${{ runner.os == 'Linux' }} 20 | run: sudo apt-get install inotify-tools 21 | shell: bash 22 | 23 | - name: Cache mix dependencies and build 24 | uses: actions/cache@v4 25 | id: mix-cache 26 | with: 27 | path: | 28 | tests/support/test_server/deps/ 29 | tests/support/test_server/_build/ 30 | key: mix-${{ github.workflow }}-${{ runner.os }}-${{runner.arch}}-${{ hashFiles('**/mix.lock') }} 31 | 32 | - run: | 33 | mix deps.get 34 | working-directory: ./tests/support/test_server 35 | shell: bash 36 | 37 | - run: mix compile 38 | working-directory: ./tests/support/test_server 39 | shell: bash 40 | 41 | - name: Run in background 42 | shell: bash 43 | working-directory: ./tests/support/test_server 44 | env: 45 | MIX_ENV: dev 46 | run: mix phx.server > server.stdout.txt 2> server.stderr.txt & disown 47 | -------------------------------------------------------------------------------- /tests/support/test_server/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | /* This file is for your main application CSS */ 6 | -------------------------------------------------------------------------------- /tests/support/test_server/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | let Hooks = {} 24 | Hooks.PhoneNumber = { 25 | mounted() { 26 | this.el.addEventListener("input", e => { 27 | let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/) 28 | if(match) { 29 | this.el.value = `${match[1]}-${match[2]}-${match[3]}` 30 | } 31 | }) 32 | } 33 | } 34 | 35 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 36 | let liveSocket = new LiveSocket("/live", 37 | Socket, 38 | { 39 | params: {_csrf_token: csrfToken}, 40 | hooks: Hooks, 41 | } 42 | ) 43 | 44 | // Show progress bar on live navigation and form submits 45 | 46 | // connect if there are any LiveViews on the page 47 | liveSocket.connect() 48 | 49 | // expose liveSocket on window for web console debug logs and latency simulation: 50 | // >> liveSocket.enableDebug() 51 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 52 | // >> liveSocket.disableLatencySim() 53 | window.liveSocket = liveSocket 54 | 55 | -------------------------------------------------------------------------------- /tests/support/test_server/assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | 4 | module.exports = { 5 | content: [ 6 | "./js/**/*.js", 7 | "../lib/test_server_web.ex", 8 | "../lib/test_server_web/**/*.*ex" 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | brand: "#FD4F00", 14 | } 15 | }, 16 | }, 17 | plugins: [ ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/support/test_server/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :test_server, 11 | generators: [timestamp_type: :utc_datetime] 12 | 13 | # Configures the endpoint 14 | config :test_server, TestServerWeb.Endpoint, 15 | url: [host: "localhost"], 16 | adapter: Phoenix.Endpoint.Cowboy2Adapter, 17 | render_errors: [ 18 | formats: [html: TestServerWeb.ErrorHTML, json: TestServerWeb.ErrorJSON], 19 | layout: false 20 | ], 21 | pubsub_server: TestServer.PubSub, 22 | live_view: [signing_salt: "B40/4H2u"] 23 | 24 | # Configures the mailer 25 | # 26 | # By default it uses the "Local" adapter which stores the emails 27 | # locally. You can see the emails in your browser, at "/dev/mailbox". 28 | # 29 | # For production it's recommended to configure a different adapter 30 | # at the `config/runtime.exs`. 31 | config :test_server, TestServer.Mailer, adapter: Swoosh.Adapters.Local 32 | 33 | config :live_view_native, plugins: [ 34 | LiveViewNative.SwiftUI, 35 | LiveViewNative.Jetpack, 36 | ] 37 | 38 | config :mime, :types, %{ 39 | "text/swiftui" => ["swiftui"], 40 | "text/jetpack" => ["jetpack"], 41 | "text/styles" => ["styles"] 42 | } 43 | 44 | # LVN - Required, you must configure LiveView Native Stylesheets 45 | # on where class names should be extracted from 46 | config :live_view_native_stylesheet, 47 | content: [ 48 | swiftui: [ 49 | "lib/**/*swiftui*" 50 | ], 51 | jetpack: [ 52 | "lib/**/*jetpack*" 53 | ] 54 | ], 55 | output: "priv/static/assets" 56 | 57 | config :live_view_native_stylesheet, 58 | parsers: [ 59 | swiftui: LiveViewNative.SwiftUI.RulesParser 60 | ] 61 | 62 | # LVN - Required, you must configure Phoenix to know how 63 | # to encode for the swiftui format 64 | config :phoenix_template, :format_encoders, [ 65 | swiftui: Phoenix.HTML.Engine, 66 | jetpack: Phoenix.HTML.Engine 67 | ] 68 | 69 | # LVN - Required, you must configure Phoenix so it knows 70 | # how to compile LVN's neex templates 71 | config :phoenix, :template_engines, neex: LiveViewNative.Engine 72 | 73 | # Configure esbuild (the version is required) 74 | config :esbuild, 75 | version: "0.17.11", 76 | default: [ 77 | args: 78 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 79 | cd: Path.expand("../assets", __DIR__), 80 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 81 | ] 82 | 83 | # Configure tailwind (the version is required) 84 | config :tailwind, 85 | version: "3.3.2", 86 | default: [ 87 | args: ~w( 88 | --config=tailwind.config.js 89 | --input=css/app.css 90 | --output=../priv/static/assets/app.css 91 | ), 92 | cd: Path.expand("../assets", __DIR__) 93 | ] 94 | 95 | # Configures Elixir's Logger 96 | config :logger, :console, 97 | format: "$time $metadata[$level] $message\n", 98 | metadata: [:request_id] 99 | 100 | # Use Jason for JSON parsing in Phoenix 101 | config :phoenix, :json_library, Jason 102 | 103 | # Import environment specific config. This must remain at the bottom 104 | # of this file so it overrides the configuration defined above. 105 | import_config "#{config_env()}.exs" 106 | -------------------------------------------------------------------------------- /tests/support/test_server/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we can use it 8 | # to bundle .js and .css sources. 9 | config :test_server, TestServerWeb.Endpoint, 10 | # Binding to loopback ipv4 address prevents access from other machines. 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4001], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "p3kccsHPe2TBCKQjkLIf8x22bHhdS7V1POdeUrrdxE9G5sQeQKBjLbCDFa7uvOP5", 17 | watchers: [ 18 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, 19 | tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} 20 | ] 21 | 22 | # ## SSL Support 23 | # 24 | # In order to use HTTPS in development, a self-signed 25 | # certificate can be generated by running the following 26 | # Mix task: 27 | # 28 | # mix phx.gen.cert 29 | # 30 | # Run `mix help phx.gen.cert` for more information. 31 | # 32 | # The `http:` config above can be replaced with: 33 | # 34 | # https: [ 35 | # port: 4001, 36 | # cipher_suite: :strong, 37 | # keyfile: "priv/cert/selfsigned_key.pem", 38 | # certfile: "priv/cert/selfsigned.pem" 39 | # ], 40 | # 41 | # If desired, both `http:` and `https:` keys can be 42 | # configured to run both http and https servers on 43 | # different ports. 44 | 45 | # Watch static and templates for browser reloading. 46 | config :test_server, TestServerWeb.Endpoint, 47 | live_reload: [ 48 | patterns: [ 49 | ~r"priv/static/assets/(js|css|png|jpeg|jpg|gif|svg)$", 50 | ~r"priv/static/images/(js|css|png|jpeg|jpg|gif|svg)$", 51 | ~r"priv/static/(js|css|png|jpeg|jpg|gif|svg)$", 52 | ~r"priv/gettext/.*(po)$", 53 | ~r"lib/test_server_web/(controllers|live|components)/.*(ex|heex)$" 54 | ] 55 | ] 56 | 57 | # Enable dev routes for dashboard and mailbox 58 | config :test_server, dev_routes: true 59 | 60 | # Do not include metadata nor timestamps in development logs 61 | config :logger, :console, format: "[$level] $message\n" 62 | 63 | # Set a higher stacktrace during development. Avoid configuring such 64 | # in production as building large stacktraces may be expensive. 65 | config :phoenix, :stacktrace_depth, 20 66 | 67 | # Initialize plugs at runtime for faster development compilation 68 | config :phoenix, :plug_init_mode, :runtime 69 | 70 | # Include HEEx debug annotations as HTML comments in rendered markup 71 | config :phoenix_live_view, :debug_heex_annotations, true 72 | 73 | # Disable swoosh api client as it is only required for production adapters. 74 | config :swoosh, :api_client, false 75 | 76 | config :live_view_native_stylesheet, 77 | annotations: true, 78 | pretty: true 79 | -------------------------------------------------------------------------------- /tests/support/test_server/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :test_server, TestServerWeb.Endpoint, 9 | cache_static_manifest: "priv/static/cache_manifest.json" 10 | 11 | # Configures Swoosh API Client 12 | config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: TestServer.Finch 13 | 14 | # Disable Swoosh Local Memory Storage 15 | config :swoosh, local: false 16 | 17 | # Do not print debug messages in production 18 | config :logger, level: :info 19 | 20 | # Runtime production configuration, including reading 21 | # of environment variables, is done on config/runtime.exs. 22 | -------------------------------------------------------------------------------- /tests/support/test_server/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/test_server start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :test_server, TestServerWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :test_server, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 40 | 41 | config :test_server, TestServerWeb.Endpoint, 42 | url: [host: host, port: 443, scheme: "https"], 43 | http: [ 44 | # Enable IPv6 and bind on all interfaces. 45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 46 | # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html 47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 48 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 49 | port: port 50 | ], 51 | secret_key_base: secret_key_base 52 | 53 | # ## SSL Support 54 | # 55 | # To get SSL working, you will need to add the `https` key 56 | # to your endpoint configuration: 57 | # 58 | # config :test_server, TestServerWeb.Endpoint, 59 | # https: [ 60 | # ..., 61 | # port: 443, 62 | # cipher_suite: :strong, 63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 65 | # ] 66 | # 67 | # The `cipher_suite` is set to `:strong` to support only the 68 | # latest and more secure SSL ciphers. This means old browsers 69 | # and clients may not be supported. You can set it to 70 | # `:compatible` for wider support. 71 | # 72 | # `:keyfile` and `:certfile` expect an absolute path to the key 73 | # and cert in disk or a relative path inside priv, for example 74 | # "priv/ssl/server.key". For all supported SSL configuration 75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 76 | # 77 | # We also recommend setting `force_ssl` in your endpoint, ensuring 78 | # no data is ever sent via http, always redirecting to https: 79 | # 80 | # config :test_server, TestServerWeb.Endpoint, 81 | # force_ssl: [hsts: true] 82 | # 83 | # Check `Plug.SSL` for all available options in `force_ssl`. 84 | 85 | # ## Configuring the mailer 86 | # 87 | # In production you need to configure the mailer to use a different adapter. 88 | # Also, you may need to configure the Swoosh API client of your choice if you 89 | # are not using SMTP. Here is an example of the configuration: 90 | # 91 | # config :test_server, TestServer.Mailer, 92 | # adapter: Swoosh.Adapters.Mailgun, 93 | # api_key: System.get_env("MAILGUN_API_KEY"), 94 | # domain: System.get_env("MAILGUN_DOMAIN") 95 | # 96 | # For this example you need include a HTTP client required by Swoosh API client. 97 | # Swoosh supports Hackney and Finch out of the box: 98 | # 99 | # config :swoosh, :api_client, Swoosh.ApiClient.Hackney 100 | # 101 | # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. 102 | end 103 | -------------------------------------------------------------------------------- /tests/support/test_server/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :test_server, TestServerWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "DDssS1iT6DUJCZDjdF8G96oAe0ltmvXxizf1ll50h6WVtu90IarmexIMkrfDl79x", 8 | server: false 9 | 10 | # In test we don't send emails. 11 | config :test_server, TestServer.Mailer, adapter: Swoosh.Adapters.Test 12 | 13 | # Disable swoosh api client as it is only required for production adapters. 14 | config :swoosh, :api_client, false 15 | 16 | # Print only warnings and errors during test 17 | config :logger, level: :warning 18 | 19 | # Initialize plugs at runtime for faster test compilation 20 | config :phoenix, :plug_init_mode, :runtime 21 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServer do 2 | @moduledoc """ 3 | TestServer keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server/application.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServer.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | TestServerWeb.Telemetry, 12 | {DNSCluster, query: Application.get_env(:test_server, :dns_cluster_query) || :ignore}, 13 | {Phoenix.PubSub, name: TestServer.PubSub}, 14 | # Start the Finch HTTP client for sending emails 15 | {Finch, name: TestServer.Finch}, 16 | # Start a worker by calling: TestServer.Worker.start_link(arg) 17 | # {TestServer.Worker, arg}, 18 | # Start to serve requests, typically the last entry 19 | TestServerWeb.Endpoint, 20 | TestServerWeb.SongPublisher, 21 | ] 22 | 23 | # See https://hexdocs.pm/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: TestServer.Supervisor] 26 | Supervisor.start_link(children, opts) 27 | end 28 | 29 | # Tell Phoenix to update the endpoint configuration 30 | # whenever the application is updated. 31 | @impl true 32 | def config_change(changed, _new, removed) do 33 | TestServerWeb.Endpoint.config_change(changed, removed) 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServer.Mailer do 2 | use Swoosh.Mailer, otp_app: :test_server 3 | end 4 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server/song_publisher.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule TestServerWeb.SongPublisher do 3 | use GenServer 4 | alias TestServer.{Song} 5 | 6 | def start_link(_opts) do 7 | GenServer.start_link(__MODULE__, %{}) 8 | end 9 | 10 | @impl true 11 | def init(_) do 12 | :timer.send_interval(10000, :update) 13 | {:ok, 1} 14 | end 15 | 16 | @impl true 17 | def handle_info(:update, count) 18 | do 19 | new_song = %Song{id: count, title: "song #{count}"} 20 | Phoenix.PubSub.broadcast(TestServer.PubSub, "songs", new_song) 21 | IO.puts("Sending song for count #{count}") 22 | {:noreply, count + 1} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server/songs.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServer.Song do 2 | defstruct title: "Never gonna give you up", id: 1 3 | end 4 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_native.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServerNative do 2 | @moduledoc """ 3 | The entrypoint for defining your native interfaces, such 4 | as components, render components, layouts, and live views. 5 | 6 | This can be used in your application as: 7 | 8 | use TestServerNative, :live_view 9 | 10 | The definitions below will be executed for every 11 | component, so keep them short and clean, focused 12 | on imports, uses and aliases. 13 | 14 | Do NOT define functions inside the quoted expressions 15 | below. Instead, define additional modules and import 16 | those modules here. 17 | """ 18 | 19 | import TestServerWeb, only: [verified_routes: 0] 20 | 21 | @doc ~S''' 22 | Set up an existing LiveView module for use with LiveView Native 23 | 24 | defmodule MyAppWeb.HomeLive do 25 | use MyAppWeb, :live_view 26 | use MyAppNative, :live_view 27 | end 28 | 29 | An `on_mount` callback will be injected that will negotiate 30 | the inbound connection content type. If it is a LiveView Native 31 | type the `render/1` will be delegated to the format-specific 32 | render component. 33 | ''' 34 | def live_view() do 35 | quote do 36 | use LiveViewNative.LiveView, 37 | formats: [ 38 | :jetpack, 39 | :swiftui 40 | ], 41 | layouts: [ 42 | jetpack: {TestServerWeb.Layouts.Jetpack, :app}, 43 | swiftui: {TestServerWeb.Layouts.SwiftUI, :app} 44 | ] 45 | 46 | unquote(verified_routes()) 47 | end 48 | end 49 | 50 | @doc ~S''' 51 | Set up a module as a LiveView Native format-specific render component 52 | 53 | defmodule MyAppWeb.HomeLive.SwiftUI do 54 | use MyAppNative, [:render_component, format: :swiftui] 55 | 56 | def render(assigns, _interface) do 57 | ~LVN""" 58 | Hello, world! 59 | """ 60 | end 61 | end 62 | ''' 63 | def render_component(opts) do 64 | opts = 65 | opts 66 | |> Keyword.take([:format]) 67 | |> Keyword.put(:as, :render) 68 | 69 | quote do 70 | use LiveViewNative.Component, unquote(opts) 71 | 72 | unquote(helpers(opts[:format])) 73 | end 74 | end 75 | 76 | @doc ~S''' 77 | Set up a module as a LiveView Native Component 78 | 79 | defmodule MyAppWeb.Components.CustomSwiftUI do 80 | use MyAppNative, [:component, format: :swiftui] 81 | 82 | attr :msg, :string, :required 83 | def home_textk(assigns) do 84 | ~LVN""" 85 | @msg 86 | """ 87 | end 88 | end 89 | 90 | LiveView Native Components are identical to Phoenix Components. Please 91 | refer to the `Phoenix.Component` documentation for more information. 92 | ''' 93 | def component(opts) do 94 | opts = Keyword.take(opts, [:format, :root, :as]) 95 | 96 | quote do 97 | use LiveViewNative.Component, unquote(opts) 98 | 99 | unquote(helpers(opts[:format])) 100 | end 101 | end 102 | 103 | @doc ~S''' 104 | Set up a module as a LiveView Natve Layout Component 105 | 106 | defmodule MyAppWeb.Layouts.SwiftUI do 107 | use MyAppNative, [:layout, format: :swiftui] 108 | 109 | embed_templates "layouts_swiftui/*" 110 | end 111 | ''' 112 | def layout(opts) do 113 | opts = Keyword.take(opts, [:format, :root]) 114 | 115 | quote do 116 | use LiveViewNative.Component, unquote(opts) 117 | 118 | import LiveViewNative.Component, only: [csrf_token: 1] 119 | 120 | unquote(helpers(opts[:format])) 121 | end 122 | end 123 | 124 | defp helpers(format) do 125 | gettext_quoted = quote do 126 | import TestServerWeb.Gettext 127 | end 128 | 129 | plugin = LiveViewNative.fetch_plugin!(format) 130 | plugin_component_quoted = try do 131 | Code.ensure_compiled!(plugin.component) 132 | 133 | quote do 134 | import unquote(plugin.component) 135 | end 136 | rescue 137 | _ -> nil 138 | end 139 | 140 | live_form_quoted = quote do 141 | import LiveViewNative.LiveForm.Component 142 | end 143 | 144 | core_component_module = Module.concat([TestServerWeb, CoreComponents, plugin.module_suffix]) 145 | 146 | core_component_quoted = try do 147 | Code.ensure_compiled!(core_component_module) 148 | 149 | quote do 150 | import unquote(core_component_module) 151 | end 152 | rescue 153 | _ -> nil 154 | end 155 | 156 | [ 157 | gettext_quoted, 158 | plugin_component_quoted, 159 | live_form_quoted, 160 | core_component_quoted, 161 | verified_routes() 162 | ] 163 | 164 | end 165 | 166 | @doc """ 167 | When used, dispatch to the appropriate controller/view/etc. 168 | """ 169 | defmacro __using__([which | opts]) when is_atom(which) do 170 | apply(__MODULE__, which, [opts]) 171 | end 172 | 173 | defmacro __using__(which) when is_atom(which) do 174 | apply(__MODULE__, which, []) 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServerWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use TestServerWeb, :controller 9 | use TestServerWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: TestServerWeb.Layouts] 44 | 45 | import Plug.Conn 46 | import TestServerWeb.Gettext 47 | 48 | unquote(verified_routes()) 49 | end 50 | end 51 | 52 | def live_view do 53 | quote do 54 | use Phoenix.LiveView, 55 | layout: {TestServerWeb.Layouts, :app} 56 | 57 | unquote(html_helpers()) 58 | 59 | end 60 | end 61 | 62 | def live_component do 63 | quote do 64 | use Phoenix.LiveComponent 65 | 66 | unquote(html_helpers()) 67 | 68 | end 69 | end 70 | 71 | def html do 72 | quote do 73 | use Phoenix.Component 74 | 75 | # Import convenience functions from controllers 76 | import Phoenix.Controller, 77 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 78 | 79 | # Include general helpers for rendering HTML 80 | unquote(html_helpers()) 81 | end 82 | end 83 | 84 | defp html_helpers do 85 | quote do 86 | # HTML escaping functionality 87 | import Phoenix.HTML 88 | # Core UI components and translation 89 | import TestServerWeb.CoreComponents 90 | import TestServerWeb.Gettext 91 | 92 | # Shortcut for generating JS commands 93 | alias Phoenix.LiveView.JS 94 | 95 | # Routes generation with the ~p sigil 96 | unquote(verified_routes()) 97 | end 98 | end 99 | 100 | def verified_routes do 101 | quote do 102 | use Phoenix.VerifiedRoutes, 103 | endpoint: TestServerWeb.Endpoint, 104 | router: TestServerWeb.Router, 105 | statics: TestServerWeb.static_paths() 106 | end 107 | end 108 | 109 | @doc """ 110 | When used, dispatch to the appropriate controller/view/etc. 111 | """ 112 | defmacro __using__(which) when is_atom(which) do 113 | apply(__MODULE__, which, []) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServerWeb.Layouts do 2 | use TestServerWeb, :html 3 | # use LiveViewNative.Layouts 4 | 5 | embed_templates "layouts/*" 6 | end 7 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web/components/layouts.jetpack.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServerWeb.Layouts.Jetpack do 2 | use TestServerNative, [:layout, format: :jetpack] 3 | 4 | embed_templates "layouts_jetpack/*" 5 | end 6 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web/components/layouts.swiftui.ex: -------------------------------------------------------------------------------- 1 | defmodule TestServerWeb.Layouts.SwiftUI do 2 | use TestServerNative, [:layout, format: :swiftui] 3 | 4 | embed_templates "layouts_swiftui/*" 5 | end 6 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |

8 | v<%= Application.spec(:phoenix, :vsn) %> 9 |

10 |
11 | 25 |
26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | <%= @inner_content %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title suffix=" · Phoenix Framework"> 8 | <%= assigns[:page_title] || "TestServer" %> 9 | 10 | 11 | 13 | 14 | 15 | <%= @inner_content %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web/components/layouts_jetpack/app.jetpack.neex: -------------------------------------------------------------------------------- 1 | <%= @inner_content %> -------------------------------------------------------------------------------- /tests/support/test_server/lib/test_server_web/components/layouts_jetpack/root.jetpack.neex: -------------------------------------------------------------------------------- 1 | 2 |