public function scanSource($sourceDir) {
        $this->log(LogLevel::INFO, "Scanning addon source: {$sourceDir}");

        $addonsCandidates = scandir($sourceDir);
        foreach ($addonsCandidates as $addonCandidate) {
            // Ignore hidden files and directory traversal links
            $char = substr($addonCandidate, 0, 1);
            if ($char == '.') {
                continue;
            }

            // Search for addon definition file
            $definitionFile = paths($sourceDir, $addonCandidate, "addon.json");
            if (!file_exists($definitionFile)) {
                continue;
            }

            try {
                $addon = new Addon(realpath($definitionFile));
            } catch (\Exception $ex) {
                $this->log(LogLevel::WARNING, " failed loading addon '{definition}': {message}", [
                    'definition' => $definitionFile,
                    'message' => $ex->getMessage()
                ]);
                continue;
            }

            // Addon loaded
            $this->log(LogLevel::INFO, " found addon: {name} v{version} (provides {classes} classes from {path})", [
                'name' => $addon->getInfo('name'),
                'version' => $addon->getInfo('version'),
                'path' => $addon->getPath(),
                'classes' => count($addon->getClasses())
            ]);

            $this->addons[$addon->getInfo('name')] = $addon;
        }
    }