Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/rules/detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<script src>` source trees behind multi-window renderers stop reporting as `unused-files`. The static main/preload fallback globs also widen to `{ts,tsx,js,jsx,mts,mjs}`. Crediting is scoped to declared inputs (and per-section function-wrapped configs such as `renderer: defineConfig({...})` are out of scope, matching the Vite plugin's plain-object nesting requirement), so a renderer source file linked from no declared entry is still reported. See issue #600.
- **Bun `bunfig.toml` preload entries**: the `bun` plugin now activates on `@types/bun` in addition to `bun-types` (activation reads declared `package.json` deps, and `@types/bun` is the canonical Bun types package even though it transitively depends on `bun-types`). When `bunfig.toml` is present the plugin parses both the top-level `preload = [...]` array and the `[test] preload = [...]` array and seeds each referenced file as an entry pattern, normalized relative to the config file via `config_parser::normalize_config_path` (so a `./src/test-preload.ts` reference maps to `src/test-preload.ts`). Bun loads these files before every run / test without an inbound import edge, so without the seed they report as `unused-file`. Invalid TOML and non-array / non-string entries are skipped silently; entries are deduped. Both `bun-types` and `@types/bun` are credited as tooling dependencies. See issue #847.
- **Browser extension manifest entrypoints**: the `browser-extension` plugin activates without package dependencies by inspecting deduped `manifest.json` candidates at the project root and discovered-file ancestor directories. Activation requires numeric `manifest_version` 2 or 3 plus at least one extension runtime key (`background`, `content_scripts`, action/page/browser action, options, side panel, devtools, or `web_accessible_resources`) so ordinary PWA manifests stay inert. Config resolution seeds MV3 `background.service_worker`, MV2 `background.scripts`, `content_scripts[].js/css`, popup/options/devtools/side-panel HTML, MV2 string-array `web_accessible_resources`, and MV3 object `resources` arrays as runtime entry patterns, normalized relative to the manifest file. Remote/scheme URLs, protocol-relative URLs, empty strings, and parent-directory escapes are skipped; leading `/foo.js` is treated as extension-root-relative. HTML entries rely on the existing HTML asset extractor to keep their `<script>` and stylesheet graph reachable. See issue #616.
- **WXT entrypoints and config modules**: the `wxt` plugin activates from `wxt`, any non-empty `@wxt-dev/*` dependency, or a root `wxt.config.{ts,js,mts,mjs,cjs,cts}` file. It keeps WXT config files always used, credits their default export under `--include-entry-exports`, marks documented single-file `entrypoints/*` and directory `entrypoints/**/index.*` entrypoints (plus `src/entrypoints` variants) as runtime roots, and parses static `modules: [...]` strings from config as referenced dependencies. Package subpaths are normalized (`@wxt-dev/i18n/module` credits `@wxt-dev/i18n`); local, absolute, protocol, empty, and backslash-containing specifiers are ignored. The entry globs intentionally do not credit every descendant under an entrypoint directory, so unimported helper siblings stay reportable. See issue #612.
- **@sanity/pkg-utils build configs**: the `pkg-utils` plugin activates ONLY on an exact `@sanity/pkg-utils` dependency (not the broad `@sanity/` prefix the CMS `sanity` plugin uses, so a plain `@sanity/client` consumer does not get its build config auto-credited). `@sanity/pkg-utils` discovers `package.config.{ts,js,mts,mjs,cts,cjs}` (its build config) and the optional `package.bundle.{ts,js,mts,mjs,cts,cjs}` (Vite bundle config) on a filename convention rather than importing them from source, so without the plugin they surface as `unused-file`. The plugin marks both filenames as always-used at the project root AND nested (each pattern listed in bare and `**/`-prefixed form, because `always_used` matching uses `literal_separator(true)` with no automatic prefix; mirrors the `varlock` / `wuchale` precedent), covering monorepo `packages/<pkg>/package.config.ts` layouts, and credits `@sanity/pkg-utils` as a tooling dependency. It does not seed library source entries (`src/_exports/**`): the `exports.source` condition resolution plus the workspace-package fallback already keep them reachable, so parsing `package.bundle.ts`'s `build.lib.entry` would be redundant. The credit is scoped to the two build-config filenames, so an unreferenced ordinary source file still reports. Lets pkg-utils projects drop the manual `entry` array for these files (adopter: sanity-io/sdk).
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Bun `bunfig.toml` preload files are no longer reported as unused, and the Bun plugin now activates on `@types/bun`.** Two gaps combined to flag a Bun test preload file as `unused-file`: the plugin's enabler set was `bun-types` only, so a project declaring the canonical `@types/bun` types package never activated the plugin (activation reads declared `package.json` deps, not transitive `node_modules`), and even when active the plugin only marked `bunfig.toml` itself as always-used without parsing its preload arrays. The plugin now also activates on `@types/bun`, and when `bunfig.toml` is present it parses both the top-level `preload = [...]` array and the `[test] preload = [...]` array, seeding each referenced file as an entry point (normalized relative to the config file) so the files Bun loads before every run / test stay reachable. Invalid TOML and non-string entries are skipped. (Closes [#847](https://github.com/fallow-rs/fallow/issues/847).)
- **Svelte markup `<script src>` tags no longer surface as unresolved imports.** Svelte treats tags such as `<svelte:head><script src="/some-lib.min.js" async></script></svelte:head>` 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 `use<Name>Store` 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).)
Expand Down
205 changes: 202 additions & 3 deletions crates/core/src/plugins/bun.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
let Ok(value) = source.parse::<toml::Table>() 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"
);
}
}
2 changes: 1 addition & 1 deletion scripts/agent-files.sha256
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading