@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 +12 -0
- package/dist/bin/run.js +1 -1
- package/dist/commands/detect19.js +7 -5
- package/dist/file-scanner.js +3 -5
- package/dist/results.js +12 -12
- package/dist/utils/plugin.js +8 -14
- package/package.json +2 -2
- package/src/bin/run.ts +1 -1
- package/src/commands/detect19.ts +7 -5
- package/src/file-scanner.test.ts +36 -0
- package/src/file-scanner.ts +3 -6
- package/src/results.test.ts +7 -6
- package/src/results.ts +13 -11
- package/src/utils/plugin.test.ts +66 -0
- package/src/utils/plugin.ts +8 -25
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(
|
|
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(
|
|
65
|
-
const sourcemapPaths = await findSourceMapFiles(
|
|
66
|
+
async function getAllMatches(distDir) {
|
|
67
|
+
const sourcemapPaths = await findSourceMapFiles(distDir);
|
|
66
68
|
if (sourcemapPaths.length === 0) {
|
|
67
|
-
throw new Error(
|
|
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) {
|
package/dist/file-scanner.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import fg from 'fast-glob';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
|
|
4
|
-
async function findSourceMapFiles(
|
|
5
|
-
const distDirectory = join(directory, "dist");
|
|
3
|
+
async function findSourceMapFiles(distDir) {
|
|
6
4
|
try {
|
|
7
5
|
const files = await fg("**/*.js.map", {
|
|
8
|
-
cwd:
|
|
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 ${
|
|
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(
|
|
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(
|
|
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;
|
package/dist/utils/plugin.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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(
|
|
43
|
-
const webpackConfig = fs.readFileSync(path.join(
|
|
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(
|
|
58
|
-
const bundlerExternals = fs.readFileSync(path.join(
|
|
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.
|
|
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": "
|
|
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,
|
package/src/commands/detect19.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
81
|
-
const sourcemapPaths = await findSourceMapFiles(
|
|
82
|
+
async function getAllMatches(distDir: string) {
|
|
83
|
+
const sourcemapPaths = await findSourceMapFiles(distDir);
|
|
82
84
|
|
|
83
85
|
if (sourcemapPaths.length === 0) {
|
|
84
|
-
throw new Error(
|
|
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
|
+
});
|
package/src/file-scanner.ts
CHANGED
|
@@ -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:
|
|
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 ${
|
|
13
|
+
`Error finding source map files in "${distDir}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
17
14
|
);
|
|
18
15
|
}
|
|
19
16
|
}
|
package/src/results.test.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
+
});
|
package/src/utils/plugin.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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(
|
|
58
|
-
const webpackConfig = fs.readFileSync(path.join(
|
|
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(
|
|
85
|
-
const bundlerExternals = fs.readFileSync(path.join(
|
|
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) => {
|