@grafana/react-detect 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/dist/analyzer.js +37 -0
  4. package/dist/bin/run.js +12 -0
  5. package/dist/commands/detect19.js +41 -0
  6. package/dist/file-scanner.js +20 -0
  7. package/dist/libs/output/src/index.js +132 -0
  8. package/dist/parser.js +23 -0
  9. package/dist/patterns/definitions.js +119 -0
  10. package/dist/patterns/matcher.js +146 -0
  11. package/dist/reporters/console.js +132 -0
  12. package/dist/reporters/json.js +11 -0
  13. package/dist/results.js +128 -0
  14. package/dist/source-extractor.js +80 -0
  15. package/dist/utils/analyzer.js +88 -0
  16. package/dist/utils/ast.js +20 -0
  17. package/dist/utils/dependencies.js +97 -0
  18. package/dist/utils/output.js +5 -0
  19. package/dist/utils/plugin.js +36 -0
  20. package/package.json +42 -0
  21. package/src/analyzer.test.ts +14 -0
  22. package/src/analyzer.ts +42 -0
  23. package/src/bin/run.ts +17 -0
  24. package/src/commands/detect19.ts +53 -0
  25. package/src/file-scanner.ts +19 -0
  26. package/src/parser.ts +22 -0
  27. package/src/patterns/definitions.ts +125 -0
  28. package/src/patterns/matcher.test.ts +221 -0
  29. package/src/patterns/matcher.ts +268 -0
  30. package/src/reporters/console.ts +139 -0
  31. package/src/reporters/json.ts +13 -0
  32. package/src/results.ts +170 -0
  33. package/src/source-extractor.ts +101 -0
  34. package/src/types/patterns.ts +14 -0
  35. package/src/types/plugins.ts +6 -0
  36. package/src/types/processors.ts +40 -0
  37. package/src/types/reporters.ts +53 -0
  38. package/src/utils/analyzer.test.ts +190 -0
  39. package/src/utils/analyzer.ts +120 -0
  40. package/src/utils/ast.ts +19 -0
  41. package/src/utils/dependencies.test.ts +123 -0
  42. package/src/utils/dependencies.ts +141 -0
  43. package/src/utils/output.ts +3 -0
  44. package/src/utils/plugin.ts +72 -0
  45. package/test/fixtures/dependencies/package-lock.json +49 -0
  46. package/test/fixtures/dependencies/package.json +16 -0
  47. package/test/fixtures/patterns/module.js.map +1 -0
  48. package/tsconfig.json +9 -0
  49. package/vitest.config.ts +12 -0
