diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 0000000..ae76be3 --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,137 @@ +name: WebAssembly CI + +on: + push: + branches: + - main + - issue-* + paths: + - '.github/workflows/wasm.yml' + - 'Cargo.lock' + - 'Cargo.toml' + - 'package-lock.json' + - 'package.json' + - 'rust/**' + - 'src/**' + - 'tests/**' + - 'web/**' + pull_request: + branches: + - main + paths: + - '.github/workflows/wasm.yml' + - 'Cargo.lock' + - 'Cargo.toml' + - 'package-lock.json' + - 'package.json' + - 'rust/**' + - 'src/**' + - 'tests/**' + - 'web/**' + +concurrency: + group: wasm-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Install wasm-pack + run: cargo install wasm-pack --version 0.14.0 --locked + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: npm + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + rust/target + key: ${{ runner.os }}-wasm-cargo-${{ hashFiles('Cargo.lock', 'rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-wasm-cargo- + + - name: Install npm dependencies + run: npm ci + + - name: Test Rust CLI core + run: cargo test --manifest-path rust/Cargo.toml --all-features + + - name: Test WebAssembly wrapper + run: npm run test:wasm + + - name: Build React WebAssembly app + run: npm run build + + - name: Upload built app + uses: actions/upload-artifact@v4 + with: + name: link-cli-web + path: dist/ + + deploy: + name: Deploy GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Install wasm-pack + run: cargo install wasm-pack --version 0.14.0 --locked + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: Build GitHub Pages app + run: npm run build:pages + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: dist/ + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index b2dc854..2654427 100644 --- a/.gitignore +++ b/.gitignore @@ -402,4 +402,19 @@ db.links db.names.links # Rust build artifacts -rust/target/ \ No newline at end of file +target/ +rust/target/ + +# JavaScript and WebAssembly build artifacts +node_modules/ +dist/ +pkg/ +pkg-node/ +pkg-bundler/ +web/pkg/ + +# Local AI investigation artifacts +ci-logs/ +gh-data/ +experiments/ +.playwright-mcp/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2c85b0b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,905 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clink-wasm" +version = "2.3.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "link-cli", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", + "wee_alloc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.3", + "wasm-bindgen", +] + +[[package]] +name = "ctor" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "doublets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5acc4914b466aabfcf42c60b4adbb6e3fc38fb241cd30f50eab36fcc7e9872" +dependencies = [ + "cfg-if 1.0.3", + "leak_slice", + "platform-data", + "platform-mem", + "platform-num", + "platform-trees", + "tap", + "thiserror 2.0.18", +] + +[[package]] +name = "dtor" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if 1.0.3", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if 1.0.3", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leak_slice" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecf3387da9fb41906394e1306ddd3cd26dd9b7177af11c19b45b364b743aed26" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "link-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "doublets", + "links-notation", + "lino-arguments", + "thiserror 2.0.18", +] + +[[package]] +name = "links-notation" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c952b42a8c6ff6f849d7cafe3b1e13f1063a51bbb144bc6c62026ab327814c" +dependencies = [ + "nom", +] + +[[package]] +name = "lino-arguments" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be512a5c5eacea6ef5ec015fb0c7e1725c8e4cda1befd31606e203f281069968" +dependencies = [ + "clap", + "ctor", + "dotenvy", + "lino-env", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "lino-env" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f453c53827aabe91a3d3856d61d14ae3867ab1a4344db22f9fa5396664c8d0e" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "platform-data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6782bc71345465116de96d250a36dcf49336010a2320d958d12a5d4390186c90" +dependencies = [ + "beef", + "platform-num", + "thiserror 2.0.18", +] + +[[package]] +name = "platform-mem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27cff7c92440ac926c8c91ea3151db6e52a262f602d0c157f254e422fc15b12" +dependencies = [ + "allocator-api2", + "memmap2", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "platform-num" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ca8e18138b1c90ad802aff931f946a0e6bd760c35af30f1ff2489489ab54a" +dependencies = [ + "num-traits", +] + +[[package]] +name = "platform-trees" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e25a531617fa762c8505826c930f6c1cfcc226f63dea09882b56ae0b8ed078" +dependencies = [ + "platform-num", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if 1.0.3", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29826f9d9ecaa314c480d376b276d1c790e6cb6a4681fab8532da69cbabf977d" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c610311887f9e6599a546d278d12d69dfd3a3e92639b2129e4b11ad6cf1961d6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60238e5b4b1b295701d6f9a66d2a126fe19990348f5fb9dae3b623a370119d94" + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5e4fb8a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "clink-wasm" +version = "2.3.0" +authors = ["link-foundation"] +edition = "2021" +description = "Browser WebAssembly wrapper for the Rust link-cli core" +license = "Unlicense" +repository = "https://github.com/link-foundation/link-cli" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +anyhow = "1.0" +link-cli = { path = "rust" } +wasm-bindgen = "0.2" +console_error_panic_hook = { version = "0.1.6", optional = true } +wee_alloc = { version = "0.4.5", optional = true } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dev-dependencies] +wasm-bindgen-test = "0.3" + +[dependencies.web-sys] +version = "0.3" +features = [ + "console", +] + +[features] +default = ["console_error_panic_hook"] + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/README-WASM.md b/README-WASM.md new file mode 100644 index 0000000..a03eccf --- /dev/null +++ b/README-WASM.md @@ -0,0 +1,90 @@ +# link-cli WebAssembly Workbench + +The browser workbench combines three runtimes: + +- Rust `link-cli` core compiled to WebAssembly through the root `clink-wasm` + crate. +- React and Vite for the single-page browser interface in `web/`. +- `doublets-web@0.1.2` for a live WebAssembly `UnitedLinks` mirror built from + the current query result. + +## Local Development + +```bash +rustup target add wasm32-unknown-unknown +cargo install wasm-pack --version 0.14.0 --locked +npm install +npm run dev +``` + +The dev script builds the Rust WebAssembly wrapper into `web/pkg/` and starts a +Vite server. + +## Production Build + +```bash +npm run build +``` + +This creates: + +- `web/pkg/`: generated `wasm-pack --target web` package for the Rust wrapper. +- `dist/`: static React app ready for GitHub Pages. + +For the same base path used by GitHub Pages: + +```bash +npm run build:pages +``` + +## API + +```js +import init, { Clink } from './pkg/clink_wasm.js'; + +await init(); + +const clink = new Clink(); +const result = JSON.parse( + clink.execute( + '() ((child: father mother))', + JSON.stringify({ + before: false, + changes: true, + after: true, + autoCreateMissingReferences: true, + }), + ), +); + +console.log(result.output); +console.log(result.links); +``` + +`Clink#execute(query, optionsJson)` returns: + +```json +{ + "success": true, + "output": "() ((child: father mother))", + "error": null, + "links": [ + { "id": 1, "source": 1, "target": 1, "name": "father" } + ] +} +``` + +Supported options are `before`, `changes`, `after`, `trace`, +`autoCreateMissingReferences`, and `structure`. + +## Verification + +```bash +cargo test --manifest-path rust/Cargo.toml --all-features +cargo test --lib +npm run test:wasm +npm run build +``` + +The WebAssembly CI workflow runs these checks and deploys `dist/` to GitHub +Pages from `main`. diff --git a/README.md b/README.md index 662e1e6..7a6dacd 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,19 @@ It is based on [associative theory](https://habr.com/ru/articles/895896) (also i It uses C# implementation of [a links data store](https://github.com/linksplatform?view_as=public) (see also in [ru](https://github.com/linksplatform/.github/blob/main/profile/README.ru.md)). +## WebAssembly Browser Workbench + +`clink` can run in the browser through the Rust query processor compiled to +WebAssembly. The React workbench mirrors the current link set into +[`doublets-web`](https://www.npmjs.com/package/doublets-web), the WebAssembly +package built from `doublets-rs`. + +- Live demo: +- Browser app documentation: [README-WASM.md](README-WASM.md) +- Implementation notes: [WEBASSEMBLY_IMPLEMENTATION.md](WEBASSEMBLY_IMPLEMENTATION.md) + +## Installation + This CLI tool can be installed globally as `clink` using single command (that will work if you have [.NET](https://dotnet.microsoft.com/en-us/download) installed): ```bash diff --git a/WEBASSEMBLY_IMPLEMENTATION.md b/WEBASSEMBLY_IMPLEMENTATION.md new file mode 100644 index 0000000..973d21f --- /dev/null +++ b/WEBASSEMBLY_IMPLEMENTATION.md @@ -0,0 +1,83 @@ +# WebAssembly Implementation + +Issue #12 asks for a browser-executable `link-cli` experience based on the Rust +implementation of Doublets. The current implementation uses the Rust `link-cli` +core from `rust/`, compiles a small wrapper crate with `wasm-pack`, and renders a +React single-page app for GitHub Pages. + +## Architecture + +```text +rust/ Native Rust link-cli library and clink binary +src/lib.rs wasm-bindgen wrapper around the Rust query processor +web/src/ React workbench +web/pkg/ Generated Rust WASM package, ignored by git +dist/ Generated GitHub Pages artifact, ignored by git +``` + +The browser app initializes two WebAssembly-backed runtimes: + +- `clink-wasm`: exposes `Clink#execute`, `Clink#snapshot`, and `Clink#reset`. + It uses an in-memory implementation of the `NamedTypeLinks` trait, so the same + Rust `QueryProcessor` used by the native CLI can run in the browser without + filesystem access. +- `doublets-web@0.1.2`: the latest npm release of the WebAssembly bindings for + `doublets-rs`. The React app mirrors the current `Clink` snapshot into a + `UnitedLinks` instance after each query. + +## Why the Old Proof of Concept Changed + +The previous branch had a root-level Rust parser and storage implementation +that duplicated only a small subset of CLI behavior. After merging current +`main`, the repository has a fuller Rust port under `rust/`, so the WASM wrapper +now delegates query semantics to that shared Rust core. + +## CI and Pages + +`.github/workflows/wasm.yml` now: + +1. Installs stable Rust with the `wasm32-unknown-unknown` target. +2. Installs npm dependencies with `npm ci`. +3. Runs the Rust CLI core tests. +4. Runs `wasm-pack test --node` for the wrapper. +5. Builds the React app. +6. Deploys `dist/` to GitHub Pages on pushes to `main`. + +The workflow uses current Pages and artifact actions: + +- `actions/upload-artifact@v4` +- `actions/configure-pages@v5` +- `actions/upload-pages-artifact@v3` +- `actions/deploy-pages@v4` + +## Local Commands + +```bash +cargo test --manifest-path rust/Cargo.toml --all-features +cargo test --lib +npm run test:wasm +npm run build +npm run dev +``` + +## Browser Data Model + +The page session is intentionally in-memory. Query results include a structured +`links` array: + +```json +[ + { "id": 1, "source": 1, "target": 1, "name": "father" }, + { "id": 2, "source": 2, "target": 2, "name": "mother" }, + { "id": 3, "source": 1, "target": 2, "name": "child" } +] +``` + +That array drives both the rendered graph and the `doublets-web` `UnitedLinks` +mirror. + +## Follow-Up Scope + +The current implementation proves the browser runtime and static deployment. +Durable browser storage can be added later with IndexedDB without changing the +Rust query processor API. diff --git a/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj b/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj index b72f585..dd007d6 100644 --- a/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj +++ b/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj @@ -15,7 +15,7 @@ link-foundation A CLI tool for links manipulation. clink - 2.2.2 + 2.3.0 Unlicense https://github.com/link-foundation/link-cli diff --git a/docs/case-studies/issue-12/README.md b/docs/case-studies/issue-12/README.md new file mode 100644 index 0000000..33353c8 --- /dev/null +++ b/docs/case-studies/issue-12/README.md @@ -0,0 +1,76 @@ +# Issue 12 Case Study: WebAssembly link-cli + +Issue: https://github.com/link-foundation/link-cli/issues/12 +Pull request: https://github.com/link-foundation/link-cli/pull/52 + +## Evidence Collected + +- `evidence/issue-12.json`: original issue details. +- `evidence/pr-52-conversation-comments.json`: PR comments, including the + 2026-04-30 request to merge current `main`, use GitHub Pages, support + `doublets-web`, and build a React browser demo. +- `evidence/ci-runs.json`: recent branch workflow runs. +- `evidence/npm-doublets-web.json`: npm metadata showing `doublets-web@0.1.2` + as the latest release. +- `evidence/doublets-web-issue-5.json`: dependency issue details from + https://github.com/linksplatform/doublets-web/issues/5. + +GitHub Actions log downloads for runs `17617650587` and `17617649689` returned +HTTP 410 on 2026-05-01, so the exact old log bodies were no longer available. +The run metadata still showed both failures were on branch SHA +`efe5e5984191ef2b4e1deb54ca083b363ef766ad` on 2025-09-10. + +## Timeline + +- 2025-09-10: PR branch CI failed in the old WebAssembly workflow on SHA + `efe5e5984191ef2b4e1deb54ca083b363ef766ad`. +- 2026-04-30: PR feedback requested fresh `main`, a GitHub Pages browser demo, + React, Rust WebAssembly, and latest `doublets-web` support. +- 2026-05-01: `doublets-web` issue 5 was closed; npm metadata reported latest + `doublets-web` as `0.1.2`. +- 2026-05-01: This PR branch was merged with current `main` and conflict + resolution kept the branch version bump while adopting the new `csharp/` + layout. + +## Requirements + +- Run `link-cli` directly in a browser. +- Use the Rust implementation rather than a separate partial parser. +- Use `doublets-web` as the browser-facing WebAssembly Doublets package. +- Provide a React single-page demo. +- Publish the demo with GitHub Pages. +- Keep CI checks current and reproducible. +- Collect issue and dependency context in `docs/case-studies/issue-12`. + +## Root Causes + +- The previous branch duplicated a small subset of query behavior in root-level + Rust files. After `main` added the full Rust CLI implementation under `rust/`, + that duplication became stale. +- The old WebAssembly workflow used outdated artifact/deployment actions and + published from the repository root rather than a static app artifact. +- Generated Rust build output was tracked in the PR, so local builds could dirty + the working tree. +- Documentation still referenced the old `/demo/www/` page and an npm package + shape that no longer matched the React/GitHub Pages requirement. + +## Solution Plan Applied + +- Merge current `main` into the PR branch. +- Replace the partial WASM query engine with a `wasm-bindgen` wrapper around the + Rust `link-cli` `QueryProcessor`. +- Add an in-memory browser storage implementation of `NamedTypeLinks`. +- Build a React/Vite workbench that imports the Rust wrapper and + `doublets-web@0.1.2`. +- Mirror each Rust query snapshot into a `doublets-web` `UnitedLinks` instance. +- Replace the old CI workflow with stable Rust, npm, `wasm-pack`, Vite build, + artifact upload v4, and GitHub Pages deployment. +- Ignore generated `target/`, `web/pkg/`, `dist/`, and local investigation + artifacts. + +## Residual Risks + +- Browser storage is session-local. IndexedDB persistence remains a follow-up. +- The app mirrors the Rust query result into `doublets-web`; the Rust query + processor does not yet use a JS-hosted `UnitedLinks` object as its direct + storage backend. diff --git a/docs/case-studies/issue-12/evidence/ci-runs.json b/docs/case-studies/issue-12/evidence/ci-runs.json new file mode 100644 index 0000000..1c02660 --- /dev/null +++ b/docs/case-studies/issue-12/evidence/ci-runs.json @@ -0,0 +1 @@ +[{"conclusion":"failure","createdAt":"2025-09-10T14:49:03Z","databaseId":17617650587,"event":"pull_request","headSha":"efe5e5984191ef2b4e1deb54ca083b363ef766ad","name":"WebAssembly CI","status":"completed"},{"conclusion":"failure","createdAt":"2025-09-10T14:49:01Z","databaseId":17617649689,"event":"push","headSha":"efe5e5984191ef2b4e1deb54ca083b363ef766ad","name":"WebAssembly CI","status":"completed"}] diff --git a/docs/case-studies/issue-12/evidence/doublets-web-issue-5.json b/docs/case-studies/issue-12/evidence/doublets-web-issue-5.json new file mode 100644 index 0000000..00e5a7b --- /dev/null +++ b/docs/case-studies/issue-12/evidence/doublets-web-issue-5.json @@ -0,0 +1 @@ +{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","name":"Konstantin Diachenko"},"body":"https://www.npmjs.com/package/doublets-web\n\nFirst we should use latest version of http://github.com/linksplatform/doublets-rs (it is now supports stable rust).\n\nWe also should support only stable rust features in this repository, implementing everything via stable rust tool chain.\n\nThat should be done using trusted publishing, that we use in our CI/CD templates.\n\nAlso we need to make sure our docs and GitHub releases contain badges with links to package.\n\nUse all the best practices from CI/CD templates (check full file tree to compare for all GitHub workflow and CI/CD scripts file), if the same issue is found in template report issue also in templates:\n- https://github.com/link-foundation/js-ai-driven-development-pipeline-template\n- https://github.com/link-foundation/rust-ai-driven-development-pipeline-template\n\nWe should compare all files, so we don't have more CI/CD errors in the future and reuse all the best practices from these templates.\n\nWe need to download all logs and data related about the issue to this repository, make sure we compile that data to `./docs/case-studies/issue-{id}` folder, and use it to do deep case study analysis (also make sure to search online for additional facts and data), in which we will reconstruct timeline/sequence of events, list of each and all requirements from the issue, find root causes of the each problem, and propose possible solutions and solution plans for each requirement (we should also check known existing components/libraries, that solve similar problem or can help in solutions).\n\nIf there is not enough data to find actual root cause, add debug output and verbose mode if not present, that will allow us to find root cause on next iteration.\n\nIf issue related to any other repository/project, where we can report issues on GitHub, please do so. Each issue must contain reproducible examples, workarounds and suggestions for fix the issue in code.\n","comments":[],"createdAt":"2026-04-30T08:34:11Z","labels":[{"id":"LA_kwDOGgeHac7YNTHM","name":"documentation","description":"Improvements or additions to documentation","color":"0075ca"},{"id":"LA_kwDOGgeHac7YNTHO","name":"enhancement","description":"New feature or request","color":"a2eeef"}],"state":"CLOSED","title":"We need to revive our CI/CD to actually auto deploy https://www.npmjs.com/package/doublets-web","updatedAt":"2026-05-01T05:28:26Z","url":"https://github.com/linksplatform/doublets-web/issues/5"} diff --git a/docs/case-studies/issue-12/evidence/issue-12.json b/docs/case-studies/issue-12/evidence/issue-12.json new file mode 100644 index 0000000..5a203f8 --- /dev/null +++ b/docs/case-studies/issue-12/evidence/issue-12.json @@ -0,0 +1 @@ +{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","name":"Konstantin Diachenko"},"body":"This will allow users to execute link-cli directly in browser.\r\n\r\nhttps://www.npmjs.com/package/doublets-web\r\n\r\nTo make it work, we also need to make sure all tests pass here: https://github.com/linksplatform/Protocols.Lino/blob/03be561ef9612fe7a86ed9f2ad964827cc6b4df5/rust/src/lib.rs","comments":[],"createdAt":"2024-12-13T15:44:16Z","labels":[{"id":"LA_kwDONXCAbs8AAAAB0ixEQA","name":"enhancement","description":"New feature or request","color":"a2eeef"}],"state":"OPEN","title":"Make WebAssembly version using Rust version of Doublets","updatedAt":"2024-12-13T15:48:33Z","url":"https://github.com/link-foundation/link-cli/issues/12"} diff --git a/docs/case-studies/issue-12/evidence/npm-doublets-web.json b/docs/case-studies/issue-12/evidence/npm-doublets-web.json new file mode 100644 index 0000000..2fcdec8 --- /dev/null +++ b/docs/case-studies/issue-12/evidence/npm-doublets-web.json @@ -0,0 +1,13 @@ +{ + "version": "0.1.2", + "dist-tags": { + "latest": "0.1.2" + }, + "description": "WebAssembly bindings for the LinksPlatform Doublets associative storage library.", + "repository": { + "type": "git", + "url": "git+https://github.com/linksplatform/doublets-web.git" + }, + "homepage": "https://github.com/linksplatform/doublets-web#readme", + "license": "Unlicense" +} diff --git a/docs/case-studies/issue-12/evidence/pr-52-conversation-comments.json b/docs/case-studies/issue-12/evidence/pr-52-conversation-comments.json new file mode 100644 index 0000000..52a37e8 --- /dev/null +++ b/docs/case-studies/issue-12/evidence/pr-52-conversation-comments.json @@ -0,0 +1 @@ +[{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments/4350894582","html_url":"https://github.com/link-foundation/link-cli/pull/52#issuecomment-4350894582","issue_url":"https://api.github.com/repos/link-foundation/link-cli/issues/52","id":4350894582,"node_id":"IC_kwDONXCAbs8AAAABA1Vh9g","user":{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2026-04-30T08:31:13Z","updated_at":"2026-05-01T11:10:04Z","body":"Fresh changes from default branch should be taken.\r\n\r\nThat should be based on GitHub Pages and https://www.npmjs.com/package/doublets-web, we also need to make sure both link-cli in JavaScript and Rust are able to start web-server connected to links instance.\r\n\r\nDepends on:\r\n- https://github.com/linksplatform/doublets-web/issues/5\r\n\r\n\r\nReread the issue of https://github.com/link-foundation/link-cli/issues/12\r\n\r\nAnd fully support new latest version of https://www.npmjs.com/package/doublets-web.\r\n\r\nWe need fully function demo using WebAssembly + Rust + JavaScript + React.js inside a browser, and it should be published as single page website, while also tell a story about link-cli.\r\n\r\nThat would be web interface to links data storage, made using web assembly version of doublet-rs for maximum efficiency.\r\n\r\nWe need to collect data related about the issue to this repository, make sure we compile that data to `./docs/case-studies/issue-{id}` folder, and use it to do deep case study analysis (also make sure to search online for additional facts and data), list of each and all requirements from the issue, and propose possible solutions and solution plans for each requirement (we should also check known existing components/libraries, that solve similar problem or can help in solutions).\r\n\r\nPlease plan and execute everything in a single pull request, you have unlimited time and context, as context autocompacts and you can continue indefinetely, do as much as possible in one go, until it is each and every requirement fully addressed, and everything is totally done.","author_association":"MEMBER","reactions":{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments/4350894582/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null},{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments/4359021043","html_url":"https://github.com/link-foundation/link-cli/pull/52#issuecomment-4359021043","issue_url":"https://api.github.com/repos/link-foundation/link-cli/issues/52","id":4359021043,"node_id":"IC_kwDONXCAbs8AAAABA9Fh8w","user":{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2026-05-01T11:10:39Z","updated_at":"2026-05-01T11:10:39Z","body":"🤖 **AI Work Session Started**\n\nStarting automated work session at 2026-05-01T11:10:38.133Z\n\nThe PR has been converted to draft mode while work is in progress.\n\n_This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback._","author_association":"MEMBER","reactions":{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments/4359021043/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null}] \ No newline at end of file diff --git a/docs/screenshots/issue-12/webassembly-workbench-desktop.png b/docs/screenshots/issue-12/webassembly-workbench-desktop.png new file mode 100644 index 0000000..c348f55 Binary files /dev/null and b/docs/screenshots/issue-12/webassembly-workbench-desktop.png differ diff --git a/docs/screenshots/issue-12/webassembly-workbench-mobile.png b/docs/screenshots/issue-12/webassembly-workbench-mobile.png new file mode 100644 index 0000000..f60823d Binary files /dev/null and b/docs/screenshots/issue-12/webassembly-workbench-mobile.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..29b0f16 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1020 @@ +{ + "name": "link-cli-web", + "version": "2.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "link-cli-web", + "version": "2.3.0", + "dependencies": { + "doublets-web": "^0.1.2", + "lucide-react": "^0.468.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.10", + "vite-plugin-wasm": "^3.6.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doublets-web": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/doublets-web/-/doublets-web-0.1.2.tgz", + "integrity": "sha512-rrX2szkoe/5Il9gelqAZvQLwwge2N042m8Fkw/bOLPcDeK309uzyWiCzX6UPv6vvalar7qAqXDvGSkU9SEWP4Q==", + "license": "Unlicense" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c4c062 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "link-cli-web", + "version": "2.3.0", + "description": "React and WebAssembly browser workbench for link-cli", + "type": "module", + "private": true, + "scripts": { + "build": "npm run build:wasm && npm run build:web", + "build:pages": "npm run build:wasm && DEPLOY_TARGET=github-pages npm run build:web", + "build:wasm": "wasm-pack build --target web --out-dir web/pkg", + "build:web": "vite build --config web/vite.config.js", + "dev": "npm run build:wasm && vite --config web/vite.config.js --host 0.0.0.0", + "preview": "vite preview --config web/vite.config.js --host 0.0.0.0", + "test": "npm run test:wasm && npm run build", + "test:wasm": "wasm-pack test --node", + "clean": "rm -rf dist web/pkg pkg pkg-node pkg-bundler target" + }, + "dependencies": { + "doublets-web": "^0.1.2", + "lucide-react": "^0.468.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.10", + "vite-plugin-wasm": "^3.6.0" + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b68289c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,345 @@ +use std::collections::HashMap; + +use anyhow::Result; +use link_cli::{Link, LinkError, NamedTypeLinks, QueryProcessor}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[derive(Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct ClinkOptions { + pub trace: bool, + pub auto_create_missing_references: bool, + pub structure: Option, + pub before: bool, + pub changes: bool, + pub after: bool, +} + +impl Default for ClinkOptions { + fn default() -> Self { + Self { + trace: false, + auto_create_missing_references: true, + structure: None, + before: false, + changes: true, + after: true, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ClinkResult { + pub success: bool, + pub output: String, + pub error: Option, + pub links: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct WebLink { + pub id: u32, + pub source: u32, + pub target: u32, + pub name: Option, +} + +#[wasm_bindgen] +pub struct Clink { + storage: BrowserStorage, +} + +#[wasm_bindgen] +impl Clink { + #[wasm_bindgen(constructor)] + pub fn new() -> Clink { + set_panic_hook(); + Clink { + storage: BrowserStorage::new(), + } + } + + #[wasm_bindgen] + pub fn execute(&mut self, query: &str, options_json: &str) -> String { + to_json(&match self.execute_inner(query, options_json) { + Ok(result) => result, + Err(error) => ClinkResult { + success: false, + output: String::new(), + error: Some(error.to_string()), + links: self.storage.snapshot(), + }, + }) + } + + #[wasm_bindgen] + pub fn snapshot(&mut self) -> String { + to_json(&ClinkResult { + success: true, + output: self.storage.lino_lines().unwrap_or_default().join("\n"), + error: None, + links: self.storage.snapshot(), + }) + } + + #[wasm_bindgen] + pub fn reset(&mut self) -> String { + self.storage = BrowserStorage::new(); + self.snapshot() + } + + #[wasm_bindgen] + pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() + } + + #[wasm_bindgen(js_name = rustCoreVersion)] + pub fn rust_core_version() -> String { + link_cli::cli::Cli::version_text() + } + + #[wasm_bindgen] + pub fn test() -> bool { + true + } +} + +impl Default for Clink { + fn default() -> Self { + Self::new() + } +} + +impl Clink { + fn execute_inner(&mut self, query: &str, options_json: &str) -> Result { + let options = parse_options(options_json)?; + let mut output = Vec::new(); + + if let Some(structure_id) = options.structure { + output.push(self.storage.format_structure(structure_id)?); + return Ok(self.result(output, true, None)); + } + + if options.before { + output.extend(self.storage.lino_lines()?); + } + + if !query.trim().is_empty() { + let processor = QueryProcessor::new(options.trace) + .with_auto_create_missing_references(options.auto_create_missing_references); + let changes = processor.process_query(&mut self.storage, query)?; + if options.changes { + for (before, after) in &changes { + output.push(self.storage.format_change(before, after)?); + } + } + } + + if options.after { + output.extend(self.storage.lino_lines()?); + } + + Ok(self.result(output, true, None)) + } + + fn result(&self, output: Vec, success: bool, error: Option) -> ClinkResult { + ClinkResult { + success, + output: output.join("\n"), + error, + links: self.storage.snapshot(), + } + } +} + +fn parse_options(options_json: &str) -> Result { + let trimmed = options_json.trim(); + if trimmed.is_empty() { + return Ok(ClinkOptions::default()); + } + + serde_json::from_str(trimmed).map_err(|error| anyhow::anyhow!("Invalid options JSON: {error}")) +} + +fn to_json(value: &T) -> String { + serde_json::to_string(value).unwrap_or_else(|error| { + format!( + r#"{{"success":false,"output":"","error":"Failed to serialize result: {error}","links":[]}}"# + ) + }) +} + +fn set_panic_hook() { + #[cfg(all(feature = "console_error_panic_hook", target_arch = "wasm32"))] + console_error_panic_hook::set_once(); +} + +#[derive(Default)] +struct BrowserStorage { + links: HashMap, + names: HashMap, + name_to_id: HashMap, + next_id: u32, +} + +impl BrowserStorage { + fn new() -> Self { + Self { + next_id: 1, + ..Self::default() + } + } + + fn snapshot(&self) -> Vec { + let mut links: Vec<_> = self + .links + .values() + .map(|link| WebLink { + id: link.index, + source: link.source, + target: link.target, + name: self.names.get(&link.index).cloned(), + }) + .collect(); + links.sort_by_key(|link| link.id); + links + } + + fn format_change(&mut self, before: &Option, after: &Option) -> Result { + let before_text = before + .map(|link| self.format_lino(&link)) + .transpose()? + .unwrap_or_default(); + let after_text = after + .map(|link| self.format_lino(&link)) + .transpose()? + .unwrap_or_default(); + Ok(format!("({before_text}) ({after_text})")) + } +} + +impl NamedTypeLinks for BrowserStorage { + fn create(&mut self, source: u32, target: u32) -> u32 { + let id = self.next_id.max(1); + self.next_id = id + 1; + self.links.insert(id, Link::new(id, source, target)); + id + } + + fn ensure_created(&mut self, id: u32) -> u32 { + if id == 0 || self.links.contains_key(&id) { + return id; + } + + self.next_id = self.next_id.max(id + 1); + self.links.insert(id, Link::new(id, 0, 0)); + id + } + + fn get_link(&mut self, id: u32) -> Option { + self.links.get(&id).copied() + } + + fn exists(&mut self, id: u32) -> bool { + self.links.contains_key(&id) + } + + fn update(&mut self, id: u32, source: u32, target: u32) -> Result { + let link = self.links.get_mut(&id).ok_or(LinkError::NotFound(id))?; + let before = *link; + link.source = source; + link.target = target; + Ok(before) + } + + fn delete(&mut self, id: u32) -> Result { + self.remove_name(id)?; + self.links.remove(&id).ok_or(LinkError::NotFound(id).into()) + } + + fn all_links(&mut self) -> Vec { + self.links.values().copied().collect() + } + + fn search(&mut self, source: u32, target: u32) -> Option { + self.links + .values() + .find(|link| link.source == source && link.target == target) + .map(|link| link.index) + } + + fn get_or_create(&mut self, source: u32, target: u32) -> u32 { + self.search(source, target) + .unwrap_or_else(|| self.create(source, target)) + } + + fn get_name(&mut self, id: u32) -> Result> { + Ok(self.names.get(&id).cloned()) + } + + fn set_name(&mut self, id: u32, name: &str) -> Result { + if let Some(previous_name) = self.names.remove(&id) { + self.name_to_id.remove(&previous_name); + } + if let Some(previous_id) = self.name_to_id.insert(name.to_string(), id) { + if previous_id != id { + self.names.remove(&previous_id); + } + } + self.names.insert(id, name.to_string()); + Ok(id) + } + + fn get_by_name(&mut self, name: &str) -> Result> { + Ok(self.name_to_id.get(name).copied()) + } + + fn remove_name(&mut self, id: u32) -> Result<()> { + if let Some(name) = self.names.remove(&id) { + self.name_to_id.remove(&name); + } + Ok(()) + } + + fn save(&mut self) -> Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::Value; + + #[test] + fn executes_queries_with_the_rust_core() { + let mut clink = Clink::new(); + let raw = clink.execute( + "() ((child: father mother))", + r#"{"changes":true,"after":true}"#, + ); + let parsed: Value = serde_json::from_str(&raw).unwrap(); + + assert_eq!(parsed["success"], true); + assert!(parsed["output"].as_str().unwrap().contains("child")); + assert_eq!(parsed["links"].as_array().unwrap().len(), 3); + } + + #[test] + fn rejects_invalid_options() { + let mut clink = Clink::new(); + let raw = clink.execute("() ((1 1))", "not json"); + let parsed: Value = serde_json::from_str(&raw).unwrap(); + + assert_eq!(parsed["success"], false); + assert!(parsed["error"] + .as_str() + .unwrap() + .contains("Invalid options JSON")); + } +} diff --git a/tests/web.rs b/tests/web.rs new file mode 100644 index 0000000..a048410 --- /dev/null +++ b/tests/web.rs @@ -0,0 +1,45 @@ +//! WebAssembly tests for the browser-facing Rust wrapper. + +#![cfg(target_arch = "wasm32")] + +use clink_wasm::Clink; +use serde_json::Value; +use wasm_bindgen_test::*; + +#[wasm_bindgen_test] +fn creates_a_clink_instance() { + let _clink = Clink::new(); +} + +#[wasm_bindgen_test] +fn exposes_versions() { + assert_eq!(Clink::version(), "2.3.0"); + assert!(Clink::rust_core_version().starts_with("clink ")); +} + +#[wasm_bindgen_test] +fn executes_lino_queries_with_the_rust_core() { + let mut clink = Clink::new(); + let raw = clink.execute( + "() ((child: father mother))", + r#"{"changes":true,"after":true}"#, + ); + let parsed: Value = serde_json::from_str(&raw).unwrap(); + + assert_eq!(parsed["success"], true); + assert!(parsed["output"].as_str().unwrap().contains("child")); + assert_eq!(parsed["links"].as_array().unwrap().len(), 3); +} + +#[wasm_bindgen_test] +fn reports_invalid_options() { + let mut clink = Clink::new(); + let raw = clink.execute("() ((1 1))", "invalid json"); + let parsed: Value = serde_json::from_str(&raw).unwrap(); + + assert_eq!(parsed["success"], false); + assert!(parsed["error"] + .as_str() + .unwrap() + .contains("Invalid options JSON")); +} diff --git a/web/favicon.svg b/web/favicon.svg new file mode 100644 index 0000000..dbddd9f --- /dev/null +++ b/web/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..6bac01f --- /dev/null +++ b/web/index.html @@ -0,0 +1,17 @@ + + + + + + + + link-cli WebAssembly Workbench + + +
+ + + diff --git a/web/src/App.jsx b/web/src/App.jsx new file mode 100644 index 0000000..8457ef0 --- /dev/null +++ b/web/src/App.jsx @@ -0,0 +1,488 @@ +import initClink, { Clink } from '../pkg/clink_wasm.js'; +import { Link, LinksConstants, UnitedLinks } from 'doublets-web'; +import { + Activity, + Database, + FileClock, + GitBranch, + Play, + RefreshCw, + RotateCcw, + Server, + SquareTerminal, +} from 'lucide-react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +const DOUBLETS_WEB_VERSION = '0.1.2'; + +const defaultQuery = '() ((child: father mother))'; + +const samples = [ + { label: 'Create family links', query: '() ((child: father mother))' }, + { label: 'Read all links', query: '((($i: $s $t)) (($i: $s $t)))' }, + { label: 'Swap parents', query: '((($id: father mother)) (($id: mother father)))' }, + { label: 'Delete child', query: '((child: mother father)) ()' }, +]; + +const story = [ + { + title: 'CLI', + text: 'link-cli starts as a compact LiNo command: one substitution expression can create, read, update, or delete links.', + }, + { + title: 'Rust', + text: 'The browser wrapper calls the same Rust query processor used by the native clink binary, with in-memory storage for the page session.', + }, + { + title: 'Web', + text: 'React renders the workbench and mirrors the current link set into doublets-web, the WebAssembly package built from doublets-rs.', + }, + { + title: 'Pages', + text: 'GitHub Pages publishes the static bundle: JavaScript, React assets, Rust WASM, and doublets-web WASM.', + }, +]; + +function App() { + const clinkRef = useRef(null); + const doubletsRef = useRef(null); + const [readyState, setReadyState] = useState({ + phase: 'loading', + wasmVersion: '', + rustVersion: '', + message: 'Booting WebAssembly runtimes', + }); + const [doubletsState, setDoubletsState] = useState({ + phase: 'loading', + count: 0, + message: 'Preparing doublets-web', + }); + const [query, setQuery] = useState(defaultQuery); + const [options, setOptions] = useState({ + before: false, + changes: true, + after: true, + autoCreateMissingReferences: true, + trace: false, + }); + const [output, setOutput] = useState(''); + const [links, setLinks] = useState([]); + const [activeSample, setActiveSample] = useState(samples[0].label); + + const syncDoublets = useCallback((snapshot) => { + try { + const runtime = createDoubletsRuntime(snapshot); + disposeDoubletsRuntime(doubletsRef.current); + doubletsRef.current = runtime; + setDoubletsState({ + phase: 'ready', + count: runtime.count, + message: `UnitedLinks mirror: ${runtime.count} stored link${runtime.count === 1 ? '' : 's'}`, + }); + } catch (error) { + setDoubletsState({ + phase: 'error', + count: 0, + message: error instanceof Error ? error.message : String(error), + }); + } + }, []); + + useEffect(() => { + let cancelled = false; + + async function boot() { + try { + await initClink({ module_or_path: new URL('../pkg/clink_wasm_bg.wasm', import.meta.url) }); + if (cancelled) { + return; + } + + const clink = new Clink(); + clinkRef.current = clink; + const snapshot = JSON.parse(clink.snapshot()); + setLinks(snapshot.links); + setOutput(snapshot.output); + setReadyState({ + phase: 'ready', + wasmVersion: Clink.version(), + rustVersion: Clink.rustCoreVersion(), + message: 'Rust query processor online', + }); + syncDoublets(snapshot.links); + } catch (error) { + if (!cancelled) { + setReadyState({ + phase: 'error', + wasmVersion: '', + rustVersion: '', + message: error instanceof Error ? error.message : String(error), + }); + } + } + } + + boot(); + + return () => { + cancelled = true; + disposeDoubletsRuntime(doubletsRef.current); + doubletsRef.current = null; + if (clinkRef.current) { + clinkRef.current.free?.(); + clinkRef.current = null; + } + }; + }, [syncDoublets]); + + const runQuery = useCallback(() => { + if (!clinkRef.current) { + return; + } + + const raw = clinkRef.current.execute(query, JSON.stringify(options)); + const result = JSON.parse(raw); + setOutput(result.output || result.error || ''); + setLinks(result.links || []); + syncDoublets(result.links || []); + }, [options, query, syncDoublets]); + + const resetSession = useCallback(() => { + if (!clinkRef.current) { + return; + } + + const result = JSON.parse(clinkRef.current.reset()); + setOutput(result.output || ''); + setLinks(result.links || []); + syncDoublets(result.links || []); + }, [syncDoublets]); + + const stats = useMemo(() => { + const references = new Set(); + for (const link of links) { + references.add(link.id); + references.add(link.source); + references.add(link.target); + } + + return { + links: links.length, + references: references.size, + named: links.filter((link) => link.name).length, + }; + }, [links]); + + return ( +
+
+
+

link-cli

+

WebAssembly Workbench

+
+
+ + +
+
+ +
+
+
+
+

LiNo query

+

Substitution

+
+ +
+ +