mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Replace the dev server's wasm-pack, cargo-watch, and concurrently tools with custom cargo-run tooling (#4254)
Replace wasm-pack, cargo-watch and concurrently with custom build tooling Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
},
|
||||
"onCreateCommand": "cargo install cargo-watch wasm-pack cargo-about && cargo install -f wasm-bindgen-cli@0.2.121",
|
||||
"onCreateCommand": "cargo install cargo-about && cargo install -f wasm-bindgen-cli@0.2.121",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
// NOTE: Keep this in sync with `.vscode/extensions.json`
|
||||
|
||||
40
.github/workflows/build.yml
vendored
40
.github/workflows/build.yml
vendored
@@ -58,6 +58,8 @@ jobs:
|
||||
RUSTC_WRAPPER: /usr/bin/sccache
|
||||
CARGO_INCREMENTAL: 0
|
||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||
BINARYEN_VERSION: "130"
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -65,19 +67,11 @@ jobs:
|
||||
repository: ${{ inputs.checkout_repo || github.repository }}
|
||||
ref: ${{ inputs.checkout_ref || '' }}
|
||||
|
||||
- name: 🗑 Clear wasm-bindgen cache
|
||||
run: rm -r ~/.cache/.wasm-pack || true
|
||||
|
||||
- name: 🟢 Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: 🚧 Install build dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm run setup
|
||||
|
||||
- name: 🦀 Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
@@ -87,6 +81,19 @@ jobs:
|
||||
rustflags: ""
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: 🚧 Install wasm-bindgen-cli and Binaryen wasm-opt
|
||||
run: |
|
||||
if ! wasm-bindgen --version 2>/dev/null | grep -qF "$WASM_BINDGEN_CLI_VERSION"; then
|
||||
cargo install -f "wasm-bindgen-cli@$WASM_BINDGEN_CLI_VERSION"
|
||||
fi
|
||||
|
||||
BINARYEN_DIR="$HOME/.cache/binaryen-version_$BINARYEN_VERSION"
|
||||
if [ ! -x "$BINARYEN_DIR/bin/wasm-opt" ]; then
|
||||
mkdir -p "$BINARYEN_DIR"
|
||||
curl -sSfL "https://github.com/WebAssembly/binaryen/releases/download/version_$BINARYEN_VERSION/binaryen-version_$BINARYEN_VERSION-x86_64-linux.tar.gz" | tar xz -C "$BINARYEN_DIR" --strip-components=1
|
||||
fi
|
||||
echo "$BINARYEN_DIR/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: 🔀 Choose production deployment environment and insert template
|
||||
id: production-env
|
||||
if: github.event_name == 'push'
|
||||
@@ -294,6 +301,7 @@ jobs:
|
||||
|
||||
env:
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||
BINARYEN_VERSION: "130"
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
@@ -341,13 +349,15 @@ jobs:
|
||||
winget install --id LLVM.LLVM -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id Kitware.CMake -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id OpenSSL.OpenSSL -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id WebAssembly.Binaryen -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id GnuWin32.PkgConfig -e --accept-package-agreements --accept-source-agreements
|
||||
|
||||
"OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"PKG_CONFIG_PATH=C:\Program Files\OpenSSL-Win64\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
cargo binstall --no-confirm --force wasm-pack
|
||||
curl.exe -sSfL -o "$env:RUNNER_TEMP\binaryen.tar.gz" "https://github.com/WebAssembly/binaryen/releases/download/version_$env:BINARYEN_VERSION/binaryen-version_$env:BINARYEN_VERSION-x86_64-windows.tar.gz"
|
||||
tar -xzf "$env:RUNNER_TEMP\binaryen.tar.gz" -C $env:RUNNER_TEMP
|
||||
"$env:RUNNER_TEMP\binaryen-version_$env:BINARYEN_VERSION\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||
|
||||
cargo binstall --no-confirm --force cargo-about
|
||||
cargo binstall --no-confirm --force "wasm-bindgen-cli@$env:WASM_BINDGEN_CLI_VERSION"
|
||||
|
||||
@@ -485,6 +495,7 @@ jobs:
|
||||
|
||||
env:
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||
BINARYEN_VERSION: "130"
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
@@ -529,7 +540,6 @@ jobs:
|
||||
brew install \
|
||||
pkg-config \
|
||||
openssl@3 \
|
||||
binaryen \
|
||||
llvm \
|
||||
cargo-binstall
|
||||
|
||||
@@ -537,7 +547,13 @@ jobs:
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH
|
||||
|
||||
cargo binstall --no-confirm --force wasm-pack
|
||||
BINARYEN_DIR="$HOME/.cache/binaryen-version_$BINARYEN_VERSION"
|
||||
if [ ! -x "$BINARYEN_DIR/bin/wasm-opt" ]; then
|
||||
mkdir -p "$BINARYEN_DIR"
|
||||
curl -sSfL "https://github.com/WebAssembly/binaryen/releases/download/version_$BINARYEN_VERSION/binaryen-version_$BINARYEN_VERSION-arm64-macos.tar.gz" | tar xz -C "$BINARYEN_DIR" --strip-components=1
|
||||
fi
|
||||
echo "$BINARYEN_DIR/bin" >> "$GITHUB_PATH"
|
||||
|
||||
cargo binstall --no-confirm --force cargo-about
|
||||
cargo binstall --no-confirm --force "wasm-bindgen-cli@${WASM_BINDGEN_CLI_VERSION}"
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ pkgs.mkShell (
|
||||
pkgs.nodejs
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli_0_2_121
|
||||
pkgs.wasm-pack
|
||||
pkgs.cargo-about
|
||||
|
||||
pkgs.rustc
|
||||
@@ -41,7 +40,6 @@ pkgs.mkShell (
|
||||
|
||||
pkgs.git
|
||||
|
||||
pkgs.cargo-watch
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-expand
|
||||
|
||||
|
||||
@@ -84,7 +84,6 @@ deps.crane.lib.buildPackage (
|
||||
pkgs.nodejs
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli_0_2_121
|
||||
pkgs.wasm-pack
|
||||
pkgs.cargo-about
|
||||
pkgs.removeReferencesTo
|
||||
pkgs.importNpmLock.npmConfigHook
|
||||
@@ -94,7 +93,6 @@ deps.crane.lib.buildPackage (
|
||||
npmRoot = "${info.src}/frontend";
|
||||
};
|
||||
npmRoot = "frontend";
|
||||
npmConfigScript = "setup";
|
||||
makeCacheWritable = true;
|
||||
|
||||
env = {
|
||||
|
||||
180
Cargo.lock
generated
180
Cargo.lock
generated
@@ -505,9 +505,20 @@ dependencies = [
|
||||
name = "cargo-run"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"duct",
|
||||
"semver",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-run-internal-watch"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"cargo-run",
|
||||
"notify",
|
||||
"notify-debouncer-full",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-util-schemas"
|
||||
version = "0.8.2"
|
||||
@@ -1335,6 +1346,18 @@ dependencies = [
|
||||
"dtoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "duct"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"shared_child",
|
||||
"shared_thread",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
@@ -1514,6 +1537,15 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-id"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.26"
|
||||
@@ -1637,6 +1669,15 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
@@ -2797,6 +2838,26 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpreted-executor"
|
||||
version = "0.1.0"
|
||||
@@ -2992,6 +3053,26 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.13.1"
|
||||
@@ -3257,6 +3338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -3433,6 +3515,46 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-full"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302"
|
||||
dependencies = [
|
||||
"file-id",
|
||||
"log",
|
||||
"notify",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
@@ -3807,6 +3929,16 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.25.1"
|
||||
@@ -5207,12 +5339,60 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_child"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sigchld",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_thread"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "sigchld"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
|
||||
@@ -27,6 +27,7 @@ members = [
|
||||
"node-graph/preprocessor",
|
||||
"proc-macros",
|
||||
"tools/cargo-run",
|
||||
"tools/cargo-run/internal/*",
|
||||
"tools/crate-hierarchy-viz",
|
||||
"tools/third-party-licenses",
|
||||
"tools/editor-message-tree",
|
||||
|
||||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
node_modules/
|
||||
wasm/pkg/
|
||||
wrapper/pkg/
|
||||
public/build/
|
||||
dist/
|
||||
|
||||
@@ -12,7 +12,7 @@ Source code for the web app in the form of Svelte components and [TypeScript](ht
|
||||
|
||||
## Editor wrapper: `wrapper/`
|
||||
|
||||
Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use as an entry point, unburdened by Rust's complex data types that are incompatible with JS data types. Bindings (JS functions that call into the Wasm module) are provided by [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) in concert with [wasm-pack](https://github.com/rustwasm/wasm-pack).
|
||||
Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use as an entry point, unburdened by Rust's complex data types that are incompatible with JS data types. Bindings (JS functions that call into the Wasm module) are provided by `wasm-bindgen`. As part of `cargo run`, our build tool compiles this crate to Wasm, runs the `wasm-bindgen` CLI to generate the JS/TS bindings in `wrapper/pkg/`, and (for release builds) optimizes the binary with Binaryen's `wasm-opt`.
|
||||
|
||||
## ESLint configuration: `eslint.config.js`
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// This script automatically installs the npm packages listed in package-lock.json and runs before `npm start`.
|
||||
// This script automatically installs the npm packages listed in package-lock.json and runs as part of `npm run setup` (invoked by `cargo run`).
|
||||
// It skips the installation if this has already run and neither package.json nor package-lock.json has been modified since.
|
||||
|
||||
import { execSync } from "child_process";
|
||||
@@ -11,7 +11,7 @@ const isInstallNeeded = () => {
|
||||
if (!existsSync(INSTALL_TIMESTAMP_FILE)) return true;
|
||||
|
||||
const timestamp = statSync(INSTALL_TIMESTAMP_FILE).mtime;
|
||||
return ["package.json", "package-lock.json"].some((file) => {
|
||||
return ["package.json", "package-lock.json", "package-installer.js"].some((file) => {
|
||||
return existsSync(file) && statSync(file).mtime > timestamp;
|
||||
});
|
||||
};
|
||||
@@ -22,8 +22,10 @@ if (isInstallNeeded()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Installing npm packages...");
|
||||
|
||||
// Check if packages are up to date, doing so quickly by using `npm ci`, preferring local cached packages, and skipping the package audit and other checks
|
||||
execSync("npm ci --prefer-offline --no-audit --no-fund", { stdio: "inherit" });
|
||||
// Check if packages are up to date, doing so quickly by using `npm ci`, preferring local cached packages, and skipping the package audit and other checks.
|
||||
// The devDependencies are explicitly included because they hold the build tooling (Vite, etc.), which npm would
|
||||
// otherwise omit in environments that set NODE_ENV=production (like CI does for the sake of the Vite build).
|
||||
execSync("npm ci --include=dev --prefer-offline --no-audit --no-fund", { stdio: "inherit" });
|
||||
|
||||
// Touch the install timestamp file
|
||||
writeFileSync(INSTALL_TIMESTAMP_FILE, "");
|
||||
|
||||
270
frontend/package-lock.json
generated
270
frontend/package-lock.json
generated
@@ -14,7 +14,6 @@
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"@types/node": "^25.5.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
@@ -2069,84 +2068,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -2184,31 +2105,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2570,16 +2466,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -3206,16 +3092,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -5270,16 +5146,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "2.0.0-next.6",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
|
||||
@@ -5358,16 +5224,6 @@
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sade": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
@@ -5539,19 +5395,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@@ -5951,22 +5794,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
@@ -6126,16 +5953,6 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/treeify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz",
|
||||
@@ -6177,7 +5994,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
@@ -6705,16 +6523,6 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
@@ -6725,80 +6533,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -6,25 +6,9 @@
|
||||
"browserslist": "> 1.5%, last 2 versions, not dead, not ie 11, not op_mini all, not ios_saf < 13",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"---------- DEV SERVER ----------": "",
|
||||
"start": "npm run setup && npm run wasm:build-dev && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-dev\"",
|
||||
"production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"",
|
||||
"---------- BUILDS ----------": "",
|
||||
"build": "npm run setup && npm run wasm:build-production && vite build",
|
||||
"build-dev": "npm run setup && npm run wasm:build-dev && vite build --mode dev",
|
||||
"build-native": "npm run setup && npm run native:build-production",
|
||||
"build-native-dev": "npm run setup && npm run native:build-dev",
|
||||
"---------- UTILITIES ----------": "",
|
||||
"check": "svelte-check --fail-on-warnings && eslint",
|
||||
"fix": "eslint --fix",
|
||||
"---------- INTERNAL ----------": "",
|
||||
"setup": "node package-installer.js && node branding-installer.js",
|
||||
"native:build-dev": "wasm-pack build ./wrapper --dev --target=web --no-default-features --features native && vite build --mode native",
|
||||
"native:build-production": "wasm-pack build ./wrapper --release --target=web --no-default-features --features native && vite build --mode native",
|
||||
"wasm:build-dev": "wasm-pack build ./wrapper --dev --target=web",
|
||||
"wasm:build-production": "wasm-pack build ./wrapper --release --target=web",
|
||||
"wasm:watch-dev": "cargo watch --postpone --watch-when-idle --workdir=wrapper --shell \"wasm-pack build . --dev --target=web -- --color=always\"",
|
||||
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wrapper --shell \"wasm-pack build . --release --target=web -- --color=always\""
|
||||
"setup": "node package-installer.js && node branding-installer.js"
|
||||
},
|
||||
"//": "NOTE: `source-sans-pro` is never to be upgraded to 3.x because that renders 1px above its intended position.",
|
||||
"///": "Waiting on <https://github.com/import-js/eslint-plugin-import/issues/3227> before we can update @eslint/js and eslint to 10.x",
|
||||
@@ -36,7 +20,6 @@
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"@types/node": "^25.5.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
|
||||
@@ -40,22 +40,6 @@ ron = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
[package.metadata.wasm-pack.profile.dev]
|
||||
wasm-opt = false
|
||||
|
||||
[package.metadata.wasm-pack.profile.dev.wasm-bindgen]
|
||||
debug-js-glue = true
|
||||
demangle-name-section = true
|
||||
dwarf-debug-info = false
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ["-Os", "-g"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.release.wasm-bindgen]
|
||||
debug-js-glue = false
|
||||
demangle-name-section = false
|
||||
dwarf-debug-info = false
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(wasm_bindgen_unstable_test_coverage)',
|
||||
|
||||
@@ -9,3 +9,5 @@ default-run = "cargo-run"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
semver = "1"
|
||||
duct = "1"
|
||||
|
||||
11
tools/cargo-run/internal/watch/Cargo.toml
Normal file
11
tools/cargo-run/internal/watch/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "cargo-run-internal-watch"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cargo-run = { path = "../.." }
|
||||
notify = "8.2.0"
|
||||
notify-debouncer-full = "0.7.0"
|
||||
93
tools/cargo-run/internal/watch/src/main.rs
Normal file
93
tools/cargo-run/internal/watch/src/main.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use cargo_run::Error;
|
||||
use cargo_run::cmd::prelude::*;
|
||||
use notify::RecursiveMode;
|
||||
use notify::event::{EventKind, ModifyKind};
|
||||
use notify_debouncer_full::{Debouncer, RecommendedCache, new_debouncer, notify::RecommendedWatcher};
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
use std::time::Duration;
|
||||
|
||||
const EXCLUDED_DIRECTORIES: &[&str] = &["target", ".git", "frontend/node_modules", "frontend/dist", "frontend/wrapper/pkg"];
|
||||
const INCLUDED_EXTENSIONS: &[&str] = &["rs"];
|
||||
|
||||
const DEBOUNCE: Duration = Duration::from_millis(500);
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let release = std::env::args().nth(1).as_deref() == Some("release");
|
||||
|
||||
let _guard = match watch(cargo_run::frontend::build_wasm_steps(release, false)) {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
eprintln!("Error setting up file watcher: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
println!("Watching for changes...");
|
||||
loop {
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WatchGuard {
|
||||
_debouncer: Debouncer<RecommendedWatcher, RecommendedCache>,
|
||||
}
|
||||
|
||||
pub fn watch(steps: impl IntoIterator<Item = Expression>) -> Result<WatchGuard, Error> {
|
||||
let steps: Vec<Expression> = steps.into_iter().collect();
|
||||
let root = std::env::current_dir()
|
||||
.and_then(|p| p.canonicalize())
|
||||
.map_err(|e| Error::Io(e, "Failed to resolve root for file watcher".into()))?;
|
||||
let root_clone = root.clone();
|
||||
|
||||
let mut current: Option<Sequence> = None;
|
||||
|
||||
let mut debouncer = new_debouncer(DEBOUNCE, None, move |result: notify_debouncer_full::DebounceEventResult| match result {
|
||||
Ok(events) => {
|
||||
let mut seen = HashSet::new();
|
||||
let mut triggered = false;
|
||||
for ev in events {
|
||||
if !matches!(
|
||||
&ev.event.kind,
|
||||
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Modify(ModifyKind::Data(_) | ModifyKind::Name(_) | ModifyKind::Any)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for path in &ev.event.paths {
|
||||
if is_excluded(path, &root) {
|
||||
continue;
|
||||
}
|
||||
if seen.insert(path.clone()) {
|
||||
triggered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if triggered {
|
||||
if let Some(c) = current.take() {
|
||||
c.kill();
|
||||
}
|
||||
current = Some(sequence(steps.clone()));
|
||||
}
|
||||
}
|
||||
Err(errors) => {
|
||||
for e in errors {
|
||||
eprintln!("watch: {e}");
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|e| Error::Io(std::io::Error::other(e.to_string()), "file watcher".into()))?;
|
||||
|
||||
debouncer
|
||||
.watch(&root_clone, RecursiveMode::Recursive)
|
||||
.map_err(|e| Error::Io(std::io::Error::other(e.to_string()), "file watcher".into()))?;
|
||||
|
||||
Ok(WatchGuard { _debouncer: debouncer })
|
||||
}
|
||||
|
||||
fn is_excluded(path: &Path, root: &Path) -> bool {
|
||||
let rel = path.strip_prefix(root).unwrap_or(path);
|
||||
if EXCLUDED_DIRECTORIES.iter().any(|d| rel.starts_with(d)) {
|
||||
return true;
|
||||
}
|
||||
!path.extension().and_then(|e| e.to_str()).is_some_and(|e| INCLUDED_EXTENSIONS.contains(&e))
|
||||
}
|
||||
334
tools/cargo-run/src/cmd.rs
Normal file
334
tools/cargo-run/src/cmd.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use crate::Error;
|
||||
use duct::{Handle, ReaderHandle};
|
||||
use std::ffi::OsString;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
pub use duct::{Expression, cmd};
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::{Expression, ExpressionExt, Sequence, TerminalColor, cmd, sequence, supervise, utils};
|
||||
}
|
||||
|
||||
pub trait ExpressionExt {
|
||||
fn arg(self, arg: impl Into<OsString>) -> Self;
|
||||
fn args<I, S>(self, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>;
|
||||
fn arg_if(self, cond: bool, arg: impl Into<OsString>) -> Self;
|
||||
fn args_if<I, S>(self, cond: bool, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>;
|
||||
fn run(self) -> Result<(), Error>;
|
||||
fn read(self) -> Result<String, Error>;
|
||||
fn output_unchecked(self) -> Result<std::process::Output, Error>;
|
||||
}
|
||||
|
||||
impl ExpressionExt for Expression {
|
||||
fn arg(self, arg: impl Into<OsString>) -> Self {
|
||||
let arg = arg.into();
|
||||
self.before_spawn(move |c| {
|
||||
c.arg(&arg);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn args<I, S>(self, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
let args: Vec<OsString> = args.into_iter().map(Into::into).collect();
|
||||
self.before_spawn(move |c| {
|
||||
c.args(&args);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn arg_if(self, cond: bool, arg: impl Into<OsString>) -> Self {
|
||||
if cond { self.arg(arg) } else { self }
|
||||
}
|
||||
|
||||
fn args_if<I, S>(self, cond: bool, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
if cond { self.args(args) } else { self }
|
||||
}
|
||||
|
||||
fn run(self) -> Result<(), Error> {
|
||||
Expression::run(&self).map_err(Error::Command)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(self) -> Result<String, Error> {
|
||||
Expression::read(&self).map_err(Error::Command)
|
||||
}
|
||||
|
||||
fn output_unchecked(self) -> Result<std::process::Output, Error> {
|
||||
let e = self.unchecked().stdout_capture().stderr_capture();
|
||||
Expression::run(&e).map_err(Error::Command)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TerminalColor {
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
White,
|
||||
Reset,
|
||||
}
|
||||
impl TerminalColor {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Black => "\x1b[30m",
|
||||
Self::Red => "\x1b[31m",
|
||||
Self::Green => "\x1b[32m",
|
||||
Self::Yellow => "\x1b[33m",
|
||||
Self::Blue => "\x1b[34m",
|
||||
Self::Magenta => "\x1b[35m",
|
||||
Self::Cyan => "\x1b[36m",
|
||||
Self::White => "\x1b[37m",
|
||||
Self::Reset => "\x1b[0m",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sequence<I: IntoIterator<Item = Expression>>(expressions: I) -> Sequence {
|
||||
let expressions: Vec<Expression> = expressions.into_iter().collect();
|
||||
let current: Arc<Mutex<Option<Arc<Handle>>>> = Arc::new(Mutex::new(None));
|
||||
let killed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let worker_current = Arc::clone(¤t);
|
||||
let worker_killed = Arc::clone(&killed);
|
||||
let worker = std::thread::spawn(move || {
|
||||
for expr in expressions {
|
||||
if worker_killed.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
let handle = match expr.start() {
|
||||
Ok(h) => Arc::new(h),
|
||||
Err(e) => {
|
||||
eprintln!("sequence: failed to start step: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut slot = worker_current.lock().unwrap();
|
||||
if worker_killed.load(Ordering::SeqCst) {
|
||||
let _ = handle.kill();
|
||||
return;
|
||||
}
|
||||
*slot = Some(Arc::clone(&handle));
|
||||
}
|
||||
let result = handle.wait().map(|_| ());
|
||||
worker_current.lock().unwrap().take();
|
||||
if worker_killed.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = result {
|
||||
eprintln!("sequence: step failed: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Sequence {
|
||||
current,
|
||||
killed,
|
||||
worker: Some(worker),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sequence {
|
||||
current: Arc<Mutex<Option<Arc<Handle>>>>,
|
||||
killed: Arc<AtomicBool>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
}
|
||||
impl Sequence {
|
||||
pub fn kill(&self) {
|
||||
let slot = self.current.lock().unwrap();
|
||||
self.killed.store(true, Ordering::SeqCst);
|
||||
if let Some(handle) = slot.as_ref() {
|
||||
let _ = handle.kill();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait(&mut self) {
|
||||
if let Some(w) = self.worker.take() {
|
||||
let _ = w.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for Sequence {
|
||||
fn drop(&mut self) {
|
||||
self.kill();
|
||||
self.wait();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supervise<I, S>(children: I) -> Result<(), Error>
|
||||
where
|
||||
I: IntoIterator<Item = (S, TerminalColor, Expression)>,
|
||||
S: Into<String>,
|
||||
{
|
||||
use std::io::{BufRead, BufReader, IsTerminal, Write};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
windows_ctrl_c::install();
|
||||
|
||||
let mut handles: Vec<(String, TerminalColor, Arc<ReaderHandle>)> = Vec::new();
|
||||
for (label, color, expr) in children {
|
||||
#[cfg(target_os = "windows")]
|
||||
let expr = expr.before_spawn(|cmd| {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
|
||||
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let handle = expr.stderr_to_stdout().reader().map_err(Error::Command)?;
|
||||
handles.push((label.into(), color, Arc::new(handle)));
|
||||
}
|
||||
|
||||
let mut io_threads = Vec::new();
|
||||
for (label, color, handle) in handles.iter() {
|
||||
let prefix = if std::io::stdout().is_terminal() {
|
||||
format!("{color}[{label}]{reset} ", color = color.as_str(), reset = TerminalColor::Reset.as_str())
|
||||
} else {
|
||||
format!("[{label}] ")
|
||||
};
|
||||
let handle = handle.clone();
|
||||
io_threads.push(thread::spawn(move || {
|
||||
let reader = BufReader::new(&*handle);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
let out = std::io::stdout();
|
||||
let mut out = out.lock();
|
||||
let _ = writeln!(out, "{prefix}{line}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let mut reason: Option<(String, std::process::ExitStatus)> = None;
|
||||
|
||||
loop {
|
||||
#[cfg(target_os = "windows")]
|
||||
if windows_ctrl_c::interrupted() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (label, _color, handle) in handles.iter() {
|
||||
if let Ok(Some(output)) = handle.try_wait() {
|
||||
reason = Some((label.clone(), output.status));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if reason.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
for (_, _color, handle) in handles.iter() {
|
||||
if matches!(handle.try_wait(), Ok(Some(_))) {
|
||||
continue;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
for pid in handle.pids() {
|
||||
let _ = std::process::Command::new("taskkill")
|
||||
.args(["/T", "/F", "/PID", &pid.to_string()])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = handle.kill();
|
||||
}
|
||||
}
|
||||
|
||||
for t in io_threads {
|
||||
let _ = t.join();
|
||||
}
|
||||
|
||||
match reason {
|
||||
Some((label, status)) if !status.success() => Err(Error::Command(std::io::Error::other(format!("supervised child '{label}' exited with status {status}")))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod utils {
|
||||
use super::*;
|
||||
|
||||
pub fn internal(name: &str) -> Expression {
|
||||
let package = format!("cargo-run-internal-{name}");
|
||||
cmd!("cargo", "run", "-p", package, "--")
|
||||
}
|
||||
|
||||
pub fn npm<I, S>(args: I) -> Expression
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
let prog = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
|
||||
cmd(prog, args)
|
||||
}
|
||||
|
||||
pub fn node_bin(rel: &str) -> Expression {
|
||||
cmd!("node", format!("node_modules/{rel}"))
|
||||
}
|
||||
|
||||
pub fn open_url(url: &str) -> Result<(), Error> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let expr = cmd!("cmd", "/c", "start", url);
|
||||
#[cfg(target_os = "macos")]
|
||||
let expr = cmd!("open", url);
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
let expr = cmd!("xdg-open", url);
|
||||
expr.run()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_ctrl_c {
|
||||
use std::sync::Once;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[link(name = "kernel32")]
|
||||
unsafe extern "system" {
|
||||
fn SetConsoleCtrlHandler(handler: Option<unsafe extern "system" fn(u32) -> i32>, add: i32) -> i32;
|
||||
}
|
||||
|
||||
unsafe extern "system" fn handler(_ctrl_type: u32) -> i32 {
|
||||
INTERRUPTED.store(true, Ordering::SeqCst);
|
||||
1 // Report the event as handled
|
||||
}
|
||||
|
||||
pub fn install() {
|
||||
INTERRUPTED.store(false, Ordering::SeqCst);
|
||||
static REGISTER: Once = Once::new();
|
||||
REGISTER.call_once(|| unsafe {
|
||||
SetConsoleCtrlHandler(Some(handler), 1);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn interrupted() -> bool {
|
||||
INTERRUPTED.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
62
tools/cargo-run/src/frontend.rs
Normal file
62
tools/cargo-run/src/frontend.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::cmd::prelude::*;
|
||||
use crate::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const WRAPPER_CRATE: &str = "graphite-wasm-wrapper";
|
||||
const WASM_TARGET: &str = "wasm32-unknown-unknown";
|
||||
const OUT_NAME: &str = "graphite_wasm_wrapper";
|
||||
|
||||
pub fn frontend_dir() -> PathBuf {
|
||||
workspace_dir().join("frontend")
|
||||
}
|
||||
|
||||
pub fn setup() -> Result<(), Error> {
|
||||
utils::npm(["run", "setup"]).dir(frontend_dir()).run()
|
||||
}
|
||||
|
||||
pub fn build_wasm(release: bool, native: bool) -> Result<(), Error> {
|
||||
sequence(build_wasm_steps(release, native)).wait();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_wasm_steps(release: bool, native: bool) -> Vec<Expression> {
|
||||
let wasm_artifact = target_dir().join(WASM_TARGET).join(if release { "release" } else { "debug" }).join(format!("{OUT_NAME}.wasm"));
|
||||
let pkg_dir = frontend_dir().join("wrapper").join("pkg");
|
||||
|
||||
let mut steps = vec![
|
||||
cmd!("cargo", "build", "--lib", "--package", WRAPPER_CRATE, "--target", WASM_TARGET)
|
||||
.arg_if(release, "--release")
|
||||
.args_if(native, ["--no-default-features", "--features", "native"])
|
||||
.dir(workspace_dir()),
|
||||
cmd!("wasm-bindgen", "--target", "web", "--out-name", OUT_NAME, "--out-dir", &pkg_dir, &wasm_artifact)
|
||||
.arg_if(release, "--no-demangle")
|
||||
.arg_if(!release, "--debug"),
|
||||
];
|
||||
|
||||
if release {
|
||||
let wasm_file = pkg_dir.join(format!("{OUT_NAME}_bg.wasm"));
|
||||
steps.push(cmd!("wasm-opt", "-Os", "-g", &wasm_file, "-o", &wasm_file));
|
||||
}
|
||||
|
||||
steps
|
||||
}
|
||||
|
||||
pub fn vite() -> Expression {
|
||||
utils::node_bin("vite/bin/vite.js").dir(frontend_dir()).env("CARGO_TARGET_DIR", target_dir())
|
||||
}
|
||||
|
||||
pub fn watch(release: bool) -> Result<(), Error> {
|
||||
use crate::cmd::prelude::*;
|
||||
|
||||
setup()?;
|
||||
build_wasm(release, false)?;
|
||||
|
||||
let vite = vite().env("FORCE_COLOR", "1").env("CARGO_TERM_COLOR", "always");
|
||||
let rust = utils::internal("watch")
|
||||
.arg_if(release, "release")
|
||||
.dir(workspace_dir())
|
||||
.env("CARGO_TARGET_DIR", target_dir())
|
||||
.env("CARGO_TERM_COLOR", "always");
|
||||
|
||||
supervise([("VITE", TerminalColor::Magenta, vite), ("RUST", TerminalColor::Blue, rust)])
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
pub mod cmd;
|
||||
pub mod frontend;
|
||||
pub mod requirements;
|
||||
|
||||
pub enum Action {
|
||||
@@ -65,70 +66,25 @@ impl Task {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(command: &str) -> Result<(), Error> {
|
||||
run_from(command, None)
|
||||
pub fn workspace_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_WORKSPACE_DIR"))
|
||||
}
|
||||
|
||||
pub fn npm_run_in_frontend_dir(args: &str) -> Result<(), Error> {
|
||||
let workspace_dir = std::path::PathBuf::from(env!("CARGO_WORKSPACE_DIR"));
|
||||
let frontend_dir = workspace_dir.join("frontend");
|
||||
let npm = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
|
||||
run_from(&format!("{npm} run {args}"), Some(&frontend_dir))
|
||||
}
|
||||
|
||||
pub fn open_url(url: &str) -> Result<(), Error> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut cmd = process::Command::new("cmd");
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.args(["/c", "start", url]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut cmd = process::Command::new("open");
|
||||
#[cfg(target_os = "macos")]
|
||||
cmd.arg(url);
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
let mut cmd = process::Command::new("xdg-open");
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
cmd.arg(url);
|
||||
|
||||
let command_str = format!("{:?}", cmd);
|
||||
let exit_code = cmd
|
||||
.spawn()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to spawn command '{command_str}'")))?
|
||||
.wait()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to wait for command '{command_str}'")))?;
|
||||
if !exit_code.success() {
|
||||
return Err(Error::Command(command_str, exit_code));
|
||||
pub fn target_dir() -> PathBuf {
|
||||
match std::env::var_os("CARGO_TARGET_DIR") {
|
||||
Some(custom_dir) => workspace_dir().join(custom_dir),
|
||||
None => workspace_dir().join("target"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_from(command: &str, dir: Option<&PathBuf>) -> Result<(), Error> {
|
||||
let command = command.split_whitespace().collect::<Vec<_>>();
|
||||
let mut cmd = process::Command::new(command[0]);
|
||||
if command.len() > 1 {
|
||||
cmd.args(&command[1..]);
|
||||
}
|
||||
if let Some(dir) = dir {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
let exit_code = cmd
|
||||
.spawn()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to spawn command '{}'", command.join(" "))))?
|
||||
.wait()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to wait for command '{}'", command.join(" "))))?;
|
||||
if !exit_code.success() {
|
||||
return Err(Error::Command(command.join(" "), exit_code));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("One or more requirements were not met")]
|
||||
RequirementsNotMet,
|
||||
|
||||
#[error("{1}: {0}")]
|
||||
Io(#[source] std::io::Error, String),
|
||||
|
||||
#[error("Command '{0}' exited with code {1}")]
|
||||
Command(String, process::ExitStatus),
|
||||
/// Used by the duct-based `cmd` module; folds in `Command` once call sites are migrated.
|
||||
#[error("{0}")]
|
||||
Command(#[source] std::io::Error),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::process::ExitCode;
|
||||
|
||||
use cargo_run::cmd::prelude::*;
|
||||
use cargo_run::*;
|
||||
|
||||
fn usage() {
|
||||
@@ -33,6 +34,8 @@ fn usage() {
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
prepend_path();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let args: Vec<&str> = args.iter().skip(1).map(String::as_str).collect();
|
||||
|
||||
@@ -67,9 +70,9 @@ fn explore_usage() {
|
||||
fn run_task(task: &Task) -> Result<(), Error> {
|
||||
if let Action::Explore(tool) = &task.action {
|
||||
match tool.as_deref() {
|
||||
Some("bisect") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/debugging-tips/#build-bisect-tool"),
|
||||
Some("deps") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/#crate-dependency-graph"),
|
||||
Some("editor") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/editor-structure/#editor-outline"),
|
||||
Some("bisect") => return utils::open_url("https://graphite.art/volunteer/guide/codebase-overview/debugging-tips/#build-bisect-tool"),
|
||||
Some("deps") => return utils::open_url("https://graphite.art/volunteer/guide/codebase-overview/#crate-dependency-graph"),
|
||||
Some("editor") => return utils::open_url("https://graphite.art/volunteer/guide/codebase-overview/editor-structure/#editor-outline"),
|
||||
None | Some("--help") => {
|
||||
explore_usage();
|
||||
return Ok(());
|
||||
@@ -85,11 +88,19 @@ fn run_task(task: &Task) -> Result<(), Error> {
|
||||
requirements::check(task)?;
|
||||
|
||||
match (&task.action, &task.target, &task.profile) {
|
||||
(Action::Run, Target::Web, Profile::Debug | Profile::Default) => npm_run_in_frontend_dir("start")?,
|
||||
(Action::Run, Target::Web, Profile::Release) => npm_run_in_frontend_dir("production")?,
|
||||
(Action::Run, Target::Web, Profile::Debug | Profile::Default) => frontend::watch(false)?,
|
||||
(Action::Run, Target::Web, Profile::Release) => frontend::watch(true)?,
|
||||
|
||||
(Action::Build, Target::Web, Profile::Debug) => npm_run_in_frontend_dir("build-dev")?,
|
||||
(Action::Build, Target::Web, Profile::Release | Profile::Default) => npm_run_in_frontend_dir("build")?,
|
||||
(Action::Build, Target::Web, Profile::Debug) => {
|
||||
frontend::setup()?;
|
||||
frontend::build_wasm(false, false)?;
|
||||
frontend::vite().args(["build", "--mode", "dev"]).run()?;
|
||||
}
|
||||
(Action::Build, Target::Web, Profile::Release | Profile::Default) => {
|
||||
frontend::setup()?;
|
||||
frontend::build_wasm(true, false)?;
|
||||
frontend::vite().args(["build"]).run()?;
|
||||
}
|
||||
|
||||
(action, Target::Desktop, mut profile) => {
|
||||
if matches!(profile, Profile::Default) {
|
||||
@@ -100,34 +111,41 @@ fn run_task(task: &Task) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(profile, Profile::Release) {
|
||||
npm_run_in_frontend_dir("build-native")?;
|
||||
} else {
|
||||
npm_run_in_frontend_dir("build-native-dev")?;
|
||||
};
|
||||
// Build the editor's Wasm module with the `native` feature, then bundle the frontend with Vite
|
||||
frontend::setup()?;
|
||||
frontend::build_wasm(matches!(profile, Profile::Release), true)?;
|
||||
frontend::vite().args(["build", "--mode", "native"]).run()?;
|
||||
|
||||
run("cargo run -p third-party-licenses --features desktop")?;
|
||||
cmd!("cargo", "run", "-p", "third-party-licenses", "--features", "desktop").run()?;
|
||||
|
||||
let cargo_profile = match profile {
|
||||
Profile::Debug => "dev",
|
||||
Profile::Release => "release",
|
||||
Profile::Default => unreachable!(),
|
||||
};
|
||||
let args = if matches!(action, Action::Run) {
|
||||
format!(" -- open {}", task.args.join(" "))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
run(&format!("cargo run --profile {cargo_profile} -p graphite-desktop-bundle{args}"))?;
|
||||
cmd!("cargo", "run", "--profile", cargo_profile, "-p", "graphite-desktop-bundle")
|
||||
.args_if(matches!(action, Action::Run), ["--", "open"].into_iter().chain(task.args.iter().map(String::as_str)))
|
||||
.run()?;
|
||||
}
|
||||
|
||||
(Action::Run, Target::Cli, Profile::Debug | Profile::Default) => run(&format!("cargo run -p graphene-cli -- {}", task.args.join(" ")))?,
|
||||
(Action::Run, Target::Cli, Profile::Release) => run(&format!("cargo run -r -p graphene-cli -- {}", task.args.join(" ")))?,
|
||||
(Action::Run, Target::Cli, Profile::Debug | Profile::Default) => cmd!("cargo", "run", "-p", "graphene-cli", "--").args(&task.args).run()?,
|
||||
(Action::Run, Target::Cli, Profile::Release) => cmd!("cargo", "run", "-r", "-p", "graphene-cli", "--").args(&task.args).run()?,
|
||||
|
||||
(Action::Build, Target::Cli, Profile::Debug) => run("cargo build -p graphene-cli")?,
|
||||
(Action::Build, Target::Cli, Profile::Release | Profile::Default) => run("cargo build -r -p graphene-cli")?,
|
||||
(Action::Build, Target::Cli, Profile::Debug) => cmd!("cargo", "build", "-p", "graphene-cli").run()?,
|
||||
(Action::Build, Target::Cli, Profile::Release | Profile::Default) => cmd!("cargo", "build", "-r", "-p", "graphene-cli").run()?,
|
||||
|
||||
(Action::Explore(_), _, _) => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepend_path() {
|
||||
let mut paths = vec![target_dir().join("cargo-run").join("bin")];
|
||||
if let Some(path) = std::env::var_os("PATH") {
|
||||
paths.extend(std::env::split_paths(&path));
|
||||
}
|
||||
if let Ok(joined) = std::env::join_paths(paths) {
|
||||
// Safety: this runs before any other threads are spawned
|
||||
unsafe { std::env::set_var("PATH", joined) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use semver::{Version, VersionReq};
|
||||
use std::io::IsTerminal;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::cmd::prelude::*;
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct Requirement {
|
||||
pub struct Requirement {
|
||||
command: &'static str,
|
||||
args: &'static [&'static str],
|
||||
name: &'static str,
|
||||
check: Check,
|
||||
version: Option<&'static str>,
|
||||
install: Option<&'static str>,
|
||||
install: InstallAction,
|
||||
skip: Option<&'static dyn Fn(&Task) -> bool>,
|
||||
}
|
||||
|
||||
@@ -22,43 +24,43 @@ fn requirements(task: &Task) -> Vec<Requirement> {
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "cargo-about",
|
||||
args: &["--version"],
|
||||
name: "cargo-about",
|
||||
install: Some("cargo install cargo-about"),
|
||||
command: "rustc",
|
||||
args: &["--print", "target-libdir", "--target", "wasm32-unknown-unknown"],
|
||||
check: Check::Matches(&|out| std::path::Path::new(out.trim()).is_dir()),
|
||||
name: "Rust Wasm Toolchain",
|
||||
install: "rustup target add wasm32-unknown-unknown".into(),
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "cargo-watch",
|
||||
command: "cargo-about",
|
||||
args: &["--version"],
|
||||
name: "cargo-watch",
|
||||
install: Some("cargo install cargo-watch"),
|
||||
name: "Cargo About",
|
||||
install: "cargo install cargo-about".into(),
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "wasm-opt",
|
||||
args: &["--version"],
|
||||
name: "Wasm Opt",
|
||||
version: Some(">=130"),
|
||||
skip: Some(&|task| {
|
||||
!matches!(
|
||||
task,
|
||||
Task {
|
||||
target: Target::Web,
|
||||
action: Action::Run,
|
||||
..
|
||||
matches!(task.target, Target::Cli)
|
||||
|| match task.profile {
|
||||
Profile::Debug => true,
|
||||
Profile::Release => false,
|
||||
Profile::Default => matches!(task.action, Action::Run),
|
||||
}
|
||||
)
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "wasm-bindgen",
|
||||
args: &["--version"],
|
||||
name: "wasm-bindgen-cli",
|
||||
version: Some("0.2.121"),
|
||||
install: Some("cargo install -f wasm-bindgen-cli@0.2.121"),
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
},
|
||||
Requirement {
|
||||
command: "wasm-pack",
|
||||
args: &["--version"],
|
||||
name: "wasm-pack",
|
||||
install: Some("cargo install wasm-pack"),
|
||||
name: "Wasm Bindgen",
|
||||
version: Some("=0.2.121"),
|
||||
install: "cargo install -f wasm-bindgen-cli@0.2.121".into(),
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -98,24 +100,44 @@ pub fn check(task: &Task) -> Result<(), Error> {
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
|
||||
for dep in requirements(task) {
|
||||
match Command::new(dep.command).args(dep.args).output() {
|
||||
match cmd(dep.command, dep.args.iter().copied()).output_unchecked() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout);
|
||||
let version = version.lines().next().unwrap_or_default().trim();
|
||||
|
||||
if let Some(expected) = dep.version {
|
||||
if version.contains(expected) {
|
||||
eprintln!(" ✓ {} ({})", dep.name, version);
|
||||
} else {
|
||||
eprintln!(" ✗ {} (found {}, expected {})", dep.name, version, expected);
|
||||
if dep.install.is_some() {
|
||||
installable.push(dep);
|
||||
} else {
|
||||
failures.push(format!("{}: version mismatch (found {version}, expected {expected})", dep.name));
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
match dep.check {
|
||||
Check::PrintsVersion => {
|
||||
let line = stdout.lines().next().unwrap_or_default().trim();
|
||||
match dep.version {
|
||||
None => eprintln!(" ✓ {} ({line})", dep.name),
|
||||
Some(req_str) => {
|
||||
let req = VersionReq::parse(req_str).expect("invalid semver requirement");
|
||||
match extract_version(line) {
|
||||
Some(version) if req.matches(&version) => eprintln!(" ✓ {} ({version})", dep.name),
|
||||
Some(version) => {
|
||||
eprintln!(" ✗ {} (found {version}, requires {req_str})", dep.name);
|
||||
if dep.install.is_some() {
|
||||
installable.push(dep);
|
||||
} else {
|
||||
failures.push(format!("{}: version mismatch (found {version}, requires {req_str})", dep.name));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
eprintln!(" ✗ {} (could not parse version from '{line}')", dep.name);
|
||||
failures.push(format!("{}: could not parse version from '{line}'", dep.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Check::Matches(check) => {
|
||||
if !check(stdout.to_string()) {
|
||||
eprintln!(" ✗ {} - check failed", dep.name);
|
||||
if dep.install.is_some() {
|
||||
installable.push(dep);
|
||||
}
|
||||
} else {
|
||||
eprintln!(" ✓ {}", dep.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(" ✓ {} ({})", dep.name, version);
|
||||
}
|
||||
}
|
||||
Ok(output) => {
|
||||
@@ -147,7 +169,7 @@ pub fn check(task: &Task) -> Result<(), Error> {
|
||||
let total = installable.len() + failures.len();
|
||||
eprintln!("{total} requirement{} not met:", if total > 1 { "s" } else { "" });
|
||||
for dep in &installable {
|
||||
eprintln!(" - {}: {}", dep.name, dep.install.unwrap());
|
||||
eprintln!(" - {}", dep.name);
|
||||
}
|
||||
for msg in &failures {
|
||||
eprintln!(" - {msg}");
|
||||
@@ -158,43 +180,149 @@ pub fn check(task: &Task) -> Result<(), Error> {
|
||||
eprintln!("See: https://graphite.art/volunteer/guide/project-setup/");
|
||||
}
|
||||
|
||||
let is_interactive = std::io::stdout().is_terminal() && std::io::stderr().is_terminal() && std::io::stdin().is_terminal();
|
||||
|
||||
// Don't prompt for automatic installation if we're not interactive session
|
||||
if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() || !std::io::stdin().is_terminal() {
|
||||
if !is_interactive {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if installable.is_empty() {
|
||||
if installable.is_empty() && failures.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!();
|
||||
eprintln!("The following can be installed automatically:");
|
||||
for dep in &installable {
|
||||
eprintln!(" {}", dep.install.unwrap());
|
||||
}
|
||||
eprintln!();
|
||||
if installable.len() == 1 {
|
||||
eprint!("Install it now? [Y/n] ");
|
||||
} else {
|
||||
eprint!("Install them now? [Y/n] ");
|
||||
}
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).map_err(|e| Error::Io(e, "Failed to read from stdin".into()))?;
|
||||
let input = input.trim();
|
||||
|
||||
if input.is_empty() || input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
|
||||
if !installable.is_empty() {
|
||||
eprintln!();
|
||||
eprintln!("The following can be installed automatically:");
|
||||
for dep in &installable {
|
||||
let parts: Vec<&str> = dep.install.unwrap().split_whitespace().collect();
|
||||
eprintln!("Running: {}...", dep.install.unwrap());
|
||||
let status = Command::new(parts[0])
|
||||
.args(&parts[1..])
|
||||
.status()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to run '{}'", dep.install.unwrap())))?;
|
||||
if !status.success() {
|
||||
eprintln!("Failed to install {}", dep.name);
|
||||
eprintln!(" {}: {}", dep.name, dep.install.description());
|
||||
}
|
||||
eprintln!();
|
||||
if installable.len() == 1 {
|
||||
eprint!("Install it now? [Y/n] ");
|
||||
} else {
|
||||
eprint!("Install them now? [Y/n] ");
|
||||
}
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).map_err(|e| Error::Io(e, "Failed to read from stdin".into()))?;
|
||||
let input = input.trim();
|
||||
|
||||
if input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
|
||||
let mut successfully_installed = Vec::new();
|
||||
|
||||
for (i, dep) in installable.iter().enumerate() {
|
||||
eprintln!("Installing {}...", dep.name);
|
||||
match &dep.install {
|
||||
InstallAction::Command(install_cmd) => {
|
||||
let parts: Vec<&str> = install_cmd.split_whitespace().collect();
|
||||
let expr = cmd(parts[0], parts[1..].iter().copied()).unchecked();
|
||||
match Expression::run(&expr) {
|
||||
Ok(output) if output.status.success() => successfully_installed.push(i),
|
||||
Ok(_) => eprintln!("Failed to install {}", dep.name),
|
||||
Err(e) => return Err(Error::Command(e)),
|
||||
}
|
||||
}
|
||||
InstallAction::Function { function, .. } => {
|
||||
if let Err(e) = function() {
|
||||
eprintln!("{e}");
|
||||
eprintln!("Failed to install {}", dep.name);
|
||||
} else {
|
||||
successfully_installed.push(i);
|
||||
}
|
||||
}
|
||||
InstallAction::None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
for i in successfully_installed.into_iter().rev() {
|
||||
installable.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
eprintln!();
|
||||
eprintln!("The following requirements must be resolved manually:");
|
||||
for msg in &failures {
|
||||
eprintln!(" - {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!installable.is_empty() || !failures.is_empty()) && is_interactive {
|
||||
eprintln!();
|
||||
eprintln!("Continue without resolving these requirements? [y/N]");
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).map_err(|e| Error::Io(e, "Failed to read from stdin".into()))?;
|
||||
let input = input.trim();
|
||||
|
||||
if input.eq_ignore_ascii_case("n") || input.eq_ignore_ascii_case("no") {
|
||||
return Err(Error::RequirementsNotMet);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_version(line: &str) -> Option<Version> {
|
||||
line.split_whitespace().find_map(|token| {
|
||||
let token = token.trim_start_matches('v').trim_end_matches(|c: char| !c.is_ascii_alphanumeric());
|
||||
if token.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(version) = Version::parse(token) {
|
||||
return Some(version);
|
||||
}
|
||||
let (core, suffix) = match token.find(['-', '+']) {
|
||||
Some(i) => token.split_at(i),
|
||||
None => (token, ""),
|
||||
};
|
||||
let parts: Vec<&str> = core.split('.').collect();
|
||||
if parts.iter().any(|p| p.is_empty() || !p.chars().all(|c| c.is_ascii_digit())) {
|
||||
return None;
|
||||
}
|
||||
let major = parts[0];
|
||||
let minor = parts.get(1).copied().unwrap_or("0");
|
||||
let patch = parts.get(2).copied().unwrap_or("0");
|
||||
Version::parse(&format!("{major}.{minor}.{patch}{suffix}")).ok()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
enum Check {
|
||||
#[default]
|
||||
PrintsVersion,
|
||||
Matches(&'static dyn Fn(String) -> bool),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
enum InstallAction {
|
||||
#[default]
|
||||
None,
|
||||
Command(&'static str),
|
||||
#[expect(dead_code)] // TODO: Remove after followup pr landed
|
||||
Function {
|
||||
description: &'static str,
|
||||
function: &'static dyn Fn() -> Result<(), Error>,
|
||||
},
|
||||
}
|
||||
|
||||
impl InstallAction {
|
||||
fn is_some(&self) -> bool {
|
||||
!matches!(self, InstallAction::None)
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
match self {
|
||||
InstallAction::None => "",
|
||||
InstallAction::Command(cmd) => cmd,
|
||||
InstallAction::Function { description, .. } => description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for InstallAction {
|
||||
fn from(value: &'static str) -> Self {
|
||||
InstallAction::Command(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ fn parse(parsed: Output) -> Vec<LicenseEntry> {
|
||||
|
||||
fn run() -> Result<Output, Error> {
|
||||
let output = Command::new("cargo")
|
||||
.args(["about", "generate", "--format", "json", "--frozen"])
|
||||
.args(["about", "generate", "--format", "json", "--locked"])
|
||||
.current_dir(env!("CARGO_WORKSPACE_DIR"))
|
||||
.output()
|
||||
.map_err(|e| Error::Io(e, "Failed to run cargo about generate".into()))?;
|
||||
|
||||
@@ -98,10 +98,9 @@ fn run() -> Result<(), Error> {
|
||||
let current_hash = format!("{:016x}", hasher.finish());
|
||||
|
||||
if current_hash == fs::read_to_string(¤t_hash_path).unwrap_or_default() {
|
||||
eprintln!("No changes in licenses detected, skipping generation.");
|
||||
return Ok(());
|
||||
}
|
||||
eprintln!("Changes in licenses detected, generating new license file.");
|
||||
eprintln!("Changes in licenses detected, generating new license file...");
|
||||
|
||||
let licenses = merge_filter_dedup_and_sort(vec![
|
||||
cargo_source.licenses()?,
|
||||
|
||||
Reference in New Issue
Block a user