From 9c35aef60f2f5ea230d4385a8bf754fe1ddb54cd Mon Sep 17 00:00:00 2001 From: bymle Date: Sun, 7 Jun 2026 13:25:34 +0800 Subject: [PATCH] fix(project-detect): match packageKeys on boundaries, not substrings (#2181) Framework detection matched a dependency against a framework's packageKeys with unbounded substring containment (dep.includes(key)), so any dependency whose name merely contained a key was misclassified: `preact` and even `reactive` were both detected as `react`. Match only when the dependency equals the key, or the key is a prefix immediately followed by a delimiter (/ . _ -). This still matches every real case (react-dom, @remix-run/node, spring-boot-starter, org.springframework.boot, github.com/labstack/echo/v4, phoenix_live_view) while excluding preact/reactive (and incidentally nextra). Adds regression tests. Co-authored-by: bymle <229636660+bymle@users.noreply.github.com> --- scripts/lib/project-detect.js | 15 ++++++++++++++- tests/lib/project-detect.test.js | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/scripts/lib/project-detect.js b/scripts/lib/project-detect.js index 7c7d605b..9f1566ad 100644 --- a/scripts/lib/project-detect.js +++ b/scripts/lib/project-detect.js @@ -390,7 +390,20 @@ function detectProjectType(projectDir) { depList = elixirDeps; break; } - hasDep = rule.packageKeys.some(key => depList.some(dep => dep.toLowerCase().includes(key.toLowerCase()))); + // Boundary-aware match: a dependency matches a packageKey only when it + // equals the key, or the key is a prefix immediately followed by a + // delimiter (/ . _ -). Plain substring matching wrongly classified + // `preact` / `reactive` as `react`. This still matches the real cases: + // react-dom, @remix-run/node, spring-boot-starter, org.springframework.boot, + // github.com/labstack/echo/v4, phoenix_live_view. + hasDep = rule.packageKeys.some(key => { + const k = key.toLowerCase(); + return depList.some(dep => { + const d = dep.toLowerCase(); + if (!d.startsWith(k)) return false; + return d.length === k.length || /[/._-]/.test(d[k.length]); + }); + }); } if (hasMarker || hasDep) { diff --git a/tests/lib/project-detect.test.js b/tests/lib/project-detect.test.js index 94830af9..15382b24 100644 --- a/tests/lib/project-detect.test.js +++ b/tests/lib/project-detect.test.js @@ -207,6 +207,39 @@ function runTests() { } })) passed++; else failed++; + if (test('does not misclassify preact as react (substring guard)', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'package.json', '{"dependencies":{"preact":"10.0.0"}}'); + const result = detectProjectType(dir); + assert.ok(!result.frameworks.includes('react'), `preact wrongly detected as react: ${JSON.stringify(result.frameworks)}`); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('does not misclassify reactive as react (substring guard)', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'package.json', '{"dependencies":{"reactive":"1.0.0"}}'); + const result = detectProjectType(dir); + assert.ok(!result.frameworks.includes('react'), `reactive wrongly detected as react: ${JSON.stringify(result.frameworks)}`); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('still detects react from react-dom alone (prefix-delimiter match preserved)', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'package.json', '{"dependencies":{"react-dom":"18.0.0"}}'); + const result = detectProjectType(dir); + assert.ok(result.frameworks.includes('react'), `react-dom should still map to react: ${JSON.stringify(result.frameworks)}`); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + if (test('detects angular from angular.json', () => { const dir = createTempDir(); try {