diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php
index eeb5835..49dce99 100644
--- a/app/Services/PluginImportService.php
+++ b/app/Services/PluginImportService.php
@@ -20,12 +20,13 @@ class PluginImportService
/**
* Validate YAML settings
*
- * @param array $settings The parsed YAML settings
+ * @param array $settings The parsed YAML settings
+ *
* @throws Exception
*/
private function validateYAML(array $settings): void
{
- if (!isset($settings['custom_fields']) || !is_array($settings['custom_fields'])) {
+ if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) {
return;
}
@@ -43,6 +44,7 @@ class PluginImportService
}
}
}
+
/**
* Import a plugin from a ZIP file
*
@@ -73,12 +75,17 @@ class PluginImportService
$zip->extractTo($tempDir);
$zip->close();
- // Find the required files (settings.yml and full.liquid/full.blade.php)
+ // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php)
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
// Validate that we found the required files
- if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
- throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php
+ if (! $filePaths['settingsYamlPath']) {
+ throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
+ }
+
+ // Validate that we have at least one template file
+ if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) {
+ throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
}
// Parse settings.yml
@@ -86,20 +93,37 @@ class PluginImportService
$settings = Yaml::parse($settingsYaml);
$this->validateYAML($settings);
- // Read full.liquid content
- $fullLiquid = File::get($filePaths['fullLiquidPath']);
-
- // Prepend shared.liquid content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
+ // Determine which template file to use and read its content
+ $templatePath = null;
$markupLanguage = 'blade';
- if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
+
+ if ($filePaths['fullLiquidPath']) {
+ $templatePath = $filePaths['fullLiquidPath'];
+ $fullLiquid = File::get($templatePath);
+
+ // Prepend shared.liquid or shared.blade.php content if available
+ if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
+ $fullLiquid = $sharedLiquid."\n".$fullLiquid;
+ } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
+ $sharedBlade = File::get($filePaths['sharedBladePath']);
+ $fullLiquid = $sharedBlade."\n".$fullLiquid;
+ }
+
+ // Check if the file ends with .liquid to set markup language
+ if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
+ $markupLanguage = 'liquid';
+ $fullLiquid = '
'."\n".$fullLiquid."\n".'
';
+ }
+ } elseif ($filePaths['sharedLiquidPath']) {
+ $templatePath = $filePaths['sharedLiquidPath'];
+ $fullLiquid = File::get($templatePath);
$markupLanguage = 'liquid';
$fullLiquid = ''."\n".$fullLiquid."\n".'
';
+ } elseif ($filePaths['sharedBladePath']) {
+ $templatePath = $filePaths['sharedBladePath'];
+ $fullLiquid = File::get($templatePath);
+ $markupLanguage = 'blade';
}
// Ensure custom_fields is properly formatted
@@ -204,12 +228,17 @@ class PluginImportService
$zip->extractTo($tempDir);
$zip->close();
- // Find the required files (settings.yml and full.liquid/full.blade.php)
+ // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php)
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
// Validate that we found the required files
- if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
- throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.');
+ if (! $filePaths['settingsYamlPath']) {
+ throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
+ }
+
+ // Validate that we have at least one template file
+ if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) {
+ throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
}
// Parse settings.yml
@@ -217,20 +246,37 @@ class PluginImportService
$settings = Yaml::parse($settingsYaml);
$this->validateYAML($settings);
- // Read full.liquid content
- $fullLiquid = File::get($filePaths['fullLiquidPath']);
-
- // Prepend shared.liquid content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
+ // Determine which template file to use and read its content
+ $templatePath = null;
$markupLanguage = 'blade';
- if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
+
+ if ($filePaths['fullLiquidPath']) {
+ $templatePath = $filePaths['fullLiquidPath'];
+ $fullLiquid = File::get($templatePath);
+
+ // Prepend shared.liquid or shared.blade.php content if available
+ if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
+ $fullLiquid = $sharedLiquid."\n".$fullLiquid;
+ } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
+ $sharedBlade = File::get($filePaths['sharedBladePath']);
+ $fullLiquid = $sharedBlade."\n".$fullLiquid;
+ }
+
+ // Check if the file ends with .liquid to set markup language
+ if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
+ $markupLanguage = 'liquid';
+ $fullLiquid = ''."\n".$fullLiquid."\n".'
';
+ }
+ } elseif ($filePaths['sharedLiquidPath']) {
+ $templatePath = $filePaths['sharedLiquidPath'];
+ $fullLiquid = File::get($templatePath);
$markupLanguage = 'liquid';
$fullLiquid = ''."\n".$fullLiquid."\n".'
';
+ } elseif ($filePaths['sharedBladePath']) {
+ $templatePath = $filePaths['sharedBladePath'];
+ $fullLiquid = File::get($templatePath);
+ $markupLanguage = 'blade';
}
// Ensure custom_fields is properly formatted
@@ -310,6 +356,7 @@ class PluginImportService
$settingsYamlPath = null;
$fullLiquidPath = null;
$sharedLiquidPath = null;
+ $sharedBladePath = null;
// If zipEntryPath is specified, look for files in that specific directory first
if ($zipEntryPath) {
@@ -327,6 +374,8 @@ class PluginImportService
if (File::exists($targetDir.'/shared.liquid')) {
$sharedLiquidPath = $targetDir.'/shared.liquid';
+ } elseif (File::exists($targetDir.'/shared.blade.php')) {
+ $sharedBladePath = $targetDir.'/shared.blade.php';
}
}
@@ -342,15 +391,18 @@ class PluginImportService
if (File::exists($targetDir.'/src/shared.liquid')) {
$sharedLiquidPath = $targetDir.'/src/shared.liquid';
+ } elseif (File::exists($targetDir.'/src/shared.blade.php')) {
+ $sharedBladePath = $targetDir.'/src/shared.blade.php';
}
}
// If we found the required files in the target directory, return them
- if ($settingsYamlPath && $fullLiquidPath) {
+ if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) {
return [
'settingsYamlPath' => $settingsYamlPath,
'fullLiquidPath' => $fullLiquidPath,
'sharedLiquidPath' => $sharedLiquidPath,
+ 'sharedBladePath' => $sharedBladePath,
];
}
}
@@ -367,9 +419,11 @@ class PluginImportService
$fullLiquidPath = $tempDir.'/src/full.blade.php';
}
- // Check for shared.liquid in the same directory
+ // Check for shared.liquid or shared.blade.php in the same directory
if (File::exists($tempDir.'/src/shared.liquid')) {
$sharedLiquidPath = $tempDir.'/src/shared.liquid';
+ } elseif (File::exists($tempDir.'/src/shared.blade.php')) {
+ $sharedBladePath = $tempDir.'/src/shared.blade.php';
}
} else {
// Search for the files in the extracted directory structure
@@ -386,20 +440,24 @@ class PluginImportService
$fullLiquidPath = $filepath;
} elseif ($filename === 'shared.liquid') {
$sharedLiquidPath = $filepath;
+ } elseif ($filename === 'shared.blade.php') {
+ $sharedBladePath = $filepath;
}
}
- // Check if shared.liquid exists in the same directory as full.liquid
- if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath) {
+ // Check if shared.liquid or shared.blade.php exists in the same directory as full.liquid
+ if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath && ! $sharedBladePath) {
$fullLiquidDir = dirname((string) $fullLiquidPath);
if (File::exists($fullLiquidDir.'/shared.liquid')) {
$sharedLiquidPath = $fullLiquidDir.'/shared.liquid';
+ } elseif (File::exists($fullLiquidDir.'/shared.blade.php')) {
+ $sharedBladePath = $fullLiquidDir.'/shared.blade.php';
}
}
// If we found the files but they're not in the src folder,
// check if they're in the root of the ZIP or in a subfolder
- if ($settingsYamlPath && $fullLiquidPath) {
+ if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) {
// If the files are in the root of the ZIP, create a src folder and move them there
$srcDir = dirname((string) $settingsYamlPath);
@@ -410,17 +468,25 @@ class PluginImportService
// Copy the files to the src directory
File::copy($settingsYamlPath, $newSrcDir.'/settings.yml');
- File::copy($fullLiquidPath, $newSrcDir.'/full.liquid');
- // Copy shared.liquid if it exists
+ // Copy full.liquid or full.blade.php if it exists
+ if ($fullLiquidPath) {
+ $extension = pathinfo((string) $fullLiquidPath, PATHINFO_EXTENSION);
+ File::copy($fullLiquidPath, $newSrcDir.'/full.'.$extension);
+ $fullLiquidPath = $newSrcDir.'/full.'.$extension;
+ }
+
+ // Copy shared.liquid or shared.blade.php if it exists
if ($sharedLiquidPath) {
File::copy($sharedLiquidPath, $newSrcDir.'/shared.liquid');
$sharedLiquidPath = $newSrcDir.'/shared.liquid';
+ } elseif ($sharedBladePath) {
+ File::copy($sharedBladePath, $newSrcDir.'/shared.blade.php');
+ $sharedBladePath = $newSrcDir.'/shared.blade.php';
}
// Update the paths
$settingsYamlPath = $newSrcDir.'/settings.yml';
- $fullLiquidPath = $newSrcDir.'/full.liquid';
}
}
}
@@ -429,6 +495,7 @@ class PluginImportService
'settingsYamlPath' => $settingsYamlPath,
'fullLiquidPath' => $fullLiquidPath,
'sharedLiquidPath' => $sharedLiquidPath,
+ 'sharedBladePath' => $sharedBladePath,
];
}
diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php
index fae28a8..f3ef1fa 100644
--- a/tests/Feature/PluginImportTest.php
+++ b/tests/Feature/PluginImportTest.php
@@ -83,19 +83,34 @@ it('throws exception for invalid zip file', function (): void {
->toThrow(Exception::class, 'Could not open the ZIP file.');
});
-it('throws exception for missing required files', function (): void {
+it('throws exception for missing settings.yml', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
- 'src/settings.yml' => getValidSettingsYaml(),
- // Missing full.liquid
+ 'src/full.liquid' => getValidFullLiquid(),
+ // Missing settings.yml
]);
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
$pluginImportService = new PluginImportService();
expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.');
+ ->toThrow(Exception::class, 'Invalid ZIP structure. Required file settings.yml is missing.');
+});
+
+it('throws exception for missing template files', function (): void {
+ $user = User::factory()->create();
+
+ $zipContent = createMockZipFile([
+ 'src/settings.yml' => getValidSettingsYaml(),
+ // Missing all template files
+ ]);
+
+ $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
+
+ $pluginImportService = new PluginImportService();
+ expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
+ ->toThrow(Exception::class, 'Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
});
it('sets default values when settings are missing', function (): void {
@@ -431,7 +446,7 @@ it('throws exception when multi_string default value contains a comma', function
$user = User::factory()->create();
// YAML with a comma in the 'default' field of a multi_string
- $invalidYaml = << $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, "Validation Error: The default value for multistring fields like `api_key` cannot contain commas.");
+ ->toThrow(Exception::class, 'Validation Error: The default value for multistring fields like `api_key` cannot contain commas.');
});
it('throws exception when multi_string placeholder contains a comma', function (): void {
$user = User::factory()->create();
// YAML with a comma in the 'placeholder' field
- $invalidYaml = << $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, "Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.");
+ ->toThrow(Exception::class, 'Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.');
+});
+
+it('imports plugin with only shared.liquid file', function (): void {
+ $user = User::factory()->create();
+
+ $zipContent = createMockZipFile([
+ 'src/settings.yml' => getValidSettingsYaml(),
+ 'src/shared.liquid' => '{{ data.title }}
',
+ ]);
+
+ $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
+
+ $pluginImportService = new PluginImportService();
+ $plugin = $pluginImportService->importFromZip($zipFile, $user);
+
+ expect($plugin)->toBeInstanceOf(Plugin::class)
+ ->and($plugin->markup_language)->toBe('liquid')
+ ->and($plugin->render_markup)->toContain('')
+ ->and($plugin->render_markup)->toContain('
{{ data.title }}
');
+});
+
+it('imports plugin with only shared.blade.php file', function (): void {
+ $user = User::factory()->create();
+
+ $zipContent = createMockZipFile([
+ 'src/settings.yml' => getValidSettingsYaml(),
+ 'src/shared.blade.php' => '
{{ $data["title"] }}
',
+ ]);
+
+ $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
+
+ $pluginImportService = new PluginImportService();
+ $plugin = $pluginImportService->importFromZip($zipFile, $user);
+
+ expect($plugin)->toBeInstanceOf(Plugin::class)
+ ->and($plugin->markup_language)->toBe('blade')
+ ->and($plugin->render_markup)->toBe('
{{ $data["title"] }}
')
+ ->and($plugin->render_markup)->not->toContain('
');
});
// Helper methods