diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5696a0f34..582f8ac04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- **Quoted globs in package.json scripts are now registered as entry points.** A test script using the idiomatic `node --test --import tsx 'src/**/*.test.ts'` form captured its argument as the literal `'src/**/*.test.ts'` with the surrounding single quotes intact, so the compiled entry pattern matched zero files and the test files (and their test-only helper modules) surfaced as false `unused-file` / `unused-export` findings. The script tokenizer now strips a matching pair of surrounding single or double quotes from each token, while leaving tokens with internal quotes (such as `can't`) unchanged. Unquoted globs continue to work as before. (Closes [#841](https://github.com/fallow-rs/fallow/issues/841).)
- **Svelte markup `` 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/scripts/mod.rs b/crates/core/src/scripts/mod.rs
index 7f2415d85..7f57ee0c5 100644
--- a/crates/core/src/scripts/mod.rs
+++ b/crates/core/src/scripts/mod.rs
@@ -293,9 +293,27 @@ fn extract_args_for_binary(
(file_args, config_args)
}
+/// Strip a matching pair of surrounding single or double quotes from a token.
+///
+/// Only strips when the token both starts and ends with the same quote character.
+/// A token with a single internal quote (e.g. `can't`) is returned unchanged.
+fn strip_surrounding_quotes(token: &str) -> &str {
+ if token.len() >= 2 {
+ let first = token.as_bytes()[0];
+ let last = token.as_bytes()[token.len() - 1];
+ if (first == b'\'' || first == b'"') && first == last {
+ return &token[1..token.len() - 1];
+ }
+ }
+ token
+}
+
/// Parse a single command segment (after splitting on shell operators).
fn parse_command_segment(segment: &str) -> Option {
- let tokens: Vec<&str> = segment.split_whitespace().collect();
+ let tokens: Vec<&str> = segment
+ .split_whitespace()
+ .map(strip_surrounding_quotes)
+ .collect();
if tokens.is_empty() {
return None;
}
@@ -1327,6 +1345,54 @@ mod tests {
assert!(!result.used_packages.contains("server"));
}
+ #[test]
+ fn node_test_quoted_glob_strips_quotes() {
+ // Regression test for issue #841: quoted glob args kept their quotes,
+ // causing looks_like_file_path to reject them and the entry pattern to
+ // match zero files.
+ let cmds = parse_script("node --test --import tsx 'src/**/*.test.ts'");
+ assert_eq!(cmds.len(), 1);
+ assert_eq!(cmds[0].binary, "node");
+ // The surrounding quotes must be stripped from the glob.
+ assert!(
+ cmds[0].file_args.contains(&"src/**/*.test.ts".to_string()),
+ "expected unquoted glob in file_args, got: {:?}",
+ cmds[0].file_args
+ );
+ assert!(
+ !cmds[0]
+ .file_args
+ .iter()
+ .any(|f| f.starts_with('\'') || f.ends_with('\'')),
+ "file_args must not contain surrounding single quotes"
+ );
+ }
+
+ #[test]
+ fn node_test_unquoted_glob_still_works() {
+ // Unquoted globs must continue to be extracted correctly.
+ let cmds = parse_script("node --test src/**/*.test.ts");
+ assert_eq!(cmds.len(), 1);
+ assert_eq!(cmds[0].binary, "node");
+ assert!(cmds[0].file_args.contains(&"src/**/*.test.ts".to_string()));
+ }
+
+ #[test]
+ fn token_with_internal_single_quote_unchanged() {
+ // A token whose quote is internal (not surrounding) must not be mangled.
+ // Use a file arg that contains an internal apostrophe but is not shell-quoted.
+ // We exercise strip_surrounding_quotes directly via a known non-file-path
+ // context: confirm parse_script does not mangle such a token.
+ assert_eq!(super::strip_surrounding_quotes("can't"), "can't");
+ assert_eq!(super::strip_surrounding_quotes("'quoted'"), "quoted");
+ assert_eq!(super::strip_surrounding_quotes("\"quoted\""), "quoted");
+ assert_eq!(
+ super::strip_surrounding_quotes("'mismatched\""),
+ "'mismatched\""
+ );
+ assert_eq!(super::strip_surrounding_quotes(""), "");
+ }
+
mod proptests {
use super::*;
use proptest::prelude::*;