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 45028ba00..f4a478258 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)
+ && 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_section_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"
+ );
+ }
}
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