use crate::tests::{parse_at_path, parse_ts};
use crate::visitor::ROUTE_LOADER_DATA_OBJECT;
fn key_names(info: &crate::ModuleInfo) -> Vec<String> {
info.load_return_keys
.iter()
.map(|k| k.name.clone())
.collect()
}
fn has_route_data_access(info: &crate::ModuleInfo, member: &str) -> bool {
info.member_accesses
.iter()
.any(|access| access.object == ROUTE_LOADER_DATA_OBJECT && access.member == member)
}
fn has_route_data_whole_use(info: &crate::ModuleInfo) -> bool {
info.whole_object_uses
.iter()
.any(|name| name == ROUTE_LOADER_DATA_OBJECT)
}
#[test]
fn harvests_object_literal_keys_from_arrow_load() {
let info = parse_at_path(
"src/routes/+page.ts",
"export const load = async () => { return { used: 1, dead: 2 }; };",
);
assert_eq!(key_names(&info), vec!["used", "dead"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn harvests_keys_from_async_function_load() {
let info = parse_at_path(
"src/routes/+page.server.ts",
"export async function load() { return { a: 1, b: 2 }; }",
);
assert_eq!(key_names(&info), vec!["a", "b"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn harvests_through_satisfies_pageload() {
let info = parse_at_path(
"src/routes/+page.ts",
"export const load = (async () => ({ x: 1 })) satisfies PageLoad;",
);
assert_eq!(key_names(&info), vec!["x"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn abstains_on_spread_return() {
let info = parse_at_path(
"src/routes/+page.ts",
"export const load = async () => { return { ...base, extra: 1 }; };",
);
assert!(info.load_return_keys.is_empty());
assert!(info.has_unharvestable_load);
}
#[test]
fn abstains_on_non_object_return() {
let info = parse_at_path(
"src/routes/+page.ts",
"export const load = async () => { return makeData(); };",
);
assert!(info.load_return_keys.is_empty());
assert!(info.has_unharvestable_load);
}
#[test]
fn abstains_on_multi_return_body() {
let info = parse_at_path(
"src/routes/+page.ts",
"export async function load(x) { if (x) { return { a: 1 }; } return { b: 2 }; }",
);
assert!(info.has_unharvestable_load);
}
#[test]
fn abstains_on_computed_key() {
let info = parse_at_path(
"src/routes/+page.ts",
"export const load = async () => { return { [k]: 1 }; };",
);
assert!(info.has_unharvestable_load);
}
#[test]
fn abstains_on_reexported_load() {
let info = parse_at_path("src/routes/+page.ts", "export { load } from './shared';");
assert!(info.load_return_keys.is_empty());
assert!(info.has_unharvestable_load);
}
#[test]
fn non_page_file_harvests_nothing() {
let info = parse_at_path(
"src/lib/helpers.ts",
"export const load = async () => { return { a: 1 }; };",
);
assert!(info.load_return_keys.is_empty());
assert!(!info.has_unharvestable_load);
}
#[test]
fn sveltekit_page_ignores_route_loader_exports() {
let info = parse_at_path(
"src/routes/+page.ts",
"export const loader = async () => ({ routeOnly: 1 }); export const load = async () => ({ pageOnly: 1 });",
);
assert_eq!(key_names(&info), vec!["pageOnly"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn harvests_object_literal_keys_from_react_router_loader() {
let info = parse_at_path(
"app/routes/home.tsx",
"export async function loader() { return { used: 1, dead: 2 }; }",
);
assert_eq!(key_names(&info), vec!["used", "dead"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn conventional_route_ignores_sveltekit_load_exports() {
let info = parse_at_path(
"app/routes/home.tsx",
"export const load = async () => ({ pageOnly: 1 }); export const loader = async () => ({ routeOnly: 1 });",
);
assert_eq!(key_names(&info), vec!["routeOnly"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn harvests_object_literal_keys_from_client_loader() {
let info = parse_at_path(
"app/routes/home.tsx",
"export const clientLoader = async () => ({ local: 1, stale: 2 });",
);
assert_eq!(key_names(&info), vec!["local", "stale"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn harvests_remix_json_return_keys() {
let info = parse_at_path(
"app/routes/home.tsx",
"export const loader = async () => json({ used: 1, dead: 2 });",
);
assert_eq!(key_names(&info), vec!["used", "dead"]);
assert!(!info.has_unharvestable_load);
}
#[test]
fn route_loader_data_destructure_records_key_reads() {
let info = parse_at_path(
"app/routes/home.tsx",
"import { useLoaderData } from 'react-router'; const { used } = useLoaderData() as Data;",
);
assert!(has_route_data_access(&info, "used"));
}
#[test]
fn route_loader_data_alias_member_records_key_reads() {
let info = parse_at_path(
"app/routes/home.tsx",
"import { useLoaderData as useData } from '@remix-run/react'; const data = useData(); console.log(data.used);",
);
assert!(has_route_data_access(&info, "used"));
}
#[test]
fn route_loader_data_namespace_import_records_key_reads() {
let info = parse_at_path(
"app/routes/home.tsx",
"import * as Router from 'react-router-dom'; const { used } = Router.useLoaderData();",
);
assert!(has_route_data_access(&info, "used"));
}
#[test]
fn route_loader_data_rest_destructure_marks_whole_use() {
let info = parse_at_path(
"app/routes/home.tsx",
"import { useLoaderData } from 'react-router'; const { used, ...rest } = useLoaderData();",
);
assert!(
info.whole_object_uses
.iter()
.any(|name| name == ROUTE_LOADER_DATA_OBJECT)
);
}
#[test]
fn route_loader_data_spread_marks_synthetic_whole_use() {
let info = parse_at_path(
"app/routes/home.tsx",
"import { useLoaderData as useData } from '@remix-run/react'; const data = useData(); const copy = { ...data };",
);
assert!(
info.whole_object_uses
.iter()
.any(|name| name == ROUTE_LOADER_DATA_OBJECT),
"spreading a tracked route-loader binding must preserve its route-loader identity"
);
}
#[test]
fn conventional_loader_data_for_in_marks_synthetic_whole_use() {
let info = parse_at_path(
"app/routes/home.tsx",
"function Home({ loaderData }) { for (const key in loaderData) console.log(key); }",
);
assert!(
info.whole_object_uses
.iter()
.any(|name| name == ROUTE_LOADER_DATA_OBJECT),
"iterating conventional loaderData must preserve its route-loader identity"
);
}
#[test]
fn route_loader_data_object_keys_and_values_mark_synthetic_whole_use() {
for method in ["keys", "values"] {
let source = format!(
"import {{ useLoaderData }} from 'react-router'; const data = useLoaderData(); Object.{method}(data);"
);
let info = parse_at_path("app/routes/home.tsx", &source);
assert!(
has_route_data_whole_use(&info),
"Object.{method} on route-loader data must preserve its route-loader identity"
);
}
}
#[test]
fn route_loader_data_dynamic_computed_access_marks_synthetic_whole_use() {
let info = parse_at_path(
"app/routes/home.tsx",
"import { useLoaderData } from 'react-router'; const data = useLoaderData(); console.log(data[key]);",
);
assert!(
has_route_data_whole_use(&info),
"a dynamic route-loader key can consume any returned key"
);
}
#[test]
fn route_loader_data_alias_assignment_marks_synthetic_whole_use() {
let info = parse_at_path(
"app/routes/home.tsx",
"import { useLoaderData } from 'react-router'; const data = useLoaderData(); const alias = data; consume(alias);",
);
assert!(
has_route_data_whole_use(&info),
"assigning route-loader data to an alias is an opaque whole-object use"
);
}
#[test]
fn route_loader_data_call_argument_marks_synthetic_whole_use() {
let info = parse_at_path(
"app/routes/home.tsx",
"import { useLoaderData } from 'react-router'; const data = useLoaderData(); consume(data);",
);
assert!(
has_route_data_whole_use(&info),
"passing route-loader data to an unknown call can consume any returned key"
);
}
#[test]
fn ordinary_whole_object_uses_do_not_mark_route_loader_abstention() {
let info = parse_at_path(
"app/routes/home.tsx",
"const ordinary = { used: 1 }; Object.values(ordinary); const copy = { ...ordinary }; for (const key in ordinary) console.log(ordinary[key], copy);",
);
assert!(
!has_route_data_whole_use(&info),
"ordinary whole-object uses must not gain route-loader identity"
);
}
#[test]
fn whole_data_use_script_const_assignment() {
let info = parse_ts("const d = data;");
assert!(
info.has_load_data_whole_use,
"const X = data is a whole use"
);
}
#[test]
fn whole_data_use_function_call_arg() {
let info = parse_ts("someFn(data);");
assert!(info.has_load_data_whole_use, "fn(data) is a whole use");
}
#[test]
fn whole_data_use_spread_call_arg() {
let info = parse_ts("someFn(...data);");
assert!(info.has_load_data_whole_use, "fn(...data) is a whole use");
}
#[test]
fn whole_data_use_destructure_assignment() {
let info = parse_ts("let guests; ({ guests } = data);");
assert!(
info.has_load_data_whole_use,
"({{ x }} = data) destructure-assignment is a whole-data use"
);
}
#[test]
fn member_access_on_data_is_not_a_whole_use() {
let info = parse_ts("const x = data.title;");
assert!(
!info.has_load_data_whole_use,
"data.x member access must not set the whole-use flag"
);
}
#[test]
fn non_data_binding_does_not_trip_whole_use() {
let info = parse_ts("const d = other; fn(other);");
assert!(
!info.has_load_data_whole_use,
"only the `data` binding is name-gated for the whole-use signal"
);
}