diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 921a840c0a..dc64f34a7e 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -26,22 +26,35 @@ use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Query\Cursor; -use Utopia\Detector\Adapter\Bun; -use Utopia\Detector\Adapter\CPP; -use Utopia\Detector\Adapter\Dart; -use Utopia\Detector\Adapter\Deno; -use Utopia\Detector\Adapter\Dotnet; -use Utopia\Detector\Adapter\Java; -use Utopia\Detector\Adapter\JavaScript; -use Utopia\Detector\Adapter\PHP; -use Utopia\Detector\Adapter\Python; -use Utopia\Detector\Adapter\Ruby; -use Utopia\Detector\Adapter\Swift; -use Utopia\Detector\Detector; +use Utopia\Detector\Detection\Framework\Astro; +use Utopia\Detector\Detection\Framework\Flutter; +use Utopia\Detector\Detection\Framework\NextJs; +use Utopia\Detector\Detection\Framework\Nuxt; +use Utopia\Detector\Detection\Framework\Remix; +use Utopia\Detector\Detection\Framework\SvelteKit; +use Utopia\Detector\Detection\Packager\NPM; +use Utopia\Detector\Detection\Packager\PNPM; +use Utopia\Detector\Detection\Packager\Yarn; +use Utopia\Detector\Detection\Runtime\Bun; +use Utopia\Detector\Detection\Runtime\CPP; +use Utopia\Detector\Detection\Runtime\Dart; +use Utopia\Detector\Detection\Runtime\Deno; +use Utopia\Detector\Detection\Runtime\Dotnet; +use Utopia\Detector\Detection\Runtime\Java; +use Utopia\Detector\Detection\Runtime\Node; +use Utopia\Detector\Detection\Runtime\PHP; +use Utopia\Detector\Detection\Runtime\Python; +use Utopia\Detector\Detection\Runtime\Ruby; +use Utopia\Detector\Detection\Runtime\Swift; +use Utopia\Detector\Detector\Framework; +use Utopia\Detector\Detector\Packager; +use Utopia\Detector\Detector\Runtime; +use Utopia\Detector\Detector\Strategy; use Utopia\System\System; use Utopia\Validator\Boolean; use Utopia\Validator\Host; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; @@ -544,8 +557,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro ]), Response::MODEL_VCS_CONTENT_LIST); }); -App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') - ->desc('Detect runtime settings from source code') +App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detections') + ->alias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') + ->desc('Detect runtime and framework settings from source code') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') ->label('sdk', new Method( @@ -556,18 +570,22 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, - model: Response::MODEL_DETECTION, + model: Response::MODEL_RUNTIME_DETECTION, + ), + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_FRAMEWORK_DETECTION, ) ] )) ->param('installationId', '', new Text(256), 'Installation Id') ->param('providerRepositoryId', '', new Text(256), 'Repository Id') ->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true) + ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework', true) ->inject('gitHub') ->inject('response') - ->inject('project') ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) { + ->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, string $type, GitHub $github, Response $response, Database $dbForPlatform) { $installation = $dbForPlatform->getDocument('installations', $installationId); if ($installation->isEmpty()) { @@ -593,32 +611,100 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr $files = \array_column($files, 'name'); $languages = $github->listRepositoryLanguages($owner, $repositoryName); - $detectorFactory = new Detector($files, $languages); + $detector = new Packager($files); + $detector + ->addOption(new Yarn()) + ->addOption(new PNPM()) + ->addOption(new NPM()); + $detectedPackager = $detector->detect(); - $detectorFactory - ->addDetector(new JavaScript()) - ->addDetector(new Bun()) - ->addDetector(new PHP()) - ->addDetector(new Python()) - ->addDetector(new Dart()) - ->addDetector(new Swift()) - ->addDetector(new Ruby()) - ->addDetector(new Java()) - ->addDetector(new CPP()) - ->addDetector(new Deno()) - ->addDetector(new Dotnet()); - - $runtime = $detectorFactory->detect(); - - $runtimes = Config::getParam('runtimes'); - $runtimeDetail = \array_reverse(\array_filter(\array_keys($runtimes), function ($key) use ($runtime, $runtimes) { - return $runtimes[$key]['key'] === $runtime; - }))[0] ?? ''; + $packagerName = $detectedPackager ? $detectedPackager->getName() : 'npm'; $detection = []; - $detection['runtime'] = $runtimeDetail; - $response->dynamic(new Document($detection), Response::MODEL_DETECTION); + if ($type === 'framework') { + $detection = [ + 'framework' => '', + 'installCommand' => '', + 'buildCommand' => '', + 'outputDirectory' => '', + ]; + + $frameworkDetector = new Framework($files, $packagerName); + $frameworkDetector + ->addOption(new Flutter()) + ->addOption(new Nuxt()) + ->addOption(new Astro()) + ->addOption(new Remix()) + ->addOption(new SvelteKit()) + ->addOption(new NextJs()); + + $detectedFramework = $frameworkDetector->detect(); + + if ($detectedFramework) { + $framework = $detectedFramework->getName(); + $detection['installCommand'] = $detectedFramework->getInstallCommand(); + $detection['buildCommand'] = $detectedFramework->getBuildCommand(); + $detection['outputDirectory'] = $detectedFramework->getOutputDirectory(); + } + + if (!empty($framework)) { + $frameworks = Config::getParam('frameworks'); + $frameworkDetail = \array_reverse(\array_filter(\array_keys($frameworks), function ($key) use ($framework, $frameworks) { + return $frameworks[$key]['key'] === $framework; + }))[0] ?? ''; + $detection['framework'] = $frameworkDetail; + } + + $response->dynamic(new Document($detection), Response::MODEL_FRAMEWORK_DETECTION); + } else { + $detection = [ + 'runtime' => '', + 'commands' => '', + 'entrypoint' => '', + ]; + + $strategies = [ + new Strategy(Strategy::FILEMATCH), + new Strategy(Strategy::LANGUAGES), + new Strategy(Strategy::EXTENSION), + ]; + + foreach ($strategies as $strategy) { + $runtimeDetector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, $packagerName); + $runtimeDetector + ->addOption(new Node()) + ->addOption(new Bun()) + ->addOption(new Deno()) + ->addOption(new PHP()) + ->addOption(new Python()) + ->addOption(new Dart()) + ->addOption(new Swift()) + ->addOption(new Ruby()) + ->addOption(new Java()) + ->addOption(new CPP()) + ->addOption(new Dotnet()); + + $detectedRuntime = $runtimeDetector->detect(); + + if ($detectedRuntime) { + $detection['commands'] = $detectedRuntime->getCommands(); + $detection['entrypoint'] = $detectedRuntime->getEntrypoint(); + $runtime = $detectedRuntime->getName(); + break; + } + } + + if (!empty($runtime)) { + $runtimes = Config::getParam('runtimes'); + $runtimeDetail = \array_reverse(\array_filter(\array_keys($runtimes), function ($key) use ($runtime, $runtimes) { + return $runtimes[$key]['key'] === $runtime; + }))[0] ?? ''; + $detection['runtime'] = $runtimeDetail; + } + + $response->dynamic(new Document($detection), Response::MODEL_RUNTIME_DETECTION); + } }); App::get('/v1/vcs/github/installations/:installationId/providerRepositories') @@ -680,29 +766,44 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories') $files = \array_column($files, 'name'); $languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']); - $detectorFactory = new Detector($files, $languages); + $strategies = [ + new Strategy(Strategy::FILEMATCH), + new Strategy(Strategy::LANGUAGES), + new Strategy(Strategy::EXTENSION), + ]; - $detectorFactory - ->addDetector(new JavaScript()) - ->addDetector(new Bun()) - ->addDetector(new PHP()) - ->addDetector(new Python()) - ->addDetector(new Dart()) - ->addDetector(new Swift()) - ->addDetector(new Ruby()) - ->addDetector(new Java()) - ->addDetector(new CPP()) - ->addDetector(new Deno()) - ->addDetector(new Dotnet()); + foreach ($strategies as $strategy) { + $runtimeDetector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, 'npm'); + $runtimeDetector + ->addOption(new Node()) + ->addOption(new Bun()) + ->addOption(new Deno()) + ->addOption(new PHP()) + ->addOption(new Python()) + ->addOption(new Dart()) + ->addOption(new Swift()) + ->addOption(new Ruby()) + ->addOption(new Java()) + ->addOption(new CPP()) + ->addOption(new Dotnet()); - $runtime = $detectorFactory->detect(); + $detectedRuntime = $runtimeDetector->detect(); - $runtimes = Config::getParam('runtimes'); - $runtimeDetail = \array_reverse(\array_filter(\array_keys($runtimes), function ($key) use ($runtime, $runtimes) { - return $runtimes[$key]['key'] === $runtime; - }))[0] ?? ''; + if ($detectedRuntime) { + $runtime = $detectedRuntime->getName(); + break; + } + } - $repo['runtime'] = $runtimeDetail; + if (!empty($runtime)) { + $runtimes = Config::getParam('runtimes'); + $runtimeDetail = \array_reverse(\array_filter(\array_keys($runtimes), function ($key) use ($runtime, $runtimes) { + return $runtimes[$key]['key'] === $runtime; + }))[0] ?? ''; + $repo['runtime'] = $runtimeDetail; + } else { + throw new Exception("Runtime not detected"); + } } catch (Throwable $error) { $repo['runtime'] = ""; Console::warning("Runtime not detected for " . $repo['organization'] . "/" . $repo['name']); diff --git a/composer.json b/composer.json index b497451efd..2d5eb3edb1 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", "utopia-php/database": "0.56.4", + "utopia-php/detector": "dev-feat-pseudocode-draft2 as 0.1.99", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "dev-fix-prevent-duplicate-compression as 0.33.99", @@ -102,5 +103,11 @@ "php-http/discovery": true, "tbachert/spi": true } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/detector" + } + ] } diff --git a/composer.lock b/composer.lock index a0bcb622b2..00855c40c6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2caa51e1b7d11e3e67ade41347530f92", + "content-hash": "895d12203ebc543ae26dced08f811b04", "packages": [ { "name": "adhocore/jwt", @@ -1237,16 +1237,16 @@ }, { "name": "open-telemetry/api", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "74b1a03263be8c5acb578f41da054b4bac3af4a0" + "reference": "8b925df3047628968bc5be722468db1b98b82d51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/74b1a03263be8c5acb578f41da054b4bac3af4a0", - "reference": "74b1a03263be8c5acb578f41da054b4bac3af4a0", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/8b925df3047628968bc5be722468db1b98b82d51", + "reference": "8b925df3047628968bc5be722468db1b98b82d51", "shasum": "" }, "require": { @@ -1303,7 +1303,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-20T23:35:16+00:00" + "time": "2025-02-03T21:49:11+00:00" }, { "name": "open-telemetry/context", @@ -1493,16 +1493,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "96aeaee5b7cb8c0bc4af7ff4717b429f2d9f67e1" + "reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/96aeaee5b7cb8c0bc4af7ff4717b429f2d9f67e1", - "reference": "96aeaee5b7cb8c0bc4af7ff4717b429f2d9f67e1", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/37eec0fe47ddd627911f318f29b6cd48196be0c0", + "reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0", "shasum": "" }, "require": { @@ -1579,24 +1579,24 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-09T23:17:14+00:00" + "time": "2025-01-29T21:40:28+00:00" }, { "name": "open-telemetry/sem-conv", - "version": "1.27.1", + "version": "1.30.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6" + "reference": "4178c9f390da8e4dbca9b181a9d1efd50cf7ee0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/1dba705fea74bc0718d04be26090e3697e56f4e6", - "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/4178c9f390da8e4dbca9b181a9d1efd50cf7ee0a", + "reference": "4178c9f390da8e4dbca9b181a9d1efd50cf7ee0a", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.0" }, "type": "library", "extra": { @@ -1636,7 +1636,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-08-28T09:20:31+00:00" + "time": "2025-02-06T00:21:48+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -3530,6 +3530,69 @@ }, "time": "2025-01-20T09:22:08+00:00" }, + { + "name": "utopia-php/detector", + "version": "dev-feat-pseudocode-draft2", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/detector.git", + "reference": "09512d06b4b3a1a4acca2403ea9c22f03975d79e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/detector/zipball/09512d06b4b3a1a4acca2403ea9c22f03975d79e", + "reference": "09512d06b4b3a1a4acca2403ea9c22f03975d79e", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.8.*", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Detector\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Utopia\\Tests\\": "tests/Detector/" + } + }, + "scripts": { + "lint": [ + "./vendor/bin/pint --test --config pint.json" + ], + "format": [ + "./vendor/bin/pint --config pint.json" + ], + "check": [ + "./vendor/bin/phpstan analyse --level 8 -c phpstan.neon src tests" + ], + "test": [ + "./vendor/bin/phpunit --configuration phpunit.xml --debug" + ] + }, + "license": [ + "MIT" + ], + "description": "A simple library for fast and reliable environment identification.", + "keywords": [ + "detector", + "framework", + "php", + "utopia" + ], + "support": { + "source": "https://github.com/utopia-php/detector/tree/feat-pseudocode-draft2", + "issues": "https://github.com/utopia-php/detector/issues" + }, + "time": "2025-02-06T13:36:29+00:00" + }, { "name": "utopia-php/domains", "version": "0.5.0", @@ -8502,6 +8565,12 @@ } ], "aliases": [ + { + "package": "utopia-php/detector", + "version": "dev-feat-pseudocode-draft2", + "alias": "0.1.99", + "alias_normalized": "0.1.99.0" + }, { "package": "utopia-php/framework", "version": "dev-fix-prevent-duplicate-compression", @@ -8511,6 +8580,7 @@ ], "minimum-stability": "stable", "stability-flags": { + "utopia-php/detector": 20, "utopia-php/framework": 20 }, "prefer-stable": false, @@ -8536,5 +8606,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index da222822e0..e9c3540672 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -38,7 +38,6 @@ use Appwrite\Utopia\Response\Model\Country; use Appwrite\Utopia\Response\Model\Currency; use Appwrite\Utopia\Response\Model\Database; use Appwrite\Utopia\Response\Model\Deployment; -use Appwrite\Utopia\Response\Model\Detection; use Appwrite\Utopia\Response\Model\Document as ModelDocument; use Appwrite\Utopia\Response\Model\Error; use Appwrite\Utopia\Response\Model\ErrorDev; @@ -46,6 +45,7 @@ use Appwrite\Utopia\Response\Model\Execution; use Appwrite\Utopia\Response\Model\File; use Appwrite\Utopia\Response\Model\Framework; use Appwrite\Utopia\Response\Model\FrameworkAdapter; +use Appwrite\Utopia\Response\Model\FrameworkDetection; use Appwrite\Utopia\Response\Model\Func; use Appwrite\Utopia\Response\Model\Headers; use Appwrite\Utopia\Response\Model\HealthAntivirus; @@ -85,6 +85,7 @@ use Appwrite\Utopia\Response\Model\Provider; use Appwrite\Utopia\Response\Model\ProviderRepository; use Appwrite\Utopia\Response\Model\Rule; use Appwrite\Utopia\Response\Model\Runtime; +use Appwrite\Utopia\Response\Model\RuntimeDetection; use Appwrite\Utopia\Response\Model\Session; use Appwrite\Utopia\Response\Model\Site; use Appwrite\Utopia\Response\Model\Specification; @@ -249,7 +250,8 @@ class Response extends SwooleResponse public const MODEL_PROVIDER_REPOSITORY_LIST = 'providerRepositoryList'; public const MODEL_BRANCH = 'branch'; public const MODEL_BRANCH_LIST = 'branchList'; - public const MODEL_DETECTION = 'detection'; + public const MODEL_FRAMEWORK_DETECTION = 'frameworkDetection'; + public const MODEL_RUNTIME_DETECTION = 'runtimeDetection'; public const MODEL_VCS_CONTENT = 'vcsContent'; public const MODEL_VCS_CONTENT_LIST = 'vcsContentList'; @@ -453,7 +455,8 @@ class Response extends SwooleResponse ->setModel(new TemplateVariable()) ->setModel(new Installation()) ->setModel(new ProviderRepository()) - ->setModel(new Detection()) + ->setModel(new FrameworkDetection()) + ->setModel(new RuntimeDetection()) ->setModel(new VcsContent()) ->setModel(new Branch()) ->setModel(new Runtime()) diff --git a/src/Appwrite/Utopia/Response/Model/FrameworkDetection.php b/src/Appwrite/Utopia/Response/Model/FrameworkDetection.php new file mode 100644 index 0000000000..d0c666ae91 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/FrameworkDetection.php @@ -0,0 +1,58 @@ +addRule('framework', [ + 'type' => self::TYPE_STRING, + 'description' => 'Framework', + 'default' => '', + 'example' => 'nuxt', + ]) + ->addRule('installCommand', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site Install Command', + 'default' => '', + 'example' => 'npm install', + ]) + ->addRule('buildCommand', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site Build Command', + 'default' => '', + 'example' => 'npm run build', + ]) + ->addRule('outputDirectory', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site Output Directory', + 'default' => '', + 'example' => 'dist', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'FrameworkDetection'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_FRAMEWORK_DETECTION; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Detection.php b/src/Appwrite/Utopia/Response/Model/RuntimeDetection.php similarity index 50% rename from src/Appwrite/Utopia/Response/Model/Detection.php rename to src/Appwrite/Utopia/Response/Model/RuntimeDetection.php index c71baa0b0c..b79dccff9d 100644 --- a/src/Appwrite/Utopia/Response/Model/Detection.php +++ b/src/Appwrite/Utopia/Response/Model/RuntimeDetection.php @@ -5,7 +5,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; -class Detection extends Model +class RuntimeDetection extends Model { public function __construct() { @@ -15,6 +15,18 @@ class Detection extends Model 'description' => 'Runtime', 'default' => '', 'example' => 'node', + ]) + ->addRule('entrypoint', [ + 'type' => self::TYPE_STRING, + 'description' => 'Function Entrypoint', + 'default' => '', + 'example' => 'index.js', + ]) + ->addRule('commands', [ + 'type' => self::TYPE_STRING, + 'description' => 'Function install and build commands', + 'default' => '', + 'example' => 'npm install && npm run build', ]); } @@ -25,7 +37,7 @@ class Detection extends Model */ public function getName(): string { - return 'Detection'; + return 'RuntimeDetection'; } /** @@ -35,6 +47,6 @@ class Detection extends Model */ public function getType(): string { - return Response::MODEL_DETECTION; + return Response::MODEL_RUNTIME_DETECTION; } }