Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions gitnexus/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion gitnexus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"prepare": "npm run build",
"postinstall": "node scripts/patch-tree-sitter-swift.cjs",
"postinstall": "node scripts/patch-tree-sitter-swift.cjs && node scripts/patch-tree-sitter-dart.cjs",
"prepack": "npm run build && chmod +x dist/cli/index.js"
},
"dependencies": {
Expand Down Expand Up @@ -81,6 +81,7 @@
"uuid": "^13.0.0"
},
"optionalDependencies": {
"tree-sitter-dart": "^1.0.0",
"tree-sitter-kotlin": "^0.3.8",
"tree-sitter-swift": "0.7.1"
},
Expand Down
119 changes: 119 additions & 0 deletions gitnexus/scripts/patch-tree-sitter-dart.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
/**
* WORKAROUND: tree-sitter-dart@1.0.0 NAN → NAPI binding conversion
*
* Background:
* tree-sitter-dart@1.0.0 uses NAN (Native Abstractions for Node.js) for its
* native binding. NAN modules fail with "Module did not self-register" when
* loaded in Node.js worker_threads alongside NAPI-based tree-sitter modules.
* All other tree-sitter grammars in this project use NAPI, which is thread-safe.
*
* How this workaround works:
* 1. Replaces the NAN-based binding.cc with a NAPI-based one
* 2. Updates binding.gyp to use node-addon-api instead of nan
* 3. Installs node-addon-api as a local dependency
* 4. Rebuilds the native binding
*
* TODO: Remove this script when tree-sitter-dart publishes a version with NAPI
* bindings (or when an alternative package is available).
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const dartDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-dart');
const bindingCcPath = path.join(dartDir, 'bindings', 'node', 'binding.cc');
const bindingGypPath = path.join(dartDir, 'binding.gyp');

const NAPI_BINDING_CC = `#include <napi.h>

typedef struct TSLanguage TSLanguage;

extern "C" TSLanguage *tree_sitter_dart();

// "tree-sitter", "language" hashed with BLAKE2
const napi_type_tag LANGUAGE_TYPE_TAG = {
0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
};

Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports["name"] = Napi::String::New(env, "dart");
auto language = Napi::External<TSLanguage>::New(env, tree_sitter_dart());
language.TypeTag(&LANGUAGE_TYPE_TAG);
exports["language"] = language;
return exports;
}

NODE_API_MODULE(tree_sitter_dart_binding, Init)
`;

const NAPI_BINDING_GYP = `{
"targets": [
{
"target_name": "tree_sitter_dart_binding",
"dependencies": [
"<!(node -p \\"require('node-addon-api').targets\\"):node_addon_api_except"
],
"include_dirs": [
"src"
],
"sources": [
"bindings/node/binding.cc",
"src/parser.c",
"src/scanner.c"
],
"cflags_c": [
"-std=c99"
]
}
]
}
`;

try {
if (!fs.existsSync(bindingCcPath)) {
process.exit(0);
}

const currentBinding = fs.readFileSync(bindingCcPath, 'utf8');
let needsRebuild = false;

// Check if still using NAN
if (currentBinding.includes('nan.h')) {
console.log('[tree-sitter-dart] Converting NAN binding to NAPI for worker_threads compatibility...');
fs.writeFileSync(bindingCcPath, NAPI_BINDING_CC);
fs.writeFileSync(bindingGypPath, NAPI_BINDING_GYP);
needsRebuild = true;

// Install node-addon-api if not present
const napiDir = path.join(dartDir, 'node_modules', 'node-addon-api');
if (!fs.existsSync(napiDir)) {
console.log('[tree-sitter-dart] Installing node-addon-api...');
execSync('npm install node-addon-api', {
cwd: dartDir,
stdio: 'pipe',
timeout: 60000,
});
}
}

// Check if native binding exists
const bindingNode = path.join(dartDir, 'build', 'Release', 'tree_sitter_dart_binding.node');
if (!fs.existsSync(bindingNode)) {
needsRebuild = true;
}

if (needsRebuild) {
console.log('[tree-sitter-dart] Rebuilding native binding with NAPI...');
execSync('npx node-gyp rebuild', {
cwd: dartDir,
stdio: 'pipe',
timeout: 120000,
});
console.log('[tree-sitter-dart] NAPI binding built successfully');
}
} catch (err) {
console.warn('[tree-sitter-dart] Could not build NAPI binding:', err.message);
console.warn('[tree-sitter-dart] Dart support will be unavailable in worker threads.');
console.warn('[tree-sitter-dart] You may need to manually run: cd node_modules/tree-sitter-dart && npx node-gyp rebuild');
}
1 change: 1 addition & 0 deletions gitnexus/src/config/supported-languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ export enum SupportedLanguages {
PHP = 'php',
Kotlin = 'kotlin',
Swift = 'swift',
Dart = 'dart',
}
17 changes: 17 additions & 0 deletions gitnexus/src/core/ingestion/entry-point-scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,21 @@ export const ENTRY_POINT_PATTERNS = {
/^perform$/, // Background jobs (Sidekiq, ActiveJob)
/^execute$/, // Command pattern
],

// Dart / Flutter
[SupportedLanguages.Dart]: [
/^main$/, // THE entry point
/^build$/, // Widget.build() method
/^initState$/, // StatefulWidget lifecycle
/^dispose$/, // Cleanup lifecycle
/^createState$/, // StatefulWidget.createState()
/Widget$/, // Custom widget classes
/Page$/, // Page classes
/Screen$/, // Screen classes
/Controller$/, // Controllers
/Notifier$/, // Riverpod notifiers
/Provider$/, // Provider pattern
],
} satisfies Record<SupportedLanguages, RegExp[]>;

/** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */
Expand Down Expand Up @@ -378,6 +393,8 @@ export function isTestFile(filePath: string): boolean {
p.includes('.integrationtests/') ||
p.includes('.unittests/') ||
p.includes('/testproject/') ||
// Dart/Flutter test patterns
p.endsWith('_test.dart') ||
// PHP/Laravel test patterns
p.endsWith('test.php') ||
p.endsWith('spec.php') ||
Expand Down
2 changes: 2 additions & 0 deletions gitnexus/src/core/ingestion/export-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,5 @@ export const swiftExportChecker: ExportChecker = (node, _name) => {
/** Ruby: all top-level definitions are public (no export syntax). */
export const rubyExportChecker: ExportChecker = (_node, _name) => true;

/** Dart: names starting with _ are library-private, everything else is public. */
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');
40 changes: 40 additions & 0 deletions gitnexus/src/core/ingestion/framework-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,38 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
return { framework: 'ios-router', entryPointMultiplier: 2.0, reason: 'ios-router' };
}

// ========== DART / FLUTTER ==========

// Flutter main entry point
if (p.endsWith('/main.dart') || p.endsWith('/app.dart')) {
return { framework: 'flutter', entryPointMultiplier: 3.0, reason: 'flutter-main' };
}

// Flutter pages/screens (high confidence entry points)
if ((p.includes('/pages/') || p.includes('/screens/') || p.includes('/views/')) && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-page' };
}

// Flutter presentation layer (controllers, providers, notifiers)
if (p.includes('/presentation/') && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.0, reason: 'flutter-presentation' };
}

