use crate::context::LintContext;
use crate::diagnostic::Severity;
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_relief::ast::{ElementNode, ExpressionNode, InterpolationNode};
const HYDRATION_MISMATCH_PATTERNS: &[(&str, &str)] = &[
("Math.random", "Random values differ between server and client. Use `useId()` for unique IDs"),
("crypto.randomUUID", "Random UUIDs differ between server and client. Use `useId()` for unique IDs"),
("crypto.getRandomValues", "Random values differ between server and client"),
("Math.floor(Math.random", "Random values differ between server and client. Use `useId()` for unique IDs"),
("uuid()", "Random UUIDs differ between server and client. Use `useId()` for unique IDs"),
("nanoid()", "Random IDs differ between server and client. Use `useId()` for unique IDs"),
("Date.now", "Current time differs between server and client. Consider using `<ClientOnly>` or a fixed timestamp"),
("new Date()", "Current time differs between server and client. Consider using `<ClientOnly>` or a fixed timestamp"),
(".getTime()", "Time values may differ between server and client"),
(".toLocaleString()", "Locale formatting may differ between server and client environments"),
(".toLocaleDateString()", "Locale formatting may differ between server and client environments"),
(".toLocaleTimeString()", "Locale formatting may differ between server and client environments"),
("performance.now", "Performance timing differs between server and client"),
("process.env", "Environment variables may differ between server and client. Ensure they are consistent or use runtime config"),
("import.meta.env", "Environment variables may differ between server and client. Ensure they are consistent or use runtime config"),
];
static META: RuleMeta = RuleMeta {
name: "ssr/no-hydration-mismatch",
description: "Disallow non-deterministic values that cause hydration mismatch",
category: RuleCategory::Recommended,
fixable: false,
default_severity: Severity::Warning,
};
pub struct NoHydrationMismatch;
impl NoHydrationMismatch {
fn check_expression(content: &str) -> Option<(&'static str, &'static str)> {
for (pattern, help) in HYDRATION_MISMATCH_PATTERNS {
if content.contains(pattern) {
return Some((pattern, help));
}
}
None
}
}
impl Rule for NoHydrationMismatch {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn check_interpolation<'a>(
&self,
ctx: &mut LintContext<'a>,
interpolation: &InterpolationNode<'a>,
) {
if !ctx.is_ssr_enabled() {
return;
}
let content = match &interpolation.content {
ExpressionNode::Simple(s) => s.content.as_str(),
ExpressionNode::Compound(_) => return, };
if let Some((pattern, _help)) = Self::check_expression(content) {
ctx.warn_with_help(
ctx.t_fmt("ssr/no-hydration-mismatch.message", &[("pattern", pattern)]),
&interpolation.loc,
ctx.t_fmt("ssr/no-hydration-mismatch.help", &[("pattern", pattern)]),
);
}
}
fn check_directive<'a>(
&self,
ctx: &mut LintContext<'a>,
_element: &ElementNode<'a>,
directive: &vize_relief::ast::DirectiveNode<'a>,
) {
if !ctx.is_ssr_enabled() {
return;
}
if let Some(exp) = &directive.exp {
let content = match exp {
ExpressionNode::Simple(s) => s.content.as_str(),
ExpressionNode::Compound(_) => return, };
if let Some((pattern, _help)) = Self::check_expression(content) {
ctx.warn_with_help(
ctx.t_fmt(
"ssr/no-hydration-mismatch.message-attr",
&[("pattern", pattern)],
),
&directive.loc,
ctx.t_fmt("ssr/no-hydration-mismatch.help", &[("pattern", pattern)]),
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::NoHydrationMismatch;
#[test]
fn test_detects_math_random() {
let content = "items.map(() => Math.random())";
assert!(NoHydrationMismatch::check_expression(content).is_some());
}
#[test]
fn test_detects_date_now() {
let content = "Date.now()";
assert!(NoHydrationMismatch::check_expression(content).is_some());
}
#[test]
fn test_detects_new_date() {
let content = "new Date()";
assert!(NoHydrationMismatch::check_expression(content).is_some());
}
#[test]
fn test_detects_crypto_random() {
let content = "crypto.randomUUID()";
assert!(NoHydrationMismatch::check_expression(content).is_some());
}
#[test]
fn test_allows_safe_code() {
let content = "items.map(item => item.name)";
assert!(NoHydrationMismatch::check_expression(content).is_none());
}
}