@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.
- package/dist/patterns/definitions.js +9 -9
- package/dist/reporters/console.js +31 -25
- package/dist/results.js +5 -1
- package/dist/utils/dependencies.js +32 -1
- package/dist/utils/plugin.js +23 -1
- package/package.json +5 -3
- package/src/patterns/definitions.ts +9 -9
- package/src/patterns/matcher.test.ts +0 -12
- package/src/reporters/console.ts +37 -25
- package/src/results.ts +10 -1
- package/src/utils/plugin.ts +30 -19
|
@@ -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: "
|
|
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: "
|
|
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://
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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}.
|
|
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
|
|
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 };
|
package/dist/utils/plugin.js
CHANGED
|
@@ -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.
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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://
|
|
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
|
|
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
|
|
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);
|
package/src/reporters/console.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
'
|
|
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}.
|
|
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
|
|
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
|
|
package/src/utils/plugin.ts
CHANGED
|
@@ -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
|
|
51
|
-
const
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
82
|
+
return found;
|
|
72
83
|
}
|