Add framework detection to Appwrite

This commit is contained in:
Khushboo Verma 2025-02-06 23:59:43 +05:30
parent e0131aa749
commit 8dd99b7d2a
6 changed files with 334 additions and 83 deletions

View file

@ -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']);

View file

@ -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"
}
]
}

106
composer.lock generated
View file

@ -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"
}

View file

@ -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())

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class FrameworkDetection extends Model
{
public function __construct()
{
$this
->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;
}
}

View file

@ -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;
}
}