├── tools ├── deno.jsonc ├── encode.ts ├── common.ts └── decode.ts ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── Cargo.toml ├── LICENSE ├── tests ├── build_with_errors.rs ├── define.rs ├── basic.rs ├── metafile.rs ├── context.rs ├── common.rs └── plugin_hook.rs ├── README.md ├── src ├── flags.rs ├── protocol │ └── encode.rs ├── protocol.rs └── lib.rs └── Cargo.lock /tools/deno.jsonc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | **/.claude/settings.local.json 4 | .docs-cache 5 | .claude 6 | .vscode 7 | *.txt 8 | temp/ -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - runner: ubuntu-latest 16 | - runner: macos-13 17 | - runner: macos-latest 18 | - runner: windows-latest 19 | 20 | runs-on: ${{ matrix.runner }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - uses: taiki-e/install-action@v2 26 | with: 27 | tool: cargo-nextest 28 | - name: Install Rust toolchain 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: stable 32 | components: rustfmt, clippy 33 | override: true 34 | 35 | - name: Check formatting 36 | run: cargo fmt -- --check 37 | 38 | - name: Run Clippy 39 | run: cargo clippy --all-targets -- -D warnings 40 | 41 | - name: Run tests 42 | run: cargo nextest run -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esbuild_client" 3 | version = "0.7.1" 4 | edition = "2024" 5 | license = "MIT" 6 | description = "A Rust implementation of a client for communicating with esbuild's service API over stdio" 7 | repository = "https://github.com/denoland/esbuild_client" 8 | 9 | [dependencies] 10 | anyhow = "1.0.98" 11 | async-trait = "0.1.88" 12 | deno_unsync = "0.4.2" 13 | indexmap = { version = "2.9.0" } 14 | log = "0.4.27" 15 | parking_lot = "0.12.3" 16 | paste = "1.0.15" 17 | serde = { version = "1.0.219", features = ["derive"], optional = true } 18 | tokio = { version = "1", features = ["process", "sync", "io-util", "macros"] } 19 | 20 | [dev-dependencies] 21 | pretty_assertions = "1.4.1" 22 | directories = "6.0.0" 23 | flate2 = "1.1.1" 24 | tar = "0.4.44" 25 | ureq = "3.0.11" 26 | sys_traits = { version = "0.1.14", features = ["libc", "real", "winapi"] } 27 | tokio = { version = "1", features = ["time"] } 28 | pretty_env_logger = "0.5.0" 29 | esbuild_client = { path = ".", features = ["serde"] } 30 | serde_json = "1.0.140" 31 | pathdiff = "0.2.3" 32 | 33 | [features] 34 | default = [] 35 | serde = ["dep:serde"] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2025 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tests/build_with_errors.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::*; 3 | use esbuild_client::{EsbuildFlagsBuilder, protocol::BuildRequest}; 4 | 5 | #[tokio::test] 6 | async fn test_build_with_errors() -> Result<(), Box> { 7 | let test_dir = TestDir::new("esbuild_test_errors")?; 8 | let input_file = test_dir.create_file("input.js", "console.log('unclosed string")?; 9 | let output_file = test_dir.path.join("output.js"); 10 | 11 | let esbuild = create_esbuild_service(&test_dir).await?; 12 | 13 | let flags = EsbuildFlagsBuilder::default() 14 | .bundle(true) 15 | .build_with_defaults(); 16 | 17 | let response = esbuild 18 | .client() 19 | .send_build_request(BuildRequest { 20 | entries: vec![( 21 | output_file.to_string_lossy().into_owned(), 22 | input_file.to_string_lossy().into_owned(), 23 | )], 24 | flags, 25 | ..Default::default() 26 | }) 27 | .await? 28 | .unwrap(); 29 | 30 | // Check that build failed with errors 31 | assert!( 32 | !response.errors.is_empty(), 33 | "Expected build errors but got none" 34 | ); 35 | 36 | assert_eq!(response.errors.len(), 1); 37 | assert_eq!(response.errors[0].text, "Unterminated string literal"); 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /tests/define.rs: -------------------------------------------------------------------------------- 1 | use esbuild_client::{EsbuildFlagsBuilder, protocol::BuildRequest}; 2 | use pretty_assertions::assert_eq; 3 | mod common; 4 | 5 | use common::{TestDir, create_esbuild_service}; 6 | 7 | #[tokio::test] 8 | async fn test_basic_build() -> Result<(), Box> { 9 | let test_dir = TestDir::new("esbuild_test")?; 10 | let input_file = test_dir.create_file("input.js", "console.log(process.env.NODE_ENV);")?; 11 | 12 | let esbuild = create_esbuild_service(&test_dir).await?; 13 | 14 | let flags = EsbuildFlagsBuilder::default() 15 | .bundle(true) 16 | .minify(false) 17 | .defines([( 18 | "process.env.NODE_ENV".to_string(), 19 | "\"production\"".to_string(), 20 | )]) 21 | .build_with_defaults(); 22 | 23 | let response = esbuild 24 | .client() 25 | .send_build_request(BuildRequest { 26 | entries: vec![("".to_string(), input_file.to_string_lossy().into_owned())], 27 | write: true, 28 | flags, 29 | ..Default::default() 30 | }) 31 | .await? 32 | .unwrap(); 33 | 34 | // Check that build succeeded 35 | assert!( 36 | response.errors.is_empty(), 37 | "Build had errors: {:?}", 38 | response.errors 39 | ); 40 | assert!( 41 | response.write_to_stdout.is_some(), 42 | "No output files generated" 43 | ); 44 | let result = String::from_utf8_lossy(response.write_to_stdout.as_deref().unwrap()); 45 | 46 | let lines = result.split("\n").collect::>(); 47 | 48 | assert_eq!(lines[1..].join("\n"), "console.log(\"production\");\n"); 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /tools/encode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ByteBuffer, 3 | encodeUTF8, 4 | Packet, 5 | Value, 6 | writeUInt32LE, 7 | } from "./common.ts"; 8 | 9 | function encodeValue(value: Value, bb: ByteBuffer) { 10 | if (value === null) { 11 | bb.write8(0); 12 | } else if (typeof value === "boolean") { 13 | bb.write8(1); 14 | bb.write8(+value); 15 | } else if (typeof value === "number") { 16 | bb.write8(2); 17 | bb.write32(value | 0); 18 | } else if (typeof value === "string") { 19 | bb.write8(3); 20 | bb.write(encodeUTF8(value)); 21 | } else if (value instanceof Uint8Array) { 22 | bb.write8(4); 23 | bb.write(value); 24 | } else if (value instanceof Array) { 25 | bb.write8(5); 26 | bb.write32(value.length); 27 | for (const item of value) { 28 | encodeValue(item, bb); 29 | } 30 | } else { 31 | const keys = Object.keys(value); 32 | bb.write8(6); 33 | bb.write32(keys.length); 34 | for (const key of keys) { 35 | bb.write(encodeUTF8(key)); 36 | encodeValue(value[key], bb); 37 | } 38 | } 39 | } 40 | 41 | export function encodePacket(packet: Packet): Uint8Array { 42 | const bb = new ByteBuffer(); 43 | bb.write32(0); // Reserve space for the length 44 | bb.write32((packet.id << 1) | +!packet.isRequest); 45 | encodeValue(packet.value, bb); 46 | 47 | writeUInt32LE(bb.buf, bb.len - 4, 0); // Patch the length in 48 | const res = bb.buf.subarray(0, bb.len); 49 | return res; 50 | } 51 | 52 | const packet = { 53 | id: 0, 54 | isRequest: false, 55 | value: { 56 | errors: [], 57 | warnings: [], 58 | }, 59 | }; 60 | 61 | const encoded = encodePacket(packet); 62 | 63 | const s = Array.from(encoded).map((v) => v.toString()).join(", "); 64 | 65 | console.log(`vec![${s}]`); 66 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use esbuild_client::{EsbuildFlagsBuilder, protocol::BuildRequest}; 2 | mod common; 3 | 4 | use common::{TestDir, create_esbuild_service}; 5 | 6 | #[tokio::test] 7 | async fn test_basic_build() -> Result<(), Box> { 8 | let test_dir = TestDir::new("esbuild_test_basic")?; 9 | let input_file = test_dir.create_file("input.js", "console.log('Hello from esbuild!');")?; 10 | let output_file = test_dir.path.join("output.js"); 11 | 12 | let esbuild = create_esbuild_service(&test_dir).await?; 13 | 14 | let flags = EsbuildFlagsBuilder::default() 15 | .bundle(true) 16 | .minify(false) 17 | .build(); 18 | 19 | let response = esbuild 20 | .client() 21 | .send_build_request(BuildRequest { 22 | entries: vec![( 23 | output_file.to_string_lossy().into_owned(), 24 | input_file.to_string_lossy().into_owned(), 25 | )], 26 | flags, 27 | ..Default::default() 28 | }) 29 | .await? 30 | .unwrap(); 31 | 32 | // Check that build succeeded 33 | assert!( 34 | response.errors.is_empty(), 35 | "Build had errors: {:?}", 36 | response.errors 37 | ); 38 | assert!(response.output_files.is_some(), "No output files generated"); 39 | 40 | // Check the output files from the response instead of filesystem 41 | let output_files = response.output_files.unwrap(); 42 | assert!(!output_files.is_empty(), "No output files in response"); 43 | 44 | let output_content = String::from_utf8(output_files[0].contents.clone())?; 45 | assert!( 46 | output_content.contains("Hello from esbuild"), 47 | "Output doesn't contain expected content" 48 | ); 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /tests/metafile.rs: -------------------------------------------------------------------------------- 1 | use esbuild_client::{ 2 | EsbuildFlagsBuilder, Metafile, 3 | protocol::{BuildRequest, ImportKind}, 4 | }; 5 | mod common; 6 | 7 | use common::{TestDir, create_esbuild_service}; 8 | 9 | #[tokio::test] 10 | async fn test_basic_build() -> Result<(), Box> { 11 | let test_dir = TestDir::new("esbuild_test_metafile")?; 12 | let input_file = test_dir.create_file( 13 | "input.js", 14 | "import { foo } from './foo.js'; console.log(foo);", 15 | )?; 16 | test_dir.create_file("foo.js", "export const foo = 'foo';")?; 17 | 18 | let esbuild = create_esbuild_service(&test_dir).await?; 19 | 20 | let flags = EsbuildFlagsBuilder::default() 21 | .metafile(true) 22 | .outfile("output.js") 23 | .bundle(true) 24 | .build_with_defaults(); 25 | 26 | let response = esbuild 27 | .client() 28 | .send_build_request(BuildRequest { 29 | entries: vec![("".into(), input_file.to_string_lossy().into_owned())], 30 | flags, 31 | ..Default::default() 32 | }) 33 | .await? 34 | .unwrap(); 35 | 36 | // Check that build succeeded 37 | assert!( 38 | response.errors.is_empty(), 39 | "Build had errors: {:?}", 40 | response.errors 41 | ); 42 | assert!(response.output_files.is_some(), "No output files generated"); 43 | 44 | assert!(response.metafile.is_some()); 45 | 46 | let metafile = serde_json::from_str::(&response.metafile.unwrap()).unwrap(); 47 | assert_eq!(metafile.inputs.len(), 2); 48 | eprintln!("metafile: {metafile:?}"); 49 | let input = metafile.inputs.get("input.js").unwrap().clone(); 50 | assert!(input.bytes > 0); 51 | assert_eq!(input.imports.len(), 1); 52 | assert_eq!(input.imports[0].kind, ImportKind::ImportStatement); 53 | 54 | assert_eq!(metafile.outputs.len(), 1); 55 | let output = metafile.outputs.get("output.js").unwrap().clone(); 56 | assert!(output.inputs.contains_key("input.js")); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /tools/common.ts: -------------------------------------------------------------------------------- 1 | export interface Packet { 2 | id: number; 3 | isRequest: boolean; 4 | value: Value; 5 | } 6 | 7 | const decoder = new TextDecoder(); 8 | export type Value = 9 | | null 10 | | boolean 11 | | number 12 | | string 13 | | Uint8Array 14 | | Value[] 15 | | { [key: string]: Value }; 16 | 17 | export function writeUInt32LE( 18 | buffer: Uint8Array, 19 | value: number, 20 | offset: number, 21 | ): void { 22 | buffer[offset++] = value; 23 | buffer[offset++] = value >> 8; 24 | buffer[offset++] = value >> 16; 25 | buffer[offset++] = value >> 24; 26 | } 27 | 28 | export function readUInt32LE(buffer: Uint8Array, offset: number): number { 29 | return buffer[offset++] | 30 | (buffer[offset++] << 8) | 31 | (buffer[offset++] << 16) | 32 | (buffer[offset++] << 24); 33 | } 34 | 35 | export const decodeUTF8: (bytes: Uint8Array) => string = (bytes) => 36 | decoder.decode(bytes); 37 | 38 | const encoder = new TextEncoder(); 39 | export const encodeUTF8 = (text: string) => encoder.encode(text); 40 | 41 | export class ByteBuffer { 42 | len = 0; 43 | ptr = 0; 44 | 45 | constructor(public buf = new Uint8Array(1024)) { 46 | } 47 | 48 | private _write(delta: number): number { 49 | if (this.len + delta > this.buf.length) { 50 | const clone = new Uint8Array((this.len + delta) * 2); 51 | clone.set(this.buf); 52 | this.buf = clone; 53 | } 54 | this.len += delta; 55 | return this.len - delta; 56 | } 57 | 58 | write8(value: number): void { 59 | const offset = this._write(1); 60 | this.buf[offset] = value; 61 | } 62 | 63 | write32(value: number): void { 64 | const offset = this._write(4); 65 | writeUInt32LE(this.buf, value, offset); 66 | } 67 | 68 | write(bytes: Uint8Array): void { 69 | const offset = this._write(4 + bytes.length); 70 | writeUInt32LE(this.buf, bytes.length, offset); 71 | this.buf.set(bytes, offset + 4); 72 | } 73 | 74 | private _read(delta: number): number { 75 | if (this.ptr + delta > this.buf.length) { 76 | throw new Error("Invalid packet"); 77 | } 78 | this.ptr += delta; 79 | return this.ptr - delta; 80 | } 81 | 82 | read8(): number { 83 | return this.buf[this._read(1)]; 84 | } 85 | 86 | read32(): number { 87 | return readUInt32LE(this.buf, this._read(4)); 88 | } 89 | 90 | read(): Uint8Array { 91 | const length = this.read32(); 92 | const bytes = new Uint8Array(length); 93 | const ptr = this._read(bytes.length); 94 | bytes.set(this.buf.subarray(ptr, ptr + length)); 95 | return bytes; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tools/decode.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "node:util"; 2 | import { ByteBuffer, decodeUTF8, Packet, Value } from "./common.ts"; 3 | 4 | function decodeValue(bb: ByteBuffer): Value { 5 | const value = bb.read8(); 6 | switch (value) { 7 | case 0: // null 8 | return null; 9 | case 1: // boolean 10 | return !!bb.read8(); 11 | case 2: // number 12 | return bb.read32(); 13 | case 3: // string 14 | return decodeUTF8(bb.read()); 15 | case 4: // Uint8Array 16 | return bb.read(); 17 | case 5: { // Value[] 18 | const count = bb.read32(); 19 | const value: Value[] = []; 20 | for (let i = 0; i < count; i++) { 21 | value.push(decodeValue(bb)); 22 | } 23 | return value; 24 | } 25 | case 6: { // { [key: string]: Value } 26 | const count = bb.read32(); 27 | const value: { [key: string]: Value } = {}; 28 | for (let i = 0; i < count; i++) { 29 | value[decodeUTF8(bb.read())] = decodeValue(bb); 30 | } 31 | return value; 32 | } 33 | default: 34 | return `[ERROR ${value}]`; 35 | // throw new Error("Invalid packet"); 36 | } 37 | } 38 | 39 | export function decodePacketLengthPrefixed(bytes: Uint8Array): Packet { 40 | const bb = new ByteBuffer(bytes as any); 41 | const length = bb.read32(); 42 | const packet = decodePacketInner(bb); 43 | if (bb.ptr - 4 !== length) { 44 | throw new Error( 45 | `Invalid packet: ${bb.ptr} !== ${bytes.length};\n${ 46 | inspect({ 47 | bytes: bytes.slice(bb.ptr), 48 | }) 49 | }`, 50 | ); 51 | } 52 | return packet; 53 | } 54 | 55 | function decodePacketInner(bb: ByteBuffer): Packet { 56 | let id = bb.read32(); 57 | const isRequest = (id & 1) === 0; 58 | id >>>= 1; 59 | const value = decodeValue(bb); 60 | if (bb.ptr !== bytes.length) { 61 | throw new Error( 62 | `Invalid packet: ${bb.ptr} !== ${bytes.length};\n${ 63 | inspect({ 64 | id, 65 | isRequest, 66 | value, 67 | bytes: bytes.slice(bb.ptr), 68 | }) 69 | }`, 70 | ); 71 | } 72 | return { id, isRequest, value }; 73 | } 74 | 75 | export function decodePacket(bytes: Uint8Array): Packet { 76 | // deno-lint-ignore no-explicit-any 77 | const bb = new ByteBuffer(bytes as any); 78 | return decodePacketInner(bb); 79 | } 80 | 81 | function parseInput(input: string): Uint8Array { 82 | // [41, 0, 0, 0] 83 | input = input.trim(); 84 | input = input.replace(/[\[\]]/g, ""); 85 | const parts = input.split(","); 86 | const bytes = new Uint8Array(parts.length); 87 | for (let i = 0; i < parts.length; i++) { 88 | if (parts[i].trim() === "") { 89 | continue; 90 | } 91 | bytes[i] = parseInt(parts[i].trim()); 92 | } 93 | return bytes; 94 | } 95 | 96 | const input = Deno.args[0]; 97 | const bytes = parseInput(input); 98 | const packet = decodePacketLengthPrefixed(bytes); 99 | console.log(packet); 100 | -------------------------------------------------------------------------------- /tests/context.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | use anyhow::Error as AnyError; 5 | use common::*; 6 | use esbuild_client::{ 7 | EsbuildFlagsBuilder, OnEndArgs, OnEndResult, OnLoadArgs, OnLoadResult, OnResolveArgs, 8 | OnResolveResult, OnStartArgs, OnStartResult, PluginHandler, 9 | protocol::{BuildPlugin, BuildRequest}, 10 | }; 11 | 12 | struct ContextPluginHandler { 13 | // context: String, 14 | result: Arc>>, 15 | } 16 | 17 | #[async_trait::async_trait(?Send)] 18 | impl PluginHandler for ContextPluginHandler { 19 | async fn on_start(&self, _args: OnStartArgs) -> Result, AnyError> { 20 | Ok(None) 21 | } 22 | 23 | async fn on_end(&self, args: OnEndArgs) -> Result, AnyError> { 24 | *self.result.lock().unwrap() = Some(args); 25 | Ok(None) 26 | } 27 | 28 | async fn on_resolve(&self, _args: OnResolveArgs) -> Result, AnyError> { 29 | Ok(None) 30 | } 31 | 32 | async fn on_load(&self, _args: OnLoadArgs) -> Result, AnyError> { 33 | Ok(None) 34 | } 35 | } 36 | 37 | #[tokio::test] 38 | async fn context_simple() -> Result<(), Box> { 39 | let test_dir = TestDir::new("esbuild_test_context_simple")?; 40 | let input_file = test_dir.create_file("input.js", "console.log('Hello from esbuild!');")?; 41 | let output_file = test_dir.path.join("output.js"); 42 | 43 | let result = Arc::new(Mutex::new(None)); 44 | 45 | let esbuild = create_esbuild_service_with_plugin( 46 | &test_dir, 47 | Arc::new(ContextPluginHandler { 48 | result: result.clone(), 49 | }), 50 | ) 51 | .await?; 52 | 53 | let flags = EsbuildFlagsBuilder::default() 54 | .bundle(true) 55 | .metafile(true) 56 | .build_with_defaults(); 57 | 58 | let response = esbuild 59 | .client() 60 | .send_build_request(BuildRequest { 61 | key: 1, 62 | entries: vec![( 63 | output_file.to_string_lossy().into_owned(), 64 | input_file.to_string_lossy().into_owned(), 65 | )], 66 | flags, 67 | context: true, 68 | plugins: Some(vec![BuildPlugin { 69 | name: "plugin".to_string(), 70 | on_end: true, 71 | on_load: vec![], 72 | on_resolve: vec![], 73 | on_start: false, 74 | }]), 75 | ..Default::default() 76 | }) 77 | .await? 78 | .unwrap(); 79 | 80 | assert!(response.errors.is_empty()); 81 | 82 | let response = esbuild.client().send_rebuild_request(1).await?.unwrap(); 83 | assert!(response.errors.is_empty()); 84 | assert!(response.warnings.is_empty()); 85 | 86 | let result = result.lock().unwrap().take().unwrap(); 87 | assert!(result.output_files.is_some()); 88 | 89 | let output_files = result.output_files.unwrap(); 90 | assert!(!output_files.is_empty()); 91 | 92 | let output_content = String::from_utf8(output_files[0].contents.clone())?; 93 | assert!(output_content.contains("Hello from esbuild!")); 94 | 95 | esbuild.client().send_dispose_request(1).await?; 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esbuild_client 2 | 3 | A Rust implementation of a client for communicating with esbuild's service API 4 | over stdio. This project implements the binary protocol used by esbuild to 5 | encode and decode messages between the client and the service. 6 | 7 | ## Usage 8 | 9 | Create an `EsbuildService`, and send a build request: 10 | 11 | ```rust 12 | use esbuild_client::{EsbuildFlagsBuilder, EsbuildService, protocol::BuildRequest}; 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<(), Box> { 16 | // using a no-op plugin handler 17 | let esbuild = esbuild_client::EsbuildService::new("/path/to/esbuild/binary", "0.25.5", None); 18 | let flags = esbuild_client::EsbuildFlagsBuilder::default() 19 | .bundle(true) 20 | .minify(true) 21 | .format(esbuild::Format::Esm) 22 | .build_with_defaults(); 23 | 24 | let response = esbuild 25 | .client() 26 | .send_build_request(esbuild_client::protocol::BuildRequest { 27 | entries: vec![("output.js".into(), "input.js".into())], 28 | flags, 29 | ..Default::default() 30 | }) 31 | .await?; 32 | println!("build response: {:?}", response); 33 | 34 | Ok(()) 35 | } 36 | ``` 37 | 38 | Custom plugin handling: 39 | 40 | ```rust 41 | use esbuild_client::{EsbuildFlagsBuilder, EsbuildService, PluginHandler, protocol::BuildRequest}; 42 | 43 | struct MyPluginHandler; 44 | #[async_trait::async_trait(?Send)] 45 | impl PluginHandler for MyPluginHandler { 46 | async fn on_resolve(&self, args: OnResolveArgs) -> Result, AnyError> { 47 | println!("on_resolve: {:?}", args); 48 | Ok(None) 49 | } 50 | async fn on_load(&self, args: OnLoadArgs) -> Result, AnyError> { 51 | println!("on_load: {:?}", args); 52 | Ok(None) 53 | } 54 | async fn on_start(&self, args: OnStartArgs) -> Result, AnyError> { 55 | println!("on_start: {:?}", args); 56 | Ok(None) 57 | } 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() -> Result<(), Box> { 62 | let esbuild = esbuild_client::EsbuildService::new( 63 | "/path/to/esbuild/binary", 64 | "0.25.5", 65 | Arc::new(MyPluginHandler), 66 | ); 67 | let flags = esbuild_client::EsbuildFlagsBuilder::default() 68 | .bundle(true) 69 | .minify(true) 70 | .format(esbuild::Format::Esm) 71 | .build_with_defaults(); 72 | 73 | let response = esbuild 74 | .client() 75 | .send_build_request(esbuild_client::protocol::BuildRequest { 76 | entries: vec![("output.js".into(), "input.js".into())], 77 | flags, 78 | plugins: Some(vec![esbuild_client::protocol::BuildPlugin { 79 | name: "my-plugin".into(), 80 | on_resolve: vec![esbuild_client::protocol::OnResolveSetupOptions { 81 | id: 0, 82 | filter: ".*".into(), 83 | namespace: "my-plugin".into(), 84 | }], 85 | on_load: vec![esbuild_client::protocol::OnLoadSetupOptions { 86 | id: 0, 87 | filter: ".*".into(), 88 | namespace: "my-plugin".into(), 89 | }], 90 | on_start: true, 91 | on_end: false, 92 | }]), 93 | ..Default::default() 94 | }) 95 | .await?; 96 | println!("build response: {:?}", response); 97 | 98 | Ok(()) 99 | } 100 | 101 | ``` 102 | 103 | ## Layout 104 | 105 | ### Protocol Module (`src/protocol.rs`) 106 | 107 | Defines the data structures and types that correspond to esbuild API messages: 108 | 109 | - Structs representing various esbuild requests and responses (`BuildRequest`, 110 | `ServeRequest`, etc.) 111 | - Message serialization/deserialization logic 112 | 113 | ### Encoding Module (`src/protocol/encode.rs`) 114 | 115 | Implements the custom binary protocol encoding, based off of the implementation 116 | in `npm:esbuild`. 117 | 118 | ### EsbuildService (`src/lib.rs`) 119 | 120 | Manages the connection to the esbuild service: 121 | 122 | - Spawns and manages the esbuild service process 123 | - Handles communication via stdin/stdout 124 | - Processes messages received from the service 125 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::sync::Once; 3 | use std::{fs, io}; 4 | use sys_traits::{FsFileLock, OpenOptions}; 5 | 6 | use directories::ProjectDirs; 7 | use esbuild_client::{EsbuildService, EsbuildServiceOptions}; 8 | use sys_traits::FsOpen; 9 | use sys_traits::impls::RealSys; 10 | 11 | fn base_dir() -> PathBuf { 12 | let project_dirs = ProjectDirs::from( 13 | "esbuild_client_test", 14 | "esbuild_client_test", 15 | "esbuild_client_test", 16 | ) 17 | .unwrap(); 18 | project_dirs.cache_dir().to_path_buf() 19 | } 20 | 21 | fn npm_package_name() -> String { 22 | let platform = match (std::env::consts::ARCH, std::env::consts::OS) { 23 | ("x86_64", "linux") => "linux-x64".to_string(), 24 | ("aarch64", "linux") => "linux-arm64".to_string(), 25 | ("x86_64", "macos" | "apple") => "darwin-x64".to_string(), 26 | ("aarch64", "macos" | "apple") => "darwin-arm64".to_string(), 27 | ("x86_64", "windows") => "win32-x64".to_string(), 28 | ("aarch64", "windows") => "win32-arm64".to_string(), 29 | ("x86_64", "android") => "android-x64".to_string(), 30 | ("aarch64", "android") => "android-arm64".to_string(), 31 | _ => panic!( 32 | "Unsupported platform: {} {}", 33 | std::env::consts::ARCH, 34 | std::env::consts::OS 35 | ), 36 | }; 37 | 38 | format!("@esbuild/{}", platform) 39 | } 40 | 41 | pub const ESBUILD_VERSION: &str = "0.25.5"; 42 | 43 | fn npm_package_url() -> String { 44 | let package_name = npm_package_name(); 45 | let Some((_, platform)) = package_name.split_once('/') else { 46 | panic!("Invalid package name: {}", package_name); 47 | }; 48 | 49 | format!( 50 | "https://registry.npmjs.org/{}/-/{}-{}.tgz", 51 | package_name, platform, ESBUILD_VERSION 52 | ) 53 | } 54 | 55 | struct EsbuildFileLock { 56 | file: sys_traits::impls::RealFsFile, 57 | } 58 | 59 | impl Drop for EsbuildFileLock { 60 | fn drop(&mut self) { 61 | let _ = self.file.fs_file_unlock(); 62 | } 63 | } 64 | 65 | impl EsbuildFileLock { 66 | fn new(access_path: &Path) -> Self { 67 | let path = access_path.parent().unwrap().join(".esbuild.lock"); 68 | let mut options = OpenOptions::new_write(); 69 | options.create = true; 70 | options.read = true; 71 | let mut file = RealSys.fs_open(&path, &options).unwrap(); 72 | file.fs_file_lock(sys_traits::FsFileLockMode::Exclusive) 73 | .unwrap(); 74 | Self { file } 75 | } 76 | } 77 | 78 | pub fn fetch_esbuild() -> PathBuf { 79 | static ONCE: Once = Once::new(); 80 | ONCE.call_once(|| { 81 | pretty_env_logger::init(); 82 | }); 83 | 84 | let esbuild_bin_dir = base_dir().join("bin"); 85 | eprintln!("esbuild_bin_dir: {:?}", esbuild_bin_dir); 86 | 87 | let esbuild_bin_path = esbuild_bin_dir.join("esbuild"); 88 | eprintln!("esbuild_bin_path: {:?}", esbuild_bin_path); 89 | if esbuild_bin_path.exists() { 90 | eprintln!("esbuild_bin_path exists"); 91 | return esbuild_bin_path; 92 | } 93 | 94 | if !esbuild_bin_dir.exists() { 95 | std::fs::create_dir_all(&esbuild_bin_dir).unwrap(); 96 | } 97 | let _lock = EsbuildFileLock::new(&esbuild_bin_path); 98 | if esbuild_bin_path.exists() { 99 | eprintln!("esbuild_bin_path exists"); 100 | return esbuild_bin_path; 101 | } 102 | 103 | let esbuild_bin_url = npm_package_url(); 104 | eprintln!("fetching esbuild from: {}", esbuild_bin_url); 105 | let response = ureq::get(esbuild_bin_url).call().unwrap(); 106 | 107 | let reader = response.into_body().into_reader(); 108 | let decoder = flate2::read::GzDecoder::new(reader); 109 | let mut archive = tar::Archive::new(decoder); 110 | 111 | let want_path = if cfg!(target_os = "windows") { 112 | "package/esbuild.exe" 113 | } else { 114 | "package/bin/esbuild" 115 | }; 116 | 117 | for entry in archive.entries().unwrap() { 118 | let mut entry = entry.unwrap(); 119 | let path = entry.path().unwrap(); 120 | 121 | eprintln!("on entry: {:?}", path); 122 | if path == std::path::Path::new(want_path) { 123 | eprintln!("extracting esbuild to: {}", esbuild_bin_path.display()); 124 | std::io::copy( 125 | &mut entry, 126 | &mut std::fs::File::create(&esbuild_bin_path).unwrap(), 127 | ) 128 | .unwrap(); 129 | 130 | #[cfg(unix)] 131 | { 132 | use std::os::unix::fs::PermissionsExt; 133 | std::fs::set_permissions(&esbuild_bin_path, std::fs::Permissions::from_mode(0o755)) 134 | .unwrap(); 135 | } 136 | eprintln!("esbuild_bin_path created"); 137 | 138 | break; 139 | } 140 | } 141 | 142 | esbuild_bin_path 143 | } 144 | 145 | pub struct TestDir { 146 | pub path: PathBuf, 147 | } 148 | 149 | impl TestDir { 150 | pub fn new(name: &str) -> io::Result { 151 | let path = std::env::temp_dir().join(name); 152 | fs::create_dir_all(&path)?; 153 | Ok(TestDir { path }) 154 | } 155 | 156 | pub fn create_file(&self, name: &str, content: &str) -> io::Result { 157 | let file_path = self.path.join(name); 158 | fs::write(&file_path, content)?; 159 | Ok(file_path) 160 | } 161 | } 162 | 163 | impl Drop for TestDir { 164 | fn drop(&mut self) { 165 | let _ = fs::remove_dir_all(&self.path); 166 | } 167 | } 168 | 169 | #[allow(dead_code)] 170 | pub async fn create_esbuild_service( 171 | test_dir: &TestDir, 172 | ) -> Result> { 173 | create_esbuild_service_with_plugin(test_dir, None).await 174 | } 175 | 176 | #[allow(dead_code)] 177 | pub async fn create_esbuild_service_with_plugin( 178 | test_dir: &TestDir, 179 | plugin_handler: impl esbuild_client::MakePluginHandler, 180 | ) -> Result> { 181 | let esbuild_path = if std::env::var("ESBUILD_PATH").is_ok() { 182 | PathBuf::from(std::env::var("ESBUILD_PATH").unwrap()) 183 | } else { 184 | fetch_esbuild() 185 | }; 186 | eprintln!("fetched esbuild: {:?}", esbuild_path); 187 | Ok(EsbuildService::new( 188 | esbuild_path, 189 | ESBUILD_VERSION, 190 | plugin_handler, 191 | EsbuildServiceOptions { 192 | cwd: Some(&test_dir.path), 193 | }, 194 | ) 195 | .await?) 196 | } 197 | -------------------------------------------------------------------------------- /tests/plugin_hook.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::*; 3 | use esbuild_client::{EsbuildFlagsBuilder, OnEndArgs, OnEndResult, protocol::BuildRequest}; 4 | use pretty_assertions::assert_eq; 5 | 6 | #[tokio::test] 7 | async fn test_plugin_hook_counting() -> Result<(), Box> { 8 | use async_trait::async_trait; 9 | use esbuild_client::{ 10 | BuiltinLoader, OnLoadArgs, OnLoadResult, OnResolveArgs, OnResolveResult, OnStartArgs, 11 | OnStartResult, PluginHandler, 12 | }; 13 | use std::sync::Arc; 14 | use std::sync::atomic::{AtomicU32, Ordering}; 15 | 16 | struct CountingPluginHandler { 17 | on_start_count: AtomicU32, 18 | on_resolve_count: AtomicU32, 19 | on_load_count: AtomicU32, 20 | on_end_count: AtomicU32, 21 | } 22 | 23 | impl CountingPluginHandler { 24 | fn new() -> Self { 25 | Self { 26 | on_start_count: AtomicU32::new(0), 27 | on_resolve_count: AtomicU32::new(0), 28 | on_load_count: AtomicU32::new(0), 29 | on_end_count: AtomicU32::new(0), 30 | } 31 | } 32 | } 33 | 34 | #[async_trait(?Send)] 35 | impl PluginHandler for CountingPluginHandler { 36 | async fn on_start( 37 | &self, 38 | _args: OnStartArgs, 39 | ) -> Result, esbuild_client::AnyError> { 40 | self.on_start_count.fetch_add(1, Ordering::Relaxed); 41 | Ok(None) 42 | } 43 | 44 | async fn on_resolve( 45 | &self, 46 | args: OnResolveArgs, 47 | ) -> Result, esbuild_client::AnyError> { 48 | self.on_resolve_count.fetch_add(1, Ordering::Relaxed); 49 | 50 | // Only handle our custom virtual files 51 | if args.path.starts_with("virtual:") { 52 | let path = args.path.strip_prefix("virtual:").unwrap(); 53 | Ok(Some(OnResolveResult { 54 | path: Some(format!("virtual:{}", path)), 55 | namespace: Some("virtual".to_string()), 56 | ..Default::default() 57 | })) 58 | } else { 59 | Ok(None) 60 | } 61 | } 62 | 63 | async fn on_load( 64 | &self, 65 | args: OnLoadArgs, 66 | ) -> Result, esbuild_client::AnyError> { 67 | self.on_load_count.fetch_add(1, Ordering::Relaxed); 68 | 69 | if args.namespace == "virtual" { 70 | let content = match args.path.as_str() { 71 | "virtual:utils.js" => { 72 | r#" 73 | export function add(a, b) { 74 | return a + b; 75 | } 76 | 77 | export function multiply(a, b) { 78 | return a * b; 79 | } 80 | 81 | export const PI = 3.14159; 82 | 83 | export default function greet(name) { 84 | return `Hello, ${name}!`; 85 | } 86 | "# 87 | } 88 | "virtual:main.js" => { 89 | r#" 90 | import greet, { add, PI } from 'virtual:utils.js'; 91 | 92 | console.log(greet('World')); 93 | console.log('2 + 3 =', add(2, 3)); 94 | console.log('PI =', PI); 95 | "# 96 | } 97 | _ => return Ok(None), 98 | }; 99 | 100 | Ok(Some(OnLoadResult { 101 | contents: Some(content.as_bytes().to_vec()), 102 | loader: Some(BuiltinLoader::Js), 103 | ..Default::default() 104 | })) 105 | } else { 106 | Ok(None) 107 | } 108 | } 109 | 110 | async fn on_end( 111 | &self, 112 | _args: OnEndArgs, 113 | ) -> Result, esbuild_client::AnyError> { 114 | self.on_end_count.fetch_add(1, Ordering::Relaxed); 115 | Ok(Some(OnEndResult { 116 | errors: None, 117 | warnings: None, 118 | })) 119 | } 120 | } 121 | 122 | let test_dir = TestDir::new("esbuild_plugin_test")?; 123 | let input_file = test_dir.create_file("main.js", "import 'virtual:main.js';")?; 124 | let output_file = test_dir.path.join("output.js"); 125 | 126 | let plugin_handler = Arc::new(CountingPluginHandler::new()); 127 | 128 | let esbuild = create_esbuild_service_with_plugin(&test_dir, plugin_handler.clone()).await?; 129 | 130 | let mut flags = EsbuildFlagsBuilder::default() 131 | .bundle(true) 132 | .minify(false) 133 | .build(); 134 | 135 | // Set up plugin configuration 136 | let plugin = esbuild_client::protocol::BuildPlugin { 137 | name: "counting-plugin".to_string(), 138 | on_start: true, 139 | on_end: true, 140 | on_resolve: vec![esbuild_client::protocol::OnResolveSetupOptions { 141 | id: 1, 142 | filter: "virtual:.*".to_string(), 143 | namespace: "".to_string(), 144 | }], 145 | on_load: vec![esbuild_client::protocol::OnLoadSetupOptions { 146 | id: 2, 147 | filter: ".*".to_string(), 148 | namespace: "virtual".to_string(), 149 | }], 150 | }; 151 | 152 | flags.push("--metafile".to_string()); 153 | 154 | let response = esbuild 155 | .client() 156 | .send_build_request(BuildRequest { 157 | entries: vec![( 158 | output_file.to_string_lossy().into_owned(), 159 | input_file.to_string_lossy().into_owned(), 160 | )], 161 | flags, 162 | plugins: Some(vec![plugin]), 163 | ..Default::default() 164 | }) 165 | .await? 166 | .unwrap(); 167 | 168 | // Check that build succeeded 169 | assert!( 170 | response.errors.is_empty(), 171 | "Build had errors: {:?}", 172 | response.errors 173 | ); 174 | 175 | // Verify hook call counts 176 | let on_start_calls = plugin_handler.on_start_count.load(Ordering::Relaxed); 177 | let on_resolve_calls = plugin_handler.on_resolve_count.load(Ordering::Relaxed); 178 | let on_load_calls = plugin_handler.on_load_count.load(Ordering::Relaxed); 179 | let on_end_calls = plugin_handler.on_end_count.load(Ordering::Relaxed); 180 | 181 | println!("Hook call counts:"); 182 | println!(" on_start: {}", on_start_calls); 183 | println!(" on_resolve: {}", on_resolve_calls); 184 | println!(" on_load: {}", on_load_calls); 185 | println!(" on_end: {}", on_end_calls); 186 | 187 | // Verify that hooks were called 188 | assert_eq!(on_start_calls, 1, "on_start should be called at least once"); 189 | assert_eq!( 190 | on_resolve_calls, 2, 191 | "on_resolve should be called 2 times for virtual imports" 192 | ); 193 | assert_eq!( 194 | on_load_calls, 2, 195 | "on_load should be called 2 times for virtual files" 196 | ); 197 | // assert_eq!(on_end_calls, 1, "on_end should be called once"); 198 | 199 | // Check that the output contains expected content 200 | if let Some(output_files) = response.output_files { 201 | assert!(!output_files.is_empty(), "No output files generated"); 202 | let output_content = String::from_utf8(output_files[0].contents.clone())?; 203 | assert!( 204 | output_content.contains("Hello") || output_content.contains("add"), 205 | "Output should contain content from the virtual modules" 206 | ); 207 | } 208 | 209 | Ok(()) 210 | } 211 | -------------------------------------------------------------------------------- /src/flags.rs: -------------------------------------------------------------------------------- 1 | use crate::{BuiltinLoader, Format, LogLevel, PackagesHandling, Platform, Sourcemap}; 2 | 3 | #[derive(Default)] 4 | pub struct EsbuildFlagsBuilder { 5 | flags: Vec, 6 | set: u64, 7 | } 8 | 9 | impl EsbuildFlagsBuilder { 10 | const S_BUNDLE_TRUE: u64 = 1 << 0; 11 | const S_BUNDLE_FALSE: u64 = 1 << 1; 12 | const S_PACKAGES: u64 = 1 << 2; 13 | const S_TREE_SHAKING: u64 = 1 << 3; 14 | const S_PLATFORM: u64 = 1 << 4; 15 | const S_FORMAT: u64 = 1 << 5; 16 | 17 | #[inline] 18 | fn mark(&mut self, bit: u64) { 19 | self.set |= bit; 20 | } 21 | 22 | #[inline] 23 | fn is_set(&self, bit: u64) -> bool { 24 | (self.set & bit) != 0 25 | } 26 | 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | 31 | pub fn with_defaults(&mut self) -> &mut Self { 32 | if !self.is_set(Self::S_FORMAT) { 33 | self.flags.push(format!("--format={}", Format::Esm)); 34 | } 35 | if !self.is_set(Self::S_PLATFORM) { 36 | self.flags.push(format!("--platform={}", Platform::Node)); 37 | } 38 | let bundle_is_true = 39 | if !self.is_set(Self::S_BUNDLE_TRUE) && !self.is_set(Self::S_BUNDLE_FALSE) { 40 | self.flags.push("--bundle=true".to_string()); 41 | true 42 | } else { 43 | self.is_set(Self::S_BUNDLE_TRUE) 44 | }; 45 | 46 | if bundle_is_true { 47 | if !self.is_set(Self::S_PACKAGES) { 48 | self.flags 49 | .push(format!("--packages={}", PackagesHandling::Bundle)); 50 | } 51 | if !self.is_set(Self::S_TREE_SHAKING) { 52 | self.flags.push("--tree-shaking=true".to_string()); 53 | } 54 | } 55 | self 56 | } 57 | 58 | pub fn finish(&mut self) -> Vec { 59 | self.set = 0; 60 | std::mem::take(&mut self.flags) 61 | } 62 | 63 | pub fn build(&mut self) -> Vec { 64 | self.finish() 65 | } 66 | 67 | pub fn finish_with_defaults(&mut self) -> Vec { 68 | self.with_defaults().finish() 69 | } 70 | 71 | pub fn build_with_defaults(&mut self) -> Vec { 72 | self.with_defaults().build() 73 | } 74 | 75 | pub fn raw_flag(&mut self, flag: impl Into) -> &mut Self { 76 | self.flags.push(flag.into()); 77 | self 78 | } 79 | 80 | pub fn color(&mut self, value: bool) -> &mut Self { 81 | self.flags.push(format!("--color={}", value)); 82 | self 83 | } 84 | 85 | pub fn log_level(&mut self, level: LogLevel) -> &mut Self { 86 | self.flags.push(format!("--log-level={}", level)); 87 | self 88 | } 89 | 90 | pub fn log_limit(&mut self, limit: u32) -> &mut Self { 91 | self.flags.push(format!("--log-limit={}", limit)); 92 | self 93 | } 94 | 95 | pub fn format(&mut self, format: Format) -> &mut Self { 96 | self.flags.push(format!("--format={}", format)); 97 | self.mark(Self::S_FORMAT); 98 | self 99 | } 100 | 101 | pub fn platform(&mut self, platform: Platform) -> &mut Self { 102 | self.flags.push(format!("--platform={}", platform)); 103 | self.mark(Self::S_PLATFORM); 104 | self 105 | } 106 | 107 | pub fn tree_shaking(&mut self, value: bool) -> &mut Self { 108 | self.flags.push(format!("--tree-shaking={}", value)); 109 | self.mark(Self::S_TREE_SHAKING); 110 | self 111 | } 112 | 113 | pub fn bundle(&mut self, value: bool) -> &mut Self { 114 | self.flags.push(format!("--bundle={}", value)); 115 | if value { 116 | self.mark(Self::S_BUNDLE_TRUE); 117 | } else { 118 | self.mark(Self::S_BUNDLE_FALSE); 119 | } 120 | self 121 | } 122 | 123 | pub fn outfile(&mut self, o: impl Into) -> &mut Self { 124 | let o = o.into(); 125 | self.flags.push(format!("--outfile={}", o)); 126 | self 127 | } 128 | 129 | pub fn outdir(&mut self, o: impl Into) -> &mut Self { 130 | let o = o.into(); 131 | self.flags.push(format!("--outdir={}", o)); 132 | self 133 | } 134 | 135 | pub fn packages(&mut self, handling: PackagesHandling) -> &mut Self { 136 | self.flags.push(format!("--packages={}", handling)); 137 | self.mark(Self::S_PACKAGES); 138 | self 139 | } 140 | 141 | pub fn tsconfig(&mut self, path: impl Into) -> &mut Self { 142 | self.flags.push(format!("--tsconfig={}", path.into())); 143 | self 144 | } 145 | 146 | pub fn tsconfig_raw(&mut self, json: impl Into) -> &mut Self { 147 | self.flags.push(format!("--tsconfig-raw={}", json.into())); 148 | self 149 | } 150 | 151 | pub fn loader(&mut self, ext: impl Into, loader: BuiltinLoader) -> &mut Self { 152 | self.flags 153 | .push(format!("--loader:{}={}", ext.into(), loader)); 154 | self 155 | } 156 | 157 | pub fn loaders(&mut self, entries: I) -> &mut Self 158 | where 159 | I: IntoIterator, 160 | K: Into, 161 | { 162 | for (k, v) in entries { 163 | self.flags.push(format!("--loader:{}={}", k.into(), v)); 164 | } 165 | self 166 | } 167 | 168 | pub fn external(&mut self, spec: impl Into) -> &mut Self { 169 | self.flags.push(format!("--external:{}", spec.into())); 170 | self 171 | } 172 | 173 | pub fn externals(&mut self, specs: I) -> &mut Self 174 | where 175 | I: IntoIterator, 176 | S: Into, 177 | { 178 | for s in specs { 179 | self.flags.push(format!("--external:{}", s.into())); 180 | } 181 | self 182 | } 183 | 184 | pub fn minify(&mut self, value: bool) -> &mut Self { 185 | if value { 186 | self.flags.push("--minify".to_string()); 187 | } else { 188 | self.flags.push("--minify=false".to_string()); 189 | } 190 | self 191 | } 192 | 193 | pub fn splitting(&mut self, value: bool) -> &mut Self { 194 | if value { 195 | self.flags.push("--splitting".to_string()); 196 | } else { 197 | self.flags.push("--splitting=false".to_string()); 198 | } 199 | self 200 | } 201 | 202 | pub fn define(&mut self, key: impl Into, value: impl Into) -> &mut Self { 203 | self.flags 204 | .push(format!("--define:{}={}", key.into(), value.into())); 205 | self 206 | } 207 | 208 | pub fn defines(&mut self, entries: I) -> &mut Self 209 | where 210 | I: IntoIterator, 211 | K: Into, 212 | V: Into, 213 | { 214 | for (k, v) in entries { 215 | self.flags 216 | .push(format!("--define:{}={}", k.into(), v.into())); 217 | } 218 | self 219 | } 220 | 221 | pub fn metafile(&mut self, value: bool) -> &mut Self { 222 | if value { 223 | self.flags.push("--metafile".to_string()); 224 | } else { 225 | self.flags.push("--metafile=false".to_string()); 226 | } 227 | self 228 | } 229 | 230 | pub fn sourcemap(&mut self, kind: Sourcemap) -> &mut Self { 231 | self.flags.push(format!("--sourcemap={}", kind)); 232 | self 233 | } 234 | 235 | pub fn entry_names(&mut self, pattern: impl Into) -> &mut Self { 236 | self.flags.push(format!("--entry-names={}", pattern.into())); 237 | self 238 | } 239 | 240 | pub fn chunk_names(&mut self, pattern: impl Into) -> &mut Self { 241 | self.flags.push(format!("--chunk-names={}", pattern.into())); 242 | self 243 | } 244 | 245 | pub fn asset_names(&mut self, pattern: impl Into) -> &mut Self { 246 | self.flags.push(format!("--asset-names={}", pattern.into())); 247 | self 248 | } 249 | 250 | pub fn outbase(&mut self, base: impl Into) -> &mut Self { 251 | self.flags.push(format!("--outbase={}", base.into())); 252 | self 253 | } 254 | 255 | pub fn out_extension(&mut self, ext: impl Into, to: impl Into) -> &mut Self { 256 | self.flags 257 | .push(format!("--out-extension:{}={}", ext.into(), to.into())); 258 | self 259 | } 260 | 261 | pub fn out_extensions(&mut self, entries: I) -> &mut Self 262 | where 263 | I: IntoIterator, 264 | K: Into, 265 | V: Into, 266 | { 267 | for (k, v) in entries { 268 | self.flags 269 | .push(format!("--out-extension:{}={}", k.into(), v.into())); 270 | } 271 | self 272 | } 273 | 274 | pub fn public_path(&mut self, path: impl Into) -> &mut Self { 275 | self.flags.push(format!("--public-path={}", path.into())); 276 | self 277 | } 278 | 279 | pub fn condition(&mut self, cond: impl Into) -> &mut Self { 280 | self.flags.push(format!("--conditions={}", cond.into())); 281 | self 282 | } 283 | 284 | pub fn conditions(&mut self, conds: I) -> &mut Self 285 | where 286 | I: IntoIterator, 287 | S: Into, 288 | { 289 | for c in conds { 290 | self.flags.push(format!("--conditions={}", c.into())); 291 | } 292 | self 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/protocol/encode.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use indexmap::IndexMap; 4 | 5 | use super::{ 6 | AnyRequest, AnyResponse, AnyValue, BuildRequest, ImportKind, MangleCacheEntry, Packet, 7 | ProtocolMessage, ProtocolPacket, 8 | }; 9 | 10 | #[macro_export] 11 | macro_rules! count_idents { 12 | () => {0}; 13 | ($last_ident:ident, $($idents:ident),* $(,)?) => { 14 | { 15 | #[allow(dead_code, non_camel_case_types)] 16 | enum Idents { $($idents,)* $last_ident } 17 | const COUNT: u32 = Idents::$last_ident as u32 + 1; 18 | COUNT 19 | } 20 | }; 21 | ($last_ident: ident) => { 22 | 1 23 | }; 24 | } 25 | 26 | pub fn snake_to_camel(s: &str) -> String { 27 | let mut result = String::new(); 28 | let mut capitalize = false; 29 | 30 | for c in s.chars() { 31 | if c == '_' { 32 | capitalize = true; 33 | } else if capitalize { 34 | result.push(c.to_ascii_uppercase()); 35 | capitalize = false; 36 | } else { 37 | result.push(c); 38 | } 39 | } 40 | result 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! delegate { 45 | ($buf: ident, $self: ident; $($field: ident),*) => { 46 | $( 47 | paste::paste! { 48 | if $self.$field.should_encode() { 49 | encode_key($buf, &$crate::protocol::encode::snake_to_camel(stringify!($field))); 50 | $self.$field.encode_into($buf); 51 | } 52 | } 53 | )* 54 | }; 55 | } 56 | 57 | #[macro_export] 58 | macro_rules! impl_encode_command { 59 | (for $name: ident { const Command = $command_name: literal; 60 | $($field: ident),* 61 | }) => { 62 | impl Encode for $name { 63 | fn encode_into(&self, buf: &mut Vec) { 64 | buf.push(6); // discrim 65 | let dont_count = { 66 | $(!self.$field.should_encode() as u32 +)* 0 67 | }; 68 | encode_u32_raw(buf, 1 + $crate::count_idents!($($field),*) - dont_count); // num fields 69 | encode_key(buf, "command"); 70 | $command_name.encode_into(buf); 71 | $crate::delegate!(buf, self; $($field),*); 72 | } 73 | } 74 | }; 75 | } 76 | 77 | #[macro_export] 78 | macro_rules! impl_encode_struct { 79 | (for $name: ty { 80 | $($field: ident),* 81 | }) => { 82 | impl Encode for $name { 83 | fn encode_into(&self, buf: &mut Vec) { 84 | buf.push(6); // discrim 85 | let dont_count = { 86 | $(!self.$field.should_encode() as u32 +)* 0 87 | }; 88 | encode_u32_raw(buf, $crate::count_idents!($($field),*) - dont_count); // num fields 89 | $crate::delegate!(buf, self; $($field),*); 90 | } 91 | } 92 | }; 93 | } 94 | 95 | pub trait Encode { 96 | fn encode_into(&self, buf: &mut Vec); 97 | 98 | fn should_encode(&self) -> bool { 99 | true 100 | } 101 | } 102 | 103 | pub fn encode_length_delimited(buf: &mut Vec, value: &[u8]) { 104 | encode_u32_raw(buf, value.len() as u32); 105 | buf.extend_from_slice(value); 106 | } 107 | 108 | pub fn encode_key(buf: &mut Vec, key: &str) { 109 | encode_length_delimited(buf, key.as_bytes()); 110 | } 111 | 112 | impl Encode for super::OptionNull { 113 | fn encode_into(&self, buf: &mut Vec) { 114 | self.0.encode_into(buf); 115 | } 116 | fn should_encode(&self) -> bool { 117 | true 118 | } 119 | } 120 | 121 | impl Encode for Option { 122 | fn encode_into(&self, buf: &mut Vec) { 123 | if let Some(value) = self { 124 | value.encode_into(buf); 125 | } else { 126 | buf.push(0); 127 | } 128 | } 129 | 130 | fn should_encode(&self) -> bool { 131 | self.is_some() 132 | } 133 | } 134 | 135 | impl Encode for bool { 136 | fn encode_into(&self, buf: &mut Vec) { 137 | buf.push(1); 138 | buf.push(*self as u8); 139 | } 140 | } 141 | 142 | impl Encode for u32 { 143 | fn encode_into(&self, buf: &mut Vec) { 144 | buf.push(2); 145 | encode_u32_raw(buf, *self); 146 | } 147 | } 148 | 149 | impl Encode for str { 150 | fn encode_into(&self, buf: &mut Vec) { 151 | buf.push(3); 152 | encode_length_delimited(buf, self.as_bytes()); 153 | } 154 | } 155 | impl Encode for String { 156 | fn encode_into(&self, buf: &mut Vec) { 157 | self.as_str().encode_into(buf); 158 | } 159 | } 160 | 161 | impl Encode for [u8] { 162 | fn encode_into(&self, buf: &mut Vec) { 163 | buf.push(4); 164 | encode_length_delimited(buf, self); 165 | } 166 | } 167 | 168 | impl Encode for Vec { 169 | fn encode_into(&self, buf: &mut Vec) { 170 | buf.push(4); 171 | encode_length_delimited(buf, self); 172 | } 173 | } 174 | 175 | impl Encode for Vec { 176 | fn encode_into(&self, buf: &mut Vec) { 177 | buf.push(5); 178 | encode_u32_raw(buf, self.len() as u32); 179 | for item in self { 180 | item.encode_into(buf); 181 | } 182 | } 183 | } 184 | 185 | impl Encode for (T, T) { 186 | fn encode_into(&self, buf: &mut Vec) { 187 | buf.push(5); 188 | encode_u32_raw(buf, 2); 189 | self.0.encode_into(buf); 190 | self.1.encode_into(buf); 191 | } 192 | } 193 | 194 | impl Encode for HashMap { 195 | fn encode_into(&self, buf: &mut Vec) { 196 | buf.push(6); 197 | encode_u32_raw(buf, self.len() as u32); 198 | for (key, value) in self { 199 | encode_key(buf, key); 200 | value.encode_into(buf); 201 | } 202 | } 203 | } 204 | 205 | impl Encode for IndexMap { 206 | fn encode_into(&self, buf: &mut Vec) { 207 | buf.push(6); 208 | encode_u32_raw(buf, self.len() as u32); 209 | for (key, value) in self { 210 | encode_key(buf, key); 211 | value.encode_into(buf); 212 | } 213 | } 214 | } 215 | 216 | pub fn encode_u32_raw(buf: &mut Vec, value: u32) { 217 | buf.extend(value.to_le_bytes()); 218 | } 219 | 220 | impl Encode for ProtocolMessage { 221 | fn encode_into(&self, buf: &mut Vec) { 222 | match self { 223 | ProtocolMessage::Request(req) => req.encode_into(buf), 224 | ProtocolMessage::Response(res) => res.encode_into(buf), 225 | } 226 | } 227 | } 228 | 229 | impl Encode for AnyRequest { 230 | fn encode_into(&self, buf: &mut Vec) { 231 | match self { 232 | AnyRequest::Build(build) => build.encode_into(buf), 233 | AnyRequest::Dispose(dispose) => dispose.encode_into(buf), 234 | AnyRequest::Rebuild(rebuild) => rebuild.encode_into(buf), 235 | // AnyRequest::Import(import) => import.encode_into(buf), 236 | AnyRequest::Cancel(cancel) => cancel.encode_into(buf), 237 | } 238 | } 239 | } 240 | 241 | impl Encode for AnyResponse { 242 | #[allow(unused)] 243 | fn encode_into(&self, buf: &mut Vec) { 244 | match self { 245 | AnyResponse::Build(build_response) => todo!(), 246 | AnyResponse::Serve(serve_response) => todo!(), 247 | AnyResponse::OnEnd(on_end_response) => on_end_response.encode_into(buf), 248 | AnyResponse::Rebuild(rebuild_response) => rebuild_response.encode_into(buf), 249 | AnyResponse::Transform(transform_response) => todo!(), 250 | AnyResponse::FormatMsgs(format_msgs_response) => todo!(), 251 | AnyResponse::AnalyzeMetafile(analyze_metafile_response) => todo!(), 252 | AnyResponse::OnStart(on_start_response) => on_start_response.encode_into(buf), 253 | AnyResponse::Resolve(resolve_response) => todo!(), 254 | AnyResponse::OnResolve(on_resolve_response) => on_resolve_response.encode_into(buf), 255 | AnyResponse::OnLoad(on_load_response) => on_load_response.encode_into(buf), 256 | AnyResponse::Ping(ping_response) => ping_response.encode_into(buf), 257 | } 258 | } 259 | } 260 | 261 | impl Encode for ProtocolPacket { 262 | fn encode_into(&self, buf: &mut Vec) { 263 | let idx = buf.len(); 264 | log::trace!("encoding packet: {self:?}"); 265 | buf.extend(std::iter::repeat_n(0, 4)); 266 | // eprintln!("tag: {}", (self.id << 1) | !self.is_request as u32); 267 | // eprintln!("len: {}", buf.len()); 268 | encode_u32_raw(buf, (self.id << 1) | !self.is_request as u32); 269 | // eprintln!("encoding packet: {buf:?}"); 270 | log::trace!("encoding value: {self:?}"); 271 | self.value.encode_into(buf); 272 | log::trace!("encoded value: {self:?}"); 273 | let end: u32 = buf.len() as u32; 274 | let len: u32 = end - (idx as u32) - 4; 275 | // eprintln!("len: {len}"); 276 | buf[idx..idx + 4].copy_from_slice(&len.to_le_bytes()); 277 | } 278 | } 279 | 280 | impl Encode for Packet { 281 | fn encode_into(&self, buf: &mut Vec) { 282 | let idx = buf.len(); 283 | buf.extend(std::iter::repeat_n(0, 4)); 284 | // eprintln!("tag: {}", (self.id << 1) | !self.is_request as u32); 285 | // eprintln!("len: {}", buf.len()); 286 | encode_u32_raw(buf, (self.id << 1) | !self.is_request as u32); 287 | // eprintln!("encoding packet: {buf:?}"); 288 | self.value.encode_into(buf); 289 | let end: u32 = buf.len() as u32; 290 | let len: u32 = end - (idx as u32) - 4; 291 | // eprintln!("len: {len}"); 292 | buf[idx..idx + 4].copy_from_slice(&len.to_le_bytes()); 293 | } 294 | } 295 | 296 | impl_encode_command!(for BuildRequest { 297 | const Command = "build"; 298 | key, entries, flags, write, stdin_contents, stdin_resolve_dir, abs_working_dir, node_paths, context, plugins, mangle_cache 299 | }); 300 | 301 | impl Encode for ImportKind { 302 | fn encode_into(&self, buf: &mut Vec) { 303 | match self { 304 | ImportKind::EntryPoint => "entry_point".encode_into(buf), 305 | ImportKind::ImportStatement => "import_statement".encode_into(buf), 306 | ImportKind::RequireCall => "require_call".encode_into(buf), 307 | ImportKind::DynamicImport => "dynamic_import".encode_into(buf), 308 | ImportKind::RequireResolve => "require_resolve".encode_into(buf), 309 | ImportKind::ImportRule => "import_rule".encode_into(buf), 310 | ImportKind::ComposesFrom => "composes_from".encode_into(buf), 311 | ImportKind::UrlToken => "url_token".encode_into(buf), 312 | } 313 | } 314 | } 315 | 316 | impl Encode for MangleCacheEntry { 317 | fn encode_into(&self, buf: &mut Vec) { 318 | match self { 319 | MangleCacheEntry::StringValue(s) => { 320 | s.encode_into(buf); 321 | } 322 | MangleCacheEntry::BoolValue(b) => { 323 | b.encode_into(buf); 324 | } 325 | } 326 | } 327 | } 328 | 329 | impl Encode for AnyValue { 330 | fn encode_into(&self, buf: &mut Vec) { 331 | match self { 332 | AnyValue::Null => { 333 | buf.push(0); 334 | } 335 | AnyValue::Bool(b) => b.encode_into(buf), 336 | AnyValue::U32(n) => n.encode_into(buf), 337 | AnyValue::String(s) => s.encode_into(buf), 338 | AnyValue::Bytes(items) => items.encode_into(buf), 339 | AnyValue::Vec(any_values) => any_values.encode_into(buf), 340 | AnyValue::Map(hash_map) => hash_map.encode_into(buf), 341 | } 342 | } 343 | } 344 | 345 | #[cfg(test)] 346 | mod tests { 347 | use super::super::*; 348 | use super::*; 349 | use pretty_assertions::assert_eq; 350 | 351 | #[test] 352 | fn encode_protocol_packet() { 353 | let packet = ProtocolPacket { 354 | id: 0, 355 | is_request: false, 356 | value: ProtocolMessage::Response(AnyResponse::OnStart(OnStartResponse { 357 | errors: vec![], 358 | warnings: vec![], 359 | })), 360 | }; 361 | let mut buf = Vec::new(); 362 | packet.encode_into(&mut buf); 363 | assert_eq!( 364 | buf, 365 | vec![ 366 | 41, 0, 0, 0, 1, 0, 0, 0, 6, 2, 0, 0, 0, 6, 0, 0, 0, 101, 114, 114, 111, 114, 115, 367 | 5, 0, 0, 0, 0, 8, 0, 0, 0, 119, 97, 114, 110, 105, 110, 103, 115, 5, 0, 0, 0, 0 368 | ] 369 | ); 370 | } 371 | #[test] 372 | fn test_decode_packet_build() { 373 | let input = Packet { 374 | id: 0, 375 | is_request: true, 376 | value: BuildRequest { 377 | key: 0, 378 | entries: vec![("".to_string(), "./testing.ts".to_string())], 379 | flags: vec![ 380 | "--color=true".to_string(), 381 | "--log-level=warning".to_string(), 382 | "--log-limit=0".to_string(), 383 | "--format=esm".to_string(), 384 | "--platform=node".to_string(), 385 | "--tree-shaking=true".to_string(), 386 | "--bundle".to_string(), 387 | "--outfile=./temp/mod.js".to_string(), 388 | "--packages=bundle".to_string(), 389 | ], 390 | write: true, 391 | stdin_contents: OptionNull::new(None), 392 | stdin_resolve_dir: OptionNull::new(None), 393 | abs_working_dir: "/Users/nathanwhit/Documents/Code/esbuild-at-home".to_string(), 394 | node_paths: vec![], 395 | context: false, 396 | plugins: Some(vec![ 397 | BuildPlugin { 398 | name: "deno".to_string(), 399 | on_start: false, 400 | on_end: false, 401 | on_resolve: vec![OnResolveSetupOptions { 402 | id: 0, 403 | filter: ".*".to_string(), 404 | namespace: "deno".to_string(), 405 | }], 406 | on_load: vec![OnLoadSetupOptions { 407 | id: 0, 408 | filter: ".*".to_string(), 409 | namespace: "deno".to_string(), 410 | }], 411 | }, 412 | BuildPlugin { 413 | name: "test".to_string(), 414 | on_start: false, 415 | on_end: false, 416 | on_resolve: vec![OnResolveSetupOptions { 417 | id: 1, 418 | filter: ".*$".to_string(), 419 | namespace: "".to_string(), 420 | }], 421 | on_load: vec![OnLoadSetupOptions { 422 | id: 2, 423 | filter: ".*$".to_string(), 424 | namespace: "".to_string(), 425 | }], 426 | }, 427 | ]), 428 | mangle_cache: None, 429 | }, 430 | }; 431 | let mut buf = Vec::new(); 432 | input.encode_into(&mut buf); 433 | eprintln!("buf: {:?}", buf); 434 | assert_eq!( 435 | buf, 436 | vec![ 437 | 52, 3, 0, 0, 0, 0, 0, 0, 6, 11, 0, 0, 0, 7, 0, 0, 0, 99, 111, 109, 109, 97, 110, 438 | 100, 3, 5, 0, 0, 0, 98, 117, 105, 108, 100, 3, 0, 0, 0, 107, 101, 121, 2, 0, 0, 0, 439 | 0, 7, 0, 0, 0, 101, 110, 116, 114, 105, 101, 115, 5, 1, 0, 0, 0, 5, 2, 0, 0, 0, 3, 440 | 0, 0, 0, 0, 3, 12, 0, 0, 0, 46, 47, 116, 101, 115, 116, 105, 110, 103, 46, 116, 441 | 115, 5, 0, 0, 0, 102, 108, 97, 103, 115, 5, 9, 0, 0, 0, 3, 12, 0, 0, 0, 45, 45, 99, 442 | 111, 108, 111, 114, 61, 116, 114, 117, 101, 3, 19, 0, 0, 0, 45, 45, 108, 111, 103, 443 | 45, 108, 101, 118, 101, 108, 61, 119, 97, 114, 110, 105, 110, 103, 3, 13, 0, 0, 0, 444 | 45, 45, 108, 111, 103, 45, 108, 105, 109, 105, 116, 61, 48, 3, 12, 0, 0, 0, 45, 45, 445 | 102, 111, 114, 109, 97, 116, 61, 101, 115, 109, 3, 15, 0, 0, 0, 45, 45, 112, 108, 446 | 97, 116, 102, 111, 114, 109, 61, 110, 111, 100, 101, 3, 19, 0, 0, 0, 45, 45, 116, 447 | 114, 101, 101, 45, 115, 104, 97, 107, 105, 110, 103, 61, 116, 114, 117, 101, 3, 8, 448 | 0, 0, 0, 45, 45, 98, 117, 110, 100, 108, 101, 3, 23, 0, 0, 0, 45, 45, 111, 117, 449 | 116, 102, 105, 108, 101, 61, 46, 47, 116, 101, 109, 112, 47, 109, 111, 100, 46, 450 | 106, 115, 3, 17, 0, 0, 0, 45, 45, 112, 97, 99, 107, 97, 103, 101, 115, 61, 98, 117, 451 | 110, 100, 108, 101, 5, 0, 0, 0, 119, 114, 105, 116, 101, 1, 1, 13, 0, 0, 0, 115, 452 | 116, 100, 105, 110, 67, 111, 110, 116, 101, 110, 116, 115, 0, 15, 0, 0, 0, 115, 453 | 116, 100, 105, 110, 82, 101, 115, 111, 108, 118, 101, 68, 105, 114, 0, 13, 0, 0, 0, 454 | 97, 98, 115, 87, 111, 114, 107, 105, 110, 103, 68, 105, 114, 3, 48, 0, 0, 0, 47, 455 | 85, 115, 101, 114, 115, 47, 110, 97, 116, 104, 97, 110, 119, 104, 105, 116, 47, 68, 456 | 111, 99, 117, 109, 101, 110, 116, 115, 47, 67, 111, 100, 101, 47, 101, 115, 98, 457 | 117, 105, 108, 100, 45, 97, 116, 45, 104, 111, 109, 101, 9, 0, 0, 0, 110, 111, 100, 458 | 101, 80, 97, 116, 104, 115, 5, 0, 0, 0, 0, 7, 0, 0, 0, 99, 111, 110, 116, 101, 120, 459 | 116, 1, 0, 7, 0, 0, 0, 112, 108, 117, 103, 105, 110, 115, 5, 2, 0, 0, 0, 6, 5, 0, 460 | 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 3, 4, 0, 0, 0, 100, 101, 110, 111, 7, 0, 0, 0, 461 | 111, 110, 83, 116, 97, 114, 116, 1, 0, 5, 0, 0, 0, 111, 110, 69, 110, 100, 1, 0, 9, 462 | 0, 0, 0, 111, 110, 82, 101, 115, 111, 108, 118, 101, 5, 1, 0, 0, 0, 6, 3, 0, 0, 0, 463 | 2, 0, 0, 0, 105, 100, 2, 0, 0, 0, 0, 6, 0, 0, 0, 102, 105, 108, 116, 101, 114, 3, 464 | 2, 0, 0, 0, 46, 42, 9, 0, 0, 0, 110, 97, 109, 101, 115, 112, 97, 99, 101, 3, 4, 0, 465 | 0, 0, 100, 101, 110, 111, 6, 0, 0, 0, 111, 110, 76, 111, 97, 100, 5, 1, 0, 0, 0, 6, 466 | 3, 0, 0, 0, 2, 0, 0, 0, 105, 100, 2, 0, 0, 0, 0, 6, 0, 0, 0, 102, 105, 108, 116, 467 | 101, 114, 3, 2, 0, 0, 0, 46, 42, 9, 0, 0, 0, 110, 97, 109, 101, 115, 112, 97, 99, 468 | 101, 3, 4, 0, 0, 0, 100, 101, 110, 111, 6, 5, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 469 | 101, 3, 4, 0, 0, 0, 116, 101, 115, 116, 7, 0, 0, 0, 111, 110, 83, 116, 97, 114, 470 | 116, 1, 0, 5, 0, 0, 0, 111, 110, 69, 110, 100, 1, 0, 9, 0, 0, 0, 111, 110, 82, 101, 471 | 115, 111, 108, 118, 101, 5, 1, 0, 0, 0, 6, 3, 0, 0, 0, 2, 0, 0, 0, 105, 100, 2, 1, 472 | 0, 0, 0, 6, 0, 0, 0, 102, 105, 108, 116, 101, 114, 3, 3, 0, 0, 0, 46, 42, 36, 9, 0, 473 | 0, 0, 110, 97, 109, 101, 115, 112, 97, 99, 101, 3, 0, 0, 0, 0, 6, 0, 0, 0, 111, 474 | 110, 76, 111, 97, 100, 5, 1, 0, 0, 0, 6, 3, 0, 0, 0, 2, 0, 0, 0, 105, 100, 2, 2, 0, 475 | 0, 0, 6, 0, 0, 0, 102, 105, 108, 116, 101, 114, 3, 3, 0, 0, 0, 46, 42, 36, 9, 0, 0, 476 | 0, 110, 97, 109, 101, 115, 112, 97, 99, 101, 3, 0, 0, 0, 0 477 | ] 478 | ); 479 | } 480 | 481 | #[test] 482 | fn test_encode_packet() { 483 | let v = vec![ 484 | ("a".into(), AnyValue::String("b".into())), 485 | ("c".into(), AnyValue::String("d".into())), 486 | ("e".into(), AnyValue::Bytes(vec![1u8, 2, 3, 4])), 487 | ] 488 | .into_iter() 489 | .collect::>(); 490 | let mut buf = Vec::new(); 491 | let packet = Packet { 492 | id: 1, 493 | is_request: true, 494 | value: AnyValue::Map(v), 495 | }; 496 | packet.encode_into(&mut buf); 497 | 498 | eprintln!("buf: {:?}", buf); 499 | // assert_eq!( 500 | // buf, 501 | // vec![ 502 | // 45, 0, 0, 0, 0, 0, 0, 0, 6, 3, 0, 0, 0, 1, 0, 0, 0, 97, 3, 1, 0, 0, 0, 98, 1, 0, 0, 0, 503 | // 99, 3, 1, 0, 0, 0, 100, 1, 0, 0, 0, 101, 4, 4, 0, 0, 0, 1, 2, 3, 4 504 | // ] 505 | // ); 506 | assert_eq!( 507 | buf, 508 | vec![ 509 | 45, 0, 0, 0, 2, 0, 0, 0, 6, 3, 0, 0, 0, 1, 0, 0, 0, 97, 3, 1, 0, 0, 0, 98, 1, 0, 0, 510 | 0, 99, 3, 1, 0, 0, 0, 100, 1, 0, 0, 0, 101, 4, 4, 0, 0, 0, 1, 2, 3, 4 511 | ] 512 | ); 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | pub mod encode; 2 | 3 | use anyhow::Context; 4 | use std::{fmt::Debug, hash::Hash}; 5 | use tokio::sync::oneshot; 6 | 7 | use crate::{impl_encode_command, impl_encode_struct}; 8 | #[allow(unused_imports)] 9 | use encode::encode_length_delimited; 10 | pub use encode::{Encode, encode_key, encode_u32_raw}; 11 | use indexmap::IndexMap; 12 | 13 | macro_rules! get { 14 | ($self:expr, $key:expr) => { 15 | $self 16 | .get($key) 17 | .ok_or_else(|| anyhow::anyhow!("Missing field: {}", $key)) 18 | .and_then(|v| v.clone().to_type()) 19 | .context(format!("on key: {}", $key)) 20 | }; 21 | } 22 | 23 | macro_rules! impl_from_map { 24 | (for $t: ty { $( $(#[$optional:ident])? $field: ident),* }) => { 25 | impl FromMap for $t { 26 | fn from_map(map: &IndexMap) -> Result { 27 | $( 28 | $crate::protocol::impl_from_map!(@helper; map; $($optional)? $field); 29 | )* 30 | Ok(Self { $($field),* }) 31 | } 32 | } 33 | }; 34 | (@helper; $self: ident; optional $field: ident) => { 35 | let $field = if let Some(val) = $self.get(&encode::snake_to_camel(stringify!($field))) { 36 | val.clone().to_type()? 37 | } else { 38 | None 39 | }; 40 | }; 41 | (@helper; $self: ident; $field: ident) => { 42 | let $field = get!($self, &encode::snake_to_camel(stringify!($field)))?; 43 | }; 44 | } 45 | 46 | macro_rules! protocol_impls { 47 | (for $t: ty { $( $(#[$optional:ident])? $field: ident),* }) => { 48 | impl_from_map!(for $t { $( $(#[$optional])? $field),* }); 49 | impl_encode_struct!(for $t { $($field),* }); 50 | }; 51 | } 52 | 53 | macro_rules! enum_impl_from { 54 | (for $t: ty { $( $variant:ident($field: ty)),* $(,)? }) => { 55 | $( 56 | impl From<$field> for $t { 57 | fn from(value: $field) -> Self { 58 | <$t>::$variant(value) 59 | } 60 | } 61 | 62 | )* 63 | }; 64 | } 65 | 66 | pub(crate) use impl_from_map; 67 | 68 | pub trait Decode { 69 | fn decode_from<'a>(buf: &mut Buf<'a>) -> Result 70 | where 71 | Self: Sized; 72 | } 73 | 74 | #[derive(Debug, Clone)] 75 | pub struct Buf<'a> { 76 | buf: &'a [u8], 77 | idx: usize, 78 | } 79 | 80 | impl<'a> Buf<'a> { 81 | pub fn new(buf: &'a [u8]) -> Self { 82 | Self { buf, idx: 0 } 83 | } 84 | 85 | fn read_u8(&mut self) -> u8 { 86 | let value = self.buf[self.idx]; 87 | self.idx += 1; 88 | value 89 | } 90 | 91 | fn read_n(&mut self, n: usize, into: &mut [u8]) { 92 | into.copy_from_slice(&self.buf[self.idx..self.idx + n]); 93 | self.idx += n; 94 | } 95 | 96 | fn read_u32(&mut self) -> u32 { 97 | let value = u32::from_le_bytes(self.buf[self.idx..self.idx + 4].try_into().unwrap()); 98 | self.idx += 4; 99 | value 100 | } 101 | } 102 | 103 | pub trait FromAnyValue: Sized { 104 | fn from_any_value(value: AnyValue) -> Result; 105 | } 106 | 107 | impl FromAnyValue for AnyValue { 108 | fn from_any_value(value: AnyValue) -> Result { 109 | Ok(value) 110 | } 111 | } 112 | 113 | impl FromAnyValue for String { 114 | fn from_any_value(value: AnyValue) -> Result { 115 | match value { 116 | AnyValue::String(s) => Ok(s), 117 | _ => Err(anyhow::anyhow!("expected string")), 118 | } 119 | } 120 | } 121 | 122 | impl FromAnyValue for u32 { 123 | fn from_any_value(value: AnyValue) -> Result { 124 | match value { 125 | AnyValue::U32(u) => Ok(u), 126 | _ => Err(anyhow::anyhow!("expected u32, got {value:?}")), 127 | } 128 | } 129 | } 130 | 131 | impl FromAnyValue for bool { 132 | fn from_any_value(value: AnyValue) -> Result { 133 | match value { 134 | AnyValue::Bool(b) => Ok(b), 135 | _ => Err(anyhow::anyhow!("expected bool")), 136 | } 137 | } 138 | } 139 | 140 | impl FromAnyValue for Vec { 141 | fn from_any_value(value: AnyValue) -> Result { 142 | match value { 143 | AnyValue::Bytes(b) => Ok(b), 144 | _ => Err(anyhow::anyhow!("expected bytes")), 145 | } 146 | } 147 | } 148 | 149 | impl FromAnyValue for Vec { 150 | fn from_any_value(value: AnyValue) -> Result { 151 | match value { 152 | AnyValue::Vec(v) => Ok(v 153 | .into_iter() 154 | .map(T::from_any_value) 155 | .collect::, _>>()?), 156 | _ => Err(anyhow::anyhow!("expected vec")), 157 | } 158 | } 159 | } 160 | 161 | impl FromAnyValue for IndexMap { 162 | fn from_any_value(value: AnyValue) -> Result { 163 | match value { 164 | AnyValue::Map(m) => { 165 | let mut map = IndexMap::new(); 166 | for (k, v) in m { 167 | map.insert(k, V::from_any_value(v)?); 168 | } 169 | Ok(map) 170 | } 171 | _ => Err(anyhow::anyhow!("expected map")), 172 | } 173 | } 174 | } 175 | 176 | impl FromAnyValue for Option { 177 | fn from_any_value(value: AnyValue) -> Result { 178 | match value { 179 | AnyValue::Null => Ok(None), 180 | _ => Ok(Some(T::from_any_value(value)?)), 181 | } 182 | } 183 | } 184 | 185 | impl FromAnyValue for T 186 | where 187 | T: FromMap, 188 | { 189 | fn from_any_value(value: AnyValue) -> Result { 190 | Self::from_map(value.as_map()?) 191 | } 192 | } 193 | 194 | #[derive(Debug, Clone)] 195 | pub struct Packet { 196 | pub id: u32, 197 | pub is_request: bool, 198 | pub value: T, 199 | } 200 | 201 | #[derive(Debug, Clone, Default)] 202 | pub struct BuildRequest { 203 | pub key: u32, 204 | pub entries: Vec<(String, String)>, 205 | pub flags: Vec, 206 | pub write: bool, 207 | pub stdin_contents: OptionNull>, 208 | pub stdin_resolve_dir: OptionNull, 209 | pub abs_working_dir: String, 210 | pub node_paths: Vec, 211 | pub context: bool, 212 | pub plugins: Option>, 213 | pub mangle_cache: Option>, 214 | } 215 | 216 | #[derive(Debug, Clone)] 217 | pub struct Message { 218 | pub id: String, 219 | pub plugin_name: String, 220 | pub text: String, 221 | pub location: Option, 222 | pub notes: Vec, 223 | pub detail: Option, 224 | // detail: any 225 | } 226 | 227 | protocol_impls!(for Message { id, plugin_name, text, #[optional] location, notes, #[optional] detail }); 228 | 229 | struct MessageFromAnyValue(Message); 230 | impl FromAnyValue for MessageFromAnyValue { 231 | fn from_any_value(value: AnyValue) -> Result { 232 | if let AnyValue::Map(map) = value { 233 | let message = Message::from_map(&map)?; 234 | return Ok(MessageFromAnyValue(message)); 235 | } else if let AnyValue::String(s) = value { 236 | let message = Message { 237 | text: s, 238 | detail: None, 239 | id: "".to_string(), 240 | plugin_name: "".to_string(), 241 | notes: vec![], 242 | location: None, 243 | }; 244 | return Ok(MessageFromAnyValue(message)); 245 | } 246 | Ok(MessageFromAnyValue(Message::from_any_value(value)?)) 247 | } 248 | } 249 | 250 | #[derive(Debug, Clone)] 251 | pub struct Note { 252 | pub text: String, 253 | pub location: Option, 254 | } 255 | protocol_impls!(for Note { text, #[optional] location }); 256 | 257 | #[derive(Debug, Clone)] 258 | pub struct Location { 259 | pub file: String, 260 | pub namespace: String, 261 | pub line: u32, 262 | pub column: u32, 263 | pub length: u32, 264 | pub line_text: String, 265 | pub suggestion: String, 266 | } 267 | 268 | protocol_impls!(for Location { file, namespace, line, column, length, line_text, suggestion }); 269 | 270 | #[derive(Debug, Clone, Default)] 271 | pub struct PartialMessage { 272 | pub id: String, 273 | pub plugin_name: String, 274 | pub text: String, 275 | pub location: Option, 276 | pub notes: Vec, 277 | pub detail: u32, 278 | } 279 | 280 | protocol_impls!(for PartialMessage { id, plugin_name, text, #[optional] location, notes, detail }); 281 | 282 | #[derive(Debug, Clone)] 283 | pub struct ServeOnRequestArgs { 284 | pub remote_address: String, 285 | pub method: String, 286 | pub path: String, 287 | pub status: u32, 288 | /// The time to generate the response, not to send it 289 | pub time_in_ms: u32, 290 | } 291 | 292 | impl_encode_struct!(for ServeOnRequestArgs { remote_address, method, path, status, time_in_ms }); 293 | 294 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 295 | #[cfg_attr(feature = "serde", derive(serde::Deserialize))] 296 | #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] 297 | pub enum ImportKind { 298 | EntryPoint, 299 | ImportStatement, 300 | RequireCall, 301 | DynamicImport, 302 | RequireResolve, 303 | // Css 304 | ImportRule, 305 | ComposesFrom, 306 | UrlToken, 307 | } 308 | 309 | impl FromAnyValue for ImportKind { 310 | fn from_any_value(value: AnyValue) -> Result { 311 | let s = value.as_string()?; 312 | Ok(match s.as_str() { 313 | "entry-point" => ImportKind::EntryPoint, 314 | "import-statement" => ImportKind::ImportStatement, 315 | "require-call" => ImportKind::RequireCall, 316 | "dynamic-import" => ImportKind::DynamicImport, 317 | "require-resolve" => ImportKind::RequireResolve, 318 | "import-rule" => ImportKind::ImportRule, 319 | "composes-from" => ImportKind::ComposesFrom, 320 | "url-token" => ImportKind::UrlToken, 321 | _ => return Err(anyhow::anyhow!("invalid import kind: {}", s)), 322 | }) 323 | } 324 | } 325 | // Equivalent to TypeScript MangleCacheEntry: string | false 326 | #[derive(Debug, Clone)] 327 | pub enum MangleCacheEntry { 328 | StringValue(String), 329 | BoolValue(bool), 330 | } 331 | 332 | impl FromAnyValue for MangleCacheEntry { 333 | fn from_any_value(value: AnyValue) -> Result { 334 | match value { 335 | AnyValue::Bool(b) => Ok(MangleCacheEntry::BoolValue(b)), 336 | AnyValue::String(s) => Ok(MangleCacheEntry::StringValue(s)), 337 | value => Err(anyhow::anyhow!("invalid mangle cache entry: {:?}", value)), 338 | } 339 | } 340 | } 341 | 342 | #[derive(Debug, Clone)] 343 | pub struct ServeRequest { 344 | // command: "serve"; // This will likely be handled by an enum or similar in a real IPC setup 345 | pub key: u32, 346 | pub on_request: bool, 347 | pub port: Option, 348 | pub host: Option, 349 | pub servedir: Option, 350 | pub keyfile: Option, 351 | pub certfile: Option, 352 | pub fallback: Option, 353 | pub cors_origin: Option>, 354 | } 355 | 356 | impl_encode_command!(for ServeRequest { 357 | const Command = "serve"; 358 | key, on_request, port, host, servedir, keyfile, certfile, fallback, cors_origin 359 | }); 360 | 361 | #[derive(Debug, Clone)] 362 | pub struct ServeResponse { 363 | pub port: u32, 364 | pub hosts: Vec, 365 | } 366 | 367 | #[derive(Debug, Clone)] 368 | pub struct BuildPlugin { 369 | pub name: String, 370 | pub on_start: bool, 371 | pub on_end: bool, 372 | pub on_resolve: Vec, 373 | pub on_load: Vec, 374 | } 375 | 376 | impl_encode_struct!(for BuildPlugin { name, on_start, on_end, on_resolve, on_load }); 377 | 378 | #[derive(Debug, Clone)] 379 | pub struct OnResolveSetupOptions { 380 | pub id: u32, 381 | pub filter: String, 382 | pub namespace: String, 383 | } 384 | 385 | impl_encode_struct!(for OnResolveSetupOptions { id, filter, namespace }); 386 | 387 | #[derive(Debug, Clone)] 388 | pub struct OnLoadSetupOptions { 389 | pub id: u32, 390 | pub filter: String, 391 | pub namespace: String, 392 | } 393 | 394 | impl_encode_struct!(for OnLoadSetupOptions { id, filter, namespace }); 395 | 396 | #[derive(Debug, Clone)] 397 | pub struct BuildResponse { 398 | pub errors: Vec, 399 | pub warnings: Vec, 400 | pub output_files: Option>, 401 | pub metafile: Option, 402 | pub mangle_cache: Option>, 403 | pub write_to_stdout: Option>, 404 | } 405 | 406 | impl_from_map!(for BuildResponse { 407 | errors, warnings, #[optional] output_files, #[optional] metafile, #[optional] mangle_cache, #[optional] write_to_stdout 408 | }); 409 | 410 | #[derive(Debug, Clone)] 411 | pub struct OnEndRequest { 412 | // Extends BuildResponse 413 | // command: "on-end"; 414 | // Fields from BuildResponse 415 | pub errors: Vec, 416 | pub warnings: Vec, 417 | pub output_files: Option>, 418 | pub metafile: Option, 419 | pub mangle_cache: Option>, 420 | pub write_to_stdout: Option>, 421 | } 422 | 423 | protocol_impls!(for OnEndRequest { errors, warnings, #[optional] output_files, #[optional] metafile, #[optional] mangle_cache, #[optional] write_to_stdout }); 424 | 425 | #[derive(Debug, Clone, Default)] 426 | pub struct OnEndResponse { 427 | pub errors: Vec, 428 | pub warnings: Vec, 429 | } 430 | 431 | impl_encode_struct!(for OnEndResponse { errors, warnings }); 432 | 433 | #[derive(Debug, Clone)] 434 | pub struct BuildOutputFile { 435 | pub path: String, 436 | pub contents: Vec, 437 | pub hash: String, 438 | } 439 | 440 | protocol_impls!(for BuildOutputFile { path, contents, hash }); 441 | 442 | #[derive(Debug, Clone)] 443 | pub struct PingRequest { 444 | // command: "ping"; 445 | } 446 | 447 | impl_encode_command!(for PingRequest { 448 | const Command = "ping"; 449 | }); 450 | 451 | #[derive(Debug, Clone)] 452 | pub struct RebuildRequest { 453 | // command: "rebuild"; 454 | pub key: u32, 455 | } 456 | 457 | impl_encode_command!(for RebuildRequest { 458 | const Command = "rebuild"; 459 | key 460 | }); 461 | 462 | #[derive(Debug, Clone)] 463 | pub struct RebuildResponse { 464 | pub errors: Vec, 465 | pub warnings: Vec, 466 | } 467 | 468 | protocol_impls!(for RebuildResponse { errors, warnings }); 469 | 470 | #[derive(Debug, Clone)] 471 | pub struct DisposeRequest { 472 | // command: "dispose"; 473 | pub key: u32, 474 | } 475 | 476 | impl_encode_command!(for DisposeRequest { 477 | const Command = "dispose"; 478 | key 479 | }); 480 | 481 | #[derive(Debug, Clone)] 482 | pub struct CancelRequest { 483 | // command: "cancel"; 484 | pub key: u32, 485 | } 486 | 487 | impl_encode_command!(for CancelRequest { 488 | const Command = "cancel"; 489 | key 490 | }); 491 | 492 | #[derive(Debug, Clone)] 493 | pub struct WatchRequest { 494 | // command: "watch"; 495 | pub key: u32, 496 | } 497 | 498 | impl_encode_command!(for WatchRequest { 499 | const Command = "watch"; 500 | key 501 | }); 502 | 503 | #[derive(Debug, Clone)] 504 | pub struct OnServeRequest { 505 | // command: "serve-request"; 506 | pub key: u32, 507 | pub args: ServeOnRequestArgs, 508 | } 509 | 510 | impl_encode_command!(for OnServeRequest { 511 | const Command = "serve-request"; 512 | key, args 513 | }); 514 | 515 | #[derive(Debug, Clone)] 516 | pub struct TransformRequest { 517 | // command: "transform"; 518 | pub flags: Vec, 519 | pub input: Vec, 520 | pub input_fs: bool, 521 | pub mangle_cache: Option>, 522 | } 523 | 524 | impl_encode_command!(for TransformRequest { 525 | const Command = "transform"; 526 | flags, input, input_fs, mangle_cache 527 | }); 528 | 529 | #[derive(Debug, Clone)] 530 | pub struct TransformResponse { 531 | pub errors: Vec, 532 | pub warnings: Vec, 533 | pub code: String, 534 | pub code_fs: bool, 535 | pub map: String, 536 | pub map_fs: bool, 537 | pub legal_comments: Option, 538 | pub mangle_cache: Option>, 539 | } 540 | 541 | #[derive(Debug, Clone)] 542 | pub struct FormatMsgsRequest { 543 | // command: "format-msgs"; 544 | pub messages: Vec, 545 | pub is_warning: bool, 546 | pub color: Option, 547 | pub terminal_width: Option, 548 | } 549 | 550 | impl_encode_command!(for FormatMsgsRequest { 551 | const Command = "format-msgs"; 552 | messages, is_warning, color, terminal_width 553 | }); 554 | 555 | #[derive(Debug, Clone)] 556 | pub struct FormatMsgsResponse { 557 | pub messages: Vec, 558 | } 559 | 560 | #[derive(Debug, Clone)] 561 | pub struct AnalyzeMetafileRequest { 562 | // command: "analyze-metafile"; 563 | pub metafile: String, 564 | pub color: Option, 565 | pub verbose: Option, 566 | } 567 | 568 | impl_encode_command!(for AnalyzeMetafileRequest { 569 | const Command = "analyze-metafile"; 570 | metafile, color, verbose 571 | }); 572 | 573 | #[derive(Debug, Clone)] 574 | pub struct AnalyzeMetafileResponse { 575 | pub result: String, 576 | } 577 | 578 | #[derive(Debug, Clone)] 579 | pub struct OnStartRequest { 580 | // command: "on-start"; 581 | pub key: u32, 582 | } 583 | 584 | impl_encode_command!(for OnStartRequest { 585 | const Command = "on-start"; 586 | key 587 | }); 588 | 589 | #[derive(Debug, Clone, Default)] 590 | pub struct OnStartResponse { 591 | pub errors: Vec, 592 | pub warnings: Vec, 593 | } 594 | 595 | impl_encode_struct!(for OnStartResponse { errors, warnings }); 596 | 597 | #[derive(Debug, Clone)] 598 | pub struct ResolveRequest { 599 | // command: "resolve"; 600 | pub key: u32, 601 | pub path: String, 602 | pub plugin_name: String, 603 | pub importer: Option, 604 | pub namespace: Option, 605 | pub resolve_dir: Option, 606 | pub kind: Option, // Consider using an enum if kinds are fixed 607 | pub plugin_data: Option, // Assuming u32 for opaque data, adjust if needed 608 | pub with: Option>, 609 | } 610 | 611 | impl_encode_command!(for ResolveRequest { 612 | const Command = "resolve"; 613 | key, path, plugin_name, importer, namespace, resolve_dir, kind, plugin_data, with 614 | }); 615 | 616 | #[derive(Debug, Clone)] 617 | pub struct ResolveResponse { 618 | pub errors: Vec, 619 | pub warnings: Vec, 620 | pub path: String, 621 | pub external: bool, 622 | pub side_effects: bool, 623 | pub namespace: String, 624 | pub suffix: String, 625 | pub plugin_data: u32, // Assuming u32 for opaque data 626 | } 627 | 628 | #[derive(Debug, Clone)] 629 | pub struct OnResolveRequest { 630 | // command: "on-resolve"; 631 | pub key: u32, 632 | pub ids: Vec, 633 | pub path: String, 634 | pub importer: String, 635 | pub namespace: String, 636 | pub resolve_dir: Option, 637 | pub kind: ImportKind, 638 | #[allow(dead_code)] 639 | pub plugin_data: Option, 640 | pub with: IndexMap, 641 | } 642 | 643 | impl_from_map!(for OnResolveRequest { 644 | key, ids, path, importer, namespace, #[optional] resolve_dir, kind, #[optional] plugin_data, with 645 | }); 646 | 647 | #[derive(Clone, Default)] 648 | pub struct OptionNull(Option); 649 | 650 | impl OptionNull { 651 | pub fn new(value: Option) -> Self { 652 | Self(value) 653 | } 654 | } 655 | 656 | impl From> for Option { 657 | fn from(value: OptionNull) -> Self { 658 | value.0 659 | } 660 | } 661 | 662 | impl From> for OptionNull { 663 | fn from(value: Option) -> Self { 664 | Self(value) 665 | } 666 | } 667 | 668 | impl Debug for OptionNull { 669 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 670 | write!(f, "{:?}", self.0) 671 | } 672 | } 673 | 674 | #[derive(Debug, Clone, Default)] 675 | pub struct OnResolveResponse { 676 | // TODO(nathanwhit): plugin id or request id or ?? 677 | pub id: Option, 678 | pub plugin_name: Option, 679 | pub errors: Option>, 680 | pub warnings: Option>, 681 | pub path: Option, 682 | pub external: Option, 683 | pub side_effects: Option, 684 | pub namespace: Option, 685 | pub suffix: Option, 686 | pub plugin_data: Option, 687 | pub watch_files: Option>, 688 | pub watch_dirs: Option>, 689 | } 690 | 691 | impl_encode_struct!(for OnResolveResponse { 692 | id, 693 | plugin_name, 694 | errors, 695 | warnings, 696 | path, 697 | external, 698 | side_effects, 699 | namespace, 700 | suffix, 701 | plugin_data, 702 | watch_files, 703 | watch_dirs 704 | }); 705 | 706 | #[derive(Debug, Clone)] 707 | pub struct OnLoadRequest { 708 | // command: "on-load"; 709 | pub key: u32, 710 | pub ids: Vec, 711 | pub path: String, 712 | pub namespace: String, 713 | pub suffix: String, 714 | pub plugin_data: Option, 715 | pub with: IndexMap, 716 | } 717 | 718 | impl_from_map!(for OnLoadRequest { 719 | key, ids, path, namespace, suffix, #[optional] plugin_data, with 720 | }); 721 | 722 | impl_encode_command!(for OnLoadRequest { 723 | const Command = "on-load"; 724 | key, ids, path, namespace, suffix, plugin_data, with 725 | }); 726 | 727 | #[derive(Clone, Default)] 728 | pub struct OnLoadResponse { 729 | pub id: Option, 730 | pub plugin_name: Option, 731 | pub errors: Option>, 732 | pub warnings: Option>, 733 | pub contents: Option>, 734 | pub resolve_dir: Option, 735 | pub loader: Option, 736 | pub plugin_data: Option, 737 | pub watch_files: Option>, 738 | pub watch_dirs: Option>, 739 | } 740 | 741 | impl Debug for OnLoadResponse { 742 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 743 | f.debug_struct("OnLoadResponse") 744 | .field("id", &self.id) 745 | .field("plugin_name", &self.plugin_name) 746 | .field("errors", &self.errors) 747 | .field("warnings", &self.warnings) 748 | .field( 749 | "contents", 750 | &self.contents.as_ref().map(|c| String::from_utf8_lossy(c)), 751 | ) 752 | .field("resolve_dir", &self.resolve_dir) 753 | .field("loader", &self.loader) 754 | .field("plugin_data", &self.plugin_data) 755 | .field("watch_files", &self.watch_files) 756 | .field("watch_dirs", &self.watch_dirs) 757 | .finish() 758 | } 759 | } 760 | 761 | impl_encode_struct!(for OnLoadResponse { 762 | id, 763 | plugin_name, 764 | errors, 765 | warnings, 766 | contents, 767 | resolve_dir, 768 | loader, 769 | plugin_data, 770 | watch_files, 771 | watch_dirs 772 | }); 773 | 774 | #[derive(Debug, Clone)] 775 | pub enum AnyRequest { 776 | Build(Box), 777 | // Serve(ServeRequest), 778 | // OnEnd(OnEndRequest), 779 | // Ping(PingRequest), 780 | Rebuild(RebuildRequest), 781 | Dispose(DisposeRequest), 782 | Cancel(CancelRequest), 783 | // Watch(WatchRequest), 784 | // OnServe(OnServeRequest), 785 | // Transform(TransformRequest), 786 | // FormatMsgs(FormatMsgsRequest), 787 | // AnalyzeMetafile(AnalyzeMetafileRequest), 788 | // OnStart(OnStartRequest), 789 | // Resolve(ResolveRequest), 790 | // OnResolve(OnResolveRequest), 791 | // OnLoad(OnLoadRequest), 792 | } 793 | 794 | #[derive(Debug)] 795 | pub enum RequestKind { 796 | Build(oneshot::Sender>), 797 | Dispose(oneshot::Sender<()>), 798 | Rebuild(oneshot::Sender>), 799 | Cancel(oneshot::Sender<()>), 800 | } 801 | 802 | #[derive(Debug, Clone, Default)] 803 | pub struct PingResponse { 804 | // command: "ping"; 805 | } 806 | 807 | impl_encode_struct!(for PingResponse {}); 808 | 809 | #[derive(Debug, Clone)] 810 | pub enum AnyResponse { 811 | Build(BuildResponse), 812 | Serve(ServeResponse), 813 | OnEnd(OnEndResponse), 814 | Rebuild(RebuildResponse), 815 | Transform(TransformResponse), 816 | FormatMsgs(FormatMsgsResponse), 817 | AnalyzeMetafile(AnalyzeMetafileResponse), 818 | OnStart(OnStartResponse), 819 | Resolve(ResolveResponse), 820 | OnResolve(OnResolveResponse), 821 | OnLoad(OnLoadResponse), 822 | Ping(PingResponse), 823 | } 824 | 825 | enum_impl_from!(for AnyResponse { 826 | Build(BuildResponse), 827 | Serve(ServeResponse), 828 | OnEnd(OnEndResponse), 829 | Rebuild(RebuildResponse), 830 | Transform(TransformResponse), 831 | FormatMsgs(FormatMsgsResponse), 832 | AnalyzeMetafile(AnalyzeMetafileResponse), 833 | OnStart(OnStartResponse), 834 | Resolve(ResolveResponse), 835 | OnResolve(OnResolveResponse), 836 | OnLoad(OnLoadResponse), 837 | Ping(PingResponse), 838 | }); 839 | 840 | #[derive(Debug, Clone)] 841 | pub enum ProtocolMessage { 842 | Request(AnyRequest), 843 | Response(AnyResponse), 844 | } 845 | 846 | #[derive(Debug, Clone)] 847 | pub struct AnyPacket { 848 | pub id: u32, 849 | pub is_request: bool, 850 | pub value: AnyValue, 851 | } 852 | 853 | #[derive(Debug, Clone)] 854 | pub struct ProtocolPacket { 855 | pub id: u32, 856 | pub is_request: bool, 857 | pub value: ProtocolMessage, 858 | } 859 | 860 | // impl Decode 861 | 862 | #[derive(Debug, Clone)] 863 | pub enum AnyValue { 864 | Null, 865 | Bool(bool), 866 | U32(u32), 867 | String(String), 868 | Bytes(Vec), 869 | Vec(Vec), 870 | Map(IndexMap), 871 | } 872 | 873 | impl AnyValue { 874 | pub fn as_string(&self) -> Result<&String, anyhow::Error> { 875 | match self { 876 | AnyValue::String(s) => Ok(s), 877 | _ => Err(anyhow::anyhow!("Expected string")), 878 | } 879 | } 880 | 881 | pub fn as_map(&self) -> Result<&IndexMap, anyhow::Error> { 882 | match self { 883 | AnyValue::Map(m) => Ok(m), 884 | _ => Err(anyhow::anyhow!("Expected map")), 885 | } 886 | } 887 | } 888 | 889 | impl Decode for bool { 890 | fn decode_from(buf: &mut Buf) -> Result { 891 | Ok(buf.read_u8() == 1) 892 | } 893 | } 894 | 895 | impl Decode for u32 { 896 | fn decode_from(buf: &mut Buf) -> Result { 897 | Ok(buf.read_u32()) 898 | } 899 | } 900 | 901 | impl Decode for String { 902 | fn decode_from(buf: &mut Buf) -> Result { 903 | let length = buf.read_u32() as usize; 904 | let mut string = vec![0; length]; 905 | buf.read_n(length, &mut string); 906 | String::from_utf8(string).map_err(|e| anyhow::anyhow!("Failed to decode string: {}", e)) 907 | } 908 | } 909 | 910 | impl Decode for Vec { 911 | fn decode_from(buf: &mut Buf) -> Result { 912 | let length = buf.read_u32() as usize; 913 | let mut vec = vec![0; length]; 914 | buf.read_n(length, &mut vec); 915 | Ok(vec) 916 | } 917 | } 918 | 919 | impl Decode for Vec { 920 | fn decode_from(buf: &mut Buf) -> Result { 921 | let length = buf.read_u32() as usize; 922 | let mut vec = Vec::with_capacity(length); 923 | for _ in 0..length { 924 | vec.push(T::decode_from(buf)?); 925 | } 926 | Ok(vec) 927 | } 928 | } 929 | 930 | impl Decode for IndexMap { 931 | fn decode_from(buf: &mut Buf) -> Result { 932 | let length = buf.read_u32() as usize; 933 | let mut map = IndexMap::with_capacity(length); 934 | for _ in 0..length { 935 | let key = K::decode_from(buf)?; 936 | let value = V::decode_from(buf)?; 937 | map.insert(key, value); 938 | } 939 | Ok(map) 940 | } 941 | } 942 | 943 | impl Decode for AnyValue { 944 | fn decode_from(buf: &mut Buf) -> Result { 945 | let value = buf.read_u8(); 946 | match value { 947 | 0 => Ok(AnyValue::Null), 948 | 1 => Ok(AnyValue::Bool(bool::decode_from(buf)?)), 949 | 2 => Ok(AnyValue::U32(u32::decode_from(buf)?)), 950 | 3 => Ok(AnyValue::String(String::decode_from(buf)?)), 951 | 4 => Ok(AnyValue::Bytes(Vec::decode_from(buf)?)), 952 | 5 => Ok(AnyValue::Vec(Vec::::decode_from(buf)?)), 953 | 6 => Ok(AnyValue::Map(IndexMap::::decode_from( 954 | buf, 955 | )?)), 956 | _ => Err(anyhow::anyhow!("Invalid value: {}", value)), 957 | } 958 | } 959 | } 960 | 961 | impl Decode for AnyPacket { 962 | fn decode_from<'a>(buf: &mut Buf<'a>) -> Result { 963 | let mut id = u32::decode_from(buf)?; 964 | let is_request = id & 1 == 0; 965 | id >>= 1; 966 | let value = AnyValue::decode_from(buf)?; 967 | Ok(AnyPacket { 968 | id, 969 | is_request, 970 | value, 971 | }) 972 | } 973 | } 974 | 975 | impl AnyValue { 976 | pub fn to_type(self) -> Result { 977 | T::from_any_value(self) 978 | } 979 | } 980 | 981 | pub fn decode_any_packet(buf: &[u8]) -> Result { 982 | let mut buf = Buf::new(buf); 983 | AnyPacket::decode_from(&mut buf) 984 | } 985 | 986 | pub trait FromMap: Sized { 987 | fn from_map(map: &IndexMap) -> Result; 988 | } 989 | 990 | impl FromMap for OnStartRequest { 991 | fn from_map(map: &IndexMap) -> Result { 992 | let key = get!(map, "key")?; 993 | Ok(OnStartRequest { key }) 994 | } 995 | } 996 | 997 | impl FromMap for Result { 998 | fn from_map(map: &IndexMap) -> Result { 999 | if let Some(value) = map.get("error") { 1000 | let error = MessageFromAnyValue::from_any_value(value.clone())?; 1001 | return Ok(Err(error.0)); 1002 | } 1003 | Ok(Ok(T::from_map(map)?)) 1004 | } 1005 | } 1006 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.98" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 34 | 35 | [[package]] 36 | name = "async-trait" 37 | version = "0.1.88" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 40 | dependencies = [ 41 | "proc-macro2", 42 | "quote", 43 | "syn", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.4.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 51 | 52 | [[package]] 53 | name = "backtrace" 54 | version = "0.3.74" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 57 | dependencies = [ 58 | "addr2line", 59 | "cfg-if", 60 | "libc", 61 | "miniz_oxide", 62 | "object", 63 | "rustc-demangle", 64 | "windows-targets", 65 | ] 66 | 67 | [[package]] 68 | name = "base64" 69 | version = "0.22.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 72 | 73 | [[package]] 74 | name = "bitflags" 75 | version = "2.9.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 78 | 79 | [[package]] 80 | name = "bytes" 81 | version = "1.10.1" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 84 | 85 | [[package]] 86 | name = "cc" 87 | version = "1.2.24" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" 90 | dependencies = [ 91 | "shlex", 92 | ] 93 | 94 | [[package]] 95 | name = "cfg-if" 96 | version = "1.0.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 99 | 100 | [[package]] 101 | name = "crc32fast" 102 | version = "1.4.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 105 | dependencies = [ 106 | "cfg-if", 107 | ] 108 | 109 | [[package]] 110 | name = "deno_unsync" 111 | version = "0.4.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "d774fd83f26b24f0805a6ab8b26834a0d06ceac0db517b769b1e4633c96a2057" 114 | dependencies = [ 115 | "futures", 116 | "parking_lot", 117 | "tokio", 118 | ] 119 | 120 | [[package]] 121 | name = "diff" 122 | version = "0.1.13" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 125 | 126 | [[package]] 127 | name = "directories" 128 | version = "6.0.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" 131 | dependencies = [ 132 | "dirs-sys", 133 | ] 134 | 135 | [[package]] 136 | name = "dirs-sys" 137 | version = "0.5.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 140 | dependencies = [ 141 | "libc", 142 | "option-ext", 143 | "redox_users", 144 | "windows-sys 0.59.0", 145 | ] 146 | 147 | [[package]] 148 | name = "env_logger" 149 | version = "0.10.2" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 152 | dependencies = [ 153 | "humantime", 154 | "is-terminal", 155 | "log", 156 | "regex", 157 | "termcolor", 158 | ] 159 | 160 | [[package]] 161 | name = "equivalent" 162 | version = "1.0.2" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 165 | 166 | [[package]] 167 | name = "errno" 168 | version = "0.3.12" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 171 | dependencies = [ 172 | "libc", 173 | "windows-sys 0.59.0", 174 | ] 175 | 176 | [[package]] 177 | name = "esbuild_client" 178 | version = "0.7.1" 179 | dependencies = [ 180 | "anyhow", 181 | "async-trait", 182 | "deno_unsync", 183 | "directories", 184 | "esbuild_client", 185 | "flate2", 186 | "indexmap", 187 | "log", 188 | "parking_lot", 189 | "paste", 190 | "pathdiff", 191 | "pretty_assertions", 192 | "pretty_env_logger", 193 | "serde", 194 | "serde_json", 195 | "sys_traits", 196 | "tar", 197 | "tokio", 198 | "ureq", 199 | ] 200 | 201 | [[package]] 202 | name = "filetime" 203 | version = "0.2.25" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 206 | dependencies = [ 207 | "cfg-if", 208 | "libc", 209 | "libredox", 210 | "windows-sys 0.59.0", 211 | ] 212 | 213 | [[package]] 214 | name = "flate2" 215 | version = "1.1.1" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 218 | dependencies = [ 219 | "crc32fast", 220 | "miniz_oxide", 221 | ] 222 | 223 | [[package]] 224 | name = "fnv" 225 | version = "1.0.7" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 228 | 229 | [[package]] 230 | name = "futures" 231 | version = "0.3.31" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 234 | dependencies = [ 235 | "futures-channel", 236 | "futures-core", 237 | "futures-executor", 238 | "futures-io", 239 | "futures-sink", 240 | "futures-task", 241 | "futures-util", 242 | ] 243 | 244 | [[package]] 245 | name = "futures-channel" 246 | version = "0.3.31" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 249 | dependencies = [ 250 | "futures-core", 251 | "futures-sink", 252 | ] 253 | 254 | [[package]] 255 | name = "futures-core" 256 | version = "0.3.31" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 259 | 260 | [[package]] 261 | name = "futures-executor" 262 | version = "0.3.31" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 265 | dependencies = [ 266 | "futures-core", 267 | "futures-task", 268 | "futures-util", 269 | ] 270 | 271 | [[package]] 272 | name = "futures-io" 273 | version = "0.3.31" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 276 | 277 | [[package]] 278 | name = "futures-macro" 279 | version = "0.3.31" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 282 | dependencies = [ 283 | "proc-macro2", 284 | "quote", 285 | "syn", 286 | ] 287 | 288 | [[package]] 289 | name = "futures-sink" 290 | version = "0.3.31" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 293 | 294 | [[package]] 295 | name = "futures-task" 296 | version = "0.3.31" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 299 | 300 | [[package]] 301 | name = "futures-util" 302 | version = "0.3.31" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 305 | dependencies = [ 306 | "futures-channel", 307 | "futures-core", 308 | "futures-io", 309 | "futures-macro", 310 | "futures-sink", 311 | "futures-task", 312 | "memchr", 313 | "pin-project-lite", 314 | "pin-utils", 315 | "slab", 316 | ] 317 | 318 | [[package]] 319 | name = "getrandom" 320 | version = "0.2.16" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 323 | dependencies = [ 324 | "cfg-if", 325 | "libc", 326 | "wasi", 327 | ] 328 | 329 | [[package]] 330 | name = "gimli" 331 | version = "0.31.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 334 | 335 | [[package]] 336 | name = "hashbrown" 337 | version = "0.15.3" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 340 | 341 | [[package]] 342 | name = "hermit-abi" 343 | version = "0.5.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" 346 | 347 | [[package]] 348 | name = "http" 349 | version = "1.3.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 352 | dependencies = [ 353 | "bytes", 354 | "fnv", 355 | "itoa", 356 | ] 357 | 358 | [[package]] 359 | name = "httparse" 360 | version = "1.10.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 363 | 364 | [[package]] 365 | name = "humantime" 366 | version = "2.2.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 369 | 370 | [[package]] 371 | name = "indexmap" 372 | version = "2.9.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 375 | dependencies = [ 376 | "equivalent", 377 | "hashbrown", 378 | ] 379 | 380 | [[package]] 381 | name = "is-terminal" 382 | version = "0.4.16" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 385 | dependencies = [ 386 | "hermit-abi", 387 | "libc", 388 | "windows-sys 0.59.0", 389 | ] 390 | 391 | [[package]] 392 | name = "itoa" 393 | version = "1.0.15" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 396 | 397 | [[package]] 398 | name = "libc" 399 | version = "0.2.172" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 402 | 403 | [[package]] 404 | name = "libredox" 405 | version = "0.1.3" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 408 | dependencies = [ 409 | "bitflags", 410 | "libc", 411 | "redox_syscall", 412 | ] 413 | 414 | [[package]] 415 | name = "linux-raw-sys" 416 | version = "0.9.4" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 419 | 420 | [[package]] 421 | name = "lock_api" 422 | version = "0.4.12" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 425 | dependencies = [ 426 | "autocfg", 427 | "scopeguard", 428 | ] 429 | 430 | [[package]] 431 | name = "log" 432 | version = "0.4.27" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 435 | 436 | [[package]] 437 | name = "memchr" 438 | version = "2.7.4" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 441 | 442 | [[package]] 443 | name = "miniz_oxide" 444 | version = "0.8.8" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 447 | dependencies = [ 448 | "adler2", 449 | ] 450 | 451 | [[package]] 452 | name = "mio" 453 | version = "1.0.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 456 | dependencies = [ 457 | "libc", 458 | "wasi", 459 | "windows-sys 0.52.0", 460 | ] 461 | 462 | [[package]] 463 | name = "object" 464 | version = "0.36.7" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 467 | dependencies = [ 468 | "memchr", 469 | ] 470 | 471 | [[package]] 472 | name = "once_cell" 473 | version = "1.21.3" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 476 | 477 | [[package]] 478 | name = "option-ext" 479 | version = "0.2.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 482 | 483 | [[package]] 484 | name = "parking_lot" 485 | version = "0.12.3" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 488 | dependencies = [ 489 | "lock_api", 490 | "parking_lot_core", 491 | ] 492 | 493 | [[package]] 494 | name = "parking_lot_core" 495 | version = "0.9.10" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 498 | dependencies = [ 499 | "cfg-if", 500 | "libc", 501 | "redox_syscall", 502 | "smallvec", 503 | "windows-targets", 504 | ] 505 | 506 | [[package]] 507 | name = "paste" 508 | version = "1.0.15" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 511 | 512 | [[package]] 513 | name = "pathdiff" 514 | version = "0.2.3" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 517 | 518 | [[package]] 519 | name = "percent-encoding" 520 | version = "2.3.1" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 523 | 524 | [[package]] 525 | name = "pin-project-lite" 526 | version = "0.2.16" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 529 | 530 | [[package]] 531 | name = "pin-utils" 532 | version = "0.1.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 535 | 536 | [[package]] 537 | name = "pretty_assertions" 538 | version = "1.4.1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 541 | dependencies = [ 542 | "diff", 543 | "yansi", 544 | ] 545 | 546 | [[package]] 547 | name = "pretty_env_logger" 548 | version = "0.5.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" 551 | dependencies = [ 552 | "env_logger", 553 | "log", 554 | ] 555 | 556 | [[package]] 557 | name = "proc-macro2" 558 | version = "1.0.95" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 561 | dependencies = [ 562 | "unicode-ident", 563 | ] 564 | 565 | [[package]] 566 | name = "quote" 567 | version = "1.0.40" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 570 | dependencies = [ 571 | "proc-macro2", 572 | ] 573 | 574 | [[package]] 575 | name = "redox_syscall" 576 | version = "0.5.11" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" 579 | dependencies = [ 580 | "bitflags", 581 | ] 582 | 583 | [[package]] 584 | name = "redox_users" 585 | version = "0.5.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 588 | dependencies = [ 589 | "getrandom", 590 | "libredox", 591 | "thiserror", 592 | ] 593 | 594 | [[package]] 595 | name = "regex" 596 | version = "1.11.1" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 599 | dependencies = [ 600 | "aho-corasick", 601 | "memchr", 602 | "regex-automata", 603 | "regex-syntax", 604 | ] 605 | 606 | [[package]] 607 | name = "regex-automata" 608 | version = "0.4.9" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 611 | dependencies = [ 612 | "aho-corasick", 613 | "memchr", 614 | "regex-syntax", 615 | ] 616 | 617 | [[package]] 618 | name = "regex-syntax" 619 | version = "0.8.5" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 622 | 623 | [[package]] 624 | name = "ring" 625 | version = "0.17.14" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 628 | dependencies = [ 629 | "cc", 630 | "cfg-if", 631 | "getrandom", 632 | "libc", 633 | "untrusted", 634 | "windows-sys 0.52.0", 635 | ] 636 | 637 | [[package]] 638 | name = "rustc-demangle" 639 | version = "0.1.24" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 642 | 643 | [[package]] 644 | name = "rustix" 645 | version = "1.0.7" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 648 | dependencies = [ 649 | "bitflags", 650 | "errno", 651 | "libc", 652 | "linux-raw-sys", 653 | "windows-sys 0.59.0", 654 | ] 655 | 656 | [[package]] 657 | name = "rustls" 658 | version = "0.23.27" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" 661 | dependencies = [ 662 | "log", 663 | "once_cell", 664 | "ring", 665 | "rustls-pki-types", 666 | "rustls-webpki", 667 | "subtle", 668 | "zeroize", 669 | ] 670 | 671 | [[package]] 672 | name = "rustls-pemfile" 673 | version = "2.2.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 676 | dependencies = [ 677 | "rustls-pki-types", 678 | ] 679 | 680 | [[package]] 681 | name = "rustls-pki-types" 682 | version = "1.12.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 685 | dependencies = [ 686 | "zeroize", 687 | ] 688 | 689 | [[package]] 690 | name = "rustls-webpki" 691 | version = "0.103.3" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 694 | dependencies = [ 695 | "ring", 696 | "rustls-pki-types", 697 | "untrusted", 698 | ] 699 | 700 | [[package]] 701 | name = "ryu" 702 | version = "1.0.20" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 705 | 706 | [[package]] 707 | name = "scopeguard" 708 | version = "1.2.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 711 | 712 | [[package]] 713 | name = "serde" 714 | version = "1.0.219" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 717 | dependencies = [ 718 | "serde_derive", 719 | ] 720 | 721 | [[package]] 722 | name = "serde_derive" 723 | version = "1.0.219" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 726 | dependencies = [ 727 | "proc-macro2", 728 | "quote", 729 | "syn", 730 | ] 731 | 732 | [[package]] 733 | name = "serde_json" 734 | version = "1.0.140" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 737 | dependencies = [ 738 | "itoa", 739 | "memchr", 740 | "ryu", 741 | "serde", 742 | ] 743 | 744 | [[package]] 745 | name = "shlex" 746 | version = "1.3.0" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 749 | 750 | [[package]] 751 | name = "signal-hook-registry" 752 | version = "1.4.5" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 755 | dependencies = [ 756 | "libc", 757 | ] 758 | 759 | [[package]] 760 | name = "slab" 761 | version = "0.4.9" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 764 | dependencies = [ 765 | "autocfg", 766 | ] 767 | 768 | [[package]] 769 | name = "smallvec" 770 | version = "1.15.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 773 | 774 | [[package]] 775 | name = "subtle" 776 | version = "2.6.1" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 779 | 780 | [[package]] 781 | name = "syn" 782 | version = "2.0.101" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 785 | dependencies = [ 786 | "proc-macro2", 787 | "quote", 788 | "unicode-ident", 789 | ] 790 | 791 | [[package]] 792 | name = "sys_traits" 793 | version = "0.1.14" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "b0f8c2c55b6b4dd67f0f8df8de9bdf00b16c8ea4fbc4be0c2133d5d3924be5d4" 796 | dependencies = [ 797 | "libc", 798 | "sys_traits_macros", 799 | "windows-sys 0.59.0", 800 | ] 801 | 802 | [[package]] 803 | name = "sys_traits_macros" 804 | version = "0.1.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "181f22127402abcf8ee5c83ccd5b408933fec36a6095cf82cda545634692657e" 807 | dependencies = [ 808 | "proc-macro2", 809 | "quote", 810 | "syn", 811 | ] 812 | 813 | [[package]] 814 | name = "tar" 815 | version = "0.4.44" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" 818 | dependencies = [ 819 | "filetime", 820 | "libc", 821 | "xattr", 822 | ] 823 | 824 | [[package]] 825 | name = "termcolor" 826 | version = "1.4.1" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 829 | dependencies = [ 830 | "winapi-util", 831 | ] 832 | 833 | [[package]] 834 | name = "thiserror" 835 | version = "2.0.12" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 838 | dependencies = [ 839 | "thiserror-impl", 840 | ] 841 | 842 | [[package]] 843 | name = "thiserror-impl" 844 | version = "2.0.12" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 847 | dependencies = [ 848 | "proc-macro2", 849 | "quote", 850 | "syn", 851 | ] 852 | 853 | [[package]] 854 | name = "tokio" 855 | version = "1.45.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" 858 | dependencies = [ 859 | "backtrace", 860 | "bytes", 861 | "libc", 862 | "mio", 863 | "pin-project-lite", 864 | "signal-hook-registry", 865 | "tokio-macros", 866 | "windows-sys 0.52.0", 867 | ] 868 | 869 | [[package]] 870 | name = "tokio-macros" 871 | version = "2.5.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 874 | dependencies = [ 875 | "proc-macro2", 876 | "quote", 877 | "syn", 878 | ] 879 | 880 | [[package]] 881 | name = "unicode-ident" 882 | version = "1.0.18" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 885 | 886 | [[package]] 887 | name = "untrusted" 888 | version = "0.9.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 891 | 892 | [[package]] 893 | name = "ureq" 894 | version = "3.0.11" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" 897 | dependencies = [ 898 | "base64", 899 | "flate2", 900 | "log", 901 | "percent-encoding", 902 | "rustls", 903 | "rustls-pemfile", 904 | "rustls-pki-types", 905 | "ureq-proto", 906 | "utf-8", 907 | "webpki-roots 0.26.11", 908 | ] 909 | 910 | [[package]] 911 | name = "ureq-proto" 912 | version = "0.4.1" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" 915 | dependencies = [ 916 | "base64", 917 | "http", 918 | "httparse", 919 | "log", 920 | ] 921 | 922 | [[package]] 923 | name = "utf-8" 924 | version = "0.7.6" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 927 | 928 | [[package]] 929 | name = "wasi" 930 | version = "0.11.0+wasi-snapshot-preview1" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 933 | 934 | [[package]] 935 | name = "webpki-roots" 936 | version = "0.26.11" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 939 | dependencies = [ 940 | "webpki-roots 1.0.0", 941 | ] 942 | 943 | [[package]] 944 | name = "webpki-roots" 945 | version = "1.0.0" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" 948 | dependencies = [ 949 | "rustls-pki-types", 950 | ] 951 | 952 | [[package]] 953 | name = "winapi-util" 954 | version = "0.1.9" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 957 | dependencies = [ 958 | "windows-sys 0.59.0", 959 | ] 960 | 961 | [[package]] 962 | name = "windows-sys" 963 | version = "0.52.0" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 966 | dependencies = [ 967 | "windows-targets", 968 | ] 969 | 970 | [[package]] 971 | name = "windows-sys" 972 | version = "0.59.0" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 975 | dependencies = [ 976 | "windows-targets", 977 | ] 978 | 979 | [[package]] 980 | name = "windows-targets" 981 | version = "0.52.6" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 984 | dependencies = [ 985 | "windows_aarch64_gnullvm", 986 | "windows_aarch64_msvc", 987 | "windows_i686_gnu", 988 | "windows_i686_gnullvm", 989 | "windows_i686_msvc", 990 | "windows_x86_64_gnu", 991 | "windows_x86_64_gnullvm", 992 | "windows_x86_64_msvc", 993 | ] 994 | 995 | [[package]] 996 | name = "windows_aarch64_gnullvm" 997 | version = "0.52.6" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1000 | 1001 | [[package]] 1002 | name = "windows_aarch64_msvc" 1003 | version = "0.52.6" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1006 | 1007 | [[package]] 1008 | name = "windows_i686_gnu" 1009 | version = "0.52.6" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1012 | 1013 | [[package]] 1014 | name = "windows_i686_gnullvm" 1015 | version = "0.52.6" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1018 | 1019 | [[package]] 1020 | name = "windows_i686_msvc" 1021 | version = "0.52.6" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1024 | 1025 | [[package]] 1026 | name = "windows_x86_64_gnu" 1027 | version = "0.52.6" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1030 | 1031 | [[package]] 1032 | name = "windows_x86_64_gnullvm" 1033 | version = "0.52.6" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1036 | 1037 | [[package]] 1038 | name = "windows_x86_64_msvc" 1039 | version = "0.52.6" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1042 | 1043 | [[package]] 1044 | name = "xattr" 1045 | version = "1.5.0" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" 1048 | dependencies = [ 1049 | "libc", 1050 | "rustix", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "yansi" 1055 | version = "1.0.1" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1058 | 1059 | [[package]] 1060 | name = "zeroize" 1061 | version = "1.8.1" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1064 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::Display, 4 | ops::Deref, 5 | path::Path, 6 | process::{ExitStatus, Stdio}, 7 | sync::{ 8 | Arc, 9 | atomic::{AtomicU32, Ordering}, 10 | }, 11 | }; 12 | 13 | pub use anyhow::Error as AnyError; 14 | use async_trait::async_trait; 15 | use indexmap::IndexMap; 16 | use parking_lot::Mutex; 17 | use protocol::{ 18 | AnyPacket, AnyValue, Encode, FromAnyValue, FromMap, ImportKind, PartialMessage, ProtocolPacket, 19 | }; 20 | use tokio::{ 21 | io::{AsyncReadExt, AsyncWriteExt}, 22 | process::{Child, ChildStdin, ChildStdout}, 23 | sync::{mpsc, oneshot, watch}, 24 | }; 25 | mod flags; 26 | pub mod protocol; 27 | 28 | pub use flags::EsbuildFlagsBuilder; 29 | 30 | pub struct EsbuildService { 31 | exited: watch::Receiver>, 32 | client: ProtocolClient, 33 | } 34 | 35 | impl EsbuildService { 36 | pub fn client(&self) -> &ProtocolClient { 37 | &self.client 38 | } 39 | 40 | pub async fn stop(self) -> Result<(), AnyError> { 41 | self.client.send_stop().await 42 | } 43 | 44 | pub async fn wait_for_exit(&self) -> Result { 45 | if self.exited.borrow().is_some() { 46 | return Ok(self.exited.borrow().unwrap()); 47 | } 48 | let mut exited = self.exited.clone(); 49 | 50 | let _ = exited.changed().await; 51 | let status = exited.borrow().unwrap(); 52 | Ok(status) 53 | } 54 | } 55 | 56 | // fn handle_packet(is_first_packet: bool, packet: &[u8]) { 57 | 58 | // } 59 | // 60 | 61 | struct ProtocolState { 62 | buffer: Vec, 63 | read_into_offset: usize, 64 | offset: usize, 65 | first_packet: bool, 66 | ready_tx: Option>, 67 | packet_tx: mpsc::Sender, 68 | } 69 | 70 | impl ProtocolState { 71 | fn new(ready_tx: oneshot::Sender<()>, packet_tx: mpsc::Sender) -> Self { 72 | let mut buffer = Vec::with_capacity(1024); 73 | buffer.extend(std::iter::repeat_n(0, 1024)); 74 | let read_into_offset = 0; 75 | let offset = 0; 76 | 77 | let first_packet = true; 78 | Self { 79 | buffer, 80 | first_packet, 81 | offset, 82 | read_into_offset, 83 | ready_tx: Some(ready_tx), 84 | packet_tx, 85 | } 86 | } 87 | } 88 | 89 | async fn handle_read(amount: usize, state: &mut ProtocolState) { 90 | state.read_into_offset += amount; 91 | if state.read_into_offset >= state.buffer.len() { 92 | state.buffer.extend(std::iter::repeat_n(0, 1024)); 93 | } 94 | while state.offset + 4 <= state.read_into_offset { 95 | let length = u32::from_le_bytes( 96 | state.buffer[state.offset..state.offset + 4] 97 | .try_into() 98 | .unwrap(), 99 | ) as usize; 100 | // eprintln!( 101 | // "length: {}; offset: {}; read_into_offset: {}", 102 | // length, offset, read_into_offset 103 | // ); 104 | if state.offset + 4 + length > state.read_into_offset { 105 | break; 106 | } 107 | state.offset += 4; 108 | 109 | let message = &state.buffer[state.offset..state.offset + length]; 110 | // eprintln!("here"); 111 | if state.first_packet { 112 | // eprintln!("first packet"); 113 | state.first_packet = false; 114 | // let version = String::from_utf8(message.to_vec()).unwrap(); 115 | // eprintln!("version: {}", version); 116 | state.ready_tx.take().unwrap().send(()).unwrap(); 117 | } else { 118 | match protocol::decode_any_packet(message) { 119 | Ok(packet) => { 120 | log::trace!("decoded packet: {packet:?}"); 121 | state.packet_tx.send(packet).await.unwrap() 122 | } 123 | Err(e) => eprintln!("Error decoding packet: {}", e), 124 | } 125 | } 126 | 127 | state.offset += length; 128 | } 129 | } 130 | 131 | #[allow(clippy::too_many_arguments)] 132 | async fn protocol_task( 133 | mut child: Child, 134 | stdout: ChildStdout, 135 | stdin: ChildStdin, 136 | ready_tx: oneshot::Sender<()>, 137 | mut response_rx: mpsc::Receiver, 138 | packet_tx: mpsc::Sender, 139 | mut stop_rx: mpsc::Receiver<()>, 140 | exited_tx: watch::Sender>, 141 | ) -> Result<(), AnyError> { 142 | let mut stdout = stdout; 143 | 144 | let mut state = ProtocolState::new(ready_tx, packet_tx); 145 | let mut stdin = stdin; 146 | 147 | loop { 148 | tokio::select! { 149 | status = child.wait() => { 150 | let status = status.unwrap(); 151 | log::debug!("esbuild exited with status: {status}"); 152 | let _ = exited_tx.send(Some(status)); 153 | return Ok(()); 154 | } 155 | 156 | _ = stop_rx.recv() => { 157 | stdin.shutdown().await?; 158 | drop(stdout); 159 | let _ = exited_tx.send(None); 160 | child.kill().await?; 161 | 162 | return Ok(()); 163 | } 164 | res = response_rx.recv() => { 165 | let packet: protocol::ProtocolPacket = res.unwrap(); 166 | log::trace!("got send packet from receiver: {packet:?}"); 167 | let mut encoded = Vec::new(); 168 | packet.encode_into(&mut encoded); 169 | stdin.write_all(&encoded).await?; 170 | } 171 | read_length = stdout.read(&mut state.buffer[state.read_into_offset..]) => { 172 | let Ok(read_length) = read_length else { 173 | eprintln!("Error reading stdout"); 174 | continue; 175 | }; 176 | handle_read(read_length, &mut state).await; 177 | // eprintln!( 178 | // "read_length: {}; read_into_offset: {}; offset: {}; buffer.len(): {}", 179 | // read_length, 180 | // read_into_offset, 181 | // offset, 182 | // buffer.len() 183 | // ); 184 | 185 | } 186 | } 187 | } 188 | 189 | #[allow(unreachable_code)] 190 | Ok::<(), AnyError>(()) 191 | } 192 | 193 | pub trait MakePluginHandler { 194 | fn make_plugin_handler(self, client: ProtocolClient) -> Arc; 195 | } 196 | 197 | impl MakePluginHandler for F 198 | where 199 | F: FnOnce(ProtocolClient) -> Arc, 200 | { 201 | fn make_plugin_handler(self, client: ProtocolClient) -> Arc { 202 | (self)(client) 203 | } 204 | } 205 | 206 | impl MakePluginHandler for Arc { 207 | fn make_plugin_handler(self, _client: ProtocolClient) -> Arc { 208 | self 209 | } 210 | } 211 | 212 | impl MakePluginHandler for Arc { 213 | fn make_plugin_handler(self, _client: ProtocolClient) -> Arc { 214 | self 215 | } 216 | } 217 | 218 | pub struct NoopPluginHandler; 219 | #[async_trait(?Send)] 220 | impl PluginHandler for NoopPluginHandler { 221 | async fn on_resolve(&self, _args: OnResolveArgs) -> Result, AnyError> { 222 | Ok(None) 223 | } 224 | async fn on_load(&self, _args: OnLoadArgs) -> Result, AnyError> { 225 | Ok(None) 226 | } 227 | async fn on_start(&self, _args: OnStartArgs) -> Result, AnyError> { 228 | Ok(None) 229 | } 230 | async fn on_end(&self, _args: OnEndArgs) -> Result, AnyError> { 231 | Ok(None) 232 | } 233 | } 234 | 235 | impl MakePluginHandler for Option<()> { 236 | fn make_plugin_handler(self, _client: ProtocolClient) -> Arc { 237 | Arc::new(NoopPluginHandler) 238 | } 239 | } 240 | 241 | #[derive(Default)] 242 | pub struct EsbuildServiceOptions<'a> { 243 | pub cwd: Option<&'a Path>, 244 | } 245 | 246 | impl EsbuildService { 247 | pub async fn new( 248 | path: impl AsRef, 249 | version: &str, 250 | plugin_handler: impl MakePluginHandler, 251 | options: EsbuildServiceOptions<'_>, 252 | ) -> Result { 253 | let path = path.as_ref(); 254 | let mut cmd = tokio::process::Command::new(path); 255 | if let Some(cwd) = options.cwd { 256 | cmd.current_dir(cwd); 257 | } 258 | let mut esbuild = cmd 259 | .arg(format!("--service={}", version)) 260 | .arg("--ping") 261 | .stdin(Stdio::piped()) 262 | .stdout(Stdio::piped()) 263 | .stderr(Stdio::inherit()) 264 | .kill_on_drop(true) 265 | .spawn()?; 266 | 267 | let stdin = esbuild.stdin.take().unwrap(); 268 | let stdout = esbuild.stdout.take().unwrap(); 269 | 270 | let (ready_tx, ready_rx) = oneshot::channel(); 271 | let (packet_tx, mut packet_rx) = mpsc::channel(100); 272 | let (response_tx, response_rx) = mpsc::channel(100); 273 | let (exited_tx, exited_rx) = watch::channel(None); 274 | let (stop_tx, stop_rx) = mpsc::channel(1); 275 | deno_unsync::spawn(protocol_task( 276 | esbuild, 277 | stdout, 278 | stdin, 279 | ready_tx, 280 | response_rx, 281 | packet_tx, 282 | stop_rx, 283 | exited_tx, 284 | )); 285 | 286 | deno_unsync::spawn(async move {}); 287 | 288 | let client = ProtocolClient::new(response_tx.clone(), stop_tx); 289 | let plugin_handler = plugin_handler.make_plugin_handler(client.clone()); 290 | let pending = client.0.pending.clone(); 291 | 292 | deno_unsync::spawn(async move { 293 | loop { 294 | let packet = packet_rx.recv().await; 295 | 296 | log::trace!("got packet from receiver: {packet:?}"); 297 | if let Some(packet) = packet { 298 | let _ = handle_packet(packet, &response_tx, plugin_handler.clone(), &pending) 299 | .await 300 | .inspect_err(|err| { 301 | eprintln!("failed to handle packet {err}"); 302 | }); 303 | } else { 304 | break; 305 | } 306 | } 307 | }); 308 | 309 | let _ = ready_rx.await; 310 | 311 | Ok(Self { 312 | exited: exited_rx, 313 | client, 314 | }) 315 | } 316 | } 317 | 318 | #[derive(Debug, Clone)] 319 | pub struct OnResolveArgs { 320 | pub key: u32, 321 | pub ids: Vec, 322 | pub path: String, 323 | pub importer: Option, 324 | pub kind: ImportKind, 325 | pub namespace: Option, 326 | pub resolve_dir: Option, 327 | pub with: IndexMap, 328 | } 329 | 330 | #[derive(Debug, Default)] 331 | pub struct OnResolveResult { 332 | pub plugin_name: Option, 333 | pub errors: Option>, 334 | pub warnings: Option>, 335 | pub path: Option, 336 | pub external: Option, 337 | pub side_effects: Option, 338 | pub namespace: Option, 339 | pub suffix: Option, 340 | pub plugin_data: Option, 341 | pub watch_files: Option>, 342 | pub watch_dirs: Option>, 343 | } 344 | 345 | fn resolve_result_to_response( 346 | id: u32, 347 | result: Option, 348 | ) -> protocol::OnResolveResponse { 349 | match result { 350 | Some(result) => protocol::OnResolveResponse { 351 | id: Some(id), 352 | plugin_name: result.plugin_name, 353 | errors: result.errors, 354 | warnings: result.warnings, 355 | path: result.path, 356 | external: result.external, 357 | side_effects: result.side_effects, 358 | namespace: result.namespace, 359 | suffix: result.suffix, 360 | plugin_data: result.plugin_data, 361 | watch_files: result.watch_files, 362 | watch_dirs: result.watch_dirs, 363 | }, 364 | None => protocol::OnResolveResponse { 365 | id: Some(id), 366 | ..Default::default() 367 | }, 368 | } 369 | } 370 | 371 | fn load_result_to_response(id: u32, result: Option) -> protocol::OnLoadResponse { 372 | match result { 373 | Some(result) => protocol::OnLoadResponse { 374 | id: result.id, 375 | plugin_name: result.plugin_name, 376 | errors: result.errors, 377 | warnings: result.warnings, 378 | contents: result.contents, 379 | resolve_dir: result.resolve_dir, 380 | loader: result.loader.map(|loader| loader.to_string()), 381 | plugin_data: result.plugin_data, 382 | watch_files: result.watch_files, 383 | watch_dirs: result.watch_dirs, 384 | }, 385 | None => protocol::OnLoadResponse { 386 | id: Some(id), 387 | ..Default::default() 388 | }, 389 | } 390 | } 391 | 392 | #[async_trait(?Send)] 393 | pub trait PluginHandler { 394 | async fn on_resolve(&self, _args: OnResolveArgs) -> Result, AnyError>; 395 | async fn on_load(&self, _args: OnLoadArgs) -> Result, AnyError>; 396 | async fn on_start(&self, _args: OnStartArgs) -> Result, AnyError>; 397 | async fn on_end(&self, _args: OnEndArgs) -> Result, AnyError>; 398 | } 399 | 400 | #[derive(Default)] 401 | pub struct OnStartResult { 402 | pub errors: Option>, 403 | pub warnings: Option>, 404 | } 405 | 406 | #[derive(Default)] 407 | pub struct OnLoadResult { 408 | pub id: Option, 409 | pub plugin_name: Option, 410 | pub errors: Option>, 411 | pub warnings: Option>, 412 | pub contents: Option>, 413 | pub resolve_dir: Option, 414 | pub loader: Option, 415 | pub plugin_data: Option, 416 | pub watch_files: Option>, 417 | pub watch_dirs: Option>, 418 | } 419 | 420 | impl std::fmt::Debug for OnLoadResult { 421 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 422 | f.debug_struct("OnLoadResult") 423 | .field("id", &self.id) 424 | .field("plugin_name", &self.plugin_name) 425 | .field("errors", &self.errors) 426 | .field("warnings", &self.warnings) 427 | .field( 428 | "contents", 429 | &self.contents.as_ref().map(|c| String::from_utf8_lossy(c)), 430 | ) 431 | .field("resolve_dir", &self.resolve_dir) 432 | .field("loader", &self.loader) 433 | .field("plugin_data", &self.plugin_data) 434 | .field("watch_files", &self.watch_files) 435 | .field("watch_dirs", &self.watch_dirs) 436 | .finish() 437 | } 438 | } 439 | 440 | #[derive(Debug)] 441 | pub struct OnLoadArgs { 442 | pub key: u32, 443 | pub ids: Vec, 444 | pub path: String, 445 | pub namespace: String, 446 | pub suffix: String, 447 | pub plugin_data: Option, 448 | pub with: IndexMap, 449 | } 450 | 451 | #[derive(Debug)] 452 | pub struct OnStartArgs { 453 | pub key: u32, 454 | } 455 | 456 | trait PluginHook { 457 | type Request: FromMap; 458 | type Response: Into; 459 | type Result; 460 | type Args: From; 461 | fn call_handler( 462 | plugin_handler: Arc, 463 | args: Self::Args, 464 | ) -> impl Future, AnyError>>; 465 | 466 | fn response_from_result( 467 | id: u32, 468 | result: Result, AnyError>, 469 | ) -> Self::Response; 470 | } 471 | 472 | struct OnResolveHook; 473 | impl PluginHook for OnResolveHook { 474 | type Request = protocol::OnResolveRequest; 475 | type Response = protocol::OnResolveResponse; 476 | type Args = OnResolveArgs; 477 | type Result = OnResolveResult; 478 | async fn call_handler( 479 | plugin_handler: Arc, 480 | args: Self::Args, 481 | ) -> Result, AnyError> { 482 | plugin_handler.on_resolve(args).await 483 | } 484 | fn response_from_result( 485 | id: u32, 486 | result: Result, AnyError>, 487 | ) -> Self::Response { 488 | match result { 489 | Ok(result) => resolve_result_to_response(id, result), 490 | Err(e) => { 491 | log::debug!("error calling on-resolve: {}", e); 492 | protocol::OnResolveResponse::default() 493 | } 494 | } 495 | } 496 | } 497 | 498 | impl From for OnResolveArgs { 499 | fn from(on_resolve: protocol::OnResolveRequest) -> Self { 500 | fn empty_none(s: String) -> Option { 501 | if s.is_empty() { None } else { Some(s) } 502 | } 503 | OnResolveArgs { 504 | key: on_resolve.key, 505 | ids: on_resolve.ids, 506 | path: on_resolve.path, 507 | importer: empty_none(on_resolve.importer), 508 | kind: on_resolve.kind, 509 | namespace: empty_none(on_resolve.namespace), 510 | resolve_dir: on_resolve.resolve_dir, 511 | with: on_resolve.with, 512 | } 513 | } 514 | } 515 | 516 | struct OnLoadHook; 517 | impl PluginHook for OnLoadHook { 518 | type Request = protocol::OnLoadRequest; 519 | type Response = protocol::OnLoadResponse; 520 | type Args = OnLoadArgs; 521 | type Result = OnLoadResult; 522 | async fn call_handler( 523 | plugin_handler: Arc, 524 | args: Self::Args, 525 | ) -> Result, AnyError> { 526 | plugin_handler.on_load(args).await 527 | } 528 | fn response_from_result( 529 | id: u32, 530 | result: Result, AnyError>, 531 | ) -> Self::Response { 532 | match result { 533 | Ok(result) => load_result_to_response(id, result), 534 | Err(e) => { 535 | log::debug!("error calling on-load: {}", e); 536 | protocol::OnLoadResponse::default() 537 | } 538 | } 539 | } 540 | } 541 | impl From for OnLoadArgs { 542 | fn from(on_load: protocol::OnLoadRequest) -> Self { 543 | OnLoadArgs { 544 | key: on_load.key, 545 | path: on_load.path, 546 | ids: on_load.ids, 547 | namespace: on_load.namespace, 548 | suffix: on_load.suffix, 549 | plugin_data: on_load.plugin_data, 550 | with: on_load.with, 551 | } 552 | } 553 | } 554 | 555 | struct OnStartHook; 556 | impl PluginHook for OnStartHook { 557 | type Request = protocol::OnStartRequest; 558 | type Response = protocol::OnStartResponse; 559 | type Args = OnStartArgs; 560 | type Result = OnStartResult; 561 | async fn call_handler( 562 | plugin_handler: Arc, 563 | args: Self::Args, 564 | ) -> Result, AnyError> { 565 | plugin_handler.on_start(args).await 566 | } 567 | fn response_from_result( 568 | _id: u32, 569 | result: Result, AnyError>, 570 | ) -> Self::Response { 571 | match result { 572 | Ok(Some(result)) => protocol::OnStartResponse { 573 | errors: result.errors.unwrap_or_default(), 574 | warnings: result.warnings.unwrap_or_default(), 575 | }, 576 | Ok(None) => protocol::OnStartResponse::default(), 577 | Err(e) => { 578 | log::debug!("error calling on-start: {}", e); 579 | protocol::OnStartResponse::default() 580 | } 581 | } 582 | } 583 | } 584 | 585 | impl From for OnStartArgs { 586 | fn from(on_start: protocol::OnStartRequest) -> Self { 587 | OnStartArgs { key: on_start.key } 588 | } 589 | } 590 | 591 | #[derive(Debug)] 592 | pub struct OnEndArgs { 593 | pub errors: Vec, 594 | pub warnings: Vec, 595 | pub output_files: Option>, 596 | pub metafile: Option, 597 | pub mangle_cache: Option>, 598 | pub write_to_stdout: Option>, 599 | } 600 | 601 | impl From for protocol::BuildResponse { 602 | fn from(end: OnEndArgs) -> Self { 603 | Self { 604 | errors: end.errors, 605 | warnings: end.warnings, 606 | output_files: end.output_files, 607 | metafile: end.metafile, 608 | mangle_cache: end.mangle_cache, 609 | write_to_stdout: end.write_to_stdout, 610 | } 611 | } 612 | } 613 | 614 | struct OnEndHook; 615 | impl PluginHook for OnEndHook { 616 | type Request = protocol::OnEndRequest; 617 | type Response = protocol::OnEndResponse; 618 | type Args = OnEndArgs; 619 | type Result = OnEndResult; 620 | async fn call_handler( 621 | plugin_handler: Arc, 622 | args: Self::Args, 623 | ) -> Result, AnyError> { 624 | plugin_handler.on_end(args).await 625 | } 626 | fn response_from_result( 627 | _id: u32, 628 | _result: Result, AnyError>, 629 | ) -> Self::Response { 630 | match _result { 631 | Ok(Some(result)) => protocol::OnEndResponse { 632 | errors: result.errors.unwrap_or_default(), 633 | warnings: result.warnings.unwrap_or_default(), 634 | }, 635 | Ok(None) => protocol::OnEndResponse::default(), 636 | Err(e) => { 637 | log::debug!("error calling on-end: {}", e); 638 | protocol::OnEndResponse::default() 639 | } 640 | } 641 | } 642 | } 643 | 644 | #[derive(Default)] 645 | pub struct OnEndResult { 646 | pub errors: Option>, 647 | pub warnings: Option>, 648 | } 649 | 650 | impl From for OnEndArgs { 651 | fn from(value: protocol::OnEndRequest) -> Self { 652 | OnEndArgs { 653 | errors: value.errors, 654 | warnings: value.warnings, 655 | output_files: value.output_files, 656 | metafile: value.metafile, 657 | mangle_cache: value.mangle_cache, 658 | write_to_stdout: value.write_to_stdout, 659 | } 660 | } 661 | } 662 | 663 | async fn handle_hook( 664 | id: u32, 665 | request: H::Request, 666 | response_tx: &mpsc::Sender, 667 | plugin_handler: Arc, 668 | ) -> Result<(), AnyError> { 669 | let args = H::Args::from(request); 670 | let result = H::call_handler(plugin_handler, args).await; 671 | let response = H::response_from_result(id, result); 672 | response_tx 673 | .send(protocol::ProtocolPacket { 674 | id, 675 | is_request: false, 676 | value: protocol::ProtocolMessage::Response(response.into()), 677 | }) 678 | .await?; 679 | 680 | Ok(()) 681 | } 682 | 683 | fn spawn_hook( 684 | id: u32, 685 | map: &IndexMap, 686 | response_tx: &mpsc::Sender, 687 | plugin_handler: Arc, 688 | ) -> Result>, AnyError> 689 | where 690 | H::Request: 'static, 691 | { 692 | let request = H::Request::from_map(map)?; 693 | let response_tx = response_tx.clone(); 694 | let plugin_handler = plugin_handler.clone(); 695 | Ok(deno_unsync::spawn(async move { 696 | handle_hook::(id, request, &response_tx, plugin_handler).await?; 697 | Ok(()) 698 | })) 699 | } 700 | 701 | async fn handle_packet( 702 | packet: AnyPacket, 703 | response_tx: &mpsc::Sender, 704 | plugin_handler: Arc, 705 | pending: &Mutex, 706 | ) -> Result<(), AnyError> { 707 | match &packet.value { 708 | protocol::AnyValue::Map(index_map) => { 709 | if packet.is_request { 710 | match index_map.get("command").map(|v| v.as_string()) { 711 | Some(Ok(s)) => match s.as_str() { 712 | "on-start" => { 713 | handle_hook::( 714 | packet.id, 715 | protocol::OnStartRequest::from_map(index_map)?, 716 | response_tx, 717 | plugin_handler, 718 | ) 719 | .await?; 720 | Ok(()) 721 | } 722 | "on-resolve" => { 723 | spawn_hook::( 724 | packet.id, 725 | index_map, 726 | response_tx, 727 | plugin_handler, 728 | )?; 729 | 730 | Ok(()) 731 | } 732 | "on-load" => { 733 | spawn_hook::( 734 | packet.id, 735 | index_map, 736 | response_tx, 737 | plugin_handler, 738 | )?; 739 | Ok(()) 740 | } 741 | "on-end" => { 742 | spawn_hook::( 743 | packet.id, 744 | index_map, 745 | response_tx, 746 | plugin_handler, 747 | )?; 748 | Ok(()) 749 | } 750 | "ping" => { 751 | response_tx 752 | .send(protocol::ProtocolPacket { 753 | id: packet.id, 754 | is_request: false, 755 | value: protocol::ProtocolMessage::Response( 756 | protocol::PingResponse::default().into(), 757 | ), 758 | }) 759 | .await?; 760 | Ok(()) 761 | } 762 | _ => { 763 | todo!("handle: {:?}", packet.value) 764 | } 765 | }, 766 | _ => { 767 | todo!("handle: {:?}", packet.value) 768 | } 769 | } 770 | } else { 771 | let req_id = packet.id; 772 | let kind = pending.lock().remove(&req_id).unwrap(); 773 | match kind { 774 | protocol::RequestKind::Build(tx) => { 775 | let build_response = 776 | Result::::from_any_value( 777 | packet.value.clone(), 778 | )?; 779 | let _ = tx.send(build_response); 780 | } 781 | protocol::RequestKind::Dispose(tx) => { 782 | let _ = tx.send(()); 783 | } 784 | protocol::RequestKind::Rebuild(tx) => { 785 | let rebuild_response = 786 | Result::::from_any_value( 787 | packet.value.clone(), 788 | )?; 789 | let _ = tx.send(rebuild_response); 790 | } 791 | protocol::RequestKind::Cancel(tx) => { 792 | let _ = tx.send(()); 793 | } 794 | } 795 | 796 | Ok(()) 797 | } 798 | } 799 | _ => { 800 | todo!("handle: {:?}", packet.value) 801 | } 802 | } 803 | } 804 | 805 | type PendingResponseMap = HashMap; 806 | 807 | pub struct ProtocolClientInner { 808 | response_tx: mpsc::Sender, 809 | id: AtomicU32, 810 | pending: Arc>, 811 | stop_tx: mpsc::Sender<()>, 812 | } 813 | 814 | #[derive(Clone)] 815 | pub struct ProtocolClient(Arc); 816 | 817 | impl ProtocolClient { 818 | pub fn new( 819 | response_tx: mpsc::Sender, 820 | stop_tx: mpsc::Sender<()>, 821 | ) -> Self { 822 | Self(Arc::new(ProtocolClientInner::new(response_tx, stop_tx))) 823 | } 824 | } 825 | 826 | impl Deref for ProtocolClient { 827 | type Target = ProtocolClientInner; 828 | fn deref(&self) -> &Self::Target { 829 | &self.0 830 | } 831 | } 832 | 833 | impl ProtocolClientInner { 834 | fn new(response_tx: mpsc::Sender, stop_tx: mpsc::Sender<()>) -> Self { 835 | Self { 836 | response_tx, 837 | id: AtomicU32::new(0), 838 | pending: Default::default(), 839 | stop_tx, 840 | } 841 | } 842 | 843 | async fn send_stop(&self) -> Result<(), AnyError> { 844 | self.stop_tx.send(()).await?; 845 | Ok(()) 846 | } 847 | 848 | pub async fn send_build_request( 849 | &self, 850 | req: protocol::BuildRequest, 851 | ) -> Result, AnyError> { 852 | let id = self.id.fetch_add(1, Ordering::Relaxed); 853 | let packet = protocol::ProtocolPacket { 854 | id, 855 | is_request: true, 856 | value: protocol::ProtocolMessage::Request(protocol::AnyRequest::Build(Box::new(req))), 857 | }; 858 | let (tx, rx) = oneshot::channel(); 859 | self.pending 860 | .lock() 861 | .insert(id, protocol::RequestKind::Build(tx)); 862 | self.response_tx.send(packet).await?; 863 | let response = rx.await?; 864 | Ok(response) 865 | } 866 | 867 | pub async fn send_dispose_request(&self, key: u32) -> Result<(), AnyError> { 868 | let id = self.id.fetch_add(1, Ordering::Relaxed); 869 | let packet = protocol::ProtocolPacket { 870 | id, 871 | is_request: true, 872 | value: protocol::ProtocolMessage::Request(protocol::AnyRequest::Dispose( 873 | protocol::DisposeRequest { key }, 874 | )), 875 | }; 876 | let (tx, rx) = oneshot::channel(); 877 | self.pending 878 | .lock() 879 | .insert(id, protocol::RequestKind::Dispose(tx)); 880 | self.response_tx.send(packet).await?; 881 | rx.await?; 882 | Ok(()) 883 | } 884 | 885 | pub async fn send_rebuild_request( 886 | &self, 887 | key: u32, 888 | ) -> Result, AnyError> { 889 | let id = self.id.fetch_add(1, Ordering::Relaxed); 890 | let packet = protocol::ProtocolPacket { 891 | id, 892 | is_request: true, 893 | value: protocol::ProtocolMessage::Request(protocol::AnyRequest::Rebuild( 894 | protocol::RebuildRequest { key }, 895 | )), 896 | }; 897 | let (tx, rx) = oneshot::channel(); 898 | self.pending 899 | .lock() 900 | .insert(id, protocol::RequestKind::Rebuild(tx)); 901 | self.response_tx.send(packet).await?; 902 | let response = rx.await?; 903 | Ok(response) 904 | } 905 | 906 | pub async fn send_cancel_request(&self, key: u32) -> Result<(), AnyError> { 907 | let id = self.id.fetch_add(1, Ordering::Relaxed); 908 | let packet = protocol::ProtocolPacket { 909 | id, 910 | is_request: true, 911 | value: protocol::ProtocolMessage::Request(protocol::AnyRequest::Cancel( 912 | protocol::CancelRequest { key }, 913 | )), 914 | }; 915 | let (tx, rx) = oneshot::channel(); 916 | self.pending 917 | .lock() 918 | .insert(id, protocol::RequestKind::Cancel(tx)); 919 | self.response_tx.send(packet).await?; 920 | rx.await?; 921 | Ok(()) 922 | } 923 | } 924 | 925 | #[derive(Clone, Debug, Copy)] 926 | pub enum PackagesHandling { 927 | Bundle, 928 | External, 929 | } 930 | 931 | #[derive(Clone, Debug, Copy)] 932 | pub enum Platform { 933 | Node, 934 | Browser, 935 | Neutral, 936 | } 937 | 938 | #[derive(Clone, Debug, Copy)] 939 | pub enum Format { 940 | Esm, 941 | Cjs, 942 | Iife, 943 | } 944 | 945 | #[derive(Clone, Debug, Copy)] 946 | pub enum LogLevel { 947 | Silent, 948 | Error, 949 | Warning, 950 | Info, 951 | Debug, 952 | Verbose, 953 | } 954 | 955 | #[derive(Clone, Debug, Copy)] 956 | pub enum BuiltinLoader { 957 | Js, 958 | Ts, 959 | Jsx, 960 | Json, 961 | Css, 962 | Text, 963 | Binary, 964 | Base64, 965 | DataUrl, 966 | File, 967 | Copy, 968 | Empty, 969 | } 970 | 971 | #[derive(Clone, Debug)] 972 | pub enum Loader { 973 | Builtin(BuiltinLoader), 974 | Map(IndexMap), 975 | } 976 | 977 | impl Display for LogLevel { 978 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 979 | match self { 980 | LogLevel::Silent => write!(f, "silent"), 981 | LogLevel::Error => write!(f, "error"), 982 | LogLevel::Warning => write!(f, "warning"), 983 | LogLevel::Info => write!(f, "info"), 984 | LogLevel::Debug => write!(f, "debug"), 985 | LogLevel::Verbose => write!(f, "verbose"), 986 | } 987 | } 988 | } 989 | 990 | impl Display for Format { 991 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 992 | match self { 993 | Format::Iife => write!(f, "iife"), 994 | Format::Cjs => write!(f, "cjs"), 995 | Format::Esm => write!(f, "esm"), 996 | } 997 | } 998 | } 999 | 1000 | impl Display for Platform { 1001 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1002 | match self { 1003 | Platform::Browser => write!(f, "browser"), 1004 | Platform::Node => write!(f, "node"), 1005 | Platform::Neutral => write!(f, "neutral"), 1006 | } 1007 | } 1008 | } 1009 | 1010 | impl Display for PackagesHandling { 1011 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1012 | match self { 1013 | PackagesHandling::Bundle => write!(f, "bundle"), 1014 | PackagesHandling::External => write!(f, "external"), 1015 | } 1016 | } 1017 | } 1018 | 1019 | impl Display for BuiltinLoader { 1020 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1021 | match self { 1022 | BuiltinLoader::Js => write!(f, "js"), 1023 | BuiltinLoader::Jsx => write!(f, "jsx"), 1024 | BuiltinLoader::Ts => write!(f, "ts"), 1025 | BuiltinLoader::Json => write!(f, "json"), 1026 | BuiltinLoader::Css => write!(f, "css"), 1027 | BuiltinLoader::Text => write!(f, "text"), 1028 | BuiltinLoader::Base64 => write!(f, "base64"), 1029 | BuiltinLoader::DataUrl => write!(f, "dataurl"), 1030 | BuiltinLoader::File => write!(f, "file"), 1031 | BuiltinLoader::Binary => write!(f, "binary"), 1032 | BuiltinLoader::Copy => write!(f, "copy"), 1033 | BuiltinLoader::Empty => write!(f, "empty"), 1034 | } 1035 | } 1036 | } 1037 | 1038 | #[derive(Clone, Debug, Copy)] 1039 | pub enum Sourcemap { 1040 | Linked, 1041 | External, 1042 | Inline, 1043 | Both, 1044 | } 1045 | 1046 | impl Display for Sourcemap { 1047 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1048 | match self { 1049 | Sourcemap::Linked => write!(f, "linked"), 1050 | Sourcemap::External => write!(f, "external"), 1051 | Sourcemap::Inline => write!(f, "inline"), 1052 | Sourcemap::Both => write!(f, "both"), 1053 | } 1054 | } 1055 | } 1056 | 1057 | #[cfg_attr(feature = "serde", derive(serde::Deserialize))] 1058 | #[derive(Clone, Debug)] 1059 | pub struct Metafile { 1060 | #[cfg_attr(feature = "serde", serde(default))] 1061 | pub inputs: HashMap, 1062 | #[cfg_attr(feature = "serde", serde(default))] 1063 | pub outputs: HashMap, 1064 | } 1065 | 1066 | #[cfg_attr(feature = "serde", derive(serde::Deserialize))] 1067 | #[derive(Clone, Debug)] 1068 | pub struct MetafileInput { 1069 | pub bytes: u64, 1070 | pub imports: Vec, 1071 | pub format: Option, 1072 | pub with: Option>, 1073 | } 1074 | 1075 | #[cfg_attr(feature = "serde", derive(serde::Deserialize))] 1076 | #[derive(Clone, Debug)] 1077 | pub struct MetafileInputImport { 1078 | pub path: String, 1079 | pub kind: ImportKind, 1080 | #[cfg_attr(feature = "serde", serde(default))] 1081 | pub external: bool, 1082 | pub original: Option, 1083 | pub with: Option>, 1084 | } 1085 | 1086 | #[cfg_attr(feature = "serde", derive(serde::Deserialize))] 1087 | #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] 1088 | #[derive(Clone, Debug)] 1089 | pub struct MetafileOutput { 1090 | pub bytes: u64, 1091 | pub inputs: HashMap, 1092 | pub imports: Vec, 1093 | #[cfg_attr(feature = "serde", serde(default))] 1094 | pub exports: Vec, 1095 | pub entry_point: Option, 1096 | pub css_bundle: Option, 1097 | } 1098 | 1099 | #[cfg_attr(feature = "serde", derive(serde::Deserialize))] 1100 | #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] 1101 | #[derive(Clone, Debug)] 1102 | pub struct MetafileOutputInput { 1103 | pub bytes_in_output: u64, 1104 | } 1105 | 1106 | #[cfg_attr(feature = "serde", derive(serde::Deserialize))] 1107 | #[derive(Clone, Debug)] 1108 | pub struct MetafileOutputImport { 1109 | pub path: String, 1110 | pub kind: ImportKind, 1111 | #[cfg_attr(feature = "serde", serde(default))] 1112 | pub external: Option, 1113 | } 1114 | --------------------------------------------------------------------------------