From e0f19367b40e2e3180c7dfe26e37d3fbb2028437 Mon Sep 17 00:00:00 2001 From: Bart Waardenburg Date: Mon, 1 Jun 2026 23:16:51 +0200 Subject: [PATCH 1/3] fix(plugins): activate bun plugin on @types/bun and parse bunfig.toml [test] preload entries Fixes #847 --- crates/core/src/plugins/bun.rs | 205 ++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 3 deletions(-) diff --git a/crates/core/src/plugins/bun.rs b/crates/core/src/plugins/bun.rs index 45028ba00..4df6989ac 100644 --- a/crates/core/src/plugins/bun.rs +++ b/crates/core/src/plugins/bun.rs @@ -1,18 +1,217 @@ //! Bun plugin. //! //! Detects Bun runtime projects and marks config files as always used. +//! Parses `bunfig.toml` to seed `[test] preload` and top-level `preload` +//! entries as support entry points so they are not reported unused. -use super::Plugin; +use std::path::Path; -const ENABLERS: &[&str] = &["bun-types"]; +use super::{Plugin, PluginResult, config_parser}; + +const ENABLERS: &[&str] = &["bun-types", "@types/bun"]; + +const CONFIG_PATTERNS: &[&str] = &["bunfig.toml"]; const ALWAYS_USED: &[&str] = &["bunfig.toml"]; -const TOOLING_DEPENDENCIES: &[&str] = &["bun-types"]; +const TOOLING_DEPENDENCIES: &[&str] = &["bun-types", "@types/bun"]; define_plugin! { struct BunPlugin => "bun", enablers: ENABLERS, + config_patterns: CONFIG_PATTERNS, always_used: ALWAYS_USED, tooling_dependencies: TOOLING_DEPENDENCIES, + resolve_config(config_path, source, root) { + let mut result = PluginResult::default(); + for path in extract_preload_entries(config_path, source, root) { + result.push_entry_pattern(path); + } + result + }, +} + +fn extract_preload_entries(config_path: &Path, source: &str, root: &Path) -> Vec { + let Ok(value) = source.parse::() else { + return Vec::new(); + }; + + let mut entries = Vec::new(); + + // Top-level `preload = [...]` + if let Some(arr) = value.get("preload").and_then(toml::Value::as_array) { + for item in arr { + if let Some(raw) = item.as_str() + && let Some(path) = config_parser::normalize_config_path(raw, config_path, root) + { + entries.push(path); + } + } + } + + // `[test] preload = [...]` + if let Some(test) = value.get("test").and_then(toml::Value::as_table) { + if let Some(arr) = test.get("preload").and_then(toml::Value::as_array) { + for item in arr { + if let Some(raw) = item.as_str() + && let Some(path) = config_parser::normalize_config_path(raw, config_path, root) + { + entries.push(path); + } + } + } + } + + entries.sort(); + entries.dedup(); + entries +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn types_bun_activates_plugin() { + // @types/bun should be sufficient to enable the bun plugin + let plugin = BunPlugin; + assert!( + plugin.enablers().contains(&"@types/bun"), + "enablers must include @types/bun, got: {:?}", + plugin.enablers() + ); + } + + #[test] + fn bun_types_still_activates_plugin() { + let plugin = BunPlugin; + assert!( + plugin.enablers().contains(&"bun-types"), + "enablers must still include bun-types" + ); + } + + #[test] + fn test_preload_entries_are_entry_patterns() { + let plugin = BunPlugin; + let result = plugin.resolve_config( + Path::new("/repo/bunfig.toml"), + r#" +[test] +preload = ["./src/test-preload.ts"] +"#, + Path::new("/repo"), + ); + + let entries: Vec<&str> = result + .entry_patterns + .iter() + .map(|e| e.pattern.as_str()) + .collect(); + + assert!( + entries.contains(&"src/test-preload.ts"), + "test preload must be an entry pattern, got: {entries:?}" + ); + } + + #[test] + fn top_level_preload_entries_are_entry_patterns() { + let plugin = BunPlugin; + let result = plugin.resolve_config( + Path::new("/repo/bunfig.toml"), + r#" +preload = ["./src/setup.ts", "./src/polyfills.ts"] +"#, + Path::new("/repo"), + ); + + let entries: Vec<&str> = result + .entry_patterns + .iter() + .map(|e| e.pattern.as_str()) + .collect(); + + assert!( + entries.contains(&"src/setup.ts"), + "top-level preload must be an entry pattern, got: {entries:?}" + ); + assert!( + entries.contains(&"src/polyfills.ts"), + "all top-level preload entries must be seeded, got: {entries:?}" + ); + } + + #[test] + fn empty_bunfig_produces_no_entry_patterns() { + let plugin = BunPlugin; + let result = plugin.resolve_config( + Path::new("/repo/bunfig.toml"), + r#" +[install] +exact = true +"#, + Path::new("/repo"), + ); + + assert!( + result.entry_patterns.is_empty(), + "no preload sections must yield no entry patterns, got: {:?}", + result.entry_patterns + ); + } + + #[test] + fn both_preload_sections_are_combined() { + let plugin = BunPlugin; + let result = plugin.resolve_config( + Path::new("/repo/bunfig.toml"), + r#" +preload = ["./src/global-setup.ts"] + +[test] +preload = ["./src/test-setup.ts"] +"#, + Path::new("/repo"), + ); + + let entries: Vec<&str> = result + .entry_patterns + .iter() + .map(|e| e.pattern.as_str()) + .collect(); + + assert!( + entries.contains(&"src/global-setup.ts"), + "top-level preload must be present, got: {entries:?}" + ); + assert!( + entries.contains(&"src/test-setup.ts"), + "test preload must be present, got: {entries:?}" + ); + } + + #[test] + fn invalid_toml_produces_no_entry_patterns() { + let plugin = BunPlugin; + let result = plugin.resolve_config( + Path::new("/repo/bunfig.toml"), + "not valid [[[ toml", + Path::new("/repo"), + ); + + assert!( + result.entry_patterns.is_empty(), + "invalid TOML must produce no entry patterns" + ); + } + + #[test] + fn config_patterns_includes_bunfig_toml() { + let plugin = BunPlugin; + assert!( + plugin.config_patterns().contains(&"bunfig.toml"), + "bunfig.toml must be a config pattern to trigger resolve_config" + ); + } } From 1d901c4187ef5c82b4608b85fc4c5c0af2874181 Mon Sep 17 00:00:00 2001 From: Bart Waardenburg Date: Mon, 1 Jun 2026 23:54:59 +0200 Subject: [PATCH 2/3] review: address fallow-review findings (#847) Collapse the [test] preload nested if-let into a single let-chain to satisfy clippy::collapsible_if under -D warnings, matching the top-level preload block. Drop needless raw-string hashes in the empty-bunfig test and rename the test-section case for naming consistency. Add CHANGELOG and detection.md notes. --- .claude/rules/detection.md | 1 + CHANGELOG.md | 1 + crates/core/src/plugins/bun.rs | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.claude/rules/detection.md b/.claude/rules/detection.md index efbb48dff..6b8f7572c 100644 --- a/.claude/rules/detection.md +++ b/.claude/rules/detection.md @@ -27,6 +27,7 @@ Non-obvious implementation details for each detection feature. These are NOT dis - **SvelteKit remote functions**: SvelteKit 2.27+ remote functions are exported from `*.remote.{ts,js}` files (`export const getPosts = query(...)`, plus `command` / `form` / `prerender`) and invoked through framework-generated client/server bindings that fallow's import graph cannot see. The SvelteKit plugin marks `src/**/*.remote.{ts,js}` as an entry pattern (so a generated-binding-only remote file is not reported as `unused-file`) and adds a `used_exports` rule mapping the same pattern to `["*"]` (so the user-named remote-function exports are credited and never report as `unused-export`, including under `--include-entry-exports` where the entry-point skip is disabled). Remote-function export names are user-defined, so a fixed convention list cannot enumerate them; `"*"` is the conservative credit, mirroring the Storybook story-file precedent. Import edges for dependencies referenced inside remote files are preserved because the file is parsed as an entry point. Scoping is by the `.remote.` double-extension only, so ordinary `src/**/*.ts` files (and a genuinely-unused non-remote module) stay governed by normal detection; `src/lib/server/**` is not excluded because SvelteKit forbids remote files there and crediting a stray one is harmless. See issue #611. - **SvelteKit layout-reset routes**: SvelteKit's layout-reset convention appends an `@` suffix to a page or layout component to break it out of its parent layout chain (`+page@.svelte` resets to the root layout, `+page@named.svelte` resets to a named layout). The SvelteKit plugin marks `src/routes/**/+page@*.svelte` and `src/routes/**/+layout@*.svelte` as entry patterns and adds `used_exports` rules crediting their `default` export, so layout-reset routes stop reporting as `unused-file` and their component default stays credited. The `*` covers both the empty (`@.svelte`) and named (`@named.svelte`) forms; `literal_separator(true)` keeps the glob within a single path segment. Only `.svelte` component files take the suffix: SvelteKit itself throws on `@` in `.ts`/`.js` route files (`+page@.ts` is invalid), and `+error.svelte` / `+server.{ts,js}` have no `@` variant, so no module-file or error/server patterns are added. See issue #791. - **electron-vite `rollupOptions.input` entries**: the Electron plugin activates on `electron` / `electron-builder` / `@electron-forge/cli` / `electron-vite`. When `electron.vite.config.{ts,js,mjs}` is present it now parses each of the `main` / `preload` / `renderer` sections' `build.rollupOptions.input` (string, array, and object-of-values forms) and seeds each value as an entry point, normalized relative to the config file via `normalize_config_path` (so a monorepo subpackage config maps `src/renderer/index.html` to `apps/desktop/src/renderer/index.html`). Object values commonly use path helpers (`resolve(__dirname, 'src/renderer/index.html')`, `join(...)`, `fileURLToPath(...)`, `new URL(..., import.meta.url)`); the shared `config_parser::extract_config_string_or_array` evaluates those through `expression_to_path_string` in string / array / object positions (broadened in issue #604), so the Electron plugin reuses it directly rather than carrying its own extractor. Renderer HTML entries become entry points, so the `` as runtime HTML, not bundled SFC script modules. Fallow now keeps parsing the `src` metadata for span fidelity but no longer emits synthetic imports for Svelte `script src` references, so root-relative browser assets do not become false `unresolved-imports`. Vue external scripts keep their existing graph edges. Thanks [@codingthat](https://github.com/codingthat) for the report. (Closes [#835](https://github.com/fallow-rs/fallow/issues/835).) - **Pinia stores auto-imported by `@pinia/nuxt` are now tracked in the module graph.** Fallow now activates a Pinia plugin on `@pinia/nuxt`, scans direct store files in `stores/` and Nuxt 4 `app/stores/`, and maps value exports named `useStore` to the file that provides them. Pages that call `useUserStore()` without an import now keep `stores/user.ts` reachable and credit the matching export, while sibling helper exports can still report as unused. Nested stores and custom `storesDirs` remain out of scope for the default detector. (Closes [#740](https://github.com/fallow-rs/fallow/issues/740).) - **TanStack Router `routeFileIgnorePattern` warnings now call out JavaScript regex compatibility instead of treating every unsupported pattern as a user typo.** TanStack accepts JavaScript regular expressions, while fallow validates route ignore patterns with Rust's regex engine before matching route file segments. Patterns that use JavaScript-only syntax such as lookahead are still ignored during analysis so the run can continue, but the warning now names `routeFileIgnorePattern`, points at the source config file when known, and explains that the syntax is unsupported by fallow's Rust regex engine. (Refs [#513](https://github.com/fallow-rs/fallow/issues/513).) diff --git a/crates/core/src/plugins/bun.rs b/crates/core/src/plugins/bun.rs index 4df6989ac..f4a478258 100644 --- a/crates/core/src/plugins/bun.rs +++ b/crates/core/src/plugins/bun.rs @@ -50,14 +50,14 @@ fn extract_preload_entries(config_path: &Path, source: &str, root: &Path) -> Vec } // `[test] preload = [...]` - if let Some(test) = value.get("test").and_then(toml::Value::as_table) { - if let Some(arr) = test.get("preload").and_then(toml::Value::as_array) { - for item in arr { - if let Some(raw) = item.as_str() - && let Some(path) = config_parser::normalize_config_path(raw, config_path, root) - { - entries.push(path); - } + if let Some(test) = value.get("test").and_then(toml::Value::as_table) + && let Some(arr) = test.get("preload").and_then(toml::Value::as_array) + { + for item in arr { + if let Some(raw) = item.as_str() + && let Some(path) = config_parser::normalize_config_path(raw, config_path, root) + { + entries.push(path); } } } @@ -92,7 +92,7 @@ mod tests { } #[test] - fn test_preload_entries_are_entry_patterns() { + fn test_section_preload_entries_are_entry_patterns() { let plugin = BunPlugin; let result = plugin.resolve_config( Path::new("/repo/bunfig.toml"), @@ -147,10 +147,10 @@ preload = ["./src/setup.ts", "./src/polyfills.ts"] let plugin = BunPlugin; let result = plugin.resolve_config( Path::new("/repo/bunfig.toml"), - r#" + r" [install] exact = true -"#, +", Path::new("/repo"), ); From 6e895fbbd801e84c980db1914e3e4ba9badbf103 Mon Sep 17 00:00:00 2001 From: Bart Waardenburg Date: Tue, 2 Jun 2026 09:40:40 +0200 Subject: [PATCH 3/3] chore: re-bless agent-file manifest for #847 --- scripts/agent-files.sha256 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/agent-files.sha256 b/scripts/agent-files.sha256 index 56d7f55ff..513438f7a 100644 --- a/scripts/agent-files.sha256 +++ b/scripts/agent-files.sha256 @@ -12,7 +12,7 @@ b40014209809c5a25ce8d4f8b434a77a1fb6d9deb78b9059badd098d39062c13 .claude/agents 7f64808f1721c30f92123e2d363e9b7b5c782b3155571d23a66a58b2b3a9bf26 .claude/rules/cli-crate.md ba291bbb73bed92aa102c43eb6360f1107e1d86af443d3249b800f4292900c07 .claude/rules/code-quality.md 80d9fea77af6652946d6eefe4f30ed35c1df8dcbabeb9d8f47f4308019caddc6 .claude/rules/core-crate.md -d13800a6d81fc4058b2d5c3a4110a146822a4e4851ea888c99d8ba8c69e2ee72 .claude/rules/detection.md +67cb689c834783b48dfabab919c50442e9c7688d67a6a718354f768192c3692f .claude/rules/detection.md 4d34b7f8589ea48e012346d294947867ddaa78fb74263707f6b8951817f357db .claude/rules/extract-crate.md d550b4a609495732c98006423b168437d8b6c435ab7ed360adf15b8a4346fc71 .claude/rules/graph-crate.md 0279743bfb1c788af2f0273ff2dd6bc9c255743813d0213798f7bd4c71002ab9 .claude/rules/lsp-server.md