@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,146 @@
1
+ import { walk, getSurroundingCode } from '../utils/ast.js';
2
+
3
+ function findPatternMatches(ast, code) {
4
+ const matches = [];
5
+ matches.push(...findDefaultProps(ast, code));
6
+ matches.push(...findPropTypes(ast, code));
7
+ matches.push(...findContextTypes(ast, code));
8
+ matches.push(...findGetChildContext(ast, code));
9
+ matches.push(...findSecretInternals(ast, code));
10
+ matches.push(...findJsxRuntimeImports(ast, code));
11
+ matches.push(...findStringRefs(ast, code));
12
+ matches.push(...findFindDOMNode(ast, code));
13
+ matches.push(...findReactDOMRender(ast, code));
14
+ matches.push(...findReactDOMUnmountComponentAtNode(ast, code));
15
+ matches.push(...findCreateFactory(ast, code));
16
+ return matches;
17
+ }
18
+ function findDefaultProps(ast, code) {
19
+ const matches = [];
20
+ walk(ast, (node) => {
21
+ if (node && node.type === "AssignmentExpression" && node.left.type === "MemberExpression" && node.left.property.type === "Identifier" && node.left.property.name === "defaultProps") {
22
+ matches.push(createPatternMatch(node, "defaultProps", code));
23
+ }
24
+ });
25
+ return matches;
26
+ }
27
+ function findPropTypes(ast, code) {
28
+ const matches = [];
29
+ walk(ast, (node) => {
30
+ if (node && node.type === "AssignmentExpression" && node.left.type === "MemberExpression" && node.left.property.type === "Identifier" && node.left.property.name === "propTypes") {
31
+ matches.push(createPatternMatch(node, "propTypes", code));
32
+ }
33
+ });
34
+ return matches;
35
+ }
36
+ function findContextTypes(ast, code) {
37
+ const matches = [];
38
+ walk(ast, (node) => {
39
+ if (node && node.type === "AssignmentExpression" && node.left.type === "MemberExpression" && node.left.property.type === "Identifier" && node.left.property.name === "contextTypes") {
40
+ matches.push(createPatternMatch(node, "contextTypes", code));
41
+ }
42
+ });
43
+ return matches;
44
+ }
45
+ function findGetChildContext(ast, code) {
46
+ const matches = [];
47
+ walk(ast, (node) => {
48
+ if (node && node.type === "AssignmentExpression" && node.left.type === "MemberExpression" && node.left.property.type === "Identifier" && node.left.property.name === "getChildContext") {
49
+ matches.push(createPatternMatch(node, "getChildContext", code));
50
+ }
51
+ });
52
+ return matches;
53
+ }
54
+ function findSecretInternals(ast, code) {
55
+ const matches = [];
56
+ walk(ast, (node) => {
57
+ if (node && node.type === "MemberExpression" && node.property.type === "Identifier" && node.property.name === "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED") {
58
+ matches.push(createPatternMatch(node, "__SECRET_INTERNALS", code));
59
+ }
60
+ });
61
+ return matches;
62
+ }
63
+ function findJsxRuntimeImports(ast, code) {
64
+ const matches = [];
65
+ walk(ast, (node) => {
66
+ if (node && node.type === "ImportDeclaration") {
67
+ const source = node.source.value;
68
+ if (source === "react/jsx-runtime" || source === "react/jsx-dev-runtime") {
69
+ matches.push(createPatternMatch(node, "jsxRuntimeImport", code));
70
+ }
71
+ }
72
+ });
73
+ return matches;
74
+ }
75
+ function findStringRefs(ast, code) {
76
+ const matches = [];
77
+ walk(ast, (node) => {
78
+ if (node && node.type === "MemberExpression" && node.object.type === "ThisExpression" && node.property.type === "Identifier" && node.property.name === "refs") {
79
+ matches.push(createPatternMatch(node, "stringRefs", code));
80
+ }
81
+ });
82
+ return matches;
83
+ }
84
+ function findFindDOMNode(ast, code) {
85
+ const matches = [];
86
+ walk(ast, (node) => {
87
+ if (node && node.type === "CallExpression") {
88
+ if (node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && (node.callee.object.name === "ReactDOM" || node.callee.object.name === "React") && node.callee.property.type === "Identifier" && node.callee.property.name === "findDOMNode") {
89
+ matches.push(createPatternMatch(node, "findDOMNode", code));
90
+ } else if (node.callee.type === "Identifier" && node.callee.name === "findDOMNode") {
91
+ matches.push(createPatternMatch(node, "findDOMNode", code));
92
+ }
93
+ }
94
+ });
95
+ return matches;
96
+ }
97
+ function findReactDOMRender(ast, code) {
98
+ const matches = [];
99
+ walk(ast, (node) => {
100
+ if (node && node.type === "CallExpression") {
101
+ if (node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "ReactDOM" && node.callee.property.type === "Identifier" && node.callee.property.name === "render" && (node.arguments.length === 2 || node.arguments.length === 3)) {
102
+ matches.push(createPatternMatch(node, "ReactDOM.render", code));
103
+ } else if (node.callee.type === "Identifier" && node.callee.name === "render" && (node.arguments.length === 2 || node.arguments.length === 3)) {
104
+ matches.push(createPatternMatch(node, "ReactDOM.render", code));
105
+ }
106
+ }
107
+ });
108
+ return matches;
109
+ }
110
+ function findReactDOMUnmountComponentAtNode(ast, code) {
111
+ const matches = [];
112
+ walk(ast, (node) => {
113
+ if (node && node.type === "CallExpression") {
114
+ if (node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "ReactDOM" && node.callee.property.type === "Identifier" && node.callee.property.name === "unmountComponentAtNode" && node.arguments.length === 1) {
115
+ matches.push(createPatternMatch(node, "ReactDOM.unmountComponentAtNode", code));
116
+ } else if (node.callee.type === "Identifier" && node.callee.name === "unmountComponentAtNode" && node.arguments.length === 1) {
117
+ matches.push(createPatternMatch(node, "ReactDOM.unmountComponentAtNode", code));
118
+ }
119
+ }
120
+ });
121
+ return matches;
122
+ }
123
+ function findCreateFactory(ast, code) {
124
+ const matches = [];
125
+ walk(ast, (node) => {
126
+ if (node && node.type === "CallExpression") {
127
+ if (node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "React" && node.callee.property.type === "Identifier" && node.callee.property.name === "createFactory" && node.arguments.length === 1) {
128
+ matches.push(createPatternMatch(node, "createFactory", code));
129
+ } else if (node.callee.type === "Identifier" && node.callee.name === "createFactory" && node.arguments.length === 1) {
130
+ matches.push(createPatternMatch(node, "createFactory", code));
131
+ }
132
+ }
133
+ });
134
+ return matches;
135
+ }
136
+ function createPatternMatch(node, pattern, code) {
137
+ return {
138
+ pattern,
139
+ line: node.loc.start.line,
140
+ column: node.loc.start.column,
141
+ matched: code.slice(node.range[0], node.range[1]),
142
+ context: getSurroundingCode(code, node)
143
+ };
144
+ }
145
+
146
+ export { createPatternMatch, findContextTypes, findCreateFactory, findDefaultProps, findFindDOMNode, findGetChildContext, findJsxRuntimeImports, findPatternMatches, findPropTypes, findReactDOMRender, findReactDOMUnmountComponentAtNode, findSecretInternals, findStringRefs };
@@ -0,0 +1,132 @@
1
+ import { output } from '../utils/output.js';
2
+
3
+ function consoleReporter(results) {
4
+ if (results.summary.totalIssues === 0) {
5
+ output.success({
6
+ title: "No React 19 breaking changes detected!",
7
+ body: [
8
+ `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
9
+ "Good news! Your plugin appears to be compatible with React 19.",
10
+ "",
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!"
20
+ ]
21
+ });
22
+ return;
23
+ }
24
+ output.error({
25
+ title: "React 19 breaking changes detected!",
26
+ body: [
27
+ `Plugin: ${results.plugin.name} version: ${results.plugin.version}`,
28
+ "Your plugin appears to be incompatible with React 19. Note that this tool can give false positives, please review the issues carefully."
29
+ ]
30
+ });
31
+ const criticalSourceIssues = results.issues.critical.filter((issue) => issue.location.type === "source");
32
+ const warningSourceIssues = results.issues.warnings.filter((issue) => issue.location.type === "source");
33
+ const sourceCodeIssues = [...criticalSourceIssues, ...warningSourceIssues];
34
+ if (sourceCodeIssues.length > 0) {
35
+ output.error({
36
+ title: "Source code issues",
37
+ body: ["The following source code issues were found. Please refer to the suggestions to help resolve them."],
38
+ withPrefix: false
39
+ });
40
+ const groupedByPattern = groupByPattern(sourceCodeIssues);
41
+ for (const [pattern, issues] of Object.entries(groupedByPattern)) {
42
+ const uniqueFileLocations = new Set(issues.map((issue) => issue.location.file));
43
+ const fileLocationList = output.bulletList(Array.from(uniqueFileLocations));
44
+ const patternInfo = issues[0];
45
+ output.error({
46
+ title: `${pattern} (${patternInfo.problem})`,
47
+ body: [
48
+ `fix: ${patternInfo.fix.description}.`,
49
+ ...patternInfo.fix.before ? [`before: ${output.formatCode(patternInfo.fix.before)}`] : [],
50
+ ...patternInfo.fix.after ? [`after: ${output.formatCode(patternInfo.fix.after)}`] : [],
51
+ `link: ${output.formatUrl(patternInfo.link)}`,
52
+ "",
53
+ "found in:",
54
+ ...fileLocationList
55
+ ],
56
+ withPrefix: false
57
+ });
58
+ output.addHorizontalLine("red");
59
+ }
60
+ }
61
+ const criticalDependencyIssues = results.issues.critical.filter((issue) => issue.location.type === "dependency");
62
+ const warningDependencyIssues = results.issues.warnings.filter((issue) => issue.location.type === "dependency");
63
+ const dependencyIssues = [...criticalDependencyIssues, ...warningDependencyIssues];
64
+ if (dependencyIssues.length > 0) {
65
+ output.error({
66
+ title: "Dependency issues",
67
+ body: [
68
+ "The following issues were found in bundled dependencies.",
69
+ "We recommend checking for dependency updates which are compatible with React 19."
70
+ ],
71
+ withPrefix: false
72
+ });
73
+ const groupedByPackage = groupByPackage(dependencyIssues);
74
+ for (const [pkgName, issues] of Object.entries(groupedByPackage)) {
75
+ const uniquePatterns = new Set(
76
+ issues.map(
77
+ (issue) => `${issue.problem}. ${issue.fix.description}. Further information: ${output.formatUrl(issue.link)}`
78
+ )
79
+ );
80
+ const uniqueFileLocations = new Set(issues.map((issue) => issue.location.file));
81
+ const fileLocationList = output.bulletList(Array.from(uniqueFileLocations));
82
+ const patternInfoList = output.bulletList(Array.from(uniquePatterns));
83
+ output.error({
84
+ title: `\u{1F4E6} ${pkgName}`,
85
+ body: ["issues found:", ...patternInfoList, "", "found in:", ...fileLocationList],
86
+ withPrefix: false
87
+ });
88
+ output.addHorizontalLine("red");
89
+ }
90
+ }
91
+ output.error({
92
+ 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
+ ],
104
+ withPrefix: false
105
+ });
106
+ }
107
+ function groupByPattern(issues) {
108
+ return issues.reduce(
109
+ (groups, issue) => {
110
+ if (!groups[issue.pattern]) {
111
+ groups[issue.pattern] = [];
112
+ }
113
+ groups[issue.pattern].push(issue);
114
+ return groups;
115
+ },
116
+ {}
117
+ );
118
+ }
119
+ function groupByPackage(issues) {
120
+ return issues.reduce(
121
+ (groups, issue) => {
122
+ if (!groups[issue.packageName]) {
123
+ groups[issue.packageName] = [];
124
+ }
125
+ groups[issue.packageName].push(issue);
126
+ return groups;
127
+ },
128
+ {}
129
+ );
130
+ }
131
+
132
+ export { consoleReporter };
@@ -0,0 +1,11 @@
1
+ function jsonReporter(results) {
2
+ const {
3
+ // Remove the duplicated dependencies issues
4
+ issues: { dependencies, ...issues },
5
+ ...rest
6
+ } = results;
7
+ const flattenedIssues = [...issues.critical, ...issues.warnings];
8
+ console.log(JSON.stringify({ ...rest, issues: flattenedIssues }, null, 2));
9
+ }
10
+
11
+ export { jsonReporter };
@@ -0,0 +1,128 @@
1
+ import { getPattern } from './patterns/definitions.js';
2
+ import { getPluginJson } from './utils/plugin.js';
3
+ import path from 'node:path';
4
+
5
+ function generateAnalysisResults(matches, pluginRoot, depContext) {
6
+ const filtered = filterMatches(matches);
7
+ const pluginJson = getPluginJson(pluginRoot);
8
+ const sourceMatches = filtered.filter((m) => m.type === "source");
9
+ const dependencyMatches = filtered.filter((m) => m.type === "dependency");
10
+ const criticalMatches = filtered.filter((m) => {
11
+ const pattern = getPattern(m.pattern);
12
+ return pattern?.impactLevel === "critical";
13
+ });
14
+ const warningMatches = filtered.filter((m) => {
15
+ const pattern = getPattern(m.pattern);
16
+ return pattern?.impactLevel === "warning";
17
+ });
18
+ const critical = criticalMatches.map((m) => generateResult(m));
19
+ const warnings = warningMatches.map((m) => generateResult(m));
20
+ const dependencies = buildDependencyIssues(dependencyMatches, depContext);
21
+ const totalIssues = filtered.length;
22
+ const affectedDeps = new Set(
23
+ dependencyMatches.map((m) => m.packageName).filter((name) => name !== void 0)
24
+ );
25
+ return {
26
+ plugin: {
27
+ id: pluginJson?.id || "",
28
+ name: pluginJson?.name || "",
29
+ version: pluginJson?.info.version || "",
30
+ type: pluginJson?.type || ""
31
+ },
32
+ summary: {
33
+ totalIssues,
34
+ critical: critical.length,
35
+ warnings: warnings.length,
36
+ sourceIssuesCount: sourceMatches.length,
37
+ dependencyIssuesCount: dependencyMatches.length,
38
+ status: totalIssues > 0 ? "action_required" : "no_action_required",
39
+ affectedDependencies: Array.from(affectedDeps)
40
+ },
41
+ issues: {
42
+ critical,
43
+ warnings,
44
+ dependencies
45
+ }
46
+ };
47
+ }
48
+ function filterMatches(matches) {
49
+ const filtered = matches.filter((match) => {
50
+ if (match.type === "source" && (match.confidence === "none" || match.confidence === "unknown")) {
51
+ return false;
52
+ }
53
+ if (match.type === "source") {
54
+ const pattern = getPattern(match.pattern);
55
+ if (pattern?.functionComponentOnly && match.componentType !== "function") {
56
+ return false;
57
+ }
58
+ }
59
+ if (match.type === "dependency" && match.pattern === "__SECRET_INTERNALS" && match.packageName === "react" && (match.sourceFile.includes("jsx-runtime") || match.sourceFile.includes("jsx-dev-runtime"))) {
60
+ return false;
61
+ }
62
+ return true;
63
+ });
64
+ return filtered;
65
+ }
66
+ function generateResult(match) {
67
+ const pattern = getPattern(match.pattern);
68
+ if (!pattern) {
69
+ throw new Error(`Pattern not found: ${match.pattern}`);
70
+ }
71
+ const cleanFilePath = cleanSourceFilePath(match.sourceFile);
72
+ const result = {
73
+ pattern: match.pattern,
74
+ severity: pattern.severity,
75
+ impactLevel: pattern.impactLevel,
76
+ location: {
77
+ type: match.type,
78
+ file: path.join(process.cwd(), cleanFilePath),
79
+ line: match.sourceLine,
80
+ column: match.sourceColumn
81
+ },
82
+ problem: pattern.description,
83
+ fix: {
84
+ description: pattern.fix?.description || "",
85
+ before: pattern.fix?.before || "",
86
+ after: pattern.fix?.after || ""
87
+ },
88
+ link: pattern.link || ""
89
+ };
90
+ if (match.type === "dependency") {
91
+ result.packageName = match.packageName;
92
+ result.rootDependency = match.rootDependency;
93
+ }
94
+ return result;
95
+ }
96
+ function buildDependencyIssues(dependencyMatches, depContext) {
97
+ const byPackage = /* @__PURE__ */ new Map();
98
+ for (const match of dependencyMatches) {
99
+ if (match.type === "dependency" && match.packageName) {
100
+ const existing = byPackage.get(match.packageName) || [];
101
+ existing.push(match);
102
+ byPackage.set(match.packageName, existing);
103
+ }
104
+ }
105
+ const issues = [];
106
+ for (const [packageName, matches] of byPackage) {
107
+ const rootDep = matches[0].rootDependency || packageName;
108
+ const version = depContext.getVersion(packageName) || null;
109
+ issues.push({
110
+ packageName,
111
+ version: version || "unknown",
112
+ rootDependency: rootDep,
113
+ issues: matches.map((m) => generateResult(m))
114
+ });
115
+ }
116
+ return issues;
117
+ }
118
+ function cleanSourceFilePath(filePath) {
119
+ let cleanPath = filePath;
120
+ if (filePath.includes("node_modules")) {
121
+ cleanPath = filePath.replace(/webpack:\/\/[^/]+\/node_modules\//, "./node_modules/");
122
+ } else {
123
+ cleanPath = filePath.replace(/webpack:\/\/[^/]+/, "./src/");
124
+ }
125
+ return cleanPath;
126
+ }
127
+
128
+ export { generateAnalysisResults };
@@ -0,0 +1,80 @@
1
+ import { SourceMapConsumer } from 'source-map';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ async function extractSourcesFromMap(sourcemapFilePath) {
5
+ const mapContent = await readFile(sourcemapFilePath, "utf8");
6
+ const sourceMap = JSON.parse(mapContent);
7
+ const consumer = await new SourceMapConsumer(sourceMap);
8
+ const sourceFiles = [];
9
+ const bundledFilePath = sourcemapFilePath.replace(".map", "");
10
+ for (const source of consumer.sources) {
11
+ if (shouldSkipSource(source)) {
12
+ continue;
13
+ }
14
+ const content = consumer.sourceContentFor(source, true);
15
+ if (!content) {
16
+ console.warn(`No sourceContent for ${source} in ${sourcemapFilePath}`);
17
+ continue;
18
+ }
19
+ const classified = classifySource(source);
20
+ sourceFiles.push({
21
+ path: source,
22
+ content,
23
+ type: classified.type,
24
+ packageName: classified.packageName,
25
+ bundledFilePath
26
+ });
27
+ }
28
+ consumer.destroy();
29
+ return sourceFiles;
30
+ }
31
+ async function extractAllSources(sourcemapFilePaths) {
32
+ const allSources = [];
33
+ const seenPaths = /* @__PURE__ */ new Set();
34
+ for (const mapFilePath of sourcemapFilePaths) {
35
+ try {
36
+ const sources = await extractSourcesFromMap(mapFilePath);
37
+ for (const source of sources) {
38
+ if (!seenPaths.has(source.path)) {
39
+ seenPaths.add(source.path);
40
+ allSources.push(source);
41
+ }
42
+ }
43
+ } catch (error) {
44
+ console.error(`Failed to extract sources from ${mapFilePath}:`, error);
45
+ }
46
+ }
47
+ return allSources;
48
+ }
49
+ function classifySource(sourcePath) {
50
+ if (sourcePath.includes("node_modules")) {
51
+ const packageName = getPackageName(sourcePath);
52
+ return { type: "dependency", packageName };
53
+ }
54
+ return { type: "source" };
55
+ }
56
+ function getPackageName(sourcePath) {
57
+ const pnpmMatch = sourcePath.match(/\.pnpm\/[^/]+\/node_modules\/((?:@[^/]+\/)?[^/]+)/);
58
+ if (pnpmMatch) {
59
+ return pnpmMatch[1];
60
+ }
61
+ const match = sourcePath.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
62
+ return match ? match[1] : void 0;
63
+ }
64
+ function shouldSkipSource(sourcePath) {
65
+ if (sourcePath.includes("webpack/bootstrap") || sourcePath.includes("webpack/runtime")) {
66
+ return true;
67
+ }
68
+ if (sourcePath.match(/^webpack:\/\/[^/]+\/?$/)) {
69
+ return true;
70
+ }
71
+ if (sourcePath.includes("/external%20amd")) {
72
+ return true;
73
+ }
74
+ if (sourcePath.includes("node_modules/grafana-public-path.js")) {
75
+ return true;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ export { extractAllSources, extractSourcesFromMap };
@@ -0,0 +1,88 @@
1
+ import { walk } from './ast.js';
2
+
3
+ function analyzeConfidence(ast) {
4
+ let score = 0;
5
+ if (hasReactImport(ast)) {
6
+ score += 3;
7
+ }
8
+ if (hasJSX(ast)) {
9
+ score += 3;
10
+ }
11
+ if (hasHooks(ast)) {
12
+ score += 2;
13
+ }
14
+ if (hasReactComponent(ast)) {
15
+ score += 3;
16
+ }
17
+ if (score >= 6) {
18
+ return "high";
19
+ }
20
+ if (score >= 3) {
21
+ return "medium";
22
+ }
23
+ if (score >= 1) {
24
+ return "low";
25
+ }
26
+ return "none";
27
+ }
28
+ function analyzeComponentType(ast) {
29
+ if (hasReactComponent(ast)) {
30
+ return "class";
31
+ }
32
+ if (hasFunctionComponentPattern(ast)) {
33
+ return "function";
34
+ }
35
+ return "unknown";
36
+ }
37
+ function hasReactComponent(ast) {
38
+ let found = false;
39
+ walk(ast, (node) => {
40
+ if (node.type === "ClassDeclaration" && node.superClass?.type === "MemberExpression" && node.superClass.object.type === "Identifier" && node.superClass.object.name === "React" && node.superClass.property.type === "Identifier" && (node.superClass.property.name === "Component" || node.superClass.property.name === "PureComponent")) {
41
+ found = true;
42
+ }
43
+ });
44
+ return found;
45
+ }
46
+ function hasFunctionComponentPattern(ast) {
47
+ const hasJSXInFile = hasJSX(ast);
48
+ const hasHooksInFile = hasHooks(ast);
49
+ if (!hasJSXInFile && !hasHooksInFile) {
50
+ return false;
51
+ }
52
+ let found = false;
53
+ walk(ast, (node) => {
54
+ if (node.type === "FunctionDeclaration" || node.type === "ArrowFunctionExpression") {
55
+ found = true;
56
+ }
57
+ });
58
+ return found;
59
+ }
60
+ function hasReactImport(ast) {
61
+ let found = false;
62
+ walk(ast, (node) => {
63
+ if (node.type === "ImportDeclaration" && node.source.type === "Literal" && (node.source.value === "react" || node.source.value === "react-dom")) {
64
+ found = true;
65
+ }
66
+ });
67
+ return found;
68
+ }
69
+ function hasJSX(ast) {
70
+ let found = false;
71
+ walk(ast, (node) => {
72
+ if (node.type === "JSXElement" || node.type === "JSXFragment") {
73
+ found = true;
74
+ }
75
+ });
76
+ return found;
77
+ }
78
+ function hasHooks(ast) {
79
+ let found = false;
80
+ walk(ast, (node) => {
81
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name.startsWith("use")) {
82
+ found = true;
83
+ }
84
+ });
85
+ return found;
86
+ }
87
+
88
+ export { analyzeComponentType, analyzeConfidence, hasFunctionComponentPattern, hasReactComponent };
@@ -0,0 +1,20 @@
1
+ function walk(node, callback) {
2
+ callback(node);
3
+ for (const key in node) {
4
+ if (node[key] && typeof node[key] === "object") {
5
+ if (Array.isArray(node[key])) {
6
+ node[key].forEach((child) => walk(child, callback));
7
+ } else if (node[key].type) {
8
+ walk(node[key], callback);
9
+ }
10
+ }
11
+ }
12
+ }
13
+ function getSurroundingCode(code, node) {
14
+ const lines = code.split("\n");
15
+ const startLine = Math.max(0, node.loc.start.line - 3);
16
+ const endLine = Math.min(lines.length, node.loc.end.line + 2);
17
+ return lines.slice(startLine, endLine).join("\n");
18
+ }
19
+
20
+ export { getSurroundingCode, walk };