From a3561fdd66d00eb5eb425814f67156b712655577 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Fri, 17 Apr 2026 12:00:09 +0200 Subject: [PATCH 1/3] Bump rust version to 2024 --- Cargo.toml | 2 +- src/action.rs | 5 ++- src/check.rs | 2 +- src/check/authors.rs | 2 +- src/check/compile.rs | 2 +- src/check/files.rs | 18 ++++---- src/check/imports.rs | 33 +++++++-------- src/check/kebab_case.rs | 6 +-- src/check/manifest.rs | 92 ++++++++++++++++++++--------------------- src/check/readme.rs | 4 +- src/cli.rs | 2 +- src/github.rs | 58 +++++++++++++------------- src/github/api/pr.rs | 2 +- src/world.rs | 52 +++++++++++------------ 14 files changed, 139 insertions(+), 141 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d1cbd22..2849095 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "typst-package-check" version = "0.4.6" -edition = "2021" +edition = "2024" [dependencies] casbab = "0.1.1" diff --git a/src/action.rs b/src/action.rs index b4d5606..8eb1213 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,8 +1,9 @@ use crate::{ check::TryExt, github::{ - api::{pr::PullRequestEvent, GitHubAuth, Installation, Repository}, - run_github_check, AppState, + AppState, + api::{GitHubAuth, Installation, Repository, pr::PullRequestEvent}, + run_github_check, }, }; diff --git a/src/check.rs b/src/check.rs index 72b414a..3647fef 100644 --- a/src/check.rs +++ b/src/check.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use codespan_reporting::diagnostic::Label; use typst::{ - syntax::{package::PackageSpec, FileId, Span}, WorldExt, + syntax::{FileId, Span, package::PackageSpec}, }; use crate::world::SystemWorld; diff --git a/src/check/authors.rs b/src/check/authors.rs index b0a7fb0..5a4eaae 100644 --- a/src/check/authors.rs +++ b/src/check/authors.rs @@ -1,5 +1,5 @@ use codespan_reporting::diagnostic::{Diagnostic, Label}; -use typst::syntax::{package::PackageSpec, FileId, VirtualPath}; +use typst::syntax::{FileId, VirtualPath, package::PackageSpec}; use crate::{github::git, package::PackageExt}; diff --git a/src/check/compile.rs b/src/check/compile.rs index a986139..3482a42 100644 --- a/src/check/compile.rs +++ b/src/check/compile.rs @@ -7,7 +7,7 @@ use typst::{ use crate::world::SystemWorld; -use super::{label, Diagnostics}; +use super::{Diagnostics, label}; pub fn check(diags: &mut Diagnostics, world: &SystemWorld) -> Option { let result = typst::compile(world); diff --git a/src/check/files.rs b/src/check/files.rs index 6d218d7..0fa9cd3 100644 --- a/src/check/files.rs +++ b/src/check/files.rs @@ -3,10 +3,10 @@ use std::path::Path; use codespan_reporting::diagnostic::{Diagnostic, Label}; +use crate::check::Diagnostics; use crate::check::manifest::Manifest; use crate::check::path::PackagePath; use crate::check::readme::Readme; -use crate::check::Diagnostics; pub fn check( diags: &mut Diagnostics, @@ -218,13 +218,15 @@ fn link_manuals( let note = (!excluded) .then(|| "It should also be added to `exclude` in your `typst.toml`.".into()); - diags.emit(Diagnostic::warning() - .with_label(Label::primary(path.file_id(), 0..0)) - .with_code("files/manual/unlinked") - .with_message( - "This file seems to be a manual/documentation, but isn't linked in the readme. \ + diags.emit( + Diagnostic::warning() + .with_label(Label::primary(path.file_id(), 0..0)) + .with_code("files/manual/unlinked") + .with_message( + "This file seems to be a manual/documentation, but isn't linked in the readme. \ It will be inacessible on Typst Universe.", - ) - .with_notes_iter(note)); + ) + .with_notes_iter(note), + ); } } diff --git a/src/check/imports.rs b/src/check/imports.rs index 0df5b80..17e975f 100644 --- a/src/check/imports.rs +++ b/src/check/imports.rs @@ -5,16 +5,16 @@ use std::{ use codespan_reporting::diagnostic::Diagnostic; use typst::{ + World, syntax::{ ast::{self, AstNode, ModuleImport}, package::{PackageSpec, PackageVersion, VersionlessPackageSpec}, }, - World, }; use walkdir::WalkDir; use crate::check::path::PackagePath; -use crate::check::{label, Diagnostics, Result, TryExt}; +use crate::check::{Diagnostics, Result, TryExt, label}; use crate::world::SystemWorld; pub fn check(diags: &mut Diagnostics, package_dir: &Path, world: &SystemWorld) -> Result<()> { @@ -85,23 +85,18 @@ pub fn check_ast( ) } - if let Some(all_packages) = all_packages { - if let Ok(import_spec) = PackageSpec::from_str(source_str.get().as_str()) { - if let Some(latest_version) = - latest_package_version(all_packages, import_spec.versionless()) - { - if latest_version != import_spec.version { - diags.emit( - Diagnostic::warning() - .with_labels(label(world, import.span()).into_iter().collect()) - .with_code("import/outdated") - .with_message( - "This import seems to use an older version of the package.", - ), - ) - } - } - } + if let Some(all_packages) = all_packages + && let Ok(import_spec) = PackageSpec::from_str(source_str.get().as_str()) + && let Some(latest_version) = + latest_package_version(all_packages, import_spec.versionless()) + && latest_version != import_spec.version + { + diags.emit( + Diagnostic::warning() + .with_labels(label(world, import.span()).into_iter().collect()) + .with_code("import/outdated") + .with_message("This import seems to use an older version of the package."), + ) } } } diff --git a/src/check/kebab_case.rs b/src/check/kebab_case.rs index f44f905..af7a337 100644 --- a/src/check/kebab_case.rs +++ b/src/check/kebab_case.rs @@ -3,17 +3,17 @@ use std::collections::HashSet; use codespan_reporting::diagnostic::{Diagnostic, Severity}; use comemo::Track; use typst::{ + ROUTINES, World, engine::{Route, Sink, Traced}, syntax::{ - ast::{self, AstNode}, FileId, Source, SyntaxNode, + ast::{self, AstNode}, }, - World, ROUTINES, }; use crate::world::SystemWorld; -use super::{label, Diagnostics}; +use super::{Diagnostics, label}; // Check that all public identifiers are in kebab-case pub fn check(diags: &mut Diagnostics, world: &SystemWorld) -> Option<()> { diff --git a/src/check/manifest.rs b/src/check/manifest.rs index a148e47..63d7dac 100644 --- a/src/check/manifest.rs +++ b/src/check/manifest.rs @@ -7,8 +7,8 @@ use reqwest::StatusCode; use toml_edit::{Array, Item, Table}; use tracing::{debug, warn}; use typst::syntax::{ - package::{PackageSpec, PackageVersion}, FileId, VirtualPath, + package::{PackageSpec, PackageVersion}, }; use crate::check::path::{self, PackagePath}; @@ -177,20 +177,20 @@ fn check_name( ); } - if let Some(package_spec) = package_spec { - if name.val != package_spec.name { - diags.emit( - error - .with_code("manifest/package/name/mismatch") - .with_message(format!( - "Unexpected package name. `{name}` was expected. \ - If you want to publish a new package, create a new \ - directory in `packages/{namespace}/`.", - name = package_spec.name, - namespace = package_spec.namespace, - )), - ) - } + if let Some(package_spec) = package_spec + && name.val != package_spec.name + { + diags.emit( + error + .with_code("manifest/package/name/mismatch") + .with_message(format!( + "Unexpected package name. `{name}` was expected. \ + If you want to publish a new package, create a new \ + directory in `packages/{namespace}/`.", + name = package_spec.name, + namespace = package_spec.namespace, + )), + ) } Some(name.to_owned()) @@ -232,27 +232,27 @@ fn check_version( .with_code("manifest/package/version/invalid") .with_message( "`version` must be a valid semantic version \ - (i.e follow the `MAJOR.MINOR.PATCH` format).", + (i.e follow the `MAJOR.MINOR.PATCH` format).", ), ); return None; }; - if let Some(package_spec) = package_spec { - if version.val != package_spec.version { - diags.emit( - error - .with_code("manifest/package/version/mismatch") - .with_message(format!( - "Unexpected version number. `{version}` was expected. \ - If you want to publish a new version, create a new \ - directory in `packages/{namespace}/{name}`.", - version = package_spec.version, - name = package_spec.name, - namespace = package_spec.namespace, - )), - ) - } + if let Some(package_spec) = package_spec + && version.val != package_spec.version + { + diags.emit( + error + .with_code("manifest/package/version/mismatch") + .with_message(format!( + "Unexpected version number. `{version}` was expected. \ + If you want to publish a new version, create a new \ + directory in `packages/{namespace}/{name}`.", + version = package_spec.version, + name = package_spec.name, + namespace = package_spec.namespace, + )), + ) } Some(version) @@ -489,7 +489,7 @@ async fn check_repo(diags: &mut Diagnostics, package: Spanned<&Table>) { .with_code("manifest/package/homepage/redundant") .with_message( "Use the homepage field only if there is a dedicated website. \ - Otherwise, prefer the `repository` field.", + Otherwise, prefer the `repository` field.", ), ) } @@ -695,20 +695,20 @@ fn check_thumbnail(diags: &mut Diagnostics, exclude: &Override, template: &Spann ); } - if let Some(template_path) = &template.path { - if thumbnail_path.full().starts_with(template_path.full()) { - diags.emit( - Diagnostic::error() - .with_label(Label::primary(manifest_id(), thumbnail_path.span())) - .with_code("manifest/template/thumbnail/location") - .with_message( - "The thumbnail file should be outside of the template directory.\n\n\ - When your template will be used as a base for users's projects, \ - the template directory will be copied as is, and the thumbnail file - is generally not displayed in documents based on your template.", - ), - ); - } + if let Some(template_path) = &template.path + && thumbnail_path.full().starts_with(template_path.full()) + { + diags.emit( + Diagnostic::error() + .with_label(Label::primary(manifest_id(), thumbnail_path.span())) + .with_code("manifest/template/thumbnail/location") + .with_message( + "The thumbnail file should be outside of the template directory.\n\n\ + When your template will be used as a base for users's projects, \ + the template directory will be copied as is, and the thumbnail file + is generally not displayed in documents based on your template.", + ), + ); } } diff --git a/src/check/readme.rs b/src/check/readme.rs index 365bf00..4c8ca72 100644 --- a/src/check/readme.rs +++ b/src/check/readme.rs @@ -7,15 +7,15 @@ use comrak::nodes::{LineColumn, NodeList, NodeValue as MdNode, Sourcepos}; use html5ever::tendril::TendrilSink; use regex::Regex; use typst::{ + World, foundations::Bytes, syntax::{FileId, VirtualPath}, - World, }; use url::Url; use crate::check::path::PackagePath; use crate::{ - check::{imports, kebab_case, label, Diagnostics, TryExt}, + check::{Diagnostics, TryExt, imports, kebab_case, label}, world::SystemWorld, }; diff --git a/src/cli.rs b/src/cli.rs index bb9a426..1b7f7ac 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use std::process::exit; use codespan_reporting::{diagnostic::Diagnostic, term}; use ignore::overrides::Override; use tracing::error; -use typst::syntax::{package::PackageSpec, FileId, Source}; +use typst::syntax::{FileId, Source, package::PackageSpec}; use crate::{check::all_checks, package::PackageExt, world::SystemWorld}; diff --git a/src/github.rs b/src/github.rs index b4b30d8..6722069 100644 --- a/src/github.rs +++ b/src/github.rs @@ -10,7 +10,7 @@ use codespan_reporting::{ use jwt_simple::prelude::*; use pr::{PullRequest, PullRequestUpdate}; use tracing::{debug, warn}; -use typst::syntax::{package::PackageSpec, FileId}; +use typst::syntax::{FileId, package::PackageSpec}; use crate::{ check::{self, Result, TryExt}, @@ -158,22 +158,22 @@ pub async fn run_github_check( expected_pr_title, labels ); // Actually update the PR, if needed - if let Some(expected_pr_title) = expected_pr_title { - if pr.title != expected_pr_title || !labels.is_empty() || body.is_some() { - api_client - .update_pull_request( - repository.owner(), - repository.name(), - pr.number, - PullRequestUpdate { - title: expected_pr_title, - labels, - body, - }, - ) - .await - .error("github/api/pull/update", "Failed to update pull request")?; - } + if let Some(expected_pr_title) = expected_pr_title + && (pr.title != expected_pr_title || !labels.is_empty() || body.is_some()) + { + api_client + .update_pull_request( + repository.owner(), + repository.name(), + pr.number, + PullRequestUpdate { + title: expected_pr_title, + labels, + body, + }, + ) + .await + .error("github/api/pull/update", "Failed to update pull request")?; } } @@ -238,7 +238,7 @@ pub async fn run_github_check( previous_pr.number, previous_pr.user.login ); if previous_pr.user.login != current_pr.user.login { - if let Err(e) = api_client + let res = api_client .post_pr_comment( repository.owner(), repository.name(), @@ -252,15 +252,19 @@ pub async fn run_github_check( will not be merged.", previous_pr.user.login, package.name, - package.previous_version() - .expect("If there is no previous version, this branch should not be reached") + package + .previous_version() + .expect( + "If there is no previous version, \ + this branch should not be reached" + ) .version ), ) - .await - { - warn!("Error while posting PR comment: {:?}", e) - } + .await; + if let Err(e) = res { + warn!("Error while posting PR comment: {:?}", e) + } } } } @@ -405,11 +409,7 @@ fn update_description_checks( }) .unwrap_or_default(); - if body_changed { - Some(new_body) - } else { - None - } + body_changed.then_some(new_body) } fn diagnostic_to_annotation( diff --git a/src/github/api/pr.rs b/src/github/api/pr.rs index e21072d..dbce711 100644 --- a/src/github/api/pr.rs +++ b/src/github/api/pr.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use super::{user::User, ApiError, AuthInstallation, GitHub, JsonExt, OwnerId, RepoId}; +use super::{ApiError, AuthInstallation, GitHub, JsonExt, OwnerId, RepoId, user::User}; #[derive(Deserialize)] pub struct PullRequestEvent { diff --git a/src/world.rs b/src/world.rs index 3b642de..2cd4a89 100644 --- a/src/world.rs +++ b/src/world.rs @@ -12,14 +12,14 @@ use chrono::{DateTime, Datelike, FixedOffset, Local, Utc}; use fontdb::Database; use ignore::overrides::Override; use parking_lot::Mutex; -use tracing::{debug, span, Level}; +use tracing::{Level, debug, span}; use typst::{ + Library, LibraryExt, World, diag::{FileError, FileResult, PackageError, PackageResult}, foundations::{Bytes, Datetime}, - syntax::{package::PackageSpec, FileId, Source, VirtualPath}, + syntax::{FileId, Source, VirtualPath, package::PackageSpec}, text::{Font, FontBook, FontInfo}, utils::LazyHash, - Library, LibraryExt, World, }; use crate::package::PackageExt; @@ -265,10 +265,10 @@ impl SlotCell { f: impl FnOnce(Vec, Option) -> FileResult, ) -> FileResult { // If we accessed the file already in this compilation, retrieve it. - if std::mem::replace(&mut self.accessed, true) { - if let Some(data) = &self.data { - return data.clone(); - } + if std::mem::replace(&mut self.accessed, true) + && let Some(data) = &self.data + { + return data.clone(); } // Read and hash the file. @@ -276,10 +276,10 @@ impl SlotCell { let fingerprint = typst::utils::hash128(&result); // If the file contents didn't change, yield the old processed data. - if std::mem::replace(&mut self.fingerprint, fingerprint) == fingerprint { - if let Some(data) = &self.data { - return data.clone(); - } + if std::mem::replace(&mut self.fingerprint, fingerprint) == fingerprint + && let Some(data) = &self.data + { + return data.clone(); } let prev = self.data.take().and_then(Result::ok); @@ -302,13 +302,13 @@ fn system_path( debug!("File ID = {:?}", id); let exclude = |file: FileResult| match file { Ok(f) => { - if let Ok(canonical_path) = f.canonicalize() { - if excluded.matched(canonical_path, false).is_ignore() { - debug!("This file is excluded"); - return Err(FileError::Other(Some( - "This file exists but is excluded from your package.".into(), - ))); - } + if let Ok(canonical_path) = f.canonicalize() + && excluded.matched(canonical_path, false).is_ignore() + { + debug!("This file is excluded"); + return Err(FileError::Other(Some( + "This file exists but is excluded from your package.".into(), + ))); } debug!("Resolved to {}", f.display()); @@ -320,14 +320,14 @@ fn system_path( // Determine the root path relative to which the file path // will be resolved. let root = if let Some(spec) = id.package() { - if let Some(package_override) = package_override { - if *spec == package_override.0 { - return exclude( - id.vpath() - .resolve(&package_override.1) - .ok_or(FileError::AccessDenied), - ); - } + if let Some(package_override) = package_override + && *spec == package_override.0 + { + return exclude( + id.vpath() + .resolve(&package_override.1) + .ok_or(FileError::AccessDenied), + ); } expect_parents( From 26de7c22b1218018e535c8fbe902446e6693ca58 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Fri, 17 Apr 2026 11:04:56 +0200 Subject: [PATCH 2/3] Fix how excluded files are determined everywhere --- src/check.rs | 1 + src/check/files.rs | 55 ++++++++------------ src/check/manifest.rs | 118 +++++++++++++++++++++++++++++------------- src/check/path.rs | 11 +--- src/cli.rs | 4 +- src/world.rs | 61 ++++++++++------------ 6 files changed, 137 insertions(+), 113 deletions(-) diff --git a/src/check.rs b/src/check.rs index 3647fef..19727e1 100644 --- a/src/check.rs +++ b/src/check.rs @@ -19,6 +19,7 @@ mod path; mod readme; pub use diagnostics::{Diagnostics, Result, TryExt}; +pub use manifest::Exclude; pub async fn all_checks( package_spec: Option<&PackageSpec>, diff --git a/src/check/files.rs b/src/check/files.rs index 0fa9cd3..bdc769d 100644 --- a/src/check/files.rs +++ b/src/check/files.rs @@ -1,13 +1,31 @@ use std::collections::HashSet; +use std::ffi::OsStr; use std::path::Path; use codespan_reporting::diagnostic::{Diagnostic, Label}; +use walkdir::WalkDir; use crate::check::Diagnostics; use crate::check::manifest::Manifest; use crate::check::path::PackagePath; use crate::check::readme::Readme; +/// Creates a directory iterator with the same settings as the package bundler. +pub fn walk(dir: &Path) -> ignore::Walk { + ignore::WalkBuilder::new(dir) + .sort_by_file_name(|a, b| a.cmp(b)) + // Disable non-local ignore features + .parents(false) + .require_git(false) + .git_global(false) + .git_exclude(false) + // Keep local ignore features for now. + .git_ignore(true) + .ignore(true) + .hidden(true) + .build() +} + pub fn check( diags: &mut Diagnostics, package_dir: &Path, @@ -15,36 +33,17 @@ pub fn check( readme: &Option, ) { let exclude = &manifest.package.exclude; - let thumbnail_path = manifest.thumbnail(); - - // Manually keep track of excluded directories, to figure out if nested - // files are ignored. This is done, so we can generate diagnostics for - // excluded files. - let mut excluded_dirs = HashSet::new(); - for ch in ignore::WalkBuilder::new(package_dir).hidden(false).build() { - let Ok(ch) = ch else { continue }; + for ch in WalkDir::new(package_dir).into_iter().flatten() { let Ok(metadata) = ch.metadata() else { continue; }; - - let file_path = PackagePath::from_full(package_dir, ch.path()); - - if metadata.is_dir() { - // If the parent directory is ignored, all children are ignored too. - if parent_is_excluded(&excluded_dirs, file_path) - || exclude.matched(file_path.relative(), true).is_ignore() - { - excluded_dirs.insert(ch.into_path()); - } + if !metadata.is_file() { continue; } - // The thumbnail is always excluded. - let is_thumbnail = thumbnail_path.is_some_and(|t| t.val == file_path); - let excluded = is_thumbnail - || parent_is_excluded(&excluded_dirs, file_path) - || exclude.matched(file_path.relative(), false).is_ignore(); + let file_path = PackagePath::from_full(package_dir, ch.path()); + let excluded = exclude.matches_file(&file_path); forbid_font_files(diags, file_path); exclude_large_files(diags, file_path, excluded, metadata.len()); @@ -53,16 +52,6 @@ pub fn check( } } -fn parent_is_excluded( - excluded_dirs: &HashSet, - file_path: PackagePath<&Path>, -) -> bool { - file_path - .full() - .parent() - .is_some_and(|parent| excluded_dirs.contains(parent)) -} - fn exclude_large_files( diags: &mut Diagnostics, path: PackagePath<&Path>, diff --git a/src/check/manifest.rs b/src/check/manifest.rs index 63d7dac..146144c 100644 --- a/src/check/manifest.rs +++ b/src/check/manifest.rs @@ -11,6 +11,7 @@ use typst::syntax::{ package::{PackageSpec, PackageVersion}, }; +use crate::check::files; use crate::check::path::{self, PackagePath}; use crate::{ check::{Diagnostics, Result, TryExt}, @@ -26,22 +27,16 @@ pub struct Worlds { #[derive(Debug, Clone)] pub struct Manifest { pub package: Spanned, + #[allow(unused)] pub template: Option>, } -impl Manifest { - pub fn thumbnail(&self) -> Option>> { - let thumbnail = self.template.as_ref()?.thumbnail.as_ref()?; - Some(thumbnail.as_ref().map(PackagePath::as_path)) - } -} - #[derive(Debug, Clone)] pub struct Package { pub entrypoint: Spanned, pub name: Option>, pub version: Option>, - pub exclude: Spanned, + pub exclude: Spanned, } #[derive(Debug, Clone)] @@ -88,7 +83,8 @@ pub async fn check( let entrypoint = entrypoint.map(|e| PackagePath::from_relative(package_dir, e)); let name = check_name(diags, package, package_spec); let version = check_version(diags, package, package_spec); - let exclude = check_exclude(diags, package, package_dir)?; + let template = check_template(diags, &manifest, package_dir); + let exclude = check_exclude(diags, package_dir, package, &template)?; check_compiler_version(diags, package); check_universe_fields(diags, package); @@ -107,9 +103,8 @@ pub async fn check( exclude, }); - let template = check_template(diags, &manifest, package_dir); if let Some(template) = &template { - check_thumbnail(diags, &package.exclude, template); + check_thumbnail(diags, template); dont_exclude_template_files(diags, package_dir, &package.exclude, template); } @@ -284,10 +279,10 @@ fn check_compiler_version(diags: &mut Diagnostics, package: Spanned<&Table>) -> Some(()) } -fn dont_over_exclude(diags: &mut Diagnostics, exclude: &Spanned) -> Result<()> { +fn dont_over_exclude(diags: &mut Diagnostics, exclude: &Spanned) -> Result<()> { let warning = Diagnostic::warning().with_label(Label::primary(manifest_id(), exclude.span())); - if exclude.matched("LICENSE", false).is_ignore() { + if exclude.matches_relative_file("LICENSE") { diags.emit( warning .clone() @@ -296,7 +291,7 @@ fn dont_over_exclude(diags: &mut Diagnostics, exclude: &Spanned) -> Re ); } - if exclude.matched("README.md", false).is_ignore() { + if exclude.matches_relative_file("README.md") { diags.emit( warning .with_code("exclude/readme") @@ -500,11 +495,12 @@ async fn check_repo(diags: &mut Diagnostics, package: Spanned<&Table>) { /// a lot of false positives in other diagnostics. fn check_exclude( diags: &mut Diagnostics, - package: Spanned<&Table>, package_dir: &Path, -) -> Result> { + package: Spanned<&Table>, + template: &Option>, +) -> Result> { let Some(exclude) = package.get_spanned("exclude") else { - return Ok(Spanned::new(Override::empty(), package.span())); + return Ok(Spanned::new(Exclude::empty(), package.span())); }; let exclude = exclude.try_map(Item::as_array).error( @@ -537,10 +533,74 @@ fn check_exclude( exclude_globs.add(&format!("!{exclusion_str}")).ok(); } + let thumbnail_path = template.as_ref().and_then(|t| t.thumbnail.as_ref()); + if let Some(thumbnail_path) = thumbnail_path { + // Check if the thumbnail is directly excluded, it's fine if a parent + // directory is excluded. + if let Ok(exclude) = exclude_globs.build() + && exclude.matched(thumbnail_path.full(), false).is_ignore() + { + diags.emit( + Diagnostic::error() + .with_label(Label::primary(manifest_id(), thumbnail_path.span())) + .with_code("manifest/template/thumbnail/exclude") + .with_message("The template thumbnail is automatically excluded"), + ); + } + + exclude_globs + .add(&format!("!{}", thumbnail_path.relative().display())) + .ok(); + } + let exclude_globs = exclude_globs .build() .error("manifest/package/exclude/invalid", "Invalid exclude globs")?; - Ok(exclude.map(|_| exclude_globs)) + Ok(exclude.map(|_| Exclude::new(exclude_globs))) +} + +#[derive(Debug, Clone)] +pub struct Exclude { + globs: Override, +} + +impl Exclude { + pub fn empty() -> Self { + Self { + globs: Override::empty(), + } + } + + pub fn new(globs: Override) -> Self { + Self { globs } + } + + /// Whether the package file path is excluded. + pub fn matches_file>(&self, path: &PackagePath) -> bool { + self.matches_relative_file(path.relative()) + } + + /// Whether the package relative file path is excluded. + pub fn matches_relative_file(&self, relative_path: impl AsRef) -> bool { + let relative_path = relative_path.as_ref(); + if self.globs.matched(relative_path, false).is_ignore() { + return true; + } + + // Manually check if any of the parent directories is excluded. + // This has to be done to mimic the behavior of [`ignore::Walk`], which + // won't enter a directory if it is ignored. [`Override::matched] + // function doesn't check if a parent directory is excluded. + let mut parent = relative_path.parent(); + while let Some(dir) = parent { + if self.globs.matched(dir, true).is_ignore() { + return true; + } + parent = dir.parent(); + } + + false + } } fn check_template( @@ -569,7 +629,7 @@ fn check_template( let template_dir = path.as_ref()?; Some( entrypoint - .map(|entrypoint| path::relative_to(template_dir.full(), entrypoint)) + .map(|entrypoint| path::join_to(template_dir.full(), entrypoint)) .map(|path| PackagePath::from_full(package_dir, path)), ) }); @@ -618,14 +678,14 @@ fn world_for_template( fn dont_exclude_template_files( diags: &mut Diagnostics, package_dir: &Path, - exclude: &Override, + exclude: &Exclude, template: &Spanned