@grafana/react-detect 0.1.0 → 0.2.0-canary.2358.20374737796.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,25 +4,25 @@ const PATTERN_DEFINITIONS = {
4
4
  impactLevel: "critical",
5
5
  description: "React internals __SECRET_INTERNALS renamed to _DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE",
6
6
  fix: {
7
- description: "Check the list of libraries depending on React internals. Alternatively externalise react/jsx-runtime in webpack config. Your plugin will only be compatible with Grafana >=12.3.0"
7
+ description: "Externalise react/jsx-runtime in webpack config. Your plugin will only be compatible with Grafana >=12.3.0"
8
8
  },
9
9
  link: "https://react.dev/blog/2024/04/25/react-19-upgrade-guide#libraries-depending-on-react-internals-may-block-upgrades"
10
10
  },
11
11
  jsxRuntimeImport: {
12
12
  severity: "renamed",
13
13
  impactLevel: "critical",
14
- description: "Dependency bundles react/jsx-runtime which will break with React 19 due to `__SECRET_INTERNALS` being renamed",
14
+ description: "Bundling react/jsx-runtime will break with React 19 due to `__SECRET_INTERNALS...` renamed to `_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE`",
15
15
  fix: {
16
16
  description: "Externalize react/jsx-runtime in webpack config. Your plugin will only be compatible with Grafana >=12.3.0"
17
17
  },
18
- link: "https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config"
18
+ link: "https://react.dev/blog/2024/04/25/react-19-upgrade-guide#libraries-depending-on-react-internals-may-block-upgrades"
19
19
  },