@@ -0,0 +1,221 @@
1
+ import { parseFile } from '../parser.js';
2
+ import {
3
+ findDefaultProps,
4
+ findPropTypes,
5
+ findContextTypes,
6
+ findGetChildContext,
7
+ findSecretInternals,
8
+ findStringRefs,
9
+ findFindDOMNode,
10
+ findReactDOMRender,
11
+ findReactDOMUnmountComponentAtNode,
12
+ findCreateFactory,
13
+ } from './matcher.js';
14
+
15
+ describe('matcher', () => {
16
+ describe('findDefaultProps', () => {
17
+ it('should find defaultProps assignments in source code', () => {
18
+ const code = `
19
+ function MyComponent() {}
20
+ MyComponent.defaultProps = { foo: 'bar' };
21
+ `;
22
+ const ast = parseFile(code, 'module.js');
23
+ const matches = findDefaultProps(ast, code);
24
+
25
+ expect(matches).toHaveLength(1);
26
+ expect(matches[0].pattern).toBe('defaultProps');
27
+ expect(matches[0].line).toBe(3);
28
+ });
29
+ });
30
+
31
+ describe('findPropTypes', () => {
32
+ it('should find propTypes assignments in source code', () => {
33
+ const code = `
34
+ function MyComponent() {}
35
+ MyComponent.propTypes = { name: PropTypes.string };
36
+ `;
37
+ const ast = parseFile(code, 'test.js');
38
+ const matches = findPropTypes(ast, code);
39
+
40
+ expect(matches).toHaveLength(1);
41
+ expect(matches[0].pattern).toBe('propTypes');
42
+ expect(matches[0].matched).toContain('propTypes');
43
+ });
44
+ });
45
+
46
+ describe('findContextTypes', () => {
47
+ it('should find contextTypes assignments in source code', () => {
48
+ const code = `
49
+ class MyComponent extends React.Component {}
50
+ MyComponent.contextTypes = { theme: PropTypes.object };
51
+ `;
52
+ const ast = parseFile(code, 'test.js');
53
+ const matches = findContextTypes(ast, code);
54
+
55
+ expect(matches).toHaveLength(1);
56
+ expect(matches[0].pattern).toBe('contextTypes');
57
+ expect(matches[0].matched).toContain('contextTypes');
58
+ });
59
+ });
60
+
61
+ describe('findGetChildContext', () => {
62
+ it('should find getChildContext assignments in source code', () => {
63
+ const code = `
64
+ class MyComponent extends React.Component {
65
+ getChildContext() { return { theme: 'dark' }; }
66
+ }
67
+ MyComponent.getChildContext = function() {};
68
+ `;
69
+ const ast = parseFile(code, 'test.js');
70
+ const matches = findGetChildContext(ast, code);
71
+
72
+ expect(matches).toHaveLength(1);
73
+ expect(matches[0].pattern).toBe('getChildContext');
74
+ expect(matches[0].matched).toContain('getChildContext');
75
+ });
76
+ });
77
+
78
+ describe('findSecretInternals', () => {
79
+ it('should find secretInternals assignments in source code', () => {
80
+ const code = `
81
+ const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
82
+ `;
83
+ const ast = parseFile(code, 'test.js');
84
+ const matches = findSecretInternals(ast, code);
85
+
86
+ expect(matches).toHaveLength(1);
87
+ expect(matches[0].pattern).toBe('__SECRET_INTERNALS');
88
+ expect(matches[0].matched).toContain('__SECRET_INTERNALS');
89
+ });
90
+ });
91
+
92
+ describe('findStringRefs', () => {
93
+ it('should find stringRefs assignments in source code', () => {
94
+ const code = `
95
+ class MyComponent extends React.Component {
96
+ componentDidMount() {
97
+ this.refs.input.focus();
98
+ }
99
+ render() {
100
+ return <input ref="input" />;
101
+ }
102
+ }
103
+ `;
104
+ const ast = parseFile(code, 'test.js');
105
+ const matches = findStringRefs(ast, code);
106
+
107
+ expect(matches).toHaveLength(1);
108
+ expect(matches[0].pattern).toBe('stringRefs');
109
+ expect(matches[0].matched).toContain('this.refs');
110
+ });
111
+
112
+ it('should find bracket notation refs access', () => {
113
+ const code = `
114
+ class MyComponent extends React.Component {
115
+ componentDidMount() {
116
+ this.refs['input'].focus();
117
+ }
118
+ render() {
119
+ return <input ref="input" />;
120
+ }
121
+ }
122
+ `;
123
+ const ast = parseFile(code, 'test.js');
124
+ const matches = findStringRefs(ast, code);
125
+
126
+ expect(matches).toHaveLength(1);
127
+ expect(matches[0].pattern).toBe('stringRefs');
128
+ expect(matches[0].matched).toContain('this.refs');
129
+ });
130
+ });
131
+
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
+ it('should find ReactDOM.findDOMNode calls', () => {
146
+ const code = `
147
+ const node = ReactDOM.findDOMNode(this);
148
+ `;
149
+ const ast = parseFile(code, 'test.js');
150
+ const matches = findFindDOMNode(ast, code);
151
+
152
+ expect(matches).toHaveLength(1);
153
+ expect(matches[0].pattern).toBe('findDOMNode');
154
+ expect(matches[0].matched).toContain('ReactDOM.findDOMNode');
155
+ });
156
+
157
+ it('should find direct findDOMNode calls', () => {
158
+ const code = `
159
+ import { findDOMNode } from 'react-dom';
160
+ const node = findDOMNode(component);
161
+ `;
162
+ const ast = parseFile(code, 'test.js');
163
+ const matches = findFindDOMNode(ast, code);
164
+
165
+ expect(matches).toHaveLength(1);
166
+ expect(matches[0].pattern).toBe('findDOMNode');
167
+ });
168
+ });
169
+
170
+ describe('findReactDOMRender', () => {
171
+ it('should find ReactDOM.render with 2 arguments', () => {
172
+ const code = `
173
+ ReactDOM.render(<App />, document.getElementById('root'));
174
+ `;
175
+ const ast = parseFile(code, 'test.js');
176
+ const matches = findReactDOMRender(ast, code);
177
+
178
+ expect(matches).toHaveLength(1);
179
+ expect(matches[0].pattern).toBe('ReactDOM.render');
180
+ expect(matches[0].matched).toContain('render');
181
+ });
182
+
183
+ it('should find ReactDOM.render with 3 arguments (callback)', () => {
184
+ const code = `
185
+ ReactDOM.render(<App />, container, () => console.log('done'));
186
+ `;
187
+ const ast = parseFile(code, 'test.js');
188
+ const matches = findReactDOMRender(ast, code);
189
+
190
+ expect(matches).toHaveLength(1);
191
+ expect(matches[0].pattern).toBe('ReactDOM.render');
192
+ });
193
+ });
194
+
195
+ describe('findReactDOMUnmountComponentAtNode', () => {
196
+ it('should find ReactDOM.unmountComponentAtNode calls', () => {
197
+ const code = `
198
+ ReactDOM.unmountComponentAtNode(container);
199
+ `;
200
+ const ast = parseFile(code, 'test.js');
201
+ const matches = findReactDOMUnmountComponentAtNode(ast, code);
202
+ expect(matches).toHaveLength(1);
203
+ expect(matches[0].pattern).toBe('ReactDOM.unmountComponentAtNode');
204
+ expect(matches[0].matched).toContain('ReactDOM.unmountComponentAtNode');
205
+ });
206
+ });
207
+
208
+ describe('findCreateFactory', () => {
209
+ it('should find React.createFactory', () => {
210
+ const code = `
211
+ const Button = React.createFactory('button');
212
+ `;
213
+ const ast = parseFile(code, 'test.js');
214
+ const matches = findCreateFactory(ast, code);
215
+
216
+ expect(matches).toHaveLength(1);
217
+ expect(matches[0].pattern).toBe('createFactory');
218
+ expect(matches[0].matched).toContain('createFactory');
219
+ });
220
+ });
221
+ });
@@ -0,0 +1,268 @@
1
+ import { TSESTree } from '@typescript-eslint/typescript-estree';
2
+ import { PatternMatch } from '../types/processors.js';
3
+ import { getSurroundingCode, walk } from '../utils/ast.js';
4
+
5
+ export function findPatternMatches(ast: TSESTree.Program, code: string): PatternMatch[] {
6
+ const matches: PatternMatch[] = [];
7
+
8
+ matches.push(...findDefaultProps(ast, code));
9
+ matches.push(...findPropTypes(ast, code));
10
+ matches.push(...findContextTypes(ast, code));
11
+ matches.push(...findGetChildContext(ast, code));
12
+ matches.push(...findSecretInternals(ast, code));
13
+ matches.push(...findJsxRuntimeImports(ast, code));
14
+ matches.push(...findStringRefs(ast, code));
15
+ matches.push(...findFindDOMNode(ast, code));
16
+ matches.push(...findReactDOMRender(ast, code));
17
+ matches.push(...findReactDOMUnmountComponentAtNode(ast, code));
18
+ matches.push(...findCreateFactory(ast, code));
19
+
20
+ return matches;
21
+ }
22
+
23
+ export function findDefaultProps(ast: TSESTree.Program, code: string): PatternMatch[] {
24
+ const matches: PatternMatch[] = [];
25
+
26
+ walk(ast, (node) => {
27
+ if (
28
+ node &&
29
+ node.type === 'AssignmentExpression' &&
30
+ node.left.type === 'MemberExpression' &&
31
+ node.left.property.type === 'Identifier' &&
32
+ node.left.property.name === 'defaultProps'
33
+ ) {
34
+ matches.push(createPatternMatch(node, 'defaultProps', code));
35
+ }
36
+ });
37
+
38
+ return matches;
39
+ }
40
+
41
+ export function findPropTypes(ast: TSESTree.Program, code: string): PatternMatch[] {
42
+ const matches: PatternMatch[] = [];
43
+
44
+ walk(ast, (node) => {
45
+ if (
46
+ node &&
47
+ node.type === 'AssignmentExpression' &&
48
+ node.left.type === 'MemberExpression' &&
49
+ node.left.property.type === 'Identifier' &&
50
+ node.left.property.name === 'propTypes'
51
+ ) {
52
+ matches.push(createPatternMatch(node, 'propTypes', code));
53
+ }
54
+ });
55
+
56
+ return matches;
57
+ }
58
+
59
+ export function findContextTypes(ast: TSESTree.Program, code: string): PatternMatch[] {
60
+ const matches: PatternMatch[] = [];
61
+
62
+ walk(ast, (node) => {
63
+ if (
64
+ node &&
65
+ node.type === 'AssignmentExpression' &&
66
+ node.left.type === 'MemberExpression' &&
67
+ node.left.property.type === 'Identifier' &&
68
+ node.left.property.name === 'contextTypes'
69
+ ) {
70
+ matches.push(createPatternMatch(node, 'contextTypes', code));
71
+ }
72
+ });
73
+
74
+ return matches;
75
+ }
76
+
77
+ export function findGetChildContext(ast: TSESTree.Program, code: string): PatternMatch[] {
78
+ const matches: PatternMatch[] = [];
79
+
80
+ walk(ast, (node) => {
81
+ if (
82
+ node &&
83
+ node.type === 'AssignmentExpression' &&
84
+ node.left.type === 'MemberExpression' &&
85
+ node.left.property.type === 'Identifier' &&
86
+ node.left.property.name === 'getChildContext'
87
+ ) {
88
+ matches.push(createPatternMatch(node, 'getChildContext', code));
89
+ }
90
+ });
91
+
92
+ return matches;
93
+ }
94
+
95
+ export function findSecretInternals(ast: TSESTree.Program, code: string): PatternMatch[] {
96
+ const matches: PatternMatch[] = [];
97
+
98
+ walk(ast, (node) => {
99
+ if (
100
+ node &&
101
+ node.type === 'MemberExpression' &&
102
+ node.property.type === 'Identifier' &&
103
+ node.property.name === '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED'
104
+ ) {
105
+ matches.push(createPatternMatch(node, '__SECRET_INTERNALS', code));
106
+ }
107
+ });
108
+
109
+ return matches;
110
+ }
111
+
112
+ export function findJsxRuntimeImports(ast: TSESTree.Program, code: string): PatternMatch[] {
113
+ const matches: PatternMatch[] = [];
114
+
115
+ walk(ast, (node) => {
116
+ if (node && node.type === 'ImportDeclaration') {
117
+ const source = node.source.value;
118
+ if (source === 'react/jsx-runtime' || source === 'react/jsx-dev-runtime') {
119
+ matches.push(createPatternMatch(node, 'jsxRuntimeImport', code));
120
+ }
121
+ }
122
+ });
123
+
124
+ return matches;
125
+ }
126
+
127
+ export function findStringRefs(ast: TSESTree.Program, code: string): PatternMatch[] {
128
+ const matches: PatternMatch[] = [];
129
+
130
+ walk(ast, (node) => {
131
+ if (
132
+ node &&
133
+ node.type === 'MemberExpression' &&
134
+ node.object.type === 'ThisExpression' &&
135
+ node.property.type === 'Identifier' &&
136
+ node.property.name === 'refs'
137
+ ) {
138
+ matches.push(createPatternMatch(node, 'stringRefs', code));
139
+ }
140
+ });
141
+
142
+ return matches;
143
+ }
144
+
145
+ export function findFindDOMNode(ast: TSESTree.Program, code: string): PatternMatch[] {
146
+ const matches: PatternMatch[] = [];
147
+
148
+ walk(ast, (node) => {
149
+ if (node && node.type === 'CallExpression') {
150
+ // React.findDOMNode() or ReactDOM.findDOMNode()
151
+ if (
152
+ node.callee.type === 'MemberExpression' &&
153
+ node.callee.object.type === 'Identifier' &&
154
+ (node.callee.object.name === 'ReactDOM' || node.callee.object.name === 'React') &&
155
+ node.callee.property.type === 'Identifier' &&
156
+ node.callee.property.name === 'findDOMNode'
157
+ ) {
158
+ matches.push(createPatternMatch(node, 'findDOMNode', code));
159
+ }
160
+ // findDOMNode() (direct import)
161
+ else if (node.callee.type === 'Identifier' && node.callee.name === 'findDOMNode') {
162
+ matches.push(createPatternMatch(node, 'findDOMNode', code));
163
+ }
164
+ }
165
+ });
166
+
167
+ return matches;
168
+ }
169
+
170
+ export function findReactDOMRender(ast: TSESTree.Program, code: string): PatternMatch[] {
171
+ const matches: PatternMatch[] = [];
172
+
173
+ walk(ast, (node) => {
174
+ if (node && node.type === 'CallExpression') {
175
+ // ReactDOM.render()
176
+ if (
177
+ node.callee.type === 'MemberExpression' &&
178
+ node.callee.object.type === 'Identifier' &&
179
+ node.callee.object.name === 'ReactDOM' &&
180
+ node.callee.property.type === 'Identifier' &&
181
+ node.callee.property.name === 'render' &&
182
+ (node.arguments.length === 2 || node.arguments.length === 3)
183
+ ) {
184
+ matches.push(createPatternMatch(node, 'ReactDOM.render', code));
185
+ }
186
+ // render() (direct import from 'react-dom')
187
+ else if (
188
+ node.callee.type === 'Identifier' &&
189
+ node.callee.name === 'render' &&
190
+ (node.arguments.length === 2 || node.arguments.length === 3)
191
+ ) {
192
+ matches.push(createPatternMatch(node, 'ReactDOM.render', code));
193
+ }
194
+ }
195
+ });
196
+
197
+ return matches;
198
+ }
199
+
200
+ export function findReactDOMUnmountComponentAtNode(ast: TSESTree.Program, code: string): PatternMatch[] {
201
+ const matches: PatternMatch[] = [];
202
+
203
+ walk(ast, (node) => {
204
+ if (node && node.type === 'CallExpression') {
205
+ // ReactDOM.unmountComponentAtNode()
206
+ if (
207
+ node.callee.type === 'MemberExpression' &&
208
+ node.callee.object.type === 'Identifier' &&
209
+ node.callee.object.name === 'ReactDOM' &&
210
+ node.callee.property.type === 'Identifier' &&
211
+ node.callee.property.name === 'unmountComponentAtNode' &&
212
+ node.arguments.length === 1
213
+ ) {
214
+ matches.push(createPatternMatch(node, 'ReactDOM.unmountComponentAtNode', code));
215
+ }
216
+ // unmountComponentAtNode() (direct import)
217
+ else if (
218
+ node.callee.type === 'Identifier' &&
219
+ node.callee.name === 'unmountComponentAtNode' &&
220
+ node.arguments.length === 1
221
+ ) {
222
+ matches.push(createPatternMatch(node, 'ReactDOM.unmountComponentAtNode', code));
223
+ }
224
+ }
225
+ });
226
+
227
+ return matches;
228
+ }
229
+
230
+ export function findCreateFactory(ast: TSESTree.Program, code: string): PatternMatch[] {
231
+ const matches: PatternMatch[] = [];
232
+
233
+ walk(ast, (node) => {
234
+ if (node && node.type === 'CallExpression') {
235
+ // React.createFactory()
236
+ if (
237
+ node.callee.type === 'MemberExpression' &&
238
+ node.callee.object.type === 'Identifier' &&
239
+ node.callee.object.name === 'React' &&
240
+ node.callee.property.type === 'Identifier' &&
241
+ node.callee.property.name === 'createFactory' &&
242
+ node.arguments.length === 1
243
+ ) {
244
+ matches.push(createPatternMatch(node, 'createFactory', code));
245
+ }
246
+ // createFactory() (direct import)
247
+ else if (
248
+ node.callee.type === 'Identifier' &&
249
+ node.callee.name === 'createFactory' &&
250
+ node.arguments.length === 1
251
+ ) {
252
+ matches.push(createPatternMatch(node, 'createFactory', code));
253
+ }
254
+ }
255
+ });
256
+
257
+ return matches;
258
+ }
259
+
260
+ export function createPatternMatch(node: any, pattern: string, code: string): PatternMatch {
261
+ return {
262
+ pattern,
263
+ line: node.loc.start.line,
264
+ column: node.loc.start.column,
265
+ matched: code.slice(node.range[0], node.range[1]),
266
+ context: getSurroundingCode(code, node),
267
+ };
268
+ }
@@ -0,0 +1,139 @@
1
+ import { AnalysisResult, PluginAnalysisResults } from '../types/reporters.js';
2
+ import { output } from '../utils/output.js';
3
+
4
+ export function consoleReporter(results: PluginAnalysisResults) {
5
+ if (results.summary.totalIssues === 0) {
6
+ output.success({
7
+ title: 'No React 19 breaking changes detected!',
8
+ body: [
9
+ `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
10
+ 'Good news! Your plugin appears to be compatible with React 19.',
11
+ '',
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!',
21
+ ],
22
+ });
23
+ return;
24
+ }
25
+
26
+ output.error({
27
+ title: 'React 19 breaking changes detected!',
28
+ body: [
29
+ `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
30
+ 'Your plugin appears to be incompatible with React 19. Note that this tool can give false positives, please review the issues carefully.',
31
+ ],
32
+ });
33
+
34
+ const criticalSourceIssues = results.issues.critical.filter((issue) => issue.location.type === 'source');
35
+ const warningSourceIssues = results.issues.warnings.filter((issue) => issue.location.type === 'source');
36
+ const sourceCodeIssues = [...criticalSourceIssues, ...warningSourceIssues];
37
+ if (sourceCodeIssues.length > 0) {
38
+ output.error({
39
+ title: 'Source code issues',
40
+ body: ['The following source code issues were found. Please refer to the suggestions to help resolve them.'],
41
+ withPrefix: false,
42
+ });
43
+ const groupedByPattern = groupByPattern(sourceCodeIssues);
44
+ for (const [pattern, issues] of Object.entries(groupedByPattern)) {
45
+ // dedupe file locations
46
+ const uniqueFileLocations = new Set(issues.map((issue) => issue.location.file));
47
+ const fileLocationList = output.bulletList(Array.from(uniqueFileLocations));
48
+ const patternInfo = issues[0];
49
+ output.error({
50
+ title: `${pattern} (${patternInfo.problem})`,
51
+ body: [
52
+ `fix: ${patternInfo.fix.description}.`,
53
+ ...(patternInfo.fix.before ? [`before: ${output.formatCode(patternInfo.fix.before)}`] : []),
54
+ ...(patternInfo.fix.after ? [`after: ${output.formatCode(patternInfo.fix.after)}`] : []),
55
+ `link: ${output.formatUrl(patternInfo.link)}`,
56
+ '',
57
+ 'found in:',
58
+ ...fileLocationList,
59
+ ],
60
+ withPrefix: false,
61
+ });
62
+ output.addHorizontalLine('red');
63
+ }
64
+ }
65
+
66
+ const criticalDependencyIssues = results.issues.critical.filter((issue) => issue.location.type === 'dependency');
67
+ const warningDependencyIssues = results.issues.warnings.filter((issue) => issue.location.type === 'dependency');
68
+ const dependencyIssues = [...criticalDependencyIssues, ...warningDependencyIssues];
69
+ if (dependencyIssues.length > 0) {
70
+ output.error({
71
+ title: 'Dependency issues',
72
+ body: [
73
+ 'The following issues were found in bundled dependencies.',
74
+ 'We recommend checking for dependency updates which are compatible with React 19.',
75
+ ],
76
+ withPrefix: false,
77
+ });
78
+
79
+ const groupedByPackage = groupByPackage(dependencyIssues);
80
+ for (const [pkgName, issues] of Object.entries(groupedByPackage)) {
81
+ const uniquePatterns = new Set(
82
+ issues.map(
83
+ (issue) => `${issue.problem}. ${issue.fix.description}. Further information: ${output.formatUrl(issue.link)}`
84
+ )
85
+ );
86
+ const uniqueFileLocations = new Set(issues.map((issue) => issue.location.file));
87
+ const fileLocationList = output.bulletList(Array.from(uniqueFileLocations));
88
+ const patternInfoList = output.bulletList(Array.from(uniquePatterns));
89
+ output.error({
90
+ title: `📦 ${pkgName}`,
91
+ body: ['issues found:', ...patternInfoList, '', 'found in:', ...fileLocationList],
92
+ withPrefix: false,
93
+ });
94
+ output.addHorizontalLine('red');
95
+ }
96
+ }
97
+
98
+ output.error({
99
+ 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
+ ],
111
+ withPrefix: false,
112
+ });
113
+ }
114
+
115
+ function groupByPattern(issues: AnalysisResult[]): Record<string, AnalysisResult[]> {
116
+ return issues.reduce(
117
+ (groups, issue) => {
118
+ if (!groups[issue.pattern]) {
119
+ groups[issue.pattern] = [];
120
+ }
121
+ groups[issue.pattern].push(issue);
122
+ return groups;
123
+ },
124
+ {} as Record<string, AnalysisResult[]>
125
+ );
126
+ }
127
+
128
+ function groupByPackage(issues: AnalysisResult[]): Record<string, AnalysisResult[]> {
129
+ return issues.reduce(
130
+ (groups, issue) => {
131
+ if (!groups[issue.packageName!]) {
132
+ groups[issue.packageName!] = [];
133
+ }
134
+ groups[issue.packageName!].push(issue);
135
+ return groups;
136
+ },
137
+ {} as Record<string, AnalysisResult[]>
138
+ );
139
+ }
@@ -0,0 +1,13 @@
1
+ import { PluginAnalysisResults } from '../types/reporters.js';
2
+
3
+ export function jsonReporter(results: PluginAnalysisResults) {
4
+ const {
5
+ // Remove the duplicated dependencies issues
6
+ issues: { dependencies, ...issues },
7
+ ...rest
8
+ } = results;
9
+
10
+ const flattenedIssues = [...issues.critical, ...issues.warnings];
11
+
12
+ console.log(JSON.stringify({ ...rest, issues: flattenedIssues }, null, 2));
13
+ }