├── .rustfmt.toml ├── .gitignore ├── rust-toolchain.toml ├── src ├── lib.rs ├── main.rs ├── handler.rs └── configuration.rs ├── .github ├── CONTRIBUTING.md └── workflows │ ├── release.yml │ ├── ci.generate.ts │ └── ci.yml ├── .cargo └── config.toml ├── tests ├── specs │ ├── General │ │ ├── Rustfmt.txt │ │ ├── Associations_Match.txt │ │ ├── Associations_NoMatch.txt │ │ └── Stdout_All.txt │ └── FormatFile │ │ ├── MultiLineFile.txt │ │ └── OneLineFile.txt ├── resources │ ├── one-line.txt │ └── multi-line.txt ├── tests.rs └── fold.ts ├── dprint.json ├── scripts ├── create_plugin_file.ts └── generate_release_notes.ts ├── Cargo.toml ├── LICENSE ├── deployment └── schema.json ├── README.md └── Cargo.lock /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | tab_spaces = 2 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | .vscode 3 | *.iml 4 | /.idea 5 | /target 6 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.89.0" 3 | components = ["clippy", "rustfmt"] 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate dprint_core; 2 | 3 | pub mod configuration; 4 | pub mod handler; 5 | 6 | pub use handler::format_bytes; 7 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Install [Deno](https://deno.land/) which is used in some tests. 4 | 1. Install [Rust](https://www.rust-lang.org/) 5 | 1. Run `cargo test` to run the tests. 6 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/rust-lang/rust/issues/46651#issuecomment-433611633 2 | [target.aarch64-unknown-linux-musl] 3 | linker = "aarch64-linux-gnu-gcc" 4 | rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc"] 5 | -------------------------------------------------------------------------------- /tests/specs/General/Rustfmt.txt: -------------------------------------------------------------------------------- 1 | -- file.rs -- 2 | ~~ { 3 | "lineWidth": 35, 4 | "commands": [{ 5 | "command": "rustfmt", 6 | "exts": ["rs"] 7 | }] 8 | } ~~ 9 | == should format with rustfmt == 10 | struct Test { } 11 | 12 | [expect] 13 | struct Test {} 14 | -------------------------------------------------------------------------------- /tests/resources/one-line.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /tests/resources/multi-line.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 2 | et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation 3 | ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit 4 | in voluptate velit esse cillum dolore eu 5 | fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 6 | deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /tests/specs/General/Associations_Match.txt: -------------------------------------------------------------------------------- 1 | -- resources/test.txt -- 2 | ~~ { 3 | "lineWidth": 35, 4 | "commands": [{ 5 | "associations": "**/*.txt", 6 | "command": "deno run -A ./tests/fold.ts -w {{line_width}}" 7 | }] 8 | } ~~ 9 | == should format when the association matches == 10 | Testing this out with some very very long text testing testing testing testing testing. 11 | 12 | [expect] 13 | Testing this out with some very very 14 | long text testing testing testing 15 | testing testing. 16 | -------------------------------------------------------------------------------- /tests/specs/General/Associations_NoMatch.txt: -------------------------------------------------------------------------------- 1 | -- resources/test.txt -- 2 | ~~ { 3 | "lineWidth": 35, 4 | "commands": [{ 5 | "associations": "**/*.rs", 6 | "command": "deno run -A ./tests/fold.ts -w {{line_width}}" 7 | }] 8 | } ~~ 9 | == should do nothing when the association doesn't match == 10 | Testing this out with some very very long text testing testing testing testing testing. 11 | 12 | [expect] 13 | Testing this out with some very very long text testing testing testing testing testing. 14 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use dprint_core::plugins::process::get_parent_process_id_from_cli_args; 3 | use dprint_core::plugins::process::handle_process_stdio_messages; 4 | use dprint_core::plugins::process::start_parent_process_checker_task; 5 | use dprint_plugin_exec::handler::ExecHandler; 6 | 7 | fn main() -> Result<()> { 8 | let rt = tokio::runtime::Builder::new_current_thread() 9 | .enable_time() 10 | .build() 11 | .unwrap(); 12 | 13 | rt.block_on(async move { 14 | if let Some(parent_process_id) = get_parent_process_id_from_cli_args() { 15 | start_parent_process_checker_task(parent_process_id); 16 | } 17 | 18 | handle_process_stdio_messages(ExecHandler).await 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentWidth": 2, 3 | "lineWidth": 100, 4 | "exec": { 5 | "cwd": "${configDir}", 6 | "commands": [{ 7 | "command": "rustfmt --edition 2024 --config imports_granularity=item", 8 | "exts": ["rs"] 9 | }] 10 | }, 11 | "excludes": [ 12 | "**/dist", 13 | "**/target", 14 | "**/*-lock.json" 15 | ], 16 | "plugins": [ 17 | "https://plugins.dprint.dev/json-0.19.4.wasm", 18 | "https://plugins.dprint.dev/markdown-0.17.8.wasm", 19 | "https://plugins.dprint.dev/toml-0.6.3.wasm", 20 | "https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0", 21 | "https://plugins.dprint.dev/typescript-0.93.3.wasm" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /scripts/create_plugin_file.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | CargoToml, 4 | processPlugin, 5 | } from "https://raw.githubusercontent.com/dprint/automation/0.10.0/mod.ts"; 6 | 7 | const currentDirPath = $.path(import.meta.dirname!); 8 | const cargoFilePath = currentDirPath.join("../Cargo.toml"); 9 | 10 | await processPlugin.createDprintOrgProcessPlugin({ 11 | pluginName: "dprint-plugin-exec", 12 | version: new CargoToml(cargoFilePath).version(), 13 | platforms: [ 14 | "darwin-aarch64", 15 | "darwin-x86_64", 16 | "linux-aarch64", 17 | "linux-aarch64-musl", 18 | "linux-x86_64", 19 | "linux-x86_64-musl", 20 | "linux-riscv64", 21 | "windows-x86_64", 22 | ], 23 | isTest: Deno.args.some(a => a == "--test"), 24 | }); 25 | -------------------------------------------------------------------------------- /tests/specs/FormatFile/MultiLineFile.txt: -------------------------------------------------------------------------------- 1 | -- resources/multi-line.txt -- 2 | ~~ { 3 | "lineWidth": 42, 4 | "commands": [{ 5 | "command": "deno run -A ./tests/fold.ts -w {{line_width}} {{file_path}}", 6 | "exts": ["txt"] 7 | }] 8 | } ~~ 9 | == Long text == 10 | // does not matter 11 | 12 | [expect] 13 | Lorem ipsum dolor sit amet, consectetur 14 | adipiscing elit, sed do eiusmod tempor 15 | incididunt ut labore et dolore magna 16 | aliqua. Ut enim ad minim veniam, quis 17 | nostrud exercitation ullamco laboris nisi 18 | ut aliquip ex ea commodo consequat. Duis 19 | aute irure dolor in reprehenderit in 20 | voluptate velit esse cillum dolore eu 21 | fugiat nulla pariatur. Excepteur sint 22 | occaecat cupidatat non proident, sunt in 23 | culpa qui officia deserunt mollit anim id 24 | est laborum. 25 | -------------------------------------------------------------------------------- /tests/specs/FormatFile/OneLineFile.txt: -------------------------------------------------------------------------------- 1 | -- resources/one-line.txt -- 2 | ~~ { 3 | "lineWidth": 30, 4 | "commands": [{ 5 | "command": "deno run -A ./tests/fold.ts -w {{line_width}} {{file_path}}", 6 | "exts": ["txt"], 7 | "stdin": false 8 | }] 9 | } ~~ 10 | == Long text == 11 | // does not matter 12 | 13 | [expect] 14 | Lorem ipsum dolor sit amet, 15 | consectetur adipiscing elit, 16 | sed do eiusmod tempor 17 | incididunt ut labore et dolore 18 | magna aliqua. Ut enim ad minim 19 | veniam, quis nostrud 20 | exercitation ullamco laboris 21 | nisi ut aliquip ex ea commodo 22 | consequat. Duis aute irure 23 | dolor in reprehenderit in 24 | voluptate velit esse cillum 25 | dolore eu fugiat nulla 26 | pariatur. Excepteur sint 27 | occaecat cupidatat non 28 | proident, sunt in culpa qui 29 | officia deserunt mollit anim id 30 | est laborum. 31 | -------------------------------------------------------------------------------- /scripts/generate_release_notes.ts: -------------------------------------------------------------------------------- 1 | import { generateChangeLog } from "https://raw.githubusercontent.com/dprint/automation/0.10.0/changelog.ts"; 2 | 3 | const version = Deno.args[0]; 4 | const checksum = Deno.args[1]; 5 | const changelog = await generateChangeLog({ 6 | versionTo: version, 7 | }); 8 | const text = `## Changes 9 | 10 | ${changelog} 11 | 12 | ## Install 13 | 14 | Dependencies: 15 | 16 | - Install dprint's CLI >= 0.40.0 17 | 18 | In a dprint configuration file: 19 | 20 | 1. Specify the plugin url and checksum in the \`"plugins"\` array or run \`dprint config add exec\`: 21 | \`\`\`jsonc 22 | { 23 | // etc... 24 | "plugins": [ 25 | "https://plugins.dprint.dev/exec-${version}.json@${checksum}" 26 | ] 27 | } 28 | \`\`\` 29 | 2. Follow the configuration setup instructions found at https://github.com/dprint/dprint-plugin-exec#configuration 30 | `; 31 | 32 | console.log(text); 33 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dprint-plugin-exec" 3 | version = "0.6.0" 4 | authors = ["Alex Zherebtsov ", "David Sherret "] 5 | edition = "2024" 6 | homepage = "https://github.com/dprint/dprint-plugin-exec" 7 | keywords = ["formatting", "formatter", "exec"] 8 | license = "MIT" 9 | repository = "https://github.com/dprint/dprint-plugin-exec" 10 | description = "Code formatter based on external tool execution." 11 | 12 | [profile.release] 13 | opt-level = 3 14 | debug = false 15 | lto = true 16 | debug-assertions = false 17 | overflow-checks = false 18 | panic = "abort" 19 | 20 | [dependencies] 21 | anyhow = "1.0.86" 22 | dprint-core = { version = "0.67.0", features = ["process"] } 23 | globset = "0.4.14" 24 | handlebars = "5.1.2" 25 | serde = { version = "1.0.204", features = ["derive"] } 26 | sha2 = "0.10.9" 27 | splitty = "1.0.1" 28 | tokio = { version = "1.38.0", features = ["time"] } 29 | 30 | [dev-dependencies] 31 | dprint-development = "0.10.1" 32 | pretty_assertions = "1.4.0" 33 | serde_json = "1.0.120" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (MIT) 2 | 3 | Copyright (c) 2022 Canva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the \"Software\"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: 'Kind of release' 8 | default: 'minor' 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | required: true 14 | 15 | jobs: 16 | rust: 17 | name: release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v4 24 | with: 25 | token: ${{ secrets.GH_DPRINTBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v2 28 | - uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Bump version and tag 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GH_DPRINTBOT_PAT }} 33 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 34 | run: | 35 | git config user.email "${{ github.actor }}@users.noreply.github.com" 36 | git config user.name "${{ github.actor }}" 37 | deno run -A https://raw.githubusercontent.com/dprint/automation/0.10.0/tasks/publish_release.ts --${{github.event.inputs.releaseKind}} 38 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | extern crate dprint_development; 2 | extern crate dprint_plugin_exec; 3 | 4 | #[test] 5 | fn test_specs() { 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | use std::sync::Arc; 9 | 10 | use dprint_core::configuration::*; 11 | use dprint_development::*; 12 | use dprint_plugin_exec::configuration::Configuration; 13 | 14 | let mut tests_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 15 | tests_dir.push("tests"); 16 | 17 | let runtime = tokio::runtime::Builder::new_current_thread() 18 | .enable_time() 19 | .build() 20 | .unwrap(); 21 | let handle = runtime.handle().clone(); 22 | 23 | run_specs( 24 | &PathBuf::from("./tests/specs"), 25 | &ParseSpecOptions { 26 | default_file_name: "default.txt", 27 | }, 28 | &RunSpecsOptions { 29 | fix_failures: false, 30 | format_twice: true, 31 | }, 32 | Arc::new(move |file_name, file_text, spec_config| { 33 | let map: ConfigKeyMap = serde_json::from_value(spec_config.clone().into()).unwrap(); 34 | let config_result = Configuration::resolve(map, &Default::default()); 35 | ensure_no_diagnostics(&config_result.diagnostics); 36 | 37 | let mut file = file_name.to_path_buf(); 38 | let mut td = tests_dir.clone(); 39 | if !file_name.ends_with(Path::new("default.txt")) { 40 | td.push(file_name); 41 | file = td.clone(); 42 | } 43 | 44 | eprintln!("{}", file_name.display()); 45 | let file_text = file_text.to_string(); 46 | handle.block_on(async { 47 | dprint_plugin_exec::handler::format_bytes( 48 | file, 49 | file_text.into_bytes(), 50 | Arc::new(config_result.config), 51 | Arc::new(dprint_core::plugins::NullCancellationToken), 52 | ) 53 | .await 54 | .map(|maybe_bytes| maybe_bytes.map(|bytes| String::from_utf8(bytes).unwrap())) 55 | }) 56 | }), 57 | Arc::new(move |_file_name, _file_text, _spec_config| panic!("Not supported.")), 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /tests/specs/General/Stdout_All.txt: -------------------------------------------------------------------------------- 1 | ~~ { 2 | "lineWidth": 30, 3 | "commands": [{ 4 | "command": "deno run -A ./tests/fold.ts -w 30", 5 | "exts": "txt" 6 | }] 7 | } ~~ 8 | == Process returns the formatted text via stdout == 9 | this should be wrapped because it is a long text 10 | 11 | [expect] 12 | this should be wrapped because 13 | it is a long text 14 | 15 | == Long text == 16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 17 | 18 | [expect] 19 | Lorem ipsum dolor sit amet, 20 | consectetur adipiscing elit, 21 | sed do eiusmod tempor 22 | incididunt ut labore et dolore 23 | magna aliqua. Ut enim ad minim 24 | veniam, quis nostrud 25 | exercitation ullamco laboris 26 | nisi ut aliquip ex ea commodo 27 | consequat. Duis aute irure 28 | dolor in reprehenderit in 29 | voluptate velit esse cillum 30 | dolore eu fugiat nulla 31 | pariatur. Excepteur sint 32 | occaecat cupidatat non 33 | proident, sunt in culpa qui 34 | officia deserunt mollit anim id 35 | est laborum. 36 | 37 | == Newlines in long text preserved == 38 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 39 | et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation 40 | ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit 41 | in voluptate velit esse cillum dolore eu 42 | fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 43 | deserunt mollit anim id est laborum. 44 | 45 | [expect] 46 | Lorem ipsum dolor sit amet, 47 | consectetur adipiscing elit, 48 | sed do eiusmod tempor 49 | incididunt ut labore et dolore 50 | magna aliqua. Ut enim ad minim 51 | veniam, quis nostrud 52 | exercitation ullamco laboris 53 | nisi ut aliquip ex ea commodo 54 | consequat. Duis aute irure 55 | dolor in reprehenderit in 56 | voluptate velit esse cillum 57 | dolore eu fugiat nulla 58 | pariatur. Excepteur sint 59 | occaecat cupidatat non 60 | proident, sunt in culpa qui 61 | officia deserunt mollit anim id 62 | est laborum. 63 | -------------------------------------------------------------------------------- /tests/fold.ts: -------------------------------------------------------------------------------- 1 | // re-implementation of the unix "fold" command so it runs on Windows 2 | 3 | const args = parseArgs(Deno.args); 4 | const fileText = args.filePath == null 5 | ? await readStdin() 6 | : await Deno.readTextFile(args.filePath); 7 | 8 | console.log(limitLinesToWidth( 9 | paragraphsToSingleLines(fileText), 10 | args.lineWidth, 11 | )); 12 | 13 | function paragraphsToSingleLines(text: string) { 14 | text = text.trim(); 15 | let finalText = ""; 16 | let previousLine: string | undefined = undefined; 17 | for (const line of text.split(/\r?\n/g).map((l) => l.trim())) { 18 | const previousLineEmpty = previousLine == null || previousLine.length === 0; 19 | if (line.length === 0) { 20 | finalText += "\n"; 21 | if (!previousLineEmpty) { 22 | finalText += "\n"; 23 | } 24 | } else if (previousLineEmpty) { 25 | finalText += line; 26 | } else { 27 | finalText += " " + line; 28 | } 29 | previousLine = line; 30 | } 31 | return finalText; 32 | } 33 | 34 | function limitLinesToWidth(fileText: string, width: number) { 35 | let finalText = ""; 36 | let lineLength = 0; 37 | for (const token of tokenize(fileText)) { 38 | if (token.kind === "newline") { 39 | finalText += "\n"; 40 | lineLength = 0; 41 | } else if (token.kind === "word") { 42 | if (lineLength + token.text.length > width) { 43 | finalText += "\n"; 44 | lineLength = 0; 45 | } 46 | if (lineLength > 0) { 47 | finalText += " "; 48 | lineLength++; 49 | } 50 | finalText += token.text; 51 | lineLength += token.text.length; 52 | } 53 | } 54 | 55 | return finalText; 56 | } 57 | 58 | type Token = Word | NewLine; 59 | 60 | interface Word { 61 | kind: "word"; 62 | text: string; 63 | } 64 | 65 | interface NewLine { 66 | kind: "newline"; 67 | } 68 | 69 | function* tokenize(text: string): Iterable { 70 | text = text.trim(); 71 | let wordStart = 0; 72 | for (let i = 0; i < text.length; i++) { 73 | if (text[i] === "\n" || text[i] === " ") { 74 | const word = text.slice(wordStart, i).trim(); 75 | if (word.length > 0) { 76 | yield { kind: "word", text: word }; 77 | } 78 | if (text[i] === "\n") { 79 | yield { kind: "newline" }; 80 | } 81 | wordStart = i + 1; 82 | } 83 | } 84 | 85 | const word = text.slice(wordStart).trim(); 86 | if (word.length > 0) { 87 | yield { kind: "word", text: word }; 88 | } 89 | } 90 | 91 | async function readStdin() { 92 | const readable = Deno.stdin.readable.pipeThrough(new TextDecoderStream()); 93 | let finalText = ""; 94 | for await (const chunk of readable) { 95 | finalText += chunk; 96 | } 97 | return finalText; 98 | } 99 | 100 | interface Args { 101 | lineWidth: number; 102 | filePath: string | undefined; 103 | } 104 | 105 | function parseArgs(args: string[]): Args { 106 | // super super basic so it works in the tests 107 | let i = 0; 108 | let lineWidth = 80; 109 | if (args[i] === "-w") { 110 | lineWidth = parseInt(args[++i], 10); 111 | } 112 | const filePath = args[++i]; 113 | if (args[i + 1] != null) { 114 | throw new Error("Invalid"); 115 | } 116 | return { 117 | lineWidth, 118 | filePath, 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /deployment/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://plugins.dprint.dev/dprint/dprint-plugin-exec/0.0.0/schema.json", 4 | "type": "object", 5 | "required": ["commands"], 6 | "properties": { 7 | "locked": { 8 | "description": "Whether the configuration is not allowed to be overriden or extended.", 9 | "type": "boolean" 10 | }, 11 | "lineWidth": { 12 | "description": "The width of a line the printer will try to stay under. Note that the printer may exceed this width in certain cases.", 13 | "default": 120, 14 | "type": "number" 15 | }, 16 | "indentWidth": { 17 | "description": "The number of characters for an indent.", 18 | "default": 2, 19 | "type": "number" 20 | }, 21 | "useTabs": { 22 | "description": "Whether to use tabs (true) or spaces (false).", 23 | "type": "boolean", 24 | "default": false, 25 | "oneOf": [{ 26 | "const": true, 27 | "description": "" 28 | }, { 29 | "const": false, 30 | "description": "" 31 | }] 32 | }, 33 | "associations": { 34 | "description": "Glob pattern that associates this plugin with certain file paths (ex. \"**/*.{rs,java,py}\").", 35 | "anyOf": [{ 36 | "description": "Glob pattern that associates this plugin with certain file paths (ex. \"**/*.{rs,java,py}\").", 37 | "type": "string" 38 | }, { 39 | "description": "Glob patterns that associates this plugin with certain file paths.", 40 | "type": "array", 41 | "items": { 42 | "type": "string" 43 | } 44 | }] 45 | }, 46 | "cacheKey": { 47 | "description": "Optional value used to bust dprint's incremental cache (ex. provide \"1\"). This is useful if you want to force formatting to occur because the underlying command has changed.", 48 | "type": "string" 49 | }, 50 | "cwd": { 51 | "type": "string", 52 | "description": "The current working directory to launch all executables with." 53 | }, 54 | "timeout": { 55 | "description": "Number of seconds to allow a format to progress before a timeout error occurs.", 56 | "type": "number", 57 | "default": 30 58 | }, 59 | "commands": { 60 | "description": "Commands to format with.", 61 | "type": "array", 62 | "items": { 63 | "type": "object", 64 | "properties": { 65 | "command": { 66 | "description": "The commmand to execute to format with.", 67 | "type": "string" 68 | }, 69 | "exts": { 70 | "description": "File extensions to use this command for.", 71 | "anyOf": [{ 72 | "description": "File extension to use this command for.", 73 | "type": "string" 74 | }, { 75 | "description": "File extensions to use this command for.", 76 | "type": "array", 77 | "items": [{ 78 | "type": "string" 79 | }] 80 | }] 81 | }, 82 | "fileNames": { 83 | "description": "File names to format with this command. Useful for filenames without extensions", 84 | "anyOf": [{ 85 | "description": "File name to format with this command. Useful for filenames without extensions.", 86 | "type": "string" 87 | }, { 88 | "description": "File names to format with this command. Useful for filenames without extensions", 89 | "type": "array", 90 | "items": [{ 91 | "type": "string" 92 | }] 93 | }] 94 | }, 95 | "stdin": { 96 | "type": "boolean", 97 | "description": "Whether to pass the file text in via stdin.", 98 | "default": true 99 | }, 100 | "cwd": { 101 | "type": "string", 102 | "description": "The current working directory to launch the executable with." 103 | }, 104 | "associations": { 105 | "description": "Glob pattern that associates certain file paths with this command. Prefer using 'exts' instead.", 106 | "anyOf": [{ 107 | "description": "Glob pattern that associates certain file paths with this command.", 108 | "type": "string" 109 | }, { 110 | "description": "Glob patterns that associates certain file paths with this command.", 111 | "type": "array", 112 | "items": { 113 | "type": "string" 114 | } 115 | }] 116 | } 117 | }, 118 | "required": [ 119 | "command" 120 | ] 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dprint-plugin-exec 2 | 3 | Plugin that formats code via mostly any formatting CLI found on the host machine. 4 | 5 | This plugin executes CLI commands to format code via stdin (recommended) or via a file path. 6 | 7 | ## Install 8 | 9 | 1. Install [dprint](https://dprint.dev/install/) 10 | 2. Follow instructions at https://github.com/dprint/dprint-plugin-exec/releases/ 11 | 12 | ## Configuration 13 | 14 | 1. Add general configuration if desired (shown below). 15 | 1. Add binaries similar to what's shown below and specify what file extensions they match via a `exts` property. 16 | 17 | ```jsonc 18 | { 19 | // ...etc... 20 | "exec": { 21 | // lets the plugin know the cwd, see https://dprint.dev/config/#configuration-variables 22 | "cwd": "${configDir}", 23 | 24 | // general config (optional -- shown are the defaults) 25 | "lineWidth": 120, 26 | "indentWidth": 2, 27 | "useTabs": false, 28 | "cacheKey": "1", 29 | "timeout": 30, 30 | 31 | // now define your commands, for example... 32 | "commands": [{ 33 | "command": "rustfmt", 34 | "exts": ["rs"] 35 | }, { 36 | "command": "java -jar formatter.jar {{file_path}}", 37 | "exts": ["java"] 38 | }, { 39 | "command": "yapf", 40 | "exts": ["py"] 41 | }] 42 | }, 43 | "plugins": [ 44 | // run `dprint config add exec` to add the latest exec plugin's url here 45 | ] 46 | } 47 | ``` 48 | 49 | General config: 50 | 51 | - `cacheKey` - Optional value used to bust dprint's incremental cache (ex. provide `"1"`). This is useful if you want to force formatting to occur because the underlying command's code has changed. 52 | - If you want to automatically calculate the cache key, consider using `command.cacheKeyFiles`. 53 | - `timeout` - Number of seconds to allow an executable format to occur before a timeout error occurs (default: `30`). 54 | - `cwd` - Recommend setting this to `${configDir}` to force it to use the cwd of the current config file. 55 | 56 | Command config: 57 | 58 | - `command` - Command to execute. 59 | - `exts` - Array of file extensions to format with this command. 60 | - `fileNames` - Array of file names to format with this command (useful for files without extensions). 61 | - `associations` - File patterns to format with this command. If specified, then you MUST specify associations on this plugin's config as well. 62 | - You may have associations match multiple binaries in order to format a file with multiple binaries instead of just one. The order in the config file will dictate the order the formatting occurs in. 63 | - `stdin` - If the text should be provided via stdin (default: `true`) 64 | - `cwd` - Current working directory to use when launching this command (default: dprint's cwd or the root `cwd` setting if set) 65 | - `cacheKeyFiles` - A list of paths (relative to `cwd`) to files used to automatically compute a `cacheKey`. This allows automatic invalidation of dprint's incremental cache when any of these files are changed. 66 | 67 | Command templates (ex. see the prettier example above): 68 | 69 | - `{{file_path}}` - File path being formatted. 70 | - `{{line_width}}` - Configured line width. 71 | - `{{use_tabs}}` - Whether tabs should be used. 72 | - `{{indent_width}}` - Whether tabs should be used. 73 | - `{{cwd}}` - Current working directory. 74 | - `{{timeout}}` - Specified timeout in seconds. 75 | 76 | ### Example - yapf 77 | 78 | ```jsonc 79 | { 80 | // ...etc... 81 | "exec": { 82 | "cwd": "${configDir}", 83 | "commands": [{ 84 | "command": "yapf", 85 | "exts": ["py"] 86 | }] 87 | }, 88 | "plugins": [ 89 | // run `dprint config add exec` to add the latest exec plugin's url here 90 | ] 91 | } 92 | ``` 93 | 94 | ### Example - java 95 | 96 | ```jsonc 97 | { 98 | // ...etc... 99 | "exec": { 100 | "cwd": "${configDir}", 101 | "commands": [{ 102 | "command": "java -jar formatter.jar {{file_path}}", 103 | "exts": ["java"] 104 | }] 105 | }, 106 | "plugins": [ 107 | // run `dprint config add exec` to add the latest exec plugin's url here 108 | ] 109 | } 110 | ``` 111 | 112 | ### Example - rustfmt 113 | 114 | Use the `rustfmt` binary so you can format stdin. 115 | 116 | ```jsonc 117 | { 118 | // ...etc... 119 | "exec": { 120 | "cwd": "${configDir}", 121 | "commands": [{ 122 | "command": "rustfmt --edition 2024", 123 | "exts": ["rs"], 124 | // add the config files for automatic cache invalidation when the rust version or rustfmt config changes 125 | "cacheKeyFiles": [ 126 | "rustfmt.toml", 127 | "rust-toolchain.toml" 128 | ] 129 | }] 130 | }, 131 | "plugins": [ 132 | // run `dprint config add exec` to add the latest exec plugin's url here 133 | ] 134 | } 135 | ``` 136 | 137 | ### Example - prettier 138 | 139 | Consider using [dprint-plugin-prettier](https://dprint.dev/plugins/prettier/) instead as it will be much faster. 140 | 141 | ```jsonc 142 | { 143 | // ...etc... 144 | "exec": { 145 | "cwd": "${configDir}", 146 | "commands": [{ 147 | "command": "prettier --stdin-filepath {{file_path}} --tab-width {{indent_width}} --print-width {{line_width}}", 148 | // add more extensions that prettier should format 149 | "exts": ["js", "ts", "html"], 150 | // add the config files for automatic cache invalidation when the prettier config config changes 151 | "cacheKeyFiles": [ 152 | ".prettierrc.json" 153 | ] 154 | }] 155 | }, 156 | "plugins": [ 157 | // run `dprint config add exec` to add the latest exec plugin's url here 158 | ] 159 | } 160 | ``` 161 | -------------------------------------------------------------------------------- /.github/workflows/ci.generate.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from "https://deno.land/std@0.170.0/encoding/yaml.ts"; 2 | 3 | enum OperatingSystem { 4 | Macx86 = "macos-13", 5 | MacArm = "macos-latest", 6 | Windows = "windows-latest", 7 | Linux = "ubuntu-22.04", 8 | } 9 | 10 | interface ProfileData { 11 | os: OperatingSystem; 12 | target: string; 13 | cross?: boolean; 14 | runTests?: boolean; 15 | } 16 | 17 | const profileDataItems: ProfileData[] = [{ 18 | os: OperatingSystem.Macx86, 19 | target: "x86_64-apple-darwin", 20 | runTests: true, 21 | }, { 22 | os: OperatingSystem.MacArm, 23 | target: "aarch64-apple-darwin", 24 | runTests: true, 25 | }, { 26 | os: OperatingSystem.Windows, 27 | target: "x86_64-pc-windows-msvc", 28 | runTests: true, 29 | }, { 30 | os: OperatingSystem.Linux, 31 | target: "x86_64-unknown-linux-gnu", 32 | runTests: true, 33 | }, { 34 | os: OperatingSystem.Linux, 35 | target: "x86_64-unknown-linux-musl", 36 | }, { 37 | os: OperatingSystem.Linux, 38 | target: "aarch64-unknown-linux-gnu", 39 | }, { 40 | os: OperatingSystem.Linux, 41 | target: "aarch64-unknown-linux-musl", 42 | }, { 43 | os: OperatingSystem.Linux, 44 | cross: true, 45 | target: "riscv64gc-unknown-linux-gnu", 46 | }]; 47 | const profiles = profileDataItems.map((profile) => { 48 | return { 49 | ...profile, 50 | artifactsName: `${profile.target}-artifacts`, 51 | zipFileName: `dprint-plugin-exec-${profile.target}.zip`, 52 | zipChecksumEnvVarName: `ZIP_CHECKSUM_${profile.target.toUpperCase().replaceAll("-", "_")}`, 53 | }; 54 | }); 55 | 56 | const ci = { 57 | name: "CI", 58 | on: { 59 | pull_request: { branches: ["main"] }, 60 | push: { branches: ["main"], tags: ["*"] }, 61 | }, 62 | concurrency: { 63 | // https://stackoverflow.com/a/72408109/188246 64 | group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}", 65 | "cancel-in-progress": true, 66 | }, 67 | jobs: { 68 | build: { 69 | name: "${{ matrix.config.target }}", 70 | "runs-on": "${{ matrix.config.os }}", 71 | strategy: { 72 | matrix: { 73 | config: profiles.map((profile) => ({ 74 | os: profile.os, 75 | run_tests: (profile.runTests ?? false).toString(), 76 | target: profile.target, 77 | cross: (profile.cross ?? false).toString(), 78 | })), 79 | }, 80 | }, 81 | outputs: Object.fromEntries( 82 | profiles.map((profile) => [ 83 | profile.zipChecksumEnvVarName, 84 | "${{steps.pre_release_" + profile.target.replaceAll("-", "_") 85 | + ".outputs.ZIP_CHECKSUM}}", 86 | ]), 87 | ), 88 | env: { 89 | // disabled to reduce ./target size and generally it's slower enabled 90 | CARGO_INCREMENTAL: 0, 91 | RUST_BACKTRACE: "full", 92 | }, 93 | steps: [ 94 | { 95 | name: "Prepare git", 96 | run: [ 97 | "git config --global core.autocrlf false", 98 | "git config --global core.eol lf", 99 | ].join("\n"), 100 | }, 101 | { uses: "actions/checkout@v4" }, 102 | { uses: "dsherret/rust-toolchain-file@v1" }, 103 | { 104 | name: "Cache cargo", 105 | if: "startsWith(github.ref, 'refs/tags/') != true", 106 | uses: "Swatinem/rust-cache@v2", 107 | with: { 108 | key: "${{ matrix.config.target }}", 109 | }, 110 | }, 111 | { uses: "denoland/setup-deno@v2" }, 112 | { 113 | name: "Setup (Linux x86_64-musl)", 114 | if: "matrix.config.target == 'x86_64-unknown-linux-musl'", 115 | run: [ 116 | "sudo apt update", 117 | "sudo apt install musl musl-dev musl-tools", 118 | "rustup target add x86_64-unknown-linux-musl", 119 | ].join("\n"), 120 | }, 121 | { 122 | name: "Setup (Linux aarch64)", 123 | if: "matrix.config.target == 'aarch64-unknown-linux-gnu'", 124 | run: [ 125 | "sudo apt update", 126 | "sudo apt install -y gcc-aarch64-linux-gnu", 127 | "rustup target add aarch64-unknown-linux-gnu", 128 | ].join("\n"), 129 | }, 130 | { 131 | name: "Setup (Linux aarch64-musl)", 132 | if: "matrix.config.target == 'aarch64-unknown-linux-musl'", 133 | run: [ 134 | "sudo apt update", 135 | "sudo apt install gcc-aarch64-linux-gnu musl musl-dev musl-tools", 136 | "rustup target add aarch64-unknown-linux-musl", 137 | ].join("\n"), 138 | }, 139 | { 140 | name: "Setup cross", 141 | if: "matrix.config.cross == 'true'", 142 | run: [ 143 | "cargo install cross --git https://github.com/cross-rs/cross --rev 88f49ff79e777bef6d3564531636ee4d3cc2f8d2", 144 | ].join("\n"), 145 | }, 146 | { 147 | name: "Build (Debug)", 148 | if: "matrix.config.cross != 'true' && !startsWith(github.ref, 'refs/tags/')", 149 | env: { 150 | "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "aarch64-linux-gnu-gcc", 151 | }, 152 | run: "cargo build --locked --all-targets --target ${{matrix.config.target}}", 153 | }, 154 | { 155 | name: "Build release", 156 | if: "matrix.config.cross != 'true' && startsWith(github.ref, 'refs/tags/')", 157 | env: { 158 | "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "aarch64-linux-gnu-gcc", 159 | }, 160 | run: "cargo build --locked --all-targets --target ${{matrix.config.target}} --release", 161 | }, 162 | { 163 | name: "Build cross (Debug)", 164 | if: "matrix.config.cross == 'true' && !startsWith(github.ref, 'refs/tags/')", 165 | run: [ 166 | "cross build --locked --target ${{matrix.config.target}}", 167 | ].join("\n"), 168 | }, 169 | { 170 | name: "Build cross (Release)", 171 | if: "matrix.config.cross == 'true' && startsWith(github.ref, 'refs/tags/')", 172 | run: [ 173 | "cross build --locked --target ${{matrix.config.target}} --release", 174 | ].join("\n"), 175 | }, 176 | { 177 | name: "Lint", 178 | if: 179 | "!startsWith(github.ref, 'refs/tags/') && matrix.config.target == 'x86_64-unknown-linux-gnu'", 180 | run: "cargo clippy", 181 | }, 182 | { 183 | name: "Test (Debug)", 184 | if: "matrix.config.run_tests == 'true' && !startsWith(github.ref, 'refs/tags/')", 185 | run: "cargo test --locked --all-features", 186 | }, 187 | { 188 | name: "Test (Release)", 189 | if: "matrix.config.run_tests == 'true' && startsWith(github.ref, 'refs/tags/')", 190 | run: "cargo test --locked --all-features --release", 191 | }, 192 | // zip files 193 | ...profiles.map((profile) => { 194 | function getRunSteps() { 195 | switch (profile.os) { 196 | case OperatingSystem.MacArm: 197 | case OperatingSystem.Macx86: 198 | return [ 199 | `cd target/${profile.target}/release`, 200 | `zip -r ${profile.zipFileName} dprint-plugin-exec`, 201 | `echo \"::set-output name=ZIP_CHECKSUM::$(shasum -a 256 ${profile.zipFileName} | awk '{print $1}')\"`, 202 | ]; 203 | case OperatingSystem.Linux: 204 | return [ 205 | `cd target/${profile.target}/release`, 206 | `zip -r ${profile.zipFileName} dprint-plugin-exec`, 207 | `echo \"::set-output name=ZIP_CHECKSUM::$(shasum -a 256 ${profile.zipFileName} | awk '{print $1}')\"`, 208 | ]; 209 | case OperatingSystem.Windows: 210 | return [ 211 | `Compress-Archive -CompressionLevel Optimal -Force -Path target/${profile.target}/release/dprint-plugin-exec.exe -DestinationPath target/${profile.target}/release/${profile.zipFileName}`, 212 | `echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 target/${profile.target}/release/${profile.zipFileName} | awk '{print $1}')"`, 213 | ]; 214 | } 215 | } 216 | return { 217 | name: `Pre-release (${profile.target})`, 218 | id: `pre_release_${profile.target.replaceAll("-", "_")}`, 219 | if: 220 | `matrix.config.target == '${profile.target}' && startsWith(github.ref, 'refs/tags/')`, 221 | run: getRunSteps().join("\n"), 222 | }; 223 | }), 224 | // upload artifacts 225 | ...profiles.map((profile) => { 226 | return { 227 | name: `Upload artifacts (${profile.target})`, 228 | if: 229 | `matrix.config.target == '${profile.target}' && startsWith(github.ref, 'refs/tags/')`, 230 | uses: "actions/upload-artifact@v4", 231 | with: { 232 | name: profile.artifactsName, 233 | path: `target/${profile.target}/release/${profile.zipFileName}`, 234 | }, 235 | }; 236 | }), 237 | ], 238 | }, 239 | draft_release: { 240 | name: "draft_release", 241 | if: "startsWith(github.ref, 'refs/tags/')", 242 | needs: "build", 243 | "runs-on": "ubuntu-latest", 244 | steps: [ 245 | { name: "Checkout", uses: "actions/checkout@v4" }, 246 | { name: "Download artifacts", uses: "actions/download-artifact@v4" }, 247 | { uses: "denoland/setup-deno@v2" }, 248 | { 249 | name: "Move downloaded artifacts to root directory", 250 | run: profiles.map((profile) => { 251 | return `mv ${profile.artifactsName}/${profile.zipFileName} .`; 252 | }).join("\n"), 253 | }, 254 | { 255 | name: "Output checksums", 256 | run: profiles.map((profile) => { 257 | return `echo "${profile.zipFileName}: \${{needs.build.outputs.${profile.zipChecksumEnvVarName}}}"`; 258 | }).join("\n"), 259 | }, 260 | { 261 | name: "Create plugin file", 262 | run: "deno run --allow-read=. --allow-write=. scripts/create_plugin_file.ts", 263 | }, 264 | { 265 | name: "Get tag version", 266 | id: "get_tag_version", 267 | run: "echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\\/tags\\//}", 268 | }, 269 | { 270 | name: "Get plugin file checksum", 271 | id: "get_plugin_file_checksum", 272 | run: 273 | "echo \"::set-output name=CHECKSUM::$(shasum -a 256 plugin.json | awk '{print $1}')\"", 274 | }, 275 | { 276 | name: "Update Config Schema Version", 277 | run: 278 | "sed -i 's/exec\\/0.0.0/exec\\/${{ steps.get_tag_version.outputs.TAG_VERSION }}/' deployment/schema.json", 279 | }, 280 | { 281 | name: "Create release notes", 282 | run: 283 | "deno run -A ./scripts/generate_release_notes.ts ${{ steps.get_tag_version.outputs.TAG_VERSION }} ${{ steps.get_plugin_file_checksum.outputs.CHECKSUM }} > ${{ github.workspace }}-CHANGELOG.txt", 284 | }, 285 | { 286 | name: "Release", 287 | uses: "softprops/action-gh-release@v1", 288 | env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }, 289 | with: { 290 | files: [ 291 | ...profiles.map((profile) => profile.zipFileName), 292 | "plugin.json", 293 | "deployment/schema.json", 294 | ].join("\n"), 295 | "body_path": "${{ github.workspace }}-CHANGELOG.txt", 296 | }, 297 | }, 298 | ], 299 | }, 300 | }, 301 | }; 302 | 303 | let finalText = `# GENERATED BY ./ci.generate.ts -- DO NOT DIRECTLY EDIT\n\n`; 304 | finalText += yaml.stringify(ci, { 305 | noRefs: true, 306 | lineWidth: 10_000, 307 | noCompatMode: true, 308 | }); 309 | 310 | Deno.writeTextFileSync(new URL("./ci.yml", import.meta.url), finalText); 311 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::Write; 3 | use std::ops::Deref; 4 | use std::ops::DerefMut; 5 | use std::path::Path; 6 | use std::path::PathBuf; 7 | use std::process::Command; 8 | use std::process::ExitStatus; 9 | use std::process::Stdio; 10 | use std::sync::Arc; 11 | use std::time::Duration; 12 | 13 | use anyhow::Error; 14 | use anyhow::Result; 15 | use anyhow::anyhow; 16 | use anyhow::bail; 17 | use dprint_core::async_runtime::LocalBoxFuture; 18 | use dprint_core::async_runtime::async_trait; 19 | use dprint_core::configuration::ConfigKeyMap; 20 | use dprint_core::configuration::GlobalConfiguration; 21 | use dprint_core::plugins::AsyncPluginHandler; 22 | use dprint_core::plugins::CancellationToken; 23 | use dprint_core::plugins::FileMatchingInfo; 24 | use dprint_core::plugins::FormatRequest; 25 | use dprint_core::plugins::FormatResult; 26 | use dprint_core::plugins::HostFormatRequest; 27 | use dprint_core::plugins::PluginInfo; 28 | use dprint_core::plugins::PluginResolveConfigurationResult; 29 | use handlebars::Handlebars; 30 | use serde::Deserialize; 31 | use serde::Serialize; 32 | use tokio::sync::oneshot; 33 | use tokio::sync::oneshot::Receiver; 34 | use tokio::sync::oneshot::Sender; 35 | 36 | use crate::configuration::CommandConfiguration; 37 | use crate::configuration::Configuration; 38 | 39 | struct ChildKillOnDrop(std::process::Child); 40 | 41 | impl Drop for ChildKillOnDrop { 42 | fn drop(&mut self) { 43 | let _ignore = self.0.kill(); 44 | } 45 | } 46 | 47 | impl Deref for ChildKillOnDrop { 48 | type Target = std::process::Child; 49 | 50 | fn deref(&self) -> &Self::Target { 51 | &self.0 52 | } 53 | } 54 | 55 | impl DerefMut for ChildKillOnDrop { 56 | fn deref_mut(&mut self) -> &mut Self::Target { 57 | &mut self.0 58 | } 59 | } 60 | 61 | pub struct ExecHandler; 62 | 63 | #[async_trait(?Send)] 64 | impl AsyncPluginHandler for ExecHandler { 65 | type Configuration = Configuration; 66 | 67 | fn plugin_info(&self) -> PluginInfo { 68 | let name = env!("CARGO_PKG_NAME").to_string(); 69 | let version = env!("CARGO_PKG_VERSION").to_string(); 70 | PluginInfo { 71 | name: name.clone(), 72 | version: version.clone(), 73 | config_key: "exec".to_string(), 74 | help_url: env!("CARGO_PKG_HOMEPAGE").to_string(), 75 | config_schema_url: format!( 76 | "https://plugins.dprint.dev/dprint/{}/{}/schema.json", 77 | name, version 78 | ), 79 | update_url: Some(format!( 80 | "https://plugins.dprint.dev/dprint/{}/latest.json", 81 | name 82 | )), 83 | } 84 | } 85 | 86 | fn license_text(&self) -> String { 87 | include_str!("../LICENSE").to_string() 88 | } 89 | 90 | async fn resolve_config( 91 | &self, 92 | config: ConfigKeyMap, 93 | global_config: GlobalConfiguration, 94 | ) -> PluginResolveConfigurationResult { 95 | let result = Configuration::resolve(config, &global_config); 96 | let config = result.config; 97 | PluginResolveConfigurationResult { 98 | file_matching: FileMatchingInfo { 99 | file_extensions: config 100 | .commands 101 | .iter() 102 | .flat_map(|c| c.file_extensions.iter()) 103 | .map(|s| s.trim_start_matches('.').to_string()) 104 | .collect(), 105 | file_names: config 106 | .commands 107 | .iter() 108 | .flat_map(|c| c.file_names.iter()) 109 | .map(|s| s.to_string()) 110 | .collect(), 111 | }, 112 | config, 113 | diagnostics: result.diagnostics, 114 | } 115 | } 116 | 117 | async fn format( 118 | &self, 119 | request: FormatRequest, 120 | _format_with_host: impl FnMut(HostFormatRequest) -> LocalBoxFuture<'static, FormatResult> + 'static, 121 | ) -> FormatResult { 122 | if request.range.is_some() { 123 | // we don't support range formatting for this plugin 124 | return Ok(None); 125 | } 126 | 127 | format_bytes( 128 | request.file_path, 129 | request.file_bytes, 130 | request.config, 131 | request.token.clone(), 132 | ) 133 | .await 134 | } 135 | } 136 | 137 | pub async fn format_bytes( 138 | file_path: PathBuf, 139 | original_file_bytes: Vec, 140 | config: Arc, 141 | token: Arc, 142 | ) -> FormatResult { 143 | fn trim_bytes_len(bytes: &[u8]) -> usize { 144 | let mut start = 0; 145 | let mut end = bytes.len(); 146 | 147 | while start < end && bytes[start].is_ascii_whitespace() { 148 | start += 1; 149 | } 150 | 151 | if start == end { 152 | return 0; 153 | } 154 | 155 | while end > start && bytes[end - 1].is_ascii_whitespace() { 156 | end -= 1; 157 | } 158 | 159 | if end < start { 0 } else { end - start } 160 | } 161 | 162 | let mut file_bytes: Cow> = Cow::Borrowed(&original_file_bytes); 163 | for command in select_commands(&config, &file_path)? { 164 | // format here 165 | let args = maybe_substitute_variables(&file_path, &config, command); 166 | 167 | let mut child = ChildKillOnDrop( 168 | Command::new(&command.executable) 169 | .current_dir(&command.cwd) 170 | .stdout(Stdio::piped()) 171 | .stdin(if command.stdin { 172 | Stdio::piped() 173 | } else { 174 | Stdio::null() 175 | }) 176 | .stderr(Stdio::piped()) 177 | .args(args) 178 | .spawn() 179 | .map_err(|e| anyhow!("Cannot start formatter process: {}", e))?, 180 | ); 181 | 182 | // capturing stdout 183 | let (out_tx, out_rx) = oneshot::channel(); 184 | let mut handles = Vec::with_capacity(2); 185 | if let Some(stdout) = child.stdout.take() { 186 | handles.push(dprint_core::async_runtime::spawn_blocking(|| { 187 | read_stream_lines(stdout, out_tx) 188 | })); 189 | } else { 190 | let _ = child.kill(); 191 | return Err(anyhow!("Formatter did not have a handle for stdout")); 192 | } 193 | 194 | // capturing stderr 195 | let (err_tx, err_rx) = oneshot::channel(); 196 | if let Some(stderr) = child.stderr.take() { 197 | handles.push(dprint_core::async_runtime::spawn_blocking(|| { 198 | read_stream_lines(stderr, err_tx) 199 | })); 200 | } 201 | 202 | // write file text into child's stdin 203 | if command.stdin { 204 | let mut stdin = child 205 | .stdin 206 | .take() 207 | .ok_or_else(|| { 208 | anyhow!( 209 | "Cannot open the command's stdin. Perhaps you meant to set the command's \"stdin\" configuration to false?", 210 | ) 211 | })?; 212 | let file_bytes = file_bytes.into_owned(); 213 | dprint_core::async_runtime::spawn_blocking(move || { 214 | stdin 215 | .write_all(&file_bytes) 216 | .map_err(|err| anyhow!("Cannot write into the command's stdin. {}", err)) 217 | }) 218 | .await??; 219 | } 220 | 221 | let child_completed = dprint_core::async_runtime::spawn_blocking(move || match child.wait() { 222 | Ok(status) => Ok(status), 223 | Err(e) => Err(anyhow!( 224 | "Error while waiting for formatter to complete: {}", 225 | e 226 | )), 227 | }); 228 | 229 | let result_future = async { 230 | let handles_future = dprint_core::async_runtime::future::join_all(handles); 231 | let (output_result, child_rs, handle_results) = 232 | tokio::join!(out_rx, child_completed, handles_future); 233 | let exit_status = child_rs??; 234 | let output = output_result?; 235 | for handle_result in handle_results { 236 | handle_result??; // surface any errors capturing 237 | } 238 | Ok::<_, Error>((output, exit_status)) 239 | }; 240 | 241 | tokio::select! { 242 | _ = token.wait_cancellation() => { 243 | // return back the original text when cancelled 244 | return Ok(None); 245 | } 246 | _ = tokio::time::sleep(Duration::from_secs(config.timeout as u64)) => { 247 | return Err(timeout_err(&config)); 248 | } 249 | result = result_future => { 250 | let (ok_text, exit_status) = result?; 251 | file_bytes = Cow::Owned(handle_child_exit_status(ok_text, err_rx, exit_status).await?) 252 | } 253 | } 254 | } 255 | 256 | const MIN_CHARS_TO_EMPTY: usize = 100; 257 | Ok(if *file_bytes == original_file_bytes { 258 | None 259 | } else if trim_bytes_len(&original_file_bytes) > MIN_CHARS_TO_EMPTY 260 | && trim_bytes_len(&file_bytes) == 0 261 | { 262 | // prevent someone formatting all their files to empty files 263 | bail!( 264 | concat!( 265 | "The original file text was greater than {} characters, but the formatted text was empty. ", 266 | "Perhaps dprint-plugin-exec has been misconfigured?", 267 | ), 268 | MIN_CHARS_TO_EMPTY 269 | ) 270 | } else { 271 | Some(file_bytes.into_owned()) 272 | }) 273 | } 274 | 275 | fn select_commands<'a>( 276 | config: &'a Configuration, 277 | file_path: &Path, 278 | ) -> Result> { 279 | if !config.is_valid { 280 | bail!("Cannot format because the configuration was not valid."); 281 | } 282 | 283 | let mut binaries = Vec::new(); 284 | 285 | for command in &config.commands { 286 | if let Some(associations) = &command.associations { 287 | if associations.is_match(file_path) { 288 | binaries.push(command); 289 | } 290 | } else if binaries.is_empty() && command.matches_exts_or_filenames(file_path) { 291 | binaries.push(command); 292 | break; 293 | } 294 | } 295 | 296 | Ok(binaries) 297 | } 298 | 299 | async fn handle_child_exit_status( 300 | ok_text: Vec, 301 | err_rx: Receiver>, 302 | exit_status: ExitStatus, 303 | ) -> Result, Error> { 304 | if exit_status.success() { 305 | return Ok(ok_text); 306 | } 307 | Err(anyhow!( 308 | "Child process exited with code {}: {}", 309 | exit_status.code().unwrap(), 310 | String::from_utf8_lossy( 311 | &err_rx 312 | .await 313 | .expect("Could not propagate error message from child process") 314 | ) 315 | )) 316 | } 317 | 318 | fn timeout_err(config: &Configuration) -> Error { 319 | anyhow!( 320 | "Child process has not returned a result within {} seconds.", 321 | config.timeout, 322 | ) 323 | } 324 | 325 | fn read_stream_lines(mut readable: R, sender: Sender>) -> Result<(), Error> 326 | where 327 | R: std::io::Read + Unpin, 328 | { 329 | let mut bytes = Vec::new(); 330 | readable.read_to_end(&mut bytes)?; 331 | let _ignore = sender.send(bytes); // ignore error as that means the other end is closed 332 | Ok(()) 333 | } 334 | 335 | fn maybe_substitute_variables( 336 | file_path: &Path, 337 | config: &Configuration, 338 | command: &CommandConfiguration, 339 | ) -> Vec { 340 | let mut handlebars = Handlebars::new(); 341 | handlebars.set_strict_mode(true); 342 | 343 | #[derive(Clone, Serialize, Deserialize)] 344 | struct TemplateVariables { 345 | file_path: String, 346 | line_width: u32, 347 | use_tabs: bool, 348 | indent_width: u8, 349 | cwd: String, 350 | timeout: u32, 351 | } 352 | 353 | let vars = TemplateVariables { 354 | file_path: file_path.to_string_lossy().to_string(), 355 | line_width: config.line_width, 356 | use_tabs: config.use_tabs, 357 | indent_width: config.indent_width, 358 | cwd: command.cwd.to_string_lossy().to_string(), 359 | timeout: config.timeout, 360 | }; 361 | 362 | let mut c_args = vec![]; 363 | for arg in &command.args { 364 | let formatted = handlebars 365 | .render_template(arg, &vars) 366 | .unwrap_or_else(|err| panic!("Cannot format: {}\n\n{}", arg, err)); 367 | c_args.push(formatted); 368 | } 369 | c_args 370 | } 371 | 372 | #[cfg(test)] 373 | mod test { 374 | use std::path::PathBuf; 375 | use std::sync::Arc; 376 | 377 | use dprint_core::plugins::NullCancellationToken; 378 | 379 | use crate::configuration::Configuration; 380 | use crate::format_bytes; 381 | 382 | #[tokio::test] 383 | async fn should_error_output_empty_file() { 384 | let token = Arc::new(NullCancellationToken); 385 | let unresolved_config = r#"{ 386 | "commands": [{ 387 | "command": "deno eval 'Deno.exit(0)'", 388 | "exts": ["txt"] 389 | }] 390 | }"#; 391 | let unresolved_config = serde_json::from_str(unresolved_config).unwrap(); 392 | let config = Configuration::resolve(unresolved_config, &Default::default()).config; 393 | let result = format_bytes( 394 | PathBuf::from("path.txt"), 395 | "1".repeat(101).into_bytes(), 396 | Arc::new(config), 397 | token, 398 | ) 399 | .await; 400 | let err_text = result.err().unwrap().to_string(); 401 | assert_eq!( 402 | err_text, 403 | concat!( 404 | "The original file text was greater than 100 characters, ", 405 | "but the formatted text was empty. ", 406 | "Perhaps dprint-plugin-exec has been misconfigured?" 407 | ) 408 | ) 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # GENERATED BY ./ci.generate.ts -- DO NOT DIRECTLY EDIT 2 | 3 | name: CI 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | tags: 12 | - '*' 13 | concurrency: 14 | group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}' 15 | cancel-in-progress: true 16 | jobs: 17 | build: 18 | name: '${{ matrix.config.target }}' 19 | runs-on: '${{ matrix.config.os }}' 20 | strategy: 21 | matrix: 22 | config: 23 | - os: macos-13 24 | run_tests: 'true' 25 | target: x86_64-apple-darwin 26 | cross: 'false' 27 | - os: macos-latest 28 | run_tests: 'true' 29 | target: aarch64-apple-darwin 30 | cross: 'false' 31 | - os: windows-latest 32 | run_tests: 'true' 33 | target: x86_64-pc-windows-msvc 34 | cross: 'false' 35 | - os: ubuntu-22.04 36 | run_tests: 'true' 37 | target: x86_64-unknown-linux-gnu 38 | cross: 'false' 39 | - os: ubuntu-22.04 40 | run_tests: 'false' 41 | target: x86_64-unknown-linux-musl 42 | cross: 'false' 43 | - os: ubuntu-22.04 44 | run_tests: 'false' 45 | target: aarch64-unknown-linux-gnu 46 | cross: 'false' 47 | - os: ubuntu-22.04 48 | run_tests: 'false' 49 | target: aarch64-unknown-linux-musl 50 | cross: 'false' 51 | - os: ubuntu-22.04 52 | run_tests: 'false' 53 | target: riscv64gc-unknown-linux-gnu 54 | cross: 'true' 55 | outputs: 56 | ZIP_CHECKSUM_X86_64_APPLE_DARWIN: '${{steps.pre_release_x86_64_apple_darwin.outputs.ZIP_CHECKSUM}}' 57 | ZIP_CHECKSUM_AARCH64_APPLE_DARWIN: '${{steps.pre_release_aarch64_apple_darwin.outputs.ZIP_CHECKSUM}}' 58 | ZIP_CHECKSUM_X86_64_PC_WINDOWS_MSVC: '${{steps.pre_release_x86_64_pc_windows_msvc.outputs.ZIP_CHECKSUM}}' 59 | ZIP_CHECKSUM_X86_64_UNKNOWN_LINUX_GNU: '${{steps.pre_release_x86_64_unknown_linux_gnu.outputs.ZIP_CHECKSUM}}' 60 | ZIP_CHECKSUM_X86_64_UNKNOWN_LINUX_MUSL: '${{steps.pre_release_x86_64_unknown_linux_musl.outputs.ZIP_CHECKSUM}}' 61 | ZIP_CHECKSUM_AARCH64_UNKNOWN_LINUX_GNU: '${{steps.pre_release_aarch64_unknown_linux_gnu.outputs.ZIP_CHECKSUM}}' 62 | ZIP_CHECKSUM_AARCH64_UNKNOWN_LINUX_MUSL: '${{steps.pre_release_aarch64_unknown_linux_musl.outputs.ZIP_CHECKSUM}}' 63 | ZIP_CHECKSUM_RISCV64GC_UNKNOWN_LINUX_GNU: '${{steps.pre_release_riscv64gc_unknown_linux_gnu.outputs.ZIP_CHECKSUM}}' 64 | env: 65 | CARGO_INCREMENTAL: 0 66 | RUST_BACKTRACE: full 67 | steps: 68 | - name: Prepare git 69 | run: |- 70 | git config --global core.autocrlf false 71 | git config --global core.eol lf 72 | - uses: actions/checkout@v4 73 | - uses: dsherret/rust-toolchain-file@v1 74 | - name: Cache cargo 75 | if: 'startsWith(github.ref, ''refs/tags/'') != true' 76 | uses: Swatinem/rust-cache@v2 77 | with: 78 | key: '${{ matrix.config.target }}' 79 | - uses: denoland/setup-deno@v2 80 | - name: Setup (Linux x86_64-musl) 81 | if: matrix.config.target == 'x86_64-unknown-linux-musl' 82 | run: |- 83 | sudo apt update 84 | sudo apt install musl musl-dev musl-tools 85 | rustup target add x86_64-unknown-linux-musl 86 | - name: Setup (Linux aarch64) 87 | if: matrix.config.target == 'aarch64-unknown-linux-gnu' 88 | run: |- 89 | sudo apt update 90 | sudo apt install -y gcc-aarch64-linux-gnu 91 | rustup target add aarch64-unknown-linux-gnu 92 | - name: Setup (Linux aarch64-musl) 93 | if: matrix.config.target == 'aarch64-unknown-linux-musl' 94 | run: |- 95 | sudo apt update 96 | sudo apt install gcc-aarch64-linux-gnu musl musl-dev musl-tools 97 | rustup target add aarch64-unknown-linux-musl 98 | - name: Setup cross 99 | if: matrix.config.cross == 'true' 100 | run: 'cargo install cross --git https://github.com/cross-rs/cross --rev 88f49ff79e777bef6d3564531636ee4d3cc2f8d2' 101 | - name: Build (Debug) 102 | if: 'matrix.config.cross != ''true'' && !startsWith(github.ref, ''refs/tags/'')' 103 | env: 104 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc 105 | run: 'cargo build --locked --all-targets --target ${{matrix.config.target}}' 106 | - name: Build release 107 | if: 'matrix.config.cross != ''true'' && startsWith(github.ref, ''refs/tags/'')' 108 | env: 109 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc 110 | run: 'cargo build --locked --all-targets --target ${{matrix.config.target}} --release' 111 | - name: Build cross (Debug) 112 | if: 'matrix.config.cross == ''true'' && !startsWith(github.ref, ''refs/tags/'')' 113 | run: 'cross build --locked --target ${{matrix.config.target}}' 114 | - name: Build cross (Release) 115 | if: 'matrix.config.cross == ''true'' && startsWith(github.ref, ''refs/tags/'')' 116 | run: 'cross build --locked --target ${{matrix.config.target}} --release' 117 | - name: Lint 118 | if: '!startsWith(github.ref, ''refs/tags/'') && matrix.config.target == ''x86_64-unknown-linux-gnu''' 119 | run: cargo clippy 120 | - name: Test (Debug) 121 | if: 'matrix.config.run_tests == ''true'' && !startsWith(github.ref, ''refs/tags/'')' 122 | run: cargo test --locked --all-features 123 | - name: Test (Release) 124 | if: 'matrix.config.run_tests == ''true'' && startsWith(github.ref, ''refs/tags/'')' 125 | run: cargo test --locked --all-features --release 126 | - name: Pre-release (x86_64-apple-darwin) 127 | id: pre_release_x86_64_apple_darwin 128 | if: 'matrix.config.target == ''x86_64-apple-darwin'' && startsWith(github.ref, ''refs/tags/'')' 129 | run: |- 130 | cd target/x86_64-apple-darwin/release 131 | zip -r dprint-plugin-exec-x86_64-apple-darwin.zip dprint-plugin-exec 132 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 dprint-plugin-exec-x86_64-apple-darwin.zip | awk '{print $1}')" 133 | - name: Pre-release (aarch64-apple-darwin) 134 | id: pre_release_aarch64_apple_darwin 135 | if: 'matrix.config.target == ''aarch64-apple-darwin'' && startsWith(github.ref, ''refs/tags/'')' 136 | run: |- 137 | cd target/aarch64-apple-darwin/release 138 | zip -r dprint-plugin-exec-aarch64-apple-darwin.zip dprint-plugin-exec 139 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 dprint-plugin-exec-aarch64-apple-darwin.zip | awk '{print $1}')" 140 | - name: Pre-release (x86_64-pc-windows-msvc) 141 | id: pre_release_x86_64_pc_windows_msvc 142 | if: 'matrix.config.target == ''x86_64-pc-windows-msvc'' && startsWith(github.ref, ''refs/tags/'')' 143 | run: |- 144 | Compress-Archive -CompressionLevel Optimal -Force -Path target/x86_64-pc-windows-msvc/release/dprint-plugin-exec.exe -DestinationPath target/x86_64-pc-windows-msvc/release/dprint-plugin-exec-x86_64-pc-windows-msvc.zip 145 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 target/x86_64-pc-windows-msvc/release/dprint-plugin-exec-x86_64-pc-windows-msvc.zip | awk '{print $1}')" 146 | - name: Pre-release (x86_64-unknown-linux-gnu) 147 | id: pre_release_x86_64_unknown_linux_gnu 148 | if: 'matrix.config.target == ''x86_64-unknown-linux-gnu'' && startsWith(github.ref, ''refs/tags/'')' 149 | run: |- 150 | cd target/x86_64-unknown-linux-gnu/release 151 | zip -r dprint-plugin-exec-x86_64-unknown-linux-gnu.zip dprint-plugin-exec 152 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 dprint-plugin-exec-x86_64-unknown-linux-gnu.zip | awk '{print $1}')" 153 | - name: Pre-release (x86_64-unknown-linux-musl) 154 | id: pre_release_x86_64_unknown_linux_musl 155 | if: 'matrix.config.target == ''x86_64-unknown-linux-musl'' && startsWith(github.ref, ''refs/tags/'')' 156 | run: |- 157 | cd target/x86_64-unknown-linux-musl/release 158 | zip -r dprint-plugin-exec-x86_64-unknown-linux-musl.zip dprint-plugin-exec 159 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 dprint-plugin-exec-x86_64-unknown-linux-musl.zip | awk '{print $1}')" 160 | - name: Pre-release (aarch64-unknown-linux-gnu) 161 | id: pre_release_aarch64_unknown_linux_gnu 162 | if: 'matrix.config.target == ''aarch64-unknown-linux-gnu'' && startsWith(github.ref, ''refs/tags/'')' 163 | run: |- 164 | cd target/aarch64-unknown-linux-gnu/release 165 | zip -r dprint-plugin-exec-aarch64-unknown-linux-gnu.zip dprint-plugin-exec 166 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 dprint-plugin-exec-aarch64-unknown-linux-gnu.zip | awk '{print $1}')" 167 | - name: Pre-release (aarch64-unknown-linux-musl) 168 | id: pre_release_aarch64_unknown_linux_musl 169 | if: 'matrix.config.target == ''aarch64-unknown-linux-musl'' && startsWith(github.ref, ''refs/tags/'')' 170 | run: |- 171 | cd target/aarch64-unknown-linux-musl/release 172 | zip -r dprint-plugin-exec-aarch64-unknown-linux-musl.zip dprint-plugin-exec 173 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 dprint-plugin-exec-aarch64-unknown-linux-musl.zip | awk '{print $1}')" 174 | - name: Pre-release (riscv64gc-unknown-linux-gnu) 175 | id: pre_release_riscv64gc_unknown_linux_gnu 176 | if: 'matrix.config.target == ''riscv64gc-unknown-linux-gnu'' && startsWith(github.ref, ''refs/tags/'')' 177 | run: |- 178 | cd target/riscv64gc-unknown-linux-gnu/release 179 | zip -r dprint-plugin-exec-riscv64gc-unknown-linux-gnu.zip dprint-plugin-exec 180 | echo "::set-output name=ZIP_CHECKSUM::$(shasum -a 256 dprint-plugin-exec-riscv64gc-unknown-linux-gnu.zip | awk '{print $1}')" 181 | - name: Upload artifacts (x86_64-apple-darwin) 182 | if: 'matrix.config.target == ''x86_64-apple-darwin'' && startsWith(github.ref, ''refs/tags/'')' 183 | uses: actions/upload-artifact@v4 184 | with: 185 | name: x86_64-apple-darwin-artifacts 186 | path: target/x86_64-apple-darwin/release/dprint-plugin-exec-x86_64-apple-darwin.zip 187 | - name: Upload artifacts (aarch64-apple-darwin) 188 | if: 'matrix.config.target == ''aarch64-apple-darwin'' && startsWith(github.ref, ''refs/tags/'')' 189 | uses: actions/upload-artifact@v4 190 | with: 191 | name: aarch64-apple-darwin-artifacts 192 | path: target/aarch64-apple-darwin/release/dprint-plugin-exec-aarch64-apple-darwin.zip 193 | - name: Upload artifacts (x86_64-pc-windows-msvc) 194 | if: 'matrix.config.target == ''x86_64-pc-windows-msvc'' && startsWith(github.ref, ''refs/tags/'')' 195 | uses: actions/upload-artifact@v4 196 | with: 197 | name: x86_64-pc-windows-msvc-artifacts 198 | path: target/x86_64-pc-windows-msvc/release/dprint-plugin-exec-x86_64-pc-windows-msvc.zip 199 | - name: Upload artifacts (x86_64-unknown-linux-gnu) 200 | if: 'matrix.config.target == ''x86_64-unknown-linux-gnu'' && startsWith(github.ref, ''refs/tags/'')' 201 | uses: actions/upload-artifact@v4 202 | with: 203 | name: x86_64-unknown-linux-gnu-artifacts 204 | path: target/x86_64-unknown-linux-gnu/release/dprint-plugin-exec-x86_64-unknown-linux-gnu.zip 205 | - name: Upload artifacts (x86_64-unknown-linux-musl) 206 | if: 'matrix.config.target == ''x86_64-unknown-linux-musl'' && startsWith(github.ref, ''refs/tags/'')' 207 | uses: actions/upload-artifact@v4 208 | with: 209 | name: x86_64-unknown-linux-musl-artifacts 210 | path: target/x86_64-unknown-linux-musl/release/dprint-plugin-exec-x86_64-unknown-linux-musl.zip 211 | - name: Upload artifacts (aarch64-unknown-linux-gnu) 212 | if: 'matrix.config.target == ''aarch64-unknown-linux-gnu'' && startsWith(github.ref, ''refs/tags/'')' 213 | uses: actions/upload-artifact@v4 214 | with: 215 | name: aarch64-unknown-linux-gnu-artifacts 216 | path: target/aarch64-unknown-linux-gnu/release/dprint-plugin-exec-aarch64-unknown-linux-gnu.zip 217 | - name: Upload artifacts (aarch64-unknown-linux-musl) 218 | if: 'matrix.config.target == ''aarch64-unknown-linux-musl'' && startsWith(github.ref, ''refs/tags/'')' 219 | uses: actions/upload-artifact@v4 220 | with: 221 | name: aarch64-unknown-linux-musl-artifacts 222 | path: target/aarch64-unknown-linux-musl/release/dprint-plugin-exec-aarch64-unknown-linux-musl.zip 223 | - name: Upload artifacts (riscv64gc-unknown-linux-gnu) 224 | if: 'matrix.config.target == ''riscv64gc-unknown-linux-gnu'' && startsWith(github.ref, ''refs/tags/'')' 225 | uses: actions/upload-artifact@v4 226 | with: 227 | name: riscv64gc-unknown-linux-gnu-artifacts 228 | path: target/riscv64gc-unknown-linux-gnu/release/dprint-plugin-exec-riscv64gc-unknown-linux-gnu.zip 229 | draft_release: 230 | name: draft_release 231 | if: 'startsWith(github.ref, ''refs/tags/'')' 232 | needs: build 233 | runs-on: ubuntu-latest 234 | steps: 235 | - name: Checkout 236 | uses: actions/checkout@v4 237 | - name: Download artifacts 238 | uses: actions/download-artifact@v4 239 | - uses: denoland/setup-deno@v2 240 | - name: Move downloaded artifacts to root directory 241 | run: |- 242 | mv x86_64-apple-darwin-artifacts/dprint-plugin-exec-x86_64-apple-darwin.zip . 243 | mv aarch64-apple-darwin-artifacts/dprint-plugin-exec-aarch64-apple-darwin.zip . 244 | mv x86_64-pc-windows-msvc-artifacts/dprint-plugin-exec-x86_64-pc-windows-msvc.zip . 245 | mv x86_64-unknown-linux-gnu-artifacts/dprint-plugin-exec-x86_64-unknown-linux-gnu.zip . 246 | mv x86_64-unknown-linux-musl-artifacts/dprint-plugin-exec-x86_64-unknown-linux-musl.zip . 247 | mv aarch64-unknown-linux-gnu-artifacts/dprint-plugin-exec-aarch64-unknown-linux-gnu.zip . 248 | mv aarch64-unknown-linux-musl-artifacts/dprint-plugin-exec-aarch64-unknown-linux-musl.zip . 249 | mv riscv64gc-unknown-linux-gnu-artifacts/dprint-plugin-exec-riscv64gc-unknown-linux-gnu.zip . 250 | - name: Output checksums 251 | run: |- 252 | echo "dprint-plugin-exec-x86_64-apple-darwin.zip: ${{needs.build.outputs.ZIP_CHECKSUM_X86_64_APPLE_DARWIN}}" 253 | echo "dprint-plugin-exec-aarch64-apple-darwin.zip: ${{needs.build.outputs.ZIP_CHECKSUM_AARCH64_APPLE_DARWIN}}" 254 | echo "dprint-plugin-exec-x86_64-pc-windows-msvc.zip: ${{needs.build.outputs.ZIP_CHECKSUM_X86_64_PC_WINDOWS_MSVC}}" 255 | echo "dprint-plugin-exec-x86_64-unknown-linux-gnu.zip: ${{needs.build.outputs.ZIP_CHECKSUM_X86_64_UNKNOWN_LINUX_GNU}}" 256 | echo "dprint-plugin-exec-x86_64-unknown-linux-musl.zip: ${{needs.build.outputs.ZIP_CHECKSUM_X86_64_UNKNOWN_LINUX_MUSL}}" 257 | echo "dprint-plugin-exec-aarch64-unknown-linux-gnu.zip: ${{needs.build.outputs.ZIP_CHECKSUM_AARCH64_UNKNOWN_LINUX_GNU}}" 258 | echo "dprint-plugin-exec-aarch64-unknown-linux-musl.zip: ${{needs.build.outputs.ZIP_CHECKSUM_AARCH64_UNKNOWN_LINUX_MUSL}}" 259 | echo "dprint-plugin-exec-riscv64gc-unknown-linux-gnu.zip: ${{needs.build.outputs.ZIP_CHECKSUM_RISCV64GC_UNKNOWN_LINUX_GNU}}" 260 | - name: Create plugin file 261 | run: deno run --allow-read=. --allow-write=. scripts/create_plugin_file.ts 262 | - name: Get tag version 263 | id: get_tag_version 264 | run: 'echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\/tags\//}' 265 | - name: Get plugin file checksum 266 | id: get_plugin_file_checksum 267 | run: 'echo "::set-output name=CHECKSUM::$(shasum -a 256 plugin.json | awk ''{print $1}'')"' 268 | - name: Update Config Schema Version 269 | run: 'sed -i ''s/exec\/0.0.0/exec\/${{ steps.get_tag_version.outputs.TAG_VERSION }}/'' deployment/schema.json' 270 | - name: Create release notes 271 | run: 'deno run -A ./scripts/generate_release_notes.ts ${{ steps.get_tag_version.outputs.TAG_VERSION }} ${{ steps.get_plugin_file_checksum.outputs.CHECKSUM }} > ${{ github.workspace }}-CHANGELOG.txt' 272 | - name: Release 273 | uses: softprops/action-gh-release@v1 274 | env: 275 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 276 | with: 277 | files: |- 278 | dprint-plugin-exec-x86_64-apple-darwin.zip 279 | dprint-plugin-exec-aarch64-apple-darwin.zip 280 | dprint-plugin-exec-x86_64-pc-windows-msvc.zip 281 | dprint-plugin-exec-x86_64-unknown-linux-gnu.zip 282 | dprint-plugin-exec-x86_64-unknown-linux-musl.zip 283 | dprint-plugin-exec-aarch64-unknown-linux-gnu.zip 284 | dprint-plugin-exec-aarch64-unknown-linux-musl.zip 285 | dprint-plugin-exec-riscv64gc-unknown-linux-gnu.zip 286 | plugin.json 287 | deployment/schema.json 288 | body_path: '${{ github.workspace }}-CHANGELOG.txt' 289 | -------------------------------------------------------------------------------- /src/configuration.rs: -------------------------------------------------------------------------------- 1 | use dprint_core::configuration::ConfigKeyMap; 2 | use dprint_core::configuration::ConfigKeyValue; 3 | use dprint_core::configuration::ConfigurationDiagnostic; 4 | use dprint_core::configuration::GlobalConfiguration; 5 | use dprint_core::configuration::RECOMMENDED_GLOBAL_CONFIGURATION; 6 | use dprint_core::configuration::ResolveConfigurationResult; 7 | use dprint_core::configuration::get_nullable_value; 8 | use dprint_core::configuration::get_nullable_vec; 9 | use dprint_core::configuration::get_unknown_property_diagnostics; 10 | use dprint_core::configuration::get_value; 11 | use globset::GlobMatcher; 12 | use handlebars::Handlebars; 13 | use serde::Serialize; 14 | use serde::Serializer; 15 | use sha2::Digest; 16 | use sha2::Sha256; 17 | use std::fs::read_to_string; 18 | use std::path::Path; 19 | use std::path::PathBuf; 20 | 21 | #[derive(Clone, Serialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct Configuration { 24 | /// Doesn't allow formatting unless the configuration had no diagnostics. 25 | pub is_valid: bool, 26 | pub cache_key: String, 27 | pub line_width: u32, 28 | pub use_tabs: bool, 29 | pub indent_width: u8, 30 | /// Formatting commands to run 31 | pub commands: Vec, 32 | pub timeout: u32, 33 | } 34 | 35 | #[derive(Clone, Serialize)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct CommandConfiguration { 38 | pub executable: String, 39 | /// Executable arguments to add 40 | pub args: Vec, 41 | pub cwd: PathBuf, 42 | pub stdin: bool, 43 | #[serde(serialize_with = "serialize_glob")] 44 | pub associations: Option, 45 | pub file_extensions: Vec, 46 | pub file_names: Vec, 47 | pub cache_key_files_hash: Option, 48 | } 49 | 50 | impl CommandConfiguration { 51 | pub fn matches_exts_or_filenames(&self, path: &Path) -> bool { 52 | if let Some(filename) = path.file_name() { 53 | let filename = filename.to_string_lossy().to_lowercase(); 54 | for ext in &self.file_extensions { 55 | if filename.ends_with(ext) { 56 | return true; 57 | } 58 | } 59 | self.file_names.iter().any(|name| name == &filename) 60 | } else { 61 | false 62 | } 63 | } 64 | } 65 | 66 | fn serialize_glob(value: &Option, s: S) -> Result { 67 | match value { 68 | Some(value) => s.serialize_str(value.glob().glob()), 69 | None => s.serialize_none(), 70 | } 71 | } 72 | 73 | impl Configuration { 74 | /// Resolves configuration from a collection of key value strings. 75 | /// 76 | /// # Example 77 | /// 78 | /// ``` 79 | /// use dprint_core::configuration::ConfigKeyMap; 80 | /// use dprint_core::configuration::resolve_global_config; 81 | /// use dprint_plugin_exec::configuration::Configuration; 82 | /// 83 | /// let mut config_map = ConfigKeyMap::new(); // get a collection of key value pairs from somewhere 84 | /// let global_config_result = resolve_global_config(&mut config_map); 85 | /// 86 | /// // check global_config_result.diagnostics here... 87 | /// 88 | /// let exec_config_map = ConfigKeyMap::new(); // get a collection of k/v pairs from somewhere 89 | /// let config_result = Configuration::resolve( 90 | /// exec_config_map, 91 | /// &global_config_result.config 92 | /// ); 93 | /// 94 | /// // check config_result.diagnostics here and use config_result.config 95 | /// ``` 96 | pub fn resolve( 97 | config: ConfigKeyMap, 98 | global_config: &GlobalConfiguration, 99 | ) -> ResolveConfigurationResult { 100 | let mut diagnostics = vec![]; 101 | let mut config = config; 102 | 103 | let mut resolved_config = Configuration { 104 | is_valid: true, 105 | cache_key: "0".to_string(), 106 | line_width: get_value( 107 | &mut config, 108 | "lineWidth", 109 | global_config 110 | .line_width 111 | .unwrap_or(RECOMMENDED_GLOBAL_CONFIGURATION.line_width), 112 | &mut diagnostics, 113 | ), 114 | use_tabs: get_value( 115 | &mut config, 116 | "useTabs", 117 | global_config 118 | .use_tabs 119 | .unwrap_or(RECOMMENDED_GLOBAL_CONFIGURATION.use_tabs), 120 | &mut diagnostics, 121 | ), 122 | indent_width: get_value( 123 | &mut config, 124 | "indentWidth", 125 | global_config 126 | .indent_width 127 | .unwrap_or(RECOMMENDED_GLOBAL_CONFIGURATION.indent_width), 128 | &mut diagnostics, 129 | ), 130 | commands: Vec::new(), 131 | timeout: get_value(&mut config, "timeout", 30, &mut diagnostics), 132 | }; 133 | 134 | let root_cache_key = get_nullable_value::(&mut config, "cacheKey", &mut diagnostics); 135 | let mut cache_key_file_hashes = Vec::new(); 136 | 137 | let root_cwd = get_nullable_value(&mut config, "cwd", &mut diagnostics); 138 | 139 | if let Some(commands) = config.swap_remove("commands").and_then(|c| c.into_array()) { 140 | for (i, element) in commands.into_iter().enumerate() { 141 | let Some(command_obj) = element.into_object() else { 142 | diagnostics.push(ConfigurationDiagnostic { 143 | property_name: "commands".to_string(), 144 | message: "Expected to find only objects in the array.".to_string(), 145 | }); 146 | continue; 147 | }; 148 | let result = parse_command_obj(command_obj, root_cwd.as_ref()); 149 | diagnostics.extend(result.1.into_iter().map(|mut diagnostic| { 150 | diagnostic.property_name = format!("commands[{}].{}", i, diagnostic.property_name); 151 | diagnostic 152 | })); 153 | if let Some(mut command_config) = result.0 { 154 | if let Some(cache_key_files_hash) = command_config.cache_key_files_hash.take() { 155 | cache_key_file_hashes.push(cache_key_files_hash); 156 | } 157 | 158 | resolved_config.commands.push(command_config); 159 | } 160 | } 161 | } else { 162 | diagnostics.push(ConfigurationDiagnostic { 163 | property_name: "commands".to_string(), 164 | message: "Expected to find a \"commands\" array property (see https://github.com/dprint/dprint-plugin-exec for instructions)".to_string(), 165 | }); 166 | } 167 | 168 | diagnostics.extend(get_unknown_property_diagnostics(config)); 169 | 170 | if let Some(cache_key) = compute_cache_key(root_cache_key, &cache_key_file_hashes) { 171 | resolved_config.cache_key = cache_key; 172 | } 173 | 174 | resolved_config.is_valid = diagnostics.is_empty(); 175 | 176 | ResolveConfigurationResult { 177 | config: resolved_config, 178 | diagnostics, 179 | } 180 | } 181 | } 182 | 183 | fn parse_command_obj( 184 | mut command_obj: ConfigKeyMap, 185 | root_cwd: Option<&String>, 186 | ) -> (Option, Vec) { 187 | let mut diagnostics = Vec::new(); 188 | let mut command = splitty::split_unquoted_whitespace(&get_value( 189 | &mut command_obj, 190 | "command", 191 | String::default(), 192 | &mut diagnostics, 193 | )) 194 | .unwrap_quotes(true) 195 | .filter(|p| !p.is_empty()) 196 | .map(String::from) 197 | .collect::>(); 198 | if command.is_empty() { 199 | diagnostics.push(ConfigurationDiagnostic { 200 | property_name: "command".to_string(), 201 | message: "Expected to find a command name.".to_string(), 202 | }); 203 | return (None, diagnostics); 204 | } 205 | 206 | { 207 | let mut handlebars = Handlebars::new(); 208 | handlebars.set_strict_mode(true); 209 | for arg in command.iter().skip(1) { 210 | if let Err(e) = handlebars.register_template_string("tmp", arg) { 211 | diagnostics.push(ConfigurationDiagnostic { 212 | property_name: "command".to_string(), 213 | message: format!("Invalid template: {}", e), 214 | }); 215 | } 216 | handlebars.unregister_template("tmp"); 217 | } 218 | } 219 | 220 | let cwd = get_cwd( 221 | get_nullable_value(&mut command_obj, "cwd", &mut diagnostics) 222 | .or_else(|| root_cwd.map(ToOwned::to_owned)), 223 | ); 224 | 225 | let cache_key_files = get_nullable_vec( 226 | &mut command_obj, 227 | "cacheKeyFiles", 228 | |value, i, diagnostics| match value { 229 | ConfigKeyValue::String(value) => Some(cwd.join(value)), 230 | _ => { 231 | diagnostics.push(ConfigurationDiagnostic { 232 | property_name: format!("cacheKeyFiles[{}]", i), 233 | message: "Expected string element.".to_string(), 234 | }); 235 | None 236 | } 237 | }, 238 | &mut diagnostics, 239 | ); 240 | 241 | // compute the hash separately from the config read so we don't do the disk ops if the config is invalid. 242 | let cache_key_files_hash = { 243 | if let Some(cache_key_files) = cache_key_files { 244 | let mut hasher = Sha256::new(); 245 | for file in cache_key_files { 246 | let contents = match read_to_string(&file) { 247 | Ok(contents) => contents, 248 | Err(err) => { 249 | diagnostics.push(ConfigurationDiagnostic { 250 | property_name: "cacheKeyFiles".to_string(), 251 | message: format!("Unable to read file '{}': {}.", file.display(), err), 252 | }); 253 | return (None, diagnostics); 254 | } 255 | }; 256 | hasher.update(contents); 257 | } 258 | Some(format!("{:x}", hasher.finalize())) 259 | } else { 260 | None 261 | } 262 | }; 263 | 264 | let config = CommandConfiguration { 265 | executable: command.remove(0), 266 | args: command, 267 | associations: { 268 | let maybe_value = command_obj.swap_remove("associations").and_then(|value| match value { 269 | ConfigKeyValue::String(value) => Some(value), 270 | ConfigKeyValue::Array(mut value) => match value.len() { 271 | 0 => None, 272 | 1 => match value.remove(0) { 273 | ConfigKeyValue::String(value) => Some(value), 274 | _ => { 275 | diagnostics.push(ConfigurationDiagnostic { 276 | property_name: "associations".to_string(), 277 | message: "Expected string value in array.".to_string(), 278 | }); 279 | None 280 | } 281 | }, 282 | _ => { 283 | diagnostics.push(ConfigurationDiagnostic { 284 | property_name: "associations".to_string(), 285 | message: "Unfortunately multiple globs haven't been implemented yet. Please provide a single glob or consider contributing this feature." 286 | .to_string(), 287 | }); 288 | None 289 | } 290 | }, 291 | _ => { 292 | diagnostics.push(ConfigurationDiagnostic { 293 | property_name: "associations".to_string(), 294 | message: "Expected string or array value.".to_string(), 295 | }); 296 | None 297 | } 298 | }); 299 | 300 | maybe_value.and_then(|value| { 301 | let mut builder = globset::GlobBuilder::new(&value); 302 | builder.case_insensitive(cfg!(windows)); 303 | match builder.build() { 304 | Ok(glob) => Some(glob.compile_matcher()), 305 | Err(err) => { 306 | diagnostics.push(ConfigurationDiagnostic { 307 | message: format!("Error parsing associations glob: {:#}", err), 308 | property_name: "associations".to_string(), 309 | }); 310 | None 311 | } 312 | } 313 | }) 314 | }, 315 | cwd, 316 | stdin: get_value(&mut command_obj, "stdin", true, &mut diagnostics), 317 | file_extensions: take_string_or_string_vec(&mut command_obj, "exts", &mut diagnostics) 318 | .into_iter() 319 | .map(|ext| { 320 | if ext.starts_with('.') { 321 | ext 322 | } else { 323 | format!(".{}", ext) 324 | } 325 | }) 326 | .collect::>(), 327 | file_names: take_string_or_string_vec(&mut command_obj, "fileNames", &mut diagnostics), 328 | cache_key_files_hash, 329 | }; 330 | diagnostics.extend(get_unknown_property_diagnostics(command_obj)); 331 | 332 | if diagnostics.is_empty() 333 | && config.file_names.is_empty() 334 | && config.file_extensions.is_empty() 335 | && config.associations.is_none() 336 | { 337 | diagnostics.push(ConfigurationDiagnostic { 338 | property_name: "exts".to_string(), 339 | message: "You must specify either: exts (recommended), fileNames, or associations" 340 | .to_string(), 341 | }) 342 | } 343 | 344 | (Some(config), diagnostics) 345 | } 346 | 347 | fn take_string_or_string_vec( 348 | command_obj: &mut ConfigKeyMap, 349 | key: &str, 350 | diagnostics: &mut Vec, 351 | ) -> Vec { 352 | command_obj 353 | .swap_remove(key) 354 | .map(|values| match values { 355 | ConfigKeyValue::String(value) => vec![value], 356 | ConfigKeyValue::Array(elements) => { 357 | let mut values = Vec::with_capacity(elements.len()); 358 | for (i, element) in elements.into_iter().enumerate() { 359 | match element { 360 | ConfigKeyValue::String(value) => { 361 | values.push(value); 362 | } 363 | _ => diagnostics.push(ConfigurationDiagnostic { 364 | property_name: format!("{}[{}]", key, i), 365 | message: "Expected string element.".to_string(), 366 | }), 367 | } 368 | } 369 | values 370 | } 371 | _ => { 372 | diagnostics.push(ConfigurationDiagnostic { 373 | property_name: key.to_string(), 374 | message: "Expected string or array value.".to_string(), 375 | }); 376 | vec![] 377 | } 378 | }) 379 | .unwrap_or_default() 380 | } 381 | 382 | fn get_cwd(dir: Option) -> PathBuf { 383 | match dir { 384 | Some(dir) => PathBuf::from(dir), 385 | None => std::env::current_dir().expect("should get cwd"), 386 | } 387 | } 388 | 389 | fn compute_cache_key( 390 | root_cache_key: Option, 391 | cache_key_file_hashes: &[String], 392 | ) -> Option { 393 | match ( 394 | root_cache_key, 395 | compute_cache_key_files_hash(cache_key_file_hashes), 396 | ) { 397 | (Some(root), Some(files)) => Some(format!("{}{}", root, files)), 398 | (Some(root), None) => Some(root), 399 | (None, Some(files)) => Some(files), 400 | (None, None) => None, 401 | } 402 | } 403 | 404 | fn compute_cache_key_files_hash(cache_key_file_hashes: &[String]) -> Option { 405 | if cache_key_file_hashes.is_empty() { 406 | return None; 407 | } 408 | 409 | let mut hasher = Sha256::new(); 410 | for file_hash in cache_key_file_hashes { 411 | hasher.update(file_hash); 412 | } 413 | Some(format!("{:x}", hasher.finalize())) 414 | } 415 | 416 | #[cfg(test)] 417 | mod tests { 418 | use super::*; 419 | use dprint_core::configuration::ConfigKeyValue; 420 | use dprint_core::configuration::resolve_global_config; 421 | use pretty_assertions::assert_eq; 422 | use serde_json::json; 423 | 424 | #[test] 425 | fn handle_global_config() { 426 | let mut global_config = ConfigKeyMap::from([ 427 | ("lineWidth".to_string(), ConfigKeyValue::from_i32(80)), 428 | ("indentWidth".to_string(), ConfigKeyValue::from_i32(8)), 429 | ("useTabs".to_string(), ConfigKeyValue::from_bool(true)), 430 | ]); 431 | let global_config = resolve_global_config(&mut global_config).config; 432 | let config = Configuration::resolve(ConfigKeyMap::new(), &global_config).config; 433 | assert_eq!(config.line_width, 80); 434 | assert_eq!(config.indent_width, 8); 435 | assert!(config.use_tabs); 436 | } 437 | 438 | #[test] 439 | fn general_test() { 440 | let unresolved_config = parse_config(json!({ 441 | "cacheKey": "2", 442 | "timeout": 5 443 | })); 444 | let result = Configuration::resolve(unresolved_config, &Default::default()); 445 | let config = result.config; 446 | assert_eq!(config.line_width, 120); 447 | assert_eq!(config.indent_width, 2); 448 | assert!(!config.use_tabs); 449 | assert_eq!(config.cache_key, "2"); 450 | assert_eq!(config.timeout, 5); 451 | assert_eq!(result.diagnostics, vec![ConfigurationDiagnostic { 452 | property_name: "commands".to_string(), 453 | message: "Expected to find a \"commands\" array property (see https://github.com/dprint/dprint-plugin-exec for instructions)".to_string(), 454 | }]); 455 | } 456 | 457 | #[test] 458 | fn empty_command_name() { 459 | let config = parse_config(json!({ 460 | "commands": [{ 461 | "command": "", 462 | }], 463 | })); 464 | run_diagnostics_test( 465 | config, 466 | vec![ConfigurationDiagnostic { 467 | property_name: "commands[0].command".to_string(), 468 | message: "Expected to find a command name.".to_string(), 469 | }], 470 | ) 471 | } 472 | 473 | #[test] 474 | fn cwd_test() { 475 | let unresolved_config = parse_config(json!({ 476 | "cwd": "test-cwd", 477 | "commands": [{ 478 | "command": "1" 479 | }, { 480 | "cwd": "test-cwd2", 481 | "command": "1" 482 | }] 483 | })); 484 | let result = Configuration::resolve(unresolved_config, &Default::default()); 485 | let config = result.config; 486 | assert_eq!(config.commands[0].cwd, PathBuf::from("test-cwd")); 487 | assert_eq!(config.commands[1].cwd, PathBuf::from("test-cwd2")); 488 | } 489 | 490 | #[test] 491 | fn handle_associations_value() { 492 | let unresolved_config = parse_config(json!({ 493 | "commands": [{ 494 | "command": "command", 495 | "associations": ["**/*.rs"] 496 | }], 497 | })); 498 | let mut config = Configuration::resolve(unresolved_config, &Default::default()).config; 499 | assert!(config.commands.remove(0).associations.is_some()); 500 | 501 | let unresolved_config = parse_config(json!({ 502 | "commands": [{ 503 | "command": "command", 504 | "associations": [] 505 | }], 506 | })); 507 | let mut config = Configuration::resolve(unresolved_config, &Default::default()).config; 508 | assert!(config.commands.remove(0).associations.is_none()); 509 | 510 | let unresolved_config = parse_config(json!({ 511 | "commands": [{ 512 | "command": "command", 513 | "associations": [ 514 | "**/*.rs", 515 | "**/*.json", 516 | ] 517 | }], 518 | })); 519 | run_diagnostics_test( 520 | unresolved_config, 521 | vec![ConfigurationDiagnostic { 522 | property_name: "commands[0].associations".to_string(), 523 | message: "Unfortunately multiple globs haven't been implemented yet. Please provide a single glob or consider contributing this feature.".to_string(), 524 | }], 525 | ); 526 | 527 | let unresolved_config = parse_config(json!({ 528 | "commands": [{ 529 | "command": "command", 530 | "associations": [true] 531 | }], 532 | })); 533 | run_diagnostics_test( 534 | unresolved_config, 535 | vec![ConfigurationDiagnostic { 536 | property_name: "commands[0].associations".to_string(), 537 | message: "Expected string value in array.".to_string(), 538 | }], 539 | ); 540 | 541 | let unresolved_config = parse_config(json!({ 542 | "commands": [{ 543 | "command": "command", 544 | "associations": true 545 | }], 546 | })); 547 | run_diagnostics_test( 548 | unresolved_config, 549 | vec![ConfigurationDiagnostic { 550 | property_name: "commands[0].associations".to_string(), 551 | message: "Expected string or array value.".to_string(), 552 | }], 553 | ); 554 | } 555 | 556 | #[track_caller] 557 | fn run_diagnostics_test( 558 | config: ConfigKeyMap, 559 | expected_diagnostics: Vec, 560 | ) { 561 | let result = Configuration::resolve(config, &Default::default()); 562 | assert_eq!(result.diagnostics, expected_diagnostics); 563 | assert!(!result.config.is_valid); 564 | } 565 | 566 | fn parse_config(value: serde_json::Value) -> ConfigKeyMap { 567 | serde_json::from_value(value).unwrap() 568 | } 569 | 570 | mod cache_key { 571 | use super::*; 572 | use pretty_assertions::assert_eq; 573 | 574 | #[test] 575 | fn default_cache_key() { 576 | let unresolved_config = parse_config(json!({ 577 | "commands": [{ 578 | "exts": ["txt"], 579 | "command": "1" 580 | }], 581 | })); 582 | let result = Configuration::resolve(unresolved_config, &Default::default()); 583 | let config = result.config; 584 | assert!(result.diagnostics.is_empty()); 585 | assert_eq!(config.cache_key, "0"); 586 | } 587 | 588 | #[test] 589 | fn top_level_cache_key() { 590 | let unresolved_config = parse_config(json!({ 591 | "cacheKey": "99", 592 | "commands": [{ 593 | "exts": ["txt"], 594 | "command": "1" 595 | }], 596 | })); 597 | let result = Configuration::resolve(unresolved_config, &Default::default()); 598 | assert!(result.diagnostics.is_empty()); 599 | let config = result.config; 600 | assert_eq!(config.cache_key, "99"); 601 | } 602 | 603 | #[test] 604 | fn top_level_cache_key_plus_command_cache_key_is_allowed() { 605 | let unresolved_config = parse_config(json!({ 606 | "cacheKey": "99", 607 | "commands": [{ 608 | "exts": ["txt"], 609 | "command": "1", 610 | "cacheKeyFiles": ["./tests/resources/one-line.txt"] 611 | }], 612 | })); 613 | let result = Configuration::resolve(unresolved_config, &Default::default()); 614 | assert!(result.config.is_valid); 615 | assert_eq!(result.diagnostics, vec![]); 616 | assert_eq!( 617 | result.config.cache_key, 618 | "99c7b3af761ad02238e72bf5a60c94be2f41eec6637ec3ec1bfa853a3a1fb91225" 619 | ); 620 | } 621 | 622 | #[test] 623 | fn command_cache_key_fails_if_file_does_not_exist() { 624 | let unresolved_config = parse_config(json!({ 625 | "commands": [{ 626 | "exts": ["txt"], 627 | "command": "1", 628 | "cacheKeyFiles": ["path/to/missing/file"] 629 | }], 630 | })); 631 | let result = Configuration::resolve(unresolved_config, &Default::default()); 632 | assert!(!result.config.is_valid); 633 | assert_eq!(result.diagnostics.len(), 1); 634 | assert_eq!( 635 | result.diagnostics[0].property_name, 636 | "commands[0].cacheKeyFiles" 637 | ); 638 | assert!( 639 | result.diagnostics[0] 640 | .message 641 | .starts_with("Unable to read file") 642 | ); 643 | } 644 | 645 | #[test] 646 | fn command_cache_key_one_command_one_file() { 647 | let unresolved_config = parse_config(json!({ 648 | "commands": [{ 649 | "exts": ["txt"], 650 | "command": "1", 651 | "cacheKeyFiles": [ 652 | "./tests/resources/one-line.txt" 653 | ] 654 | }], 655 | })); 656 | let result = Configuration::resolve(unresolved_config, &Default::default()); 657 | assert!(result.diagnostics.is_empty()); 658 | let config = result.config; 659 | assert_eq!( 660 | config.cache_key, 661 | "c7b3af761ad02238e72bf5a60c94be2f41eec6637ec3ec1bfa853a3a1fb91225" 662 | ); 663 | } 664 | 665 | #[test] 666 | fn command_cache_key_one_command_multiple_files() { 667 | let unresolved_config = parse_config(json!({ 668 | "commands": [{ 669 | "exts": ["txt"], 670 | "command": "1", 671 | "cacheKeyFiles": [ 672 | "./tests/resources/one-line.txt", 673 | "./tests/resources/multi-line.txt", 674 | ] 675 | }], 676 | })); 677 | let result = Configuration::resolve(unresolved_config, &Default::default()); 678 | assert!(result.diagnostics.is_empty()); 679 | let config = result.config; 680 | assert_eq!( 681 | config.cache_key, 682 | "4321f2e747210582553e6ad8ef5b866d87c357a039cd09cdbdab6ebe33517c1a" 683 | ); 684 | } 685 | 686 | #[test] 687 | fn command_cache_key_multiple_commands() { 688 | let unresolved_config = parse_config(json!({ 689 | "commands": [ 690 | { 691 | "exts": ["txt"], 692 | "command": "1", 693 | "cacheKeyFiles": [ 694 | "./tests/resources/one-line.txt", 695 | "./tests/resources/multi-line.txt", 696 | ] 697 | }, 698 | { 699 | "exts": ["txt"], 700 | "command": "2", 701 | "cacheKeyFiles": [ 702 | "./tests/resources/one-line.txt", 703 | "./tests/resources/multi-line.txt", 704 | ] 705 | }, 706 | ], 707 | })); 708 | let result = Configuration::resolve(unresolved_config, &Default::default()); 709 | assert!(result.diagnostics.is_empty()); 710 | let config = result.config; 711 | assert_eq!( 712 | config.cache_key, 713 | "51eaf161463bb6ba4957327330e27a80d039b7d2c0c27590ebdf844e7eca954a" 714 | ); 715 | } 716 | } 717 | } 718 | -------------------------------------------------------------------------------- /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.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.18" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 46 | 47 | [[package]] 48 | name = "anyhow" 49 | version = "1.0.86" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 52 | 53 | [[package]] 54 | name = "async-trait" 55 | version = "0.1.81" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" 58 | dependencies = [ 59 | "proc-macro2", 60 | "quote", 61 | "syn", 62 | ] 63 | 64 | [[package]] 65 | name = "autocfg" 66 | version = "1.3.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 69 | 70 | [[package]] 71 | name = "backtrace" 72 | version = "0.3.73" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 75 | dependencies = [ 76 | "addr2line", 77 | "cc", 78 | "cfg-if", 79 | "libc", 80 | "miniz_oxide", 81 | "object", 82 | "rustc-demangle", 83 | ] 84 | 85 | [[package]] 86 | name = "bitflags" 87 | version = "2.6.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 90 | 91 | [[package]] 92 | name = "block-buffer" 93 | version = "0.10.4" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 96 | dependencies = [ 97 | "generic-array", 98 | ] 99 | 100 | [[package]] 101 | name = "bstr" 102 | version = "1.9.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" 105 | dependencies = [ 106 | "memchr", 107 | "serde", 108 | ] 109 | 110 | [[package]] 111 | name = "bumpalo" 112 | version = "3.16.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 115 | dependencies = [ 116 | "allocator-api2", 117 | ] 118 | 119 | [[package]] 120 | name = "bytes" 121 | version = "1.6.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 124 | 125 | [[package]] 126 | name = "cc" 127 | version = "1.1.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" 130 | 131 | [[package]] 132 | name = "cfg-if" 133 | version = "1.0.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 136 | 137 | [[package]] 138 | name = "console" 139 | version = "0.15.8" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 142 | dependencies = [ 143 | "encode_unicode", 144 | "lazy_static", 145 | "libc", 146 | "unicode-width", 147 | "windows-sys", 148 | ] 149 | 150 | [[package]] 151 | name = "cpufeatures" 152 | version = "0.2.12" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 155 | dependencies = [ 156 | "libc", 157 | ] 158 | 159 | [[package]] 160 | name = "crossbeam-channel" 161 | version = "0.5.13" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 164 | dependencies = [ 165 | "crossbeam-utils", 166 | ] 167 | 168 | [[package]] 169 | name = "crossbeam-utils" 170 | version = "0.8.20" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 173 | 174 | [[package]] 175 | name = "crypto-common" 176 | version = "0.1.6" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 179 | dependencies = [ 180 | "generic-array", 181 | "typenum", 182 | ] 183 | 184 | [[package]] 185 | name = "deno_terminal" 186 | version = "0.1.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "7e6337d4e7f375f8b986409a76fbeecfa4bd8a1343e63355729ae4befa058eaf" 189 | dependencies = [ 190 | "once_cell", 191 | "termcolor", 192 | ] 193 | 194 | [[package]] 195 | name = "diff" 196 | version = "0.1.13" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 199 | 200 | [[package]] 201 | name = "digest" 202 | version = "0.10.7" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 205 | dependencies = [ 206 | "block-buffer", 207 | "crypto-common", 208 | ] 209 | 210 | [[package]] 211 | name = "dprint-core" 212 | version = "0.67.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "2309841cd79a9c60d2976f46b2df63d9a1a52211a3c62424e1e105b52dd5c436" 215 | dependencies = [ 216 | "anyhow", 217 | "async-trait", 218 | "bumpalo", 219 | "crossbeam-channel", 220 | "futures", 221 | "hashbrown", 222 | "indexmap", 223 | "libc", 224 | "parking_lot", 225 | "rustc-hash", 226 | "serde", 227 | "serde_json", 228 | "tokio", 229 | "tokio-util", 230 | "unicode-width", 231 | "winapi", 232 | ] 233 | 234 | [[package]] 235 | name = "dprint-development" 236 | version = "0.10.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "12bc835c6195bc3bd68ccc71b86ccb33cc2e9253cfc875bc371acea1ff034268" 239 | dependencies = [ 240 | "anyhow", 241 | "console", 242 | "file_test_runner", 243 | "serde_json", 244 | "similar", 245 | ] 246 | 247 | [[package]] 248 | name = "dprint-plugin-exec" 249 | version = "0.6.0" 250 | dependencies = [ 251 | "anyhow", 252 | "dprint-core", 253 | "dprint-development", 254 | "globset", 255 | "handlebars", 256 | "pretty_assertions", 257 | "serde", 258 | "serde_json", 259 | "sha2", 260 | "splitty", 261 | "tokio", 262 | ] 263 | 264 | [[package]] 265 | name = "encode_unicode" 266 | version = "0.3.6" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 269 | 270 | [[package]] 271 | name = "equivalent" 272 | version = "1.0.1" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 275 | 276 | [[package]] 277 | name = "file_test_runner" 278 | version = "0.5.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "d5eedfda6f865129a89c9cf35dc8203aaed94adf3ac42c2d8d7769ac32ca290d" 281 | dependencies = [ 282 | "anyhow", 283 | "crossbeam-channel", 284 | "deno_terminal", 285 | "parking_lot", 286 | "regex", 287 | "thiserror", 288 | ] 289 | 290 | [[package]] 291 | name = "futures" 292 | version = "0.3.30" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 295 | dependencies = [ 296 | "futures-channel", 297 | "futures-core", 298 | "futures-executor", 299 | "futures-io", 300 | "futures-sink", 301 | "futures-task", 302 | "futures-util", 303 | ] 304 | 305 | [[package]] 306 | name = "futures-channel" 307 | version = "0.3.30" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 310 | dependencies = [ 311 | "futures-core", 312 | "futures-sink", 313 | ] 314 | 315 | [[package]] 316 | name = "futures-core" 317 | version = "0.3.30" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 320 | 321 | [[package]] 322 | name = "futures-executor" 323 | version = "0.3.30" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 326 | dependencies = [ 327 | "futures-core", 328 | "futures-task", 329 | "futures-util", 330 | ] 331 | 332 | [[package]] 333 | name = "futures-io" 334 | version = "0.3.30" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 337 | 338 | [[package]] 339 | name = "futures-macro" 340 | version = "0.3.30" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 343 | dependencies = [ 344 | "proc-macro2", 345 | "quote", 346 | "syn", 347 | ] 348 | 349 | [[package]] 350 | name = "futures-sink" 351 | version = "0.3.30" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 354 | 355 | [[package]] 356 | name = "futures-task" 357 | version = "0.3.30" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 360 | 361 | [[package]] 362 | name = "futures-util" 363 | version = "0.3.30" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 366 | dependencies = [ 367 | "futures-channel", 368 | "futures-core", 369 | "futures-io", 370 | "futures-macro", 371 | "futures-sink", 372 | "futures-task", 373 | "memchr", 374 | "pin-project-lite", 375 | "pin-utils", 376 | "slab", 377 | ] 378 | 379 | [[package]] 380 | name = "generic-array" 381 | version = "0.14.7" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 384 | dependencies = [ 385 | "typenum", 386 | "version_check", 387 | ] 388 | 389 | [[package]] 390 | name = "gimli" 391 | version = "0.29.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 394 | 395 | [[package]] 396 | name = "globset" 397 | version = "0.4.14" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" 400 | dependencies = [ 401 | "aho-corasick", 402 | "bstr", 403 | "log", 404 | "regex-automata", 405 | "regex-syntax", 406 | ] 407 | 408 | [[package]] 409 | name = "handlebars" 410 | version = "5.1.2" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" 413 | dependencies = [ 414 | "log", 415 | "pest", 416 | "pest_derive", 417 | "serde", 418 | "serde_json", 419 | "thiserror", 420 | ] 421 | 422 | [[package]] 423 | name = "hashbrown" 424 | version = "0.14.5" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 427 | dependencies = [ 428 | "ahash", 429 | "allocator-api2", 430 | ] 431 | 432 | [[package]] 433 | name = "indexmap" 434 | version = "2.2.6" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 437 | dependencies = [ 438 | "equivalent", 439 | "hashbrown", 440 | "serde", 441 | ] 442 | 443 | [[package]] 444 | name = "itoa" 445 | version = "1.0.11" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 448 | 449 | [[package]] 450 | name = "lazy_static" 451 | version = "1.5.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 454 | 455 | [[package]] 456 | name = "libc" 457 | version = "0.2.155" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 460 | 461 | [[package]] 462 | name = "lock_api" 463 | version = "0.4.12" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 466 | dependencies = [ 467 | "autocfg", 468 | "scopeguard", 469 | ] 470 | 471 | [[package]] 472 | name = "log" 473 | version = "0.4.22" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 476 | 477 | [[package]] 478 | name = "memchr" 479 | version = "2.7.4" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 482 | 483 | [[package]] 484 | name = "miniz_oxide" 485 | version = "0.7.4" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 488 | dependencies = [ 489 | "adler", 490 | ] 491 | 492 | [[package]] 493 | name = "object" 494 | version = "0.36.1" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" 497 | dependencies = [ 498 | "memchr", 499 | ] 500 | 501 | [[package]] 502 | name = "once_cell" 503 | version = "1.19.0" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 506 | 507 | [[package]] 508 | name = "parking_lot" 509 | version = "0.12.3" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 512 | dependencies = [ 513 | "lock_api", 514 | "parking_lot_core", 515 | ] 516 | 517 | [[package]] 518 | name = "parking_lot_core" 519 | version = "0.9.10" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 522 | dependencies = [ 523 | "cfg-if", 524 | "libc", 525 | "redox_syscall", 526 | "smallvec", 527 | "windows-targets", 528 | ] 529 | 530 | [[package]] 531 | name = "pest" 532 | version = "2.7.11" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" 535 | dependencies = [ 536 | "memchr", 537 | "thiserror", 538 | "ucd-trie", 539 | ] 540 | 541 | [[package]] 542 | name = "pest_derive" 543 | version = "2.7.11" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" 546 | dependencies = [ 547 | "pest", 548 | "pest_generator", 549 | ] 550 | 551 | [[package]] 552 | name = "pest_generator" 553 | version = "2.7.11" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" 556 | dependencies = [ 557 | "pest", 558 | "pest_meta", 559 | "proc-macro2", 560 | "quote", 561 | "syn", 562 | ] 563 | 564 | [[package]] 565 | name = "pest_meta" 566 | version = "2.7.11" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" 569 | dependencies = [ 570 | "once_cell", 571 | "pest", 572 | "sha2", 573 | ] 574 | 575 | [[package]] 576 | name = "pin-project-lite" 577 | version = "0.2.14" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 580 | 581 | [[package]] 582 | name = "pin-utils" 583 | version = "0.1.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 586 | 587 | [[package]] 588 | name = "pretty_assertions" 589 | version = "1.4.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" 592 | dependencies = [ 593 | "diff", 594 | "yansi", 595 | ] 596 | 597 | [[package]] 598 | name = "proc-macro2" 599 | version = "1.0.86" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 602 | dependencies = [ 603 | "unicode-ident", 604 | ] 605 | 606 | [[package]] 607 | name = "quote" 608 | version = "1.0.36" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 611 | dependencies = [ 612 | "proc-macro2", 613 | ] 614 | 615 | [[package]] 616 | name = "redox_syscall" 617 | version = "0.5.2" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" 620 | dependencies = [ 621 | "bitflags", 622 | ] 623 | 624 | [[package]] 625 | name = "regex" 626 | version = "1.10.5" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 629 | dependencies = [ 630 | "aho-corasick", 631 | "memchr", 632 | "regex-automata", 633 | "regex-syntax", 634 | ] 635 | 636 | [[package]] 637 | name = "regex-automata" 638 | version = "0.4.7" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 641 | dependencies = [ 642 | "aho-corasick", 643 | "memchr", 644 | "regex-syntax", 645 | ] 646 | 647 | [[package]] 648 | name = "regex-syntax" 649 | version = "0.8.4" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 652 | 653 | [[package]] 654 | name = "rustc-demangle" 655 | version = "0.1.24" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 658 | 659 | [[package]] 660 | name = "rustc-hash" 661 | version = "1.1.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 664 | 665 | [[package]] 666 | name = "ryu" 667 | version = "1.0.18" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 670 | 671 | [[package]] 672 | name = "scopeguard" 673 | version = "1.2.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 676 | 677 | [[package]] 678 | name = "serde" 679 | version = "1.0.204" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 682 | dependencies = [ 683 | "serde_derive", 684 | ] 685 | 686 | [[package]] 687 | name = "serde_derive" 688 | version = "1.0.204" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 691 | dependencies = [ 692 | "proc-macro2", 693 | "quote", 694 | "syn", 695 | ] 696 | 697 | [[package]] 698 | name = "serde_json" 699 | version = "1.0.120" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" 702 | dependencies = [ 703 | "indexmap", 704 | "itoa", 705 | "ryu", 706 | "serde", 707 | ] 708 | 709 | [[package]] 710 | name = "sha2" 711 | version = "0.10.9" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 714 | dependencies = [ 715 | "cfg-if", 716 | "cpufeatures", 717 | "digest", 718 | ] 719 | 720 | [[package]] 721 | name = "similar" 722 | version = "2.5.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" 725 | 726 | [[package]] 727 | name = "slab" 728 | version = "0.4.9" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 731 | dependencies = [ 732 | "autocfg", 733 | ] 734 | 735 | [[package]] 736 | name = "smallvec" 737 | version = "1.13.2" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 740 | 741 | [[package]] 742 | name = "splitty" 743 | version = "1.0.1" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "8dae68aa5bd5dc2d3a2137b0f6bcdd8255dce1983dc155fe0246572e179c9c3a" 746 | 747 | [[package]] 748 | name = "syn" 749 | version = "2.0.70" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" 752 | dependencies = [ 753 | "proc-macro2", 754 | "quote", 755 | "unicode-ident", 756 | ] 757 | 758 | [[package]] 759 | name = "termcolor" 760 | version = "1.4.1" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 763 | dependencies = [ 764 | "winapi-util", 765 | ] 766 | 767 | [[package]] 768 | name = "thiserror" 769 | version = "1.0.61" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 772 | dependencies = [ 773 | "thiserror-impl", 774 | ] 775 | 776 | [[package]] 777 | name = "thiserror-impl" 778 | version = "1.0.61" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 781 | dependencies = [ 782 | "proc-macro2", 783 | "quote", 784 | "syn", 785 | ] 786 | 787 | [[package]] 788 | name = "tokio" 789 | version = "1.38.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" 792 | dependencies = [ 793 | "backtrace", 794 | "pin-project-lite", 795 | "tokio-macros", 796 | ] 797 | 798 | [[package]] 799 | name = "tokio-macros" 800 | version = "2.3.0" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" 803 | dependencies = [ 804 | "proc-macro2", 805 | "quote", 806 | "syn", 807 | ] 808 | 809 | [[package]] 810 | name = "tokio-util" 811 | version = "0.7.11" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 814 | dependencies = [ 815 | "bytes", 816 | "futures-core", 817 | "futures-sink", 818 | "pin-project-lite", 819 | "tokio", 820 | ] 821 | 822 | [[package]] 823 | name = "typenum" 824 | version = "1.17.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 827 | 828 | [[package]] 829 | name = "ucd-trie" 830 | version = "0.1.6" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 833 | 834 | [[package]] 835 | name = "unicode-ident" 836 | version = "1.0.12" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 839 | 840 | [[package]] 841 | name = "unicode-width" 842 | version = "0.1.13" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 845 | 846 | [[package]] 847 | name = "version_check" 848 | version = "0.9.4" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 851 | 852 | [[package]] 853 | name = "winapi" 854 | version = "0.3.9" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 857 | dependencies = [ 858 | "winapi-i686-pc-windows-gnu", 859 | "winapi-x86_64-pc-windows-gnu", 860 | ] 861 | 862 | [[package]] 863 | name = "winapi-i686-pc-windows-gnu" 864 | version = "0.4.0" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 867 | 868 | [[package]] 869 | name = "winapi-util" 870 | version = "0.1.8" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 873 | dependencies = [ 874 | "windows-sys", 875 | ] 876 | 877 | [[package]] 878 | name = "winapi-x86_64-pc-windows-gnu" 879 | version = "0.4.0" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 882 | 883 | [[package]] 884 | name = "windows-sys" 885 | version = "0.52.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 888 | dependencies = [ 889 | "windows-targets", 890 | ] 891 | 892 | [[package]] 893 | name = "windows-targets" 894 | version = "0.52.6" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 897 | dependencies = [ 898 | "windows_aarch64_gnullvm", 899 | "windows_aarch64_msvc", 900 | "windows_i686_gnu", 901 | "windows_i686_gnullvm", 902 | "windows_i686_msvc", 903 | "windows_x86_64_gnu", 904 | "windows_x86_64_gnullvm", 905 | "windows_x86_64_msvc", 906 | ] 907 | 908 | [[package]] 909 | name = "windows_aarch64_gnullvm" 910 | version = "0.52.6" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 913 | 914 | [[package]] 915 | name = "windows_aarch64_msvc" 916 | version = "0.52.6" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 919 | 920 | [[package]] 921 | name = "windows_i686_gnu" 922 | version = "0.52.6" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 925 | 926 | [[package]] 927 | name = "windows_i686_gnullvm" 928 | version = "0.52.6" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 931 | 932 | [[package]] 933 | name = "windows_i686_msvc" 934 | version = "0.52.6" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 937 | 938 | [[package]] 939 | name = "windows_x86_64_gnu" 940 | version = "0.52.6" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 943 | 944 | [[package]] 945 | name = "windows_x86_64_gnullvm" 946 | version = "0.52.6" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 949 | 950 | [[package]] 951 | name = "windows_x86_64_msvc" 952 | version = "0.52.6" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 955 | 956 | [[package]] 957 | name = "yansi" 958 | version = "0.5.1" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 961 | 962 | [[package]] 963 | name = "zerocopy" 964 | version = "0.7.35" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 967 | dependencies = [ 968 | "zerocopy-derive", 969 | ] 970 | 971 | [[package]] 972 | name = "zerocopy-derive" 973 | version = "0.7.35" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 976 | dependencies = [ 977 | "proc-macro2", 978 | "quote", 979 | "syn", 980 | ] 981 | --------------------------------------------------------------------------------