20
20
  defaultProps: {
21
21
  severity: "removed",
22
22
  impactLevel: "critical",
23
- description: "Default props removed in favour of function components",
23
+ description: "Default props removed from function components",
24
24
  fix: {
25
- description: "Use ES6 default parameters",
25
+ description: "Use ES6 default parameters or pass default values to dependencies to keep consistent behavior",
26
26
  before: 'MyComponent.defaultProps = { value: "test" }',
27
27
  after: 'function MyComponent({ value = "test" }) { ... }'
28
28
  },
@@ -62,7 +62,7 @@ const PATTERN_DEFINITIONS = {
62
62
  impactLevel: "critical",
63
63
  description: "String refs removed in React 19",
64
64
  fix: {
65
- description: "Run the codemod to migrate to ref callbacks.",
65
+ description: "Run the codemod to migrate to ref callbacks",
66
66
  before: "npx codemod@latest react/19/replace-string-ref"
67
67
  },
68
68
  link: "https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-string-refs"
@@ -70,7 +70,7 @@ const PATTERN_DEFINITIONS = {
70
70
  findDOMNode: {
71
71
  severity: "removed",
72
72
  impactLevel: "critical",
73
- description: "findDOMNode removed from React and ReactDOM in React 19",
73
+ description: "findDOMNode removed from ReactDOM in React 19",
74
74
  fix: {
75
75
  description: "Replace ReactDOM.findDOMNode with DOM refs",
76
76
  before: "const node = findDOMNode(this);",
@@ -94,7 +94,7 @@ const PATTERN_DEFINITIONS = {
94
94
  impactLevel: "critical",
95
95
  description: "ReactDOM.render removed in React 19 (use createRoot)",
96
96
  fix: {
97
- description: "Use createRoot instead.",
97
+ description: "Use createRoot instead",
98
98
  before: 'ReactDOM.render(<App />, document.getElementById("root"));',
99
99
  after: 'const root = createRoot(document.getElementById("root")); root.render(<App />);'
100
100
  },
@@ -105,7 +105,7 @@ const PATTERN_DEFINITIONS = {
105
105
  impactLevel: "critical",
106
106
  description: "ReactDOM.unmountComponentAtNode removed in React 19",
107
107
  fix: {
108
- description: "Use createRoot instead.",
108
+ description: "Use createRoot instead",
109
109
  before: "ReactDOM.unmountComponentAtNode(container);",
110
110
  after: "const root = createRoot(container); root.unmount();"
111
111
  },
@@ -1,28 +1,33 @@
1
+ import { isExternal } from '../utils/dependencies.js';
1
2
  import { output } from '../utils/output.js';
2
3
 
4
+ const summaryMsg = [
5
+ `We strongly recommend testing your plugin using the React 19 Grafana docker image: ${output.formatCode("grafana/grafana:12.4.0-react19")}`,
6
+ ...output.bulletList([
7
+ `Start the server with ${output.formatCode("GRAFANA_VERSION=12.4.0-react19 GRAFANA_IMAGE=grafana docker compose up --build")} flag and manually test your plugin.`,
8
+ "Run any e2e tests to try and catch any potential issues.",
9
+ "Manually test your plugin in the browser. Look for any console warnings or errors related to React."
10
+ ]),
11
+ "",
12
+ "For more information, please refer to:",
13
+ `React 19 blog post: ${output.formatUrl("https://react.dev/blog/2024/04/25/react-19-upgrade-guide")}.`,
14
+ `Grafana developer documentation: ${output.formatUrl("https://grafana.com/developers/plugin-tools/set-up/set-up-docker")}`
15
+ ];
3
16
  function consoleReporter(results) {
4
17
  if (results.summary.totalIssues === 0) {
5
18
  output.success({
6
- title: "No React 19 breaking changes detected!",
19
+ title: "No React 19 breaking changes detected.",
7
20
  body: [
8
21
  `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
9
22
  "Good news! Your plugin appears to be compatible with React 19.",
10
23
  "",
11
- "Even so we recommend testing your plugin using the following steps:",
12
- ...output.bulletList([
13
- `1. Use the React 19 Grafana docker image: ${output.formatCode("grafana/grafana-enterprise-dev:10.0.0-255911")}`,
14
- "2. Start the server and manually test your plugin."
15
- ]),
16
- "",
17
- `For more information, please refer to the React 19 blog post: ${output.formatUrl("https://react.dev/blog/2024/04/25/react-19-upgrade-guide")}.`,
18
- "",
19
- "Thank you for using Grafana!"
24
+ ...summaryMsg
20
25
  ]
21
26
  });
22
27
  return;
23
28
  }
24
29
  output.error({
25
- title: "React 19 breaking changes detected!",
30
+ title: "React 19 breaking changes detected.",
26
31
  body: [
27
32
  `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
28
33
  "Your plugin appears to be incompatible with React 19. Note that this tool can give false positives, please review the issues carefully."
@@ -66,7 +71,7 @@ function consoleReporter(results) {
66
71
  title: "Dependency issues",
67
72
  body: [
68
73
  "The following issues were found in bundled dependencies.",
69
- "We recommend checking for dependency updates which are compatible with React 19."
74
+ "Please check for dependency updates that are compatible with React 19."
70
75
  ],
71
76
  withPrefix: false
72
77
  });
@@ -74,15 +79,26 @@ function consoleReporter(results) {
74
79
  for (const [pkgName, issues] of Object.entries(groupedByPackage)) {
75
80
  const uniquePatterns = new Set(
76
81
  issues.map(
77
- (issue) => `${issue.problem}. ${issue.fix.description}. Further information: ${output.formatUrl(issue.link)}`
82
+ (issue) => `${issue.problem}. ${issue.fix.description}.
83
+ Further information: ${output.formatUrl(issue.link)}`
78
84
  )
79
85
  );
80
86
  const uniqueFileLocations = new Set(issues.map((issue) => issue.location.file));
81
87
  const fileLocationList = output.bulletList(Array.from(uniqueFileLocations));
82
88
  const patternInfoList = output.bulletList(Array.from(uniquePatterns));
89
+ const rootDependencies = issues.filter(
90
+ (issue) => issue.rootDependency !== void 0 && issue.rootDependency !== pkgName && !isExternal(issue.rootDependency)
91
+ ).map((issue) => issue.rootDependency);
92
+ const uniqueDependencies = new Set(rootDependencies.filter((dep) => dep !== void 0));
93
+ const body = ["issues found:", ...patternInfoList, ""];
94
+ if (uniqueDependencies.size) {
95
+ body.push(`Bundled by dependenc${uniqueDependencies.size > 1 ? "ies" : "y"}:`);
96
+ body.push(...output.bulletList(Array.from(uniqueDependencies)));
97
+ }
98
+ body.push("found in:", ...fileLocationList);
83
99
  output.error({
84
100
  title: `\u{1F4E6} ${pkgName}`,
85
- body: ["issues found:", ...patternInfoList, "", "found in:", ...fileLocationList],
101
+ body,
86
102
  withPrefix: false
87
103
  });
88
104
  output.addHorizontalLine("red");
@@ -90,17 +106,7 @@ function consoleReporter(results) {
90
106
  }
91
107
  output.error({
92
108
  title: "Next steps",
93
- body: [
94
- "We recommend testing your plugin using the following steps to ensure it is compatible with React 19:",
95
- ...output.bulletList([
96
- `1. Use the React 19 Grafana docker image: ${output.formatCode("grafana/grafana-enterprise-dev:10.0.0-255911")}`,
97
- "2. Start the server and manually test your plugin."
98
- ]),
99
- "",
100
- `For more information, please refer to the React 19 blog post: ${output.formatUrl("https://react.dev/blog/2024/04/25/react-19-upgrade-guide")}.`,
101
- "",
102
- "Thank you for using Grafana!"
103
- ],
109
+ body: summaryMsg,
104
110
  withPrefix: false
105
111
  });
106
112
  }
package/dist/results.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getPattern } from './patterns/definitions.js';
2
- import { getPluginJson } from './utils/plugin.js';
2
+ import { getPluginJson, hasExternalisedJsxRuntime } from './utils/plugin.js';
3
3
  import path from 'node:path';
4
4
 
5
5
  function generateAnalysisResults(matches, pluginRoot, depContext) {
@@ -46,6 +46,7 @@ function generateAnalysisResults(matches, pluginRoot, depContext) {
46
46
  };
47
47
  }
48
48
  function filterMatches(matches) {
49
+ const externalisedJsxRuntime = hasExternalisedJsxRuntime();
49
50
  const filtered = matches.filter((match) => {
50
51
  if (match.type === "source" && (match.confidence === "none" || match.confidence === "unknown")) {
51
52
  return false;
@@ -59,6 +60,9 @@ function filterMatches(matches) {
59
60
  if (match.type === "dependency" && match.pattern === "__SECRET_INTERNALS" && match.packageName === "react" && (match.sourceFile.includes("jsx-runtime") || match.sourceFile.includes("jsx-dev-runtime"))) {
60
61
  return false;
61
62
  }
63
+ if (match.type === "dependency" && (match.pattern === "jsxRuntimeImport" || match.pattern === "__SECRET_INTERNALS")) {
64
+ return !externalisedJsxRuntime;
65
+ }
62
66
  return true;
63
67
  });
64
68
  return filtered;
@@ -93,5 +93,36 @@ class DependencyContext {
93
93
  return new Map([...this.dependencies, ...this.devDependencies]);
94
94
  }
95
95
  }
96
+ const GRAFANA_EXTERNALS = [
97
+ "@emotion/css",
98
+ "@emotion/react",
99
+ "@grafana/data",
100
+ "@grafana/runtime",
101
+ "@grafana/slate-react",
102
+ "@grafana/ui",
103
+ "angular",
104
+ "d3",
105
+ "emotion",
106
+ "i18next",
107
+ "jquery",
108
+ "lodash",
109
+ "moment",
110
+ "prismjs",
111
+ "react-dom",
112
+ "react-redux",
113
+ "react-router-dom",
114
+ "react-router",
115
+ "react",
116
+ "redux",
117
+ "rxjs",
118
+ "slate-plain-serializer",
119
+ "slate"
120
+ ];
121
+ function isExternal(packageName) {
122
+ if (GRAFANA_EXTERNALS.includes(packageName)) {
123
+ return true;
124
+ }
125
+ return GRAFANA_EXTERNALS.some((external) => packageName.startsWith(external + "/"));
126
+ }
96
127
 
97
- export { DependencyContext };
128
+ export { DependencyContext, GRAFANA_EXTERNALS, isExternal };
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
+ import { parseFile } from '../parser.js';
4
+ import { walk } from './ast.js';
3
5
 
4
6
  let cachedPluginJson = null;
5
7
  function getPluginJson(dir) {
@@ -32,5 +34,25 @@ function readJsonFile(filename) {
32
34
  throw new Error(`Failed to load json file ${filename}: ${error instanceof Error ? error.message : String(error)}`);
33
35
  }
34
36
  }
37
+ function hasExternalisedJsxRuntime() {
38
+ const webpackConfigPathsToCheck = ["webpack.config.ts", ".config/webpack/webpack.config.ts"];
39
+ let found = false;
40
+ for (const webpackConfigPath of webpackConfigPathsToCheck) {
41
+ if (isFile(path.join(process.cwd(), webpackConfigPath))) {
42
+ const webpackConfig = fs.readFileSync(path.join(process.cwd(), webpackConfigPath)).toString();
43
+ const webpackConfigAst = parseFile(webpackConfig, webpackConfigPath);
44
+ walk(webpackConfigAst, (node) => {
45
+ if (node.type === "Property" && node.key.type === "Identifier" && node.key.name === "externals" && node.value.type === "ArrayExpression") {
46
+ for (const element of node.value.elements) {
47
+ if (element && element.type === "Literal" && typeof element.value === "string" && element.value.includes("react/jsx-runtime")) {
48
+ found = true;
49
+ }
50
+ }
51
+ }
52
+ });
53
+ }
54
+ }
55
+ return found;
56
+ }
35
57
 
36
- export { getPluginJson, readJsonFile };
58
+ export { getPluginJson, hasExternalisedJsxRuntime, readJsonFile };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@grafana/react-detect",
3
3
  "description": "Run various checks to detect if a Grafana plugin is compatible with React.",
4
- "version": "0.1.0",
4
+ "version": "0.2.0-canary.2358.20374737796.0",
5
5
  "repository": {
6
6
  "directory": "packages/react-detect",
7
7
  "url": "https://github.com/grafana/plugin-tools"
@@ -12,7 +12,8 @@
12
12
  "type": "module",
13
13
  "publishConfig": {
14
14
  "registry": "https://registry.npmjs.org/",
15
- "access": "public"
15
+ "access": "public",
16
+ "provenance": true
16
17
  },
17
18
  "scripts": {
18
19
  "clean": "rm -rf ./dist",
@@ -38,5 +39,6 @@
38
39
  },
39
40
  "engines": {
40
41
  "node": ">=20"
41
- }
42
+ },
43
+ "gitHead": "834f9e9577721255794b2257955d96d0faf967ae"
42
44
  }
@@ -7,7 +7,7 @@ export const PATTERN_DEFINITIONS: Record<string, PatternDefinition> = {
7
7
  description: 'React internals __SECRET_INTERNALS renamed to _DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE',
8
8
  fix: {
9
9
  description:
10
- 'Check the list of libraries depending on React internals. Alternatively externalise react/jsx-runtime in webpack config. Your plugin will only be compatible with Grafana >=12.3.0',
10
+ 'Externalise react/jsx-runtime in webpack config. Your plugin will only be compatible with Grafana >=12.3.0',
11
11
  },
12
12
  link: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#libraries-depending-on-react-internals-may-block-upgrades',
13
13
  },
@@ -15,19 +15,19 @@ export const PATTERN_DEFINITIONS: Record<string, PatternDefinition> = {
15
15
  severity: 'renamed',
16
16
  impactLevel: 'critical',
17
17
  description:
18
- 'Dependency bundles react/jsx-runtime which will break with React 19 due to `__SECRET_INTERNALS` being renamed',
18
+ 'Bundling react/jsx-runtime will break with React 19 due to `__SECRET_INTERNALS...` renamed to `_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE`',
19
19
  fix: {
20
20
  description:
21
21
  'Externalize react/jsx-runtime in webpack config. Your plugin will only be compatible with Grafana >=12.3.0',
22
22
  },
23
- link: 'https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config',
23
+ link: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#libraries-depending-on-react-internals-may-block-upgrades',
24
24
  },
25
25
  defaultProps: {
26
26
  severity: 'removed',
27
27
  impactLevel: 'critical',
28
- description: 'Default props removed in favour of function components',
28
+ description: 'Default props removed from function components',
29
29
  fix: {
30
- description: 'Use ES6 default parameters',
30
+ description: 'Use ES6 default parameters or pass default values to dependencies to keep consistent behavior',
31
31
  before: 'MyComponent.defaultProps = { value: "test" }',
32
32
  after: 'function MyComponent({ value = "test" }) { ... }',
33
33
  },
@@ -67,7 +67,7 @@ export const PATTERN_DEFINITIONS: Record<string, PatternDefinition> = {
67
67
  impactLevel: 'critical',
68
68
  description: 'String refs removed in React 19',
69
69
  fix: {
70
- description: 'Run the codemod to migrate to ref callbacks.',
70
+ description: 'Run the codemod to migrate to ref callbacks',
71
71
  before: 'npx codemod@latest react/19/replace-string-ref',
72
72
  },
73
73
  link: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-string-refs',
@@ -75,7 +75,7 @@ export const PATTERN_DEFINITIONS: Record<string, PatternDefinition> = {
75
75
  findDOMNode: {
76
76
  severity: 'removed',
77
77
  impactLevel: 'critical',
78
- description: 'findDOMNode removed from React and ReactDOM in React 19',
78
+ description: 'findDOMNode removed from ReactDOM in React 19',
79
79
  fix: {
80
80
  description: 'Replace ReactDOM.findDOMNode with DOM refs',
81
81
  before: 'const node = findDOMNode(this);',
@@ -100,7 +100,7 @@ export const PATTERN_DEFINITIONS: Record<string, PatternDefinition> = {
100
100
  impactLevel: 'critical',
101
101
  description: 'ReactDOM.render removed in React 19 (use createRoot)',
102
102
  fix: {
103
- description: 'Use createRoot instead.',
103
+ description: 'Use createRoot instead',
104
104
  before: 'ReactDOM.render(<App />, document.getElementById("root"));',
105
105
  after: 'const root = createRoot(document.getElementById("root")); root.render(<App />);',
106
106
  },
@@ -112,7 +112,7 @@ export const PATTERN_DEFINITIONS: Record<string, PatternDefinition> = {
112
112
  impactLevel: 'critical',
113
113
  description: 'ReactDOM.unmountComponentAtNode removed in React 19',
114
114
  fix: {
115
- description: 'Use createRoot instead.',
115
+ description: 'Use createRoot instead',
116
116
  before: 'ReactDOM.unmountComponentAtNode(container);',
117
117
  after: 'const root = createRoot(container); root.unmount();',
118
118
  },
@@ -130,18 +130,6 @@ describe('matcher', () => {
130
130
  });
131
131
 
132
132
  describe('findFindDOMNode', () => {
133
- it('should find React.findDOMNode calls', () => {
134
- const code = `
135
- const node = React.findDOMNode(component);
136
- `;
137
- const ast = parseFile(code, 'test.js');
138
- const matches = findFindDOMNode(ast, code);
139
-
140
- expect(matches).toHaveLength(1);
141
- expect(matches[0].pattern).toBe('findDOMNode');
142
- expect(matches[0].matched).toContain('React.findDOMNode');
143
- });
144
-
145
133
  it('should find ReactDOM.findDOMNode calls', () => {
146
134
  const code = `
147
135
  const node = ReactDOM.findDOMNode(this);
@@ -1,30 +1,36 @@
1
1
  import { AnalysisResult, PluginAnalysisResults } from '../types/reporters.js';
2
+ import { isExternal } from '../utils/dependencies.js';
2
3
  import { output } from '../utils/output.js';
3
4
 
5
+ const summaryMsg = [
6
+ `We strongly recommend testing your plugin using the React 19 Grafana docker image: ${output.formatCode('grafana/grafana:12.4.0-react19')}`,
7
+ ...output.bulletList([
8
+ `Start the server with ${output.formatCode('GRAFANA_VERSION=12.4.0-react19 GRAFANA_IMAGE=grafana docker compose up --build')} flag and manually test your plugin.`,
9
+ 'Run any e2e tests to try and catch any potential issues.',
10
+ 'Manually test your plugin in the browser. Look for any console warnings or errors related to React.',
11
+ ]),
12
+ '',
13
+ 'For more information, please refer to:',
14
+ `React 19 blog post: ${output.formatUrl('https://react.dev/blog/2024/04/25/react-19-upgrade-guide')}.`,
15
+ `Grafana developer documentation: ${output.formatUrl('https://grafana.com/developers/plugin-tools/set-up/set-up-docker')}`,
16
+ ];
17
+
4
18
  export function consoleReporter(results: PluginAnalysisResults) {
5
19
  if (results.summary.totalIssues === 0) {
6
20
  output.success({
7
- title: 'No React 19 breaking changes detected!',
21
+ title: 'No React 19 breaking changes detected.',
8
22
  body: [
9
23
  `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
10
24
  'Good news! Your plugin appears to be compatible with React 19.',
11
25
  '',
12
- 'Even so we recommend testing your plugin using the following steps:',
13
- ...output.bulletList([
14
- `1. Use the React 19 Grafana docker image: ${output.formatCode('grafana/grafana-enterprise-dev:10.0.0-255911')}`,
15
- '2. Start the server and manually test your plugin.',
16
- ]),
17
- '',
18
- `For more information, please refer to the React 19 blog post: ${output.formatUrl('https://react.dev/blog/2024/04/25/react-19-upgrade-guide')}.`,
19
- '',
20
- 'Thank you for using Grafana!',
26
+ ...summaryMsg,
21
27
  ],
22
28
  });
23
29
  return;
24
30
  }
25
31
 
26
32
  output.error({
27
- title: 'React 19 breaking changes detected!',
33
+ title: 'React 19 breaking changes detected.',
28
34
  body: [
29
35
  `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
30
36
  'Your plugin appears to be incompatible with React 19. Note that this tool can give false positives, please review the issues carefully.',
@@ -71,7 +77,7 @@ export function consoleReporter(results: PluginAnalysisResults) {
71
77
  title: 'Dependency issues',
72
78
  body: [
73
79
  'The following issues were found in bundled dependencies.',
74
- 'We recommend checking for dependency updates which are compatible with React 19.',
80
+ 'Please check for dependency updates that are compatible with React 19.',
75
81
  ],
76
82
  withPrefix: false,
77
83
  });
@@ -80,15 +86,31 @@ export function consoleReporter(results: PluginAnalysisResults) {
80
86
  for (const [pkgName, issues] of Object.entries(groupedByPackage)) {
81
87
  const uniquePatterns = new Set(
82
88
  issues.map(
83
- (issue) => `${issue.problem}. ${issue.fix.description}. Further information: ${output.formatUrl(issue.link)}`
89
+ (issue) => `${issue.problem}. ${issue.fix.description}.
90
+ Further information: ${output.formatUrl(issue.link)}`
84
91
  )
85
92
  );
86
93
  const uniqueFileLocations = new Set(issues.map((issue) => issue.location.file));
87
94
  const fileLocationList = output.bulletList(Array.from(uniqueFileLocations));
88
95
  const patternInfoList = output.bulletList(Array.from(uniquePatterns));
96
+ const rootDependencies = issues
97
+ .filter(
98
+ (issue) =>
99
+ issue.rootDependency !== undefined && issue.rootDependency !== pkgName && !isExternal(issue.rootDependency)
100
+ )
101
+ .map((issue) => issue.rootDependency);
102
+ const uniqueDependencies = new Set(rootDependencies.filter((dep) => dep !== undefined));
103
+
104
+ const body = ['issues found:', ...patternInfoList, ''];
105
+ if (uniqueDependencies.size) {
106
+ body.push(`Bundled by dependenc${uniqueDependencies.size > 1 ? 'ies' : 'y'}:`);
107
+ body.push(...output.bulletList(Array.from(uniqueDependencies)));
108
+ }
109
+
110
+ body.push('found in:', ...fileLocationList);
89
111
  output.error({
90
112
  title: `📦 ${pkgName}`,
91
- body: ['issues found:', ...patternInfoList, '', 'found in:', ...fileLocationList],
113
+ body,
92
114
  withPrefix: false,
93
115
  });
94
116
  output.addHorizontalLine('red');
@@ -97,17 +119,7 @@ export function consoleReporter(results: PluginAnalysisResults) {
97
119
 
98
120
  output.error({
99
121
  title: 'Next steps',
100
- body: [
101
- 'We recommend testing your plugin using the following steps to ensure it is compatible with React 19:',
102
- ...output.bulletList([
103
- `1. Use the React 19 Grafana docker image: ${output.formatCode('grafana/grafana-enterprise-dev:10.0.0-255911')}`,
104
- '2. Start the server and manually test your plugin.',
105
- ]),
106
- '',
107
- `For more information, please refer to the React 19 blog post: ${output.formatUrl('https://react.dev/blog/2024/04/25/react-19-upgrade-guide')}.`,
108
- '',
109
- 'Thank you for using Grafana!',
110
- ],
122
+ body: summaryMsg,
111
123
  withPrefix: false,
112
124
  });
113
125
  }
package/src/results.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AnalyzedMatch } from './types/processors.js';
2
2
  import { PluginAnalysisResults, AnalysisResult, DependencyIssue } from './types/reporters.js';
3
3
  import { getPattern } from './patterns/definitions.js';
4
- import { getPluginJson } from './utils/plugin.js';
4
+ import { getPluginJson, hasExternalisedJsxRuntime } from './utils/plugin.js';
5
5
  import { DependencyContext } from './utils/dependencies.js';
6
6
  import path from 'node:path';
7
7
 
@@ -59,6 +59,7 @@ export function generateAnalysisResults(
59
59
  }
60
60
 
61
61
  function filterMatches(matches: AnalyzedMatch[]): AnalyzedMatch[] {
62
+ const externalisedJsxRuntime = hasExternalisedJsxRuntime();
62
63
  const filtered = matches.filter((match) => {
63
64
  // TODO: add mode for strict / loose filtering
64
65
  if (match.type === 'source' && (match.confidence === 'none' || match.confidence === 'unknown')) {
@@ -84,6 +85,14 @@ function filterMatches(matches: AnalyzedMatch[]): AnalyzedMatch[] {
84
85
  return false;
85
86
  }
86
87
 
88
+ // JSX runtime imports are only an issue if they are not externalised in webpack config.
89
+ if (
90
+ match.type === 'dependency' &&
91
+ (match.pattern === 'jsxRuntimeImport' || match.pattern === '__SECRET_INTERNALS')
92
+ ) {
93
+ return !externalisedJsxRuntime;
94
+ }
95
+
87
96
  return true;
88
97
  });
89
98
 
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
+ import { parseFile } from '../parser.js';
4
+ import { walk } from './ast.js';
3
5
 
4
6
  interface PluginJson {
5
7
  id: string;
@@ -47,26 +49,35 @@ export function readJsonFile(filename: string) {
47
49
  }
48
50
  }
49
51
 
50
- export function hasTsConfigAutomaticJsx(): boolean {
51
- const tsConfigPathsToCheck = ['tsconfig.json', '.config/tsconfig.json'];
52
-
53
- for (const tsConfigPath of tsConfigPathsToCheck) {
54
- const jsxConfig = readJsonFile(path.join(process.cwd(), tsConfigPath));
55
- const jsx = jsxConfig?.compilerOptions?.jsx;
56
- if (jsx === 'react-jsx' || jsx === 'react-jsxdev') {
57
- return true;
58
- }
59
- }
60
- return false;
61
- }
62
-
63
- export function hasSwcAutomaticJsx(): boolean {
64
- const webpackConfigPathsToCheck = ['webpack.config.js', '.config/webpack/webpack.config.js'];
52
+ export function hasExternalisedJsxRuntime(): boolean {
53
+ const webpackConfigPathsToCheck = ['webpack.config.ts', '.config/webpack/webpack.config.ts'];
54
+ let found = false;
65
55
  for (const webpackConfigPath of webpackConfigPathsToCheck) {
66
- const webpackConfig = fs.readFileSync(path.join(process.cwd(), webpackConfigPath)).toString();
67
- if (webpackConfig.includes("runtime: 'automatic'")) {
68
- return true;
56
+ if (isFile(path.join(process.cwd(), webpackConfigPath))) {
57
+ const webpackConfig = fs.readFileSync(path.join(process.cwd(), webpackConfigPath)).toString();
58
+ const webpackConfigAst = parseFile(webpackConfig, webpackConfigPath);
59
+
60
+ walk(webpackConfigAst, (node) => {
61
+ // check for 'react/jsx-runtime' in externals: [array]
62
+ if (
63
+ node.type === 'Property' &&
64
+ node.key.type === 'Identifier' &&
65
+ node.key.name === 'externals' &&
66
+ node.value.type === 'ArrayExpression'
67
+ ) {
68
+ for (const element of node.value.elements) {
69
+ if (
70
+ element &&
71
+ element.type === 'Literal' &&
72
+ typeof element.value === 'string' &&
73
+ element.value.includes('react/jsx-runtime')
74
+ ) {
75
+ found = true;
76
+ }
77
+ }
78
+ }
79
+ });
69
80
  }
70
81
  }
71
- return false;
82
+ return found;
72
83
  }