mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter) eslint-plugin-next next-script-for-ga (#1934)
This commit is contained in:
parent
0475bcbd92
commit
0f15099f6f
3 changed files with 366 additions and 0 deletions
|
|
@ -272,6 +272,7 @@ mod nextjs {
|
|||
pub mod google_font_display;
|
||||
pub mod google_font_preconnect;
|
||||
pub mod inline_script_id;
|
||||
pub mod next_script_for_ga;
|
||||
}
|
||||
|
||||
oxc_macros::declare_all_lint_rules! {
|
||||
|
|
@ -513,4 +514,5 @@ oxc_macros::declare_all_lint_rules! {
|
|||
nextjs::google_font_display,
|
||||
nextjs::google_font_preconnect,
|
||||
nextjs::inline_script_id,
|
||||
nextjs::next_script_for_ga,
|
||||
}
|
||||
|
|
|
|||
314
crates/oxc_linter/src/rules/nextjs/next_script_for_ga.rs
Normal file
314
crates/oxc_linter/src/rules/nextjs/next_script_for_ga.rs
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
use oxc_ast::{
|
||||
ast::{
|
||||
Expression, JSXAttributeItem, JSXAttributeValue, JSXElementName, JSXExpression,
|
||||
JSXOpeningElement, ObjectProperty, ObjectPropertyKind, PropertyKey,
|
||||
},
|
||||
AstKind,
|
||||
};
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
rule::Rule,
|
||||
utils::{get_string_literal_prop_value, has_jsx_prop_lowercase},
|
||||
AstNode,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("eslint-plugin-next(next-script-for-ga): Prefer `next/script` component when using the inline script for Google Analytics.")]
|
||||
#[diagnostic(severity(warning), help("See https://nextjs.org/docs/messages/next-script-for-ga"))]
|
||||
struct NextScriptForGaDiagnostic(#[label] pub Span);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NextScriptForGa;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
///
|
||||
/// ### Example
|
||||
/// ```javascript
|
||||
/// ```
|
||||
NextScriptForGa,
|
||||
correctness
|
||||
);
|
||||
|
||||
impl Rule for NextScriptForGa {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
let AstKind::JSXOpeningElement(jsx_opening_element) = node.kind() else { return };
|
||||
|
||||
let JSXElementName::Identifier(jsx_opening_element_name) = &jsx_opening_element.name else {
|
||||
return;
|
||||
};
|
||||
|
||||
if jsx_opening_element_name.name.as_str() != "script" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the Alternative async tag is being used to add GA.
|
||||
// https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs
|
||||
if let Some(src_prop) = has_jsx_prop_lowercase(jsx_opening_element, "src") {
|
||||
if let Some(src_prop_value) = get_string_literal_prop_value(src_prop) {
|
||||
if SUPPORTED_SRCS.iter().any(|s| src_prop_value.contains(s)) {
|
||||
ctx.diagnostic(NextScriptForGaDiagnostic(jsx_opening_element_name.span));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if inline script is being used to add GA.
|
||||
// https://developers.google.com/analytics/devguides/collection/analyticsjs#the_google_analytics_tag
|
||||
// https://developers.google.com/tag-manager/quickstart
|
||||
if let Some(danger_value) = get_dangerously_set_inner_html_prop_value(jsx_opening_element) {
|
||||
let Expression::TemplateLiteral(template_literal) = &danger_value.value else { return };
|
||||
let template_literal = template_literal.quasis[0].value.raw.as_str();
|
||||
if SUPPORTED_HTML_CONTENT_URLS.iter().any(|s| template_literal.contains(s)) {
|
||||
ctx.diagnostic(NextScriptForGaDiagnostic(jsx_opening_element_name.span));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SUPPORTED_SRCS: [&str; 2] =
|
||||
["www.google-analytics.com/analytics.js", "www.googletagmanager.com/gtag/js"];
|
||||
|
||||
const SUPPORTED_HTML_CONTENT_URLS: [&str; 2] =
|
||||
["www.google-analytics.com/analytics.js", "www.googletagmanager.com/gtm.js"];
|
||||
|
||||
fn get_dangerously_set_inner_html_prop_value<'a>(
|
||||
jsx_opening_element: &'a JSXOpeningElement<'a>,
|
||||
) -> Option<&'a ObjectProperty<'a>> {
|
||||
let Some(JSXAttributeItem::Attribute(dangerously_set_inner_html_prop)) =
|
||||
has_jsx_prop_lowercase(jsx_opening_element, "dangerouslysetinnerhtml")
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let Some(JSXAttributeValue::ExpressionContainer(object_expr)) =
|
||||
&dangerously_set_inner_html_prop.value
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let JSXExpression::Expression(Expression::ObjectExpression(object_expr)) =
|
||||
&object_expr.expression
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(html_prop) = object_expr.properties.iter().find_map(|prop| {
|
||||
if let ObjectPropertyKind::ObjectProperty(html_prop) = prop {
|
||||
if let PropertyKey::Identifier(html_prop_ident) = &html_prop.key {
|
||||
if html_prop_ident.name == "__html" {
|
||||
Some(html_prop)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return Some(html_prop);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
r#"import Script from 'next/script'
|
||||
|
||||
export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
<Script id="google-analytics">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){window.dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'GA_MEASUREMENT_ID');
|
||||
`}
|
||||
</Script>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}"#,
|
||||
r#"import Script from 'next/script'
|
||||
|
||||
export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<Script id="google-analytics">
|
||||
{`(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-XXXXX-Y', 'auto');
|
||||
ga('send', 'pageview');
|
||||
})`}
|
||||
</Script>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}"#,
|
||||
r#"import Script from 'next/script'
|
||||
|
||||
export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<Script id="google-analytics">
|
||||
{`window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
|
||||
ga('create', 'UA-XXXXX-Y', 'auto');
|
||||
ga('send', 'pageview');
|
||||
})`}
|
||||
</Script>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}"#,
|
||||
r"export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<script dangerouslySetInnerHTML={{}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}",
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
r"
|
||||
export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}' />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${GA_TRACKING_ID}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`,
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}",
|
||||
r"
|
||||
export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1> qqq
|
||||
{/* Google Tag Manager - Global base code */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer', '${GTM_ID}');
|
||||
`,
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}",
|
||||
r"
|
||||
export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<script dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-XXXXX-Y', 'auto');
|
||||
ga('send', 'pageview');
|
||||
`,
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}",
|
||||
r"
|
||||
export class Blah extends Head {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<script dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
|
||||
ga('create', 'UA-XXXXX-Y', 'auto');
|
||||
ga('send', 'pageview');
|
||||
`,
|
||||
}}/>
|
||||
<script async src='https://www.google-analytics.com/analytics.js'></script>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}",
|
||||
r"
|
||||
export class Blah extends Head {
|
||||
createGoogleAnalyticsMarkup() {
|
||||
return {
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-148481588-2');`,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
<script dangerouslySetInnerHTML={this.createGoogleAnalyticsMarkup()} />
|
||||
<script async src='https://www.google-analytics.com/analytics.js'></script>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}",
|
||||
];
|
||||
|
||||
Tester::new_without_config(NextScriptForGa::NAME, pass, fail).test_and_snapshot();
|
||||
}
|
||||
50
crates/oxc_linter/src/snapshots/next_script_for_ga.snap
Normal file
50
crates/oxc_linter/src/snapshots/next_script_for_ga.snap
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
expression: next_script_for_ga
|
||||
---
|
||||
⚠ eslint-plugin-next(next-script-for-ga): Prefer `next/script` component when using the inline script for Google Analytics.
|
||||
╭─[next_script_for_ga.tsx:6:1]
|
||||
6 │ <h1>Hello title</h1>
|
||||
7 │ <script async src='https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}' />
|
||||
· ──────
|
||||
8 │ <script
|
||||
╰────
|
||||
help: See https://nextjs.org/docs/messages/next-script-for-ga
|
||||
|
||||
⚠ eslint-plugin-next(next-script-for-ga): Prefer `next/script` component when using the inline script for Google Analytics.
|
||||
╭─[next_script_for_ga.tsx:7:1]
|
||||
7 │ {/* Google Tag Manager - Global base code */}
|
||||
8 │ <script
|
||||
· ──────
|
||||
9 │ dangerouslySetInnerHTML={{
|
||||
╰────
|
||||
help: See https://nextjs.org/docs/messages/next-script-for-ga
|
||||
|
||||
⚠ eslint-plugin-next(next-script-for-ga): Prefer `next/script` component when using the inline script for Google Analytics.
|
||||
╭─[next_script_for_ga.tsx:6:1]
|
||||
6 │ <h1>Hello title</h1>
|
||||
7 │ <script dangerouslySetInnerHTML={{
|
||||
· ──────
|
||||
8 │ __html: `
|
||||
╰────
|
||||
help: See https://nextjs.org/docs/messages/next-script-for-ga
|
||||
|
||||
⚠ eslint-plugin-next(next-script-for-ga): Prefer `next/script` component when using the inline script for Google Analytics.
|
||||
╭─[next_script_for_ga.tsx:13:1]
|
||||
13 │ }}/>
|
||||
14 │ <script async src='https://www.google-analytics.com/analytics.js'></script>
|
||||
· ──────
|
||||
15 │ </div>
|
||||
╰────
|
||||
help: See https://nextjs.org/docs/messages/next-script-for-ga
|
||||
|
||||
⚠ eslint-plugin-next(next-script-for-ga): Prefer `next/script` component when using the inline script for Google Analytics.
|
||||
╭─[next_script_for_ga.tsx:17:1]
|
||||
17 │ <script dangerouslySetInnerHTML={this.createGoogleAnalyticsMarkup()} />
|
||||
18 │ <script async src='https://www.google-analytics.com/analytics.js'></script>
|
||||
· ──────
|
||||
19 │ </div>
|
||||
╰────
|
||||
help: See https://nextjs.org/docs/messages/next-script-for-ga
|
||||
|
||||
|
||||
Loading…
Reference in a new issue