diff --git a/scripts/lib/project-detect.js b/scripts/lib/project-detect.js index 98308fa0a..2aac4afbc 100644 --- a/scripts/lib/project-detect.js +++ b/scripts/lib/project-detect.js @@ -9,6 +9,7 @@ const fs = require('fs'); const path = require('path'); +const yaml = require('js-yaml'); /** * Language detection rules. @@ -84,6 +85,11 @@ const LANGUAGE_RULES = [ type: 'php', markers: ['composer.json', 'composer.lock'], extensions: ['.php'] + }, + { + type: 'dart', + markers: ['pubspec.yaml', 'pubspec.lock'], + extensions: ['.dart'] } ]; @@ -129,7 +135,10 @@ const FRAMEWORK_RULES = [ { framework: 'symfony', language: 'php', markers: ['symfony.lock'], packageKeys: ['symfony/framework-bundle'] }, // Elixir frameworks - { framework: 'phoenix', language: 'elixir', markers: [], packageKeys: ['phoenix'] } + { framework: 'phoenix', language: 'elixir', markers: [], packageKeys: ['phoenix'] }, + + // Dart frameworks + { framework: 'flutter', language: 'dart', markers: [], packageKeys: [], customCheck: hasFlutterSdkDependency } ]; /** @@ -330,6 +339,27 @@ function getElixirDeps(projectDir) { } } +/** + * Check whether pubspec.yaml declares the Flutter SDK as a dependency, + * which distinguishes a Flutter app from a plain Dart package. + * @param {string} projectDir - Project root directory + * @returns {boolean} + */ +function hasFlutterSdkDependency(projectDir) { + try { + const pubspecPath = path.join(projectDir, 'pubspec.yaml'); + if (!fs.existsSync(pubspecPath)) return false; + const content = fs.readFileSync(pubspecPath, 'utf8'); + const pubspec = yaml.load(content, { schema: yaml.JSON_SCHEMA }); + // Only a declared Flutter SDK dependency identifies the framework. + // Unrelated configuration, comments, and strings must not count. + return pubspec?.dependencies?.flutter?.sdk === 'flutter' + || pubspec?.dev_dependencies?.flutter?.sdk === 'flutter'; + } catch { + return false; + } +} + /** * Detect project languages and frameworks * @param {string} [projectDir] - Project directory (defaults to cwd) @@ -409,7 +439,9 @@ function detectProjectType(projectDir) { }); } - if (hasMarker || hasDep) { + const hasCustom = typeof rule.customCheck === 'function' ? rule.customCheck(projectDir) : false; + + if (hasMarker || hasDep || hasCustom) { frameworks.push(rule.framework); } } diff --git a/tests/lib/project-detect.test.js b/tests/lib/project-detect.test.js index 30727f360..d76abbf47 100644 --- a/tests/lib/project-detect.test.js +++ b/tests/lib/project-detect.test.js @@ -493,6 +493,87 @@ function runTests() { } })) passed++; else failed++; + // Dart/Flutter detection + console.log('\nDart/Flutter Detection:'); + + if (test('detects dart from pubspec.yaml and .dart files', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', 'name: test_pkg\nversion: 1.0.0\n'); + writeTestFile(dir, 'main.dart', 'void main() {}\n'); + const result = detectProjectType(dir); + assert.ok(result.languages.includes('dart')); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('detects flutter framework when pubspec.yaml depends on the Flutter SDK', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', [ + 'name: halal_cosmetics_app', + 'dependencies:', + ' flutter:', + ' sdk: flutter', + ' cupertino_icons: ^1.0.2' + ].join('\n')); + const result = detectProjectType(dir); + assert.ok(result.languages.includes('dart')); + assert.ok(result.frameworks.includes('flutter'), `flutter not detected: ${JSON.stringify(result.frameworks)}`); + assert.strictEqual(result.primary, 'flutter'); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('does not misclassify a plain Dart package as flutter', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', [ + 'name: plain_dart_pkg', + 'dependencies:', + ' http: ^1.0.0' + ].join('\n')); + const result = detectProjectType(dir); + assert.ok(result.languages.includes('dart')); + assert.ok(!result.frameworks.includes('flutter'), `plain Dart package wrongly detected as flutter: ${JSON.stringify(result.frameworks)}`); + assert.strictEqual(result.primary, 'dart'); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('does not combine unrelated Flutter and SDK configuration', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', 'name: plain_dart\nflutter:\n assets: []\ndependencies:\n other:\n sdk: flutter\n'); + assert.ok(!detectProjectType(dir).frameworks.includes('flutter')); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('detects inline Flutter SDK dependencies', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', 'name: flutter_pkg\ndependencies: {flutter: {sdk: flutter}}\n'); + assert.ok(detectProjectType(dir).frameworks.includes('flutter')); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('malformed pubspec does not claim Flutter detection', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', 'dependencies: [\nflutter:\n sdk: flutter\n'); + assert.ok(!detectProjectType(dir).frameworks.includes('flutter')); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + // Edge cases console.log('\nEdge Cases:');