mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-14 13:09:39 +02:00
Compare commits
26 Commits
yaak-cli-0
...
worktree-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2a3f7a669 | ||
|
|
356b8ea0d6 | ||
|
|
f283b02cfe | ||
|
|
df089e6f00 | ||
|
|
0979398215 | ||
|
|
85260ab49a | ||
|
|
b4a1c418bb | ||
|
|
45262edfbd | ||
|
|
aed7bd12ea | ||
|
|
b5928af1d7 | ||
|
|
6cc47bea38 | ||
|
|
b83d9e6765 | ||
|
|
c8ba35e268 | ||
|
|
8a330ad1ec | ||
|
|
b563319bed | ||
|
|
3d577dd7d9 | ||
|
|
591c68c59c | ||
|
|
a0cb7f813f | ||
|
|
cfab62707e | ||
|
|
267508e533 | ||
|
|
242f55b609 | ||
|
|
67a3dd15ac | ||
|
|
543325613b | ||
|
|
88f5f0e045 | ||
|
|
615f3134d2 | ||
|
|
0c7051d59c |
@@ -1,9 +1,11 @@
|
|||||||
# Claude Context: Detaching Tauri from Yaak
|
# Claude Context: Detaching Tauri from Yaak
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core Rust crates in `crates/` should be usable independently, while Tauri-specific code lives in `crates-tauri/`.
|
Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core Rust crates in `crates/` should be usable independently, while Tauri-specific code lives in `crates-tauri/`.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/ # Core crates - should NOT depend on Tauri
|
crates/ # Core crates - should NOT depend on Tauri
|
||||||
crates-tauri/ # Tauri-specific crates (yaak-app, yaak-tauri-utils, etc.)
|
crates-tauri/ # Tauri-specific crates (yaak-app, yaak-tauri-utils, etc.)
|
||||||
@@ -13,11 +15,13 @@ crates-cli/ # CLI crate (yaak-cli)
|
|||||||
## Completed Work
|
## Completed Work
|
||||||
|
|
||||||
### 1. Folder Restructure
|
### 1. Folder Restructure
|
||||||
|
|
||||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app/`
|
- Moved Tauri-dependent app code to `crates-tauri/yaak-app/`
|
||||||
- Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling)
|
- Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling)
|
||||||
- Created `crates-cli/yaak-cli/` for the standalone CLI
|
- Created `crates-cli/yaak-cli/` for the standalone CLI
|
||||||
|
|
||||||
### 2. Decoupled Crates (no longer depend on Tauri)
|
### 2. Decoupled Crates (no longer depend on Tauri)
|
||||||
|
|
||||||
- **yaak-models**: Uses `init_standalone()` pattern for CLI database access
|
- **yaak-models**: Uses `init_standalone()` pattern for CLI database access
|
||||||
- **yaak-http**: Removed Tauri plugin, HttpConnectionManager initialized in yaak-app setup
|
- **yaak-http**: Removed Tauri plugin, HttpConnectionManager initialized in yaak-app setup
|
||||||
- **yaak-common**: Only contains Tauri-free utilities (serde, platform)
|
- **yaak-common**: Only contains Tauri-free utilities (serde, platform)
|
||||||
@@ -25,6 +29,7 @@ crates-cli/ # CLI crate (yaak-cli)
|
|||||||
- **yaak-grpc**: Replaced AppHandle with GrpcConfig struct, uses tokio::process::Command instead of Tauri sidecar
|
- **yaak-grpc**: Replaced AppHandle with GrpcConfig struct, uses tokio::process::Command instead of Tauri sidecar
|
||||||
|
|
||||||
### 3. CLI Implementation
|
### 3. CLI Implementation
|
||||||
|
|
||||||
- Basic CLI at `crates-cli/yaak-cli/src/main.rs`
|
- Basic CLI at `crates-cli/yaak-cli/src/main.rs`
|
||||||
- Commands: workspaces, requests, send (by ID), get (ad-hoc URL), create
|
- Commands: workspaces, requests, send (by ID), get (ad-hoc URL), create
|
||||||
- Uses same database as Tauri app via `yaak_models::init_standalone()`
|
- Uses same database as Tauri app via `yaak_models::init_standalone()`
|
||||||
@@ -32,12 +37,14 @@ crates-cli/ # CLI crate (yaak-cli)
|
|||||||
## Remaining Work
|
## Remaining Work
|
||||||
|
|
||||||
### Crates Still Depending on Tauri (in `crates/`)
|
### Crates Still Depending on Tauri (in `crates/`)
|
||||||
|
|
||||||
1. **yaak-git** (3 files) - Moderate complexity
|
1. **yaak-git** (3 files) - Moderate complexity
|
||||||
2. **yaak-plugins** (13 files) - **Hardest** - deeply integrated with Tauri for plugin-to-window communication
|
2. **yaak-plugins** (13 files) - **Hardest** - deeply integrated with Tauri for plugin-to-window communication
|
||||||
3. **yaak-sync** (4 files) - Moderate complexity
|
3. **yaak-sync** (4 files) - Moderate complexity
|
||||||
4. **yaak-ws** (5 files) - Moderate complexity
|
4. **yaak-ws** (5 files) - Moderate complexity
|
||||||
|
|
||||||
### Pattern for Decoupling
|
### Pattern for Decoupling
|
||||||
|
|
||||||
1. Remove Tauri plugin `init()` function from the crate
|
1. Remove Tauri plugin `init()` function from the crate
|
||||||
2. Move commands to `yaak-app/src/commands.rs` or keep inline in `lib.rs`
|
2. Move commands to `yaak-app/src/commands.rs` or keep inline in `lib.rs`
|
||||||
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
|
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
|
||||||
@@ -47,6 +54,7 @@ crates-cli/ # CLI crate (yaak-cli)
|
|||||||
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
|
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- `crates-tauri/yaak-app/src/lib.rs` - Main Tauri app, setup block initializes managers
|
- `crates-tauri/yaak-app/src/lib.rs` - Main Tauri app, setup block initializes managers
|
||||||
- `crates-tauri/yaak-app/src/commands.rs` - Migrated Tauri commands
|
- `crates-tauri/yaak-app/src/commands.rs` - Migrated Tauri commands
|
||||||
- `crates-tauri/yaak-app/src/models_ext.rs` - Database plugin and extension traits
|
- `crates-tauri/yaak-app/src/models_ext.rs` - Database plugin and extension traits
|
||||||
@@ -54,9 +62,11 @@ crates-cli/ # CLI crate (yaak-cli)
|
|||||||
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
|
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
|
||||||
|
|
||||||
## Git Branch
|
## Git Branch
|
||||||
|
|
||||||
Working on `detach-tauri` branch.
|
Working on `detach-tauri` branch.
|
||||||
|
|
||||||
## Recent Commits
|
## Recent Commits
|
||||||
|
|
||||||
```
|
```
|
||||||
c40cff40 Remove Tauri dependencies from yaak-crypto and yaak-grpc
|
c40cff40 Remove Tauri dependencies from yaak-crypto and yaak-grpc
|
||||||
df495f1d Move Tauri utilities from yaak-common to yaak-tauri-utils
|
df495f1d Move Tauri utilities from yaak-common to yaak-tauri-utils
|
||||||
@@ -67,6 +77,7 @@ e718a5f1 Refactor models_ext to use init_standalone from yaak-models
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Run `cargo check -p <crate>` to verify a crate builds without Tauri
|
- Run `cargo check -p <crate>` to verify a crate builds without Tauri
|
||||||
- Run `npm run app-dev` to test the Tauri app still works
|
- Run `npm run app-dev` to test the Tauri app still works
|
||||||
- Run `cargo run -p yaak-cli -- --help` to test the CLI
|
- Run `cargo run -p yaak-cli -- --help` to test the CLI
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ The skill generates markdown-formatted release notes following this structure:
|
|||||||
|
|
||||||
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
||||||
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
||||||
|
**IMPORTANT**: These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
||||||
|
|
||||||
## After Generating Release Notes
|
## After Generating Release Notes
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ Generate formatted markdown release notes for a Yaak tag.
|
|||||||
- Keep a blank line before and after the code fence.
|
- Keep a blank line before and after the code fence.
|
||||||
- Output the markdown code block last.
|
- Output the markdown code block last.
|
||||||
- Do not append `by @gschier` for PRs authored by `@gschier`.
|
- Do not append `by @gschier` for PRs authored by `@gschier`.
|
||||||
|
- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
||||||
|
|
||||||
## Release Creation Prompt
|
## Release Creation Prompt
|
||||||
|
|
||||||
|
|||||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
|
|||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
@@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen.
|
|||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
- OS: [e.g. iOS]
|
||||||
- Version [e.g. 22]
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
- Device: [e.g. iPhone6]
|
||||||
- Browser [e.g. stock browser, safari]
|
- OS: [e.g. iOS8.1]
|
||||||
- Version [e.g. 22]
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -11,6 +11,7 @@
|
|||||||
- [ ] I added or updated tests when reasonable.
|
- [ ] I added or updated tests when reasonable.
|
||||||
|
|
||||||
Approved feedback item (required if not a bug fix or small-scope improvement):
|
Approved feedback item (required if not a bug fix or small-scope improvement):
|
||||||
|
|
||||||
<!-- https://yaak.app/feedback/... -->
|
<!-- https://yaak.app/feedback/... -->
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|||||||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -14,17 +14,20 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: voidzero-dev/setup-vp@v1
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: true
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
shared-key: ci
|
shared-key: ci
|
||||||
cache-on-failure: true
|
cache-on-failure: true
|
||||||
|
|
||||||
- run: npm ci
|
- run: vp install
|
||||||
- run: npm run bootstrap
|
- run: npm run bootstrap
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- name: Run JS Tests
|
- name: Run JS Tests
|
||||||
run: npm test
|
run: vp test
|
||||||
- name: Run Rust Tests
|
- name: Run Rust Tests
|
||||||
run: cargo test --all
|
run: cargo test --all
|
||||||
|
|||||||
1
.github/workflows/claude.yml
vendored
1
.github/workflows/claude.yml
vendored
@@ -47,4 +47,3 @@ jobs:
|
|||||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||||
|
|
||||||
|
|||||||
13
.github/workflows/release-app.yml
vendored
13
.github/workflows/release-app.yml
vendored
@@ -50,8 +50,11 @@ jobs:
|
|||||||
- name: Checkout yaakapp/app
|
- name: Checkout yaakapp/app
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Vite+
|
||||||
uses: actions/setup-node@v4
|
uses: voidzero-dev/setup-vp@v1
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: true
|
||||||
|
|
||||||
- name: install Rust stable
|
- name: install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -87,15 +90,15 @@ jobs:
|
|||||||
echo $dir >> $env:GITHUB_PATH
|
echo $dir >> $env:GITHUB_PATH
|
||||||
& $exe --version
|
& $exe --version
|
||||||
|
|
||||||
- run: npm ci
|
- run: vp install
|
||||||
- run: npm run bootstrap
|
- run: npm run bootstrap
|
||||||
env:
|
env:
|
||||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- name: Run JS Tests
|
- name: Run JS Tests
|
||||||
run: npm test
|
run: vp test
|
||||||
- name: Run Rust Tests
|
- name: Run Rust Tests
|
||||||
run: cargo test --all
|
run: cargo test --all --exclude yaak-cli
|
||||||
|
|
||||||
- name: Set version
|
- name: Set version
|
||||||
run: npm run replace-version
|
run: npm run replace-version
|
||||||
|
|||||||
10
.github/workflows/sponsors.yml
vendored
10
.github/workflows/sponsors.yml
vendored
@@ -16,23 +16,23 @@ jobs:
|
|||||||
uses: JamesIves/github-sponsors-readme-action@v1
|
uses: JamesIves/github-sponsors-readme-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.SPONSORS_PAT }}
|
token: ${{ secrets.SPONSORS_PAT }}
|
||||||
file: 'README.md'
|
file: "README.md"
|
||||||
maximum: 1999
|
maximum: 1999
|
||||||
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a> '
|
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a> '
|
||||||
active-only: false
|
active-only: false
|
||||||
include-private: true
|
include-private: true
|
||||||
marker: 'sponsors-base'
|
marker: "sponsors-base"
|
||||||
|
|
||||||
- name: Generate Sponsors
|
- name: Generate Sponsors
|
||||||
uses: JamesIves/github-sponsors-readme-action@v1
|
uses: JamesIves/github-sponsors-readme-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.SPONSORS_PAT }}
|
token: ${{ secrets.SPONSORS_PAT }}
|
||||||
file: 'README.md'
|
file: "README.md"
|
||||||
minimum: 2000
|
minimum: 2000
|
||||||
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a> '
|
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a> '
|
||||||
active-only: false
|
active-only: false
|
||||||
include-private: true
|
include-private: true
|
||||||
marker: 'sponsors-premium'
|
marker: "sponsors-premium"
|
||||||
|
|
||||||
# ⚠️ Note: You can use any deployment step here to automatically push the README
|
# ⚠️ Note: You can use any deployment step here to automatically push the README
|
||||||
# changes back to your branch.
|
# changes back to your branch.
|
||||||
@@ -41,4 +41,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
branch: main
|
branch: main
|
||||||
force: false
|
force: false
|
||||||
folder: '.'
|
folder: "."
|
||||||
|
|||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
24.14.0
|
||||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# vite-plugin-wasm has not yet declared Vite 8 in its peerDependencies
|
||||||
|
legacy-peer-deps=true
|
||||||
2
.oxfmtignore
Normal file
2
.oxfmtignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
**/bindings/**
|
||||||
|
crates/yaak-templates/pkg/**
|
||||||
1
.vite-hooks/pre-commit
Normal file
1
.vite-hooks/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
vp lint
|
||||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -1,3 +1,7 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["biomejs.biome", "rust-lang.rust-analyzer", "bradlc.vscode-tailwindcss"]
|
"recommendations": [
|
||||||
|
"rust-lang.rust-analyzer",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"VoidZero.vite-plus-extension-pack"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"biome.enabled": true,
|
"editor.formatOnSaveMode": "file",
|
||||||
"biome.lint.format.enable": true
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.oxc": "explicit"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
Cargo.lock
generated
117
Cargo.lock
generated
@@ -173,6 +173,17 @@ version = "1.0.98"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "apollo-parser"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "947e21ff51879f8a40d7519dfe619268de2afba4042a8a43878276de3cb910f0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"rowan",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "append-only-vec"
|
name = "append-only-vec"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
@@ -1347,6 +1358,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "countme"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cow-utils"
|
name = "cow-utils"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -4556,7 +4573,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"flate2",
|
"flate2",
|
||||||
"postcard",
|
"postcard",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
@@ -4599,7 +4616,7 @@ dependencies = [
|
|||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
"oxc_data_structures",
|
"oxc_data_structures",
|
||||||
"oxc_estree",
|
"oxc_estree",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4655,7 +4672,7 @@ dependencies = [
|
|||||||
"oxc_index",
|
"oxc_index",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"petgraph 0.8.3",
|
"petgraph 0.8.3",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4676,7 +4693,7 @@ dependencies = [
|
|||||||
"oxc_sourcemap",
|
"oxc_sourcemap",
|
||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4688,7 +4705,7 @@ dependencies = [
|
|||||||
"cow-utils",
|
"cow-utils",
|
||||||
"oxc-browserslist",
|
"oxc-browserslist",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4763,7 +4780,7 @@ dependencies = [
|
|||||||
"oxc_ecmascript",
|
"oxc_ecmascript",
|
||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4780,7 +4797,7 @@ dependencies = [
|
|||||||
"oxc_semantic",
|
"oxc_semantic",
|
||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4805,7 +4822,7 @@ dependencies = [
|
|||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"oxc_traverse",
|
"oxc_traverse",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4827,7 +4844,7 @@ dependencies = [
|
|||||||
"oxc_regular_expression",
|
"oxc_regular_expression",
|
||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"seq-macro",
|
"seq-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4843,7 +4860,7 @@ dependencies = [
|
|||||||
"oxc_diagnostics",
|
"oxc_diagnostics",
|
||||||
"oxc_span",
|
"oxc_span",
|
||||||
"phf 0.13.1",
|
"phf 0.13.1",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"unicode-id-start",
|
"unicode-id-start",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4860,7 +4877,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"papaya",
|
"papaya",
|
||||||
"pnp",
|
"pnp",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"self_cell",
|
"self_cell",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4890,7 +4907,7 @@ dependencies = [
|
|||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"phf 0.13.1",
|
"phf 0.13.1",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"self_cell",
|
"self_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4902,7 +4919,7 @@ checksum = "c7f89482522f3cd820817d48ee4ade5b10822060d6e5e4d419f05f6d8bd29d70"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"json-escape-simd",
|
"json-escape-simd",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@@ -4966,7 +4983,7 @@ dependencies = [
|
|||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"oxc_traverse",
|
"oxc_traverse",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha1",
|
||||||
@@ -4991,7 +5008,7 @@ dependencies = [
|
|||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"oxc_transformer",
|
"oxc_transformer",
|
||||||
"oxc_traverse",
|
"oxc_traverse",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5009,7 +5026,7 @@ dependencies = [
|
|||||||
"oxc_semantic",
|
"oxc_semantic",
|
||||||
"oxc_span",
|
"oxc_span",
|
||||||
"oxc_syntax",
|
"oxc_syntax",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5414,7 +5431,7 @@ dependencies = [
|
|||||||
"nodejs-built-in-modules",
|
"nodejs-built-in-modules",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"radix_trie",
|
"radix_trie",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
@@ -5533,6 +5550,18 @@ dependencies = [
|
|||||||
"termtree",
|
"termtree",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_graphql"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea8c38ecedb3d28a998ea783469a78587f5f984d61226cf071f6979861e9e6a9"
|
||||||
|
dependencies = [
|
||||||
|
"apollo-parser",
|
||||||
|
"memchr",
|
||||||
|
"rowan",
|
||||||
|
"tiny_pretty",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -5713,7 +5742,7 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn-proto",
|
"quinn-proto",
|
||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
@@ -5733,7 +5762,7 @@ dependencies = [
|
|||||||
"lru-slab",
|
"lru-slab",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -6227,7 +6256,7 @@ dependencies = [
|
|||||||
"rolldown_tracing",
|
"rolldown_tracing",
|
||||||
"rolldown_utils",
|
"rolldown_utils",
|
||||||
"rolldown_watcher",
|
"rolldown_watcher",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"string_wizard",
|
"string_wizard",
|
||||||
@@ -6317,7 +6346,7 @@ dependencies = [
|
|||||||
"rolldown_sourcemap",
|
"rolldown_sourcemap",
|
||||||
"rolldown_std_utils",
|
"rolldown_std_utils",
|
||||||
"rolldown_utils",
|
"rolldown_utils",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"simdutf8",
|
"simdutf8",
|
||||||
@@ -6335,7 +6364,7 @@ dependencies = [
|
|||||||
"blake3",
|
"blake3",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"rolldown_debug_action",
|
"rolldown_debug_action",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -6392,7 +6421,7 @@ dependencies = [
|
|||||||
"rolldown-ariadne",
|
"rolldown-ariadne",
|
||||||
"rolldown_utils",
|
"rolldown_utils",
|
||||||
"ropey",
|
"ropey",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"sugar_path",
|
"sugar_path",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6426,7 +6455,7 @@ dependencies = [
|
|||||||
"rolldown_resolver",
|
"rolldown_resolver",
|
||||||
"rolldown_sourcemap",
|
"rolldown_sourcemap",
|
||||||
"rolldown_utils",
|
"rolldown_utils",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"string_wizard",
|
"string_wizard",
|
||||||
@@ -6446,7 +6475,7 @@ dependencies = [
|
|||||||
"rolldown_common",
|
"rolldown_common",
|
||||||
"rolldown_plugin",
|
"rolldown_plugin",
|
||||||
"rolldown_utils",
|
"rolldown_utils",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"xxhash-rust",
|
"xxhash-rust",
|
||||||
]
|
]
|
||||||
@@ -6517,7 +6546,7 @@ dependencies = [
|
|||||||
"oxc",
|
"oxc",
|
||||||
"oxc_sourcemap",
|
"oxc_sourcemap",
|
||||||
"rolldown_utils",
|
"rolldown_utils",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6569,7 +6598,7 @@ dependencies = [
|
|||||||
"regex 1.11.1",
|
"regex 1.11.1",
|
||||||
"regress",
|
"regress",
|
||||||
"rolldown_std_utils",
|
"rolldown_std_utils",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"simdutf8",
|
"simdutf8",
|
||||||
"sugar_path",
|
"sugar_path",
|
||||||
@@ -6599,6 +6628,18 @@ dependencies = [
|
|||||||
"str_indices",
|
"str_indices",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rowan"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
||||||
|
dependencies = [
|
||||||
|
"countme",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"rustc-hash 1.1.0",
|
||||||
|
"text-size",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.32.1"
|
version = "0.32.1"
|
||||||
@@ -6641,6 +6682,12 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -7453,7 +7500,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"oxc_index",
|
"oxc_index",
|
||||||
"oxc_sourcemap",
|
"oxc_sourcemap",
|
||||||
"rustc-hash",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8154,6 +8201,12 @@ version = "0.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "text-size"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
@@ -8276,6 +8329,12 @@ dependencies = [
|
|||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny_pretty"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "650d82e943da333637be9f1567d33d605e76810a26464edfd7ae74f7ef181e95"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -10198,6 +10257,7 @@ dependencies = [
|
|||||||
"md5 0.8.0",
|
"md5 0.8.0",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
|
"pretty_graphql",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"r2d2_sqlite",
|
"r2d2_sqlite",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
@@ -10399,6 +10459,7 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
"yaak-common",
|
"yaak-common",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
|
"yaak-templates",
|
||||||
"yaak-tls",
|
"yaak-tls",
|
||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|||||||
48
Cargo.toml
48
Cargo.toml
@@ -1,30 +1,30 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/yaak",
|
"crates/yaak",
|
||||||
# Shared crates (no Tauri dependency)
|
# Shared crates (no Tauri dependency)
|
||||||
"crates/yaak-core",
|
"crates/yaak-core",
|
||||||
"crates/yaak-common",
|
"crates/yaak-common",
|
||||||
"crates/yaak-crypto",
|
"crates/yaak-crypto",
|
||||||
"crates/yaak-git",
|
"crates/yaak-git",
|
||||||
"crates/yaak-grpc",
|
"crates/yaak-grpc",
|
||||||
"crates/yaak-http",
|
"crates/yaak-http",
|
||||||
"crates/yaak-models",
|
"crates/yaak-models",
|
||||||
"crates/yaak-plugins",
|
"crates/yaak-plugins",
|
||||||
"crates/yaak-sse",
|
"crates/yaak-sse",
|
||||||
"crates/yaak-sync",
|
"crates/yaak-sync",
|
||||||
"crates/yaak-templates",
|
"crates/yaak-templates",
|
||||||
"crates/yaak-tls",
|
"crates/yaak-tls",
|
||||||
"crates/yaak-ws",
|
"crates/yaak-ws",
|
||||||
"crates/yaak-api",
|
"crates/yaak-api",
|
||||||
# CLI crates
|
# CLI crates
|
||||||
"crates-cli/yaak-cli",
|
"crates-cli/yaak-cli",
|
||||||
# Tauri-specific crates
|
# Tauri-specific crates
|
||||||
"crates-tauri/yaak-app",
|
"crates-tauri/yaak-app",
|
||||||
"crates-tauri/yaak-fonts",
|
"crates-tauri/yaak-fonts",
|
||||||
"crates-tauri/yaak-license",
|
"crates-tauri/yaak-license",
|
||||||
"crates-tauri/yaak-mac-window",
|
"crates-tauri/yaak-mac-window",
|
||||||
"crates-tauri/yaak-tauri-utils",
|
"crates-tauri/yaak-tauri-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ begin.
|
|||||||
|
|
||||||
Make sure you have the following tools installed:
|
Make sure you have the following tools installed:
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/download/package-manager)
|
- [Node.js](https://nodejs.org/en/download/package-manager) (v24+)
|
||||||
- [Rust](https://www.rust-lang.org/tools/install)
|
- [Rust](https://www.rust-lang.org/tools/install)
|
||||||
|
- [Vite+](https://vite.dev/guide/vite-plus) (`vp` CLI)
|
||||||
|
|
||||||
Check the installations with the following commands:
|
Check the installations with the following commands:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
node -v
|
node -v
|
||||||
npm -v
|
npm -v
|
||||||
|
vp --version
|
||||||
rustc --version
|
rustc --version
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,9 +63,9 @@ _Note: For safety, development builds use a separate database location from prod
|
|||||||
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
|
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linting & Formatting
|
## Linting and Formatting
|
||||||
|
|
||||||
This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
|
This repo uses [Vite+](https://vite.dev/guide/vite-plus) for linting (oxlint) and formatting (oxfmt).
|
||||||
|
|
||||||
- Lint the entire repo:
|
- Lint the entire repo:
|
||||||
|
|
||||||
@@ -71,12 +73,6 @@ This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
|
|||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
- Auto-fix lint issues where possible:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run lint:fix
|
|
||||||
```
|
|
||||||
|
|
||||||
- Format code:
|
- Format code:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -84,5 +80,7 @@ npm run format
|
|||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Many workspace packages also expose the same scripts (`lint`, `lint:fix`, and `format`).
|
|
||||||
- TypeScript type-checking still runs separately via `tsc --noEmit` in relevant packages.
|
- A pre-commit hook runs `vp lint` automatically on commit.
|
||||||
|
- Some workspace packages also run `tsc --noEmit` for type-checking.
|
||||||
|
- VS Code users should install the recommended extensions for format-on-save support.
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||||
</p>
|
</p>
|
||||||
@@ -27,13 +25,11 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
|
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
|
||||||
Built with [Tauri](https://tauri.app), Rust, and React, it’s fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
|
Built with [Tauri](https://tauri.app), Rust, and React, it’s fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
|
||||||
|
|
||||||
|
|
||||||
### 🌐 Work with any API
|
### 🌐 Work with any API
|
||||||
|
|
||||||
- Import collections from Postman, Insomnia, OpenAPI, Swagger, or Curl.
|
- Import collections from Postman, Insomnia, OpenAPI, Swagger, or Curl.
|
||||||
@@ -41,21 +37,23 @@ Built with [Tauri](https://tauri.app), Rust, and React, it’s fast, lightweight
|
|||||||
- Filter and inspect responses with JSONPath or XPath.
|
- Filter and inspect responses with JSONPath or XPath.
|
||||||
|
|
||||||
### 🔐 Stay secure
|
### 🔐 Stay secure
|
||||||
|
|
||||||
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
|
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
|
||||||
- Secure sensitive values with encrypted secrets.
|
- Secure sensitive values with encrypted secrets.
|
||||||
- Store secrets in your OS keychain.
|
- Store secrets in your OS keychain.
|
||||||
|
|
||||||
### ☁️ Organize & collaborate
|
### ☁️ Organize & collaborate
|
||||||
|
|
||||||
- Group requests into workspaces and nested folders.
|
- Group requests into workspaces and nested folders.
|
||||||
- Use environment variables to switch between dev, staging, and prod.
|
- Use environment variables to switch between dev, staging, and prod.
|
||||||
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
|
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
|
||||||
|
|
||||||
### 🧩 Extend & customize
|
### 🧩 Extend & customize
|
||||||
|
|
||||||
- Insert dynamic values like UUIDs or timestamps with template tags.
|
- Insert dynamic values like UUIDs or timestamps with template tags.
|
||||||
- Pick from built-in themes or build your own.
|
- Pick from built-in themes or build your own.
|
||||||
- Create plugins to extend authentication, template tags, or the UI.
|
- Create plugins to extend authentication, template tags, or the UI.
|
||||||
|
|
||||||
|
|
||||||
## Contribution Policy
|
## Contribution Policy
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|||||||
54
biome.json
54
biome.json
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"a11y": {
|
|
||||||
"useKeyWithClickEvents": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 100,
|
|
||||||
"bracketSpacing": true
|
|
||||||
},
|
|
||||||
"css": {
|
|
||||||
"parser": {
|
|
||||||
"tailwindDirectives": true
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "single",
|
|
||||||
"jsxQuoteStyle": "double",
|
|
||||||
"trailingCommas": "all",
|
|
||||||
"semicolons": "always"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"includes": [
|
|
||||||
"**",
|
|
||||||
"!**/node_modules",
|
|
||||||
"!**/dist",
|
|
||||||
"!**/build",
|
|
||||||
"!target",
|
|
||||||
"!scripts",
|
|
||||||
"!crates",
|
|
||||||
"!crates-tauri",
|
|
||||||
"!src-web/tailwind.config.cjs",
|
|
||||||
"!src-web/postcss.config.cjs",
|
|
||||||
"!src-web/vite.config.ts",
|
|
||||||
"!src-web/routeTree.gen.ts",
|
|
||||||
"!packages/plugin-runtime-types/lib",
|
|
||||||
"!**/bindings",
|
|
||||||
"!flatpak"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,14 @@ schemars = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-util", "net", "signal", "time"] }
|
tokio = { workspace = true, features = [
|
||||||
|
"rt-multi-thread",
|
||||||
|
"macros",
|
||||||
|
"io-util",
|
||||||
|
"net",
|
||||||
|
"signal",
|
||||||
|
"time",
|
||||||
|
] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
webbrowser = "1"
|
webbrowser = "1"
|
||||||
zip = "4"
|
zip = "4"
|
||||||
|
|||||||
@@ -47,8 +47,7 @@ impl CliContext {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let encryption_manager =
|
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||||
Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
data_dir,
|
data_dir,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ mod version_check;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands, PluginCommands, RequestCommands};
|
use cli::{Cli, Commands, PluginCommands, RequestCommands};
|
||||||
use context::{CliContext, CliExecutionContext};
|
use context::{CliContext, CliExecutionContext};
|
||||||
|
use std::path::PathBuf;
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
use yaak_models::queries::any_request::AnyRequest;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -30,9 +31,7 @@ async fn main() {
|
|||||||
|
|
||||||
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
||||||
|
|
||||||
let data_dir = data_dir.unwrap_or_else(|| {
|
let data_dir = data_dir.unwrap_or_else(|| resolve_data_dir(app_id));
|
||||||
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
|
||||||
});
|
|
||||||
|
|
||||||
version_check::maybe_check_for_updates().await;
|
version_check::maybe_check_for_updates().await;
|
||||||
|
|
||||||
@@ -162,8 +161,7 @@ fn resolve_send_execution_context(
|
|||||||
AnyRequest::GrpcRequest(r) => (Some(r.id), r.workspace_id),
|
AnyRequest::GrpcRequest(r) => (Some(r.id), r.workspace_id),
|
||||||
AnyRequest::WebsocketRequest(r) => (Some(r.id), r.workspace_id),
|
AnyRequest::WebsocketRequest(r) => (Some(r.id), r.workspace_id),
|
||||||
};
|
};
|
||||||
let cookie_jar_id =
|
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
||||||
resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
|
||||||
return Ok(CliExecutionContext {
|
return Ok(CliExecutionContext {
|
||||||
request_id,
|
request_id,
|
||||||
workspace_id: Some(workspace_id),
|
workspace_id: Some(workspace_id),
|
||||||
@@ -184,8 +182,7 @@ fn resolve_send_execution_context(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(workspace) = context.db().get_workspace(id) {
|
if let Ok(workspace) = context.db().get_workspace(id) {
|
||||||
let cookie_jar_id =
|
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace.id, explicit_cookie_jar_id)?;
|
||||||
resolve_cookie_jar_id(context, &workspace.id, explicit_cookie_jar_id)?;
|
|
||||||
return Ok(CliExecutionContext {
|
return Ok(CliExecutionContext {
|
||||||
request_id: None,
|
request_id: None,
|
||||||
workspace_id: Some(workspace.id),
|
workspace_id: Some(workspace.id),
|
||||||
@@ -213,8 +210,7 @@ fn resolve_request_execution_context(
|
|||||||
AnyRequest::GrpcRequest(r) => r.workspace_id,
|
AnyRequest::GrpcRequest(r) => r.workspace_id,
|
||||||
AnyRequest::WebsocketRequest(r) => r.workspace_id,
|
AnyRequest::WebsocketRequest(r) => r.workspace_id,
|
||||||
};
|
};
|
||||||
let cookie_jar_id =
|
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
||||||
resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
|
||||||
|
|
||||||
Ok(CliExecutionContext {
|
Ok(CliExecutionContext {
|
||||||
request_id: Some(request_id.to_string()),
|
request_id: Some(request_id.to_string()),
|
||||||
@@ -242,3 +238,46 @@ fn resolve_cookie_jar_id(
|
|||||||
.map(|jar| jar.id);
|
.map(|jar| jar.id);
|
||||||
Ok(default_cookie_jar)
|
Ok(default_cookie_jar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_data_dir(app_id: &str) -> PathBuf {
|
||||||
|
if let Some(dir) = wsl_data_dir(app_id) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect WSL and resolve the Windows AppData\Roaming path for the Yaak data directory.
|
||||||
|
fn wsl_data_dir(app_id: &str) -> Option<PathBuf> {
|
||||||
|
if !cfg!(target_os = "linux") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let proc_version = std::fs::read_to_string("/proc/version").ok()?;
|
||||||
|
let is_wsl = proc_version.to_lowercase().contains("microsoft");
|
||||||
|
if !is_wsl {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're in WSL, so try to resolve the Yaak app's data directory in Windows
|
||||||
|
|
||||||
|
// Get the Windows %APPDATA% path via cmd.exe
|
||||||
|
let appdata_output =
|
||||||
|
std::process::Command::new("cmd.exe").args(["/C", "echo", "%APPDATA%"]).output().ok()?;
|
||||||
|
|
||||||
|
let win_path = String::from_utf8(appdata_output.stdout).ok()?.trim().to_string();
|
||||||
|
if win_path.is_empty() || win_path == "%APPDATA%" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Windows path to WSL path using wslpath (handles custom mount points)
|
||||||
|
let wslpath_output = std::process::Command::new("wslpath").arg(&win_path).output().ok()?;
|
||||||
|
|
||||||
|
let wsl_appdata = String::from_utf8(wslpath_output.stdout).ok()?.trim().to_string();
|
||||||
|
if wsl_appdata.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wsl_path = PathBuf::from(wsl_appdata).join(app_id);
|
||||||
|
|
||||||
|
if wsl_path.exists() { Some(wsl_path) } else { None }
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use arboard::Clipboard;
|
|||||||
use console::Term;
|
use console::Term;
|
||||||
use inquire::{Confirm, Editor, Password, PasswordDisplayMode, Select, Text};
|
use inquire::{Confirm, Editor, Password, PasswordDisplayMode, Select, Text};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::HashMap;
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -11,11 +11,11 @@ use tokio::task::JoinHandle;
|
|||||||
use yaak::plugin_events::{
|
use yaak::plugin_events::{
|
||||||
GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event,
|
GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event,
|
||||||
};
|
};
|
||||||
use yaak::render::render_http_request;
|
use yaak::render::{render_grpc_request, render_http_request};
|
||||||
use yaak::send::{SendHttpRequestWithPluginsParams, send_http_request_with_plugins};
|
use yaak::send::{SendHttpRequestWithPluginsParams, send_http_request_with_plugins};
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
use yaak_models::blob_manager::BlobManager;
|
use yaak_models::blob_manager::BlobManager;
|
||||||
use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader};
|
use yaak_models::models::Environment;
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
use yaak_models::queries::any_request::AnyRequest;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
@@ -29,7 +29,7 @@ use yaak_plugins::events::{
|
|||||||
};
|
};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
use yaak_templates::{RenderOptions, TemplateCallback, render_json_value_raw};
|
||||||
|
|
||||||
pub struct CliPluginEventBridge {
|
pub struct CliPluginEventBridge {
|
||||||
rx_id: String,
|
rx_id: String,
|
||||||
@@ -269,7 +269,7 @@ async fn build_plugin_reply(
|
|||||||
);
|
);
|
||||||
let render_options = RenderOptions::throw();
|
let render_options = RenderOptions::throw();
|
||||||
|
|
||||||
match render_grpc_request_for_cli(
|
match render_grpc_request(
|
||||||
&grpc_request,
|
&grpc_request,
|
||||||
environment_chain,
|
environment_chain,
|
||||||
&template_callback,
|
&template_callback,
|
||||||
@@ -532,60 +532,6 @@ async fn render_json_value_for_cli<T: TemplateCallback>(
|
|||||||
render_json_value_raw(value, vars, cb, opt).await
|
render_json_value_raw(value, vars, cb, opt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_grpc_request_for_cli<T: TemplateCallback>(
|
|
||||||
grpc_request: &GrpcRequest,
|
|
||||||
environment_chain: Vec<Environment>,
|
|
||||||
cb: &T,
|
|
||||||
opt: &RenderOptions,
|
|
||||||
) -> yaak_templates::error::Result<GrpcRequest> {
|
|
||||||
let vars = &make_vars_hashmap(environment_chain);
|
|
||||||
|
|
||||||
let mut metadata = Vec::new();
|
|
||||||
for p in grpc_request.metadata.clone() {
|
|
||||||
if !p.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
metadata.push(HttpRequestHeader {
|
|
||||||
enabled: p.enabled,
|
|
||||||
name: parse_and_render(p.name.as_str(), vars, cb, opt).await?,
|
|
||||||
value: parse_and_render(p.value.as_str(), vars, cb, opt).await?,
|
|
||||||
id: p.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let authentication = {
|
|
||||||
let mut disabled = false;
|
|
||||||
let mut auth = BTreeMap::new();
|
|
||||||
match grpc_request.authentication.get("disabled") {
|
|
||||||
Some(Value::Bool(true)) => {
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
Some(Value::String(tmpl)) => {
|
|
||||||
disabled = parse_and_render(tmpl.as_str(), vars, cb, opt)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.is_empty();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if disabled {
|
|
||||||
auth.insert("disabled".to_string(), Value::Bool(true));
|
|
||||||
} else {
|
|
||||||
for (k, v) in grpc_request.authentication.clone() {
|
|
||||||
if k == "disabled" {
|
|
||||||
auth.insert(k, Value::Bool(false));
|
|
||||||
} else {
|
|
||||||
auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auth
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = parse_and_render(grpc_request.url.as_str(), vars, cb, opt).await?;
|
|
||||||
|
|
||||||
Ok(GrpcRequest { url, metadata, authentication, ..grpc_request.to_owned() })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
|
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
|
||||||
let first_part = raw_cookie.split(';').next()?.trim();
|
let first_part = raw_cookie.split(';').next()?.trim();
|
||||||
|
|||||||
@@ -30,11 +30,21 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client
|
|||||||
http = { version = "1.2.0", default-features = false }
|
http = { version = "1.2.0", default-features = false }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
md5 = "0.8.0"
|
md5 = "0.8.0"
|
||||||
|
pretty_graphql = "0.2"
|
||||||
r2d2 = "0.8.10"
|
r2d2 = "0.8.10"
|
||||||
r2d2_sqlite = "0.25.0"
|
r2d2_sqlite = "0.25.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
reqwest = { workspace = true, features = ["multipart", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks", "http2"] }
|
reqwest = { workspace = true, features = [
|
||||||
|
"multipart",
|
||||||
|
"gzip",
|
||||||
|
"brotli",
|
||||||
|
"deflate",
|
||||||
|
"json",
|
||||||
|
"rustls-tls-manual-roots-no-provider",
|
||||||
|
"socks",
|
||||||
|
"http2",
|
||||||
|
] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true, features = ["raw_value"] }
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
{
|
{
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default capabilities for all build variants",
|
"description": "Default capabilities for all build variants",
|
||||||
"windows": [
|
"windows": ["*"],
|
||||||
"*"
|
|
||||||
],
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:app:allow-identifier",
|
"core:app:allow-identifier",
|
||||||
"core:event:allow-emit",
|
"core:event:allow-emit",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/tauri",
|
"name": "@yaakapp-internal/tauri",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "bindings/index.ts"
|
"main": "bindings/index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ use tokio::time;
|
|||||||
use yaak_common::command::new_checked_command;
|
use yaak_common::command::new_checked_command;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
||||||
|
use yaak_templates::strip_json_comments::strip_json_comments;
|
||||||
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
|
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
|
||||||
use yaak_mac_window::AppHandleMacWindowExt;
|
use yaak_mac_window::AppHandleMacWindowExt;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
@@ -433,6 +434,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
result.expect("Failed to render template")
|
result.expect("Failed to render template")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
let msg = strip_json_comments(&msg);
|
||||||
in_msg_tx.try_send(msg.clone()).unwrap();
|
in_msg_tx.try_send(msg.clone()).unwrap();
|
||||||
}
|
}
|
||||||
Ok(IncomingMsg::Commit) => {
|
Ok(IncomingMsg::Commit) => {
|
||||||
@@ -468,6 +470,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
let msg = strip_json_comments(&msg);
|
||||||
|
|
||||||
app_handle.db().upsert_grpc_event(
|
app_handle.db().upsert_grpc_event(
|
||||||
&GrpcEvent {
|
&GrpcEvent {
|
||||||
@@ -869,6 +872,14 @@ async fn cmd_format_json(text: &str) -> YaakResult<String> {
|
|||||||
Ok(format_json(text, " "))
|
Ok(format_json(text, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_format_graphql(text: &str) -> YaakResult<String> {
|
||||||
|
match pretty_graphql::format_text(text, &Default::default()) {
|
||||||
|
Ok(formatted) => Ok(formatted),
|
||||||
|
Err(_) => Ok(text.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_http_response_body<R: Runtime>(
|
async fn cmd_http_response_body<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
@@ -1372,13 +1383,12 @@ async fn cmd_reload_plugins<R: Runtime>(
|
|||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
plugin_manager: State<'_, PluginManager>,
|
plugin_manager: State<'_, PluginManager>,
|
||||||
) -> YaakResult<()> {
|
) -> YaakResult<Vec<(String, String)>> {
|
||||||
let plugins = app_handle.db().list_plugins()?;
|
let plugins = app_handle.db().list_plugins()?;
|
||||||
let plugin_context =
|
let plugin_context =
|
||||||
PluginContext::new(Some(window.label().to_string()), window.workspace_id());
|
PluginContext::new(Some(window.label().to_string()), window.workspace_id());
|
||||||
let _errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
|
let errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
|
||||||
// Note: errors are returned but we don't show toasts here since this is a manual reload
|
Ok(errors)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1638,6 +1648,7 @@ pub fn run() {
|
|||||||
cmd_http_request_body,
|
cmd_http_request_body,
|
||||||
cmd_http_response_body,
|
cmd_http_response_body,
|
||||||
cmd_format_json,
|
cmd_format_json,
|
||||||
|
cmd_format_graphql,
|
||||||
cmd_get_http_authentication_summaries,
|
cmd_get_http_authentication_summaries,
|
||||||
cmd_get_http_authentication_config,
|
cmd_get_http_authentication_config,
|
||||||
cmd_get_sse_events,
|
cmd_get_sse_events,
|
||||||
@@ -1719,6 +1730,7 @@ pub fn run() {
|
|||||||
git_ext::cmd_git_rm_remote,
|
git_ext::cmd_git_rm_remote,
|
||||||
//
|
//
|
||||||
// Plugin commands
|
// Plugin commands
|
||||||
|
plugins_ext::cmd_plugin_init_errors,
|
||||||
plugins_ext::cmd_plugins_install_from_directory,
|
plugins_ext::cmd_plugins_install_from_directory,
|
||||||
plugins_ext::cmd_plugins_search,
|
plugins_ext::cmd_plugins_search,
|
||||||
plugins_ext::cmd_plugins_install,
|
plugins_ext::cmd_plugins_install,
|
||||||
|
|||||||
@@ -198,6 +198,13 @@ pub async fn cmd_plugins_uninstall<R: Runtime>(
|
|||||||
Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?)
|
Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_plugin_init_errors(
|
||||||
|
plugin_manager: State<'_, PluginManager>,
|
||||||
|
) -> Result<Vec<(String, String)>> {
|
||||||
|
Ok(plugin_manager.take_init_errors().await)
|
||||||
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_plugins_updates<R: Runtime>(
|
pub async fn cmd_plugins_updates<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
@@ -306,7 +313,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
dev_mode,
|
dev_mode,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to initialize plugins");
|
.expect("Failed to start plugin runtime");
|
||||||
|
|
||||||
app_handle_clone.manage(manager);
|
app_handle_clone.manage(manager);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use log::info;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
pub use yaak::render::{render_grpc_request, render_http_request};
|
||||||
pub use yaak::render::render_http_request;
|
use yaak_models::models::Environment;
|
||||||
use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader};
|
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
@@ -25,61 +23,3 @@ pub async fn render_json_value<T: TemplateCallback>(
|
|||||||
let vars = &make_vars_hashmap(environment_chain);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
render_json_value_raw(value, vars, cb, opt).await
|
render_json_value_raw(value, vars, cb, opt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_grpc_request<T: TemplateCallback>(
|
|
||||||
r: &GrpcRequest,
|
|
||||||
environment_chain: Vec<Environment>,
|
|
||||||
cb: &T,
|
|
||||||
opt: &RenderOptions,
|
|
||||||
) -> yaak_templates::error::Result<GrpcRequest> {
|
|
||||||
let vars = &make_vars_hashmap(environment_chain);
|
|
||||||
|
|
||||||
let mut metadata = Vec::new();
|
|
||||||
for p in r.metadata.clone() {
|
|
||||||
if !p.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
metadata.push(HttpRequestHeader {
|
|
||||||
enabled: p.enabled,
|
|
||||||
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,
|
|
||||||
value: parse_and_render(p.value.as_str(), vars, cb, &opt).await?,
|
|
||||||
id: p.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let authentication = {
|
|
||||||
let mut disabled = false;
|
|
||||||
let mut auth = BTreeMap::new();
|
|
||||||
match r.authentication.get("disabled") {
|
|
||||||
Some(Value::Bool(true)) => {
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
Some(Value::String(tmpl)) => {
|
|
||||||
disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.is_empty();
|
|
||||||
info!(
|
|
||||||
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if disabled {
|
|
||||||
auth.insert("disabled".to_string(), Value::Bool(true));
|
|
||||||
} else {
|
|
||||||
for (k, v) in r.authentication.clone() {
|
|
||||||
if k == "disabled" {
|
|
||||||
auth.insert(k, Value::Bool(false));
|
|
||||||
} else {
|
|
||||||
auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auth
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = parse_and_render(r.url.as_str(), vars, cb, &opt).await?;
|
|
||||||
|
|
||||||
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ use yaak_models::util::UpdateSource;
|
|||||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose};
|
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
|
use yaak_templates::strip_json_comments::maybe_strip_json_comments;
|
||||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||||
use yaak_tls::find_client_certificate;
|
use yaak_tls::find_client_certificate;
|
||||||
use yaak_ws::{WebsocketManager, render_websocket_request};
|
use yaak_ws::{WebsocketManager, render_websocket_request};
|
||||||
@@ -72,8 +73,10 @@ pub async fn cmd_ws_send<R: Runtime>(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let message = maybe_strip_json_comments(&request.message);
|
||||||
|
|
||||||
let mut ws_manager = ws_manager.lock().await;
|
let mut ws_manager = ws_manager.lock().await;
|
||||||
ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?;
|
ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?;
|
||||||
|
|
||||||
app_handle.db().upsert_websocket_event(
|
app_handle.db().upsert_websocket_event(
|
||||||
&WebsocketEvent {
|
&WebsocketEvent {
|
||||||
@@ -82,7 +85,7 @@ pub async fn cmd_ws_send<R: Runtime>(
|
|||||||
workspace_id: connection.workspace_id.clone(),
|
workspace_id: connection.workspace_id.clone(),
|
||||||
is_server: false,
|
is_server: false,
|
||||||
message_type: WebsocketEventType::Text,
|
message_type: WebsocketEventType::Text,
|
||||||
message: request.message.into(),
|
message: message.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::from_window_label(window.label()),
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
|||||||
@@ -14,10 +14,7 @@
|
|||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"scope": {
|
"scope": {
|
||||||
"allow": [
|
"allow": ["$APPDATA/responses/*", "$RESOURCE/static/*"]
|
||||||
"$APPDATA/responses/*",
|
|
||||||
"$RESOURCE/static/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,9 +22,7 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"deep-link": {
|
"deep-link": {
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"schemes": [
|
"schemes": ["yaak"]
|
||||||
"yaak"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,9 +16,7 @@
|
|||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"endpoints": [
|
"endpoints": ["https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"],
|
||||||
"https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"
|
|
||||||
],
|
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVGRkFGMjQxRUNEOTQ3MzAKUldRd1I5bnNRZkw2NzRtMnRlWTN3R24xYUR3aGRsUjJzWGwvdHdEcGljb3ZJMUNlMjFsaHlqVU4K"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVGRkFGMjQxRUNEOTQ3MzAKUldRd1I5bnNRZkw2NzRtMnRlWTN3R24xYUR3aGRsUjJzWGwvdHdEcGljb3ZJMUNlMjFsaHlqVU4K"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { Fonts } from './bindings/gen_fonts';
|
import { Fonts } from "./bindings/gen_fonts";
|
||||||
|
|
||||||
export async function listFonts() {
|
export async function listFonts() {
|
||||||
return invoke<Fonts>('plugin:yaak-fonts|list', {});
|
return invoke<Fonts>("plugin:yaak-fonts|list", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFonts() {
|
export function useFonts() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['list_fonts'],
|
queryKey: ["list_fonts"],
|
||||||
queryFn: () => listFonts(),
|
queryFn: () => listFonts(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/fonts",
|
"name": "@yaakapp-internal/fonts",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { appInfo } from '@yaakapp/app/lib/appInfo';
|
import { appInfo } from "@yaakapp/app/lib/appInfo";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import { LicenseCheckStatus } from './bindings/license';
|
import { LicenseCheckStatus } from "./bindings/license";
|
||||||
|
|
||||||
export * from './bindings/license';
|
export * from "./bindings/license";
|
||||||
|
|
||||||
const CHECK_QUERY_KEY = ['license.check'];
|
const CHECK_QUERY_KEY = ["license.check"];
|
||||||
|
|
||||||
export function useLicense() {
|
export function useLicense() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const activate = useMutation<void, string, { licenseKey: string }>({
|
const activate = useMutation<void, string, { licenseKey: string }>({
|
||||||
mutationKey: ['license.activate'],
|
mutationKey: ["license.activate"],
|
||||||
mutationFn: (payload) => invoke('plugin:yaak-license|activate', payload),
|
mutationFn: (payload) => invoke("plugin:yaak-license|activate", payload),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deactivate = useMutation<void, string, void>({
|
const deactivate = useMutation<void, string, void>({
|
||||||
mutationKey: ['license.deactivate'],
|
mutationKey: ["license.deactivate"],
|
||||||
mutationFn: () => invoke('plugin:yaak-license|deactivate'),
|
mutationFn: () => invoke("plugin:yaak-license|deactivate"),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check the license again after a license is activated
|
// Check the license again after a license is activated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen('license-activated', async () => {
|
const unlisten = listen("license-activated", async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY });
|
await queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY });
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
unlisten.then((fn) => fn());
|
void unlisten.then((fn) => fn());
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export function useLicense() {
|
|||||||
if (!appInfo.featureLicense) {
|
if (!appInfo.featureLicense) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return invoke<LicenseCheckStatus>('plugin:yaak-license|check');
|
return invoke<LicenseCheckStatus>("plugin:yaak-license|check");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/license",
|
"name": "@yaakapp-internal/license",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
export function setWindowTitle(title: string) {
|
export function setWindowTitle(title: string) {
|
||||||
invoke('plugin:yaak-mac-window|set_title', { title }).catch(console.error);
|
invoke("plugin:yaak-mac-window|set_title", { title }).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setWindowTheme(bgColor: string) {
|
export function setWindowTheme(bgColor: string) {
|
||||||
invoke('plugin:yaak-mac-window|set_theme', { bgColor }).catch(console.error);
|
invoke("plugin:yaak-mac-window|set_theme", { bgColor }).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/mac-window",
|
"name": "@yaakapp-internal/mac-window",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
[default]
|
[default]
|
||||||
description = "Default permissions for the plugin"
|
description = "Default permissions for the plugin"
|
||||||
permissions = [
|
permissions = ["allow-set-title", "allow-set-theme"]
|
||||||
"allow-set-title",
|
|
||||||
"allow-set-theme",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ unsafe impl Sync for UnsafeWindowHandle {}
|
|||||||
|
|
||||||
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
|
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
|
||||||
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
|
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
|
||||||
|
/// Extra pixels to add to the title bar height when the default title bar is
|
||||||
|
/// already as tall as button_height + PAD_Y (i.e. macOS Tahoe 26+, where the
|
||||||
|
/// default is 32px and 14 + 18 = 32). On pre-Tahoe this is unused because the
|
||||||
|
/// default title bar is shorter than button_height + PAD_Y.
|
||||||
|
const TITLEBAR_EXTRA_HEIGHT: f64 = 4.0;
|
||||||
const MAIN_WINDOW_PREFIX: &str = "main_";
|
const MAIN_WINDOW_PREFIX: &str = "main_";
|
||||||
|
|
||||||
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
|
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
|
||||||
@@ -95,12 +100,29 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64,
|
|||||||
ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
||||||
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
||||||
|
|
||||||
let title_bar_container_view = close.superview().superview();
|
|
||||||
|
|
||||||
let close_rect: NSRect = msg_send![close, frame];
|
let close_rect: NSRect = msg_send![close, frame];
|
||||||
let button_height = close_rect.size.height;
|
let button_height = close_rect.size.height;
|
||||||
|
|
||||||
let title_bar_frame_height = button_height + y;
|
let title_bar_container_view = close.superview().superview();
|
||||||
|
|
||||||
|
// Capture the OS default title bar height on the first call, before
|
||||||
|
// we've modified it. This avoids the height growing on repeated calls.
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
static DEFAULT_TITLEBAR_HEIGHT: OnceLock<f64> = OnceLock::new();
|
||||||
|
let default_height =
|
||||||
|
*DEFAULT_TITLEBAR_HEIGHT.get_or_init(|| NSView::frame(title_bar_container_view).size.height);
|
||||||
|
|
||||||
|
// On pre-Tahoe, button_height + y is larger than the default title bar
|
||||||
|
// height, so the resize works as before. On Tahoe (26+), the default is
|
||||||
|
// already 32px and button_height + y = 32, so nothing changes. In that
|
||||||
|
// case, add TITLEBAR_EXTRA_HEIGHT extra pixels to push the buttons down.
|
||||||
|
let desired = button_height + y;
|
||||||
|
let title_bar_frame_height = if desired > default_height {
|
||||||
|
desired
|
||||||
|
} else {
|
||||||
|
default_height + TITLEBAR_EXTRA_HEIGHT
|
||||||
|
};
|
||||||
|
|
||||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||||
title_bar_rect.size.height = title_bar_frame_height;
|
title_bar_rect.size.height = title_bar_frame_height;
|
||||||
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;
|
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;
|
||||||
|
|||||||
@@ -21,3 +21,10 @@ pub fn get_str_map<'a>(v: &'a BTreeMap<String, Value>, key: &str) -> &'a str {
|
|||||||
Some(v) => v.as_str().unwrap_or_default(),
|
Some(v) => v.as_str().unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_bool_map(v: &BTreeMap<String, Value>, key: &str, fallback: bool) -> bool {
|
||||||
|
match v.get(key) {
|
||||||
|
None => fallback,
|
||||||
|
Some(v) => v.as_bool().unwrap_or(fallback),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
export function enableEncryption(workspaceId: string) {
|
export function enableEncryption(workspaceId: string) {
|
||||||
return invoke<void>('cmd_enable_encryption', { workspaceId });
|
return invoke<void>("cmd_enable_encryption", { workspaceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function revealWorkspaceKey(workspaceId: string) {
|
export function revealWorkspaceKey(workspaceId: string) {
|
||||||
return invoke<string>('cmd_reveal_workspace_key', { workspaceId });
|
return invoke<string>("cmd_reveal_workspace_key", { workspaceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
|
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
|
||||||
return invoke<void>('cmd_set_workspace_key', args);
|
return invoke<void>("cmd_set_workspace_key", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableEncryption(workspaceId: string) {
|
export function disableEncryption(workspaceId: string) {
|
||||||
return invoke<void>('cmd_disable_encryption', { workspaceId });
|
return invoke<void>("cmd_disable_encryption", { workspaceId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/crypto",
|
"name": "@yaakapp-internal/crypto",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,66 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { createFastMutation } from '@yaakapp/app/hooks/useFastMutation';
|
import { createFastMutation } from "@yaakapp/app/hooks/useFastMutation";
|
||||||
import { queryClient } from '@yaakapp/app/lib/queryClient';
|
import { queryClient } from "@yaakapp/app/lib/queryClient";
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from "react";
|
||||||
import { BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
|
import {
|
||||||
import { showToast } from '@yaakapp/app/lib/toast';
|
BranchDeleteResult,
|
||||||
|
CloneResult,
|
||||||
|
GitCommit,
|
||||||
|
GitRemote,
|
||||||
|
GitStatusSummary,
|
||||||
|
PullResult,
|
||||||
|
PushResult,
|
||||||
|
} from "./bindings/gen_git";
|
||||||
|
import { showToast } from "@yaakapp/app/lib/toast";
|
||||||
|
|
||||||
export * from './bindings/gen_git';
|
export * from "./bindings/gen_git";
|
||||||
export * from './bindings/gen_models';
|
export * from "./bindings/gen_models";
|
||||||
|
|
||||||
export interface GitCredentials {
|
export interface GitCredentials {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DivergedStrategy = 'force_reset' | 'merge' | 'cancel';
|
export type DivergedStrategy = "force_reset" | "merge" | "cancel";
|
||||||
|
|
||||||
export type UncommittedChangesStrategy = 'reset' | 'cancel';
|
export type UncommittedChangesStrategy = "reset" | "cancel";
|
||||||
|
|
||||||
export interface GitCallbacks {
|
export interface GitCallbacks {
|
||||||
addRemote: () => Promise<GitRemote | null>;
|
addRemote: () => Promise<GitRemote | null>;
|
||||||
promptCredentials: (
|
promptCredentials: (
|
||||||
result: Extract<PushResult, { type: 'needs_credentials' }>,
|
result: Extract<PushResult, { type: "needs_credentials" }>,
|
||||||
) => Promise<GitCredentials | null>;
|
) => Promise<GitCredentials | null>;
|
||||||
promptDiverged: (
|
promptDiverged: (result: Extract<PullResult, { type: "diverged" }>) => Promise<DivergedStrategy>;
|
||||||
result: Extract<PullResult, { type: 'diverged' }>,
|
|
||||||
) => Promise<DivergedStrategy>;
|
|
||||||
promptUncommittedChanges: () => Promise<UncommittedChangesStrategy>;
|
promptUncommittedChanges: () => Promise<UncommittedChangesStrategy>;
|
||||||
forceSync: () => Promise<void>;
|
forceSync: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
|
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ["git"] });
|
||||||
|
|
||||||
export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) {
|
export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) {
|
||||||
const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]);
|
const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]);
|
||||||
const fetchAll = useQuery<void, string>({
|
const fetchAll = useQuery<void, string>({
|
||||||
queryKey: ['git', 'fetch_all', dir, refreshKey],
|
queryKey: ["git", "fetch_all", dir, refreshKey],
|
||||||
queryFn: () => invoke('cmd_git_fetch_all', { dir }),
|
queryFn: () => invoke("cmd_git_fetch_all", { dir }),
|
||||||
refetchInterval: 10 * 60_000,
|
refetchInterval: 10 * 60_000,
|
||||||
});
|
});
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
remotes: useQuery<GitRemote[], string>({
|
remotes: useQuery<GitRemote[], string>({
|
||||||
queryKey: ['git', 'remotes', dir, refreshKey],
|
queryKey: ["git", "remotes", dir, refreshKey],
|
||||||
queryFn: () => getRemotes(dir),
|
queryFn: () => getRemotes(dir),
|
||||||
placeholderData: (prev) => prev,
|
placeholderData: (prev) => prev,
|
||||||
}),
|
}),
|
||||||
log: useQuery<GitCommit[], string>({
|
log: useQuery<GitCommit[], string>({
|
||||||
queryKey: ['git', 'log', dir, refreshKey],
|
queryKey: ["git", "log", dir, refreshKey],
|
||||||
queryFn: () => invoke('cmd_git_log', { dir }),
|
queryFn: () => invoke("cmd_git_log", { dir }),
|
||||||
placeholderData: (prev) => prev,
|
placeholderData: (prev) => prev,
|
||||||
}),
|
}),
|
||||||
status: useQuery<GitStatusSummary, string>({
|
status: useQuery<GitStatusSummary, string>({
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
queryKey: ['git', 'status', dir, refreshKey, fetchAll.dataUpdatedAt],
|
queryKey: ["git", "status", dir, refreshKey, fetchAll.dataUpdatedAt],
|
||||||
queryFn: () => invoke('cmd_git_status', { dir }),
|
queryFn: () => invoke("cmd_git_status", { dir }),
|
||||||
placeholderData: (prev) => prev,
|
placeholderData: (prev) => prev,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -67,151 +73,167 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
const remotes = await getRemotes(dir);
|
const remotes = await getRemotes(dir);
|
||||||
if (remotes.length === 0) {
|
if (remotes.length === 0) {
|
||||||
const remote = await callbacks.addRemote();
|
const remote = await callbacks.addRemote();
|
||||||
if (remote == null) throw new Error('No remote found');
|
if (remote == null) throw new Error("No remote found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await invoke<PushResult>('cmd_git_push', { dir });
|
const result = await invoke<PushResult>("cmd_git_push", { dir });
|
||||||
if (result.type !== 'needs_credentials') return result;
|
if (result.type !== "needs_credentials") return result;
|
||||||
|
|
||||||
// Needs credentials, prompt for them
|
// Needs credentials, prompt for them
|
||||||
const creds = await callbacks.promptCredentials(result);
|
const creds = await callbacks.promptCredentials(result);
|
||||||
if (creds == null) throw new Error('Canceled');
|
if (creds == null) throw new Error("Canceled");
|
||||||
|
|
||||||
await invoke('cmd_git_add_credential', {
|
await invoke("cmd_git_add_credential", {
|
||||||
remoteUrl: result.url,
|
remoteUrl: result.url,
|
||||||
username: creds.username,
|
username: creds.username,
|
||||||
password: creds.password,
|
password: creds.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Push again
|
// Push again
|
||||||
return invoke<PushResult>('cmd_git_push', { dir });
|
return invoke<PushResult>("cmd_git_push", { dir });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (err: unknown) => {
|
const handleError = (err: unknown) => {
|
||||||
showToast({
|
showToast({
|
||||||
id: `${err}`,
|
id: err instanceof Error ? err.message : String(err),
|
||||||
message: `${err}`,
|
message: err instanceof Error ? err.message : String(err),
|
||||||
color: 'danger',
|
color: "danger",
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: createFastMutation<void, string, void>({
|
init: createFastMutation<void, string, void>({
|
||||||
mutationKey: ['git', 'init'],
|
mutationKey: ["git", "init"],
|
||||||
mutationFn: () => invoke('cmd_git_initialize', { dir }),
|
mutationFn: () => invoke("cmd_git_initialize", { dir }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
add: createFastMutation<void, string, { relaPaths: string[] }>({
|
add: createFastMutation<void, string, { relaPaths: string[] }>({
|
||||||
mutationKey: ['git', 'add', dir],
|
mutationKey: ["git", "add", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_add', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_add", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
addRemote: createFastMutation<GitRemote, string, GitRemote>({
|
addRemote: createFastMutation<GitRemote, string, GitRemote>({
|
||||||
mutationKey: ['git', 'add-remote'],
|
mutationKey: ["git", "add-remote"],
|
||||||
mutationFn: (args) => invoke('cmd_git_add_remote', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_add_remote", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
rmRemote: createFastMutation<void, string, { name: string }>({
|
rmRemote: createFastMutation<void, string, { name: string }>({
|
||||||
mutationKey: ['git', 'rm-remote', dir],
|
mutationKey: ["git", "rm-remote", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_rm_remote', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_rm_remote", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
createBranch: createFastMutation<void, string, { branch: string; base?: string }>({
|
createBranch: createFastMutation<void, string, { branch: string; base?: string }>({
|
||||||
mutationKey: ['git', 'branch', dir],
|
mutationKey: ["git", "branch", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_branch', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_branch", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
mergeBranch: createFastMutation<void, string, { branch: string }>({
|
mergeBranch: createFastMutation<void, string, { branch: string }>({
|
||||||
mutationKey: ['git', 'merge', dir],
|
mutationKey: ["git", "merge", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_merge_branch', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_merge_branch", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
deleteBranch: createFastMutation<BranchDeleteResult, string, { branch: string, force?: boolean }>({
|
deleteBranch: createFastMutation<
|
||||||
mutationKey: ['git', 'delete-branch', dir],
|
BranchDeleteResult,
|
||||||
mutationFn: (args) => invoke('cmd_git_delete_branch', { dir, ...args }),
|
string,
|
||||||
|
{ branch: string; force?: boolean }
|
||||||
|
>({
|
||||||
|
mutationKey: ["git", "delete-branch", dir],
|
||||||
|
mutationFn: (args) => invoke("cmd_git_delete_branch", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
deleteRemoteBranch: createFastMutation<void, string, { branch: string }>({
|
deleteRemoteBranch: createFastMutation<void, string, { branch: string }>({
|
||||||
mutationKey: ['git', 'delete-remote-branch', dir],
|
mutationKey: ["git", "delete-remote-branch", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_delete_remote_branch', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_delete_remote_branch", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
renameBranch: createFastMutation<void, string, { oldName: string, newName: string }>({
|
renameBranch: createFastMutation<void, string, { oldName: string; newName: string }>({
|
||||||
mutationKey: ['git', 'rename-branch', dir],
|
mutationKey: ["git", "rename-branch", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_rename_branch', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_rename_branch", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
checkout: createFastMutation<string, string, { branch: string; force: boolean }>({
|
checkout: createFastMutation<string, string, { branch: string; force: boolean }>({
|
||||||
mutationKey: ['git', 'checkout', dir],
|
mutationKey: ["git", "checkout", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_checkout', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_checkout", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
commit: createFastMutation<void, string, { message: string }>({
|
commit: createFastMutation<void, string, { message: string }>({
|
||||||
mutationKey: ['git', 'commit', dir],
|
mutationKey: ["git", "commit", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_commit', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_commit", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
commitAndPush: createFastMutation<PushResult, string, { message: string }>({
|
commitAndPush: createFastMutation<PushResult, string, { message: string }>({
|
||||||
mutationKey: ['git', 'commit_push', dir],
|
mutationKey: ["git", "commit_push", dir],
|
||||||
mutationFn: async (args) => {
|
mutationFn: async (args) => {
|
||||||
await invoke('cmd_git_commit', { dir, ...args });
|
await invoke("cmd_git_commit", { dir, ...args });
|
||||||
return push();
|
return push();
|
||||||
},
|
},
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
push: createFastMutation<PushResult, string, void>({
|
push: createFastMutation<PushResult, string, void>({
|
||||||
mutationKey: ['git', 'push', dir],
|
mutationKey: ["git", "push", dir],
|
||||||
mutationFn: push,
|
mutationFn: push,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
pull: createFastMutation<PullResult, string, void>({
|
pull: createFastMutation<PullResult, string, void>({
|
||||||
mutationKey: ['git', 'pull', dir],
|
mutationKey: ["git", "pull", dir],
|
||||||
async mutationFn() {
|
async mutationFn() {
|
||||||
const result = await invoke<PullResult>('cmd_git_pull', { dir });
|
const result = await invoke<PullResult>("cmd_git_pull", { dir });
|
||||||
|
|
||||||
if (result.type === 'needs_credentials') {
|
if (result.type === "needs_credentials") {
|
||||||
const creds = await callbacks.promptCredentials(result);
|
const creds = await callbacks.promptCredentials(result);
|
||||||
if (creds == null) throw new Error('Canceled');
|
if (creds == null) throw new Error("Canceled");
|
||||||
|
|
||||||
await invoke('cmd_git_add_credential', {
|
await invoke("cmd_git_add_credential", {
|
||||||
remoteUrl: result.url,
|
remoteUrl: result.url,
|
||||||
username: creds.username,
|
username: creds.username,
|
||||||
password: creds.password,
|
password: creds.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pull again after credentials
|
// Pull again after credentials
|
||||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
return invoke<PullResult>("cmd_git_pull", { dir });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.type === 'uncommitted_changes') {
|
if (result.type === "uncommitted_changes") {
|
||||||
callbacks.promptUncommittedChanges().then(async (strategy) => {
|
void callbacks
|
||||||
if (strategy === 'cancel') return;
|
.promptUncommittedChanges()
|
||||||
|
.then(async (strategy) => {
|
||||||
|
if (strategy === "cancel") return;
|
||||||
|
|
||||||
await invoke('cmd_git_reset_changes', { dir });
|
await invoke("cmd_git_reset_changes", { dir });
|
||||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
return invoke<PullResult>("cmd_git_pull", { dir });
|
||||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await onSuccess();
|
||||||
|
await callbacks.forceSync();
|
||||||
|
}, handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.type === 'diverged') {
|
if (result.type === "diverged") {
|
||||||
callbacks.promptDiverged(result).then((strategy) => {
|
void callbacks
|
||||||
if (strategy === 'cancel') return;
|
.promptDiverged(result)
|
||||||
|
.then((strategy) => {
|
||||||
|
if (strategy === "cancel") return;
|
||||||
|
|
||||||
if (strategy === 'force_reset') {
|
if (strategy === "force_reset") {
|
||||||
return invoke<PullResult>('cmd_git_pull_force_reset', {
|
return invoke<PullResult>("cmd_git_pull_force_reset", {
|
||||||
|
dir,
|
||||||
|
remote: result.remote,
|
||||||
|
branch: result.branch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoke<PullResult>("cmd_git_pull_merge", {
|
||||||
dir,
|
dir,
|
||||||
remote: result.remote,
|
remote: result.remote,
|
||||||
branch: result.branch,
|
branch: result.branch,
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
|
.then(async () => {
|
||||||
return invoke<PullResult>('cmd_git_pull_merge', {
|
await onSuccess();
|
||||||
dir,
|
await callbacks.forceSync();
|
||||||
remote: result.remote,
|
}, handleError);
|
||||||
branch: result.branch,
|
|
||||||
});
|
|
||||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -219,20 +241,20 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
unstage: createFastMutation<void, string, { relaPaths: string[] }>({
|
unstage: createFastMutation<void, string, { relaPaths: string[] }>({
|
||||||
mutationKey: ['git', 'unstage', dir],
|
mutationKey: ["git", "unstage", dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_unstage', { dir, ...args }),
|
mutationFn: (args) => invoke("cmd_git_unstage", { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
resetChanges: createFastMutation<void, string, void>({
|
resetChanges: createFastMutation<void, string, void>({
|
||||||
mutationKey: ['git', 'reset-changes', dir],
|
mutationKey: ["git", "reset-changes", dir],
|
||||||
mutationFn: () => invoke('cmd_git_reset_changes', { dir }),
|
mutationFn: () => invoke("cmd_git_reset_changes", { dir }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
} as const;
|
} as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getRemotes(dir: string) {
|
async function getRemotes(dir: string) {
|
||||||
return invoke<GitRemote[]>('cmd_git_remotes', { dir });
|
return invoke<GitRemote[]>("cmd_git_remotes", { dir });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,21 +263,24 @@ async function getRemotes(dir: string) {
|
|||||||
export async function gitClone(
|
export async function gitClone(
|
||||||
url: string,
|
url: string,
|
||||||
dir: string,
|
dir: string,
|
||||||
promptCredentials: (args: { url: string; error: string | null }) => Promise<GitCredentials | null>,
|
promptCredentials: (args: {
|
||||||
|
url: string;
|
||||||
|
error: string | null;
|
||||||
|
}) => Promise<GitCredentials | null>,
|
||||||
): Promise<CloneResult> {
|
): Promise<CloneResult> {
|
||||||
const result = await invoke<CloneResult>('cmd_git_clone', { url, dir });
|
const result = await invoke<CloneResult>("cmd_git_clone", { url, dir });
|
||||||
if (result.type !== 'needs_credentials') return result;
|
if (result.type !== "needs_credentials") return result;
|
||||||
|
|
||||||
// Prompt for credentials
|
// Prompt for credentials
|
||||||
const creds = await promptCredentials({ url: result.url, error: result.error });
|
const creds = await promptCredentials({ url: result.url, error: result.error });
|
||||||
if (creds == null) return {type: 'cancelled'};
|
if (creds == null) return { type: "cancelled" };
|
||||||
|
|
||||||
// Store credentials and retry
|
// Store credentials and retry
|
||||||
await invoke('cmd_git_add_credential', {
|
await invoke("cmd_git_add_credential", {
|
||||||
remoteUrl: result.url,
|
remoteUrl: result.url,
|
||||||
username: creds.username,
|
username: creds.username,
|
||||||
password: creds.password,
|
password: creds.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
return invoke<CloneResult>('cmd_git_clone', { url, dir });
|
return invoke<CloneResult>("cmd_git_clone", { url, dir });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/git",
|
"name": "@yaakapp-internal/git",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ hyper-util = { version = "0.1.17", default-features = false, features = ["client
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { workspace = true, features = ["rustls-tls-manual-roots-no-provider", "socks", "http2", "stream"] }
|
reqwest = { workspace = true, features = [
|
||||||
|
"rustls-tls-manual-roots-no-provider",
|
||||||
|
"socks",
|
||||||
|
"http2",
|
||||||
|
"stream",
|
||||||
|
] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
@@ -29,4 +34,5 @@ tower-service = "0.3.3"
|
|||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
yaak-common = { workspace = true }
|
yaak-common = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
yaak-templates = { workspace = true }
|
||||||
yaak-tls = { workspace = true }
|
yaak-tls = { workspace = true }
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ pub enum HttpResponseEvent {
|
|||||||
url: String,
|
url: String,
|
||||||
status: u16,
|
status: u16,
|
||||||
behavior: RedirectBehavior,
|
behavior: RedirectBehavior,
|
||||||
|
dropped_body: bool,
|
||||||
|
dropped_headers: Vec<String>,
|
||||||
},
|
},
|
||||||
SendUrl {
|
SendUrl {
|
||||||
method: String,
|
method: String,
|
||||||
@@ -67,12 +69,28 @@ impl Display for HttpResponseEvent {
|
|||||||
match self {
|
match self {
|
||||||
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
|
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
|
||||||
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
|
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
|
||||||
HttpResponseEvent::Redirect { url, status, behavior } => {
|
HttpResponseEvent::Redirect {
|
||||||
|
url,
|
||||||
|
status,
|
||||||
|
behavior,
|
||||||
|
dropped_body,
|
||||||
|
dropped_headers,
|
||||||
|
} => {
|
||||||
let behavior_str = match behavior {
|
let behavior_str = match behavior {
|
||||||
RedirectBehavior::Preserve => "preserve",
|
RedirectBehavior::Preserve => "preserve",
|
||||||
RedirectBehavior::DropBody => "drop body",
|
RedirectBehavior::DropBody => "drop body",
|
||||||
};
|
};
|
||||||
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
let body_str = if *dropped_body { ", body dropped" } else { "" };
|
||||||
|
let headers_str = if dropped_headers.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(", headers dropped: {}", dropped_headers.join(", "))
|
||||||
|
};
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"* Redirect {} -> {} ({}{}{})",
|
||||||
|
status, url, behavior_str, body_str, headers_str
|
||||||
|
)
|
||||||
}
|
}
|
||||||
HttpResponseEvent::SendUrl {
|
HttpResponseEvent::SendUrl {
|
||||||
method,
|
method,
|
||||||
@@ -130,13 +148,21 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
|||||||
match event {
|
match event {
|
||||||
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
|
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
|
||||||
HttpResponseEvent::Info(message) => D::Info { message },
|
HttpResponseEvent::Info(message) => D::Info { message },
|
||||||
HttpResponseEvent::Redirect { url, status, behavior } => D::Redirect {
|
HttpResponseEvent::Redirect {
|
||||||
|
url,
|
||||||
|
status,
|
||||||
|
behavior,
|
||||||
|
dropped_body,
|
||||||
|
dropped_headers,
|
||||||
|
} => D::Redirect {
|
||||||
url,
|
url,
|
||||||
status,
|
status,
|
||||||
behavior: match behavior {
|
behavior: match behavior {
|
||||||
RedirectBehavior::Preserve => "preserve".to_string(),
|
RedirectBehavior::Preserve => "preserve".to_string(),
|
||||||
RedirectBehavior::DropBody => "drop_body".to_string(),
|
RedirectBehavior::DropBody => "drop_body".to_string(),
|
||||||
},
|
},
|
||||||
|
dropped_body,
|
||||||
|
dropped_headers,
|
||||||
},
|
},
|
||||||
HttpResponseEvent::SendUrl {
|
HttpResponseEvent::SendUrl {
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::cookies::CookieStore;
|
use crate::cookies::CookieStore;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::sender::{HttpResponse, HttpResponseEvent, HttpSender, RedirectBehavior};
|
use crate::sender::{HttpResponse, HttpResponseEvent, HttpSender, RedirectBehavior};
|
||||||
use crate::types::SendableHttpRequest;
|
use crate::types::{SendableBody, SendableHttpRequest};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::watch::Receiver;
|
use tokio::sync::watch::Receiver;
|
||||||
@@ -87,6 +87,11 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build request for this iteration
|
// Build request for this iteration
|
||||||
|
let preserved_body = match ¤t_body {
|
||||||
|
Some(SendableBody::Bytes(b)) => Some(SendableBody::Bytes(b.clone())),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let request_had_body = current_body.is_some();
|
||||||
let req = SendableHttpRequest {
|
let req = SendableHttpRequest {
|
||||||
url: current_url.clone(),
|
url: current_url.clone(),
|
||||||
method: current_method.clone(),
|
method: current_method.clone(),
|
||||||
@@ -182,8 +187,6 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
format!("{}/{}", base_path, location)
|
format!("{}/{}", base_path, location)
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
|
||||||
|
|
||||||
// Determine redirect behavior based on status code and method
|
// Determine redirect behavior based on status code and method
|
||||||
let behavior = if status == 303 {
|
let behavior = if status == 303 {
|
||||||
// 303 See Other always changes to GET
|
// 303 See Other always changes to GET
|
||||||
@@ -197,11 +200,8 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
RedirectBehavior::Preserve
|
RedirectBehavior::Preserve
|
||||||
};
|
};
|
||||||
|
|
||||||
send_event(HttpResponseEvent::Redirect {
|
let mut dropped_headers =
|
||||||
url: current_url.clone(),
|
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
||||||
status,
|
|
||||||
behavior: behavior.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle method changes for certain redirect codes
|
// Handle method changes for certain redirect codes
|
||||||
if matches!(behavior, RedirectBehavior::DropBody) {
|
if matches!(behavior, RedirectBehavior::DropBody) {
|
||||||
@@ -211,13 +211,40 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
// Remove content-related headers
|
// Remove content-related headers
|
||||||
current_headers.retain(|h| {
|
current_headers.retain(|h| {
|
||||||
let name_lower = h.0.to_lowercase();
|
let name_lower = h.0.to_lowercase();
|
||||||
!name_lower.starts_with("content-") && name_lower != "transfer-encoding"
|
let should_drop =
|
||||||
|
name_lower.starts_with("content-") || name_lower == "transfer-encoding";
|
||||||
|
if should_drop {
|
||||||
|
Self::push_header_if_missing(&mut dropped_headers, &h.0);
|
||||||
|
}
|
||||||
|
!should_drop
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset body for next iteration (since it was moved in the send call)
|
// Restore body for Preserve redirects (307/308), drop for others.
|
||||||
// For redirects that change method to GET or for all redirects since body was consumed
|
// Stream bodies can't be replayed (same limitation as reqwest).
|
||||||
current_body = None;
|
current_body = if matches!(behavior, RedirectBehavior::Preserve) {
|
||||||
|
if request_had_body && preserved_body.is_none() {
|
||||||
|
// Stream body was consumed and can't be replayed (same as reqwest)
|
||||||
|
return Err(crate::error::Error::RequestError(
|
||||||
|
"Cannot follow redirect: request body was a stream and cannot be resent"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
preserved_body
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Body was dropped if the request had one but we can't resend it
|
||||||
|
let dropped_body = request_had_body && current_body.is_none();
|
||||||
|
|
||||||
|
send_event(HttpResponseEvent::Redirect {
|
||||||
|
url: current_url.clone(),
|
||||||
|
status,
|
||||||
|
behavior: behavior.clone(),
|
||||||
|
dropped_body,
|
||||||
|
dropped_headers,
|
||||||
|
});
|
||||||
|
|
||||||
redirect_count += 1;
|
redirect_count += 1;
|
||||||
}
|
}
|
||||||
@@ -231,7 +258,8 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
headers: &mut Vec<(String, String)>,
|
headers: &mut Vec<(String, String)>,
|
||||||
previous_url: &str,
|
previous_url: &str,
|
||||||
next_url: &str,
|
next_url: &str,
|
||||||
) {
|
) -> Vec<String> {
|
||||||
|
let mut dropped_headers = Vec::new();
|
||||||
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
|
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
|
||||||
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
||||||
});
|
});
|
||||||
@@ -241,13 +269,24 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
if previous_host != next_host {
|
if previous_host != next_host {
|
||||||
headers.retain(|h| {
|
headers.retain(|h| {
|
||||||
let name_lower = h.0.to_lowercase();
|
let name_lower = h.0.to_lowercase();
|
||||||
name_lower != "authorization"
|
let should_drop = name_lower == "authorization"
|
||||||
&& name_lower != "cookie"
|
|| name_lower == "cookie"
|
||||||
&& name_lower != "cookie2"
|
|| name_lower == "cookie2"
|
||||||
&& name_lower != "proxy-authorization"
|
|| name_lower == "proxy-authorization"
|
||||||
&& name_lower != "www-authenticate"
|
|| name_lower == "www-authenticate";
|
||||||
|
if should_drop {
|
||||||
|
Self::push_header_if_missing(&mut dropped_headers, &h.0);
|
||||||
|
}
|
||||||
|
!should_drop
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
dropped_headers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_header_if_missing(headers: &mut Vec<String>, name: &str) {
|
||||||
|
if !headers.iter().any(|h| h.eq_ignore_ascii_case(name)) {
|
||||||
|
headers.push(name.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a status code indicates a redirect
|
/// Check if a status code indicates a redirect
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ use std::collections::BTreeMap;
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
use yaak_common::serde::{get_bool, get_str, get_str_map};
|
use yaak_common::serde::{get_bool, get_bool_map, get_str, get_str_map};
|
||||||
use yaak_models::models::HttpRequest;
|
use yaak_models::models::HttpRequest;
|
||||||
|
use yaak_templates::strip_json_comments::{maybe_strip_json_comments, strip_json_comments};
|
||||||
|
|
||||||
pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary";
|
pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary";
|
||||||
|
|
||||||
@@ -134,16 +135,69 @@ pub fn append_query_params(url: &str, params: Vec<(String, String)>) -> String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn strip_query_params(url: &str, names: &[&str]) -> String {
|
||||||
|
// Split off fragment
|
||||||
|
let (base_and_query, fragment) = if let Some(hash_pos) = url.find('#') {
|
||||||
|
(&url[..hash_pos], Some(&url[hash_pos..]))
|
||||||
|
} else {
|
||||||
|
(url, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if let Some(q_pos) = base_and_query.find('?') {
|
||||||
|
let base = &base_and_query[..q_pos];
|
||||||
|
let query = &base_and_query[q_pos + 1..];
|
||||||
|
let filtered: Vec<&str> = query
|
||||||
|
.split('&')
|
||||||
|
.filter(|pair| {
|
||||||
|
let key = pair.split('=').next().unwrap_or("");
|
||||||
|
let decoded = urlencoding::decode(key).unwrap_or_default();
|
||||||
|
!names.contains(&decoded.as_ref())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if filtered.is_empty() {
|
||||||
|
base.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}?{}", base, filtered.join("&"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
base_and_query.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
match fragment {
|
||||||
|
Some(f) => format!("{}{}", result, f),
|
||||||
|
None => result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_url(r: &HttpRequest) -> String {
|
fn build_url(r: &HttpRequest) -> String {
|
||||||
let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters);
|
let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters);
|
||||||
append_query_params(
|
let mut url = append_query_params(
|
||||||
&url_string,
|
&url_string,
|
||||||
params
|
params
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|p| p.enabled && !p.name.is_empty())
|
.filter(|p| p.enabled && !p.name.is_empty())
|
||||||
.map(|p| (p.name.clone(), p.value.clone()))
|
.map(|p| (p.name.clone(), p.value.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
);
|
||||||
|
|
||||||
|
// GraphQL GET requests encode query/variables as URL query parameters
|
||||||
|
if r.method.to_lowercase() == "get" && r.body_type.as_deref() == Some("graphql") {
|
||||||
|
url = append_graphql_query_params(&url, &r.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_graphql_query_params(url: &str, body: &BTreeMap<String, serde_json::Value>) -> String {
|
||||||
|
let query = get_str_map(body, "query").to_string();
|
||||||
|
let variables = strip_json_comments(&get_str_map(body, "variables"));
|
||||||
|
let mut params = vec![("query".to_string(), query)];
|
||||||
|
if !variables.trim().is_empty() {
|
||||||
|
params.push(("variables".to_string(), variables));
|
||||||
|
}
|
||||||
|
// Strip existing query/variables params to avoid duplicates
|
||||||
|
let url = strip_query_params(url, &["query", "variables"]);
|
||||||
|
append_query_params(&url, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_headers(r: &HttpRequest) -> Vec<(String, String)> {
|
fn build_headers(r: &HttpRequest) -> Vec<(String, String)> {
|
||||||
@@ -177,7 +231,7 @@ async fn build_body(
|
|||||||
(build_form_body(&body), Some("application/x-www-form-urlencoded".to_string()))
|
(build_form_body(&body), Some("application/x-www-form-urlencoded".to_string()))
|
||||||
}
|
}
|
||||||
"multipart/form-data" => build_multipart_body(&body, &headers).await?,
|
"multipart/form-data" => build_multipart_body(&body, &headers).await?,
|
||||||
_ if body.contains_key("text") => (build_text_body(&body), None),
|
_ if body.contains_key("text") => (build_text_body(&body, body_type), None),
|
||||||
t => {
|
t => {
|
||||||
warn!("Unsupported body type: {}", t);
|
warn!("Unsupported body type: {}", t);
|
||||||
(None, None)
|
(None, None)
|
||||||
@@ -252,13 +306,20 @@ async fn build_binary_body(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_text_body(body: &BTreeMap<String, serde_json::Value>) -> Option<SendableBodyWithMeta> {
|
fn build_text_body(body: &BTreeMap<String, serde_json::Value>, body_type: &str) -> Option<SendableBodyWithMeta> {
|
||||||
let text = get_str_map(body, "text");
|
let text = get_str_map(body, "text");
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
None
|
return None;
|
||||||
} else {
|
|
||||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(text.to_string())))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let send_comments = get_bool_map(body, "sendJsonComments", false);
|
||||||
|
let text = if !send_comments && body_type == "application/json" {
|
||||||
|
maybe_strip_json_comments(text)
|
||||||
|
} else {
|
||||||
|
text.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(SendableBodyWithMeta::Bytes(Bytes::from(text)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_graphql_body(
|
fn build_graphql_body(
|
||||||
@@ -266,7 +327,7 @@ fn build_graphql_body(
|
|||||||
body: &BTreeMap<String, serde_json::Value>,
|
body: &BTreeMap<String, serde_json::Value>,
|
||||||
) -> Option<SendableBodyWithMeta> {
|
) -> Option<SendableBodyWithMeta> {
|
||||||
let query = get_str_map(body, "query");
|
let query = get_str_map(body, "query");
|
||||||
let variables = get_str_map(body, "variables");
|
let variables = strip_json_comments(&get_str_map(body, "variables"));
|
||||||
|
|
||||||
if method.to_lowercase() == "get" {
|
if method.to_lowercase() == "get" {
|
||||||
// GraphQL GET requests use query parameters, not a body
|
// GraphQL GET requests use query parameters, not a body
|
||||||
@@ -684,7 +745,7 @@ mod tests {
|
|||||||
let mut body = BTreeMap::new();
|
let mut body = BTreeMap::new();
|
||||||
body.insert("text".to_string(), json!("Hello, World!"));
|
body.insert("text".to_string(), json!("Hello, World!"));
|
||||||
|
|
||||||
let result = build_text_body(&body);
|
let result = build_text_body(&body, "application/json");
|
||||||
match result {
|
match result {
|
||||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||||
assert_eq!(bytes, Bytes::from("Hello, World!"))
|
assert_eq!(bytes, Bytes::from("Hello, World!"))
|
||||||
@@ -698,7 +759,7 @@ mod tests {
|
|||||||
let mut body = BTreeMap::new();
|
let mut body = BTreeMap::new();
|
||||||
body.insert("text".to_string(), json!(""));
|
body.insert("text".to_string(), json!(""));
|
||||||
|
|
||||||
let result = build_text_body(&body);
|
let result = build_text_body(&body, "application/json");
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,10 +767,57 @@ mod tests {
|
|||||||
async fn test_text_body_missing() {
|
async fn test_text_body_missing() {
|
||||||
let body = BTreeMap::new();
|
let body = BTreeMap::new();
|
||||||
|
|
||||||
let result = build_text_body(&body);
|
let result = build_text_body(&body, "application/json");
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_text_body_strips_json_comments_by_default() {
|
||||||
|
let mut body = BTreeMap::new();
|
||||||
|
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
|
||||||
|
|
||||||
|
let result = build_text_body(&body, "application/json");
|
||||||
|
match result {
|
||||||
|
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||||
|
let text = String::from_utf8_lossy(&bytes);
|
||||||
|
assert!(!text.contains("// comment"));
|
||||||
|
assert!(text.contains("\"foo\": \"bar\""));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_text_body_send_json_comments_when_opted_in() {
|
||||||
|
let mut body = BTreeMap::new();
|
||||||
|
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
|
||||||
|
body.insert("sendJsonComments".to_string(), json!(true));
|
||||||
|
|
||||||
|
let result = build_text_body(&body, "application/json");
|
||||||
|
match result {
|
||||||
|
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||||
|
let text = String::from_utf8_lossy(&bytes);
|
||||||
|
assert!(text.contains("// comment"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_text_body_no_strip_for_non_json() {
|
||||||
|
let mut body = BTreeMap::new();
|
||||||
|
body.insert("text".to_string(), json!("// not json\nsome text"));
|
||||||
|
|
||||||
|
let result = build_text_body(&body, "text/plain");
|
||||||
|
match result {
|
||||||
|
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||||
|
let text = String::from_utf8_lossy(&bytes);
|
||||||
|
assert!(text.contains("// not json"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_form_urlencoded_body() -> Result<()> {
|
async fn test_form_urlencoded_body() -> Result<()> {
|
||||||
let mut body = BTreeMap::new();
|
let mut body = BTreeMap::new();
|
||||||
|
|||||||
2
crates/yaak-models/bindings/gen_models.ts
generated
2
crates/yaak-models/bindings/gen_models.ts
generated
@@ -49,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from "jotai";
|
||||||
|
|
||||||
import { selectAtom } from 'jotai/utils';
|
import { selectAtom } from "jotai/utils";
|
||||||
import type { AnyModel } from '../bindings/gen_models';
|
import type { AnyModel } from "../bindings/gen_models";
|
||||||
import { ExtractModel } from './types';
|
import { ExtractModel } from "./types";
|
||||||
import { newStoreData } from './util';
|
import { newStoreData } from "./util";
|
||||||
|
|
||||||
export const modelStoreDataAtom = atom(newStoreData());
|
export const modelStoreDataAtom = atom(newStoreData());
|
||||||
|
|
||||||
export const cookieJarsAtom = createOrderedModelAtom('cookie_jar', 'name', 'asc');
|
export const cookieJarsAtom = createOrderedModelAtom("cookie_jar", "name", "asc");
|
||||||
export const environmentsAtom = createOrderedModelAtom('environment', 'sortPriority', 'asc');
|
export const environmentsAtom = createOrderedModelAtom("environment", "sortPriority", "asc");
|
||||||
export const foldersAtom = createModelAtom('folder');
|
export const foldersAtom = createModelAtom("folder");
|
||||||
export const grpcConnectionsAtom = createOrderedModelAtom('grpc_connection', 'createdAt', 'desc');
|
export const grpcConnectionsAtom = createOrderedModelAtom("grpc_connection", "createdAt", "desc");
|
||||||
export const grpcEventsAtom = createOrderedModelAtom('grpc_event', 'createdAt', 'asc');
|
export const grpcEventsAtom = createOrderedModelAtom("grpc_event", "createdAt", "asc");
|
||||||
export const grpcRequestsAtom = createModelAtom('grpc_request');
|
export const grpcRequestsAtom = createModelAtom("grpc_request");
|
||||||
export const httpRequestsAtom = createModelAtom('http_request');
|
export const httpRequestsAtom = createModelAtom("http_request");
|
||||||
export const httpResponsesAtom = createOrderedModelAtom('http_response', 'createdAt', 'desc');
|
export const httpResponsesAtom = createOrderedModelAtom("http_response", "createdAt", "desc");
|
||||||
export const httpResponseEventsAtom = createOrderedModelAtom('http_response_event', 'createdAt', 'asc');
|
export const httpResponseEventsAtom = createOrderedModelAtom(
|
||||||
export const keyValuesAtom = createModelAtom('key_value');
|
"http_response_event",
|
||||||
export const pluginsAtom = createModelAtom('plugin');
|
"createdAt",
|
||||||
export const settingsAtom = createSingularModelAtom('settings');
|
"asc",
|
||||||
export const websocketRequestsAtom = createModelAtom('websocket_request');
|
|
||||||
export const websocketEventsAtom = createOrderedModelAtom('websocket_event', 'createdAt', 'asc');
|
|
||||||
export const websocketConnectionsAtom = createOrderedModelAtom(
|
|
||||||
'websocket_connection',
|
|
||||||
'createdAt',
|
|
||||||
'desc',
|
|
||||||
);
|
);
|
||||||
export const workspaceMetasAtom = createModelAtom('workspace_meta');
|
export const keyValuesAtom = createModelAtom("key_value");
|
||||||
export const workspacesAtom = createOrderedModelAtom('workspace', 'name', 'asc');
|
export const pluginsAtom = createModelAtom("plugin");
|
||||||
|
export const settingsAtom = createSingularModelAtom("settings");
|
||||||
|
export const websocketRequestsAtom = createModelAtom("websocket_request");
|
||||||
|
export const websocketEventsAtom = createOrderedModelAtom("websocket_event", "createdAt", "asc");
|
||||||
|
export const websocketConnectionsAtom = createOrderedModelAtom(
|
||||||
|
"websocket_connection",
|
||||||
|
"createdAt",
|
||||||
|
"desc",
|
||||||
|
);
|
||||||
|
export const workspaceMetasAtom = createModelAtom("workspace_meta");
|
||||||
|
export const workspacesAtom = createOrderedModelAtom("workspace", "name", "asc");
|
||||||
|
|
||||||
export function createModelAtom<M extends AnyModel['model']>(modelType: M) {
|
export function createModelAtom<M extends AnyModel["model"]>(modelType: M) {
|
||||||
return selectAtom(
|
return selectAtom(
|
||||||
modelStoreDataAtom,
|
modelStoreDataAtom,
|
||||||
(data) => Object.values(data[modelType] ?? {}),
|
(data) => Object.values(data[modelType] ?? {}),
|
||||||
@@ -37,19 +41,19 @@ export function createModelAtom<M extends AnyModel['model']>(modelType: M) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSingularModelAtom<M extends AnyModel['model']>(modelType: M) {
|
export function createSingularModelAtom<M extends AnyModel["model"]>(modelType: M) {
|
||||||
return selectAtom(modelStoreDataAtom, (data) => {
|
return selectAtom(modelStoreDataAtom, (data) => {
|
||||||
const modelData = Object.values(data[modelType] ?? {});
|
const modelData = Object.values(data[modelType] ?? {});
|
||||||
const item = modelData[0];
|
const item = modelData[0];
|
||||||
if (item == null) throw new Error('Failed creating singular model with no data: ' + modelType);
|
if (item == null) throw new Error("Failed creating singular model with no data: " + modelType);
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOrderedModelAtom<M extends AnyModel['model']>(
|
export function createOrderedModelAtom<M extends AnyModel["model"]>(
|
||||||
modelType: M,
|
modelType: M,
|
||||||
field: keyof ExtractModel<AnyModel, M>,
|
field: keyof ExtractModel<AnyModel, M>,
|
||||||
order: 'asc' | 'desc',
|
order: "asc" | "desc",
|
||||||
) {
|
) {
|
||||||
return selectAtom(
|
return selectAtom(
|
||||||
modelStoreDataAtom,
|
modelStoreDataAtom,
|
||||||
@@ -58,7 +62,7 @@ export function createOrderedModelAtom<M extends AnyModel['model']>(
|
|||||||
return Object.values(modelData).sort(
|
return Object.values(modelData).sort(
|
||||||
(a: ExtractModel<AnyModel, M>, b: ExtractModel<AnyModel, M>) => {
|
(a: ExtractModel<AnyModel, M>, b: ExtractModel<AnyModel, M>) => {
|
||||||
const n = a[field] > b[field] ? 1 : -1;
|
const n = a[field] > b[field] ? 1 : -1;
|
||||||
return order === 'desc' ? n * -1 : n;
|
return order === "desc" ? n * -1 : n;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { AnyModel } from '../bindings/gen_models';
|
import { AnyModel } from "../bindings/gen_models";
|
||||||
|
|
||||||
export * from '../bindings/gen_models';
|
export * from "../bindings/gen_models";
|
||||||
export * from '../bindings/gen_util';
|
export * from "../bindings/gen_util";
|
||||||
export * from './store';
|
export * from "./store";
|
||||||
export * from './atoms';
|
export * from "./atoms";
|
||||||
|
|
||||||
export function modelTypeLabel(m: AnyModel): string {
|
export function modelTypeLabel(m: AnyModel): string {
|
||||||
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
return m.model.split('_').map(capitalize).join(' ');
|
return m.model.split("_").map(capitalize).join(" ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import { resolvedModelName } from '@yaakapp/app/lib/resolvedModelName';
|
import { resolvedModelName } from "@yaakapp/app/lib/resolvedModelName";
|
||||||
import { AnyModel, ModelPayload } from '../bindings/gen_models';
|
import { AnyModel, ModelPayload } from "../bindings/gen_models";
|
||||||
import { modelStoreDataAtom } from './atoms';
|
import { modelStoreDataAtom } from "./atoms";
|
||||||
import { ExtractModel, JotaiStore, ModelStoreData } from './types';
|
import { ExtractModel, JotaiStore, ModelStoreData } from "./types";
|
||||||
import { newStoreData } from './util';
|
import { newStoreData } from "./util";
|
||||||
|
|
||||||
let _store: JotaiStore | null = null;
|
let _store: JotaiStore | null = null;
|
||||||
|
|
||||||
@@ -12,11 +12,11 @@ export function initModelStore(store: JotaiStore) {
|
|||||||
_store = store;
|
_store = store;
|
||||||
|
|
||||||
getCurrentWebviewWindow()
|
getCurrentWebviewWindow()
|
||||||
.listen<ModelPayload>('model_write', ({ payload }) => {
|
.listen<ModelPayload>("model_write", ({ payload }) => {
|
||||||
if (shouldIgnoreModel(payload)) return;
|
if (shouldIgnoreModel(payload)) return;
|
||||||
|
|
||||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
||||||
if (payload.change.type === 'upsert') {
|
if (payload.change.type === "upsert") {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[payload.model.model]: {
|
[payload.model.model]: {
|
||||||
@@ -36,7 +36,7 @@ export function initModelStore(store: JotaiStore) {
|
|||||||
|
|
||||||
function mustStore(): JotaiStore {
|
function mustStore(): JotaiStore {
|
||||||
if (_store == null) {
|
if (_store == null) {
|
||||||
throw new Error('Model store was not initialized');
|
throw new Error("Model store was not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
return _store;
|
return _store;
|
||||||
@@ -45,8 +45,8 @@ function mustStore(): JotaiStore {
|
|||||||
let _activeWorkspaceId: string | null = null;
|
let _activeWorkspaceId: string | null = null;
|
||||||
|
|
||||||
export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
||||||
console.log('Syncing models with new workspace', workspaceId);
|
console.log("Syncing models with new workspace", workspaceId);
|
||||||
const workspaceModelsStr = await invoke<string>('models_workspace_models', {
|
const workspaceModelsStr = await invoke<string>("models_workspace_models", {
|
||||||
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
|
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
|
||||||
});
|
});
|
||||||
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
|
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
|
||||||
@@ -57,12 +57,12 @@ export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
|||||||
|
|
||||||
mustStore().set(modelStoreDataAtom, data);
|
mustStore().set(modelStoreDataAtom, data);
|
||||||
|
|
||||||
console.log('Synced model store with workspace', workspaceId, data);
|
console.log("Synced model store with workspace", workspaceId, data);
|
||||||
|
|
||||||
_activeWorkspaceId = workspaceId;
|
_activeWorkspaceId = workspaceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listModels<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
export function listModels<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
modelType: M | ReadonlyArray<M>,
|
modelType: M | ReadonlyArray<M>,
|
||||||
): T[] {
|
): T[] {
|
||||||
let data = mustStore().get(modelStoreDataAtom);
|
let data = mustStore().get(modelStoreDataAtom);
|
||||||
@@ -70,7 +70,7 @@ export function listModels<M extends AnyModel['model'], T extends ExtractModel<A
|
|||||||
return types.flatMap((t) => Object.values(data[t]) as T[]);
|
return types.flatMap((t) => Object.values(data[t]) as T[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
export function getModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
modelType: M | ReadonlyArray<M>,
|
modelType: M | ReadonlyArray<M>,
|
||||||
id: string,
|
id: string,
|
||||||
): T | null {
|
): T | null {
|
||||||
@@ -83,18 +83,17 @@ export function getModel<M extends AnyModel['model'], T extends ExtractModel<Any
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAnyModel(
|
export function getAnyModel(id: string): AnyModel | null {
|
||||||
id: string,
|
|
||||||
): AnyModel | null {
|
|
||||||
let data = mustStore().get(modelStoreDataAtom);
|
let data = mustStore().get(modelStoreDataAtom);
|
||||||
for (const t of Object.keys(data)) {
|
for (const t of Object.keys(data)) {
|
||||||
|
// oxlint-disable-next-line no-explicit-any
|
||||||
let v = (data as any)[t]?.[id];
|
let v = (data as any)[t]?.[id];
|
||||||
if (v?.model === t) return v;
|
if (v?.model === t) return v;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patchModelById<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
export function patchModelById<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
model: M,
|
model: M,
|
||||||
id: string,
|
id: string,
|
||||||
patch: Partial<T> | ((prev: T) => T),
|
patch: Partial<T> | ((prev: T) => T),
|
||||||
@@ -104,54 +103,55 @@ export function patchModelById<M extends AnyModel['model'], T extends ExtractMod
|
|||||||
throw new Error(`Failed to get model to patch id=${id} model=${model}`);
|
throw new Error(`Failed to get model to patch id=${id} model=${model}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newModel = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch };
|
const newModel = typeof patch === "function" ? patch(prev) : { ...prev, ...patch };
|
||||||
return updateModel(newModel);
|
return updateModel(newModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function patchModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
export async function patchModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
base: Pick<T, 'id' | 'model'>,
|
base: Pick<T, "id" | "model">,
|
||||||
patch: Partial<T>,
|
patch: Partial<T>,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return patchModelById<M, T>(base.model, base.id, patch);
|
return patchModelById<M, T>(base.model, base.id, patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
export async function updateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
model: T,
|
model: T,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return invoke<string>('models_upsert', { model });
|
return invoke<string>("models_upsert", { model });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteModelById<
|
export async function deleteModelById<
|
||||||
M extends AnyModel['model'],
|
M extends AnyModel["model"],
|
||||||
T extends ExtractModel<AnyModel, M>,
|
T extends ExtractModel<AnyModel, M>,
|
||||||
>(modelType: M | M[], id: string) {
|
>(modelType: M | M[], id: string) {
|
||||||
let model = getModel<M, T>(modelType, id);
|
let model = getModel<M, T>(modelType, id);
|
||||||
await deleteModel(model);
|
await deleteModel(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
export async function deleteModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
model: T | null,
|
model: T | null,
|
||||||
) {
|
) {
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
throw new Error('Failed to delete null model');
|
throw new Error("Failed to delete null model");
|
||||||
}
|
}
|
||||||
await invoke<string>('models_delete', { model });
|
await invoke<string>("models_delete", { model });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function duplicateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
export function duplicateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
||||||
model: T | null,
|
model: T | null,
|
||||||
) {
|
) {
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
throw new Error('Failed to duplicate null model');
|
throw new Error("Failed to duplicate null model");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the model has a name, try to duplicate it with a name that doesn't conflict
|
// If the model has an explicit (non-empty) name, try to duplicate it with a name that doesn't conflict.
|
||||||
let name = 'name' in model ? resolvedModelName(model) : undefined;
|
// When the name is empty, keep it empty so the display falls back to the URL.
|
||||||
if (name != null) {
|
let name = "name" in model ? model.name : undefined;
|
||||||
|
if (name) {
|
||||||
const existingModels = listModels(model.model);
|
const existingModels = listModels(model.model);
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
const hasConflict = existingModels.some((m) => {
|
const hasConflict = existingModels.some((m) => {
|
||||||
if ('folderId' in m && 'folderId' in model && model.folderId !== m.folderId) {
|
if ("folderId" in m && "folderId" in model && model.folderId !== m.folderId) {
|
||||||
return false;
|
return false;
|
||||||
} else if (resolvedModelName(m) !== name) {
|
} else if (resolvedModelName(m) !== name) {
|
||||||
return false;
|
return false;
|
||||||
@@ -165,7 +165,7 @@ export function duplicateModel<M extends AnyModel['model'], T extends ExtractMod
|
|||||||
// Name conflict. Try another one
|
// Name conflict. Try another one
|
||||||
const m: RegExpMatchArray | null = name.match(/ Copy( (?<n>\d+))?$/);
|
const m: RegExpMatchArray | null = name.match(/ Copy( (?<n>\d+))?$/);
|
||||||
if (m != null && m.groups?.n == null) {
|
if (m != null && m.groups?.n == null) {
|
||||||
name = name.substring(0, m.index) + ' Copy 2';
|
name = name.substring(0, m.index) + " Copy 2";
|
||||||
} else if (m != null && m.groups?.n != null) {
|
} else if (m != null && m.groups?.n != null) {
|
||||||
name = name.substring(0, m.index) + ` Copy ${parseInt(m.groups.n) + 1}`;
|
name = name.substring(0, m.index) + ` Copy ${parseInt(m.groups.n) + 1}`;
|
||||||
} else {
|
} else {
|
||||||
@@ -174,23 +174,23 @@ export function duplicateModel<M extends AnyModel['model'], T extends ExtractMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return invoke<string>('models_duplicate', { model: { ...model, name } });
|
return invoke<string>("models_duplicate", { model: { ...model, name } });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
|
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
|
||||||
patch: Partial<T> & Pick<T, 'model'>,
|
patch: Partial<T> & Pick<T, "model">,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return invoke<string>('models_upsert', { model: patch });
|
return invoke<string>("models_upsert", { model: patch });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
|
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
|
||||||
patch: Partial<T> & Pick<T, 'model' | 'workspaceId'>,
|
patch: Partial<T> & Pick<T, "model" | "workspaceId">,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return invoke<string>('models_upsert', { model: patch });
|
return invoke<string>("models_upsert", { model: patch });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceModelsInStore<
|
export function replaceModelsInStore<
|
||||||
M extends AnyModel['model'],
|
M extends AnyModel["model"],
|
||||||
T extends Extract<AnyModel, { model: M }>,
|
T extends Extract<AnyModel, { model: M }>,
|
||||||
>(model: M, models: T[]) {
|
>(model: M, models: T[]) {
|
||||||
const newModels: Record<string, T> = {};
|
const newModels: Record<string, T> = {};
|
||||||
@@ -207,7 +207,7 @@ export function replaceModelsInStore<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function mergeModelsInStore<
|
export function mergeModelsInStore<
|
||||||
M extends AnyModel['model'],
|
M extends AnyModel["model"],
|
||||||
T extends Extract<AnyModel, { model: M }>,
|
T extends Extract<AnyModel, { model: M }>,
|
||||||
>(model: M, models: T[], filter?: (model: T) => boolean) {
|
>(model: M, models: T[], filter?: (model: T) => boolean) {
|
||||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
||||||
@@ -236,7 +236,7 @@ export function mergeModelsInStore<
|
|||||||
|
|
||||||
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
||||||
// Never ignore updates from non-user sources
|
// Never ignore updates from non-user sources
|
||||||
if (updateSource.type !== 'window') {
|
if (updateSource.type !== "window") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,11 +246,11 @@ function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only sync models that belong to this workspace, if a workspace ID is present
|
// Only sync models that belong to this workspace, if a workspace ID is present
|
||||||
if ('workspaceId' in model && model.workspaceId !== _activeWorkspaceId) {
|
if ("workspaceId" in model && model.workspaceId !== _activeWorkspaceId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.model === 'key_value' && model.namespace === 'no_sync') {
|
if (model.model === "key_value" && model.namespace === "no_sync") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createStore } from 'jotai';
|
import { createStore } from "jotai";
|
||||||
import { AnyModel } from '../bindings/gen_models';
|
import { AnyModel } from "../bindings/gen_models";
|
||||||
|
|
||||||
export type ExtractModel<T, M> = T extends { model: M } ? T : never;
|
export type ExtractModel<T, M> = T extends { model: M } ? T : never;
|
||||||
export type ModelStoreData<T extends AnyModel = AnyModel> = {
|
export type ModelStoreData<T extends AnyModel = AnyModel> = {
|
||||||
[M in T['model']]: Record<string, Extract<T, { model: M }>>;
|
[M in T["model"]]: Record<string, Extract<T, { model: M }>>;
|
||||||
};
|
};
|
||||||
export type JotaiStore = ReturnType<typeof createStore>;
|
export type JotaiStore = ReturnType<typeof createStore>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ModelStoreData } from './types';
|
import { ModelStoreData } from "./types";
|
||||||
|
|
||||||
export function newStoreData(): ModelStoreData {
|
export function newStoreData(): ModelStoreData {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/models",
|
"name": "@yaakapp-internal/models",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "guest-js/index.ts"
|
"main": "guest-js/index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1499,6 +1499,10 @@ pub enum HttpResponseEventData {
|
|||||||
url: String,
|
url: String,
|
||||||
status: u16,
|
status: u16,
|
||||||
behavior: String,
|
behavior: String,
|
||||||
|
#[serde(default)]
|
||||||
|
dropped_body: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
dropped_headers: Vec<String>,
|
||||||
},
|
},
|
||||||
SendUrl {
|
SendUrl {
|
||||||
method: String,
|
method: String,
|
||||||
|
|||||||
2
crates/yaak-plugins/bindings/gen_models.ts
generated
2
crates/yaak-plugins/bindings/gen_models.ts
generated
@@ -62,7 +62,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse } from './bindings/gen_api';
|
import { PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse } from "./bindings/gen_api";
|
||||||
|
|
||||||
export * from './bindings/gen_models';
|
export * from "./bindings/gen_models";
|
||||||
export * from './bindings/gen_events';
|
export * from "./bindings/gen_events";
|
||||||
export * from './bindings/gen_search';
|
export * from "./bindings/gen_search";
|
||||||
|
|
||||||
export async function searchPlugins(query: string) {
|
export async function searchPlugins(query: string) {
|
||||||
return invoke<PluginSearchResponse>('cmd_plugins_search', { query });
|
return invoke<PluginSearchResponse>("cmd_plugins_search", { query });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installPlugin(name: string, version: string | null) {
|
export async function installPlugin(name: string, version: string | null) {
|
||||||
return invoke<void>('cmd_plugins_install', { name, version });
|
return invoke<void>("cmd_plugins_install", { name, version });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallPlugin(pluginId: string) {
|
export async function uninstallPlugin(pluginId: string) {
|
||||||
return invoke<void>('cmd_plugins_uninstall', { pluginId });
|
return invoke<void>("cmd_plugins_uninstall", { pluginId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkPluginUpdates() {
|
export async function checkPluginUpdates() {
|
||||||
return invoke<PluginUpdatesResponse>('cmd_plugins_updates', {});
|
return invoke<PluginUpdatesResponse>("cmd_plugins_updates", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateAllPlugins() {
|
export async function updateAllPlugins() {
|
||||||
return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {});
|
return invoke<PluginNameVersion[]>("cmd_plugins_update_all", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installPluginFromDirectory(directory: string) {
|
export async function installPluginFromDirectory(directory: string) {
|
||||||
return invoke<void>('cmd_plugins_install_from_directory', { directory });
|
return invoke<void>("cmd_plugins_install_from_directory", { directory });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/plugins",
|
"name": "@yaakapp-internal/plugins",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ pub struct PluginManager {
|
|||||||
vendored_plugin_dir: PathBuf,
|
vendored_plugin_dir: PathBuf,
|
||||||
pub(crate) installed_plugin_dir: PathBuf,
|
pub(crate) installed_plugin_dir: PathBuf,
|
||||||
dev_mode: bool,
|
dev_mode: bool,
|
||||||
|
/// Errors from plugin initialization, retrievable once via `take_init_errors`.
|
||||||
|
init_errors: Arc<Mutex<Vec<(String, String)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback for plugin initialization events (e.g., toast notifications)
|
/// Callback for plugin initialization events (e.g., toast notifications)
|
||||||
@@ -93,6 +95,7 @@ impl PluginManager {
|
|||||||
vendored_plugin_dir,
|
vendored_plugin_dir,
|
||||||
installed_plugin_dir,
|
installed_plugin_dir,
|
||||||
dev_mode,
|
dev_mode,
|
||||||
|
init_errors: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward events to subscribers
|
// Forward events to subscribers
|
||||||
@@ -183,17 +186,21 @@ impl PluginManager {
|
|||||||
|
|
||||||
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
|
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
|
||||||
if !init_errors.is_empty() {
|
if !init_errors.is_empty() {
|
||||||
let joined = init_errors
|
for (dir, err) in &init_errors {
|
||||||
.into_iter()
|
warn!("Plugin failed to initialize: {dir}: {err}");
|
||||||
.map(|(dir, err)| format!("{dir}: {err}"))
|
}
|
||||||
.collect::<Vec<_>>()
|
*plugin_manager.init_errors.lock().await = init_errors;
|
||||||
.join("; ");
|
|
||||||
return Err(PluginErr(format!("Failed to initialize plugin(s): {joined}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(plugin_manager)
|
Ok(plugin_manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take any initialization errors, clearing them from the manager.
|
||||||
|
/// Returns a list of `(plugin_directory, error_message)` pairs.
|
||||||
|
pub async fn take_init_errors(&self) -> Vec<(String, String)> {
|
||||||
|
std::mem::take(&mut *self.init_errors.lock().await)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
||||||
pub fn get_plugins_dir(&self) -> PathBuf {
|
pub fn get_plugins_dir(&self) -> PathBuf {
|
||||||
if self.dev_mode {
|
if self.dev_mode {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './bindings/sse';
|
export * from "./bindings/sse";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/sse",
|
"name": "@yaakapp-internal/sse",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { Channel, invoke } from '@tauri-apps/api/core';
|
import { Channel, invoke } from "@tauri-apps/api/core";
|
||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from "@tauri-apps/api/event";
|
||||||
import type { WatchResult } from '@yaakapp-internal/tauri';
|
import type { WatchResult } from "@yaakapp-internal/tauri";
|
||||||
import { SyncOp } from './bindings/gen_sync';
|
import { SyncOp } from "./bindings/gen_sync";
|
||||||
import { WatchEvent } from './bindings/gen_watch';
|
import { WatchEvent } from "./bindings/gen_watch";
|
||||||
|
|
||||||
export * from './bindings/gen_models';
|
export * from "./bindings/gen_models";
|
||||||
|
|
||||||
export async function calculateSync(workspaceId: string, syncDir: string) {
|
export async function calculateSync(workspaceId: string, syncDir: string) {
|
||||||
return invoke<SyncOp[]>('cmd_sync_calculate', {
|
return invoke<SyncOp[]>("cmd_sync_calculate", {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
syncDir,
|
syncDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function calculateSyncFsOnly(dir: string) {
|
export async function calculateSyncFsOnly(dir: string) {
|
||||||
return invoke<SyncOp[]>('cmd_sync_calculate_fs', { dir });
|
return invoke<SyncOp[]>("cmd_sync_calculate_fs", { dir });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applySync(workspaceId: string, syncDir: string, syncOps: SyncOp[]) {
|
export async function applySync(workspaceId: string, syncDir: string, syncOps: SyncOp[]) {
|
||||||
return invoke<void>('cmd_sync_apply', {
|
return invoke<void>("cmd_sync_apply", {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
syncDir,
|
syncDir,
|
||||||
syncOps: syncOps,
|
syncOps: syncOps,
|
||||||
@@ -30,40 +30,40 @@ export function watchWorkspaceFiles(
|
|||||||
syncDir: string,
|
syncDir: string,
|
||||||
callback: (e: WatchEvent) => void,
|
callback: (e: WatchEvent) => void,
|
||||||
) {
|
) {
|
||||||
console.log('Watching workspace files', workspaceId, syncDir);
|
console.log("Watching workspace files", workspaceId, syncDir);
|
||||||
const channel = new Channel<WatchEvent>();
|
const channel = new Channel<WatchEvent>();
|
||||||
channel.onmessage = callback;
|
channel.onmessage = callback;
|
||||||
const unlistenPromise = invoke<WatchResult>('cmd_sync_watch', {
|
const unlistenPromise = invoke<WatchResult>("cmd_sync_watch", {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
syncDir,
|
syncDir,
|
||||||
channel,
|
channel,
|
||||||
});
|
});
|
||||||
|
|
||||||
unlistenPromise.then(({ unlistenEvent }) => {
|
void unlistenPromise.then(({ unlistenEvent }) => {
|
||||||
addWatchKey(unlistenEvent);
|
addWatchKey(unlistenEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
unlistenPromise
|
unlistenPromise
|
||||||
.then(async ({ unlistenEvent }) => {
|
.then(async ({ unlistenEvent }) => {
|
||||||
console.log('Unwatching workspace files', workspaceId, syncDir);
|
console.log("Unwatching workspace files", workspaceId, syncDir);
|
||||||
unlistenToWatcher(unlistenEvent);
|
unlistenToWatcher(unlistenEvent);
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unlistenToWatcher(unlistenEvent: string) {
|
function unlistenToWatcher(unlistenEvent: string) {
|
||||||
emit(unlistenEvent).then(() => {
|
void emit(unlistenEvent).then(() => {
|
||||||
removeWatchKey(unlistenEvent);
|
removeWatchKey(unlistenEvent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWatchKeys() {
|
function getWatchKeys() {
|
||||||
return sessionStorage.getItem('workspace-file-watchers')?.split(',').filter(Boolean) ?? [];
|
return sessionStorage.getItem("workspace-file-watchers")?.split(",").filter(Boolean) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWatchKeys(keys: string[]) {
|
function setWatchKeys(keys: string[]) {
|
||||||
sessionStorage.setItem('workspace-file-watchers', keys.join(','));
|
sessionStorage.setItem("workspace-file-watchers", keys.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWatchKey(key: string) {
|
function addWatchKey(key: string) {
|
||||||
@@ -79,6 +79,6 @@ function removeWatchKey(key: string) {
|
|||||||
// On page load, unlisten to all zombie watchers
|
// On page load, unlisten to all zombie watchers
|
||||||
const keys = getWatchKeys();
|
const keys = getWatchKeys();
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) {
|
||||||
console.log('Unsubscribing to zombie file watchers', keys);
|
console.log("Unsubscribing to zombie file watchers", keys);
|
||||||
keys.forEach(unlistenToWatcher);
|
keys.forEach(unlistenToWatcher);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/sync",
|
"name": "@yaakapp-internal/sync",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
const { execSync } = require('node:child_process');
|
const { execSync } = require("node:child_process");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
if (process.env.SKIP_WASM_BUILD === '1') {
|
if (process.env.SKIP_WASM_BUILD === "1") {
|
||||||
console.log('Skipping wasm-pack build (SKIP_WASM_BUILD=1)');
|
console.log("Skipping wasm-pack build (SKIP_WASM_BUILD=1)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
execSync('wasm-pack build --target bundler', { stdio: 'inherit' });
|
execSync("wasm-pack build --target bundler", { stdio: "inherit" });
|
||||||
|
|
||||||
|
// Rewrite the generated entry to use Vite's ?init import style instead of
|
||||||
|
// the ES Module Integration style that wasm-pack generates, which Vite/rolldown
|
||||||
|
// does not support in production builds.
|
||||||
|
const entry = path.join(__dirname, "pkg", "yaak_templates.js");
|
||||||
|
fs.writeFileSync(
|
||||||
|
entry,
|
||||||
|
[
|
||||||
|
'import init from "./yaak_templates_bg.wasm?init";',
|
||||||
|
'export * from "./yaak_templates_bg.js";',
|
||||||
|
'import * as bg from "./yaak_templates_bg.js";',
|
||||||
|
'const instance = await init({ "./yaak_templates_bg.js": bg });',
|
||||||
|
"bg.__wbg_set_wasm(instance.exports);",
|
||||||
|
"instance.exports.__wbindgen_start();",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './bindings/parser';
|
export * from "./bindings/parser";
|
||||||
import { Tokens } from './bindings/parser';
|
import { Tokens } from "./bindings/parser";
|
||||||
import { escape_template, parse_template, unescape_template } from './pkg';
|
import { escape_template, parse_template, unescape_template } from "./pkg";
|
||||||
|
|
||||||
export function parseTemplate(template: string) {
|
export function parseTemplate(template: string) {
|
||||||
return parse_template(template) as Tokens;
|
return parse_template(template) as Tokens;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/templates",
|
"name": "@yaakapp-internal/templates",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap": "npm run build",
|
"bootstrap": "npm run build",
|
||||||
|
|||||||
9
crates/yaak-templates/pkg/yaak_templates.js
generated
9
crates/yaak-templates/pkg/yaak_templates.js
generated
@@ -1,5 +1,6 @@
|
|||||||
import * as wasm from "./yaak_templates_bg.wasm";
|
import init from "./yaak_templates_bg.wasm?init";
|
||||||
export * from "./yaak_templates_bg.js";
|
export * from "./yaak_templates_bg.js";
|
||||||
import { __wbg_set_wasm } from "./yaak_templates_bg.js";
|
import * as bg from "./yaak_templates_bg.js";
|
||||||
__wbg_set_wasm(wasm);
|
const instance = await init({ "./yaak_templates_bg.js": bg });
|
||||||
wasm.__wbindgen_start();
|
bg.__wbg_set_wasm(instance.exports);
|
||||||
|
instance.exports.__wbindgen_start();
|
||||||
|
|||||||
BIN
crates/yaak-templates/pkg/yaak_templates_bg.wasm
generated
BIN
crates/yaak-templates/pkg/yaak_templates_bg.wasm
generated
Binary file not shown.
@@ -11,6 +11,7 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
|||||||
let mut new_json = "".to_string();
|
let mut new_json = "".to_string();
|
||||||
let mut depth = 0;
|
let mut depth = 0;
|
||||||
let mut state = FormatState::None;
|
let mut state = FormatState::None;
|
||||||
|
let mut saw_newline_in_whitespace = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let rest_of_chars = chars.clone();
|
let rest_of_chars = chars.clone();
|
||||||
@@ -61,6 +62,62 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle line comments (//)
|
||||||
|
if current_char == '/' && chars.peek() == Some(&'/') {
|
||||||
|
chars.next(); // Skip second /
|
||||||
|
// Collect the rest of the comment until newline
|
||||||
|
let mut comment = String::from("//");
|
||||||
|
loop {
|
||||||
|
match chars.peek() {
|
||||||
|
Some(&'\n') | None => break,
|
||||||
|
Some(_) => comment.push(chars.next().unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if the comma handler already added \n + indent
|
||||||
|
let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t');
|
||||||
|
if trimmed.ends_with(",\n") && !saw_newline_in_whitespace {
|
||||||
|
// Trailing comment on the same line as comma (e.g. "foo",// comment)
|
||||||
|
new_json.truncate(trimmed.len() - 1);
|
||||||
|
new_json.push(' ');
|
||||||
|
} else if !trimmed.ends_with('\n') && !new_json.is_empty() {
|
||||||
|
// Trailing comment after a value (no newline before us)
|
||||||
|
new_json.push(' ');
|
||||||
|
}
|
||||||
|
new_json.push_str(&comment);
|
||||||
|
new_json.push('\n');
|
||||||
|
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
||||||
|
saw_newline_in_whitespace = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle block comments (/* ... */)
|
||||||
|
if current_char == '/' && chars.peek() == Some(&'*') {
|
||||||
|
chars.next(); // Skip *
|
||||||
|
let mut comment = String::from("/*");
|
||||||
|
loop {
|
||||||
|
match chars.next() {
|
||||||
|
None => break,
|
||||||
|
Some('*') if chars.peek() == Some(&'/') => {
|
||||||
|
chars.next(); // Skip /
|
||||||
|
comment.push_str("*/");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(c) => comment.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we're not already on a fresh line, add newline + indent before comment
|
||||||
|
let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t');
|
||||||
|
if !trimmed.is_empty() && !trimmed.ends_with('\n') {
|
||||||
|
new_json.push('\n');
|
||||||
|
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
||||||
|
}
|
||||||
|
new_json.push_str(&comment);
|
||||||
|
// After block comment, add newline + indent for the next content
|
||||||
|
new_json.push('\n');
|
||||||
|
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
match current_char {
|
match current_char {
|
||||||
',' => {
|
',' => {
|
||||||
new_json.push(current_char);
|
new_json.push(current_char);
|
||||||
@@ -125,20 +182,37 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
|||||||
|| current_char == '\t'
|
|| current_char == '\t'
|
||||||
|| current_char == '\r'
|
|| current_char == '\r'
|
||||||
{
|
{
|
||||||
|
if current_char == '\n' {
|
||||||
|
saw_newline_in_whitespace = true;
|
||||||
|
}
|
||||||
// Don't add these
|
// Don't add these
|
||||||
} else {
|
} else {
|
||||||
|
saw_newline_in_whitespace = false;
|
||||||
new_json.push(current_char);
|
new_json.push(current_char);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace only lines containing whitespace with nothing
|
// Filter out whitespace-only lines, but preserve empty lines inside block comments
|
||||||
new_json
|
let mut result_lines: Vec<&str> = Vec::new();
|
||||||
.lines()
|
let mut in_block_comment = false;
|
||||||
.filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines
|
for line in new_json.lines() {
|
||||||
.collect::<Vec<&str>>() // Collect the non-empty lines into a vector
|
if in_block_comment {
|
||||||
.join("\n") // Join the lines back into a single string
|
result_lines.push(line);
|
||||||
|
if line.contains("*/") {
|
||||||
|
in_block_comment = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if line.contains("/*") && !line.contains("*/") {
|
||||||
|
in_block_comment = true;
|
||||||
|
}
|
||||||
|
if !line.trim().is_empty() {
|
||||||
|
result_lines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result_lines.iter().map(|line| line.trim_end()).collect::<Vec<&str>>().join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -297,6 +371,161 @@ mod tests {
|
|||||||
r#"
|
r#"
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_comment_between_keys() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(
|
||||||
|
r#"{"foo":"bar",// a comment
|
||||||
|
"baz":"qux"}"#,
|
||||||
|
" "
|
||||||
|
),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"foo": "bar", // a comment
|
||||||
|
"baz": "qux"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_comment_at_end() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(
|
||||||
|
r#"{"foo":"bar" // trailing
|
||||||
|
}"#,
|
||||||
|
" "
|
||||||
|
),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"foo": "bar" // trailing
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_block_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(r#"{"foo":"bar",/* comment */"baz":"qux"}"#, " "),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
/* comment */
|
||||||
|
"baz": "qux"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comment_in_array() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(
|
||||||
|
r#"[1,// item comment
|
||||||
|
2,3]"#,
|
||||||
|
" "
|
||||||
|
),
|
||||||
|
r#"
|
||||||
|
[
|
||||||
|
1, // item comment
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comment_only_line() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(
|
||||||
|
r#"{
|
||||||
|
// this is a standalone comment
|
||||||
|
"foo": "bar"
|
||||||
|
}"#,
|
||||||
|
" "
|
||||||
|
),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
// this is a standalone comment
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_block_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(
|
||||||
|
r#"{
|
||||||
|
"foo": "bar"
|
||||||
|
/**
|
||||||
|
Hello World!
|
||||||
|
|
||||||
|
Hi there
|
||||||
|
*/
|
||||||
|
}"#,
|
||||||
|
" "
|
||||||
|
),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
/**
|
||||||
|
Hello World!
|
||||||
|
|
||||||
|
Hi there
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: trailing whitespace on output lines is trimmed by the formatter.
|
||||||
|
// We can't easily add a test for this because raw string literals get
|
||||||
|
// trailing whitespace stripped by the editor/linter.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comment_inside_string_ignored() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(r#"{"foo":"// not a comment","bar":"/* also not */"}"#, " "),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"foo": "// not a comment",
|
||||||
|
"bar": "/* also not */"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comment_on_line_after_comma() {
|
||||||
|
assert_eq!(
|
||||||
|
format_json(
|
||||||
|
r#"{
|
||||||
|
"a": "aaa",
|
||||||
|
// "b": "bbb"
|
||||||
|
}"#,
|
||||||
|
" "
|
||||||
|
),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"a": "aaa",
|
||||||
|
// "b": "bbb"
|
||||||
|
}
|
||||||
"#
|
"#
|
||||||
.trim()
|
.trim()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod escape;
|
pub mod escape;
|
||||||
pub mod format_json;
|
pub mod format_json;
|
||||||
|
pub mod strip_json_comments;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod wasm;
|
pub mod wasm;
|
||||||
|
|||||||
318
crates/yaak-templates/src/strip_json_comments.rs
Normal file
318
crates/yaak-templates/src/strip_json_comments.rs
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
/// Strips JSON comments only if the result is valid JSON. If stripping comments
|
||||||
|
/// produces invalid JSON, the original text is returned unchanged.
|
||||||
|
pub fn maybe_strip_json_comments(text: &str) -> String {
|
||||||
|
let stripped = strip_json_comments(text);
|
||||||
|
if serde_json::from_str::<serde_json::Value>(&stripped).is_ok() {
|
||||||
|
stripped
|
||||||
|
} else {
|
||||||
|
text.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strips comments from JSONC, preserving the original formatting as much as possible.
|
||||||
|
///
|
||||||
|
/// - Trailing comments on a line are removed (along with preceding whitespace)
|
||||||
|
/// - Whole-line comments are removed, including the line itself
|
||||||
|
/// - Block comments are removed, including any lines that become empty
|
||||||
|
/// - Comments inside strings and template tags are left alone
|
||||||
|
pub fn strip_json_comments(text: &str) -> String {
|
||||||
|
let mut chars = text.chars().peekable();
|
||||||
|
let mut result = String::with_capacity(text.len());
|
||||||
|
let mut in_string = false;
|
||||||
|
let mut in_template_tag = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let current_char = match chars.next() {
|
||||||
|
None => break,
|
||||||
|
Some(c) => c,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle JSON strings
|
||||||
|
if in_string {
|
||||||
|
result.push(current_char);
|
||||||
|
match current_char {
|
||||||
|
'"' => in_string = false,
|
||||||
|
'\\' => {
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle template tags
|
||||||
|
if in_template_tag {
|
||||||
|
result.push(current_char);
|
||||||
|
if current_char == ']' && chars.peek() == Some(&'}') {
|
||||||
|
result.push(chars.next().unwrap());
|
||||||
|
in_template_tag = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for template tag start
|
||||||
|
if current_char == '$' && chars.peek() == Some(&'{') {
|
||||||
|
let mut lookahead = chars.clone();
|
||||||
|
lookahead.next(); // skip {
|
||||||
|
if lookahead.peek() == Some(&'[') {
|
||||||
|
in_template_tag = true;
|
||||||
|
result.push(current_char);
|
||||||
|
result.push(chars.next().unwrap()); // {
|
||||||
|
result.push(chars.next().unwrap()); // [
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for line comment
|
||||||
|
if current_char == '/' && chars.peek() == Some(&'/') {
|
||||||
|
chars.next(); // skip second /
|
||||||
|
// Consume until newline
|
||||||
|
loop {
|
||||||
|
match chars.peek() {
|
||||||
|
Some(&'\n') | None => break,
|
||||||
|
Some(_) => {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim trailing whitespace that preceded the comment
|
||||||
|
let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len();
|
||||||
|
result.truncate(trimmed_len);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for block comment
|
||||||
|
if current_char == '/' && chars.peek() == Some(&'*') {
|
||||||
|
chars.next(); // skip *
|
||||||
|
// Consume until */
|
||||||
|
loop {
|
||||||
|
match chars.next() {
|
||||||
|
None => break,
|
||||||
|
Some('*') if chars.peek() == Some(&'/') => {
|
||||||
|
chars.next(); // skip /
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim trailing whitespace that preceded the comment
|
||||||
|
let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len();
|
||||||
|
result.truncate(trimmed_len);
|
||||||
|
// Skip whitespace/newline after the block comment if the next line is content
|
||||||
|
// (this handles the case where the block comment is on its own line)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_char == '"' {
|
||||||
|
in_string = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(current_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove lines that are now empty (were comment-only lines)
|
||||||
|
let result = result
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
// Remove trailing commas before } or ]
|
||||||
|
strip_trailing_commas(&result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes trailing commas before closing braces/brackets, respecting strings.
|
||||||
|
fn strip_trailing_commas(text: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(text.len());
|
||||||
|
let chars: Vec<char> = text.chars().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
let mut in_string = false;
|
||||||
|
|
||||||
|
while i < chars.len() {
|
||||||
|
let ch = chars[i];
|
||||||
|
|
||||||
|
if in_string {
|
||||||
|
result.push(ch);
|
||||||
|
match ch {
|
||||||
|
'"' => in_string = false,
|
||||||
|
'\\' => {
|
||||||
|
i += 1;
|
||||||
|
if i < chars.len() {
|
||||||
|
result.push(chars[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '"' {
|
||||||
|
in_string = true;
|
||||||
|
result.push(ch);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == ',' {
|
||||||
|
// Look ahead past whitespace/newlines for } or ]
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < chars.len() && chars[j].is_whitespace() {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j < chars.len() && (chars[j] == '}' || chars[j] == ']') {
|
||||||
|
// Skip the comma
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(ch);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::strip_json_comments::strip_json_comments;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_comments() {
|
||||||
|
let input = r#"{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": 123
|
||||||
|
}"#;
|
||||||
|
assert_eq!(strip_json_comments(input), input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trailing_line_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"{
|
||||||
|
"foo": "bar", // this is a comment
|
||||||
|
"baz": 123
|
||||||
|
}"#),
|
||||||
|
r#"{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": 123
|
||||||
|
}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_whole_line_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"{
|
||||||
|
// this is a comment
|
||||||
|
"foo": "bar"
|
||||||
|
}"#),
|
||||||
|
r#"{
|
||||||
|
"foo": "bar"
|
||||||
|
}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inline_block_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"{
|
||||||
|
"foo": /* a comment */ "bar"
|
||||||
|
}"#),
|
||||||
|
r#"{
|
||||||
|
"foo": "bar"
|
||||||
|
}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_whole_line_block_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"{
|
||||||
|
/* a comment */
|
||||||
|
"foo": "bar"
|
||||||
|
}"#),
|
||||||
|
r#"{
|
||||||
|
"foo": "bar"
|
||||||
|
}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_block_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"{
|
||||||
|
/**
|
||||||
|
* Hello World!
|
||||||
|
*/
|
||||||
|
"foo": "bar"
|
||||||
|
}"#),
|
||||||
|
r#"{
|
||||||
|
"foo": "bar"
|
||||||
|
}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comment_inside_string_preserved() {
|
||||||
|
let input = r#"{
|
||||||
|
"foo": "// not a comment",
|
||||||
|
"bar": "/* also not */"
|
||||||
|
}"#;
|
||||||
|
assert_eq!(strip_json_comments(input), input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comment_inside_template_tag_preserved() {
|
||||||
|
let input = r#"{
|
||||||
|
"foo": ${[ fn("// hi", "/* hey */") ]}
|
||||||
|
}"#;
|
||||||
|
assert_eq!(strip_json_comments(input), input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_comments() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"{
|
||||||
|
// first comment
|
||||||
|
"foo": "bar", // trailing
|
||||||
|
/* block */
|
||||||
|
"baz": 123
|
||||||
|
}"#),
|
||||||
|
r#"{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": 123
|
||||||
|
}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trailing_comma_after_comment_removed() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"{
|
||||||
|
"a": "aaa",
|
||||||
|
// "b": "bbb"
|
||||||
|
}"#),
|
||||||
|
r#"{
|
||||||
|
"a": "aaa"
|
||||||
|
}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trailing_comma_in_array() {
|
||||||
|
assert_eq!(
|
||||||
|
strip_json_comments(r#"[1, 2, /* 3 */]"#),
|
||||||
|
r#"[1, 2]"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comma_inside_string_preserved() {
|
||||||
|
let input = r#"{"a": "hello,}"#;
|
||||||
|
assert_eq!(strip_json_comments(input), input);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,10 @@ url = "2"
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["macros", "time", "test-util", "rt"] }
|
tokio = { workspace = true, features = ["macros", "time", "test-util", "rt"] }
|
||||||
tokio-tungstenite = { version = "0.26.2", default-features = false, features = ["rustls-tls-native-roots", "connect"] }
|
tokio-tungstenite = { version = "0.26.2", default-features = false, features = [
|
||||||
|
"rustls-tls-native-roots",
|
||||||
|
"connect",
|
||||||
|
] }
|
||||||
yaak-http = { workspace = true }
|
yaak-http = { workspace = true }
|
||||||
yaak-tls = { workspace = true }
|
yaak-tls = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { WebsocketConnection } from '@yaakapp-internal/models';
|
import { WebsocketConnection } from "@yaakapp-internal/models";
|
||||||
|
|
||||||
export function deleteWebsocketConnections(requestId: string) {
|
export function deleteWebsocketConnections(requestId: string) {
|
||||||
return invoke('cmd_ws_delete_connections', {
|
return invoke("cmd_ws_delete_connections", {
|
||||||
requestId,
|
requestId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ export function connectWebsocket({
|
|||||||
environmentId: string | null;
|
environmentId: string | null;
|
||||||
cookieJarId: string | null;
|
cookieJarId: string | null;
|
||||||
}) {
|
}) {
|
||||||
return invoke('cmd_ws_connect', {
|
return invoke("cmd_ws_connect", {
|
||||||
requestId,
|
requestId,
|
||||||
environmentId,
|
environmentId,
|
||||||
cookieJarId,
|
cookieJarId,
|
||||||
@@ -24,7 +24,7 @@ export function connectWebsocket({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function closeWebsocket({ connectionId }: { connectionId: string }) {
|
export function closeWebsocket({ connectionId }: { connectionId: string }) {
|
||||||
return invoke('cmd_ws_close', {
|
return invoke("cmd_ws_close", {
|
||||||
connectionId,
|
connectionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ export function sendWebsocket({
|
|||||||
connectionId: string;
|
connectionId: string;
|
||||||
environmentId: string | null;
|
environmentId: string | null;
|
||||||
}) {
|
}) {
|
||||||
return invoke('cmd_ws_send', {
|
return invoke("cmd_ws_send", {
|
||||||
connectionId,
|
connectionId,
|
||||||
environmentId,
|
environmentId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/ws",
|
"name": "@yaakapp-internal/ws",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use log::info;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
use yaak_http::path_placeholders::apply_path_placeholders;
|
||||||
use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
use yaak_models::models::{Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
@@ -89,6 +89,64 @@ pub async fn render_http_request<T: TemplateCallback>(
|
|||||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() })
|
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn render_grpc_request<T: TemplateCallback>(
|
||||||
|
r: &GrpcRequest,
|
||||||
|
environment_chain: Vec<Environment>,
|
||||||
|
cb: &T,
|
||||||
|
opt: &RenderOptions,
|
||||||
|
) -> yaak_templates::error::Result<GrpcRequest> {
|
||||||
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
|
|
||||||
|
let mut metadata = Vec::new();
|
||||||
|
for p in r.metadata.clone() {
|
||||||
|
if !p.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
metadata.push(HttpRequestHeader {
|
||||||
|
enabled: p.enabled,
|
||||||
|
name: parse_and_render(p.name.as_str(), vars, cb, opt).await?,
|
||||||
|
value: parse_and_render(p.value.as_str(), vars, cb, opt).await?,
|
||||||
|
id: p.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let authentication = {
|
||||||
|
let mut disabled = false;
|
||||||
|
let mut auth = BTreeMap::new();
|
||||||
|
match r.authentication.get("disabled") {
|
||||||
|
Some(Value::Bool(true)) => {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
Some(Value::String(tmpl)) => {
|
||||||
|
disabled = parse_and_render(tmpl.as_str(), vars, cb, opt)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
info!(
|
||||||
|
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if disabled {
|
||||||
|
auth.insert("disabled".to_string(), Value::Bool(true));
|
||||||
|
} else {
|
||||||
|
for (k, v) in r.authentication.clone() {
|
||||||
|
if k == "disabled" {
|
||||||
|
auth.insert(k, Value::Bool(false));
|
||||||
|
} else {
|
||||||
|
auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = parse_and_render(r.url.as_str(), vars, cb, opt).await?;
|
||||||
|
|
||||||
|
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
|
||||||
|
}
|
||||||
|
|
||||||
fn strip_disabled_form_entries(v: Value) -> Value {
|
fn strip_disabled_form_entries(v: Value) -> Value {
|
||||||
match v {
|
match v {
|
||||||
Value::Array(items) => Value::Array(
|
Value::Array(items) => Value::Array(
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
},
|
},
|
||||||
"os": ["darwin"],
|
"os": [
|
||||||
"cpu": ["arm64"]
|
"darwin"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
},
|
},
|
||||||
"os": ["darwin"],
|
"os": [
|
||||||
"cpu": ["x64"]
|
"darwin"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
},
|
},
|
||||||
"os": ["linux"],
|
"os": [
|
||||||
"cpu": ["arm64"]
|
"linux"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
},
|
},
|
||||||
"os": ["linux"],
|
"os": [
|
||||||
"cpu": ["x64"]
|
"linux"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
},
|
},
|
||||||
"os": ["win32"],
|
"os": [
|
||||||
"cpu": ["arm64"]
|
"win32"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
},
|
},
|
||||||
"os": ["win32"],
|
"os": [
|
||||||
"cpu": ["x64"]
|
"win32"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const BINARY_DISTRIBUTION_PACKAGES = {
|
|||||||
linux_arm64: "@yaakapp/cli-linux-arm64",
|
linux_arm64: "@yaakapp/cli-linux-arm64",
|
||||||
linux_x64: "@yaakapp/cli-linux-x64",
|
linux_x64: "@yaakapp/cli-linux-x64",
|
||||||
win32_x64: "@yaakapp/cli-win32-x64",
|
win32_x64: "@yaakapp/cli-win32-x64",
|
||||||
win32_arm64: "@yaakapp/cli-win32-arm64"
|
win32_arm64: "@yaakapp/cli-win32-arm64",
|
||||||
};
|
};
|
||||||
|
|
||||||
const BINARY_DISTRIBUTION_VERSION = require("./package.json").version;
|
const BINARY_DISTRIBUTION_VERSION = require("./package.json").version;
|
||||||
@@ -16,5 +16,5 @@ module.exports = {
|
|||||||
BINARY_DISTRIBUTION_PACKAGES,
|
BINARY_DISTRIBUTION_PACKAGES,
|
||||||
BINARY_DISTRIBUTION_VERSION,
|
BINARY_DISTRIBUTION_VERSION,
|
||||||
BINARY_NAME,
|
BINARY_NAME,
|
||||||
PLATFORM_SPECIFIC_PACKAGE_NAME
|
PLATFORM_SPECIFIC_PACKAGE_NAME,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const https = require("node:https");
|
|||||||
const {
|
const {
|
||||||
BINARY_DISTRIBUTION_VERSION,
|
BINARY_DISTRIBUTION_VERSION,
|
||||||
BINARY_NAME,
|
BINARY_NAME,
|
||||||
PLATFORM_SPECIFIC_PACKAGE_NAME
|
PLATFORM_SPECIFIC_PACKAGE_NAME,
|
||||||
} = require("./common");
|
} = require("./common");
|
||||||
|
|
||||||
const fallbackBinaryPath = path.join(__dirname, BINARY_NAME);
|
const fallbackBinaryPath = path.join(__dirname, BINARY_NAME);
|
||||||
@@ -27,8 +27,8 @@ function makeRequest(url) {
|
|||||||
} else {
|
} else {
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`npm responded with status code ${response.statusCode} when downloading package ${url}`
|
`npm responded with status code ${response.statusCode} when downloading package ${url}`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp/cli",
|
"name": "@yaakapp/cli",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"main": "./index.js",
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"postinstall": "node ./install.js",
|
|
||||||
"prepublishOnly": "node ./prepublish.js"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaak": "bin/cli.js",
|
"yaak": "bin/cli.js",
|
||||||
"yaakcli": "bin/cli.js"
|
"yaakcli": "bin/cli.js"
|
||||||
},
|
},
|
||||||
|
"main": "./index.js",
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "node ./install.js",
|
||||||
|
"prepublishOnly": "node ./prepublish.js"
|
||||||
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@yaakapp/cli-darwin-x64": "0.0.1",
|
|
||||||
"@yaakapp/cli-darwin-arm64": "0.0.1",
|
"@yaakapp/cli-darwin-arm64": "0.0.1",
|
||||||
|
"@yaakapp/cli-darwin-x64": "0.0.1",
|
||||||
"@yaakapp/cli-linux-arm64": "0.0.1",
|
"@yaakapp/cli-linux-arm64": "0.0.1",
|
||||||
"@yaakapp/cli-linux-x64": "0.0.1",
|
"@yaakapp/cli-linux-x64": "0.0.1",
|
||||||
"@yaakapp/cli-win32-x64": "0.0.1",
|
"@yaakapp/cli-win32-arm64": "0.0.1",
|
||||||
"@yaakapp/cli-win32-arm64": "0.0.1"
|
"@yaakapp/cli-win32-x64": "0.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,34 +14,34 @@ const packages = [
|
|||||||
"cli-linux-arm64",
|
"cli-linux-arm64",
|
||||||
"cli-linux-x64",
|
"cli-linux-x64",
|
||||||
"cli-win32-arm64",
|
"cli-win32-arm64",
|
||||||
"cli-win32-x64"
|
"cli-win32-x64",
|
||||||
];
|
];
|
||||||
|
|
||||||
const binaries = [
|
const binaries = [
|
||||||
{
|
{
|
||||||
src: join(__dirname, "dist", "cli-darwin-arm64", "yaak"),
|
src: join(__dirname, "dist", "cli-darwin-arm64", "yaak"),
|
||||||
dest: join(__dirname, "cli-darwin-arm64", "bin", "yaak")
|
dest: join(__dirname, "cli-darwin-arm64", "bin", "yaak"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: join(__dirname, "dist", "cli-darwin-x64", "yaak"),
|
src: join(__dirname, "dist", "cli-darwin-x64", "yaak"),
|
||||||
dest: join(__dirname, "cli-darwin-x64", "bin", "yaak")
|
dest: join(__dirname, "cli-darwin-x64", "bin", "yaak"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: join(__dirname, "dist", "cli-linux-arm64", "yaak"),
|
src: join(__dirname, "dist", "cli-linux-arm64", "yaak"),
|
||||||
dest: join(__dirname, "cli-linux-arm64", "bin", "yaak")
|
dest: join(__dirname, "cli-linux-arm64", "bin", "yaak"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: join(__dirname, "dist", "cli-linux-x64", "yaak"),
|
src: join(__dirname, "dist", "cli-linux-x64", "yaak"),
|
||||||
dest: join(__dirname, "cli-linux-x64", "bin", "yaak")
|
dest: join(__dirname, "cli-linux-x64", "bin", "yaak"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: join(__dirname, "dist", "cli-win32-arm64", "yaak.exe"),
|
src: join(__dirname, "dist", "cli-win32-arm64", "yaak.exe"),
|
||||||
dest: join(__dirname, "cli-win32-arm64", "bin", "yaak.exe")
|
dest: join(__dirname, "cli-win32-arm64", "bin", "yaak.exe"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: join(__dirname, "dist", "cli-win32-x64", "yaak.exe"),
|
src: join(__dirname, "dist", "cli-win32-x64", "yaak.exe"),
|
||||||
dest: join(__dirname, "cli-win32-x64", "bin", "yaak.exe")
|
dest: join(__dirname, "cli-win32-x64", "bin", "yaak.exe"),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { src, dest } of binaries) {
|
for (const { src, dest } of binaries) {
|
||||||
@@ -67,7 +67,7 @@ for (const pkg of packages) {
|
|||||||
"@yaakapp/cli-linux-arm64": version,
|
"@yaakapp/cli-linux-arm64": version,
|
||||||
"@yaakapp/cli-linux-x64": version,
|
"@yaakapp/cli-linux-x64": version,
|
||||||
"@yaakapp/cli-win32-x64": version,
|
"@yaakapp/cli-win32-x64": version,
|
||||||
"@yaakapp/cli-win32-arm64": version
|
"@yaakapp/cli-win32-arm64": version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3622
package-lock.json
generated
3622
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "yaak-app",
|
"name": "yaak-app",
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"src-web"
|
"src-web"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "vp config",
|
||||||
"init": "npm install && npm run bootstrap",
|
"init": "npm install && npm run bootstrap",
|
||||||
"start": "npm run app-dev",
|
"start": "npm run app-dev",
|
||||||
"app-build": "tauri build",
|
"app-build": "tauri build",
|
||||||
@@ -82,34 +82,36 @@
|
|||||||
"vendor:vendor-plugins": "node scripts/vendor-plugins.cjs",
|
"vendor:vendor-plugins": "node scripts/vendor-plugins.cjs",
|
||||||
"vendor:vendor-protoc": "node scripts/vendor-protoc.cjs",
|
"vendor:vendor-protoc": "node scripts/vendor-protoc.cjs",
|
||||||
"vendor:vendor-node": "node scripts/vendor-node.cjs",
|
"vendor:vendor-node": "node scripts/vendor-node.cjs",
|
||||||
|
"format": "vp fmt --ignore-path .oxfmtignore",
|
||||||
"lint": "run-p lint:*",
|
"lint": "run-p lint:*",
|
||||||
"lint:biome": "biome lint",
|
"lint:vp": "vp lint",
|
||||||
"lint:extra": "npm run --workspaces --if-present lint",
|
"lint:workspaces": "npm run --workspaces --if-present lint",
|
||||||
"format": "biome format --write .",
|
|
||||||
"replace-version": "node scripts/replace-version.cjs",
|
"replace-version": "node scripts/replace-version.cjs",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"tauri-before-build": "npm run bootstrap",
|
"tauri-before-build": "npm run bootstrap",
|
||||||
"tauri-before-dev": "node scripts/run-workspaces-dev.mjs"
|
"tauri-before-dev": "node scripts/run-workspaces-dev.mjs"
|
||||||
},
|
},
|
||||||
"overrides": {
|
|
||||||
"js-yaml": "^4.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "^2.3.13",
|
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
|
||||||
"@yaakapp/cli": "^0.5.1",
|
|
||||||
"dotenv-cli": "^11.0.0",
|
|
||||||
"husky": "^9.1.7",
|
|
||||||
"nodejs-file-downloader": "^4.13.0",
|
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vitest": "^3.2.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-go": "^6.0.1",
|
"@codemirror/lang-go": "^6.0.1",
|
||||||
"@codemirror/lang-java": "^6.0.2",
|
"@codemirror/lang-java": "^6.0.2",
|
||||||
"@codemirror/lang-php": "^6.0.2",
|
"@codemirror/lang-php": "^6.0.2",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
"@codemirror/legacy-modes": "^6.5.2"
|
"@codemirror/legacy-modes": "^6.5.2"
|
||||||
}
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
|
"@yaakapp/cli": "^0.5.1",
|
||||||
|
"dotenv-cli": "^11.0.0",
|
||||||
|
"nodejs-file-downloader": "^4.13.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite-plus": "latest",
|
||||||
|
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
||||||
|
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
||||||
|
},
|
||||||
|
"packageManager": "npm@11.11.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
// oxlint-disable-next-line no-explicit-any
|
||||||
export function debounce(fn: (...args: any[]) => void, delay = 500) {
|
export function debounce(fn: (...args: any[]) => void, delay = 500) {
|
||||||
let timer: ReturnType<typeof setTimeout>;
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
// oxlint-disable-next-line no-explicit-any
|
||||||
const result = (...args: any[]) => {
|
const result = (...args: any[]) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = setTimeout(() => fn(...args), delay);
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ export function formatSize(bytes: number): string {
|
|||||||
|
|
||||||
if (bytes > 1000 * 1000 * 1000) {
|
if (bytes > 1000 * 1000 * 1000) {
|
||||||
num = bytes / 1000 / 1000 / 1000;
|
num = bytes / 1000 / 1000 / 1000;
|
||||||
unit = 'GB';
|
unit = "GB";
|
||||||
} else if (bytes > 1000 * 1000) {
|
} else if (bytes > 1000 * 1000) {
|
||||||
num = bytes / 1000 / 1000;
|
num = bytes / 1000 / 1000;
|
||||||
unit = 'MB';
|
unit = "MB";
|
||||||
} else if (bytes > 1000) {
|
} else if (bytes > 1000) {
|
||||||
num = bytes / 1000;
|
num = bytes / 1000;
|
||||||
unit = 'KB';
|
unit = "KB";
|
||||||
} else {
|
} else {
|
||||||
num = bytes;
|
num = bytes;
|
||||||
unit = 'B';
|
unit = "B";
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${Math.round(num * 10) / 10} ${unit}`;
|
return `${Math.round(num * 10) / 10} ${unit}`;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './debounce';
|
export * from "./debounce";
|
||||||
export * from './formatSize';
|
export * from "./formatSize";
|
||||||
export * from './templateFunction';
|
export * from "./templateFunction";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp-internal/lib",
|
"name": "@yaakapp-internal/lib",
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"main": "index.ts"
|
"main": "index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ import type {
|
|||||||
CallTemplateFunctionArgs,
|
CallTemplateFunctionArgs,
|
||||||
JsonPrimitive,
|
JsonPrimitive,
|
||||||
TemplateFunctionArg,
|
TemplateFunctionArg,
|
||||||
} from '@yaakapp-internal/plugins';
|
} from "@yaakapp-internal/plugins";
|
||||||
|
|
||||||
export function validateTemplateFunctionArgs(
|
export function validateTemplateFunctionArgs(
|
||||||
fnName: string,
|
fnName: string,
|
||||||
args: TemplateFunctionArg[],
|
args: TemplateFunctionArg[],
|
||||||
values: CallTemplateFunctionArgs['values'],
|
values: CallTemplateFunctionArgs["values"],
|
||||||
): string | null {
|
): string | null {
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
if ('inputs' in arg && arg.inputs) {
|
if ("inputs" in arg && arg.inputs) {
|
||||||
// Recurse down
|
// Recurse down
|
||||||
const err = validateTemplateFunctionArgs(fnName, arg.inputs, values);
|
const err = validateTemplateFunctionArgs(fnName, arg.inputs, values);
|
||||||
if (err) return err;
|
if (err) return err;
|
||||||
}
|
}
|
||||||
if (!('name' in arg)) continue;
|
if (!("name" in arg)) continue;
|
||||||
if (arg.optional) continue;
|
if (arg.optional) continue;
|
||||||
if (arg.defaultValue != null) continue;
|
if (arg.defaultValue != null) continue;
|
||||||
if (arg.hidden) continue;
|
if (arg.hidden) continue;
|
||||||
@@ -34,14 +34,14 @@ export function applyFormInputDefaults(
|
|||||||
) {
|
) {
|
||||||
let newValues: { [p: string]: JsonPrimitive | undefined } = { ...values };
|
let newValues: { [p: string]: JsonPrimitive | undefined } = { ...values };
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
if ('defaultValue' in input && values[input.name] === undefined) {
|
if ("defaultValue" in input && values[input.name] === undefined) {
|
||||||
newValues[input.name] = input.defaultValue;
|
newValues[input.name] = input.defaultValue;
|
||||||
}
|
}
|
||||||
if (input.type === 'checkbox' && values[input.name] === undefined) {
|
if (input.type === "checkbox" && values[input.name] === undefined) {
|
||||||
newValues[input.name] = false;
|
newValues[input.name] = false;
|
||||||
}
|
}
|
||||||
// Recurse down to all child inputs
|
// Recurse down to all child inputs
|
||||||
if ('inputs' in input) {
|
if ("inputs" in input) {
|
||||||
newValues = applyFormInputDefaults(input.inputs ?? [], newValues);
|
newValues = applyFormInputDefaults(input.inputs ?? [], newValues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,23 @@
|
|||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"api-client",
|
"api-client",
|
||||||
"insomnia-alternative",
|
|
||||||
"bruno-alternative",
|
"bruno-alternative",
|
||||||
|
"insomnia-alternative",
|
||||||
"postman-alternative"
|
"postman-alternative"
|
||||||
],
|
],
|
||||||
|
"homepage": "https://yaak.app",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://feedback.yaak.app"
|
||||||
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/mountain-loop/yaak"
|
"url": "https://github.com/mountain-loop/yaak"
|
||||||
},
|
},
|
||||||
"bugs": {
|
|
||||||
"url": "https://feedback.yaak.app"
|
|
||||||
},
|
|
||||||
"homepage": "https://yaak.app",
|
|
||||||
"main": "lib/index.js",
|
|
||||||
"typings": "./lib/index.d.ts",
|
|
||||||
"files": [
|
"files": [
|
||||||
"lib/**/*"
|
"lib/**/*"
|
||||||
],
|
],
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"typings": "./lib/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap": "npm run build",
|
"bootstrap": "npm run build",
|
||||||
"build": "run-s build:copy-types build:tsc",
|
"build": "run-s build:copy-types build:tsc",
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
|||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||||
|
/**
|
||||||
|
* Variables defined in this environment scope.
|
||||||
|
* Child environments override parent variables by name.
|
||||||
|
*/
|
||||||
|
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
@@ -34,9 +39,17 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
|
|||||||
|
|
||||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||||
|
|
||||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||||
|
/**
|
||||||
|
* Server URL (http for plaintext or https for secure)
|
||||||
|
*/
|
||||||
|
url: string, };
|
||||||
|
|
||||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||||
|
/**
|
||||||
|
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
|
*/
|
||||||
|
urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
@@ -49,17 +62,24 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||||
|
|
||||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpUrlParameter = { enabled?: boolean,
|
||||||
|
/**
|
||||||
|
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||||
|
* Other entries are appended as query parameters
|
||||||
|
*/
|
||||||
|
name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||||
|
|
||||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
||||||
|
|
||||||
|
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||||
|
|
||||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||||
|
|
||||||
@@ -77,7 +97,11 @@ export type WebsocketEvent = { model: "websocket_event", id: string, createdAt:
|
|||||||
|
|
||||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||||
|
/**
|
||||||
|
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
|
*/
|
||||||
|
urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export type * from './plugins';
|
export type * from "./plugins";
|
||||||
export type * from './themes';
|
export type * from "./themes";
|
||||||
|
|
||||||
export * from './bindings/gen_models';
|
export * from "./bindings/gen_models";
|
||||||
export * from './bindings/gen_events';
|
export * from "./bindings/gen_events";
|
||||||
|
|
||||||
// Some extras for utility
|
// Some extras for utility
|
||||||
|
|
||||||
export type { PartialImportResources } from './plugins/ImporterPlugin';
|
export type { PartialImportResources } from "./plugins/ImporterPlugin";
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type {
|
|||||||
FormInput,
|
FormInput,
|
||||||
GetHttpAuthenticationSummaryResponse,
|
GetHttpAuthenticationSummaryResponse,
|
||||||
HttpAuthenticationAction,
|
HttpAuthenticationAction,
|
||||||
} from '../bindings/gen_events';
|
} from "../bindings/gen_events";
|
||||||
import type { MaybePromise } from '../helpers';
|
import type { MaybePromise } from "../helpers";
|
||||||
import type { Context } from './Context';
|
import type { Context } from "./Context";
|
||||||
|
|
||||||
type AddDynamicMethod<T> = {
|
type AddDynamicMethod<T> = {
|
||||||
dynamic?: (
|
dynamic?: (
|
||||||
@@ -16,16 +16,16 @@ type AddDynamicMethod<T> = {
|
|||||||
) => MaybePromise<Partial<T> | null | undefined>;
|
) => MaybePromise<Partial<T> | null | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
|
// oxlint-disable-next-line no-explicit-any -- distributive conditional type pattern
|
||||||
type AddDynamic<T> = T extends any
|
type AddDynamic<T> = T extends any
|
||||||
? T extends { inputs?: FormInput[] }
|
? T extends { inputs?: FormInput[] }
|
||||||
? Omit<T, 'inputs'> & {
|
? Omit<T, "inputs"> & {
|
||||||
inputs: Array<AddDynamic<FormInput>>;
|
inputs: Array<AddDynamic<FormInput>>;
|
||||||
dynamic?: (
|
dynamic?: (
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: CallHttpAuthenticationActionArgs,
|
args: CallHttpAuthenticationActionArgs,
|
||||||
) => MaybePromise<
|
) => MaybePromise<
|
||||||
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
: T & AddDynamicMethod<T>
|
: T & AddDynamicMethod<T>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user