// Flutter routes
if (p.includes('/routes/') && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-routes' };
}

// Flutter widgets (moderate - reusable components)
if (p.includes('/widgets/') && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 1.5, reason: 'flutter-widget' };
}

// Flutter domain/services (moderate)
if ((p.includes('/domain/') || p.includes('/services/')) && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 1.8, reason: 'flutter-domain' };
}

// ========== GENERIC PATTERNS ==========

// Any language: index files in API folders
Expand Down Expand Up @@ -478,6 +510,10 @@ export const FRAMEWORK_AST_PATTERNS = {
'rails': ['ApplicationController', 'ApplicationRecord', 'ActiveRecord::Base',
'before_action', 'after_action', 'has_many', 'belongs_to', 'has_one', 'validates'],
'sinatra': ['Sinatra::Base', 'Sinatra::Application'],

// Dart/Flutter
'flutter': ['@override', 'Widget build', 'StatelessWidget', 'StatefulWidget', 'ConsumerWidget'],
'riverpod': ['@riverpod', 'ref.watch', 'ref.read', 'AsyncNotifier', 'Notifier'],
};

interface AstFrameworkPatternConfig {
Expand Down Expand Up @@ -545,6 +581,10 @@ export const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE = {
{ framework: 'rails', entryPointMultiplier: 3.0, reason: 'rails-pattern', patterns: FRAMEWORK_AST_PATTERNS.rails },
{ framework: 'sinatra', entryPointMultiplier: 2.8, reason: 'sinatra-pattern', patterns: FRAMEWORK_AST_PATTERNS.sinatra },
],
[SupportedLanguages.Dart]: [
{ framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-widget', patterns: FRAMEWORK_AST_PATTERNS.flutter },
{ framework: 'riverpod', entryPointMultiplier: 2.8, reason: 'riverpod-provider', patterns: FRAMEWORK_AST_PATTERNS.riverpod },
],
} satisfies Record<SupportedLanguages, AstFrameworkPatternConfig[]>;

/** Pre-lowercased patterns for O(1) pattern matching at runtime */
Expand Down
Loading