@grafana/react-detect 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # v0.4.0 (Thu Jan 15 2026)
2
+
3
+ #### 🚀 Enhancement
4
+
5
+ - feat: allow skipping dependency and build tooling analysis [#2397](https://github.com/grafana/plugin-tools/pull/2397) ([@jackw](https://github.com/jackw))
6
+
7
+ #### Authors: 1
8
+
9
+ - Jack Westbrook ([@jackw](https://github.com/jackw))
10
+
11
+ ---
12
+
1
13
  # v0.3.0 (Thu Jan 15 2026)
2
14
 
3
15
  #### 🚀 Enhancement
package/README.md CHANGED
@@ -21,3 +21,5 @@ npx @grafana/react-detect
21
21
 
22
22
  - `--pluginRoot`: Pass the root directory of the plugin. Defaults to the current working directory.
23
23
  - `--json`: Output the results as json.
24
+ - `--skipBuildTooling`: Skip webpack configuration checks (jsx-runtime externalization). Useful when build config is missing or not applicable.
25
+ - `--skipDependencies`: Skip dependency tree analysis. Useful when lockfile is missing or for faster analysis of source code only.
package/dist/bin/run.js CHANGED
@@ -3,7 +3,15 @@ import minimist from 'minimist';
3
3
  import { detect19 } from '../commands/detect19.js';
4
4
 
5
5
  const args = process.argv.slice(2);
6
- const argv = minimist(args);
6
+ const argv = minimist(args, {
7
+ boolean: ["json", "skipBuildTooling", "skipDependencies"],
8
+ string: ["pluginRoot"],
9
+ default: {
10
+ json: false,
11
+ skipBuildTooling: false,
12
+ skipDependencies: false
13
+ }
14
+ });
7
15
  const commands = {
8
16
  detect19
9
17
  };
@@ -10,16 +10,36 @@ import { output } from '../utils/output.js';
10
10
  async function detect19(argv) {
11
11
  try {
12
12
  const pluginRoot = argv.pluginRoot || process.cwd();
13
+ const skipDependencies = argv.skipDependencies || false;
14
+ const skipBuildTooling = argv.skipBuildTooling || false;
13
15
  const allMatches = await getAllMatches(pluginRoot);
14
- const depContext = new DependencyContext();
15
- await depContext.loadDependencies(pluginRoot);
16
- const matchesWithRootDependency = allMatches.map((match) => {
16
+ let depContext = null;
17
+ if (!skipDependencies) {
18
+ depContext = new DependencyContext();
19
+ try {
20
+ await depContext.loadDependencies(pluginRoot);
21
+ } catch (error) {
22
+ output.warning({
23
+ title: "Failed to load dependencies",
24
+ body: [
25
+ error.message,
26
+ "Continuing without dependency analysis.",
27
+ "Use --skipDependencies to suppress this warning."
28
+ ]
29
+ });
30
+ depContext = null;
31
+ }
32
+ }
33
+ const matchesWithRootDependency = depContext === null ? allMatches : allMatches.map((match) => {
17
34
  if (match.type === "dependency" && match.packageName) {
18
35
  return { ...match, rootDependency: depContext.findRootDependency(match.packageName) };
19
36
  }
20
37
  return match;
21
38
  });
22
- const results = generateAnalysisResults(matchesWithRootDependency, pluginRoot, depContext);
39
+ const results = generateAnalysisResults(matchesWithRootDependency, pluginRoot, depContext, {
40
+ skipBuildTooling,
41
+ skipDependencies
42
+ });
23
43
  if (argv.json) {
24
44
  jsonReporter(results);
25
45
  } else {
@@ -14,6 +14,25 @@ const summaryMsg = [
14
14
  `Grafana developer documentation: ${output.formatUrl("https://grafana.com/developers/plugin-tools/set-up/set-up-docker")}`
15
15
  ];
16
16
  function consoleReporter(results) {
17
+ if (!results.summary.analyzedBuildTooling || !results.summary.analyzedDependencies) {
18
+ const skippedFeatures = [];
19
+ if (!results.summary.analyzedBuildTooling) {
20
+ skippedFeatures.push("Build tooling analysis (webpack config checks)");
21
+ }
22
+ if (!results.summary.analyzedDependencies) {
23
+ skippedFeatures.push("Dependency tree analysis (lockfile parsing)");
24
+ }
25
+ output.warning({
26
+ title: "Analysis Skipped",
27
+ body: [
28
+ "The following analyses were skipped:",
29
+ ...output.bulletList(skippedFeatures),
30
+ "",
31
+ "Results may be incomplete. Remove skip flags for full analysis."
32
+ ]
33
+ });
34
+ output.addHorizontalLine("yellow");
35
+ }
17
36
  if (results.summary.totalIssues === 0) {
18
37
  output.success({
19
38
  title: "No React 19 breaking changes detected.",
package/dist/results.js CHANGED
@@ -2,8 +2,8 @@ import { getPattern } from './patterns/definitions.js';
2
2
  import { getPluginJson, hasExternalisedJsxRuntime } from './utils/plugin.js';
3
3
  import path from 'node:path';
4
4
 
5
- function generateAnalysisResults(matches, pluginRoot, depContext) {
6
- const filtered = filterMatches(matches);
5
+ function generateAnalysisResults(matches, pluginRoot, depContext, options = { skipBuildTooling: false, skipDependencies: false }) {
6
+ const filtered = filterMatches(matches, options.skipBuildTooling);
7
7
  const pluginJson = getPluginJson(pluginRoot);
8
8
  const sourceMatches = filtered.filter((m) => m.type === "source");
9
9
  const dependencyMatches = filtered.filter((m) => m.type === "dependency");
@@ -36,7 +36,9 @@ function generateAnalysisResults(matches, pluginRoot, depContext) {
36
36
  sourceIssuesCount: sourceMatches.length,
37
37
  dependencyIssuesCount: dependencyMatches.length,
38
38
  status: totalIssues > 0 ? "action_required" : "no_action_required",
39
- affectedDependencies: Array.from(affectedDeps)
39
+ affectedDependencies: Array.from(affectedDeps),
40
+ analyzedBuildTooling: !options.skipBuildTooling,
41
+ analyzedDependencies: !options.skipDependencies
40
42
  },
41
43
  issues: {
42
44
  critical,
@@ -45,8 +47,8 @@ function generateAnalysisResults(matches, pluginRoot, depContext) {
45
47
  }
46
48
  };
47
49
  }
48
- function filterMatches(matches) {
49
- const externalisedJsxRuntime = hasExternalisedJsxRuntime();
50
+ function filterMatches(matches, skipBuildTooling) {
51
+ const externalisedJsxRuntime = skipBuildTooling ? false : hasExternalisedJsxRuntime();
50
52
  const filtered = matches.filter((match) => {
51
53
  if (match.type === "source" && (match.confidence === "none" || match.confidence === "unknown")) {
52
54
  return false;
@@ -109,7 +111,7 @@ function buildDependencyIssues(dependencyMatches, depContext) {
109
111
  const issues = [];
110
112
  for (const [packageName, matches] of byPackage) {
111
113
  const rootDep = matches[0].rootDependency || packageName;
112
- const version = depContext.getVersion(packageName) || null;
114
+ const version = depContext?.getVersion(packageName) || null;
113
115
  issues.push({
114
116
  packageName,
115
117
  version: version || "unknown",
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.3.0",
4
+ "version": "0.4.0",
5
5
  "repository": {
6
6
  "directory": "packages/react-detect",
7
7
  "url": "https://github.com/grafana/plugin-tools"
@@ -40,5 +40,5 @@
40
40
  "engines": {
41
41
  "node": ">=20"
42
42
  },
43
- "gitHead": "41320751786a4b7fa6c5addf92fb7f81a4df6702"
43
+ "gitHead": "1875c3b665a6bb447a6d2fdea23df11de9c066f2"
44
44
  }
package/src/bin/run.ts CHANGED
@@ -4,7 +4,15 @@ import minimist from 'minimist';
4
4
  import { detect19 } from '../commands/detect19.js';
5
5
 
6
6
  const args = process.argv.slice(2);
7
- const argv = minimist(args);
7
+ const argv = minimist(args, {
8
+ boolean: ['json', 'skipBuildTooling', 'skipDependencies'],
9
+ string: ['pluginRoot'],
10
+ default: {
11
+ json: false,
12
+ skipBuildTooling: false,
13
+ skipDependencies: false,
14
+ },
15
+ });
8
16
 
9
17
  const commands: Record<string, (argv: minimist.ParsedArgs) => Promise<void>> = {
10
18
  detect19,
@@ -13,19 +13,46 @@ import { output } from '../utils/output.js';
13
13
  export async function detect19(argv: minimist.ParsedArgs) {
14
14
  try {
15
15
  const pluginRoot = argv.pluginRoot || process.cwd();
16
+ const skipDependencies = argv.skipDependencies || false;
17
+ const skipBuildTooling = argv.skipBuildTooling || false;
16
18
 
17
19
  const allMatches = await getAllMatches(pluginRoot);
18
- const depContext = new DependencyContext();
19
- await depContext.loadDependencies(pluginRoot);
20
20
 
21
- const matchesWithRootDependency = allMatches.map((match) => {
22
- if (match.type === 'dependency' && match.packageName) {
23
- return { ...match, rootDependency: depContext.findRootDependency(match.packageName) };
21
+ // Conditionally load dependencies
22
+ let depContext: DependencyContext | null = null;
23
+ if (!skipDependencies) {
24
+ depContext = new DependencyContext();
25
+ try {
26
+ await depContext.loadDependencies(pluginRoot);
27
+ } catch (error) {
28
+ // Log warning but continue with null context
29
+ output.warning({
30
+ title: 'Failed to load dependencies',
31
+ body: [
32
+ (error as Error).message,
33
+ 'Continuing without dependency analysis.',
34
+ 'Use --skipDependencies to suppress this warning.',
35
+ ],
36
+ });
37
+ depContext = null;
24
38
  }
25
- return match;
26
- });
39
+ }
27
40
 
28
- const results = generateAnalysisResults(matchesWithRootDependency, pluginRoot, depContext);
41
+ // Enrich matches with root dependency only if we have context
42
+ const matchesWithRootDependency =
43
+ depContext === null
44
+ ? allMatches
45
+ : allMatches.map((match) => {
46
+ if (match.type === 'dependency' && match.packageName) {
47
+ return { ...match, rootDependency: depContext.findRootDependency(match.packageName) };
48
+ }
49
+ return match;
50
+ });
51
+
52
+ const results = generateAnalysisResults(matchesWithRootDependency, pluginRoot, depContext, {
53
+ skipBuildTooling,
54
+ skipDependencies,
55
+ });
29
56
 
30
57
  if (argv.json) {
31
58
  jsonReporter(results);
@@ -16,6 +16,27 @@ const summaryMsg = [
16
16
  ];
17
17
 
18
18
  export function consoleReporter(results: PluginAnalysisResults) {
19
+ if (!results.summary.analyzedBuildTooling || !results.summary.analyzedDependencies) {
20
+ const skippedFeatures: string[] = [];
21
+ if (!results.summary.analyzedBuildTooling) {
22
+ skippedFeatures.push('Build tooling analysis (webpack config checks)');
23
+ }
24
+ if (!results.summary.analyzedDependencies) {
25
+ skippedFeatures.push('Dependency tree analysis (lockfile parsing)');
26
+ }
27
+
28
+ output.warning({
29
+ title: 'Analysis Skipped',
30
+ body: [
31
+ 'The following analyses were skipped:',
32
+ ...output.bulletList(skippedFeatures),
33
+ '',
34
+ 'Results may be incomplete. Remove skip flags for full analysis.',
35
+ ],
36
+ });
37
+ output.addHorizontalLine('yellow');
38
+ }
39
+
19
40
  if (results.summary.totalIssues === 0) {
20
41
  output.success({
21
42
  title: 'No React 19 breaking changes detected.',
package/src/results.ts CHANGED
@@ -5,12 +5,18 @@ import { getPluginJson, hasExternalisedJsxRuntime } from './utils/plugin.js';
5
5
  import { DependencyContext } from './utils/dependencies.js';
6
6
  import path from 'node:path';
7
7
 
8
+ export interface AnalysisOptions {
9
+ skipBuildTooling: boolean;
10
+ skipDependencies: boolean;
11
+ }
12
+
8
13
  export function generateAnalysisResults(
9
14
  matches: AnalyzedMatch[],
10
15
  pluginRoot: string,
11
- depContext: DependencyContext
16
+ depContext: DependencyContext | null,
17
+ options: AnalysisOptions = { skipBuildTooling: false, skipDependencies: false }
12
18
  ): PluginAnalysisResults {
13
- const filtered = filterMatches(matches);
19
+ const filtered = filterMatches(matches, options.skipBuildTooling);
14
20
  const pluginJson = getPluginJson(pluginRoot);
15
21
  const sourceMatches = filtered.filter((m) => m.type === 'source');
16
22
  const dependencyMatches = filtered.filter((m) => m.type === 'dependency');
@@ -49,6 +55,8 @@ export function generateAnalysisResults(
49
55
  dependencyIssuesCount: dependencyMatches.length,
50
56
  status: totalIssues > 0 ? 'action_required' : 'no_action_required',
51
57
  affectedDependencies: Array.from(affectedDeps),
58
+ analyzedBuildTooling: !options.skipBuildTooling,
59
+ analyzedDependencies: !options.skipDependencies,
52
60
  },
53
61
  issues: {
54
62
  critical,
@@ -58,8 +66,9 @@ export function generateAnalysisResults(
58
66
  };
59
67
  }
60
68
 
61
- function filterMatches(matches: AnalyzedMatch[]): AnalyzedMatch[] {
62
- const externalisedJsxRuntime = hasExternalisedJsxRuntime();
69
+ function filterMatches(matches: AnalyzedMatch[], skipBuildTooling: boolean): AnalyzedMatch[] {
70
+ // Only check webpack config if NOT skipping build tooling
71
+ const externalisedJsxRuntime = skipBuildTooling ? false : hasExternalisedJsxRuntime();
63
72
  const filtered = matches.filter((match) => {
64
73
  // TODO: add mode for strict / loose filtering
65
74
  if (match.type === 'source' && (match.confidence === 'none' || match.confidence === 'unknown')) {
@@ -139,7 +148,10 @@ function generateResult(match: AnalyzedMatch): AnalysisResult {
139
148
  /**
140
149
  * Build DependencyIssue objects grouped by package
141
150
  */
142
- function buildDependencyIssues(dependencyMatches: AnalyzedMatch[], depContext: DependencyContext): DependencyIssue[] {
151
+ function buildDependencyIssues(
152
+ dependencyMatches: AnalyzedMatch[],
153
+ depContext: DependencyContext | null
154
+ ): DependencyIssue[] {
143
155
  // Group by package
144
156
  const byPackage = new Map<string, AnalyzedMatch[]>();
145
157
  for (const match of dependencyMatches) {
@@ -154,7 +166,8 @@ function buildDependencyIssues(dependencyMatches: AnalyzedMatch[], depContext: D
154
166
  const issues: DependencyIssue[] = [];
155
167
  for (const [packageName, matches] of byPackage) {
156
168
  const rootDep = matches[0].rootDependency || packageName;
157
- const version = depContext.getVersion(packageName) || null;
169
+ // Handle null depContext gracefully with optional chaining
170
+ const version = depContext?.getVersion(packageName) || null;
158
171
 
159
172
  issues.push({
160
173
  packageName,
@@ -32,6 +32,8 @@ export interface PluginAnalysisResults {
32
32
  dependencyIssuesCount: number;
33
33
  status: 'action_required' | 'no_action_required';
34
34
  affectedDependencies: string[];
35
+ analyzedBuildTooling: boolean;
36
+ analyzedDependencies: boolean;
35
37
  };
36
38
  issues: {
37
39
  critical: AnalysisResult[];