diff --git a/README.md b/README.md index a5b998d..64ee00b 100644 --- a/README.md +++ b/README.md @@ -10,32 +10,41 @@ The BIIGLE manual contains [some tutorials](https://biigle-admin-documentation.r ## How to use this template -First, [create a new repository](https://github.com/biigle/module/generate) based on this template. Then update the name of this module from `biigle/module` to whatever name you want to use for your module. The name has to be updated at the following locations: +First, [create a new repository](https://github.com/biigle/module/generate) (either private or public) based on this template. -1. [`QuotesController.php`](src/Http/Controllers/QuotesController.php#L16) -2. [`index.blade.php#L13`](src/resources/views/index.blade.php#L13) -3. [`index.blade.php#L16`](src/resources/views/index.blade.php#L16) -4. [`ModuleServiceProvider.php#L21`](src/ModuleServiceProvider.php#L21) -5. [`ModuleServiceProvider.php#L30`](src/ModuleServiceProvider.php#L30) -6. [`ModuleServiceProvider.php#L43`](src/ModuleServiceProvider.php#L43) -7. [`composer.json#L2`](composer.json#L2) -8. [`test.yml#L15`](.github/workflows/test.yml#L15) +## Development & Development Installation -Next, update the namespace of all PHP classes (`Biigle\Modules\Module`) and replace `Module` with the name of your module. Do this in [`webpack.mix.js#L23`](webpack.mix.js#L23), too. Also update this readme for your new module. You should remove the first two subsections and update the installation instructions. Now you can install the module and start developing. +Clone the module to a local directory. The name of this module needs to be updated in several locations. For your convenience, we provide a PHP script to automate this process. -In addition to the code of the [tutorials](https://biigle.de/manual#developer-tutorials) this repository already contains the configuration for [Laravel Mix](https://laravel.com/docs/9.x/mix) as build system. To install the build system, run and then run `npm install`. Now you can use the following commands: +Run `php setup.php YourModuleName` to update all locations at once. The name must start with an uppercase letter and may only contain letters and digits (e.g. `MyModule`). -- `npm run dev`: Starts the development server supporting hot module replacement. -- `npm run build`: Builds, minifies and publishes the assets once. Always do this before you commit new code. +If your module is not (yet) published on Packagist, you need to add the following to biigle's composer.json (found in the BIIGLE root directory; if repositories already exist, add the new one to the list): + +``` + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:/" + } + ] +``` + +If the repository is private you need to have ssh-keys configured for your GitHub account. Run `composer require biigle/:dev-main --ignore-platform-reqs` in the BIIGLE root directory to install the module to BIIGLE. Note that Composer identifies the package by the `name` in its composer.json (which `setup.php` sets to `biigle/` in lowercase), not by the repository URL. Still in the BIIGLE root directory run `php artisan vendor:publish --tag=public`. + +The module is now installed to `biigle/vendor/biigle/`. You can go there and develop your module. Run `npm install` once to set up the build system. Then you can use the following commands: + +- `npm run dev`: Continuously builds the assets during development. +- `npm run build`: Builds, minifies and publishes the assets once. - `npm run lint`: Run static analysis to check for errors. +Also update this readme for your new module. You should remove the first three subsections and update the installation instructions. And don't forget to change the author and GitHub address in `composer.json` as well. + ## Installation Note that you have to replace `biigle/module` with the actual name of your module/repository. 1. Run `composer require biigle/module`. *This requires your module to be published on [Packagist](https://packagist.org/). If you don't want to publish your package, read more on [alternative options](https://getcomposer.org/doc/05-repositories.md#vcs).* -2. Add `Biigle\Modules\Module\ModuleServiceProvider::class` to the `providers` array in `config/app.php`. *Replace `Module` in the class namespace with the name of your module.* -3. Run `php artisan vendor:publish --tag=public` to refresh the public assets of the modules. Do this for every update of this module. +2. Run `php artisan vendor:publish --tag=public` to refresh the public assets of the modules. Do this for every update of this module. ## Developing diff --git a/composer.json b/composer.json index 8a9b168..6d94e2c 100644 --- a/composer.json +++ b/composer.json @@ -18,5 +18,12 @@ "psr-4": { "Biigle\\Modules\\Module\\": "src" } + }, + "extra": { + "laravel": { + "providers": [ + "Biigle\\Modules\\Module\\ModuleServiceProvider" + ] + } } } diff --git a/setup.php b/setup.php new file mode 100644 index 0000000..c7bcaed --- /dev/null +++ b/setup.php @@ -0,0 +1,280 @@ +isFile() && $file->getExtension() === 'php') { + $files[] = $file->getPathname(); + } + } + } + + return $files; +} + +/** + * @param array{0: string, 1: string}[] $pairs Old and new text. + * + * @return string[] Paths of changed files. + */ +function replaceTextInPhpFiles(string $root, array $pairs): array +{ + $changedFiles = []; + + foreach (phpFiles($root) as $path) { + $content = file_get_contents($path); + $newContent = $content; + + foreach ($pairs as [$old, $new]) { + $newContent = str_replace($old, $new, $newContent); + } + + if ($newContent !== $content) { + file_put_contents($path, $newContent); + $changedFiles[] = $path; + } + } + + return $changedFiles; +} + +/** + * @return array{0: string, 1: string, 2: string, 3: int}[] Path, old text, new + * text and expected occurrence count. + */ +function buildReplacements( + string $root, + string $module, + string $moduleLower, + string $providerClass, + string $serviceProviderPath +): array { + return [ + [ + "{$root}/src/Http/Controllers/QuotesController.php", + "view('module::index')", + "view('{$moduleLower}::index')", + 1, + ], + [ + "{$root}/src/resources/views/index.blade.php", + 'vendor/biigle/module/hot', + "vendor/biigle/{$moduleLower}/hot", + 2, + ], + [ + "{$root}/src/resources/views/index.blade.php", + "'vendor/module')", + "'vendor/{$moduleLower}')", + 2, + ], + [ + $serviceProviderPath, + "loadViewsFrom(__DIR__.'/resources/views', 'module');", + "loadViewsFrom(__DIR__.'/resources/views', '{$moduleLower}');", + 1, + ], + [ + $serviceProviderPath, + "\$modules->register('module', [", + "\$modules->register('{$moduleLower}', [", + 1, + ], + [ + $serviceProviderPath, + "public_path('vendor/module')", + "public_path('vendor/{$moduleLower}')", + 1, + ], + [ + "{$root}/composer.json", + '"name": "biigle/module"', + "\"name\": \"biigle/{$moduleLower}\"", + 1, + ], + [ + "{$root}/composer.json", + '"Biigle\\\\Modules\\\\Module\\\\": "src"', + "\"Biigle\\\\Modules\\\\{$module}\\\\\": \"src\"", + 1, + ], + [ + "{$root}/composer.json", + '"Biigle\\\\Modules\\\\Module\\\\ModuleServiceProvider"', + "\"Biigle\\\\Modules\\\\{$module}\\\\{$providerClass}\"", + 1, + ], + [ + "{$root}/package.json", + '"name": "biigle-module"', + "\"name\": \"biigle-{$moduleLower}\"", + 1, + ], + [ + "{$root}/package.json", + 'Module\\\\\\\\ModuleServiceProvider', + "{$module}\\\\\\\\{$providerClass}", + 1, + ], + [ + "{$root}/.github/workflows/test.yml", + 'MODULE_NAME: Module', + "MODULE_NAME: {$module}", + 1, + ], + ]; +} + + +if (count($argv) < 2) { + fwrite(STDERR, "Usage: php setup.php \n"); + exit(1); +} + +$module = $argv[1]; + +if (!preg_match('/^[A-Z][A-Za-z0-9]*$/', $module)) { + fwrite(STDERR, "The module name must start with an uppercase letter and may only contain letters and digits (e.g. MyModule).\n"); + exit(1); +} + +$root = __DIR__; +$moduleLower = strtolower($module); +$providerClass = "{$module}ServiceProvider"; + +$providerOldPath = "{$root}/src/ModuleServiceProvider.php"; +$providerNewPath = "{$root}/src/{$providerClass}.php"; +$providerTestOldPath = "{$root}/tests/ModuleServiceProviderTest.php"; +$providerTestNewPath = "{$root}/tests/{$providerClass}Test.php"; + +try { + // Validate everything before modifying any file so a failure cannot leave + // the module half renamed. + validateRename($providerOldPath, $providerNewPath); + validateRename($providerTestOldPath, $providerTestNewPath); + + $providerCurrentPath = file_exists($providerOldPath) + ? $providerOldPath + : $providerNewPath; + + $replacements = buildReplacements( + $root, $module, $moduleLower, $providerClass, $providerCurrentPath + ); + + $pending = []; + foreach ($replacements as [$path, $old, $new, $expectedCount]) { + if (checkText($path, $old, $new, $expectedCount)) { + $pending[] = [$path, $old, $new]; + } + } + + $changedFiles = []; + + if (file_exists($providerOldPath)) { + rename($providerOldPath, $providerNewPath); + $changedFiles[] = $providerNewPath; + } + + if (file_exists($providerTestOldPath)) { + rename($providerTestOldPath, $providerTestNewPath); + $changedFiles[] = $providerTestNewPath; + } + + foreach ($pending as [$path, $old, $new]) { + if ($path === $providerCurrentPath) { + $path = $providerNewPath; + } + applyText($path, $old, $new); + $changedFiles[] = $path; + } + + // The test namespace prefix does not contain the regular namespace prefix + // as a substring, so it must be replaced explicitly. + $changedFiles = array_merge($changedFiles, replaceTextInPhpFiles($root, [ + ['Biigle\\Tests\\Modules\\Module', "Biigle\\Tests\\Modules\\{$module}"], + ['Biigle\\Modules\\Module', "Biigle\\Modules\\{$module}"], + ['ModuleServiceProvider', $providerClass], + ])); +} catch (RuntimeException $e) { + fwrite(STDERR, $e->getMessage()."\n"); + exit(1); +} + +$changedFiles = array_unique($changedFiles); + +if ($changedFiles) { + $count = count($changedFiles); + echo "Updated module name to '{$module}' in {$count} file(s):\n"; + foreach ($changedFiles as $path) { + echo ' '.substr($path, strlen($root) + 1)."\n"; + } +} else { + echo "No changes needed. Module name already set to '{$module}'.\n"; +}