@grafana/react-detect 0.6.3 → 0.6.4

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.6.4 (Wed Apr 22 2026)
2
+
3
+ #### 🐛 Bug Fix
4
+
5
+ - feat: add --distDir arg to specify built plugin directory [#2558](https://github.com/grafana/plugin-tools/pull/2558) ([@jackw](https://github.com/jackw))
6
+
7
+ #### Authors: 1
8
+
9
+ - Jack Westbrook ([@jackw](https://github.com/jackw))
10
+
11
+ ---
12
+
1
13
  # v0.6.3 (Tue Apr 07 2026)
2
14
 
3
15
  #### 🐛 Bug Fix
package/dist/bin/run.js CHANGED
@@ -5,7 +5,7 @@ import { detect19 } from '../commands/detect19.js';
5
5
  const args = process.argv.slice(2);
6
6
  const argv = minimist(args, {
7
7
  boolean: ["json", "skipBuildTooling", "skipDependencies", "noErrorExitCode"],
8
- string: ["pluginRoot"],
8
+ string: ["pluginRoot", "distDir"],
9
9
  default: {
10
10
  json: false,
11
11
  skipBuildTooling: false,
@@ -1,3 +1,4 @@
1
+ import { join } from 'node:path';
1
2
  import { findSourceMapFiles } from '../file-scanner.js';
2
3
  import { generateAnalysisResults } from '../results.js';
3
4
  import { DependencyContext } from '../utils/dependencies.js';
@@ -10,10 +11,11 @@ import { output } from '../utils/output.js';
10
11
  async function detect19(argv) {
11
12
  try {
12
13
  const pluginRoot = argv.pluginRoot || process.cwd();
14
+ const distDir = argv.distDir || join(pluginRoot, "dist");
13
15
  const skipDependencies = argv.skipDependencies || false;
14
16
  const skipBuildTooling = argv.skipBuildTooling || false;
15
17
  const jsonOutput = argv.json || false;
16
- const allMatches = await getAllMatches(pluginRoot);
18
+ const allMatches = await getAllMatches(distDir);
17
19
  let depContext = null;
18
20
  if (!skipDependencies) {
19
21
  depContext = new DependencyContext();
@@ -39,7 +41,7 @@ async function detect19(argv) {
39
41
  }
40
42
  return match;
41
43
  });
42
- const results = generateAnalysisResults(matchesWithRootDependency, pluginRoot, depContext, {
44
+ const results = generateAnalysisResults(matchesWithRootDependency, distDir, pluginRoot, depContext, {
43
45
  skipBuildTooling,
44
46
  skipDependencies
45
47
  });
@@ -61,10 +63,10 @@ async function detect19(argv) {
61
63
  process.exit(1);
62
64
  }
63
65
  }
64
- async function getAllMatches(pluginRoot) {
65
- const sourcemapPaths = await findSourceMapFiles(pluginRoot);
66
+ async function getAllMatches(distDir) {
67
+ const sourcemapPaths = await findSourceMapFiles(distDir);
66
68
  if (sourcemapPaths.length === 0) {
67
- throw new Error("No source map files found in dist directory. Make sure to build your plugin first.");
69
+ throw new Error(`No source map files found in "${distDir}". Make sure to build your plugin first.`);
68
70
  }
69
71
  const sources = await extractAllSources(sourcemapPaths);
70
72
  if (sources.length === 0) {
@@ -1,18 +1,16 @@
1
1
  import fg from 'fast-glob';
2
- import { join } from 'node:path';
3
2
 
4
- async function findSourceMapFiles(directory) {
5
- const distDirectory = join(directory, "dist");
3
+ async function findSourceMapFiles(distDir) {
6
4
  try {
7
5
  const files = await fg("**/*.js.map", {
8
- cwd: distDirectory,
6
+ cwd: distDir,
9
7
  absolute: true,
10
8
  ignore: ["**/node_modules/**"]
11
9
  });
12
10
  return files;
13
11
  } catch (error) {
14
12
  throw new Error(
15
- `Error finding source map files in ${distDirectory}: ${error instanceof Error ? error.message : "Unknown error"}`
13
+ `Error finding source map files in "${distDir}": ${error instanceof Error ? error.message : "Unknown error"}`
16
14
  );
17
15
  }
18
16
  }
package/dist/results.js CHANGED
@@ -3,9 +3,9 @@ import { getPluginJson, hasExternalisedJsxRuntime } from './utils/plugin.js';
3
3
  import { isExternal } from './utils/dependencies.js';
4
4
  import path from 'node:path';
5
5
 
6
- function generateAnalysisResults(matches, pluginRoot, depContext, options = { skipBuildTooling: false, skipDependencies: false }) {
7
- const filtered = filterMatches(matches, options.skipBuildTooling);
8
- const pluginJson = getPluginJson(pluginRoot);
6
+ function generateAnalysisResults(matches, distDir, pluginRoot, depContext, options = { skipBuildTooling: false, skipDependencies: false }) {
7
+ const filtered = filterMatches(matches, options.skipBuildTooling, pluginRoot);
8
+ const pluginJson = getPluginJson(distDir);
9
9
  const filteredWithoutExternals = filtered.filter((match) => shouldIncludeDependencyMatch(match));
10
10
  const sourceMatches = filteredWithoutExternals.filter((m) => m.type === "source");
11
11
  const dependencyMatches = filteredWithoutExternals.filter((m) => m.type === "dependency");
@@ -17,9 +17,9 @@ function generateAnalysisResults(matches, pluginRoot, depContext, options = { sk
17
17
  const pattern = getPattern(m.pattern);
18
18
  return pattern?.impactLevel === "warning";
19
19
  });
20
- const critical = criticalMatches.map((m) => generateResult(m));
21
- const warnings = warningMatches.map((m) => generateResult(m));
22
- const dependencies = buildDependencyIssues(dependencyMatches, depContext);
20
+ const critical = criticalMatches.map((m) => generateResult(m, pluginRoot));
21
+ const warnings = warningMatches.map((m) => generateResult(m, pluginRoot));
22
+ const dependencies = buildDependencyIssues(dependencyMatches, depContext, pluginRoot);
23
23
  const totalIssues = filteredWithoutExternals.length;
24
24
  const affectedDeps = new Set(
25
25
  dependencies.map((m) => m.packageName).filter((name) => name !== void 0)
@@ -55,8 +55,8 @@ function shouldIncludeDependencyMatch(match, _depContext) {
55
55
  }
56
56
  return true;
57
57
  }
58
- function filterMatches(matches, skipBuildTooling) {
59
- const externalisedJsxRuntime = skipBuildTooling ? false : hasExternalisedJsxRuntime();
58
+ function filterMatches(matches, skipBuildTooling, pluginRoot) {
59
+ const externalisedJsxRuntime = skipBuildTooling ? false : hasExternalisedJsxRuntime(pluginRoot);
60
60
  const filtered = matches.filter((match) => {
61
61
  if (match.type === "source" && (match.confidence === "none" || match.confidence === "unknown")) {
62
62
  return false;
@@ -77,7 +77,7 @@ function filterMatches(matches, skipBuildTooling) {
77
77
  });
78
78
  return filtered;
79
79
  }
80
- function generateResult(match) {
80
+ function generateResult(match, pluginRoot) {
81
81
  const pattern = getPattern(match.pattern);
82
82
  if (!pattern) {
83
83
  throw new Error(`Pattern not found: ${match.pattern}`);
@@ -89,7 +89,7 @@ function generateResult(match) {
89
89
  impactLevel: pattern.impactLevel,
90
90
  location: {
91
91
  type: match.type,
92
- file: path.join(process.cwd(), cleanFilePath),
92
+ file: path.join(pluginRoot, cleanFilePath),
93
93
  line: match.sourceLine,
94
94
  column: match.sourceColumn
95
95
  },
@@ -107,7 +107,7 @@ function generateResult(match) {
107
107
  }
108
108
  return result;
109
109
  }
110
- function buildDependencyIssues(dependencyMatches, depContext) {
110
+ function buildDependencyIssues(dependencyMatches, depContext, pluginRoot) {
111
111
  const byPackage = /* @__PURE__ */ new Map();
112
112
  for (const match of dependencyMatches) {
113
113
  if (match.type === "dependency" && match.packageName) {
@@ -124,7 +124,7 @@ function buildDependencyIssues(dependencyMatches, depContext) {
124
124
  packageName,
125
125
  version: version || "unknown",
126
126
  rootDependency: rootDep,
127
- issues: matches.map((m) => generateResult(m))
127
+ issues: matches.map((m) => generateResult(m, pluginRoot))
128
128
  });
129
129
  }
130
130
  return issues;
@@ -3,15 +3,9 @@ import fs from 'node:fs';
3
3
  import { parseFile } from '../parser.js';
4
4
  import { walk } from './ast.js';
5
5
 
6
- let cachedPluginJson = null;
7
- function getPluginJson(dir) {
8
- if (cachedPluginJson) {
9
- return cachedPluginJson;
10
- }
11
- const srcPath = dir ? path.join(dir, "dist") : path.join(process.cwd(), "dist");
12
- const pluginJsonPath = path.join(srcPath, "plugin.json");
13
- cachedPluginJson = readJsonFile(pluginJsonPath);
14
- return cachedPluginJson;
6
+ function getPluginJson(distDir) {
7
+ const pluginJsonPath = path.join(distDir, "plugin.json");
8
+ return readJsonFile(pluginJsonPath);
15
9
  }
16
10
  function isFile(path2) {
17
11
  try {
@@ -34,13 +28,13 @@ function readJsonFile(filename) {
34
28
  throw new Error(`Failed to load json file ${filename}: ${error instanceof Error ? error.message : String(error)}`);
35
29
  }
36
30
  }
37
- function hasExternalisedJsxRuntime() {
31
+ function hasExternalisedJsxRuntime(pluginRoot) {
38
32
  const webpackConfigPathsToCheck = ["webpack.config.ts", ".config/webpack/webpack.config.ts"];
39
33
  const bundlerExternalPathsToCheck = [".config/bundler/externals.ts"];
40
34
  let found = false;
41
35
  for (const webpackConfigPath of webpackConfigPathsToCheck) {
42
- if (isFile(path.join(process.cwd(), webpackConfigPath))) {
43
- const webpackConfig = fs.readFileSync(path.join(process.cwd(), webpackConfigPath)).toString();
36
+ if (isFile(path.join(pluginRoot, webpackConfigPath))) {
37
+ const webpackConfig = fs.readFileSync(path.join(pluginRoot, webpackConfigPath)).toString();
44
38
  const webpackConfigAst = parseFile(webpackConfig, webpackConfigPath);
45
39
  walk(webpackConfigAst, (node) => {
46
40
  if (node.type === "Property" && node.key.type === "Identifier" && node.key.name === "externals" && node.value.type === "ArrayExpression") {
@@ -54,8 +48,8 @@ function hasExternalisedJsxRuntime() {
54
48
  }
55
49
  }
56
50
  for (const bundlerExternalPath of bundlerExternalPathsToCheck) {
57
- if (isFile(path.join(process.cwd(), bundlerExternalPath))) {
58
- const bundlerExternals = fs.readFileSync(path.join(process.cwd(), bundlerExternalPath)).toString();
51
+ if (isFile(path.join(pluginRoot, bundlerExternalPath))) {
52
+ const bundlerExternals = fs.readFileSync(path.join(pluginRoot, bundlerExternalPath)).toString();
59
53
  const bundlerExternalsAst = parseFile(bundlerExternals, bundlerExternalPath);
60
54
  walk(bundlerExternalsAst, (node) => {
61
55
  if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.id.name === "externals" && node.init?.type === "ArrayExpression") {
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.6.3",
4
+ "version": "0.6.4",
5
5
  "repository": {
6
6
  "directory": "packages/react-detect",
7
7
  "url": "https://github.com/grafana/plugin-tools"
@@ -41,5 +41,5 @@
41
41
  "engines": {
42
42
  "node": ">=20"
43
43
  },
44
- "gitHead": "2a6a8313b67f6180c17140703832fca3a328d8c7"
44
+ "gitHead": "8aacee07bed65b29ca15c30285870965112c8228"
45
45
  }
package/src/bin/run.ts CHANGED
@@ -6,7 +6,7 @@ import { detect19 } from '../commands/detect19.js';
6
6
  const args = process.argv.slice(2);
7
7
  const argv = minimist(args, {
8
8
  boolean: ['json', 'skipBuildTooling', 'skipDependencies', 'noErrorExitCode'],
9
- string: ['pluginRoot'],
9
+ string: ['pluginRoot', 'distDir'],
10
10
  default: {
11
11
  json: false,
12
12
  skipBuildTooling: false,
@@ -1,4 +1,5 @@
1
1
  import minimist from 'minimist';
2
+ import { join } from 'node:path';
2
3
  import { findSourceMapFiles } from '../file-scanner.js';
3
4
  import { generateAnalysisResults } from '../results.js';
4
5
  import { DependencyContext } from '../utils/dependencies.js';
@@ -13,11 +14,12 @@ import { output } from '../utils/output.js';
13
14
  export async function detect19(argv: minimist.ParsedArgs) {
14
15
  try {
15
16
  const pluginRoot = argv.pluginRoot || process.cwd();
17
+ const distDir = argv.distDir || join(pluginRoot, 'dist');
16
18
  const skipDependencies = argv.skipDependencies || false;
17
19
  const skipBuildTooling = argv.skipBuildTooling || false;
18
20
  const jsonOutput = argv.json || false;
19
21
 
20
- const allMatches = await getAllMatches(pluginRoot);
22
+ const allMatches = await getAllMatches(distDir);
21
23
 
22
24
  // Conditionally load dependencies
23
25
  let depContext: DependencyContext | null = null;
@@ -52,7 +54,7 @@ export async function detect19(argv: minimist.ParsedArgs) {
52
54
  return match;
53
55
  });
54
56
 
55
- const results = generateAnalysisResults(matchesWithRootDependency, pluginRoot, depContext, {
57
+ const results = generateAnalysisResults(matchesWithRootDependency, distDir, pluginRoot, depContext, {
56
58
  skipBuildTooling,
57
59
  skipDependencies,
58
60
  });
@@ -77,11 +79,11 @@ export async function detect19(argv: minimist.ParsedArgs) {
77
79
  }
78
80
  }
79
81
 
80
- async function getAllMatches(pluginRoot: string) {
81
- const sourcemapPaths = await findSourceMapFiles(pluginRoot);
82
+ async function getAllMatches(distDir: string) {
83
+ const sourcemapPaths = await findSourceMapFiles(distDir);
82
84
 
83
85
  if (sourcemapPaths.length === 0) {
84
- throw new Error('No source map files found in dist directory. Make sure to build your plugin first.');
86
+ throw new Error(`No source map files found in "${distDir}". Make sure to build your plugin first.`);
85
87
  }
86
88
 
87
89
  const sources = await extractAllSources(sourcemapPaths);
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { findSourceMapFiles } from './file-scanner.js';
3
+ import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ describe('findSourceMapFiles', () => {
8
+ it('searches the given directory directly without appending a dist subdirectory', async () => {
9
+ const dir = mkdtempSync(join(tmpdir(), 'react-detect-test-'));
10
+ writeFileSync(join(dir, 'module.js.map'), '{}');
11
+
12
+ const files = await findSourceMapFiles(dir);
13
+
14
+ expect(files).toHaveLength(1);
15
+ expect(files[0]).toContain('module.js.map');
16
+ });
17
+
18
+ it('finds source map files recursively within the given directory', async () => {
19
+ const dir = mkdtempSync(join(tmpdir(), 'react-detect-test-'));
20
+ mkdirSync(join(dir, 'nested'));
21
+ writeFileSync(join(dir, 'a.js.map'), '{}');
22
+ writeFileSync(join(dir, 'nested', 'b.js.map'), '{}');
23
+
24
+ const files = await findSourceMapFiles(dir);
25
+
26
+ expect(files).toHaveLength(2);
27
+ });
28
+
29
+ it('returns empty array when no source map files exist', async () => {
30
+ const dir = mkdtempSync(join(tmpdir(), 'react-detect-test-'));
31
+
32
+ const files = await findSourceMapFiles(dir);
33
+
34
+ expect(files).toHaveLength(0);
35
+ });
36
+ });
@@ -1,19 +1,16 @@
1
1
  import fg from 'fast-glob';
2
- import { join } from 'node:path';
3
-
4
- export async function findSourceMapFiles(directory: string): Promise<string[]> {
5
- const distDirectory = join(directory, 'dist');
6
2
 
3
+ export async function findSourceMapFiles(distDir: string): Promise<string[]> {
7
4
  try {
8
5
  const files = await fg('**/*.js.map', {
9
- cwd: distDirectory,
6
+ cwd: distDir,
10
7
  absolute: true,
11
8
  ignore: ['**/node_modules/**'],
12
9
  });
13
10
  return files;
14
11
  } catch (error) {
15
12
  throw new Error(
16
- `Error finding source map files in ${distDirectory}: ${error instanceof Error ? error.message : 'Unknown error'}`
13
+ `Error finding source map files in "${distDir}": ${error instanceof Error ? error.message : 'Unknown error'}`
17
14
  );
18
15
  }
19
16
  }
@@ -36,6 +36,7 @@ describe('generateAnalysisResults', () => {
36
36
  bundledFilePath: `node_modules/${packageName}/index.js`,
37
37
  });
38
38
 
39
+ const distDir = process.cwd();
39
40
  const pluginRoot = process.cwd();
40
41
  const depContext = new DependencyContext();
41
42
  const options: AnalysisOptions = {
@@ -64,7 +65,7 @@ describe('generateAnalysisResults', () => {
64
65
  bundledFilePath: 'src/components/MyComponent.tsx',
65
66
  };
66
67
  const matches: AnalyzedMatch[] = [sourceMatch, createDependencyMatch('react', 'react')];
67
- const results = generateAnalysisResults(matches, pluginRoot, depContext, options);
68
+ const results = generateAnalysisResults(matches, distDir, pluginRoot, depContext, options);
68
69
 
69
70
  expect(results.summary.sourceIssuesCount).toBe(1);
70
71
  expect(results.summary.dependencyIssuesCount).toBe(0);
@@ -75,7 +76,7 @@ describe('generateAnalysisResults', () => {
75
76
  createDependencyMatch('lodash', 'lodash'), // lodash is externalized by Grafana
76
77
  createDependencyMatch('axios', 'axios'),
77
78
  ];
78
- const results = generateAnalysisResults(matches, pluginRoot, depContext, options);
79
+ const results = generateAnalysisResults(matches, distDir, pluginRoot, depContext, options);
79
80
 
80
81
  expect(results.issues.dependencies).toHaveLength(1);
81
82
  expect(results.issues.dependencies[0].packageName).toBe('axios');
@@ -87,7 +88,7 @@ describe('generateAnalysisResults', () => {
87
88
  createDependencyMatch('@grafana/ui', '@grafana/ui'),
88
89
  createDependencyMatch('@custom/package', '@custom/package'),
89
90
  ];
90
- const results = generateAnalysisResults(matches, pluginRoot, depContext, options);
91
+ const results = generateAnalysisResults(matches, distDir, pluginRoot, depContext, options);
91
92
 
92
93
  expect(results.issues.dependencies).toHaveLength(1);
93
94
  expect(results.issues.dependencies[0].packageName).toBe('@custom/package');
@@ -98,7 +99,7 @@ describe('generateAnalysisResults', () => {
98
99
  createDependencyMatch('@grafana/data/utils', '@grafana/data'),
99
100
  createDependencyMatch('@custom/package/utils', '@custom/package'),
100
101
  ];
101
- const results = generateAnalysisResults(matches, pluginRoot, depContext, options);
102
+ const results = generateAnalysisResults(matches, distDir, pluginRoot, depContext, options);
102
103
 
103
104
  expect(results.issues.dependencies).toHaveLength(1);
104
105
  expect(results.issues.dependencies[0].packageName).toBe('@custom/package/utils');
@@ -111,7 +112,7 @@ describe('generateAnalysisResults', () => {
111
112
  createDependencyMatch('scheduler', 'react'),
112
113
  createDependencyMatch('debug', 'axios'), // Non-externalized root dependency
113
114
  ];
114
- const results = generateAnalysisResults(matches, pluginRoot, depContext, options);
115
+ const results = generateAnalysisResults(matches, distDir, pluginRoot, depContext, options);
115
116
 
116
117
  expect(results.issues.dependencies).toHaveLength(1);
117
118
  expect(results.issues.dependencies[0].packageName).toBe('debug');
@@ -125,7 +126,7 @@ describe('generateAnalysisResults', () => {
125
126
  createDependencyMatch('@grafana/data', '@grafana/data'),
126
127
  createDependencyMatch('axios', 'axios'),
127
128
  ];
128
- const results = generateAnalysisResults(matches, pluginRoot, depContext, options);
129
+ const results = generateAnalysisResults(matches, distDir, pluginRoot, depContext, options);
129
130
  // Filter to only critical dep issues
130
131
  const reportedDeps = results.issues.critical.filter((i) => i.location.type === 'dependency');
131
132
  expect(reportedDeps).toHaveLength(1);
package/src/results.ts CHANGED
@@ -12,12 +12,13 @@ export interface AnalysisOptions {
12
12
 
13
13
  export function generateAnalysisResults(
14
14
  matches: AnalyzedMatch[],
15
+ distDir: string,
15
16
  pluginRoot: string,
16
17
  depContext: DependencyContext | null,
17
18
  options: AnalysisOptions = { skipBuildTooling: false, skipDependencies: false }
18
19
  ): PluginAnalysisResults {
19
- const filtered = filterMatches(matches, options.skipBuildTooling);
20
- const pluginJson = getPluginJson(pluginRoot);
20
+ const filtered = filterMatches(matches, options.skipBuildTooling, pluginRoot);
21
+ const pluginJson = getPluginJson(distDir);
21
22
 
22
23
  // Filter out externalized dependencies
23
24
  const filteredWithoutExternals = filtered.filter((match) => shouldIncludeDependencyMatch(match, depContext));
@@ -35,9 +36,9 @@ export function generateAnalysisResults(
35
36
  return pattern?.impactLevel === 'warning';
36
37
  });
37
38
 
38
- const critical = criticalMatches.map((m) => generateResult(m));
39
- const warnings = warningMatches.map((m) => generateResult(m));
40
- const dependencies = buildDependencyIssues(dependencyMatches, depContext);
39
+ const critical = criticalMatches.map((m) => generateResult(m, pluginRoot));
40
+ const warnings = warningMatches.map((m) => generateResult(m, pluginRoot));
41
+ const dependencies = buildDependencyIssues(dependencyMatches, depContext, pluginRoot);
41
42
 
42
43
  const totalIssues = filteredWithoutExternals.length;
43
44
  const affectedDeps = new Set(
@@ -78,9 +79,9 @@ function shouldIncludeDependencyMatch(match: AnalyzedMatch, _depContext: Depende
78
79
  return true;
79
80
  }
80
81
 
81
- function filterMatches(matches: AnalyzedMatch[], skipBuildTooling: boolean): AnalyzedMatch[] {
82
+ function filterMatches(matches: AnalyzedMatch[], skipBuildTooling: boolean, pluginRoot: string): AnalyzedMatch[] {
82
83
  // Only check webpack config if NOT skipping build tooling
83
- const externalisedJsxRuntime = skipBuildTooling ? false : hasExternalisedJsxRuntime();
84
+ const externalisedJsxRuntime = skipBuildTooling ? false : hasExternalisedJsxRuntime(pluginRoot);
84
85
  const filtered = matches.filter((match) => {
85
86
  // TODO: add mode for strict / loose filtering
86
87
  if (match.type === 'source' && (match.confidence === 'none' || match.confidence === 'unknown')) {
@@ -120,7 +121,7 @@ function filterMatches(matches: AnalyzedMatch[], skipBuildTooling: boolean): Ana
120
121
  return filtered;
121
122
  }
122
123
 
123
- function generateResult(match: AnalyzedMatch): AnalysisResult {
124
+ function generateResult(match: AnalyzedMatch, pluginRoot: string): AnalysisResult {
124
125
  const pattern = getPattern(match.pattern);
125
126
 
126
127
  if (!pattern) {
@@ -135,7 +136,7 @@ function generateResult(match: AnalyzedMatch): AnalysisResult {
135
136
  impactLevel: pattern.impactLevel,
136
137
  location: {
137
138
  type: match.type,
138
- file: path.join(process.cwd(), cleanFilePath),
139
+ file: path.join(pluginRoot, cleanFilePath),
139
140
  line: match.sourceLine,
140
141
  column: match.sourceColumn,
141
142
  },
@@ -162,7 +163,8 @@ function generateResult(match: AnalyzedMatch): AnalysisResult {
162
163
  */
163
164
  function buildDependencyIssues(
164
165
  dependencyMatches: AnalyzedMatch[],
165
- depContext: DependencyContext | null
166
+ depContext: DependencyContext | null,
167
+ pluginRoot: string
166
168
  ): DependencyIssue[] {
167
169
  // Group by package
168
170
  const byPackage = new Map<string, AnalyzedMatch[]>();
@@ -185,7 +187,7 @@ function buildDependencyIssues(
185
187
  packageName,
186
188
  version: version || 'unknown',
187
189
  rootDependency: rootDep,
188
- issues: matches.map((m) => generateResult(m)),
190
+ issues: matches.map((m) => generateResult(m, pluginRoot)),
189
191
  });
190
192
  }
191
193
 
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { getPluginJson, hasExternalisedJsxRuntime } from './plugin.js';
6
+
7
+ describe('getPluginJson', () => {
8
+ it('reads plugin.json from the given distDir directly without appending /dist', () => {
9
+ const distDir = mkdtempSync(join(tmpdir(), 'react-detect-plugin-test-'));
10
+ const pluginJson = { id: 'my-plugin', name: 'My Plugin', type: 'app', info: { version: '2.0.0' } };
11
+ writeFileSync(join(distDir, 'plugin.json'), JSON.stringify(pluginJson));
12
+
13
+ const result = getPluginJson(distDir);
14
+
15
+ expect(result?.id).toBe('my-plugin');
16
+ expect(result?.info.version).toBe('2.0.0');
17
+ });
18
+
19
+ it('returns correct data for each distDir when called with different directories', () => {
20
+ const distDir1 = mkdtempSync(join(tmpdir(), 'react-detect-plugin-test-'));
21
+ writeFileSync(
22
+ join(distDir1, 'plugin.json'),
23
+ JSON.stringify({ id: 'plugin-1', name: 'P1', type: 'app', info: { version: '1.0.0' } })
24
+ );
25
+ const distDir2 = mkdtempSync(join(tmpdir(), 'react-detect-plugin-test-'));
26
+ writeFileSync(
27
+ join(distDir2, 'plugin.json'),
28
+ JSON.stringify({ id: 'plugin-2', name: 'P2', type: 'panel', info: { version: '2.0.0' } })
29
+ );
30
+
31
+ const result1 = getPluginJson(distDir1);
32
+ const result2 = getPluginJson(distDir2);
33
+
34
+ expect(result1?.id).toBe('plugin-1');
35
+ expect(result2?.id).toBe('plugin-2');
36
+ });
37
+
38
+ it('throws when plugin.json does not exist in the given distDir', () => {
39
+ const distDir = mkdtempSync(join(tmpdir(), 'react-detect-plugin-test-'));
40
+
41
+ expect(() => getPluginJson(distDir)).toThrow('plugin.json');
42
+ });
43
+ });
44
+
45
+ describe('hasExternalisedJsxRuntime', () => {
46
+ it('detects react/jsx-runtime in webpack externals using the given pluginRoot', () => {
47
+ const pluginRoot = mkdtempSync(join(tmpdir(), 'react-detect-plugin-test-'));
48
+ writeFileSync(join(pluginRoot, 'webpack.config.ts'), `module.exports = { externals: ['react/jsx-runtime'] };`);
49
+
50
+ expect(hasExternalisedJsxRuntime(pluginRoot)).toBe(true);
51
+ });
52
+
53
+ it('returns false when the given pluginRoot has no matching webpack config', () => {
54
+ const pluginRoot = mkdtempSync(join(tmpdir(), 'react-detect-plugin-test-'));
55
+
56
+ expect(hasExternalisedJsxRuntime(pluginRoot)).toBe(false);
57
+ });
58
+
59
+ it('detects react/jsx-runtime in bundler externals using the given pluginRoot', () => {
60
+ const pluginRoot = mkdtempSync(join(tmpdir(), 'react-detect-plugin-test-'));
61
+ mkdirSync(join(pluginRoot, '.config', 'bundler'), { recursive: true });
62
+ writeFileSync(join(pluginRoot, '.config', 'bundler', 'externals.ts'), `const externals = ['react/jsx-runtime'];`);
63
+
64
+ expect(hasExternalisedJsxRuntime(pluginRoot)).toBe(true);
65
+ });
66
+ });
@@ -3,26 +3,9 @@ import fs from 'node:fs';
3
3
  import { parseFile } from '../parser.js';
4
4
  import { walk } from './ast.js';
5
5
 
6
- interface PluginJson {
7
- id: string;
8
- type: string;
9
- info: {
10
- version: string;
11
- };
12
- name: string;
13
- }
14
-
15
- let cachedPluginJson: PluginJson | null = null;
16
-
17
- export function getPluginJson(dir?: string) {
18
- if (cachedPluginJson) {
19
- return cachedPluginJson;
20
- }
21
-
22
- const srcPath = dir ? path.join(dir, 'dist') : path.join(process.cwd(), 'dist');
23
- const pluginJsonPath = path.join(srcPath, 'plugin.json');
24
- cachedPluginJson = readJsonFile(pluginJsonPath);
25
- return cachedPluginJson;
6
+ export function getPluginJson(distDir: string) {
7
+ const pluginJsonPath = path.join(distDir, 'plugin.json');
8
+ return readJsonFile(pluginJsonPath);
26
9
  }
27
10
 
28
11
  function isFile(path: string) {
@@ -49,13 +32,13 @@ export function readJsonFile(filename: string) {
49
32
  }
50
33
  }
51
34
 
52
- export function hasExternalisedJsxRuntime(): boolean {
35
+ export function hasExternalisedJsxRuntime(pluginRoot: string): boolean {
53
36
  const webpackConfigPathsToCheck = ['webpack.config.ts', '.config/webpack/webpack.config.ts'];
54
37
  const bundlerExternalPathsToCheck = ['.config/bundler/externals.ts'];
55
38
  let found = false;
56
39
  for (const webpackConfigPath of webpackConfigPathsToCheck) {
57
- if (isFile(path.join(process.cwd(), webpackConfigPath))) {
58
- const webpackConfig = fs.readFileSync(path.join(process.cwd(), webpackConfigPath)).toString();
40
+ if (isFile(path.join(pluginRoot, webpackConfigPath))) {
41
+ const webpackConfig = fs.readFileSync(path.join(pluginRoot, webpackConfigPath)).toString();
59
42
  const webpackConfigAst = parseFile(webpackConfig, webpackConfigPath);
60
43
 
61
44
  walk(webpackConfigAst, (node) => {
@@ -81,8 +64,8 @@ export function hasExternalisedJsxRuntime(): boolean {
81
64
  }
82
65
  }
83
66
  for (const bundlerExternalPath of bundlerExternalPathsToCheck) {
84
- if (isFile(path.join(process.cwd(), bundlerExternalPath))) {
85
- const bundlerExternals = fs.readFileSync(path.join(process.cwd(), bundlerExternalPath)).toString();
67
+ if (isFile(path.join(pluginRoot, bundlerExternalPath))) {
68
+ const bundlerExternals = fs.readFileSync(path.join(pluginRoot, bundlerExternalPath)).toString();
86
69
  const bundlerExternalsAst = parseFile(bundlerExternals, bundlerExternalPath);
87
70
 
88
71
  walk(bundlerExternalsAst, (node) => {