diff --git a/crubit_explorer/BUILD b/crubit_explorer/BUILD
index cdf7a15d4..9a67ffdc2 100644
--- a/crubit_explorer/BUILD
+++ b/crubit_explorer/BUILD
@@ -23,6 +23,7 @@ crubit_rust_binary(
name = "server",
srcs = ["src/main.rs"],
data = [
+ "//frontend:build",
"@rules_crubit//cc_bindings_from_rs",
"@rules_rust//rust/toolchain:current_rustc_files",
"@rules_rust//rust/toolchain:current_rustc_lib_files",
@@ -48,6 +49,7 @@ crubit_rust_test(
size = "small",
srcs = ["src/main.rs"],
data = [
+ "//frontend:build",
"@rules_crubit//cc_bindings_from_rs",
"@rules_rust//rust/toolchain:current_rustc_files",
"@rules_rust//rust/toolchain:current_rustc_lib_files",
@@ -59,6 +61,7 @@ crubit_rust_test(
deps = [
"@crubit_explorer_crates//:axum",
"@crubit_explorer_crates//:base64",
+ "@crubit_explorer_crates//:googletest",
"@crubit_explorer_crates//:serde",
"@crubit_explorer_crates//:serde_json",
"@crubit_explorer_crates//:tempfile",
diff --git a/crubit_explorer/Cargo.lock b/crubit_explorer/Cargo.lock
index 7f45dca23..3f7716a3f 100644
--- a/crubit_explorer/Cargo.lock
+++ b/crubit_explorer/Cargo.lock
@@ -2,12 +2,27 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
[[package]]
name = "axum"
version = "0.8.9"
@@ -68,9 +83,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
-version = "2.13.0"
+version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "bytes"
@@ -90,6 +105,7 @@ version = "0.1.0"
dependencies = [
"axum",
"base64",
+ "googletest",
"serde",
"serde_json",
"tempfile",
@@ -110,9 +126,9 @@ dependencies = [
[[package]]
name = "fastrand"
-version = "2.4.1"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "form_urlencoded"
@@ -125,36 +141,36 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-sink"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
[[package]]
name = "futures-task"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-core",
"futures-task",
@@ -173,6 +189,29 @@ dependencies = [
"r-efi",
]
+[[package]]
+name = "googletest"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94"
+dependencies = [
+ "googletest_macro",
+ "num-traits",
+ "regex",
+ "rustversion",
+]
+
+[[package]]
+name = "googletest_macro"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "http"
version = "1.4.2"
@@ -226,9 +265,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
-version = "1.10.1"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
dependencies = [
"atomic-waker",
"bytes",
@@ -267,9 +306,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
-version = "0.2.186"
+version = "0.2.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "22053b6a34f84abc97f9129e61334f40174659a1b9bd18c970b83db6a9a6348b"
[[package]]
name = "linux-raw-sys"
@@ -331,6 +370,15 @@ 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",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -374,18 +422,18 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
-version = "1.0.106"
+version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.46"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -405,6 +453,35 @@ dependencies = [
"bitflags",
]
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
[[package]]
name = "rustix"
version = "1.1.4"
@@ -418,6 +495,12 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
[[package]]
name = "ryu"
version = "1.0.23"
@@ -432,9 +515,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
@@ -442,29 +525,29 @@ dependencies = [
[[package]]
name = "serde_core"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.2",
]
[[package]]
name = "serde_json"
-version = "1.0.150"
+version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
@@ -530,9 +613,20 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.118"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
dependencies = [
"proc-macro2",
"quote",
@@ -560,9 +654,9 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.52.3"
+version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
@@ -577,20 +671,20 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.7.0"
+version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
name = "tokio-util"
-version = "0.7.18"
+version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [
"bytes",
"futures-core",
@@ -707,6 +801,6 @@ dependencies = [
[[package]]
name = "zmij"
-version = "1.0.21"
+version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/crubit_explorer/Cargo.toml b/crubit_explorer/Cargo.toml
index 39657e3a3..68c9b7b79 100644
--- a/crubit_explorer/Cargo.toml
+++ b/crubit_explorer/Cargo.toml
@@ -15,6 +15,7 @@ serde_json = "1.0.150"
tempfile = "3.27.0"
tokio = { version = "1.52.3", features = ["full"] }
tower = { version = "0.5.2", features = ["util"] }
+googletest = "0.14.3"
[workspace]
diff --git a/crubit_explorer/MODULE.bazel b/crubit_explorer/MODULE.bazel
index 0298231d1..8aa047dc9 100644
--- a/crubit_explorer/MODULE.bazel
+++ b/crubit_explorer/MODULE.bazel
@@ -15,6 +15,20 @@ local_path_override(
path = "..",
)
+bazel_dep(name = "aspect_rules_js", version = "2.1.0")
+bazel_dep(name = "rules_nodejs", version = "6.7.5")
+
+node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
+node.toolchain(node_version = "20.11.1")
+use_repo(node, "nodejs_toolchains")
+
+npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
+npm.npm_translate_lock(
+ name = "npm",
+ pnpm_lock = "//frontend:pnpm-lock.yaml",
+)
+use_repo(npm, "npm")
+
crubit_toolchains = use_extension("@rules_crubit//bazel:extensions.bzl", "crubit_toolchains")
crubit_toolchains.configure(
llvm_version = "22.1.0",
diff --git a/crubit_explorer/frontend/BUILD b/crubit_explorer/frontend/BUILD
new file mode 100644
index 000000000..ba5467d2f
--- /dev/null
+++ b/crubit_explorer/frontend/BUILD
@@ -0,0 +1,37 @@
+# Part of the Crubit project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("@npm//frontend:@angular/cli/package_json.bzl", "bin")
+
+package(
+ default_applicable_licenses = ["//:license"],
+)
+
+npm_link_all_packages(name = "node_modules")
+
+bin.ng(
+ name = "build",
+ srcs = [
+ "angular.json",
+ "package.json",
+ "tsconfig.app.json",
+ "tsconfig.json",
+ ":node_modules",
+ ] + glob([
+ "public/**/*",
+ "src/**/*",
+ ]),
+ args = [
+ "build",
+ "--configuration",
+ "production",
+ ],
+ chdir = "frontend",
+ # Angular build with modern application builder outputs to dist/frontend.
+ out_dirs = [
+ "dist/frontend",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/crubit_explorer/frontend/angular.json b/crubit_explorer/frontend/angular.json
new file mode 100644
index 000000000..de77f5089
--- /dev/null
+++ b/crubit_explorer/frontend/angular.json
@@ -0,0 +1,106 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "frontend": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "inlineTemplate": true,
+ "inlineStyle": true,
+ "skipTests": true
+ },
+ "@schematics/angular:class": {
+ "skipTests": true
+ },
+ "@schematics/angular:directive": {
+ "skipTests": true
+ },
+ "@schematics/angular:guard": {
+ "skipTests": true
+ },
+ "@schematics/angular:interceptor": {
+ "skipTests": true
+ },
+ "@schematics/angular:pipe": {
+ "skipTests": true
+ },
+ "@schematics/angular:resolver": {
+ "skipTests": true
+ },
+ "@schematics/angular:service": {
+ "skipTests": true
+ }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/frontend",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "preserveSymlinks": false,
+ "polyfills": "zone.js",
+ "tsConfig": "tsconfig.app.json",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "public"
+ }
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kB",
+ "maximumError": "1MB"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kB",
+ "maximumError": "4kB"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "frontend:build:production"
+ },
+ "development": {
+ "buildTarget": "frontend:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n"
+ }
+ }
+ }
+ }
+}
diff --git a/crubit_explorer/frontend/package.json b/crubit_explorer/frontend/package.json
new file mode 100644
index 000000000..3744bd08d
--- /dev/null
+++ b/crubit_explorer/frontend/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "^18.2.0",
+ "@angular/common": "^18.2.0",
+ "@angular/compiler": "^18.2.0",
+ "@angular/core": "^18.2.0",
+ "@angular/forms": "^18.2.0",
+ "@angular/platform-browser": "^18.2.0",
+ "@angular/platform-browser-dynamic": "^18.2.0",
+ "@angular/router": "^18.2.0",
+ "@monaco-editor/loader": "^1.7.0",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.14.10"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^18.2.21",
+ "@angular/cli": "^18.2.21",
+ "@angular/compiler-cli": "^18.2.0",
+ "typescript": "~5.5.2"
+ },
+ "pnpm": {
+ "onlyBuiltDependencies": []
+ }
+}
diff --git a/crubit_explorer/frontend/public/favicon.ico b/crubit_explorer/frontend/public/favicon.ico
new file mode 100644
index 000000000..57614f9c9
Binary files /dev/null and b/crubit_explorer/frontend/public/favicon.ico differ
diff --git a/crubit_explorer/frontend/src/app/app.component.html b/crubit_explorer/frontend/src/app/app.component.html
new file mode 100644
index 000000000..7aa4cf1bb
--- /dev/null
+++ b/crubit_explorer/frontend/src/app/app.component.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/crubit_explorer/frontend/src/app/app.component.ts b/crubit_explorer/frontend/src/app/app.component.ts
new file mode 100644
index 000000000..293c42ae7
--- /dev/null
+++ b/crubit_explorer/frontend/src/app/app.component.ts
@@ -0,0 +1,135 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import {CommonModule} from '@angular/common';
+import {HttpClient} from '@angular/common/http';
+import {AfterViewInit, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, OnDestroy, ViewChild} from '@angular/core';
+import {Subject, Subscription, debounceTime} from 'rxjs';
+import loader from '@monaco-editor/loader';
+
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [CommonModule],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ templateUrl: './app.component.html',
+ styleUrls: []
+})
+export class AppComponent implements AfterViewInit, OnDestroy {
+ @ViewChild('inputEditorContainer', {static: true})
+ inputContainer!: ElementRef;
+ @ViewChild('outputEditorContainer', {static: true})
+ outputContainer!: ElementRef;
+
+ inputEditor: any;
+ outputEditor: any;
+ monaco: any;
+
+ private inputChangeSubject = new Subject();
+ private subscription!: Subscription;
+ isCompiling = false;
+
+ constructor(private http: HttpClient) {}
+
+ ngAfterViewInit() {
+ loader.init().then(monaco => {
+ this.monaco = monaco;
+ const prefersDark = window.matchMedia &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
+ const theme = prefersDark ? 'vs-dark' : 'vs';
+
+ this.inputEditor = monaco.editor.create(this.inputContainer.nativeElement, {
+ value:
+ '// Write Rust code here\npub struct MyStruct {\n pub a: i32,\n}\n\npub extern "C" fn hello() {}\n',
+ language: 'rust',
+ theme: theme,
+ minimap: {enabled: false},
+ automaticLayout: true
+ });
+
+ this.outputEditor =
+ monaco.editor.create(this.outputContainer.nativeElement, {
+ value: '// Output will appear here',
+ language: 'cpp',
+ theme: theme,
+ readOnly: true,
+ minimap: {enabled: false},
+ automaticLayout: true
+ });
+
+ this.inputEditor.onDidChangeModelContent(() => {
+ this.inputChangeSubject.next(this.inputEditor.getValue());
+ });
+ });
+
+ this.subscription =
+ this.inputChangeSubject.pipe(debounceTime(600)).subscribe(content => {
+ this.compile(content);
+ });
+ }
+
+ compile(content?: string) {
+ if (!content && this.inputEditor) {
+ content = this.inputEditor.getValue();
+ }
+ if (!content) return;
+
+ this.isCompiling = true;
+
+ const payload = {
+ pluginName: 'cc_bindings_from_rs',
+ enableCodegenTracing: false,
+ pluginFlags: [],
+ input: {
+ files: [{
+ name: 'input.rs',
+ contentsB64: btoa(unescape(encodeURIComponent(content)))
+ }]
+ }
+ };
+
+ this.http.post('/api/compile', payload).subscribe({
+ next: (res) => {
+ this.isCompiling = false;
+ if (res.error) {
+ if (this.outputEditor) {
+ this.outputEditor.setValue(
+ '// Error:\n' + (res.error.reason || res.error.text));
+ }
+ } else if (
+ res.output && res.output.files && res.output.files.length > 0) {
+ // Combine all output files for display
+ let combinedOutput = '';
+ for (let file of res.output.files) {
+ const decoded = decodeURIComponent(escape(atob(file.contentsB64)));
+ combinedOutput += '// File: ' + file.name + '\n' + decoded + '\n\n';
+ }
+ if (this.outputEditor) {
+ this.outputEditor.setValue(combinedOutput);
+ }
+ }
+ },
+ error: (err) => {
+ this.isCompiling = false;
+ const errText =
+ err.error?.error?.reason || err.message || 'Unknown Error';
+ if (this.outputEditor) {
+ this.outputEditor.setValue('// Error:\n' + errText);
+ }
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ if (this.inputEditor) {
+ this.inputEditor.dispose();
+ }
+ if (this.outputEditor) {
+ this.outputEditor.dispose();
+ }
+ }
+}
diff --git a/crubit_explorer/frontend/src/app/app.config.ts b/crubit_explorer/frontend/src/app/app.config.ts
new file mode 100644
index 000000000..a709a8269
--- /dev/null
+++ b/crubit_explorer/frontend/src/app/app.config.ts
@@ -0,0 +1,16 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import {provideHttpClient} from '@angular/common/http';
+import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
+import {provideRouter} from '@angular/router';
+
+import {routes} from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes),
+ provideHttpClient()
+ ]
+};
diff --git a/crubit_explorer/frontend/src/app/app.routes.ts b/crubit_explorer/frontend/src/app/app.routes.ts
new file mode 100644
index 000000000..53a619dc7
--- /dev/null
+++ b/crubit_explorer/frontend/src/app/app.routes.ts
@@ -0,0 +1,7 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import {Routes} from '@angular/router';
+
+export const routes: Routes = [];
diff --git a/crubit_explorer/frontend/src/index.html b/crubit_explorer/frontend/src/index.html
new file mode 100644
index 000000000..6731d9d03
--- /dev/null
+++ b/crubit_explorer/frontend/src/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Crubit Explorer
+
+
+
+
+
+
+
+
diff --git a/crubit_explorer/frontend/src/main.ts b/crubit_explorer/frontend/src/main.ts
new file mode 100644
index 000000000..b83f1cd29
--- /dev/null
+++ b/crubit_explorer/frontend/src/main.ts
@@ -0,0 +1,11 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import {bootstrapApplication} from '@angular/platform-browser';
+
+import {AppComponent} from './app/app.component';
+import {appConfig} from './app/app.config';
+
+bootstrapApplication(AppComponent, appConfig)
+ .catch((err) => console.error(err));
diff --git a/crubit_explorer/frontend/src/styles.css b/crubit_explorer/frontend/src/styles.css
new file mode 100644
index 000000000..6e84b3a43
--- /dev/null
+++ b/crubit_explorer/frontend/src/styles.css
@@ -0,0 +1,445 @@
+/*
+Part of the Crubit project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+*/
+
+:root {
+ --theme-color: #ff6d3cff;
+ --dropdown-color: #c0a0a0ff;
+}
+
+.material-icons {
+ font-family: 'Google Material Icons';
+}
+
+.toolbar-flags {
+ display:none;
+}
+
+.toolbar-codegen-tracing {
+ display:none;
+}
+
+html {
+ /* Copied from tailwind */
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ line-height: 1.5;
+}
+
+@media(prefers-color-scheme: dark) {
+ html {
+ /* Matches monaco 'vs-dark' theme's editor-background */
+ background: #1e1e1e;
+ }
+}
+
+button, input, select {
+ /* Copied from tailwind */
+ font-family: inherit;
+ font-size: 100%;
+}
+
+body {
+ /* Prevent full-page UI (100vh/100vw) from creating scrollbars). */
+ margin: 0;
+}
+
+.cursor-wait, .cursor-wait * {
+ /*
+ override *all* cursors during compilation, otherwise the text editor and
+ interactive button cursors will mask the compilation indicator
+ */
+ cursor: wait !important;
+}
+
+app-root {
+ display: flex;
+ flex-direction: column;
+
+ height: 100vh;
+ width: 100vw;
+ padding: 0;
+ margin: 0;
+}
+
+header {
+ padding: .5rem;
+ display: flex;
+ background: oklch(from var(--theme-color) calc(l - .09) calc(c + .018) h);
+ color: #fff;
+}
+
+header h1 {
+ margin: 0 .5rem;
+ font-size: 1.25rem;
+ font-weight: 700;
+ flex-grow: 1;
+}
+
+header h1 .build-label {
+ font-size: .75rem;
+ font-weight: 300;
+}
+
+header .toolbar {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 1rem;
+ padding: 0 .5rem;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+header .toolbar > label {
+ font-size: .875rem;
+}
+
+header .toolbar > label input,
+header .toolbar > label select {
+ margin: 0 0.5rem;
+ font-size: 1rem;
+ display: inline-block;
+}
+
+app-shortn {
+ display: inline-flex;
+ align-items: center;
+}
+
+app-shortn button {
+ appearance: none;
+ cursor: pointer;
+ background: none;
+ border: 0;
+ color: inherit;
+}
+
+app-shortn .shortn-popup {
+ position: absolute;
+ top: 1.5rem;
+ right: 1.5rem;
+
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ background: #fff;
+ color: #000;
+ border: 2px solid oklch(from var(--theme-color) calc(l - .36) c h);
+
+ font-size: 1.25rem;
+ padding: .5rem;
+}
+
+.tab-bar {
+ width: 100%;
+ display: flex;
+ color: #fff;
+ background: oklch(from var(--theme-color) calc(l - .24) calc(c - .008) h);
+ padding: .5rem 0 0 0;
+ margin: 0 0 .5rem 0;
+}
+
+.tab-bar app-tabs {
+ display: inline-block;
+}
+
+.tab-bar app-tabs .tab {
+ display: inline-block;
+ cursor: default;
+ border: 2px solid oklch(from var(--theme-color) calc(l - .27) calc(c - .009) h);
+ border-bottom-width: 0;
+ border-top-left-radius: .5rem;
+ border-top-right-radius: .5rem;
+ padding: .5rem;
+ vertical-align: middle;
+ font-size: .875rem;
+ background: oklch(from var(--theme-color) calc(l - .24) calc(c - .008) h);
+}
+
+.tab-bar app-tabs .tab.selected {
+ background: oklch(from var(--theme-color) calc(l - .09) calc(c - .009) h);
+}
+
+.tab-bar app-tabs .tab:hover,
+.tab-bar app-tabs .tab.selected:hover {
+ background: var(--theme-color);
+}
+
+.tab-bar app-tabs .tab span {
+ display: inline-block;
+}
+
+.tab-bar app-tabs .tab span.icon {
+ padding-left: .5rem;
+ cursor: pointer;
+ vertical-align: middle;
+ font-size: .875rem;
+}
+
+.dropdown {
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+}
+
+.dropdown button {
+ appearance: none;
+ color: inherit;
+ background: none;
+ cursor: pointer;
+ border: 0;
+ opacity: .5;
+ padding: 0 .5rem;
+}
+
+.dropdown .dropdown-content {
+ position: absolute;
+ z-index: 10;
+ top: 2rem;
+ left: 0;
+ display: none;
+}
+
+.dropdown .dropdown-content > div {
+ padding: .5rem;
+ font-size: .875rem;
+ color: #000;
+ background: oklch(from var(--dropdown-color) calc(l + .18) calc(c - .018) h);
+ cursor: pointer;
+}
+
+.dropdown .dropdown-content > div.header {
+ cursor: default;
+ font-size: .75rem;
+ color: var(--dropdown-color);
+}
+
+.dropdown .dropdown-content > div.highlight {
+ background: oklch(from var(--dropdown-color) calc(l + .21) calc(c - .021) h);
+}
+
+.dropdown:hover .dropdown-content {
+ display: inline-block;
+}
+
+main {
+ display: grid;
+ flex-grow: 1;
+ grid-template-columns: 4fr 4px 6fr;
+}
+
+main .pane {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+main .pane > monaco-editor {
+ display: block;
+ flex-grow: 1;
+ /* Prevents monaco editor from resizing beyond the page boundary. */
+ position: relative;
+}
+
+main .pane > monaco-editor > .monaco-editor {
+ position: absolute;
+}
+
+main .resizer {
+ background: oklch(from var(--theme-color) calc(l + .18) calc(c - .018) h);
+ cursor: col-resize;
+}
+
+/* Doxygen Section styling */
+.doxygen-section {
+ height: 250px;
+ border-top: 1px solid #ccc;
+ padding: 10px;
+ background: #f9f9f9;
+ font-family: monospace;
+ color: #000;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ transition: height 0.2s ease-in-out;
+}
+
+.doxygen-section.collapsed {
+ height: 40px;
+ overflow: hidden;
+ padding-bottom: 0;
+}
+
+.doxygen-header-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ user-select: none;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 5px;
+ margin-bottom: 10px;
+}
+
+.doxygen-section.collapsed .doxygen-header-container {
+ border-bottom: none;
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+.doxygen-header {
+ margin: 0;
+ color: #333;
+ font-size: 1rem;
+}
+
+.doxygen-panel-toggle-icon {
+ color: #666;
+ font-size: 20px !important;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.doxygen-content {
+ flex-grow: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.doxygen-error {
+ color: red;
+ padding: 5px;
+ background: #fee;
+ border: 1px solid #fdd;
+ margin-bottom: 10px;
+}
+
+.doxygen-empty {
+ color: #666;
+ font-style: italic;
+}
+
+/* Symbol Tree styling */
+.symbol-tree-container {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.symbol-tree-row {
+ display: flex;
+ align-items: center;
+ padding: 4px 8px;
+ cursor: default;
+ border-radius: 4px;
+ user-select: none;
+ transition: background-color 0.1s ease;
+}
+
+.symbol-tree-row.has-children {
+ cursor: pointer;
+}
+
+.symbol-tree-row:hover {
+ background-color: #f0f0f0;
+}
+
+.symbol-toggle-icon {
+ font-size: 16px !important;
+ margin-right: 4px;
+ color: #666;
+ width: 16px;
+ height: 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.symbol-kind-badge {
+ font-size: 0.75rem;
+ padding: 1px 5px;
+ border-radius: 3px;
+ margin-right: 8px;
+ font-weight: 600;
+ text-transform: uppercase;
+ display: inline-block;
+ min-width: 65px;
+ text-align: center;
+}
+
+.symbol-name {
+ color: #111;
+ font-weight: bold;
+}
+
+.symbol-location {
+ color: #666;
+ font-size: 0.85em;
+ margin-left: auto;
+ font-family: monospace;
+}
+
+/* Kind specific badge colors */
+.symbol-kind-badge[data-kind="namespace"] { background-color: #e1f5fe; color: #0288d1; }
+.symbol-kind-badge[data-kind="class"] { background-color: #efebe9; color: #5d4037; }
+.symbol-kind-badge[data-kind="struct"] { background-color: #ede7f6; color: #5e35b1; }
+.symbol-kind-badge[data-kind="function"] { background-color: #e8f5e9; color: #2e7d32; }
+.symbol-kind-badge[data-kind="variable"] { background-color: #fff3e0; color: #ef6c00; }
+.symbol-kind-badge[data-kind="typedef"] { background-color: #fce4ec; color: #c2185b; }
+.symbol-kind-badge[data-kind="union"] { background-color: #fffde7; color: #f57f17; }
+.symbol-kind-badge[data-kind="enum"] { background-color: #e0f2f1; color: #004d40; }
+
+/* Dark mode overrides */
+@media(prefers-color-scheme: dark) {
+ .doxygen-section {
+ background: #1e1e1e;
+ border-top: 1px solid #444;
+ color: #fff;
+ }
+
+ .doxygen-header-container {
+ border-bottom: 1px solid #444;
+ }
+
+ .doxygen-section.collapsed .doxygen-header-container {
+ border-bottom: none;
+ }
+
+ .doxygen-header {
+ color: #eee;
+ }
+
+ .doxygen-panel-toggle-icon {
+ color: #aaa;
+ }
+
+ .doxygen-empty {
+ color: #aaa;
+ }
+
+ .symbol-tree-row:hover {
+ background-color: #2d2d2d;
+ }
+
+ .symbol-name {
+ color: #eee;
+ }
+
+ .symbol-location {
+ color: #aaa;
+ }
+
+ .symbol-kind-badge[data-kind="namespace"] { background-color: #0b2f44; color: #4fc3f7; }
+ .symbol-kind-badge[data-kind="class"] { background-color: #2d2421; color: #d7ccc8; }
+ .symbol-kind-badge[data-kind="struct"] { background-color: #241933; color: #d1c4e9; }
+ .symbol-kind-badge[data-kind="function"] { background-color: #1b301c; color: #81c784; }
+ .symbol-kind-badge[data-kind="variable"] { background-color: #321; color: #ffb74d; }
+ .symbol-kind-badge[data-kind="typedef"] { background-color: #331420; color: #f48fb1; }
+ .symbol-kind-badge[data-kind="union"] { background-color: #333111; color: #fff176; }
+ .symbol-kind-badge[data-kind="enum"] { background-color: #0d2d2a; color: #4db6ac; }
+}
\ No newline at end of file
diff --git a/crubit_explorer/frontend/tsconfig.app.json b/crubit_explorer/frontend/tsconfig.app.json
new file mode 100644
index 000000000..43d7ce642
--- /dev/null
+++ b/crubit_explorer/frontend/tsconfig.app.json
@@ -0,0 +1,19 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/crubit_explorer/frontend/tsconfig.json b/crubit_explorer/frontend/tsconfig.json
new file mode 100644
index 000000000..9f0f9317a
--- /dev/null
+++ b/crubit_explorer/frontend/tsconfig.json
@@ -0,0 +1,37 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "declaration": false,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "lib": [
+ "ES2022",
+ "dom"
+ ]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/crubit_explorer/src/main.rs b/crubit_explorer/src/main.rs
index 2c552b69f..28610b8d8 100644
--- a/crubit_explorer/src/main.rs
+++ b/crubit_explorer/src/main.rs
@@ -2,17 +2,23 @@
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+use axum::body::Body;
+use axum::extract::State;
+use axum::http::{header, StatusCode, Uri};
+use axum::response::{IntoResponse, Response};
use axum::{routing, Router};
+use std::error::Error;
use std::net::SocketAddr;
-use std::path::PathBuf;
+use std::path::{Component, Path, PathBuf};
+use std::process::Command;
use tokio::net::TcpListener;
use runfiles::Runfiles;
const CC_BINDINGS_FROM_RS_RLOCATION: &str =
- "rules_crubit/cc_bindings_from_rs/cc_bindings_from_rs";
+ "rules_crubit/cc_bindings_from_rs/cc_bindings_from_rs";
-fn get_cc_bindings_from_rs_path() -> Result> {
+fn get_cc_bindings_from_rs_path() -> Result> {
// Environment variable for location to cc_bindings_from_rs binary
if let Ok(env_path) = std::env::var("CC_BINDINGS_FROM_RS")
&& let path = PathBuf::from(env_path)
@@ -48,9 +54,10 @@ fn get_cc_bindings_from_rs_path() -> Result>
Err("cc_bindings_from_rs binary not found via CC_BINDINGS_FROM_RS env var, Bazel runfiles, adjacent to executable, or in system PATH".into())
}
-fn new_cc_bindings_from_rs_command() -> Result> {
+#[cfg(test)]
+fn new_cc_bindings_from_rs_command() -> Result> {
let binary_path = get_cc_bindings_from_rs_path()?;
- let mut cmd = std::process::Command::new(binary_path);
+ let mut cmd = Command::new(binary_path);
let mut extra_lib_dirs = Vec::new();
@@ -104,18 +111,99 @@ fn find_in_path(name: &str) -> Option {
})
}
-fn app() -> Router {
- Router::new().route("/", routing::get(|| async { "Hello, World!" }))
+fn get_frontend_dist_path() -> Option {
+ if let Ok(r) = Runfiles::create() {
+ if let Some(path) = runfiles::rlocation!(r, "crubit_explorer/frontend/dist/frontend") {
+ if path.exists() {
+ return Some(path);
+ }
+ }
+ }
+
+ if let Ok(mut exe_path) = std::env::current_exe() {
+ exe_path.pop();
+ let adjacent_path = exe_path.join("frontend/dist/frontend");
+ if adjacent_path.exists() {
+ return Some(adjacent_path);
+ }
+ let adjacent_path2 = exe_path.join("dist/frontend");
+ if adjacent_path2.exists() {
+ return Some(adjacent_path2);
+ }
+ }
+
+ None
+}
+
+fn app(frontend_path: Option) -> Router {
+ if let Some(path) = frontend_path {
+ println!("Serving frontend from {}", path.display());
+ Router::new().fallback(serve_frontend).with_state(path)
+ } else {
+ println!("Frontend not found, serving Hello World at root");
+ Router::new().route("/", routing::get(|| async { "Hello, World!" }))
+ }
+}
+
+async fn serve_frontend(uri: Uri, State(frontend_path): State) -> impl IntoResponse {
+ let path = uri.path();
+ let relative_path = path.trim_start_matches('/');
+
+ if relative_path.is_empty() {
+ return serve_file(&frontend_path.join("index.html")).await;
+ }
+
+ // Don't allow path traversal
+ let has_parent_dir =
+ Path::new(relative_path).components().any(|c| matches!(c, Component::ParentDir));
+
+ if has_parent_dir {
+ return (StatusCode::FORBIDDEN, "Forbidden").into_response();
+ }
+
+ let file_path = frontend_path.join(relative_path);
+ if file_path.exists() && file_path.is_file() {
+ return serve_file(&file_path).await;
+ }
+
+ serve_file(&frontend_path.join("index.html")).await
+}
+
+async fn serve_file(path: &Path) -> Response {
+ match tokio::fs::read(path).await {
+ Ok(content) => {
+ let mime = mime_guess_fallback(path);
+ Response::builder()
+ .header(header::CONTENT_TYPE, mime)
+ .body(Body::from(content))
+ .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
+ }
+ Err(_) => StatusCode::NOT_FOUND.into_response(),
+ }
+}
+
+fn mime_guess_fallback(path: &Path) -> &'static str {
+ match path.extension().and_then(|s| s.to_str()) {
+ Some("html") => "text/html",
+ Some("js") => "application/javascript",
+ Some("css") => "text/css",
+ Some("svg") => "image/svg+xml",
+ Some("png") => "image/png",
+ Some("jpg") | Some("jpeg") => "image/jpeg",
+ Some("wasm") => "application/wasm",
+ _ => "application/octet-stream",
+ }
}
#[tokio::main]
-async fn main() -> Result<(), Box> {
+async fn main() -> Result<(), Box> {
match get_cc_bindings_from_rs_path() {
Ok(path) => println!("cc_bindings_from_rs found at: {}", path.display()),
Err(err) => eprintln!("Error locating cc_bindings_from_rs: {}", err),
}
- let app = app();
+ let frontend_path = get_frontend_dist_path();
+ let app = app(frontend_path);
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("Listening on http://{}", addr);
@@ -132,32 +220,56 @@ mod tests {
body::Body,
http::{Request, StatusCode},
};
+ use googletest::prelude::*;
use tower::ServiceExt;
+ #[gtest]
#[tokio::test]
async fn hello_world() {
- let app = app();
+ let app = app(None);
let response =
app.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()).await.unwrap();
- assert_eq!(response.status(), StatusCode::OK);
+ expect_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
- assert_eq!(&body[..], b"Hello, World!");
+ expect_eq!(&body[..], b"Hello, World!");
+ }
+
+ #[gtest]
+ #[tokio::test]
+ async fn test_frontend_serving() {
+ let path = get_frontend_dist_path();
+ if let Some(frontend_path) = path {
+ let app = app(Some(frontend_path));
+ let response = app
+ .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
+ .await
+ .unwrap();
+
+ expect_eq!(response.status(), StatusCode::OK);
+ let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
+ let html_str = String::from_utf8(body.to_vec()).unwrap();
+ expect_that!(html_str, contains_substring(""));
+ } else {
+ println!("Frontend dist path not found, skipping test.");
+ }
}
+ #[gtest]
#[tokio::test]
async fn test_cc_bindings_from_rs_help() {
let mut cmd = new_cc_bindings_from_rs_command()
.expect("Failed to create cc_bindings_from_rs command");
let output = cmd.arg("--help").output().expect("Failed to execute cc_bindings_from_rs");
- assert!(output.status.success());
+ expect_true!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
- assert!(stdout.contains("Generates C++ bindings for a Rust crate"));
+ expect_that!(stdout, contains_substring("Generates C++ bindings for a Rust crate"));
}
+ #[gtest]
#[tokio::test]
async fn test_generate_bindings() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
@@ -181,15 +293,15 @@ mod tests {
let stderr = String::from_utf8_lossy(&output.stderr);
- assert!(output.status.success(), "Command failed with stderr: {}", stderr);
- assert!(h_out.exists());
- assert!(rs_out.exists());
+ expect_true!(output.status.success(), "Command failed with stderr: {}", stderr);
+ expect_true!(h_out.exists());
+ expect_true!(rs_out.exists());
let h_content = std::fs::read_to_string(&h_out).expect("Failed to read h_out");
let rs_content = std::fs::read_to_string(&rs_out).expect("Failed to read rs_out");
- assert!(!h_content.is_empty());
- assert!(!rs_content.is_empty());
- assert!(h_content.contains("foo"));
+ expect_false!(h_content.is_empty());
+ expect_false!(rs_content.is_empty());
+ expect_that!(h_content, contains_substring("foo"));
}
}