├── .gitignore ├── testdata ├── deno.json ├── additional_files │ └── jsr.json ├── module_graph │ └── tsconfig.json └── fmt │ └── with_config │ ├── deno.jsonc │ ├── deno.deprecated.jsonc │ └── subdir │ ├── b.ts │ ├── c.md │ └── a.ts ├── .rustfmt.toml ├── rust-toolchain.toml ├── README.md ├── src ├── sync.rs ├── lib.rs ├── util.rs ├── glob │ ├── gitignore.rs │ ├── collector.rs │ └── mod.rs ├── deno_json │ └── ts.rs └── workspace │ └── discovery.rs ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── LICENSE ├── Cargo.toml ├── clippy.toml └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | -------------------------------------------------------------------------------- /testdata/deno.json: -------------------------------------------------------------------------------- 1 | not a json file 2 | -------------------------------------------------------------------------------- /testdata/additional_files/jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@foo/bar" 3 | } -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2021" 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /testdata/module_graph/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "jsx": "preserve" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `deno_config` 2 | 3 | **ARCHIVED**: This repo was moved into https://github.com/denoland/deno 4 | 5 | An implementation of the 6 | [Deno configuration file](https://docs.deno.com/runtime/manual/getting_started/configuration_file/) 7 | in Rust. 8 | -------------------------------------------------------------------------------- /testdata/fmt/with_config/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "fmt": { 3 | "include": [ 4 | "./subdir/" 5 | ], 6 | "exclude": [ 7 | "./subdir/b.ts" 8 | ], 9 | "useTabs": true, 10 | "lineWidth": 40, 11 | "indentWidth": 8, 12 | "singleQuote": true, 13 | "proseWrap": "always", 14 | "semiColons": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/fmt/with_config/deno.deprecated.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "fmt": { 3 | "files": { 4 | "include": [ 5 | "./subdir/" 6 | ], 7 | "exclude": [ 8 | "./subdir/b.ts" 9 | ] 10 | }, 11 | "options": { 12 | "useTabs": true, 13 | "lineWidth": 40, 14 | "indentWidth": 8, 15 | "singleQuote": true, 16 | "proseWrap": "always", 17 | "semiColons": false 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/sync.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | pub use inner::*; 4 | 5 | #[cfg(feature = "sync")] 6 | mod inner { 7 | #![allow(clippy::disallowed_types)] 8 | pub use std::sync::Arc as MaybeArc; 9 | } 10 | 11 | #[cfg(not(feature = "sync"))] 12 | mod inner { 13 | pub use std::rc::Rc as MaybeArc; 14 | } 15 | 16 | // ok for constructing 17 | #[allow(clippy::disallowed_types)] 18 | pub fn new_rc(value: T) -> MaybeArc { 19 | MaybeArc::new(value) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/fmt/with_config/subdir/b.ts: -------------------------------------------------------------------------------- 1 | // This file should be excluded from formatting 2 | Deno.test( 3 | { perms: { net: true } }, 4 | async function fetchBodyUsedCancelStream() { 5 | const response = await fetch( 6 | "http://localhost:4545/assets/fixture.json", 7 | ); 8 | assert(response.body !== null); 9 | 10 | assertEquals(response.bodyUsed, false); 11 | const promise = response.body.cancel(); 12 | assertEquals(response.bodyUsed, true); 13 | await promise; 14 | }, 15 | ); -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | #![deny(clippy::print_stderr)] 4 | #![deny(clippy::print_stdout)] 5 | #![deny(clippy::unused_async)] 6 | #![deny(clippy::unnecessary_wraps)] 7 | 8 | #[cfg(feature = "deno_json")] 9 | pub mod deno_json; 10 | #[cfg(feature = "deno_json")] 11 | pub mod glob; 12 | #[cfg(feature = "deno_json")] 13 | mod sync; 14 | #[cfg(feature = "deno_json")] 15 | mod util; 16 | #[cfg(feature = "workspace")] 17 | pub mod workspace; 18 | 19 | #[cfg(feature = "deno_json")] 20 | pub use deno_path_util::UrlToFilePathError; 21 | -------------------------------------------------------------------------------- /testdata/fmt/with_config/subdir/c.md: -------------------------------------------------------------------------------- 1 | ## Permissions 2 | 3 | Deno is secure by default. Therefore, 4 | unless you specifically enable it, a 5 | program run with Deno has no file, 6 | network, or environment access. Access 7 | to security sensitive functionality 8 | requires that permisisons have been 9 | granted to an executing script through 10 | command line flags, or a runtime 11 | permission prompt. 12 | 13 | For the following example `mod.ts` has 14 | been granted read-only access to the 15 | file system. It cannot write to the file 16 | system, or perform any other security 17 | sensitive functions. 18 | -------------------------------------------------------------------------------- /testdata/fmt/with_config/subdir/a.ts: -------------------------------------------------------------------------------- 1 | Deno.test( 2 | { perms: { net: true } }, 3 | async function responseClone() { 4 | const response = 5 | await fetch( 6 | 'http://localhost:4545/assets/fixture.json', 7 | ) 8 | const response1 = 9 | response.clone() 10 | assert( 11 | response !== 12 | response1, 13 | ) 14 | assertEquals( 15 | response.status, 16 | response1 17 | .status, 18 | ) 19 | assertEquals( 20 | response.statusText, 21 | response1 22 | .statusText, 23 | ) 24 | const u8a = 25 | new Uint8Array( 26 | await response 27 | .arrayBuffer(), 28 | ) 29 | const u8a1 = 30 | new Uint8Array( 31 | await response1 32 | .arrayBuffer(), 33 | ) 34 | for ( 35 | let i = 0; 36 | i < 37 | u8a.byteLength; 38 | i++ 39 | ) { 40 | assertEquals( 41 | u8a[i], 42 | u8a1[i], 43 | ) 44 | } 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | pub fn is_skippable_io_error(e: &std::io::Error) -> bool { 4 | use std::io::ErrorKind::*; 5 | 6 | // skip over invalid filenames on windows 7 | const ERROR_INVALID_NAME: i32 = 123; 8 | if cfg!(windows) && e.raw_os_error() == Some(ERROR_INVALID_NAME) { 9 | return true; 10 | } 11 | 12 | match e.kind() { 13 | InvalidInput | PermissionDenied | NotFound => { 14 | // ok keep going 15 | true 16 | } 17 | _ => { 18 | const NOT_A_DIRECTORY: i32 = 20; 19 | cfg!(unix) && e.raw_os_error() == Some(NOT_A_DIRECTORY) 20 | } 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | #[cfg(windows)] 27 | #[test] 28 | fn is_skippable_io_error_win_invalid_filename() { 29 | let error = std::io::Error::from_raw_os_error(123); 30 | assert!(super::is_skippable_io_error(&error)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.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.DENOBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v1 28 | - uses: dsherret/rust-toolchain-file@v1 29 | 30 | - name: Tag and release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 33 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 34 | run: | 35 | git config user.email "denobot@users.noreply.github.com" 36 | git config user.name "denobot" 37 | deno run -A https://raw.githubusercontent.com/denoland/automation/0.15.0/tasks/publish_release.ts --${{github.event.inputs.releaseKind}} 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018-2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deno_config" 3 | description = "Config file implementation for the Deno CLI" 4 | version = "0.57.0" 5 | edition = "2021" 6 | authors = ["the Deno authors"] 7 | license = "MIT" 8 | repository = "https://github.com/denoland/deno_config" 9 | 10 | [workspace] 11 | members = ["."] 12 | 13 | [workspace.dependencies] 14 | sys_traits = "0.1.8" 15 | 16 | [features] 17 | default = ["workspace"] 18 | deno_json = ["jsonc-parser", "glob", "ignore", "import_map", "phf"] 19 | package_json = ["deno_package_json"] 20 | sync = ["deno_package_json/sync"] 21 | workspace = ["deno_json", "deno_semver", "package_json"] 22 | 23 | [dependencies] 24 | capacity_builder = { version = "0.5.0" } 25 | indexmap = { version = "2", features = ["serde"] } 26 | jsonc-parser = { version = "0.26.0", features = ["serde"], optional = true } 27 | log = "0.4.20" 28 | glob = { version = "0.3.1", optional = true } 29 | ignore = { version = "0.4", optional = true } 30 | percent-encoding = "2.3.0" 31 | phf = { version = "0.11", features = ["macros"], optional = true } 32 | serde = { version = "1.0.149", features = ["derive"] } 33 | serde_json = "1.0.85" 34 | url = { version = "2.3.1" } 35 | import_map = { version = "0.22.0", features = ["ext"], optional = true } 36 | thiserror = "2" 37 | deno_error = { version = "0.6.0", features = ["url"] } 38 | boxed_error = "0.2.3" 39 | deno_semver = { version = "0.8.0", optional = true } 40 | deno_package_json = { version = "0.9.0", optional = true } 41 | deno_path_util = "0.4.0" 42 | sys_traits.workspace = true 43 | 44 | [dev-dependencies] 45 | tempfile = "3.4.0" 46 | pretty_assertions = "1.4.0" 47 | sys_traits = { workspace = true, features = ["memory", "real", "serde_json"] } 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rust: 7 | name: deno_config-${{ matrix.os }} 8 | if: | 9 | (github.event_name == 'push' || !startsWith(github.event.pull_request.head.label, 'denoland:')) 10 | && github.ref_name != 'deno_config' 11 | && !startsWith(github.ref, 'refs/tags/deno/') 12 | runs-on: ${{ matrix.os }} 13 | timeout-minutes: 15 14 | strategy: 15 | matrix: 16 | os: [macOS-latest, ubuntu-latest, windows-2019] 17 | 18 | env: 19 | CARGO_INCREMENTAL: 0 20 | GH_ACTIONS: 1 21 | RUSTFLAGS: -D warnings 22 | 23 | steps: 24 | - name: Clone repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install Rust 28 | uses: dsherret/rust-toolchain-file@v1 29 | 30 | - uses: Swatinem/rust-cache@v2 31 | with: 32 | save-if: ${{ github.ref == 'refs/heads/main' }} 33 | 34 | - name: Format 35 | if: contains(matrix.os, 'ubuntu') 36 | run: | 37 | cargo fmt -- --check 38 | 39 | - name: Check build features 40 | if: contains(matrix.os, 'ubuntu') 41 | run: | 42 | cargo check --no-default-features 43 | cargo check --no-default-features --features workspace 44 | cargo check --no-default-features --features package_json 45 | cargo check --no-default-features --features workspace --features sync 46 | 47 | - name: Check builds Wasm 48 | if: contains(matrix.os, 'ubuntu') 49 | run: | 50 | rustup target add wasm32-unknown-unknown 51 | cargo check --all-features --target wasm32-unknown-unknown 52 | 53 | - name: Cargo test 54 | run: cargo test --locked --release --all-features --bins --tests --examples 55 | 56 | - name: Lint 57 | if: contains(matrix.os, 'ubuntu') 58 | run: | 59 | cargo clippy --locked --all-features --all-targets -- -D clippy::all 60 | 61 | - name: Cargo publish 62 | if: | 63 | contains(matrix.os, 'ubuntu') && 64 | github.repository == 'denoland/deno_config' && 65 | startsWith(github.ref, 'refs/tags/') 66 | env: 67 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 68 | run: cargo publish 69 | 70 | - name: Get tag version 71 | if: contains(matrix.os, 'ubuntu') && startsWith(github.ref, 'refs/tags/') 72 | id: get_tag_version 73 | run: echo TAG_VERSION=${GITHUB_REF/refs\/tags\//} >> "$GITHUB_OUTPUT" 74 | 75 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-methods = [ 2 | { path = "std::env::current_dir", reason = "File system operations should be done using the sys_traits crate" }, 3 | { path = "std::path::Path::exists", reason = "File system operations should be done using the sys_traits crate" }, 4 | { path = "std::path::Path::canonicalize", reason = "File system operations should be done using the sys_traits crate" }, 5 | { path = "std::path::Path::is_dir", reason = "File system operations should be done using the sys_traits crate" }, 6 | { path = "std::path::Path::is_file", reason = "File system operations should be done using the sys_traits crate" }, 7 | { path = "std::path::Path::is_symlink", reason = "File system operations should be done using the sys_traits crate" }, 8 | { path = "std::path::Path::metadata", reason = "File system operations should be done using the sys_traits crate" }, 9 | { path = "std::path::Path::read_dir", reason = "File system operations should be done using the sys_traits crate" }, 10 | { path = "std::path::Path::read_link", reason = "File system operations should be done using the sys_traits crate" }, 11 | { path = "std::path::Path::symlink_metadata", reason = "File system operations should be done using the sys_traits crate" }, 12 | { path = "std::path::Path::try_exists", reason = "File system operations should be done using the sys_traits crate" }, 13 | { path = "std::path::PathBuf::exists", reason = "File system operations should be done using the sys_traits crate" }, 14 | { path = "std::path::PathBuf::canonicalize", reason = "File system operations should be done using the sys_traits crate" }, 15 | { path = "std::path::PathBuf::is_dir", reason = "File system operations should be done using the sys_traits crate" }, 16 | { path = "std::path::PathBuf::is_file", reason = "File system operations should be done using the sys_traits crate" }, 17 | { path = "std::path::PathBuf::is_symlink", reason = "File system operations should be done using the sys_traits crate" }, 18 | { path = "std::path::PathBuf::metadata", reason = "File system operations should be done using the sys_traits crate" }, 19 | { path = "std::path::PathBuf::read_dir", reason = "File system operations should be done using the sys_traits crate" }, 20 | { path = "std::path::PathBuf::read_link", reason = "File system operations should be done using the sys_traits crate" }, 21 | { path = "std::path::PathBuf::symlink_metadata", reason = "File system operations should be done using the sys_traits crate" }, 22 | { path = "std::path::PathBuf::try_exists", reason = "File system operations should be done using the sys_traits crate" }, 23 | { path = "std::fs::canonicalize", reason = "File system operations should be done using the sys_traits crate" }, 24 | { path = "std::fs::copy", reason = "File system operations should be done using the sys_traits crate" }, 25 | { path = "std::fs::create_dir", reason = "File system operations should be done using the sys_traits crate" }, 26 | { path = "std::fs::create_dir_all", reason = "File system operations should be done using the sys_traits crate" }, 27 | { path = "std::fs::hard_link", reason = "File system operations should be done using the sys_traits crate" }, 28 | { path = "std::fs::metadata", reason = "File system operations should be done using the sys_traits crate" }, 29 | { path = "std::fs::read", reason = "File system operations should be done using the sys_traits crate" }, 30 | { path = "std::fs::read_dir", reason = "File system operations should be done using the sys_traits crate" }, 31 | { path = "std::fs::read_link", reason = "File system operations should be done using the sys_traits crate" }, 32 | { path = "std::fs::read_to_string", reason = "File system operations should be done using the sys_traits crate" }, 33 | { path = "std::fs::remove_dir", reason = "File system operations should be done using the sys_traits crate" }, 34 | { path = "std::fs::remove_dir_all", reason = "File system operations should be done using the sys_traits crate" }, 35 | { path = "std::fs::remove_file", reason = "File system operations should be done using the sys_traits crate" }, 36 | { path = "std::fs::rename", reason = "File system operations should be done using the sys_traits crate" }, 37 | { path = "std::fs::set_permissions", reason = "File system operations should be done using the sys_traits crate" }, 38 | { path = "std::fs::symlink_metadata", reason = "File system operations should be done using the sys_traits crate" }, 39 | { path = "std::fs::write", reason = "File system operations should be done using the sys_traits crate" } 40 | ] 41 | disallowed-types = [ 42 | { path = "std::sync::Arc", reason = "use crate::sync::MaybeArc instead" }, 43 | ] 44 | -------------------------------------------------------------------------------- /src/glob/gitignore.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use std::collections::HashMap; 4 | use std::path::Path; 5 | use std::path::PathBuf; 6 | use std::rc::Rc; 7 | 8 | use sys_traits::FsMetadata; 9 | use sys_traits::FsRead; 10 | 11 | /// Resolved gitignore for a directory. 12 | pub struct DirGitIgnores { 13 | current: Option>, 14 | parent: Option>, 15 | } 16 | 17 | impl DirGitIgnores { 18 | pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool { 19 | let mut is_ignored = false; 20 | if let Some(parent) = &self.parent { 21 | is_ignored = parent.is_ignored(path, is_dir); 22 | } 23 | if let Some(current) = &self.current { 24 | match current.matched(path, is_dir) { 25 | ignore::Match::None => {} 26 | ignore::Match::Ignore(_) => { 27 | is_ignored = true; 28 | } 29 | ignore::Match::Whitelist(_) => { 30 | is_ignored = false; 31 | } 32 | } 33 | } 34 | is_ignored 35 | } 36 | } 37 | 38 | /// Resolves gitignores in a directory tree taking into account 39 | /// ancestor gitignores that may be found in a directory. 40 | pub struct GitIgnoreTree<'a, Sys: FsRead + FsMetadata> { 41 | sys: &'a Sys, 42 | ignores: HashMap>>, 43 | include_paths: Vec, 44 | } 45 | 46 | impl<'a, Sys: FsRead + FsMetadata> GitIgnoreTree<'a, Sys> { 47 | pub fn new( 48 | sys: &'a Sys, 49 | // paths that should override what's in the gitignore 50 | include_paths: Vec, 51 | ) -> Self { 52 | Self { 53 | sys, 54 | ignores: Default::default(), 55 | include_paths, 56 | } 57 | } 58 | 59 | pub fn get_resolved_git_ignore_for_dir( 60 | &mut self, 61 | dir_path: &Path, 62 | ) -> Option> { 63 | // for directories, provide itself in order to tell 64 | // if it should stop searching for gitignores because 65 | // maybe this dir_path is a .git directory 66 | let parent = dir_path.parent()?; 67 | self.get_resolved_git_ignore_inner(parent, Some(dir_path)) 68 | } 69 | 70 | pub fn get_resolved_git_ignore_for_file( 71 | &mut self, 72 | file_path: &Path, 73 | ) -> Option> { 74 | let dir_path = file_path.parent()?; 75 | self.get_resolved_git_ignore_inner(dir_path, None) 76 | } 77 | 78 | fn get_resolved_git_ignore_inner( 79 | &mut self, 80 | dir_path: &Path, 81 | maybe_parent: Option<&Path>, 82 | ) -> Option> { 83 | let maybe_resolved = self.ignores.get(dir_path).cloned(); 84 | if let Some(resolved) = maybe_resolved { 85 | resolved 86 | } else { 87 | let resolved = self.resolve_gitignore_in_dir(dir_path, maybe_parent); 88 | self.ignores.insert(dir_path.to_owned(), resolved.clone()); 89 | resolved 90 | } 91 | } 92 | 93 | fn resolve_gitignore_in_dir( 94 | &mut self, 95 | dir_path: &Path, 96 | maybe_parent: Option<&Path>, 97 | ) -> Option> { 98 | if let Some(parent) = maybe_parent { 99 | // stop searching if the parent dir had a .git directory in it 100 | if self.sys.fs_exists_no_err(parent.join(".git")) { 101 | return None; 102 | } 103 | } 104 | 105 | let parent = dir_path.parent().and_then(|parent| { 106 | self.get_resolved_git_ignore_inner(parent, Some(dir_path)) 107 | }); 108 | let current = self 109 | .sys 110 | .fs_read_to_string_lossy(dir_path.join(".gitignore")) 111 | .ok() 112 | .and_then(|text| { 113 | let mut builder = ignore::gitignore::GitignoreBuilder::new(dir_path); 114 | for line in text.lines() { 115 | builder.add_line(None, line).ok()?; 116 | } 117 | // override the gitignore contents to include these paths 118 | for path in &self.include_paths { 119 | if let Ok(suffix) = path.strip_prefix(dir_path) { 120 | let suffix = suffix.to_string_lossy().replace('\\', "/"); 121 | let _ignore = builder.add_line(None, &format!("!/{}", suffix)); 122 | if !suffix.ends_with('/') { 123 | let _ignore = builder.add_line(None, &format!("!/{}/", suffix)); 124 | } 125 | } 126 | } 127 | let gitignore = builder.build().ok()?; 128 | Some(Rc::new(gitignore)) 129 | }); 130 | if parent.is_none() && current.is_none() { 131 | None 132 | } else { 133 | Some(Rc::new(DirGitIgnores { current, parent })) 134 | } 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod test { 140 | use sys_traits::impls::InMemorySys; 141 | use sys_traits::FsCreateDirAll; 142 | use sys_traits::FsWrite; 143 | 144 | use super::*; 145 | 146 | #[test] 147 | fn git_ignore_tree() { 148 | let sys = InMemorySys::default(); 149 | sys.fs_create_dir_all("/sub_dir/sub_dir").unwrap(); 150 | sys.fs_write("/.gitignore", "file.txt").unwrap(); 151 | sys.fs_write("/sub_dir/.gitignore", "data.txt").unwrap(); 152 | sys 153 | .fs_write("/sub_dir/sub_dir/.gitignore", "!file.txt\nignore.txt") 154 | .unwrap(); 155 | let mut ignore_tree = GitIgnoreTree::new(&sys, Vec::new()); 156 | let mut run_test = |path: &str, expected: bool| { 157 | let path = PathBuf::from(path); 158 | let gitignore = 159 | ignore_tree.get_resolved_git_ignore_for_file(&path).unwrap(); 160 | assert_eq!( 161 | gitignore.is_ignored(&path, /* is_dir */ false), 162 | expected, 163 | "Path: {}", 164 | path.display() 165 | ); 166 | }; 167 | run_test("/file.txt", true); 168 | run_test("/other.txt", false); 169 | run_test("/data.txt", false); 170 | run_test("/sub_dir/file.txt", true); 171 | run_test("/sub_dir/other.txt", false); 172 | run_test("/sub_dir/data.txt", true); 173 | run_test("/sub_dir/sub_dir/file.txt", false); // unignored up here 174 | run_test("/sub_dir/sub_dir/sub_dir/file.txt", false); 175 | run_test("/sub_dir/sub_dir/sub_dir/ignore.txt", true); 176 | run_test("/sub_dir/sub_dir/ignore.txt", true); 177 | run_test("/sub_dir/ignore.txt", false); 178 | run_test("/ignore.txt", false); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/deno_json/ts.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use serde::Serializer; 6 | use serde_json::Value; 7 | use std::fmt; 8 | use url::Url; 9 | 10 | #[derive(Debug, Default, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct RawJsxCompilerOptions { 13 | pub jsx: Option, 14 | pub jsx_import_source: Option, 15 | pub jsx_import_source_types: Option, 16 | } 17 | 18 | /// The transpile options that are significant out of a user provided tsconfig 19 | /// file, that we want to deserialize out of the final config for a transpile. 20 | #[derive(Debug, Deserialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct EmitConfigOptions { 23 | pub check_js: bool, 24 | pub experimental_decorators: bool, 25 | pub emit_decorator_metadata: bool, 26 | pub imports_not_used_as_values: String, 27 | pub inline_source_map: bool, 28 | pub inline_sources: bool, 29 | pub source_map: bool, 30 | pub jsx: String, 31 | pub jsx_factory: String, 32 | pub jsx_fragment_factory: String, 33 | pub jsx_import_source: Option, 34 | pub jsx_precompile_skip_elements: Option>, 35 | } 36 | 37 | /// A structure that represents a set of options that were ignored and the 38 | /// path those options came from. 39 | #[derive(Debug, Clone, Eq, PartialEq)] 40 | pub struct IgnoredCompilerOptions { 41 | pub items: Vec, 42 | pub maybe_specifier: Option, 43 | } 44 | 45 | impl fmt::Display for IgnoredCompilerOptions { 46 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 47 | let mut codes = self.items.clone(); 48 | codes.sort_unstable(); 49 | if let Some(specifier) = &self.maybe_specifier { 50 | write!(f, "Unsupported compiler options in \"{}\".\n The following options were ignored:\n {}", specifier, codes.join(", ")) 51 | } else { 52 | write!(f, "Unsupported compiler options provided.\n The following options were ignored:\n {}", codes.join(", ")) 53 | } 54 | } 55 | } 56 | 57 | impl Serialize for IgnoredCompilerOptions { 58 | fn serialize(&self, serializer: S) -> Result 59 | where 60 | S: Serializer, 61 | { 62 | Serialize::serialize(&self.items, serializer) 63 | } 64 | } 65 | 66 | /// A set of all the compiler options that should be allowed; 67 | static ALLOWED_COMPILER_OPTIONS: phf::Set<&'static str> = phf::phf_set! { 68 | "allowUnreachableCode", 69 | "allowUnusedLabels", 70 | "checkJs", 71 | "erasableSyntaxOnly", 72 | "emitDecoratorMetadata", 73 | "exactOptionalPropertyTypes", 74 | "experimentalDecorators", 75 | "isolatedDeclarations", 76 | "jsx", 77 | "jsxFactory", 78 | "jsxFragmentFactory", 79 | "jsxImportSource", 80 | "jsxPrecompileSkipElements", 81 | "lib", 82 | "noErrorTruncation", 83 | "noFallthroughCasesInSwitch", 84 | "noImplicitAny", 85 | "noImplicitOverride", 86 | "noImplicitReturns", 87 | "noImplicitThis", 88 | "noPropertyAccessFromIndexSignature", 89 | "noUncheckedIndexedAccess", 90 | "noUnusedLocals", 91 | "noUnusedParameters", 92 | "rootDirs", 93 | "strict", 94 | "strictBindCallApply", 95 | "strictBuiltinIteratorReturn", 96 | "strictFunctionTypes", 97 | "strictNullChecks", 98 | "strictPropertyInitialization", 99 | "types", 100 | "useUnknownInCatchVariables", 101 | "verbatimModuleSyntax", 102 | }; 103 | 104 | #[derive(Debug, Default, Clone)] 105 | pub struct ParsedTsConfigOptions { 106 | pub options: serde_json::Map, 107 | pub maybe_ignored: Option, 108 | } 109 | 110 | pub fn parse_compiler_options( 111 | compiler_options: serde_json::Map, 112 | maybe_specifier: Option<&Url>, 113 | ) -> ParsedTsConfigOptions { 114 | let mut allowed: serde_json::Map = 115 | serde_json::Map::with_capacity(compiler_options.len()); 116 | let mut ignored: Vec = Vec::new(); // don't pre-allocate because it's rare 117 | 118 | for (key, value) in compiler_options { 119 | // We don't pass "types" entries to typescript via the compiler 120 | // options and instead provide those to tsc as "roots". This is 121 | // because our "types" behavior is at odds with how TypeScript's 122 | // "types" works. 123 | // We also don't pass "jsxImportSourceTypes" to TypeScript as it doesn't 124 | // know about this option. It will still take this option into account 125 | // because the graph resolves the JSX import source to the types for TSC. 126 | if key != "types" && key != "jsxImportSourceTypes" { 127 | if ALLOWED_COMPILER_OPTIONS.contains(key.as_str()) { 128 | allowed.insert(key, value.to_owned()); 129 | } else { 130 | ignored.push(key); 131 | } 132 | } 133 | } 134 | let maybe_ignored = if !ignored.is_empty() { 135 | Some(IgnoredCompilerOptions { 136 | items: ignored, 137 | maybe_specifier: maybe_specifier.cloned(), 138 | }) 139 | } else { 140 | None 141 | }; 142 | 143 | ParsedTsConfigOptions { 144 | options: allowed, 145 | maybe_ignored, 146 | } 147 | } 148 | 149 | /// A structure for managing the configuration of TypeScript 150 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 151 | pub struct TsConfig(pub Value); 152 | 153 | impl Default for TsConfig { 154 | fn default() -> Self { 155 | Self(serde_json::Value::Object(Default::default())) 156 | } 157 | } 158 | 159 | impl TsConfig { 160 | /// Create a new `TsConfig` with the base being the `value` supplied. 161 | pub fn new(value: Value) -> Self { 162 | TsConfig(value) 163 | } 164 | 165 | pub fn merge_mut(&mut self, value: TsConfig) { 166 | json_merge(&mut self.0, value.0); 167 | } 168 | 169 | /// Merge a serde_json value into the configuration. 170 | pub fn merge_object_mut( 171 | &mut self, 172 | value: serde_json::Map, 173 | ) { 174 | json_merge(&mut self.0, serde_json::Value::Object(value)); 175 | } 176 | } 177 | 178 | impl Serialize for TsConfig { 179 | /// Serializes inner hash map which is ordered by the key 180 | fn serialize(&self, serializer: S) -> std::result::Result 181 | where 182 | S: Serializer, 183 | { 184 | Serialize::serialize(&self.0, serializer) 185 | } 186 | } 187 | 188 | /// A function that works like JavaScript's `Object.assign()`. 189 | fn json_merge(a: &mut Value, b: Value) { 190 | match (a, b) { 191 | (&mut Value::Object(ref mut a), Value::Object(b)) => { 192 | for (k, v) in b { 193 | json_merge(a.entry(k).or_insert(Value::Null), v); 194 | } 195 | } 196 | (a, b) => { 197 | *a = b; 198 | } 199 | } 200 | } 201 | 202 | #[cfg(test)] 203 | mod tests { 204 | use serde_json::json; 205 | 206 | use super::*; 207 | 208 | #[test] 209 | fn test_json_merge() { 210 | let mut value_a = json!({ 211 | "a": true, 212 | "b": "c" 213 | }); 214 | let value_b = json!({ 215 | "b": "d", 216 | "e": false, 217 | }); 218 | json_merge(&mut value_a, value_b); 219 | assert_eq!( 220 | value_a, 221 | json!({ 222 | "a": true, 223 | "b": "d", 224 | "e": false, 225 | }) 226 | ); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/glob/collector.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use std::collections::HashSet; 4 | use std::collections::VecDeque; 5 | use std::path::Path; 6 | use std::path::PathBuf; 7 | 8 | use deno_path_util::normalize_path; 9 | use sys_traits::FsDirEntry; 10 | use sys_traits::FsMetadata; 11 | use sys_traits::FsMetadataValue; 12 | use sys_traits::FsRead; 13 | use sys_traits::FsReadDir; 14 | 15 | use crate::glob::gitignore::DirGitIgnores; 16 | use crate::glob::gitignore::GitIgnoreTree; 17 | use crate::glob::FilePatternsMatch; 18 | use crate::glob::PathKind; 19 | use crate::glob::PathOrPattern; 20 | 21 | use super::FilePatterns; 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct WalkEntry<'a> { 25 | pub path: &'a Path, 26 | pub metadata: &'a dyn FsMetadataValue, 27 | pub patterns: &'a FilePatterns, 28 | } 29 | 30 | /// Collects file paths that satisfy the given predicate, by recursively walking `files`. 31 | /// If the walker visits a path that is listed in `ignore`, it skips descending into the directory. 32 | pub struct FileCollector bool> { 33 | file_filter: TFilter, 34 | ignore_git_folder: bool, 35 | ignore_node_modules: bool, 36 | vendor_folder: Option, 37 | use_gitignore: bool, 38 | } 39 | 40 | impl bool> FileCollector { 41 | pub fn new(file_filter: TFilter) -> Self { 42 | Self { 43 | file_filter, 44 | ignore_git_folder: false, 45 | ignore_node_modules: false, 46 | vendor_folder: None, 47 | use_gitignore: false, 48 | } 49 | } 50 | 51 | pub fn ignore_node_modules(mut self) -> Self { 52 | self.ignore_node_modules = true; 53 | self 54 | } 55 | 56 | pub fn set_vendor_folder(mut self, vendor_folder: Option) -> Self { 57 | self.vendor_folder = vendor_folder; 58 | self 59 | } 60 | 61 | pub fn ignore_git_folder(mut self) -> Self { 62 | self.ignore_git_folder = true; 63 | self 64 | } 65 | 66 | pub fn use_gitignore(mut self) -> Self { 67 | self.use_gitignore = true; 68 | self 69 | } 70 | 71 | pub fn collect_file_patterns( 72 | &self, 73 | sys: &TSys, 74 | file_patterns: FilePatterns, 75 | ) -> Vec { 76 | fn is_pattern_matched( 77 | maybe_git_ignore: Option<&DirGitIgnores>, 78 | path: &Path, 79 | is_dir: bool, 80 | file_patterns: &FilePatterns, 81 | ) -> bool { 82 | let path_kind = match is_dir { 83 | true => PathKind::Directory, 84 | false => PathKind::File, 85 | }; 86 | match file_patterns.matches_path_detail(path, path_kind) { 87 | FilePatternsMatch::Passed => { 88 | // check gitignore 89 | let is_gitignored = maybe_git_ignore 90 | .as_ref() 91 | .map(|git_ignore| git_ignore.is_ignored(path, is_dir)) 92 | .unwrap_or(false); 93 | !is_gitignored 94 | } 95 | FilePatternsMatch::PassedOptedOutExclude => true, 96 | FilePatternsMatch::Excluded => false, 97 | } 98 | } 99 | 100 | let mut maybe_git_ignores = if self.use_gitignore { 101 | // Override explicitly specified include paths in the 102 | // .gitignore file. This does not apply to globs because 103 | // that is way too complicated to reason about. 104 | let include_paths = file_patterns 105 | .include 106 | .as_ref() 107 | .map(|include| { 108 | include 109 | .inner() 110 | .iter() 111 | .filter_map(|path_or_pattern| { 112 | if let PathOrPattern::Path(p) = path_or_pattern { 113 | Some(p.clone()) 114 | } else { 115 | None 116 | } 117 | }) 118 | .collect::>() 119 | }) 120 | .unwrap_or_default(); 121 | Some(GitIgnoreTree::new(sys, include_paths)) 122 | } else { 123 | None 124 | }; 125 | let mut target_files = Vec::new(); 126 | let mut visited_paths: HashSet = HashSet::default(); 127 | let file_patterns_by_base = file_patterns.split_by_base(); 128 | for file_patterns in file_patterns_by_base { 129 | let specified_path = normalize_path(&file_patterns.base); 130 | let mut pending_dirs = VecDeque::new(); 131 | let mut handle_entry = 132 | |path: PathBuf, 133 | metadata: &dyn FsMetadataValue, 134 | pending_dirs: &mut VecDeque| { 135 | let maybe_gitignore = 136 | maybe_git_ignores.as_mut().and_then(|git_ignores| { 137 | if metadata.file_type().is_dir() { 138 | git_ignores.get_resolved_git_ignore_for_dir(&path) 139 | } else { 140 | git_ignores.get_resolved_git_ignore_for_file(&path) 141 | } 142 | }); 143 | if !is_pattern_matched( 144 | maybe_gitignore.as_deref(), 145 | &path, 146 | metadata.file_type().is_dir(), 147 | &file_patterns, 148 | ) { 149 | // ignore 150 | } else if metadata.file_type().is_dir() { 151 | // allow the user to opt out of ignoring by explicitly specifying the dir 152 | let opt_out_ignore = specified_path == path; 153 | let should_ignore_dir = 154 | !opt_out_ignore && self.is_ignored_dir(&path); 155 | if !should_ignore_dir && visited_paths.insert(path.clone()) { 156 | pending_dirs.push_back(path); 157 | } 158 | } else if (self.file_filter)(WalkEntry { 159 | path: &path, 160 | metadata, 161 | patterns: &file_patterns, 162 | }) && visited_paths.insert(path.clone()) 163 | { 164 | target_files.push(path); 165 | } 166 | }; 167 | 168 | if let Ok(metadata) = sys.fs_metadata(&specified_path) { 169 | handle_entry(specified_path.clone(), &metadata, &mut pending_dirs); 170 | } 171 | 172 | // use an iterator in order to minimize the number of file system operations 173 | while let Some(next_dir) = pending_dirs.pop_front() { 174 | let Ok(entries) = sys.fs_read_dir(&next_dir) else { 175 | continue; 176 | }; 177 | for entry in entries { 178 | let Ok(entry) = entry else { 179 | continue; 180 | }; 181 | let Ok(metadata) = entry.metadata() else { 182 | continue; 183 | }; 184 | handle_entry(entry.path().into_owned(), &metadata, &mut pending_dirs) 185 | } 186 | } 187 | } 188 | target_files 189 | } 190 | 191 | fn is_ignored_dir(&self, path: &Path) -> bool { 192 | path 193 | .file_name() 194 | .map(|dir_name| { 195 | let dir_name = dir_name.to_string_lossy().to_lowercase(); 196 | let is_ignored_file = match dir_name.as_str() { 197 | "node_modules" => self.ignore_node_modules, 198 | ".git" => self.ignore_git_folder, 199 | _ => false, 200 | }; 201 | is_ignored_file 202 | }) 203 | .unwrap_or(false) 204 | || self.is_vendor_folder(path) 205 | } 206 | 207 | fn is_vendor_folder(&self, path: &Path) -> bool { 208 | self 209 | .vendor_folder 210 | .as_ref() 211 | .map(|vendor_folder| path == *vendor_folder) 212 | .unwrap_or(false) 213 | } 214 | } 215 | 216 | #[cfg(test)] 217 | mod test { 218 | use std::path::PathBuf; 219 | 220 | use sys_traits::impls::RealSys; 221 | use tempfile::TempDir; 222 | 223 | use super::*; 224 | use crate::glob::FilePatterns; 225 | use crate::glob::PathOrPattern; 226 | use crate::glob::PathOrPatternSet; 227 | 228 | #[allow(clippy::disallowed_methods)] // allow fs methods 229 | #[test] 230 | fn test_collect_files() { 231 | fn create_files(dir_path: &PathBuf, files: &[&str]) { 232 | std::fs::create_dir_all(dir_path).unwrap(); 233 | for f in files { 234 | std::fs::write(dir_path.join(f), "").unwrap(); 235 | } 236 | } 237 | 238 | // dir.ts 239 | // ├── a.ts 240 | // ├── b.js 241 | // ├── child 242 | // | ├── git 243 | // | | └── git.js 244 | // | ├── node_modules 245 | // | | └── node_modules.js 246 | // | ├── vendor 247 | // | | └── vendor.js 248 | // │ ├── e.mjs 249 | // │ ├── f.mjsx 250 | // │ ├── .foo.TS 251 | // │ └── README.md 252 | // ├── c.tsx 253 | // ├── d.jsx 254 | // └── ignore 255 | // ├── g.d.ts 256 | // └── .gitignore 257 | 258 | let t = TempDir::new().unwrap(); 259 | 260 | let root_dir_path = t.path().join("dir.ts"); 261 | let root_dir_files = ["a.ts", "b.js", "c.tsx", "d.jsx"]; 262 | create_files(&root_dir_path, &root_dir_files); 263 | 264 | let child_dir_path = root_dir_path.join("child"); 265 | let child_dir_files = ["e.mjs", "f.mjsx", ".foo.TS", "README.md"]; 266 | create_files(&child_dir_path, &child_dir_files); 267 | 268 | std::fs::create_dir_all(t.path().join("dir.ts/child/node_modules")) 269 | .unwrap(); 270 | std::fs::write( 271 | t.path().join("dir.ts/child/node_modules/node_modules.js"), 272 | "", 273 | ) 274 | .unwrap(); 275 | std::fs::create_dir_all(t.path().join("dir.ts/child/.git")).unwrap(); 276 | std::fs::write(t.path().join("dir.ts/child/.git/git.js"), "").unwrap(); 277 | std::fs::create_dir_all(t.path().join("dir.ts/child/vendor")).unwrap(); 278 | std::fs::write(t.path().join("dir.ts/child/vendor/vendor.js"), "").unwrap(); 279 | 280 | let ignore_dir_path = root_dir_path.join("ignore"); 281 | let ignore_dir_files = ["g.d.ts", ".gitignore"]; 282 | create_files(&ignore_dir_path, &ignore_dir_files); 283 | 284 | let file_patterns = FilePatterns { 285 | base: root_dir_path.to_path_buf(), 286 | include: None, 287 | exclude: PathOrPatternSet::new(vec![PathOrPattern::Path( 288 | ignore_dir_path.to_path_buf(), 289 | )]), 290 | }; 291 | let file_collector = FileCollector::new(|e| { 292 | // exclude dotfiles 293 | e.path 294 | .file_name() 295 | .and_then(|f| f.to_str()) 296 | .map(|f| !f.starts_with('.')) 297 | .unwrap_or(false) 298 | }); 299 | 300 | let result = 301 | file_collector.collect_file_patterns(&RealSys, file_patterns.clone()); 302 | let expected = [ 303 | "README.md", 304 | "a.ts", 305 | "b.js", 306 | "c.tsx", 307 | "d.jsx", 308 | "e.mjs", 309 | "f.mjsx", 310 | "git.js", 311 | "node_modules.js", 312 | "vendor.js", 313 | ]; 314 | let mut file_names = result 315 | .into_iter() 316 | .map(|r| r.file_name().unwrap().to_string_lossy().to_string()) 317 | .collect::>(); 318 | file_names.sort(); 319 | assert_eq!(file_names, expected); 320 | 321 | // test ignoring the .git and node_modules folder 322 | let file_collector = file_collector 323 | .ignore_git_folder() 324 | .ignore_node_modules() 325 | .set_vendor_folder(Some(child_dir_path.join("vendor").to_path_buf())); 326 | let result = 327 | file_collector.collect_file_patterns(&RealSys, file_patterns.clone()); 328 | let expected = [ 329 | "README.md", 330 | "a.ts", 331 | "b.js", 332 | "c.tsx", 333 | "d.jsx", 334 | "e.mjs", 335 | "f.mjsx", 336 | ]; 337 | let mut file_names = result 338 | .into_iter() 339 | .map(|r| r.file_name().unwrap().to_string_lossy().to_string()) 340 | .collect::>(); 341 | file_names.sort(); 342 | assert_eq!(file_names, expected); 343 | 344 | // test opting out of ignoring by specifying the dir 345 | let file_patterns = FilePatterns { 346 | base: root_dir_path.to_path_buf(), 347 | include: Some(PathOrPatternSet::new(vec![ 348 | PathOrPattern::Path(root_dir_path.to_path_buf()), 349 | PathOrPattern::Path( 350 | root_dir_path.to_path_buf().join("child/node_modules/"), 351 | ), 352 | ])), 353 | exclude: PathOrPatternSet::new(vec![PathOrPattern::Path( 354 | ignore_dir_path.to_path_buf(), 355 | )]), 356 | }; 357 | let result = file_collector.collect_file_patterns(&RealSys, file_patterns); 358 | let expected = [ 359 | "README.md", 360 | "a.ts", 361 | "b.js", 362 | "c.tsx", 363 | "d.jsx", 364 | "e.mjs", 365 | "f.mjsx", 366 | "node_modules.js", 367 | ]; 368 | let mut file_names = result 369 | .into_iter() 370 | .map(|r| r.file_name().unwrap().to_string_lossy().to_string()) 371 | .collect::>(); 372 | file_names.sort(); 373 | assert_eq!(file_names, expected); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.4.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "2.6.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 25 | 26 | [[package]] 27 | name = "boxed_error" 28 | version = "0.2.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "17d4f95e880cfd28c4ca5a006cf7f6af52b4bcb7b5866f573b2faa126fb7affb" 31 | dependencies = [ 32 | "quote", 33 | "syn", 34 | ] 35 | 36 | [[package]] 37 | name = "bstr" 38 | version = "1.11.1" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" 41 | dependencies = [ 42 | "memchr", 43 | "serde", 44 | ] 45 | 46 | [[package]] 47 | name = "capacity_builder" 48 | version = "0.5.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "8f2d24a6dcf0cd402a21b65d35340f3a49ff3475dc5fdac91d22d2733e6641c6" 51 | dependencies = [ 52 | "capacity_builder_macros", 53 | "ecow", 54 | "hipstr", 55 | "itoa", 56 | ] 57 | 58 | [[package]] 59 | name = "capacity_builder_macros" 60 | version = "0.3.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "3b4a6cae9efc04cc6cbb8faf338d2c497c165c83e74509cf4dbedea948bbf6e5" 63 | dependencies = [ 64 | "quote", 65 | "syn", 66 | ] 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "1.0.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 73 | 74 | [[package]] 75 | name = "crossbeam-deque" 76 | version = "0.8.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 79 | dependencies = [ 80 | "crossbeam-epoch", 81 | "crossbeam-utils", 82 | ] 83 | 84 | [[package]] 85 | name = "crossbeam-epoch" 86 | version = "0.9.18" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 89 | dependencies = [ 90 | "crossbeam-utils", 91 | ] 92 | 93 | [[package]] 94 | name = "crossbeam-utils" 95 | version = "0.8.21" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 98 | 99 | [[package]] 100 | name = "deno_config" 101 | version = "0.57.0" 102 | dependencies = [ 103 | "boxed_error", 104 | "capacity_builder", 105 | "deno_error", 106 | "deno_package_json", 107 | "deno_path_util", 108 | "deno_semver", 109 | "glob", 110 | "ignore", 111 | "import_map", 112 | "indexmap", 113 | "jsonc-parser", 114 | "log", 115 | "percent-encoding", 116 | "phf", 117 | "pretty_assertions", 118 | "serde", 119 | "serde_json", 120 | "sys_traits", 121 | "tempfile", 122 | "thiserror", 123 | "url", 124 | ] 125 | 126 | [[package]] 127 | name = "deno_error" 128 | version = "0.6.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "43674d0eb32a7457e0911164fee92b45320e02a52c0c530e6cde47163cdf6dc8" 131 | dependencies = [ 132 | "deno_error_macro", 133 | "libc", 134 | "serde", 135 | "serde_json", 136 | "url", 137 | ] 138 | 139 | [[package]] 140 | name = "deno_error_macro" 141 | version = "0.6.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "06d727b287cd43aa5120cbd00d1ca5be5e829d502bbb52d4bbb144a5a057372e" 144 | dependencies = [ 145 | "proc-macro2", 146 | "quote", 147 | "syn", 148 | ] 149 | 150 | [[package]] 151 | name = "deno_package_json" 152 | version = "0.9.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "fc681ccdd5319c96bdacdd6d6b1a1e1272829fa10687120eba1d2a0c336d77f7" 155 | dependencies = [ 156 | "boxed_error", 157 | "deno_error", 158 | "deno_path_util", 159 | "deno_semver", 160 | "indexmap", 161 | "serde", 162 | "serde_json", 163 | "sys_traits", 164 | "thiserror", 165 | "url", 166 | ] 167 | 168 | [[package]] 169 | name = "deno_path_util" 170 | version = "0.4.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "516f813389095889776b81cc9108ff6f336fd9409b4b12fc0138aea23d2708e1" 173 | dependencies = [ 174 | "deno_error", 175 | "percent-encoding", 176 | "sys_traits", 177 | "thiserror", 178 | "url", 179 | ] 180 | 181 | [[package]] 182 | name = "deno_semver" 183 | version = "0.8.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "f2d807160e754edb1989b4a19cac1ac5299065a7a89ff98682a2366cbaa25795" 186 | dependencies = [ 187 | "capacity_builder", 188 | "deno_error", 189 | "ecow", 190 | "hipstr", 191 | "monch", 192 | "once_cell", 193 | "serde", 194 | "thiserror", 195 | "url", 196 | ] 197 | 198 | [[package]] 199 | name = "diff" 200 | version = "0.1.13" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 203 | 204 | [[package]] 205 | name = "displaydoc" 206 | version = "0.2.5" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 209 | dependencies = [ 210 | "proc-macro2", 211 | "quote", 212 | "syn", 213 | ] 214 | 215 | [[package]] 216 | name = "ecow" 217 | version = "0.2.3" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "e42fc0a93992b20c58b99e59d61eaf1635a25bfbe49e4275c34ba0aee98119ba" 220 | dependencies = [ 221 | "serde", 222 | ] 223 | 224 | [[package]] 225 | name = "equivalent" 226 | version = "1.0.1" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 229 | 230 | [[package]] 231 | name = "errno" 232 | version = "0.3.10" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 235 | dependencies = [ 236 | "libc", 237 | "windows-sys", 238 | ] 239 | 240 | [[package]] 241 | name = "fastrand" 242 | version = "2.3.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 245 | 246 | [[package]] 247 | name = "form_urlencoded" 248 | version = "1.2.1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 251 | dependencies = [ 252 | "percent-encoding", 253 | ] 254 | 255 | [[package]] 256 | name = "glob" 257 | version = "0.3.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 260 | 261 | [[package]] 262 | name = "globset" 263 | version = "0.4.15" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" 266 | dependencies = [ 267 | "aho-corasick", 268 | "bstr", 269 | "log", 270 | "regex-automata", 271 | "regex-syntax", 272 | ] 273 | 274 | [[package]] 275 | name = "hashbrown" 276 | version = "0.15.2" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 279 | 280 | [[package]] 281 | name = "hipstr" 282 | version = "0.6.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "97971ffc85d4c98de12e2608e992a43f5294ebb625fdb045b27c731b64c4c6d6" 285 | dependencies = [ 286 | "serde", 287 | "serde_bytes", 288 | "sptr", 289 | ] 290 | 291 | [[package]] 292 | name = "icu_collections" 293 | version = "1.5.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 296 | dependencies = [ 297 | "displaydoc", 298 | "yoke", 299 | "zerofrom", 300 | "zerovec", 301 | ] 302 | 303 | [[package]] 304 | name = "icu_locid" 305 | version = "1.5.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 308 | dependencies = [ 309 | "displaydoc", 310 | "litemap", 311 | "tinystr", 312 | "writeable", 313 | "zerovec", 314 | ] 315 | 316 | [[package]] 317 | name = "icu_locid_transform" 318 | version = "1.5.0" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 321 | dependencies = [ 322 | "displaydoc", 323 | "icu_locid", 324 | "icu_locid_transform_data", 325 | "icu_provider", 326 | "tinystr", 327 | "zerovec", 328 | ] 329 | 330 | [[package]] 331 | name = "icu_locid_transform_data" 332 | version = "1.5.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 335 | 336 | [[package]] 337 | name = "icu_normalizer" 338 | version = "1.5.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 341 | dependencies = [ 342 | "displaydoc", 343 | "icu_collections", 344 | "icu_normalizer_data", 345 | "icu_properties", 346 | "icu_provider", 347 | "smallvec", 348 | "utf16_iter", 349 | "utf8_iter", 350 | "write16", 351 | "zerovec", 352 | ] 353 | 354 | [[package]] 355 | name = "icu_normalizer_data" 356 | version = "1.5.0" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 359 | 360 | [[package]] 361 | name = "icu_properties" 362 | version = "1.5.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 365 | dependencies = [ 366 | "displaydoc", 367 | "icu_collections", 368 | "icu_locid_transform", 369 | "icu_properties_data", 370 | "icu_provider", 371 | "tinystr", 372 | "zerovec", 373 | ] 374 | 375 | [[package]] 376 | name = "icu_properties_data" 377 | version = "1.5.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 380 | 381 | [[package]] 382 | name = "icu_provider" 383 | version = "1.5.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 386 | dependencies = [ 387 | "displaydoc", 388 | "icu_locid", 389 | "icu_provider_macros", 390 | "stable_deref_trait", 391 | "tinystr", 392 | "writeable", 393 | "yoke", 394 | "zerofrom", 395 | "zerovec", 396 | ] 397 | 398 | [[package]] 399 | name = "icu_provider_macros" 400 | version = "1.5.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 403 | dependencies = [ 404 | "proc-macro2", 405 | "quote", 406 | "syn", 407 | ] 408 | 409 | [[package]] 410 | name = "idna" 411 | version = "1.0.3" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 414 | dependencies = [ 415 | "idna_adapter", 416 | "smallvec", 417 | "utf8_iter", 418 | ] 419 | 420 | [[package]] 421 | name = "idna_adapter" 422 | version = "1.2.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 425 | dependencies = [ 426 | "icu_normalizer", 427 | "icu_properties", 428 | ] 429 | 430 | [[package]] 431 | name = "ignore" 432 | version = "0.4.23" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 435 | dependencies = [ 436 | "crossbeam-deque", 437 | "globset", 438 | "log", 439 | "memchr", 440 | "regex-automata", 441 | "same-file", 442 | "walkdir", 443 | "winapi-util", 444 | ] 445 | 446 | [[package]] 447 | name = "import_map" 448 | version = "0.22.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "f315e535cb94a0e80704278d630990bb48834c8c8d976acf0a2f6bc8fede7c38" 451 | dependencies = [ 452 | "boxed_error", 453 | "deno_error", 454 | "indexmap", 455 | "log", 456 | "percent-encoding", 457 | "serde", 458 | "serde_json", 459 | "thiserror", 460 | "url", 461 | ] 462 | 463 | [[package]] 464 | name = "indexmap" 465 | version = "2.7.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 468 | dependencies = [ 469 | "equivalent", 470 | "hashbrown", 471 | "serde", 472 | ] 473 | 474 | [[package]] 475 | name = "itoa" 476 | version = "1.0.14" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 479 | 480 | [[package]] 481 | name = "jsonc-parser" 482 | version = "0.26.2" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "b558af6b49fd918e970471374e7a798b2c9bbcda624a210ffa3901ee5614bc8e" 485 | dependencies = [ 486 | "serde_json", 487 | ] 488 | 489 | [[package]] 490 | name = "libc" 491 | version = "0.2.168" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" 494 | 495 | [[package]] 496 | name = "linux-raw-sys" 497 | version = "0.4.14" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 500 | 501 | [[package]] 502 | name = "litemap" 503 | version = "0.7.4" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 506 | 507 | [[package]] 508 | name = "lock_api" 509 | version = "0.4.12" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 512 | dependencies = [ 513 | "autocfg", 514 | "scopeguard", 515 | ] 516 | 517 | [[package]] 518 | name = "log" 519 | version = "0.4.22" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 522 | 523 | [[package]] 524 | name = "memchr" 525 | version = "2.7.4" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 528 | 529 | [[package]] 530 | name = "monch" 531 | version = "0.5.0" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" 534 | 535 | [[package]] 536 | name = "once_cell" 537 | version = "1.20.2" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 540 | 541 | [[package]] 542 | name = "parking_lot" 543 | version = "0.12.3" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 546 | dependencies = [ 547 | "lock_api", 548 | "parking_lot_core", 549 | ] 550 | 551 | [[package]] 552 | name = "parking_lot_core" 553 | version = "0.9.10" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 556 | dependencies = [ 557 | "cfg-if", 558 | "libc", 559 | "redox_syscall", 560 | "smallvec", 561 | "windows-targets", 562 | ] 563 | 564 | [[package]] 565 | name = "percent-encoding" 566 | version = "2.3.1" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 569 | 570 | [[package]] 571 | name = "phf" 572 | version = "0.11.2" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 575 | dependencies = [ 576 | "phf_macros", 577 | "phf_shared", 578 | ] 579 | 580 | [[package]] 581 | name = "phf_generator" 582 | version = "0.11.2" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 585 | dependencies = [ 586 | "phf_shared", 587 | "rand", 588 | ] 589 | 590 | [[package]] 591 | name = "phf_macros" 592 | version = "0.11.2" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" 595 | dependencies = [ 596 | "phf_generator", 597 | "phf_shared", 598 | "proc-macro2", 599 | "quote", 600 | "syn", 601 | ] 602 | 603 | [[package]] 604 | name = "phf_shared" 605 | version = "0.11.2" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 608 | dependencies = [ 609 | "siphasher", 610 | ] 611 | 612 | [[package]] 613 | name = "pretty_assertions" 614 | version = "1.4.1" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 617 | dependencies = [ 618 | "diff", 619 | "yansi", 620 | ] 621 | 622 | [[package]] 623 | name = "proc-macro2" 624 | version = "1.0.92" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 627 | dependencies = [ 628 | "unicode-ident", 629 | ] 630 | 631 | [[package]] 632 | name = "quote" 633 | version = "1.0.37" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 636 | dependencies = [ 637 | "proc-macro2", 638 | ] 639 | 640 | [[package]] 641 | name = "rand" 642 | version = "0.8.5" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 645 | dependencies = [ 646 | "rand_core", 647 | ] 648 | 649 | [[package]] 650 | name = "rand_core" 651 | version = "0.6.4" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 654 | 655 | [[package]] 656 | name = "redox_syscall" 657 | version = "0.5.8" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 660 | dependencies = [ 661 | "bitflags", 662 | ] 663 | 664 | [[package]] 665 | name = "regex-automata" 666 | version = "0.4.9" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 669 | dependencies = [ 670 | "aho-corasick", 671 | "memchr", 672 | "regex-syntax", 673 | ] 674 | 675 | [[package]] 676 | name = "regex-syntax" 677 | version = "0.8.5" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 680 | 681 | [[package]] 682 | name = "rustix" 683 | version = "0.38.42" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 686 | dependencies = [ 687 | "bitflags", 688 | "errno", 689 | "libc", 690 | "linux-raw-sys", 691 | "windows-sys", 692 | ] 693 | 694 | [[package]] 695 | name = "ryu" 696 | version = "1.0.18" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 699 | 700 | [[package]] 701 | name = "same-file" 702 | version = "1.0.6" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 705 | dependencies = [ 706 | "winapi-util", 707 | ] 708 | 709 | [[package]] 710 | name = "scopeguard" 711 | version = "1.2.0" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 714 | 715 | [[package]] 716 | name = "serde" 717 | version = "1.0.216" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 720 | dependencies = [ 721 | "serde_derive", 722 | ] 723 | 724 | [[package]] 725 | name = "serde_bytes" 726 | version = "0.11.15" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" 729 | dependencies = [ 730 | "serde", 731 | ] 732 | 733 | [[package]] 734 | name = "serde_derive" 735 | version = "1.0.216" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 738 | dependencies = [ 739 | "proc-macro2", 740 | "quote", 741 | "syn", 742 | ] 743 | 744 | [[package]] 745 | name = "serde_json" 746 | version = "1.0.133" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 749 | dependencies = [ 750 | "indexmap", 751 | "itoa", 752 | "memchr", 753 | "ryu", 754 | "serde", 755 | ] 756 | 757 | [[package]] 758 | name = "siphasher" 759 | version = "0.3.11" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 762 | 763 | [[package]] 764 | name = "smallvec" 765 | version = "1.13.2" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 768 | 769 | [[package]] 770 | name = "sptr" 771 | version = "0.3.2" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" 774 | 775 | [[package]] 776 | name = "stable_deref_trait" 777 | version = "1.2.0" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 780 | 781 | [[package]] 782 | name = "syn" 783 | version = "2.0.90" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 786 | dependencies = [ 787 | "proc-macro2", 788 | "quote", 789 | "unicode-ident", 790 | ] 791 | 792 | [[package]] 793 | name = "synstructure" 794 | version = "0.13.1" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 797 | dependencies = [ 798 | "proc-macro2", 799 | "quote", 800 | "syn", 801 | ] 802 | 803 | [[package]] 804 | name = "sys_traits" 805 | version = "0.1.14" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "b0f8c2c55b6b4dd67f0f8df8de9bdf00b16c8ea4fbc4be0c2133d5d3924be5d4" 808 | dependencies = [ 809 | "parking_lot", 810 | "serde", 811 | "serde_json", 812 | "sys_traits_macros", 813 | ] 814 | 815 | [[package]] 816 | name = "sys_traits_macros" 817 | version = "0.1.0" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "181f22127402abcf8ee5c83ccd5b408933fec36a6095cf82cda545634692657e" 820 | dependencies = [ 821 | "proc-macro2", 822 | "quote", 823 | "syn", 824 | ] 825 | 826 | [[package]] 827 | name = "tempfile" 828 | version = "3.14.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 831 | dependencies = [ 832 | "cfg-if", 833 | "fastrand", 834 | "once_cell", 835 | "rustix", 836 | "windows-sys", 837 | ] 838 | 839 | [[package]] 840 | name = "thiserror" 841 | version = "2.0.8" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" 844 | dependencies = [ 845 | "thiserror-impl", 846 | ] 847 | 848 | [[package]] 849 | name = "thiserror-impl" 850 | version = "2.0.8" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" 853 | dependencies = [ 854 | "proc-macro2", 855 | "quote", 856 | "syn", 857 | ] 858 | 859 | [[package]] 860 | name = "tinystr" 861 | version = "0.7.6" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 864 | dependencies = [ 865 | "displaydoc", 866 | "zerovec", 867 | ] 868 | 869 | [[package]] 870 | name = "unicode-ident" 871 | version = "1.0.14" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 874 | 875 | [[package]] 876 | name = "url" 877 | version = "2.5.4" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 880 | dependencies = [ 881 | "form_urlencoded", 882 | "idna", 883 | "percent-encoding", 884 | "serde", 885 | ] 886 | 887 | [[package]] 888 | name = "utf16_iter" 889 | version = "1.0.5" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 892 | 893 | [[package]] 894 | name = "utf8_iter" 895 | version = "1.0.4" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 898 | 899 | [[package]] 900 | name = "walkdir" 901 | version = "2.5.0" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 904 | dependencies = [ 905 | "same-file", 906 | "winapi-util", 907 | ] 908 | 909 | [[package]] 910 | name = "winapi-util" 911 | version = "0.1.9" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 914 | dependencies = [ 915 | "windows-sys", 916 | ] 917 | 918 | [[package]] 919 | name = "windows-sys" 920 | version = "0.59.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 923 | dependencies = [ 924 | "windows-targets", 925 | ] 926 | 927 | [[package]] 928 | name = "windows-targets" 929 | version = "0.52.6" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 932 | dependencies = [ 933 | "windows_aarch64_gnullvm", 934 | "windows_aarch64_msvc", 935 | "windows_i686_gnu", 936 | "windows_i686_gnullvm", 937 | "windows_i686_msvc", 938 | "windows_x86_64_gnu", 939 | "windows_x86_64_gnullvm", 940 | "windows_x86_64_msvc", 941 | ] 942 | 943 | [[package]] 944 | name = "windows_aarch64_gnullvm" 945 | version = "0.52.6" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 948 | 949 | [[package]] 950 | name = "windows_aarch64_msvc" 951 | version = "0.52.6" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 954 | 955 | [[package]] 956 | name = "windows_i686_gnu" 957 | version = "0.52.6" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 960 | 961 | [[package]] 962 | name = "windows_i686_gnullvm" 963 | version = "0.52.6" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 966 | 967 | [[package]] 968 | name = "windows_i686_msvc" 969 | version = "0.52.6" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 972 | 973 | [[package]] 974 | name = "windows_x86_64_gnu" 975 | version = "0.52.6" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 978 | 979 | [[package]] 980 | name = "windows_x86_64_gnullvm" 981 | version = "0.52.6" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 984 | 985 | [[package]] 986 | name = "windows_x86_64_msvc" 987 | version = "0.52.6" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 990 | 991 | [[package]] 992 | name = "write16" 993 | version = "1.0.0" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 996 | 997 | [[package]] 998 | name = "writeable" 999 | version = "0.5.5" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1002 | 1003 | [[package]] 1004 | name = "yansi" 1005 | version = "1.0.1" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1008 | 1009 | [[package]] 1010 | name = "yoke" 1011 | version = "0.7.5" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1014 | dependencies = [ 1015 | "serde", 1016 | "stable_deref_trait", 1017 | "yoke-derive", 1018 | "zerofrom", 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "yoke-derive" 1023 | version = "0.7.5" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1026 | dependencies = [ 1027 | "proc-macro2", 1028 | "quote", 1029 | "syn", 1030 | "synstructure", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "zerofrom" 1035 | version = "0.1.5" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1038 | dependencies = [ 1039 | "zerofrom-derive", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "zerofrom-derive" 1044 | version = "0.1.5" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1047 | dependencies = [ 1048 | "proc-macro2", 1049 | "quote", 1050 | "syn", 1051 | "synstructure", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "zerovec" 1056 | version = "0.10.4" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1059 | dependencies = [ 1060 | "yoke", 1061 | "zerofrom", 1062 | "zerovec-derive", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "zerovec-derive" 1067 | version = "0.10.3" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1070 | dependencies = [ 1071 | "proc-macro2", 1072 | "quote", 1073 | "syn", 1074 | ] 1075 | -------------------------------------------------------------------------------- /src/workspace/discovery.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use std::borrow::Cow; 4 | use std::collections::BTreeMap; 5 | use std::collections::HashMap; 6 | use std::collections::HashSet; 7 | use std::path::Path; 8 | use std::path::PathBuf; 9 | 10 | use deno_package_json::PackageJson; 11 | use deno_package_json::PackageJsonLoadError; 12 | use deno_package_json::PackageJsonRc; 13 | use deno_path_util::url_from_directory_path; 14 | use deno_path_util::url_from_file_path; 15 | use deno_path_util::url_parent; 16 | use deno_path_util::url_to_file_path; 17 | use indexmap::IndexSet; 18 | use sys_traits::FsMetadata; 19 | use sys_traits::FsRead; 20 | use sys_traits::FsReadDir; 21 | use url::Url; 22 | 23 | use crate::deno_json::ConfigFile; 24 | use crate::deno_json::ConfigFileRc; 25 | use crate::glob::is_glob_pattern; 26 | use crate::glob::FileCollector; 27 | use crate::glob::FilePatterns; 28 | use crate::glob::PathOrPattern; 29 | use crate::glob::PathOrPatternSet; 30 | use crate::sync::new_rc; 31 | use crate::util::is_skippable_io_error; 32 | use crate::workspace::ConfigReadError; 33 | use crate::workspace::Workspace; 34 | 35 | use super::ResolveWorkspaceLinkError; 36 | use super::ResolveWorkspaceLinkErrorKind; 37 | use super::ResolveWorkspaceMemberError; 38 | use super::ResolveWorkspaceMemberErrorKind; 39 | use super::UrlRc; 40 | use super::VendorEnablement; 41 | use super::WorkspaceDiscoverError; 42 | use super::WorkspaceDiscoverErrorKind; 43 | use super::WorkspaceDiscoverOptions; 44 | use super::WorkspaceDiscoverStart; 45 | use super::WorkspaceRc; 46 | 47 | #[derive(Debug)] 48 | pub enum DenoOrPkgJson { 49 | Deno(ConfigFileRc), 50 | PkgJson(PackageJsonRc), 51 | } 52 | 53 | impl DenoOrPkgJson { 54 | pub fn specifier(&self) -> Cow { 55 | match self { 56 | Self::Deno(config) => Cow::Borrowed(&config.specifier), 57 | Self::PkgJson(pkg_json) => Cow::Owned(pkg_json.specifier()), 58 | } 59 | } 60 | } 61 | 62 | #[derive(Debug)] 63 | pub enum ConfigFolder { 64 | Single(DenoOrPkgJson), 65 | Both { 66 | deno_json: ConfigFileRc, 67 | pkg_json: PackageJsonRc, 68 | }, 69 | } 70 | 71 | impl ConfigFolder { 72 | pub fn folder_url(&self) -> Url { 73 | match self { 74 | Self::Single(DenoOrPkgJson::Deno(config)) => { 75 | url_parent(&config.specifier) 76 | } 77 | Self::Single(DenoOrPkgJson::PkgJson(pkg_json)) => { 78 | url_from_directory_path(pkg_json.path.parent().unwrap()).unwrap() 79 | } 80 | Self::Both { deno_json, .. } => url_parent(&deno_json.specifier), 81 | } 82 | } 83 | 84 | pub fn has_workspace_members(&self) -> bool { 85 | match self { 86 | Self::Single(DenoOrPkgJson::Deno(config)) => { 87 | config.json.workspace.is_some() 88 | } 89 | Self::Single(DenoOrPkgJson::PkgJson(pkg_json)) => { 90 | pkg_json.workspaces.is_some() 91 | } 92 | Self::Both { 93 | deno_json, 94 | pkg_json, 95 | } => deno_json.json.workspace.is_some() || pkg_json.workspaces.is_some(), 96 | } 97 | } 98 | 99 | pub fn deno_json(&self) -> Option<&ConfigFileRc> { 100 | match self { 101 | Self::Single(DenoOrPkgJson::Deno(deno_json)) => Some(deno_json), 102 | Self::Both { deno_json, .. } => Some(deno_json), 103 | _ => None, 104 | } 105 | } 106 | 107 | pub fn pkg_json(&self) -> Option<&PackageJsonRc> { 108 | match self { 109 | Self::Single(DenoOrPkgJson::PkgJson(pkg_json)) => Some(pkg_json), 110 | Self::Both { pkg_json, .. } => Some(pkg_json), 111 | _ => None, 112 | } 113 | } 114 | 115 | pub fn from_maybe_both( 116 | maybe_deno_json: Option, 117 | maybe_pkg_json: Option, 118 | ) -> Option { 119 | match (maybe_deno_json, maybe_pkg_json) { 120 | (Some(deno_json), Some(pkg_json)) => Some(Self::Both { 121 | deno_json, 122 | pkg_json, 123 | }), 124 | (Some(deno_json), None) => { 125 | Some(Self::Single(DenoOrPkgJson::Deno(deno_json))) 126 | } 127 | (None, Some(pkg_json)) => { 128 | Some(Self::Single(DenoOrPkgJson::PkgJson(pkg_json))) 129 | } 130 | (None, None) => None, 131 | } 132 | } 133 | } 134 | 135 | #[derive(Debug)] 136 | pub enum ConfigFileDiscovery { 137 | None { maybe_vendor_dir: Option }, 138 | Workspace { workspace: WorkspaceRc }, 139 | } 140 | 141 | impl ConfigFileDiscovery { 142 | fn root_config_specifier(&self) -> Option> { 143 | match self { 144 | Self::None { .. } => None, 145 | Self::Workspace { workspace, .. } => { 146 | let root_folder_configs = workspace.root_folder_configs(); 147 | if let Some(deno_json) = &root_folder_configs.deno_json { 148 | return Some(Cow::Borrowed(&deno_json.specifier)); 149 | } 150 | if let Some(pkg_json) = &root_folder_configs.pkg_json { 151 | return Some(Cow::Owned(pkg_json.specifier())); 152 | } 153 | None 154 | } 155 | } 156 | } 157 | } 158 | 159 | fn config_folder_config_specifier(res: &ConfigFolder) -> Cow { 160 | match res { 161 | ConfigFolder::Single(config) => config.specifier(), 162 | ConfigFolder::Both { deno_json, .. } => Cow::Borrowed(&deno_json.specifier), 163 | } 164 | } 165 | 166 | pub fn discover_workspace_config_files< 167 | TSys: FsRead + FsMetadata + FsReadDir, 168 | >( 169 | sys: &TSys, 170 | start: WorkspaceDiscoverStart, 171 | opts: &WorkspaceDiscoverOptions, 172 | ) -> Result { 173 | match start { 174 | WorkspaceDiscoverStart::Paths(dirs) => match dirs.len() { 175 | 0 => Ok(ConfigFileDiscovery::None { 176 | maybe_vendor_dir: resolve_vendor_dir( 177 | None, 178 | opts.maybe_vendor_override.as_ref(), 179 | ), 180 | }), 181 | 1 => { 182 | let dir = &dirs[0]; 183 | let start = DirOrConfigFile::Dir(dir); 184 | discover_workspace_config_files_for_single_dir(sys, start, opts, None) 185 | } 186 | _ => { 187 | let mut checked = HashSet::default(); 188 | let mut final_workspace = ConfigFileDiscovery::None { 189 | maybe_vendor_dir: resolve_vendor_dir( 190 | None, 191 | opts.maybe_vendor_override.as_ref(), 192 | ), 193 | }; 194 | for dir in dirs { 195 | let workspace = discover_workspace_config_files_for_single_dir( 196 | sys, 197 | DirOrConfigFile::Dir(dir), 198 | opts, 199 | Some(&mut checked), 200 | )?; 201 | if let Some(root_config_specifier) = workspace.root_config_specifier() 202 | { 203 | if let Some(final_workspace_config_specifier) = 204 | final_workspace.root_config_specifier() 205 | { 206 | return Err(WorkspaceDiscoverError( 207 | WorkspaceDiscoverErrorKind::MultipleWorkspaces { 208 | base_workspace_url: final_workspace_config_specifier 209 | .into_owned(), 210 | other_workspace_url: root_config_specifier.into_owned(), 211 | } 212 | .into(), 213 | )); 214 | } 215 | final_workspace = workspace; 216 | } 217 | } 218 | Ok(final_workspace) 219 | } 220 | }, 221 | WorkspaceDiscoverStart::ConfigFile(file) => { 222 | let start = DirOrConfigFile::ConfigFile(file); 223 | discover_workspace_config_files_for_single_dir(sys, start, opts, None) 224 | } 225 | } 226 | } 227 | 228 | #[derive(Debug, Clone, Copy)] 229 | enum DirOrConfigFile<'a> { 230 | Dir(&'a Path), 231 | ConfigFile(&'a Path), 232 | } 233 | 234 | fn discover_workspace_config_files_for_single_dir< 235 | TSys: FsRead + FsMetadata + FsReadDir, 236 | >( 237 | sys: &TSys, 238 | start: DirOrConfigFile, 239 | opts: &WorkspaceDiscoverOptions, 240 | mut checked: Option<&mut HashSet>, 241 | ) -> Result { 242 | fn strip_up_to_node_modules(path: &Path) -> PathBuf { 243 | path 244 | .components() 245 | .take_while(|component| match component { 246 | std::path::Component::Normal(name) => { 247 | name.to_string_lossy() != "node_modules" 248 | } 249 | _ => true, 250 | }) 251 | .collect() 252 | } 253 | 254 | if opts.workspace_cache.is_some() { 255 | // it doesn't really make sense to use a workspace cache without config 256 | // caches because that would mean the configs might change between calls 257 | // causing strange behavior, so panic if someone does this 258 | assert!( 259 | opts.deno_json_cache.is_some() && opts.pkg_json_cache.is_some(), 260 | "Using a workspace cache requires setting the deno.json and package.json caches" 261 | ); 262 | } 263 | 264 | let start_dir: Option<&Path>; 265 | let mut first_config_folder_url: Option = None; 266 | let mut found_config_folders: HashMap<_, ConfigFolder> = HashMap::new(); 267 | let config_file_names = 268 | ConfigFile::resolve_config_file_names(opts.additional_config_file_names); 269 | let load_pkg_json_in_folder = |folder_path: &Path| { 270 | if opts.discover_pkg_json { 271 | let pkg_json_path = folder_path.join("package.json"); 272 | match PackageJson::load_from_path( 273 | sys, 274 | opts.pkg_json_cache, 275 | &pkg_json_path, 276 | ) { 277 | Ok(pkg_json) => { 278 | log::debug!( 279 | "package.json file found at '{}'", 280 | pkg_json_path.display() 281 | ); 282 | Ok(Some(pkg_json)) 283 | } 284 | Err(PackageJsonLoadError::Io { source, .. }) 285 | if is_skippable_io_error(&source) => 286 | { 287 | Ok(None) 288 | } 289 | Err(err) => Err(err), 290 | } 291 | } else { 292 | Ok(None) 293 | } 294 | }; 295 | let load_config_folder = |folder_path: &Path| -> Result<_, ConfigReadError> { 296 | let maybe_config_file = ConfigFile::maybe_find_in_folder( 297 | sys, 298 | opts.deno_json_cache, 299 | folder_path, 300 | &config_file_names, 301 | )?; 302 | let maybe_pkg_json = load_pkg_json_in_folder(folder_path)?; 303 | Ok(ConfigFolder::from_maybe_both( 304 | maybe_config_file, 305 | maybe_pkg_json, 306 | )) 307 | }; 308 | match start { 309 | DirOrConfigFile::Dir(dir) => { 310 | start_dir = Some(dir); 311 | } 312 | DirOrConfigFile::ConfigFile(file) => { 313 | let specifier = url_from_file_path(file)?; 314 | let config_file = new_rc( 315 | ConfigFile::from_specifier(sys, specifier.clone()) 316 | .map_err(ConfigReadError::DenoJsonRead)?, 317 | ); 318 | 319 | // see what config would be loaded if we just specified the parent directory 320 | let natural_config_folder_result = 321 | load_config_folder(file.parent().unwrap()); 322 | let matching_config_folder = match natural_config_folder_result { 323 | Ok(Some(natual_config_folder)) => { 324 | if natual_config_folder 325 | .deno_json() 326 | .is_some_and(|d| d.specifier == config_file.specifier) 327 | { 328 | Some(natual_config_folder) 329 | } else { 330 | None 331 | } 332 | } 333 | Ok(None) | Err(_) => None, 334 | }; 335 | 336 | let parent_dir_url = url_parent(&config_file.specifier); 337 | let config_folder = match matching_config_folder { 338 | Some(config_folder) => config_folder, 339 | None => { 340 | // when loading the directory we would have loaded something else, so 341 | // don't try to load a workspace and don't store this information in 342 | // the workspace cache 343 | let config_folder = 344 | ConfigFolder::Single(DenoOrPkgJson::Deno(config_file)); 345 | 346 | if config_folder.has_workspace_members() { 347 | return handle_workspace_folder_with_members( 348 | sys, 349 | config_folder, 350 | Some(&parent_dir_url), 351 | opts, 352 | found_config_folders, 353 | &load_config_folder, 354 | ); 355 | } 356 | 357 | let maybe_vendor_dir = resolve_vendor_dir( 358 | config_folder.deno_json().map(|d| d.as_ref()), 359 | opts.maybe_vendor_override.as_ref(), 360 | ); 361 | let links = resolve_link_config_folders( 362 | sys, 363 | &config_folder, 364 | load_config_folder, 365 | )?; 366 | return Ok(ConfigFileDiscovery::Workspace { 367 | workspace: new_rc(Workspace::new( 368 | config_folder, 369 | Default::default(), 370 | links, 371 | maybe_vendor_dir, 372 | )), 373 | }); 374 | } 375 | }; 376 | 377 | if let Some(workspace_cache) = &opts.workspace_cache { 378 | if let Some(workspace) = workspace_cache.get(&config_file.dir_path()) { 379 | if cfg!(debug_assertions) { 380 | let expected_vendor_dir = resolve_vendor_dir( 381 | config_folder.deno_json().map(|d| d.as_ref()), 382 | opts.maybe_vendor_override.as_ref(), 383 | ); 384 | debug_assert_eq!( 385 | expected_vendor_dir, workspace.vendor_dir, 386 | "should not be using a different vendor dir across calls" 387 | ); 388 | } 389 | return Ok(ConfigFileDiscovery::Workspace { 390 | workspace: workspace.clone(), 391 | }); 392 | } 393 | } 394 | 395 | if config_folder.has_workspace_members() { 396 | return handle_workspace_folder_with_members( 397 | sys, 398 | config_folder, 399 | Some(&parent_dir_url), 400 | opts, 401 | found_config_folders, 402 | &load_config_folder, 403 | ); 404 | } 405 | 406 | found_config_folders.insert(parent_dir_url.clone(), config_folder); 407 | first_config_folder_url = Some(parent_dir_url); 408 | // start searching for a workspace in the parent directory 409 | start_dir = file.parent().and_then(|p| p.parent()); 410 | } 411 | } 412 | // do not auto-discover inside the node_modules folder (ex. when a 413 | // user is running something directly within there) 414 | let start_dir = start_dir.map(strip_up_to_node_modules); 415 | for current_dir in start_dir.iter().flat_map(|p| p.ancestors()) { 416 | if let Some(checked) = checked.as_mut() { 417 | if !checked.insert(current_dir.to_path_buf()) { 418 | // already visited here, so exit 419 | return Ok(ConfigFileDiscovery::None { 420 | maybe_vendor_dir: resolve_vendor_dir( 421 | None, 422 | opts.maybe_vendor_override.as_ref(), 423 | ), 424 | }); 425 | } 426 | } 427 | 428 | if let Some(workspace_with_members) = opts 429 | .workspace_cache 430 | .and_then(|c| c.get(current_dir)) 431 | .filter(|w| w.config_folders.len() > 1) 432 | { 433 | if cfg!(debug_assertions) { 434 | let expected_vendor_dir = resolve_vendor_dir( 435 | workspace_with_members.root_deno_json().map(|d| d.as_ref()), 436 | opts.maybe_vendor_override.as_ref(), 437 | ); 438 | debug_assert_eq!( 439 | expected_vendor_dir, workspace_with_members.vendor_dir, 440 | "should not be using a different vendor dir across calls" 441 | ); 442 | } 443 | 444 | return handle_workspace_with_members( 445 | sys, 446 | workspace_with_members, 447 | first_config_folder_url.as_ref(), 448 | found_config_folders, 449 | opts, 450 | load_config_folder, 451 | ); 452 | } 453 | 454 | let maybe_config_folder = load_config_folder(current_dir)?; 455 | let Some(root_config_folder) = maybe_config_folder else { 456 | continue; 457 | }; 458 | if root_config_folder.has_workspace_members() { 459 | return handle_workspace_folder_with_members( 460 | sys, 461 | root_config_folder, 462 | first_config_folder_url.as_ref(), 463 | opts, 464 | found_config_folders, 465 | &load_config_folder, 466 | ); 467 | } 468 | 469 | let config_folder_url = root_config_folder.folder_url(); 470 | if first_config_folder_url.is_none() { 471 | if let Some(workspace_cache) = &opts.workspace_cache { 472 | if let Some(workspace) = workspace_cache.get(current_dir) { 473 | if cfg!(debug_assertions) { 474 | let expected_vendor_dir = resolve_vendor_dir( 475 | root_config_folder.deno_json().map(|d| d.as_ref()), 476 | opts.maybe_vendor_override.as_ref(), 477 | ); 478 | debug_assert_eq!( 479 | expected_vendor_dir, workspace.vendor_dir, 480 | "should not be using a different vendor dir across calls" 481 | ); 482 | } 483 | return Ok(ConfigFileDiscovery::Workspace { 484 | workspace: workspace.clone(), 485 | }); 486 | } 487 | } 488 | 489 | first_config_folder_url = Some(config_folder_url.clone()); 490 | } 491 | found_config_folders.insert(config_folder_url, root_config_folder); 492 | } 493 | 494 | if let Some(first_config_folder_url) = first_config_folder_url { 495 | let config_folder = found_config_folders 496 | .remove(&first_config_folder_url) 497 | .unwrap(); 498 | let maybe_vendor_dir = resolve_vendor_dir( 499 | config_folder.deno_json().map(|d| d.as_ref()), 500 | opts.maybe_vendor_override.as_ref(), 501 | ); 502 | let link = 503 | resolve_link_config_folders(sys, &config_folder, load_config_folder)?; 504 | let workspace = new_rc(Workspace::new( 505 | config_folder, 506 | Default::default(), 507 | link, 508 | maybe_vendor_dir, 509 | )); 510 | if let Some(cache) = opts.workspace_cache { 511 | cache.set(workspace.root_dir_path(), workspace.clone()); 512 | } 513 | Ok(ConfigFileDiscovery::Workspace { workspace }) 514 | } else { 515 | Ok(ConfigFileDiscovery::None { 516 | maybe_vendor_dir: resolve_vendor_dir( 517 | None, 518 | opts.maybe_vendor_override.as_ref(), 519 | ), 520 | }) 521 | } 522 | } 523 | 524 | fn handle_workspace_folder_with_members< 525 | TSys: FsRead + FsMetadata + FsReadDir, 526 | >( 527 | sys: &TSys, 528 | root_config_folder: ConfigFolder, 529 | first_config_folder_url: Option<&Url>, 530 | opts: &WorkspaceDiscoverOptions<'_>, 531 | mut found_config_folders: HashMap, 532 | load_config_folder: &impl Fn( 533 | &Path, 534 | ) -> Result, ConfigReadError>, 535 | ) -> Result { 536 | let maybe_vendor_dir = resolve_vendor_dir( 537 | root_config_folder.deno_json().map(|d| d.as_ref()), 538 | opts.maybe_vendor_override.as_ref(), 539 | ); 540 | let raw_root_workspace = resolve_workspace_for_config_folder( 541 | sys, 542 | root_config_folder, 543 | maybe_vendor_dir, 544 | &mut found_config_folders, 545 | load_config_folder, 546 | )?; 547 | let links = resolve_link_config_folders( 548 | sys, 549 | &raw_root_workspace.root, 550 | load_config_folder, 551 | )?; 552 | let root_workspace = new_rc(Workspace::new( 553 | raw_root_workspace.root, 554 | raw_root_workspace.members, 555 | links, 556 | raw_root_workspace.vendor_dir, 557 | )); 558 | if let Some(cache) = opts.workspace_cache { 559 | cache.set(root_workspace.root_dir_path(), root_workspace.clone()); 560 | } 561 | handle_workspace_with_members( 562 | sys, 563 | root_workspace, 564 | first_config_folder_url, 565 | found_config_folders, 566 | opts, 567 | load_config_folder, 568 | ) 569 | } 570 | 571 | fn handle_workspace_with_members( 572 | sys: &TSys, 573 | root_workspace: WorkspaceRc, 574 | first_config_folder_url: Option<&Url>, 575 | mut found_config_folders: HashMap, 576 | opts: &WorkspaceDiscoverOptions, 577 | load_config_folder: impl Fn( 578 | &Path, 579 | ) -> Result, ConfigReadError>, 580 | ) -> Result { 581 | let is_root_deno_json_workspace = root_workspace 582 | .root_deno_json() 583 | .map(|d| d.json.workspace.is_some()) 584 | .unwrap_or(false); 585 | // if the root was an npm workspace that doesn't have the start config 586 | // as a member then only resolve the start config 587 | if !is_root_deno_json_workspace { 588 | if let Some(first_config_folder) = &first_config_folder_url { 589 | if !root_workspace 590 | .config_folders 591 | .contains_key(*first_config_folder) 592 | { 593 | if let Some(config_folder) = 594 | found_config_folders.remove(first_config_folder) 595 | { 596 | let maybe_vendor_dir = resolve_vendor_dir( 597 | config_folder.deno_json().map(|d| d.as_ref()), 598 | opts.maybe_vendor_override.as_ref(), 599 | ); 600 | let links = resolve_link_config_folders( 601 | sys, 602 | &config_folder, 603 | load_config_folder, 604 | )?; 605 | let workspace = new_rc(Workspace::new( 606 | config_folder, 607 | Default::default(), 608 | links, 609 | maybe_vendor_dir, 610 | )); 611 | if let Some(cache) = opts.workspace_cache { 612 | cache.set(workspace.root_dir_path(), workspace.clone()); 613 | } 614 | return Ok(ConfigFileDiscovery::Workspace { workspace }); 615 | } 616 | } 617 | } 618 | } 619 | 620 | if is_root_deno_json_workspace { 621 | for (key, config_folder) in &found_config_folders { 622 | if !root_workspace.config_folders.contains_key(key) { 623 | return Err( 624 | WorkspaceDiscoverErrorKind::ConfigNotWorkspaceMember { 625 | workspace_url: (**root_workspace.root_dir()).clone(), 626 | config_url: config_folder_config_specifier(config_folder) 627 | .into_owned(), 628 | } 629 | .into(), 630 | ); 631 | } 632 | } 633 | } 634 | 635 | // ensure no duplicate names in deno configuration files 636 | let mut seen_names: HashMap<&str, &Url> = 637 | HashMap::with_capacity(root_workspace.config_folders.len() + 1); 638 | for deno_json in root_workspace.deno_jsons() { 639 | if let Some(name) = deno_json.json.name.as_deref() { 640 | if let Some(other_member_url) = seen_names.get(name) { 641 | return Err( 642 | ResolveWorkspaceMemberErrorKind::DuplicatePackageName { 643 | name: name.to_string(), 644 | deno_json_url: deno_json.specifier.clone(), 645 | other_deno_json_url: (*other_member_url).clone(), 646 | } 647 | .into_box() 648 | .into(), 649 | ); 650 | } else { 651 | seen_names.insert(name, &deno_json.specifier); 652 | } 653 | } 654 | } 655 | 656 | Ok(ConfigFileDiscovery::Workspace { 657 | workspace: root_workspace, 658 | }) 659 | } 660 | 661 | struct RawResolvedWorkspace { 662 | root: ConfigFolder, 663 | members: BTreeMap, 664 | vendor_dir: Option, 665 | } 666 | 667 | fn resolve_workspace_for_config_folder< 668 | TSys: FsRead + FsMetadata + FsReadDir, 669 | >( 670 | sys: &TSys, 671 | root_config_folder: ConfigFolder, 672 | maybe_vendor_dir: Option, 673 | found_config_folders: &mut HashMap, 674 | load_config_folder: impl Fn( 675 | &Path, 676 | ) -> Result, ConfigReadError>, 677 | ) -> Result { 678 | let mut final_members = BTreeMap::new(); 679 | let root_config_file_directory_url = root_config_folder.folder_url(); 680 | let resolve_member_url = 681 | |raw_member: &str| -> Result { 682 | let member = ensure_trailing_slash(raw_member); 683 | let member_dir_url = root_config_file_directory_url 684 | .join(&member) 685 | .map_err(|err| { 686 | ResolveWorkspaceMemberErrorKind::InvalidMember { 687 | base: root_config_folder.folder_url(), 688 | member: raw_member.to_owned(), 689 | source: err, 690 | } 691 | .into_box() 692 | })?; 693 | Ok(member_dir_url) 694 | }; 695 | let validate_member_url_is_descendant = 696 | |member_dir_url: &Url| -> Result<(), ResolveWorkspaceMemberError> { 697 | if !member_dir_url 698 | .as_str() 699 | .starts_with(root_config_file_directory_url.as_str()) 700 | { 701 | return Err( 702 | ResolveWorkspaceMemberErrorKind::NonDescendant { 703 | workspace_url: root_config_file_directory_url.clone(), 704 | member_url: member_dir_url.clone(), 705 | } 706 | .into_box(), 707 | ); 708 | } 709 | Ok(()) 710 | }; 711 | let mut find_member_config_folder = 712 | |member_dir_url: &Url| -> Result<_, ResolveWorkspaceMemberError> { 713 | // try to find the config folder in memory from the configs we already 714 | // found on the file system 715 | if let Some(config_folder) = found_config_folders.remove(member_dir_url) { 716 | return Ok(config_folder); 717 | } 718 | 719 | let maybe_config_folder = 720 | load_config_folder(&url_to_file_path(member_dir_url)?)?; 721 | maybe_config_folder.ok_or_else(|| { 722 | // it's fine this doesn't use all the possible config file names 723 | // as this is only used to enhance the error message 724 | if member_dir_url.as_str().ends_with("/deno.json/") 725 | || member_dir_url.as_str().ends_with("/deno.jsonc/") 726 | || member_dir_url.as_str().ends_with("/package.json/") 727 | { 728 | ResolveWorkspaceMemberErrorKind::NotFoundMaybeSpecifiedFile { 729 | dir_url: member_dir_url.clone(), 730 | } 731 | .into_box() 732 | } else { 733 | ResolveWorkspaceMemberErrorKind::NotFound { 734 | dir_url: member_dir_url.clone(), 735 | } 736 | .into_box() 737 | } 738 | }) 739 | }; 740 | 741 | let collect_member_config_folders = 742 | |kind: &'static str, 743 | pattern_members: Vec<&String>, 744 | dir_path: &Path, 745 | config_file_names: &'static [&'static str]| 746 | -> Result, WorkspaceDiscoverErrorKind> { 747 | let patterns = pattern_members 748 | .iter() 749 | .flat_map(|raw_member| { 750 | config_file_names.iter().map(|config_file_name| { 751 | PathOrPattern::from_relative( 752 | dir_path, 753 | &format!( 754 | "{}{}", 755 | ensure_trailing_slash(raw_member), 756 | config_file_name 757 | ), 758 | ) 759 | .map_err(|err| { 760 | ResolveWorkspaceMemberErrorKind::MemberToPattern { 761 | kind, 762 | base: root_config_file_directory_url.clone(), 763 | member: raw_member.to_string(), 764 | source: err, 765 | } 766 | .into_box() 767 | }) 768 | }) 769 | }) 770 | .collect::, _>>()?; 771 | 772 | let paths = if patterns.is_empty() { 773 | Vec::new() 774 | } else { 775 | FileCollector::new(|_| true) 776 | .ignore_git_folder() 777 | .ignore_node_modules() 778 | .set_vendor_folder(maybe_vendor_dir.clone()) 779 | .collect_file_patterns( 780 | sys, 781 | FilePatterns { 782 | base: dir_path.to_path_buf(), 783 | include: Some(PathOrPatternSet::new(patterns)), 784 | exclude: PathOrPatternSet::new(Vec::new()), 785 | }, 786 | ) 787 | }; 788 | 789 | Ok(paths) 790 | }; 791 | 792 | if let Some(deno_json) = root_config_folder.deno_json() { 793 | if let Some(workspace_config) = deno_json.to_workspace_config()? { 794 | let (pattern_members, path_members): (Vec<_>, Vec<_>) = workspace_config 795 | .members 796 | .iter() 797 | .partition(|member| is_glob_pattern(member) || member.starts_with('!')); 798 | 799 | // Deno workspaces can discover wildcard members that use either `deno.json`, `deno.jsonc` or `package.json`. 800 | // But it only works for Deno workspaces, npm workspaces don't discover `deno.json(c)` files, otherwise 801 | // we'd be incompatible with npm workspaces if we discovered more files. 802 | let deno_json_paths = collect_member_config_folders( 803 | "Deno", 804 | pattern_members, 805 | &deno_json.dir_path(), 806 | &["deno.json", "deno.jsonc", "package.json"], 807 | )?; 808 | 809 | let mut member_dir_urls = 810 | IndexSet::with_capacity(path_members.len() + deno_json_paths.len()); 811 | for path_member in path_members { 812 | let member_dir_url = resolve_member_url(path_member)?; 813 | member_dir_urls.insert((path_member.clone(), member_dir_url)); 814 | } 815 | for deno_json_path in deno_json_paths { 816 | let member_dir_url = 817 | url_from_directory_path(deno_json_path.parent().unwrap()).unwrap(); 818 | member_dir_urls.insert(( 819 | deno_json_path 820 | .parent() 821 | .unwrap() 822 | .to_string_lossy() 823 | .to_string(), 824 | member_dir_url, 825 | )); 826 | } 827 | 828 | for (raw_member, member_dir_url) in member_dir_urls { 829 | if member_dir_url == root_config_file_directory_url { 830 | return Err( 831 | ResolveWorkspaceMemberErrorKind::InvalidSelfReference { 832 | member: raw_member.to_string(), 833 | } 834 | .into_box() 835 | .into(), 836 | ); 837 | } 838 | validate_member_url_is_descendant(&member_dir_url)?; 839 | let member_config_folder = find_member_config_folder(&member_dir_url)?; 840 | let previous_member = final_members 841 | .insert(new_rc(member_dir_url.clone()), member_config_folder); 842 | if previous_member.is_some() { 843 | return Err( 844 | ResolveWorkspaceMemberErrorKind::Duplicate { 845 | member: raw_member.to_string(), 846 | } 847 | .into_box() 848 | .into(), 849 | ); 850 | } 851 | } 852 | } 853 | } 854 | if let Some(pkg_json) = root_config_folder.pkg_json() { 855 | if let Some(members) = &pkg_json.workspaces { 856 | let (pattern_members, path_members): (Vec<_>, Vec<_>) = members 857 | .iter() 858 | .partition(|member| is_glob_pattern(member) || member.starts_with('!')); 859 | 860 | // npm workspaces can discover wildcard members `package.json` files, but not `deno.json(c)` files, otherwise 861 | // we'd be incompatible with npm workspaces if we discovered more files than just `package.json`. 862 | let pkg_json_paths = collect_member_config_folders( 863 | "npm", 864 | pattern_members, 865 | pkg_json.dir_path(), 866 | &["package.json"], 867 | )?; 868 | 869 | let mut member_dir_urls = 870 | IndexSet::with_capacity(path_members.len() + pkg_json_paths.len()); 871 | for path_member in path_members { 872 | let member_dir_url = resolve_member_url(path_member)?; 873 | member_dir_urls.insert(member_dir_url); 874 | } 875 | for pkg_json_path in pkg_json_paths { 876 | let member_dir_url = 877 | url_from_directory_path(pkg_json_path.parent().unwrap())?; 878 | member_dir_urls.insert(member_dir_url); 879 | } 880 | 881 | for member_dir_url in member_dir_urls { 882 | if member_dir_url == root_config_file_directory_url { 883 | continue; // ignore self references 884 | } 885 | validate_member_url_is_descendant(&member_dir_url)?; 886 | let member_config_folder = 887 | match find_member_config_folder(&member_dir_url) { 888 | Ok(config_folder) => config_folder, 889 | Err(err) => { 890 | return Err( 891 | match err.into_kind() { 892 | ResolveWorkspaceMemberErrorKind::NotFound { dir_url } => { 893 | // enhance the error to say we didn't find a package.json 894 | ResolveWorkspaceMemberErrorKind::NotFoundPackageJson { 895 | dir_url, 896 | } 897 | .into_box() 898 | } 899 | err => err.into_box(), 900 | } 901 | .into(), 902 | ); 903 | } 904 | }; 905 | if member_config_folder.pkg_json().is_none() { 906 | return Err( 907 | ResolveWorkspaceMemberErrorKind::NotFoundPackageJson { 908 | dir_url: member_dir_url, 909 | } 910 | .into_box() 911 | .into(), 912 | ); 913 | } 914 | // don't surface errors about duplicate members for 915 | // package.json workspace members 916 | final_members.insert(new_rc(member_dir_url), member_config_folder); 917 | } 918 | } 919 | } 920 | 921 | Ok(RawResolvedWorkspace { 922 | root: root_config_folder, 923 | members: final_members, 924 | vendor_dir: maybe_vendor_dir, 925 | }) 926 | } 927 | 928 | fn resolve_link_config_folders( 929 | sys: &TSys, 930 | root_config_folder: &ConfigFolder, 931 | load_config_folder: impl Fn( 932 | &Path, 933 | ) -> Result, ConfigReadError>, 934 | ) -> Result, WorkspaceDiscoverError> { 935 | let Some(workspace_deno_json) = root_config_folder.deno_json() else { 936 | return Ok(Default::default()); 937 | }; 938 | let Some(link_members) = workspace_deno_json.to_link_config()? else { 939 | return Ok(Default::default()); 940 | }; 941 | let root_config_file_directory_url = root_config_folder.folder_url(); 942 | let resolve_link_dir_url = 943 | |raw_link: &str| -> Result { 944 | let link = ensure_trailing_slash(raw_link); 945 | // support someone specifying an absolute path 946 | if !cfg!(windows) && link.starts_with('/') 947 | || cfg!(windows) && link.chars().any(|c| c == '\\') 948 | { 949 | if let Ok(value) = 950 | deno_path_util::url_from_file_path(&PathBuf::from(link.as_ref())) 951 | { 952 | return Ok(value); 953 | } 954 | } 955 | let link_dir_url = 956 | root_config_file_directory_url.join(&link).map_err(|err| { 957 | WorkspaceDiscoverErrorKind::ResolveLink { 958 | base: root_config_file_directory_url.clone(), 959 | link: raw_link.to_owned(), 960 | source: err.into(), 961 | } 962 | })?; 963 | Ok(link_dir_url) 964 | }; 965 | let mut final_config_folders = BTreeMap::new(); 966 | for raw_member in &link_members { 967 | let link_dir_url = resolve_link_dir_url(raw_member)?; 968 | let link_configs = resolve_link_member_config_folders( 969 | sys, 970 | &link_dir_url, 971 | &load_config_folder, 972 | ) 973 | .map_err(|err| WorkspaceDiscoverErrorKind::ResolveLink { 974 | base: root_config_file_directory_url.clone(), 975 | link: raw_member.to_string(), 976 | source: err, 977 | })?; 978 | 979 | for link_config_url in link_configs.keys() { 980 | if *link_config_url.as_ref() == root_config_file_directory_url { 981 | return Err(WorkspaceDiscoverError( 982 | WorkspaceDiscoverErrorKind::ResolveLink { 983 | base: root_config_file_directory_url.clone(), 984 | link: raw_member.to_string(), 985 | source: ResolveWorkspaceLinkErrorKind::WorkspaceMemberNotAllowed 986 | .into_box(), 987 | } 988 | .into(), 989 | )); 990 | } 991 | } 992 | 993 | final_config_folders.extend(link_configs); 994 | } 995 | 996 | Ok(final_config_folders) 997 | } 998 | 999 | fn resolve_link_member_config_folders( 1000 | sys: &TSys, 1001 | link_dir_url: &Url, 1002 | load_config_folder: impl Fn( 1003 | &Path, 1004 | ) -> Result, ConfigReadError>, 1005 | ) -> Result, ResolveWorkspaceLinkError> { 1006 | let link_dir_path = url_to_file_path(link_dir_url)?; 1007 | let maybe_config_folder = load_config_folder(&link_dir_path)?; 1008 | let Some(config_folder) = maybe_config_folder else { 1009 | return Err( 1010 | ResolveWorkspaceLinkErrorKind::NotFound { 1011 | dir_url: link_dir_url.clone(), 1012 | } 1013 | .into_box(), 1014 | ); 1015 | }; 1016 | if config_folder.has_workspace_members() { 1017 | let maybe_vendor_dir = 1018 | resolve_vendor_dir(config_folder.deno_json().map(|d| d.as_ref()), None); 1019 | let mut raw_workspace = resolve_workspace_for_config_folder( 1020 | sys, 1021 | config_folder, 1022 | maybe_vendor_dir, 1023 | &mut HashMap::new(), 1024 | &load_config_folder, 1025 | ) 1026 | .map_err(|err| ResolveWorkspaceLinkErrorKind::Workspace(Box::new(err)))?; 1027 | raw_workspace 1028 | .members 1029 | .insert(new_rc(raw_workspace.root.folder_url()), raw_workspace.root); 1030 | Ok(raw_workspace.members) 1031 | } else { 1032 | // attempt to find the root workspace directory 1033 | for ancestor in link_dir_path.ancestors().skip(1) { 1034 | let Ok(Some(config_folder)) = load_config_folder(ancestor) else { 1035 | continue; 1036 | }; 1037 | if config_folder.has_workspace_members() { 1038 | let maybe_vendor_dir = resolve_vendor_dir( 1039 | config_folder.deno_json().map(|d| d.as_ref()), 1040 | None, 1041 | ); 1042 | let Ok(mut raw_workspace) = resolve_workspace_for_config_folder( 1043 | sys, 1044 | config_folder, 1045 | maybe_vendor_dir, 1046 | &mut HashMap::new(), 1047 | &load_config_folder, 1048 | ) else { 1049 | continue; 1050 | }; 1051 | if raw_workspace.members.contains_key(link_dir_url) { 1052 | raw_workspace.members.insert( 1053 | new_rc(raw_workspace.root.folder_url()), 1054 | raw_workspace.root, 1055 | ); 1056 | return Ok(raw_workspace.members); 1057 | } 1058 | } 1059 | } 1060 | Ok(BTreeMap::from([( 1061 | new_rc(link_dir_url.clone()), 1062 | config_folder, 1063 | )])) 1064 | } 1065 | } 1066 | 1067 | fn resolve_vendor_dir( 1068 | maybe_deno_json: Option<&ConfigFile>, 1069 | maybe_vendor_override: Option<&VendorEnablement>, 1070 | ) -> Option { 1071 | if let Some(vendor_folder_override) = maybe_vendor_override { 1072 | match vendor_folder_override { 1073 | VendorEnablement::Disable => None, 1074 | VendorEnablement::Enable { cwd } => match maybe_deno_json { 1075 | Some(c) => Some(c.dir_path().join("vendor")), 1076 | None => Some(cwd.join("vendor")), 1077 | }, 1078 | } 1079 | } else { 1080 | let deno_json = maybe_deno_json?; 1081 | if deno_json.vendor() == Some(true) { 1082 | Some(deno_json.dir_path().join("vendor")) 1083 | } else { 1084 | None 1085 | } 1086 | } 1087 | } 1088 | 1089 | fn ensure_trailing_slash(path: &str) -> Cow { 1090 | if !path.ends_with('/') { 1091 | Cow::Owned(format!("{}/", path)) 1092 | } else { 1093 | Cow::Borrowed(path) 1094 | } 1095 | } 1096 | -------------------------------------------------------------------------------- /src/glob/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | use deno_error::JsError; 4 | use deno_path_util::normalize_path; 5 | use deno_path_util::url_to_file_path; 6 | use indexmap::IndexMap; 7 | use std::borrow::Cow; 8 | use std::path::Path; 9 | use std::path::PathBuf; 10 | use thiserror::Error; 11 | use url::Url; 12 | 13 | use crate::UrlToFilePathError; 14 | 15 | mod collector; 16 | mod gitignore; 17 | 18 | pub use collector::FileCollector; 19 | pub use collector::WalkEntry; 20 | 21 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 22 | pub enum FilePatternsMatch { 23 | /// File passes as matching, but further exclude matching (ex. .gitignore) 24 | /// may be necessary. 25 | Passed, 26 | /// File passes matching and further exclude matching (ex. .gitignore) 27 | /// should NOT be done. 28 | PassedOptedOutExclude, 29 | /// File was excluded. 30 | Excluded, 31 | } 32 | 33 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 34 | pub enum PathKind { 35 | File, 36 | Directory, 37 | } 38 | 39 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 40 | pub struct FilePatterns { 41 | /// Default traversal base used when calling `split_by_base()` without 42 | /// any `include` patterns. 43 | pub base: PathBuf, 44 | pub include: Option, 45 | pub exclude: PathOrPatternSet, 46 | } 47 | 48 | impl FilePatterns { 49 | pub fn new_with_base(base: PathBuf) -> Self { 50 | Self { 51 | base, 52 | include: Default::default(), 53 | exclude: Default::default(), 54 | } 55 | } 56 | 57 | pub fn with_new_base(self, new_base: PathBuf) -> Self { 58 | Self { 59 | base: new_base, 60 | ..self 61 | } 62 | } 63 | 64 | pub fn matches_specifier(&self, specifier: &Url) -> bool { 65 | self.matches_specifier_detail(specifier) != FilePatternsMatch::Excluded 66 | } 67 | 68 | pub fn matches_specifier_detail(&self, specifier: &Url) -> FilePatternsMatch { 69 | if specifier.scheme() != "file" { 70 | // can't do .gitignore on a non-file specifier 71 | return FilePatternsMatch::PassedOptedOutExclude; 72 | } 73 | let path = match url_to_file_path(specifier) { 74 | Ok(path) => path, 75 | Err(_) => return FilePatternsMatch::PassedOptedOutExclude, 76 | }; 77 | self.matches_path_detail(&path, PathKind::File) // use file matching behavior 78 | } 79 | 80 | pub fn matches_path(&self, path: &Path, path_kind: PathKind) -> bool { 81 | self.matches_path_detail(path, path_kind) != FilePatternsMatch::Excluded 82 | } 83 | 84 | pub fn matches_path_detail( 85 | &self, 86 | path: &Path, 87 | path_kind: PathKind, 88 | ) -> FilePatternsMatch { 89 | // if there's an include list, only include files that match it 90 | // the include list is a closed set 91 | if let Some(include) = &self.include { 92 | match path_kind { 93 | PathKind::File => { 94 | if include.matches_path_detail(path) != PathOrPatternsMatch::Matched { 95 | return FilePatternsMatch::Excluded; 96 | } 97 | } 98 | PathKind::Directory => { 99 | // for now ignore the include list unless there's a negated 100 | // glob for the directory 101 | for p in include.0.iter().rev() { 102 | match p.matches_path(path) { 103 | PathGlobMatch::Matched => { 104 | break; 105 | } 106 | PathGlobMatch::MatchedNegated => { 107 | return FilePatternsMatch::Excluded 108 | } 109 | PathGlobMatch::NotMatched => { 110 | // keep going 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | // the exclude list is an open set and we skip files not in the exclude list 119 | match self.exclude.matches_path_detail(path) { 120 | PathOrPatternsMatch::Matched => FilePatternsMatch::Excluded, 121 | PathOrPatternsMatch::NotMatched => FilePatternsMatch::Passed, 122 | PathOrPatternsMatch::Excluded => FilePatternsMatch::PassedOptedOutExclude, 123 | } 124 | } 125 | 126 | /// Creates a collection of `FilePatterns` where the containing patterns 127 | /// are only the ones applicable to the base. 128 | /// 129 | /// The order these are returned in is the order that the directory traversal 130 | /// should occur in. 131 | pub fn split_by_base(&self) -> Vec { 132 | let negated_excludes = self 133 | .exclude 134 | .0 135 | .iter() 136 | .filter(|e| e.is_negated()) 137 | .collect::>(); 138 | let include = match &self.include { 139 | Some(include) => Cow::Borrowed(include), 140 | None => { 141 | if negated_excludes.is_empty() { 142 | return vec![self.clone()]; 143 | } else { 144 | Cow::Owned(PathOrPatternSet::new(vec![PathOrPattern::Path( 145 | self.base.clone(), 146 | )])) 147 | } 148 | } 149 | }; 150 | 151 | let mut include_paths = Vec::with_capacity(include.0.len()); 152 | let mut include_patterns = Vec::with_capacity(include.0.len()); 153 | let mut exclude_patterns = 154 | Vec::with_capacity(include.0.len() + self.exclude.0.len()); 155 | 156 | for path_or_pattern in &include.0 { 157 | match path_or_pattern { 158 | PathOrPattern::Path(path) => include_paths.push(path), 159 | PathOrPattern::NegatedPath(path) => { 160 | exclude_patterns.push(PathOrPattern::Path(path.clone())); 161 | } 162 | PathOrPattern::Pattern(pattern) => { 163 | if pattern.is_negated() { 164 | exclude_patterns.push(PathOrPattern::Pattern(pattern.as_negated())); 165 | } else { 166 | include_patterns.push(pattern.clone()); 167 | } 168 | } 169 | PathOrPattern::RemoteUrl(_) => {} 170 | } 171 | } 172 | 173 | let capacity = include_patterns.len() + negated_excludes.len(); 174 | let mut include_patterns_by_base_path = include_patterns.into_iter().fold( 175 | IndexMap::with_capacity(capacity), 176 | |mut map: IndexMap<_, Vec<_>>, p| { 177 | map.entry(p.base_path()).or_default().push(p); 178 | map 179 | }, 180 | ); 181 | for p in &negated_excludes { 182 | if let Some(base_path) = p.base_path() { 183 | if !include_patterns_by_base_path.contains_key(&base_path) { 184 | let has_any_base_parent = include_patterns_by_base_path 185 | .keys() 186 | .any(|k| base_path.starts_with(k)) 187 | || include_paths.iter().any(|p| base_path.starts_with(p)); 188 | // don't include an orphaned negated pattern 189 | if has_any_base_parent { 190 | include_patterns_by_base_path.insert(base_path, Vec::new()); 191 | } 192 | } 193 | } 194 | } 195 | 196 | let exclude_by_base_path = exclude_patterns 197 | .iter() 198 | .chain(self.exclude.0.iter()) 199 | .filter_map(|s| Some((s.base_path()?, s))) 200 | .collect::>(); 201 | let get_applicable_excludes = |base_path: &PathBuf| -> Vec { 202 | exclude_by_base_path 203 | .iter() 204 | .filter_map(|(exclude_base_path, exclude)| { 205 | match exclude { 206 | PathOrPattern::RemoteUrl(_) => None, 207 | PathOrPattern::Path(exclude_path) 208 | | PathOrPattern::NegatedPath(exclude_path) => { 209 | // include paths that's are sub paths or an ancestor path 210 | if base_path.starts_with(exclude_path) 211 | || exclude_path.starts_with(base_path) 212 | { 213 | Some((*exclude).clone()) 214 | } else { 215 | None 216 | } 217 | } 218 | PathOrPattern::Pattern(_) => { 219 | // include globs that's are sub paths or an ancestor path 220 | if exclude_base_path.starts_with(base_path) 221 | || base_path.starts_with(exclude_base_path) 222 | { 223 | Some((*exclude).clone()) 224 | } else { 225 | None 226 | } 227 | } 228 | } 229 | }) 230 | .collect::>() 231 | }; 232 | 233 | let mut result = Vec::with_capacity( 234 | include_paths.len() + include_patterns_by_base_path.len(), 235 | ); 236 | for path in include_paths { 237 | let applicable_excludes = get_applicable_excludes(path); 238 | result.push(Self { 239 | base: path.clone(), 240 | include: if self.include.is_none() { 241 | None 242 | } else { 243 | Some(PathOrPatternSet::new(vec![PathOrPattern::Path( 244 | path.clone(), 245 | )])) 246 | }, 247 | exclude: PathOrPatternSet::new(applicable_excludes), 248 | }); 249 | } 250 | 251 | // todo(dsherret): This could be further optimized by not including 252 | // patterns that will only ever match another base. 253 | for base_path in include_patterns_by_base_path.keys() { 254 | let applicable_excludes = get_applicable_excludes(base_path); 255 | let mut applicable_includes = Vec::new(); 256 | // get all patterns that apply to the current or ancestor directories 257 | for path in base_path.ancestors() { 258 | if let Some(patterns) = include_patterns_by_base_path.get(path) { 259 | applicable_includes.extend( 260 | patterns 261 | .iter() 262 | .map(|p| PathOrPattern::Pattern((*p).clone())), 263 | ); 264 | } 265 | } 266 | result.push(Self { 267 | base: base_path.clone(), 268 | include: if self.include.is_none() 269 | || applicable_includes.is_empty() 270 | && self 271 | .include 272 | .as_ref() 273 | .map(|i| !i.0.is_empty()) 274 | .unwrap_or(false) 275 | { 276 | None 277 | } else { 278 | Some(PathOrPatternSet::new(applicable_includes)) 279 | }, 280 | exclude: PathOrPatternSet::new(applicable_excludes), 281 | }); 282 | } 283 | 284 | // Sort by the longest base path first. This ensures that we visit opted into 285 | // nested directories first before visiting the parent directory. The directory 286 | // traverser will handle not going into directories it's already been in. 287 | result.sort_by(|a, b| { 288 | // try looking at the parents first so that files in the same 289 | // folder are kept in the same order that they're provided 290 | let (a, b) = 291 | if let (Some(a), Some(b)) = (a.base.parent(), b.base.parent()) { 292 | (a, b) 293 | } else { 294 | (a.base.as_path(), b.base.as_path()) 295 | }; 296 | b.as_os_str().len().cmp(&a.as_os_str().len()) 297 | }); 298 | 299 | result 300 | } 301 | } 302 | 303 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] 304 | pub enum PathOrPatternsMatch { 305 | Matched, 306 | NotMatched, 307 | Excluded, 308 | } 309 | 310 | #[derive(Debug, Error, JsError)] 311 | pub enum FromExcludeRelativePathOrPatternsError { 312 | #[class(type)] 313 | #[error("The negation of '{negated_entry}' is never reached due to the higher priority '{entry}' exclude. Move '{negated_entry}' after '{entry}'.")] 314 | HigherPriorityExclude { 315 | negated_entry: String, 316 | entry: String, 317 | }, 318 | #[class(inherit)] 319 | #[error("{0}")] 320 | PathOrPatternParse(#[from] PathOrPatternParseError), 321 | } 322 | 323 | #[derive(Clone, Default, Debug, Hash, Eq, PartialEq)] 324 | pub struct PathOrPatternSet(Vec); 325 | 326 | impl PathOrPatternSet { 327 | pub fn new(elements: Vec) -> Self { 328 | Self(elements) 329 | } 330 | 331 | pub fn from_absolute_paths( 332 | paths: &[String], 333 | ) -> Result { 334 | Ok(Self( 335 | paths 336 | .iter() 337 | .map(|p| PathOrPattern::new(p)) 338 | .collect::, _>>()?, 339 | )) 340 | } 341 | 342 | /// Builds the set of path and patterns for an "include" list. 343 | pub fn from_include_relative_path_or_patterns( 344 | base: &Path, 345 | entries: &[String], 346 | ) -> Result { 347 | Ok(Self( 348 | entries 349 | .iter() 350 | .map(|p| PathOrPattern::from_relative(base, p)) 351 | .collect::, _>>()?, 352 | )) 353 | } 354 | 355 | /// Builds the set and ensures no negations are overruled by 356 | /// higher priority entries. 357 | pub fn from_exclude_relative_path_or_patterns( 358 | base: &Path, 359 | entries: &[String], 360 | ) -> Result { 361 | // error when someone does something like: 362 | // exclude: ["!./a/b", "./a"] as it should be the opposite 363 | fn validate_entry( 364 | found_negated_paths: &Vec<(&str, PathBuf)>, 365 | entry: &str, 366 | entry_path: &Path, 367 | ) -> Result<(), FromExcludeRelativePathOrPatternsError> { 368 | for (negated_entry, negated_path) in found_negated_paths { 369 | if negated_path.starts_with(entry_path) { 370 | return Err( 371 | FromExcludeRelativePathOrPatternsError::HigherPriorityExclude { 372 | negated_entry: negated_entry.to_string(), 373 | entry: entry.to_string(), 374 | }, 375 | ); 376 | } 377 | } 378 | Ok(()) 379 | } 380 | 381 | let mut found_negated_paths: Vec<(&str, PathBuf)> = 382 | Vec::with_capacity(entries.len()); 383 | let mut result = Vec::with_capacity(entries.len()); 384 | for entry in entries { 385 | let p = PathOrPattern::from_relative(base, entry)?; 386 | match &p { 387 | PathOrPattern::Path(p) => { 388 | validate_entry(&found_negated_paths, entry, p)?; 389 | } 390 | PathOrPattern::NegatedPath(p) => { 391 | found_negated_paths.push((entry.as_str(), p.clone())); 392 | } 393 | PathOrPattern::RemoteUrl(_) => { 394 | // ignore 395 | } 396 | PathOrPattern::Pattern(p) => { 397 | if p.is_negated() { 398 | let base_path = p.base_path(); 399 | found_negated_paths.push((entry.as_str(), base_path)); 400 | } 401 | } 402 | } 403 | result.push(p); 404 | } 405 | Ok(Self(result)) 406 | } 407 | 408 | pub fn inner(&self) -> &Vec { 409 | &self.0 410 | } 411 | 412 | pub fn inner_mut(&mut self) -> &mut Vec { 413 | &mut self.0 414 | } 415 | 416 | pub fn into_path_or_patterns(self) -> Vec { 417 | self.0 418 | } 419 | 420 | pub fn matches_path(&self, path: &Path) -> bool { 421 | self.matches_path_detail(path) == PathOrPatternsMatch::Matched 422 | } 423 | 424 | pub fn matches_path_detail(&self, path: &Path) -> PathOrPatternsMatch { 425 | for p in self.0.iter().rev() { 426 | match p.matches_path(path) { 427 | PathGlobMatch::Matched => return PathOrPatternsMatch::Matched, 428 | PathGlobMatch::MatchedNegated => return PathOrPatternsMatch::Excluded, 429 | PathGlobMatch::NotMatched => { 430 | // ignore 431 | } 432 | } 433 | } 434 | PathOrPatternsMatch::NotMatched 435 | } 436 | 437 | pub fn base_paths(&self) -> Vec { 438 | let mut result = Vec::with_capacity(self.0.len()); 439 | for element in &self.0 { 440 | match element { 441 | PathOrPattern::Path(path) | PathOrPattern::NegatedPath(path) => { 442 | result.push(path.to_path_buf()); 443 | } 444 | PathOrPattern::RemoteUrl(_) => { 445 | // ignore 446 | } 447 | PathOrPattern::Pattern(pattern) => { 448 | result.push(pattern.base_path()); 449 | } 450 | } 451 | } 452 | result 453 | } 454 | 455 | pub fn push(&mut self, item: PathOrPattern) { 456 | self.0.push(item); 457 | } 458 | 459 | pub fn append(&mut self, items: impl Iterator) { 460 | self.0.extend(items) 461 | } 462 | } 463 | 464 | #[derive(Debug, Error, JsError, Clone)] 465 | #[class(inherit)] 466 | #[error("Invalid URL '{}'", url)] 467 | pub struct UrlParseError { 468 | url: String, 469 | #[source] 470 | #[inherit] 471 | source: url::ParseError, 472 | } 473 | 474 | #[derive(Debug, Error, JsError)] 475 | pub enum PathOrPatternParseError { 476 | #[class(inherit)] 477 | #[error(transparent)] 478 | UrlParse(#[from] UrlParseError), 479 | #[class(inherit)] 480 | #[error(transparent)] 481 | UrlToFilePathError(#[from] UrlToFilePathError), 482 | #[class(inherit)] 483 | #[error(transparent)] 484 | GlobParse(#[from] GlobPatternParseError), 485 | } 486 | 487 | #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] 488 | pub enum PathOrPattern { 489 | Path(PathBuf), 490 | NegatedPath(PathBuf), 491 | RemoteUrl(Url), 492 | Pattern(GlobPattern), 493 | } 494 | 495 | impl PathOrPattern { 496 | pub fn new(path: &str) -> Result { 497 | if has_url_prefix(path) { 498 | let url = Url::parse(path).map_err(|err| UrlParseError { 499 | url: path.to_string(), 500 | source: err, 501 | })?; 502 | if url.scheme() == "file" { 503 | let path = url_to_file_path(&url)?; 504 | return Ok(Self::Path(path)); 505 | } else { 506 | return Ok(Self::RemoteUrl(url)); 507 | } 508 | } 509 | 510 | GlobPattern::new_if_pattern(path) 511 | .map(|maybe_pattern| { 512 | maybe_pattern 513 | .map(PathOrPattern::Pattern) 514 | .unwrap_or_else(|| PathOrPattern::Path(normalize_path(path))) 515 | }) 516 | .map_err(|err| err.into()) 517 | } 518 | 519 | pub fn from_relative( 520 | base: &Path, 521 | p: &str, 522 | ) -> Result { 523 | if is_glob_pattern(p) { 524 | GlobPattern::from_relative(base, p) 525 | .map(PathOrPattern::Pattern) 526 | .map_err(|err| err.into()) 527 | } else if has_url_prefix(p) { 528 | PathOrPattern::new(p) 529 | } else if let Some(path) = p.strip_prefix('!') { 530 | Ok(PathOrPattern::NegatedPath(normalize_path(base.join(path)))) 531 | } else { 532 | Ok(PathOrPattern::Path(normalize_path(base.join(p)))) 533 | } 534 | } 535 | 536 | pub fn matches_path(&self, path: &Path) -> PathGlobMatch { 537 | match self { 538 | PathOrPattern::Path(p) => { 539 | if path.starts_with(p) { 540 | PathGlobMatch::Matched 541 | } else { 542 | PathGlobMatch::NotMatched 543 | } 544 | } 545 | PathOrPattern::NegatedPath(p) => { 546 | if path.starts_with(p) { 547 | PathGlobMatch::MatchedNegated 548 | } else { 549 | PathGlobMatch::NotMatched 550 | } 551 | } 552 | PathOrPattern::RemoteUrl(_) => PathGlobMatch::NotMatched, 553 | PathOrPattern::Pattern(p) => p.matches_path(path), 554 | } 555 | } 556 | 557 | /// Returns the base path of the pattern if it's not a remote url pattern. 558 | pub fn base_path(&self) -> Option { 559 | match self { 560 | PathOrPattern::Path(p) | PathOrPattern::NegatedPath(p) => Some(p.clone()), 561 | PathOrPattern::RemoteUrl(_) => None, 562 | PathOrPattern::Pattern(p) => Some(p.base_path()), 563 | } 564 | } 565 | 566 | /// If this is a negated pattern. 567 | pub fn is_negated(&self) -> bool { 568 | match self { 569 | PathOrPattern::Path(_) => false, 570 | PathOrPattern::NegatedPath(_) => true, 571 | PathOrPattern::RemoteUrl(_) => false, 572 | PathOrPattern::Pattern(p) => p.is_negated(), 573 | } 574 | } 575 | } 576 | 577 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 578 | pub enum PathGlobMatch { 579 | Matched, 580 | MatchedNegated, 581 | NotMatched, 582 | } 583 | 584 | #[derive(Debug, Error, JsError)] 585 | #[class(type)] 586 | #[error("Failed to expand glob: \"{pattern}\"")] 587 | pub struct GlobPatternParseError { 588 | pattern: String, 589 | #[source] 590 | source: glob::PatternError, 591 | } 592 | 593 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 594 | pub struct GlobPattern { 595 | is_negated: bool, 596 | pattern: glob::Pattern, 597 | } 598 | 599 | impl GlobPattern { 600 | pub fn new_if_pattern( 601 | pattern: &str, 602 | ) -> Result, GlobPatternParseError> { 603 | if !is_glob_pattern(pattern) { 604 | return Ok(None); 605 | } 606 | Self::new(pattern).map(Some) 607 | } 608 | 609 | pub fn new(pattern: &str) -> Result { 610 | let (is_negated, pattern) = match pattern.strip_prefix('!') { 611 | Some(pattern) => (true, pattern), 612 | None => (false, pattern), 613 | }; 614 | let pattern = escape_brackets(pattern).replace('\\', "/"); 615 | let pattern = 616 | glob::Pattern::new(&pattern).map_err(|source| GlobPatternParseError { 617 | pattern: pattern.to_string(), 618 | source, 619 | })?; 620 | Ok(Self { 621 | is_negated, 622 | pattern, 623 | }) 624 | } 625 | 626 | pub fn from_relative( 627 | base: &Path, 628 | p: &str, 629 | ) -> Result { 630 | let (is_negated, p) = match p.strip_prefix('!') { 631 | Some(p) => (true, p), 632 | None => (false, p), 633 | }; 634 | let base_str = base.to_string_lossy().replace('\\', "/"); 635 | let p = p.strip_prefix("./").unwrap_or(p); 636 | let p = p.strip_suffix('/').unwrap_or(p); 637 | let pattern = capacity_builder::StringBuilder::::build(|builder| { 638 | if is_negated { 639 | builder.append('!'); 640 | } 641 | builder.append(&base_str); 642 | if !base_str.ends_with('/') { 643 | builder.append('/'); 644 | } 645 | builder.append(p); 646 | }) 647 | .unwrap(); 648 | GlobPattern::new(&pattern) 649 | } 650 | 651 | pub fn as_str(&self) -> Cow { 652 | if self.is_negated { 653 | Cow::Owned(format!("!{}", self.pattern.as_str())) 654 | } else { 655 | Cow::Borrowed(self.pattern.as_str()) 656 | } 657 | } 658 | 659 | pub fn matches_path(&self, path: &Path) -> PathGlobMatch { 660 | if self.pattern.matches_path_with(path, match_options()) { 661 | if self.is_negated { 662 | PathGlobMatch::MatchedNegated 663 | } else { 664 | PathGlobMatch::Matched 665 | } 666 | } else { 667 | PathGlobMatch::NotMatched 668 | } 669 | } 670 | 671 | pub fn base_path(&self) -> PathBuf { 672 | let base_path = self 673 | .pattern 674 | .as_str() 675 | .split('/') 676 | .take_while(|c| !has_glob_chars(c)) 677 | .collect::>() 678 | .join(std::path::MAIN_SEPARATOR_STR); 679 | PathBuf::from(base_path) 680 | } 681 | 682 | pub fn is_negated(&self) -> bool { 683 | self.is_negated 684 | } 685 | 686 | fn as_negated(&self) -> GlobPattern { 687 | Self { 688 | is_negated: !self.is_negated, 689 | pattern: self.pattern.clone(), 690 | } 691 | } 692 | } 693 | 694 | pub fn is_glob_pattern(path: &str) -> bool { 695 | !has_url_prefix(path) && has_glob_chars(path) 696 | } 697 | 698 | fn has_url_prefix(pattern: &str) -> bool { 699 | pattern.starts_with("http://") 700 | || pattern.starts_with("https://") 701 | || pattern.starts_with("file://") 702 | || pattern.starts_with("npm:") 703 | || pattern.starts_with("jsr:") 704 | } 705 | 706 | fn has_glob_chars(pattern: &str) -> bool { 707 | // we don't support [ and ] 708 | pattern.chars().any(|c| matches!(c, '*' | '?')) 709 | } 710 | 711 | fn escape_brackets(pattern: &str) -> String { 712 | // Escape brackets - we currently don't support them, because with introduction 713 | // of glob expansion paths like "pages/[id].ts" would suddenly start giving 714 | // wrong results. We might want to revisit that in the future. 715 | pattern.replace('[', "[[]").replace(']', "[]]") 716 | } 717 | 718 | fn match_options() -> glob::MatchOptions { 719 | // Matches what `deno_task_shell` does 720 | glob::MatchOptions { 721 | // false because it should work the same way on case insensitive file systems 722 | case_sensitive: false, 723 | // true because it copies what sh does 724 | require_literal_separator: true, 725 | // true because it copies with sh does—these files are considered "hidden" 726 | require_literal_leading_dot: true, 727 | } 728 | } 729 | 730 | #[cfg(test)] 731 | mod test { 732 | use std::error::Error; 733 | 734 | use deno_path_util::url_from_directory_path; 735 | use pretty_assertions::assert_eq; 736 | use tempfile::TempDir; 737 | 738 | use super::*; 739 | 740 | // For easier comparisons in tests. 741 | #[derive(Debug, PartialEq, Eq)] 742 | struct ComparableFilePatterns { 743 | base: String, 744 | include: Option>, 745 | exclude: Vec, 746 | } 747 | 748 | impl ComparableFilePatterns { 749 | pub fn new(root: &Path, file_patterns: &FilePatterns) -> Self { 750 | fn path_to_string(root: &Path, path: &Path) -> String { 751 | path 752 | .strip_prefix(root) 753 | .unwrap() 754 | .to_string_lossy() 755 | .replace('\\', "/") 756 | } 757 | 758 | fn path_or_pattern_to_string( 759 | root: &Path, 760 | p: &PathOrPattern, 761 | ) -> Option { 762 | match p { 763 | PathOrPattern::RemoteUrl(_) => None, 764 | PathOrPattern::Path(p) => Some(path_to_string(root, p)), 765 | PathOrPattern::NegatedPath(p) => { 766 | Some(format!("!{}", path_to_string(root, p))) 767 | } 768 | PathOrPattern::Pattern(p) => { 769 | let was_negated = p.is_negated(); 770 | let p = if was_negated { 771 | p.as_negated() 772 | } else { 773 | p.clone() 774 | }; 775 | let text = p 776 | .as_str() 777 | .strip_prefix(&format!( 778 | "{}/", 779 | root.to_string_lossy().replace('\\', "/") 780 | )) 781 | .unwrap_or_else(|| panic!("pattern: {:?}, root: {:?}", p, root)) 782 | .to_string(); 783 | Some(if was_negated { 784 | format!("!{}", text) 785 | } else { 786 | text 787 | }) 788 | } 789 | } 790 | } 791 | 792 | Self { 793 | base: path_to_string(root, &file_patterns.base), 794 | include: file_patterns.include.as_ref().map(|p| { 795 | p.0 796 | .iter() 797 | .filter_map(|p| path_or_pattern_to_string(root, p)) 798 | .collect() 799 | }), 800 | exclude: file_patterns 801 | .exclude 802 | .0 803 | .iter() 804 | .filter_map(|p| path_or_pattern_to_string(root, p)) 805 | .collect(), 806 | } 807 | } 808 | 809 | pub fn from_split( 810 | root: &Path, 811 | patterns_by_base: &[FilePatterns], 812 | ) -> Vec { 813 | patterns_by_base 814 | .iter() 815 | .map(|file_patterns| ComparableFilePatterns::new(root, file_patterns)) 816 | .collect() 817 | } 818 | } 819 | 820 | #[test] 821 | fn file_patterns_split_by_base_dir() { 822 | let temp_dir = TempDir::new().unwrap(); 823 | let patterns = FilePatterns { 824 | base: temp_dir.path().to_path_buf(), 825 | include: Some(PathOrPatternSet::new(vec![ 826 | PathOrPattern::Pattern( 827 | GlobPattern::new(&format!( 828 | "{}/inner/**/*.ts", 829 | temp_dir.path().to_string_lossy().replace('\\', "/") 830 | )) 831 | .unwrap(), 832 | ), 833 | PathOrPattern::Pattern( 834 | GlobPattern::new(&format!( 835 | "{}/inner/sub/deeper/**/*.js", 836 | temp_dir.path().to_string_lossy().replace('\\', "/") 837 | )) 838 | .unwrap(), 839 | ), 840 | PathOrPattern::Pattern( 841 | GlobPattern::new(&format!( 842 | "{}/other/**/*.js", 843 | temp_dir.path().to_string_lossy().replace('\\', "/") 844 | )) 845 | .unwrap(), 846 | ), 847 | PathOrPattern::from_relative(temp_dir.path(), "!./other/**/*.ts") 848 | .unwrap(), 849 | PathOrPattern::from_relative(temp_dir.path(), "sub/file.ts").unwrap(), 850 | ])), 851 | exclude: PathOrPatternSet::new(vec![ 852 | PathOrPattern::Pattern( 853 | GlobPattern::new(&format!( 854 | "{}/inner/other/**/*.ts", 855 | temp_dir.path().to_string_lossy().replace('\\', "/") 856 | )) 857 | .unwrap(), 858 | ), 859 | PathOrPattern::Path( 860 | temp_dir 861 | .path() 862 | .join("inner/sub/deeper/file.js") 863 | .to_path_buf(), 864 | ), 865 | ]), 866 | }; 867 | let split = ComparableFilePatterns::from_split( 868 | temp_dir.path(), 869 | &patterns.split_by_base(), 870 | ); 871 | assert_eq!( 872 | split, 873 | vec![ 874 | ComparableFilePatterns { 875 | base: "inner/sub/deeper".to_string(), 876 | include: Some(vec![ 877 | "inner/sub/deeper/**/*.js".to_string(), 878 | "inner/**/*.ts".to_string(), 879 | ]), 880 | exclude: vec!["inner/sub/deeper/file.js".to_string()], 881 | }, 882 | ComparableFilePatterns { 883 | base: "sub/file.ts".to_string(), 884 | include: Some(vec!["sub/file.ts".to_string()]), 885 | exclude: vec![], 886 | }, 887 | ComparableFilePatterns { 888 | base: "inner".to_string(), 889 | include: Some(vec!["inner/**/*.ts".to_string()]), 890 | exclude: vec![ 891 | "inner/other/**/*.ts".to_string(), 892 | "inner/sub/deeper/file.js".to_string(), 893 | ], 894 | }, 895 | ComparableFilePatterns { 896 | base: "other".to_string(), 897 | include: Some(vec!["other/**/*.js".to_string()]), 898 | exclude: vec!["other/**/*.ts".to_string()], 899 | } 900 | ] 901 | ); 902 | } 903 | 904 | #[test] 905 | fn file_patterns_split_by_base_dir_unexcluded() { 906 | let temp_dir = TempDir::new().unwrap(); 907 | let patterns = FilePatterns { 908 | base: temp_dir.path().to_path_buf(), 909 | include: None, 910 | exclude: PathOrPatternSet::new(vec![ 911 | PathOrPattern::from_relative(temp_dir.path(), "./ignored").unwrap(), 912 | PathOrPattern::from_relative(temp_dir.path(), "!./ignored/unexcluded") 913 | .unwrap(), 914 | PathOrPattern::from_relative(temp_dir.path(), "!./ignored/test/**") 915 | .unwrap(), 916 | ]), 917 | }; 918 | let split = ComparableFilePatterns::from_split( 919 | temp_dir.path(), 920 | &patterns.split_by_base(), 921 | ); 922 | assert_eq!( 923 | split, 924 | vec![ 925 | ComparableFilePatterns { 926 | base: "ignored/unexcluded".to_string(), 927 | include: None, 928 | exclude: vec![ 929 | // still keeps the higher level exclude for cases 930 | // where these two are accidentally swapped 931 | "ignored".to_string(), 932 | // keep the glob for the current dir because it 933 | // could be used to override the .gitignore 934 | "!ignored/unexcluded".to_string(), 935 | ], 936 | }, 937 | ComparableFilePatterns { 938 | base: "ignored/test".to_string(), 939 | include: None, 940 | exclude: vec!["ignored".to_string(), "!ignored/test/**".to_string(),], 941 | }, 942 | ComparableFilePatterns { 943 | base: "".to_string(), 944 | include: None, 945 | exclude: vec![ 946 | "ignored".to_string(), 947 | "!ignored/unexcluded".to_string(), 948 | "!ignored/test/**".to_string(), 949 | ], 950 | }, 951 | ] 952 | ); 953 | } 954 | 955 | #[test] 956 | fn file_patterns_split_by_base_dir_unexcluded_with_path_includes() { 957 | let temp_dir = TempDir::new().unwrap(); 958 | let patterns = FilePatterns { 959 | base: temp_dir.path().to_path_buf(), 960 | include: Some(PathOrPatternSet::new(vec![PathOrPattern::from_relative( 961 | temp_dir.path(), 962 | "./sub", 963 | ) 964 | .unwrap()])), 965 | exclude: PathOrPatternSet::new(vec![ 966 | PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(), 967 | PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/**") 968 | .unwrap(), 969 | PathOrPattern::from_relative(temp_dir.path(), "./orphan").unwrap(), 970 | PathOrPattern::from_relative(temp_dir.path(), "!./orphan/test/**") 971 | .unwrap(), 972 | ]), 973 | }; 974 | let split = ComparableFilePatterns::from_split( 975 | temp_dir.path(), 976 | &patterns.split_by_base(), 977 | ); 978 | assert_eq!( 979 | split, 980 | vec![ 981 | ComparableFilePatterns { 982 | base: "sub/ignored/test".to_string(), 983 | include: None, 984 | exclude: vec![ 985 | "sub/ignored".to_string(), 986 | "!sub/ignored/test/**".to_string(), 987 | ], 988 | }, 989 | ComparableFilePatterns { 990 | base: "sub".to_string(), 991 | include: Some(vec!["sub".to_string()]), 992 | exclude: vec![ 993 | "sub/ignored".to_string(), 994 | "!sub/ignored/test/**".to_string(), 995 | ], 996 | }, 997 | ] 998 | ); 999 | } 1000 | 1001 | #[test] 1002 | fn file_patterns_split_by_base_dir_unexcluded_with_glob_includes() { 1003 | let temp_dir = TempDir::new().unwrap(); 1004 | let patterns = FilePatterns { 1005 | base: temp_dir.path().to_path_buf(), 1006 | include: Some(PathOrPatternSet::new(vec![PathOrPattern::from_relative( 1007 | temp_dir.path(), 1008 | "./sub/**", 1009 | ) 1010 | .unwrap()])), 1011 | exclude: PathOrPatternSet::new(vec![ 1012 | PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(), 1013 | PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/**") 1014 | .unwrap(), 1015 | PathOrPattern::from_relative(temp_dir.path(), "!./orphan/test/**") 1016 | .unwrap(), 1017 | PathOrPattern::from_relative(temp_dir.path(), "!orphan/other").unwrap(), 1018 | ]), 1019 | }; 1020 | let split = ComparableFilePatterns::from_split( 1021 | temp_dir.path(), 1022 | &patterns.split_by_base(), 1023 | ); 1024 | assert_eq!( 1025 | split, 1026 | vec![ 1027 | ComparableFilePatterns { 1028 | base: "sub/ignored/test".to_string(), 1029 | include: Some(vec!["sub/**".to_string()]), 1030 | exclude: vec![ 1031 | "sub/ignored".to_string(), 1032 | "!sub/ignored/test/**".to_string() 1033 | ], 1034 | }, 1035 | ComparableFilePatterns { 1036 | base: "sub".to_string(), 1037 | include: Some(vec!["sub/**".to_string()]), 1038 | exclude: vec![ 1039 | "sub/ignored".to_string(), 1040 | "!sub/ignored/test/**".to_string(), 1041 | ], 1042 | } 1043 | ] 1044 | ); 1045 | } 1046 | 1047 | #[test] 1048 | fn file_patterns_split_by_base_dir_opposite_exclude() { 1049 | let temp_dir = TempDir::new().unwrap(); 1050 | let patterns = FilePatterns { 1051 | base: temp_dir.path().to_path_buf(), 1052 | include: None, 1053 | // this will actually error before it gets here in integration, 1054 | // but it's best to ensure it's handled anyway 1055 | exclude: PathOrPatternSet::new(vec![ 1056 | // this won't be unexcluded because it's lower priority than the entry below 1057 | PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/") 1058 | .unwrap(), 1059 | // this is higher priority 1060 | PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(), 1061 | ]), 1062 | }; 1063 | let split = ComparableFilePatterns::from_split( 1064 | temp_dir.path(), 1065 | &patterns.split_by_base(), 1066 | ); 1067 | assert_eq!( 1068 | split, 1069 | vec![ 1070 | ComparableFilePatterns { 1071 | base: "sub/ignored/test".to_string(), 1072 | include: None, 1073 | exclude: vec![ 1074 | "!sub/ignored/test".to_string(), 1075 | "sub/ignored".to_string(), 1076 | ], 1077 | }, 1078 | ComparableFilePatterns { 1079 | base: "".to_string(), 1080 | include: None, 1081 | exclude: vec![ 1082 | "!sub/ignored/test".to_string(), 1083 | "sub/ignored".to_string(), 1084 | ], 1085 | }, 1086 | ] 1087 | ); 1088 | } 1089 | 1090 | #[test] 1091 | fn file_patterns_split_by_base_dir_exclude_unexcluded_and_glob() { 1092 | let temp_dir = TempDir::new().unwrap(); 1093 | let patterns = FilePatterns { 1094 | base: temp_dir.path().to_path_buf(), 1095 | include: None, 1096 | exclude: PathOrPatternSet::new(vec![ 1097 | PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(), 1098 | PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/") 1099 | .unwrap(), 1100 | PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored/**/*.ts") 1101 | .unwrap(), 1102 | ]), 1103 | }; 1104 | let split = ComparableFilePatterns::from_split( 1105 | temp_dir.path(), 1106 | &patterns.split_by_base(), 1107 | ); 1108 | assert_eq!( 1109 | split, 1110 | vec![ 1111 | ComparableFilePatterns { 1112 | base: "sub/ignored/test".to_string(), 1113 | include: None, 1114 | exclude: vec![ 1115 | "sub/ignored".to_string(), 1116 | "!sub/ignored/test".to_string(), 1117 | "sub/ignored/**/*.ts".to_string() 1118 | ], 1119 | }, 1120 | ComparableFilePatterns { 1121 | base: "".to_string(), 1122 | include: None, 1123 | exclude: vec![ 1124 | "sub/ignored".to_string(), 1125 | "!sub/ignored/test".to_string(), 1126 | "sub/ignored/**/*.ts".to_string(), 1127 | ], 1128 | }, 1129 | ] 1130 | ); 1131 | } 1132 | 1133 | #[track_caller] 1134 | fn run_file_patterns_match_test( 1135 | file_patterns: &FilePatterns, 1136 | path: &Path, 1137 | kind: PathKind, 1138 | expected: FilePatternsMatch, 1139 | ) { 1140 | assert_eq!( 1141 | file_patterns.matches_path_detail(path, kind), 1142 | expected, 1143 | "path: {:?}, kind: {:?}", 1144 | path, 1145 | kind 1146 | ); 1147 | assert_eq!( 1148 | file_patterns.matches_path(path, kind), 1149 | match expected { 1150 | FilePatternsMatch::Passed 1151 | | FilePatternsMatch::PassedOptedOutExclude => true, 1152 | FilePatternsMatch::Excluded => false, 1153 | } 1154 | ) 1155 | } 1156 | 1157 | #[test] 1158 | fn file_patterns_include() { 1159 | let cwd = current_dir(); 1160 | // include is a closed set 1161 | let file_patterns = FilePatterns { 1162 | base: cwd.clone(), 1163 | include: Some(PathOrPatternSet(vec![ 1164 | PathOrPattern::from_relative(&cwd, "target").unwrap(), 1165 | PathOrPattern::from_relative(&cwd, "other/**/*.ts").unwrap(), 1166 | ])), 1167 | exclude: PathOrPatternSet(vec![]), 1168 | }; 1169 | let run_test = 1170 | |path: &Path, kind: PathKind, expected: FilePatternsMatch| { 1171 | run_file_patterns_match_test(&file_patterns, path, kind, expected); 1172 | }; 1173 | run_test(&cwd, PathKind::Directory, FilePatternsMatch::Passed); 1174 | run_test( 1175 | &cwd.join("other"), 1176 | PathKind::Directory, 1177 | FilePatternsMatch::Passed, 1178 | ); 1179 | run_test( 1180 | &cwd.join("other/sub_dir"), 1181 | PathKind::Directory, 1182 | FilePatternsMatch::Passed, 1183 | ); 1184 | run_test( 1185 | &cwd.join("not_matched"), 1186 | PathKind::File, 1187 | FilePatternsMatch::Excluded, 1188 | ); 1189 | run_test( 1190 | &cwd.join("other/test.ts"), 1191 | PathKind::File, 1192 | FilePatternsMatch::Passed, 1193 | ); 1194 | run_test( 1195 | &cwd.join("other/test.js"), 1196 | PathKind::File, 1197 | FilePatternsMatch::Excluded, 1198 | ); 1199 | } 1200 | 1201 | #[test] 1202 | fn file_patterns_exclude() { 1203 | let cwd = current_dir(); 1204 | let file_patterns = FilePatterns { 1205 | base: cwd.clone(), 1206 | include: None, 1207 | exclude: PathOrPatternSet(vec![ 1208 | PathOrPattern::from_relative(&cwd, "target").unwrap(), 1209 | PathOrPattern::from_relative(&cwd, "!not_excluded").unwrap(), 1210 | // lower items take priority 1211 | PathOrPattern::from_relative(&cwd, "excluded_then_not_excluded") 1212 | .unwrap(), 1213 | PathOrPattern::from_relative(&cwd, "!excluded_then_not_excluded") 1214 | .unwrap(), 1215 | PathOrPattern::from_relative(&cwd, "!not_excluded_then_excluded") 1216 | .unwrap(), 1217 | PathOrPattern::from_relative(&cwd, "not_excluded_then_excluded") 1218 | .unwrap(), 1219 | ]), 1220 | }; 1221 | let run_test = 1222 | |path: &Path, kind: PathKind, expected: FilePatternsMatch| { 1223 | run_file_patterns_match_test(&file_patterns, path, kind, expected); 1224 | }; 1225 | run_test(&cwd, PathKind::Directory, FilePatternsMatch::Passed); 1226 | run_test( 1227 | &cwd.join("target"), 1228 | PathKind::File, 1229 | FilePatternsMatch::Excluded, 1230 | ); 1231 | run_test( 1232 | &cwd.join("not_excluded"), 1233 | PathKind::File, 1234 | FilePatternsMatch::PassedOptedOutExclude, 1235 | ); 1236 | run_test( 1237 | &cwd.join("excluded_then_not_excluded"), 1238 | PathKind::File, 1239 | FilePatternsMatch::PassedOptedOutExclude, 1240 | ); 1241 | run_test( 1242 | &cwd.join("not_excluded_then_excluded"), 1243 | PathKind::File, 1244 | FilePatternsMatch::Excluded, 1245 | ); 1246 | } 1247 | 1248 | #[test] 1249 | fn file_patterns_include_exclude() { 1250 | let cwd = current_dir(); 1251 | let file_patterns = FilePatterns { 1252 | base: cwd.clone(), 1253 | include: Some(PathOrPatternSet(vec![ 1254 | PathOrPattern::from_relative(&cwd, "other").unwrap(), 1255 | PathOrPattern::from_relative(&cwd, "target").unwrap(), 1256 | PathOrPattern::from_relative(&cwd, "**/*.js").unwrap(), 1257 | PathOrPattern::from_relative(&cwd, "**/file.ts").unwrap(), 1258 | ])), 1259 | exclude: PathOrPatternSet(vec![ 1260 | PathOrPattern::from_relative(&cwd, "target").unwrap(), 1261 | PathOrPattern::from_relative(&cwd, "!target/unexcluded/").unwrap(), 1262 | PathOrPattern::from_relative(&cwd, "!target/other/**").unwrap(), 1263 | PathOrPattern::from_relative(&cwd, "**/*.ts").unwrap(), 1264 | PathOrPattern::from_relative(&cwd, "!**/file.ts").unwrap(), 1265 | ]), 1266 | }; 1267 | let run_test = 1268 | |path: &Path, kind: PathKind, expected: FilePatternsMatch| { 1269 | run_file_patterns_match_test(&file_patterns, path, kind, expected); 1270 | }; 1271 | // matches other 1272 | run_test( 1273 | &cwd.join("other/test.txt"), 1274 | PathKind::File, 1275 | FilePatternsMatch::Passed, 1276 | ); 1277 | // matches **/*.js 1278 | run_test( 1279 | &cwd.join("sub_dir/test.js"), 1280 | PathKind::File, 1281 | FilePatternsMatch::Passed, 1282 | ); 1283 | // not in include set 1284 | run_test( 1285 | &cwd.join("sub_dir/test.txt"), 1286 | PathKind::File, 1287 | FilePatternsMatch::Excluded, 1288 | ); 1289 | // .ts extension not matched 1290 | run_test( 1291 | &cwd.join("other/test.ts"), 1292 | PathKind::File, 1293 | FilePatternsMatch::Excluded, 1294 | ); 1295 | // file.ts excluded from excludes 1296 | run_test( 1297 | &cwd.join("other/file.ts"), 1298 | PathKind::File, 1299 | FilePatternsMatch::PassedOptedOutExclude, 1300 | ); 1301 | // not allowed target dir 1302 | run_test( 1303 | &cwd.join("target/test.txt"), 1304 | PathKind::File, 1305 | FilePatternsMatch::Excluded, 1306 | ); 1307 | run_test( 1308 | &cwd.join("target/sub_dir/test.txt"), 1309 | PathKind::File, 1310 | FilePatternsMatch::Excluded, 1311 | ); 1312 | // but allowed target/other dir 1313 | run_test( 1314 | &cwd.join("target/other/test.txt"), 1315 | PathKind::File, 1316 | FilePatternsMatch::PassedOptedOutExclude, 1317 | ); 1318 | run_test( 1319 | &cwd.join("target/other/sub/dir/test.txt"), 1320 | PathKind::File, 1321 | FilePatternsMatch::PassedOptedOutExclude, 1322 | ); 1323 | // and in target/unexcluded 1324 | run_test( 1325 | &cwd.join("target/unexcluded/test.txt"), 1326 | PathKind::File, 1327 | FilePatternsMatch::PassedOptedOutExclude, 1328 | ); 1329 | } 1330 | 1331 | #[test] 1332 | fn file_patterns_include_excluded() { 1333 | let cwd = current_dir(); 1334 | let file_patterns = FilePatterns { 1335 | base: cwd.clone(), 1336 | include: None, 1337 | exclude: PathOrPatternSet(vec![ 1338 | PathOrPattern::from_relative(&cwd, "js/").unwrap(), 1339 | PathOrPattern::from_relative(&cwd, "!js/sub_dir/").unwrap(), 1340 | ]), 1341 | }; 1342 | let run_test = 1343 | |path: &Path, kind: PathKind, expected: FilePatternsMatch| { 1344 | run_file_patterns_match_test(&file_patterns, path, kind, expected); 1345 | }; 1346 | run_test( 1347 | &cwd.join("js/test.txt"), 1348 | PathKind::File, 1349 | FilePatternsMatch::Excluded, 1350 | ); 1351 | run_test( 1352 | &cwd.join("js/sub_dir/test.txt"), 1353 | PathKind::File, 1354 | FilePatternsMatch::PassedOptedOutExclude, 1355 | ); 1356 | } 1357 | 1358 | #[test] 1359 | fn file_patterns_opposite_incorrect_excluded_include() { 1360 | let cwd = current_dir(); 1361 | let file_patterns = FilePatterns { 1362 | base: cwd.clone(), 1363 | include: None, 1364 | exclude: PathOrPatternSet(vec![ 1365 | // this is lower priority 1366 | PathOrPattern::from_relative(&cwd, "!js/sub_dir/").unwrap(), 1367 | // this wins because it's higher priority 1368 | PathOrPattern::from_relative(&cwd, "js/").unwrap(), 1369 | ]), 1370 | }; 1371 | let run_test = 1372 | |path: &Path, kind: PathKind, expected: FilePatternsMatch| { 1373 | run_file_patterns_match_test(&file_patterns, path, kind, expected); 1374 | }; 1375 | run_test( 1376 | &cwd.join("js/test.txt"), 1377 | PathKind::File, 1378 | FilePatternsMatch::Excluded, 1379 | ); 1380 | run_test( 1381 | &cwd.join("js/sub_dir/test.txt"), 1382 | PathKind::File, 1383 | FilePatternsMatch::Excluded, 1384 | ); 1385 | } 1386 | 1387 | #[test] 1388 | fn from_relative() { 1389 | let cwd = current_dir(); 1390 | // leading dot slash 1391 | { 1392 | let pattern = PathOrPattern::from_relative(&cwd, "./**/*.ts").unwrap(); 1393 | assert_eq!( 1394 | pattern.matches_path(&cwd.join("foo.ts")), 1395 | PathGlobMatch::Matched 1396 | ); 1397 | assert_eq!( 1398 | pattern.matches_path(&cwd.join("dir/foo.ts")), 1399 | PathGlobMatch::Matched 1400 | ); 1401 | assert_eq!( 1402 | pattern.matches_path(&cwd.join("foo.js")), 1403 | PathGlobMatch::NotMatched 1404 | ); 1405 | assert_eq!( 1406 | pattern.matches_path(&cwd.join("dir/foo.js")), 1407 | PathGlobMatch::NotMatched 1408 | ); 1409 | } 1410 | // no leading dot slash 1411 | { 1412 | let pattern = PathOrPattern::from_relative(&cwd, "**/*.ts").unwrap(); 1413 | assert_eq!( 1414 | pattern.matches_path(&cwd.join("foo.ts")), 1415 | PathGlobMatch::Matched 1416 | ); 1417 | assert_eq!( 1418 | pattern.matches_path(&cwd.join("dir/foo.ts")), 1419 | PathGlobMatch::Matched 1420 | ); 1421 | assert_eq!( 1422 | pattern.matches_path(&cwd.join("foo.js")), 1423 | PathGlobMatch::NotMatched 1424 | ); 1425 | assert_eq!( 1426 | pattern.matches_path(&cwd.join("dir/foo.js")), 1427 | PathGlobMatch::NotMatched 1428 | ); 1429 | } 1430 | // exact file, leading dot slash 1431 | { 1432 | let pattern = PathOrPattern::from_relative(&cwd, "./foo.ts").unwrap(); 1433 | assert_eq!( 1434 | pattern.matches_path(&cwd.join("foo.ts")), 1435 | PathGlobMatch::Matched 1436 | ); 1437 | assert_eq!( 1438 | pattern.matches_path(&cwd.join("dir/foo.ts")), 1439 | PathGlobMatch::NotMatched 1440 | ); 1441 | assert_eq!( 1442 | pattern.matches_path(&cwd.join("foo.js")), 1443 | PathGlobMatch::NotMatched 1444 | ); 1445 | } 1446 | // exact file, no leading dot slash 1447 | { 1448 | let pattern = PathOrPattern::from_relative(&cwd, "foo.ts").unwrap(); 1449 | assert_eq!( 1450 | pattern.matches_path(&cwd.join("foo.ts")), 1451 | PathGlobMatch::Matched 1452 | ); 1453 | assert_eq!( 1454 | pattern.matches_path(&cwd.join("dir/foo.ts")), 1455 | PathGlobMatch::NotMatched 1456 | ); 1457 | assert_eq!( 1458 | pattern.matches_path(&cwd.join("foo.js")), 1459 | PathGlobMatch::NotMatched 1460 | ); 1461 | } 1462 | // error for invalid url 1463 | { 1464 | let err = PathOrPattern::from_relative(&cwd, "https://raw.githubusercontent.com%2Fdyedgreen%2Fdeno-sqlite%2Frework_api%2Fmod.ts").unwrap_err(); 1465 | assert_eq!(format!("{:#}", err), "Invalid URL 'https://raw.githubusercontent.com%2Fdyedgreen%2Fdeno-sqlite%2Frework_api%2Fmod.ts'"); 1466 | assert_eq!( 1467 | format!("{:#}", err.source().unwrap()), 1468 | "invalid international domain name" 1469 | ); 1470 | } 1471 | // sibling dir 1472 | { 1473 | let pattern = PathOrPattern::from_relative(&cwd, "../sibling").unwrap(); 1474 | let parent_dir = cwd.parent().unwrap(); 1475 | assert_eq!(pattern.base_path().unwrap(), parent_dir.join("sibling")); 1476 | assert_eq!( 1477 | pattern.matches_path(&parent_dir.join("sibling/foo.ts")), 1478 | PathGlobMatch::Matched 1479 | ); 1480 | assert_eq!( 1481 | pattern.matches_path(&parent_dir.join("./other/foo.js")), 1482 | PathGlobMatch::NotMatched 1483 | ); 1484 | } 1485 | } 1486 | 1487 | #[test] 1488 | fn from_relative_dot_slash() { 1489 | let cwd = current_dir(); 1490 | let pattern = PathOrPattern::from_relative(&cwd, "./").unwrap(); 1491 | match pattern { 1492 | PathOrPattern::Path(p) => assert_eq!(p, cwd), 1493 | _ => unreachable!(), 1494 | } 1495 | } 1496 | 1497 | #[test] 1498 | fn new_ctor() { 1499 | let cwd = current_dir(); 1500 | for scheme in &["http", "https"] { 1501 | let url = format!("{}://deno.land/x/test", scheme); 1502 | let pattern = PathOrPattern::new(&url).unwrap(); 1503 | match pattern { 1504 | PathOrPattern::RemoteUrl(p) => { 1505 | assert_eq!(p.as_str(), url) 1506 | } 1507 | _ => unreachable!(), 1508 | } 1509 | } 1510 | for scheme in &["npm", "jsr"] { 1511 | let url = format!("{}:@denotest/basic", scheme); 1512 | let pattern = PathOrPattern::new(&url).unwrap(); 1513 | match pattern { 1514 | PathOrPattern::RemoteUrl(p) => { 1515 | assert_eq!(p.as_str(), url) 1516 | } 1517 | _ => unreachable!(), 1518 | } 1519 | } 1520 | { 1521 | let file_specifier = url_from_directory_path(&cwd).unwrap(); 1522 | let pattern = PathOrPattern::new(file_specifier.as_str()).unwrap(); 1523 | match pattern { 1524 | PathOrPattern::Path(p) => { 1525 | assert_eq!(p, cwd); 1526 | } 1527 | _ => { 1528 | unreachable!() 1529 | } 1530 | } 1531 | } 1532 | } 1533 | 1534 | #[test] 1535 | fn from_relative_specifier() { 1536 | let cwd = current_dir(); 1537 | for scheme in &["http", "https"] { 1538 | let url = format!("{}://deno.land/x/test", scheme); 1539 | let pattern = PathOrPattern::from_relative(&cwd, &url).unwrap(); 1540 | match pattern { 1541 | PathOrPattern::RemoteUrl(p) => { 1542 | assert_eq!(p.as_str(), url) 1543 | } 1544 | _ => unreachable!(), 1545 | } 1546 | } 1547 | for scheme in &["npm", "jsr"] { 1548 | let url = format!("{}:@denotest/basic", scheme); 1549 | let pattern = PathOrPattern::from_relative(&cwd, &url).unwrap(); 1550 | match pattern { 1551 | PathOrPattern::RemoteUrl(p) => { 1552 | assert_eq!(p.as_str(), url) 1553 | } 1554 | _ => unreachable!(), 1555 | } 1556 | } 1557 | { 1558 | let file_specifier = url_from_directory_path(&cwd).unwrap(); 1559 | let pattern = 1560 | PathOrPattern::from_relative(&cwd, file_specifier.as_str()).unwrap(); 1561 | match pattern { 1562 | PathOrPattern::Path(p) => { 1563 | assert_eq!(p, cwd); 1564 | } 1565 | _ => { 1566 | unreachable!() 1567 | } 1568 | } 1569 | } 1570 | } 1571 | 1572 | #[test] 1573 | fn negated_globs() { 1574 | #[allow(clippy::disallowed_methods)] 1575 | let cwd = current_dir(); 1576 | { 1577 | let pattern = GlobPattern::from_relative(&cwd, "!./**/*.ts").unwrap(); 1578 | assert!(pattern.is_negated()); 1579 | assert_eq!(pattern.base_path(), cwd); 1580 | assert!(pattern.as_str().starts_with('!')); 1581 | assert_eq!( 1582 | pattern.matches_path(&cwd.join("foo.ts")), 1583 | PathGlobMatch::MatchedNegated 1584 | ); 1585 | assert_eq!( 1586 | pattern.matches_path(&cwd.join("foo.js")), 1587 | PathGlobMatch::NotMatched 1588 | ); 1589 | let pattern = pattern.as_negated(); 1590 | assert!(!pattern.is_negated()); 1591 | assert_eq!(pattern.base_path(), cwd); 1592 | assert!(!pattern.as_str().starts_with('!')); 1593 | assert_eq!( 1594 | pattern.matches_path(&cwd.join("foo.ts")), 1595 | PathGlobMatch::Matched 1596 | ); 1597 | let pattern = pattern.as_negated(); 1598 | assert!(pattern.is_negated()); 1599 | assert_eq!(pattern.base_path(), cwd); 1600 | assert!(pattern.as_str().starts_with('!')); 1601 | assert_eq!( 1602 | pattern.matches_path(&cwd.join("foo.ts")), 1603 | PathGlobMatch::MatchedNegated 1604 | ); 1605 | } 1606 | } 1607 | 1608 | #[test] 1609 | fn test_is_glob_pattern() { 1610 | assert!(!is_glob_pattern("npm:@scope/pkg@*")); 1611 | assert!(!is_glob_pattern("jsr:@scope/pkg@*")); 1612 | assert!(!is_glob_pattern("https://deno.land/x/?")); 1613 | assert!(!is_glob_pattern("http://deno.land/x/?")); 1614 | assert!(!is_glob_pattern("file:///deno.land/x/?")); 1615 | assert!(is_glob_pattern("**/*.ts")); 1616 | assert!(is_glob_pattern("test/?")); 1617 | assert!(!is_glob_pattern("test/test")); 1618 | } 1619 | 1620 | fn current_dir() -> PathBuf { 1621 | // ok because this is test code 1622 | #[allow(clippy::disallowed_methods)] 1623 | std::env::current_dir().unwrap() 1624 | } 1625 | } 1626 | --------------------------------------------------------------------------------