diff --git a/CHANGELOG.md b/CHANGELOG.md index d3f7370..5776b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v3.0.0 + +The new version changes the plugin architecture and requires some modifications. It's highly recommended to check the +README.md + +* Updated the plugin's configuration array +* The methods `$this->ViteScripts->pluginScript($pluginName)` and `pluginCss()` were removed +* The `$this->ViteScripts->css()` and `$this->ViteScripts->script()` were updated +* The running environment MUST be defined explicitly or a detector should be used + ## v2.3.0 ### Changed diff --git a/README.md b/README.md index 5b7c4aa..e6ef4b2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ reloading. In production mode, the Helper loads the bundled files. `@vitejs/plugin-legacy` is supported, which will insert `nomodule`-tags for older browsers, e.g. older iOS devices, which do not support js modules. -> This readme is for **version 1.x.** If you are migrating from 0.x and something is unclear, read the Migration guide +> This readme is for **version 3.x.** If you are migrating from 0.x and something is unclear, read the Migration guide > under `/docs`. Feel free to open an issue if you run into problems. ## Installation @@ -17,12 +17,13 @@ You can install this plugin into your CakePHP application using [composer](https ### CakePHP Version Map -| CakePHP version | Plugin Version | Branch | min. PHP Version | -|-----------------|----------------|--------|------------------| -| ^3.10 | / | cake3 | ^7.4 | -| ^4.2 | 0.x | master | ^7.4 | -| ^4.2 | 1.x | master | ^8.0 | -| ^5.0 | 2.x | cake5 | ^8.1 | +| CakePHP version | Plugin Version | Branch | min. PHP Version | +|-----------------|----------------|-------------|------------------| +| ^3.10 | / | cake3 | ^7.4 | +| ^4.2 | 0.x | master | ^7.4 | +| ^4.2 | 1.x | master | ^8.0 | +| ^5.0 | 2.x | cake5 | ^8.1 | +| ^5.0 | 3.x | cake5-next | ^8.3 | The recommended way to install the plugin is: @@ -62,13 +63,13 @@ These are the default view blocks in CakePHP. In your php-template or in layout you can import javascript files with: ```php -ViteScripts->script($options) ?> +ViteScripts->script('resources/main.ts') ?> ``` -… or by using this shortcut for a single entrypoint: +… or multiple files ```php -ViteScripts->script('webroot_src/main.ts') ?> +ViteScripts->script(['resources/main.ts', 'resources/main2.ts', 'resources/main3.ts']) ?> ``` If you imported CSS files inside your JavaScript files, this method automatically @@ -79,13 +80,13 @@ appends your css tags to the css view block. In your php-template you can import css files with: ```php -ViteScripts->css($options) ?> +ViteScripts->css('resources/style.css') ?> ``` -… or by using this shortcut for a single entrypoint: +… or multiple files ```php -ViteScripts->css('webroot_src/style.css') ?> +ViteScripts->css(['resources/style.css', 'resources/style2.css', 'resources/style3.css']) ?> ``` ## Configuration @@ -93,107 +94,124 @@ In your php-template you can import css files with: The plugin comes with some default configuration. You may need to change it depending on your setup. Or you might not need any config at all. -You can override some of these config settings through the `$options` of the helper methods. Or you can pass -your own instance of `ViteHelperConfig` to a helper method as a second parameter. +The default configuration is: ```php 'ViteHelper' => [ - 'build' => [ - 'outDirectory' => false, // output directory of build assets. string (e.g. 'dist') or false. - 'manifest' => WWW_ROOT . 'manifest.json', // absolute path to manifest - ], + 'environment' => \ViteHelper\Enum\Environment::PRODUCTION, // available options PRODUCTION, DEVELOPMENT, FROM_DETECTOR 'development' => [ - 'scriptEntries' => ['someFolder/myScriptEntry.ts'], // relative to project root - 'styleEntries' => ['someFolder/myStyleEntry.scss'], // relative to project root. Unnecessary when using css-in-js. - 'hostNeedles' => ['.test', '.local'], // to check if the app is running locally 'url' => 'http://localhost:3000', // url of the vite dev server ], - 'forceProductionMode' => false, // or true to always serve build assets - 'plugin' => false, // or string 'MyPlugin' to serve plugin build assets - 'productionHint' => 'vprod', // can be a true-ish cookie or url-param to serve build assets without changing the forceProductionMode config - 'viewBlocks' => [ - 'css' => 'css', // name of the css view block - 'script' => 'script', // name of the script view block + 'builds' => [ + [ + 'plugin' => null, // the plugin name or null if it doesn't exist. Default: null + 'outputDirectory' => 'build', // the output directory relative to `webroot`. Default: 'build' + 'manifest' => 'build' . DS . '.vite' . DS . 'manifest.json', // the relative path to the manifest file. Default: 'build' . DS . '.vite' . DS . 'manifest.json' + 'environment' => \ViteHelper\Enum\Environment::PRODUCTION, // the forced environment, all files what 'falls' in this manifest file will be rendered this way. Default: the globally set environment + ], ], ], ``` -You can override the defaults in your `app.php`, `app_local.php`, or `app_vite.php`. +You can override the defaults in your `app.php`, `app_local.php`, or `app_vite.php`, also you can override in +`AppView.php` when you are loading the helper. -See the plugin's [app_vite.php](https://github.com/brandcom/cakephp-vite/blob/master/config/app_vite.php) for reference. +```php +$this->loadHelper('ViteHelper.ViteScripts', [ + // ...your config goes here +]); +``` -Example: +Since every build option has a default value: ```php -return [ - 'ViteHelper' => [ - 'forceProductionMode' => 1, - 'development' => [ - 'hostNeedles' => ['.dev'], // if you don't use one of the defaults - 'url' => 'https://192.168.0.88:3000', - ], +'ViteHelper' => [ + 'environment' => \ViteHelper\Enum\Environment::DEVELOPMENT, // available options PRODUCTION, DEVELOPMENT, FROM_DETECTOR + 'development' => [ + 'url' => 'http://localhost:3000', // url of the vite dev server + ], + 'builds' => [ + [], + ['plugin' => 'MyAwesomePlugin', 'environment' => 'prod'] ], -]; +], ``` -## Helper method usage with options +These are valid builds. Assets from `MyAwesomePlugin` will be loaded like in production, the assets from root in development mode. -You can pass an `$options` array to override config or to completely skip the necessity to have a ViteHelper config. -The options are mostly the same for `::script()` and `::css()`. +## Environment -### Example +The plugin MUST accurately determine whether you are in development or production mode. You must explicitly set in the +config that you are in either `\ViteHelper\Enum\Environment::PRODUCTION` or `\ViteHelper\Enum\Environment::DEVELOPMENT`. +To enhance the flexibility of the plugin, you can utilize `\ViteHelper\Enum\Environment::FROM_DETECTOR`. This setting +will employ a [detector](https://book.cakephp.org/5/en/controllers/request-response.html#Cake\Http\ServerRequest::is) +to automatically detect the environment. ```php -$this->ViteScripts->script([ - - // this would append both the scripts and the css to a block named 'myCustomBlock' - // don't forget to use the block through $this->fetch('myCustomBlock') - 'block' => 'myCustomBlock', - 'cssBlock' => 'myCustomBlock', // for ::script() only – if you use css imports inside js. +$this->request->addDetector( + \ViteHelper\View\Helper\ViteScriptsHelper::VITESCRIPT_DETECTOR_NAME, + function ($serverRequest) { + // your logic goes here + // return true for prod, false for dev + } +); +``` - // files that are entry files during development and that should be served during production - 'files' => [ - 'webroot_src/main.ts', - ], +In the development mode the plugins adds the necessary tags for development, in production this isn't happening. - // "devEntries" is like "files". If you set "files", it will override both "devEntries" and "prodFilters" - 'devEntries' => ['webroot_src/main.ts'] +## Helper method usage with options - // "prodFilter" filters the entry files. Useful for code-splitting if you don't use dynamic imports - 'prodFilter' => 'webroot_src/main.ts' // as string if there's only one option - 'prodFilter' => 'main.ts' // also works - only looks for parts of the string - 'prodFilter' => ['main.ts'] // as array - same as above with multiple files - 'prodFilter' => function (ManifestRecord $record) { /* do something with the record and return true or false */ } -]); -``` +The options are the same for `::script()` and `::css()`. -**Note:** You need to set `devEntries` when running the dev server. They have to either be set in the config or -through the helper method. In contrast, you only need `files` or `prodFilter` if you are interested in php-side -code-splitting and don't use dynamic imports in js. +### Example -It depends on your project and use case how you define entries. If you don't use `prodFilter` or `files`, the plugin -will serve all your entry files which might just be the case you want. So don't overconfigure it ;) +```php +$this->ViteScripts->script( + files: ['resource/file1.js', 'resource/file2.js'], // Files as array/file as string + block: null, // name of the view block to render the scripts in. Default: null + plugin: null, // Plugin's name. Default: null + + // Filter for environment. Default: null (in case of null the file(s) will be rendered both on prod and dev) + // possible values: \ViteHelper\Enum\Environment::PRODUCTION, \ViteHelper\Enum\Environment::DEVELOPMENT, null + environment: null, +); +``` -## Opt out of global config + Plugin development +## Performance -You can use the helper methods with multiple configurations through the `$config` argument. +In production, it's possible that the manifest.json file is too large. If the default render mode is set to `AUTO`, +every `::script()` and `::css()` call automatically and instantly adds the HTML tag to the block. You can disable this +feature by setting render mode `MANUAL`. -> **New in version 2.3:** You can pass a config key instead of a config instance to the helper. The default config key -> is `ViteHelper`. +```php +$this->loadHelper('ViteHelper.ViteScripts', [ + 'render_mode' => \ViteHelper\Enum\RenderMode::MANUAL +]); +``` -This might be useful when using plugin scripts or when developing plugins: +In your php-layout, right before the `viewBlocks` you should manually call the `::render()` method or dispatch +the `Vite.render` event. ```php -ViteScripts->pluginScript('MyPlugin', devMode: true, config: 'MyPlugin.ViteConfig'); ?> +$this->ViteScripts->script('resource/myscript1.js'); +$this->ViteScripts->script('resource/myscript2.js'); +// ... + a lot of script +$this->ViteScripts->script('resource/myscriptN.js'); +$this->ViteScripts->render(); +fetch('css') ?> ``` +or -The example above uses a convenience method to load plugin scripts for `MyPlugin`. DevMode is enabled and the helper -will use a CakePHP config under the key `MyPlugin.ViteConfig`. In this way, you can scope your App's and your plugin's -config. - -It is assumed that the `manifest.json` is available directly in your plugin's `webroot`. If this is not the case, you -should define the absolute path throuh the `build.manifest` config option. +```php +$this->ViteScripts->script('resource/myscript1.js'); +$this->ViteScripts->script('resource/myscript2.js'); +// ... + a lot of script +$this->ViteScripts->script('resource/myscriptN.js'); +// dispatch 'Vite.render' event +$this->getEventManager()->dispatch('Vite.render'); +fetch('css') ?> +``` ## Vite JS bundler / Dev server @@ -215,9 +233,9 @@ yarn add -D @vitejs/plugin-legacy ### Configuration After installing, you will need to refactor the files a bit to make sense of it in a php project. The default config of -this plugin assumes that you put your js, ts, scss etc. in `/webroot_src`. +this plugin assumes that you put your js, ts, scss etc. in `/resources`. -The build files will end up in `/webroot/assets` by default. Your `vite.config.js or *.ts` file for vite stays in the +The build files will end up in `/webroot/build` by default. Your `vite.config.js or *.ts` file for vite stays in the project root. > Wanted: Examples for vite/plugin configs and directory structures. Feel free to contribute with a PR to show how your diff --git a/composer.json b/composer.json index 7b61171..2129473 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "cakephp-plugin", "license": "MIT", "require": { - "php": ">=8.1", + "php": ">=8.3", "cakephp/cakephp": "^5.0" }, "require-dev": { @@ -33,8 +33,8 @@ }, "scripts": { "test": "phpunit", - "cs-check": "phpcs --colors --parallel=16 -p src/", - "cs-fix": "phpcbf --colors --parallel=16 -p src/", + "cs-check": "phpcs --colors -p src/", + "cs-fix": "phpcbf --colors -p src/", "stan": "phpstan analyse", "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^1.7.0 && mv composer.backup composer.json", "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json" diff --git a/config/app_vite.php b/config/app_vite.php index 5312d69..1aa05d2 100644 --- a/config/app_vite.php +++ b/config/app_vite.php @@ -1,25 +1,23 @@ [ - 'build' => [ - 'outDirectory' => ConfigDefaults::BUILD_OUT_DIRECTORY, - 'manifest' => ConfigDefaults::BUILD_MANIFEST, - ], + // the running environment + 'environment' => \ViteHelper\Enum\Environment::PRODUCTION, + // development settings 'development' => [ - 'scriptEntries' => ConfigDefaults::DEVELOPMENT_SCRIPT_ENTRIES, - 'styleEntries' => ConfigDefaults::DEVELOPMENT_STYLE_ENTRIES, - 'hostNeedles' => ConfigDefaults::DEVELOPMENT_HOST_NEEDLES, - 'url' => ConfigDefaults::DEVELOPMENT_URL, + 'url' => 'http://localhost:3000', ], - 'forceProductionMode' => ConfigDefaults::FORCE_PRODUCTION_MODE, - 'plugin' => false, - 'productionHint' => ConfigDefaults::PRODUCTION_HINT, - 'viewBlocks' => [ - 'css' => ConfigDefaults::VIEW_BLOCK_CSS, - 'script' => ConfigDefaults::VIEW_BLOCK_SCRIPT, + // list of builds + 'builds' => [ + [ + // the plugin name or null if it doesn't exist + 'plugin' => null, + // the output directory relative to `webroot` + 'outputDirectory' => 'build', + // the relative path to the manifest file + 'manifest' => 'build' . DS . '.vite' . DS . 'manifest.json', + ], ], ], ]; diff --git a/etc/vite.config.ts.example b/etc/vite.config.ts.example index 710f644..9a13047 100644 --- a/etc/vite.config.ts.example +++ b/etc/vite.config.ts.example @@ -5,6 +5,8 @@ import basicSsl from '@vitejs/plugin-basic-ssl' // https://vitejs.dev/config/ export default defineConfig({ + root: '', + publicDir: 'resources/assets', plugins: [ basicSsl(), vue(), @@ -26,23 +28,28 @@ export default defineConfig({ protocol: 'wss', }, watch: { - ignored: [/bin/, /config/, /plugins/, /resources/, /tests/, /vendor/, /logs/, /tmp/], - depth: 5, + ignored: [/bin/, /config/, /plugins/, /tests/, /vendor/, /logs/, /tmp/], + depth: 15, } }, build: { emptyOutDir: true, outDir: './webroot/build', - assetsDir: 'assets', + assetsDir: './assets', manifest: true, rollupOptions: { input: [ - './webroot_src/js/main.js', - './webroot_src/js/timetables.js', - './webroot_src/scss/style.scss', + './resources/js/main.js', + './resources/js/timetables.js', + './resources/scss/style.scss', ], output: { entryFileNames: '[name].[hash].min.js', + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0].toString(); + } + } } } }, diff --git a/src/Enum/Environment.php b/src/Enum/Environment.php new file mode 100644 index 0000000..04b8e20 --- /dev/null +++ b/src/Enum/Environment.php @@ -0,0 +1,14 @@ +plugin; + } + + /** + * Returns true if the build is part of a plugin + * + * @return bool + */ + public function hasPlugin(): bool + { + return !is_null($this->plugin); + } + + /** + * Returns the output directory name + * + * @return string + */ + public function getOutputDirectory(): string + { + return $this->outputDirectory; + } + + /** + * Returns the full path of the manifest file + * + * @return string + */ + public function getManifest(): string + { + if (!is_null($this->plugin)) { + return Plugin::path($this->plugin) . 'webroot' . DS . $this->manifest; + } + + return WWW_ROOT . $this->manifest; + } + + /** + * Returns the records + * + * @return \ViteHelper\Utilities\ManifestRecords + */ + public function getManifestRecords(): ManifestRecords + { + if (!is_null($this->manifestRecords)) { + return $this->manifestRecords; + } + + try { + $this->manifestRecords = ViteManifest::getRecords($this->getManifest(), $this->getOutputDirectory()); + } catch (Exception $e) { + $this->manifestRecords = new ManifestRecords([], $this->getOutputDirectory()); + trigger_error($e->getMessage(), E_USER_WARNING); + } + + return $this->manifestRecords; + } + + /** + * Returns the forced environment + * + * @return \ViteHelper\Enum\Environment + */ + public function getEnvironment(): Environment + { + return $this->environment; + } +} diff --git a/src/Model/Entity/Record.php b/src/Model/Entity/Record.php new file mode 100644 index 0000000..7d00a00 --- /dev/null +++ b/src/Model/Entity/Record.php @@ -0,0 +1,44 @@ + $environment in which environment should we render this file + */ + public function __construct( + readonly protected RecordType $type, + readonly public string $file, + readonly public array $environment = [Environment::PRODUCTION, Environment::DEVELOPMENT], + ) {} + + /** + * returns the record type + * + * @return \ViteHelper\Enum\RecordType + */ + public function getType(): RecordType + { + return $this->type; + } + + /** + * Returns true if the given environment is in the allowed environments list + * + * @param \ViteHelper\Enum\Environment $environment + * @return bool + */ + public function isValidEnvironment(Environment $environment): bool + { + return in_array($environment, $this->environment, true); + } +} diff --git a/src/Model/Entity/ScriptRecord.php b/src/Model/Entity/ScriptRecord.php new file mode 100644 index 0000000..1ec5765 --- /dev/null +++ b/src/Model/Entity/ScriptRecord.php @@ -0,0 +1,32 @@ + $environment in which environment should we render this file + * @param string|null $block the block name + * @param string|null $plugin Plugin's name + * @param array $elementOptions options for HTML element tag + * @param bool $is_rendered shows if the item was already rendered or not + */ + public function __construct( + string $file, + array $environment = [Environment::PRODUCTION, Environment::DEVELOPMENT], + readonly public ?string $block = null, + readonly public ?string $plugin = null, + public array $elementOptions = [], + public bool $is_rendered = false, + ) { + parent::__construct(RecordType::STYLE, $file, $environment); + } +} diff --git a/src/Model/Entity/StyleRecord.php b/src/Model/Entity/StyleRecord.php new file mode 100644 index 0000000..50590ba --- /dev/null +++ b/src/Model/Entity/StyleRecord.php @@ -0,0 +1,32 @@ + $environment in which environment should we render this file + * @param string|null $block the block name + * @param string|null $plugin Plugin's name + * @param array $elementOptions options for HTML element tag + * @param bool $is_rendered shows if the item was already rendered or not + */ + public function __construct( + string $file, + array $environment = [Environment::PRODUCTION, Environment::DEVELOPMENT], + readonly public ?string $block = null, + readonly public ?string $plugin = null, + public array $elementOptions = [], + public bool $is_rendered = false, + ) { + parent::__construct(RecordType::STYLE, $file, $environment); + } +} diff --git a/src/Utilities/ConfigDefaults.php b/src/Utilities/ConfigDefaults.php deleted file mode 100644 index 28b7328..0000000 --- a/src/Utilities/ConfigDefaults.php +++ /dev/null @@ -1,75 +0,0 @@ -key = $key; - $this->chunk = $chunk; - $this->config = $config; + public function __construct( + private readonly string $key, + private readonly stdClass $chunk, + private readonly string|bool $outDirectory + ) { } /** @@ -74,7 +73,7 @@ public function match(string $needle, string $property = 'src'): bool { $field = $this->getChunk($property); - return is_string($field) && str_contains($field, $needle); + return is_string($field) && str_contains($field, trim($needle, '/')); } /** @@ -207,6 +206,44 @@ public function isModuleEntryScript(): bool return $this->isEntry() && $this->isJavascript() && !$this->isLegacy() && !$this->isPolyfill(); } + /** + * Adds a key value to metadata (if exists overwrites) + * + * @param string $key + * @param string $value + * @return void + */ + public function addMetadata(string $key, string $value): void + { + $this->metadata[$key] = $value; + } + + /** + * Replaces the metadata array + * + * @param array $metadata + * @return void + */ + public function setMetadata(array $metadata): void + { + $this->metadata = $metadata; + } + + /** + * Returns a metadata value + * + * @param string|null $key + * @return mixed + */ + public function getMetadata(?string $key = null): mixed + { + if (is_null($key)) { + return $this->metadata; + } + + return $this->metadata[$key] ?? null; + } + /** * Enables users to set build.outDirectory in app_vite.php to false, * so that the outDir equals the webroot. @@ -216,14 +253,15 @@ public function isModuleEntryScript(): bool */ private function getLinkFromOutDirectory(string $assetLink): string { - $outDirectory = $this->config->read('build.outDirectory'); + $outDirectory = $this->outDirectory; if (empty($outDirectory) && $outDirectory !== false) { - $outDirectory = ConfigDefaults::BUILD_OUT_DIRECTORY; + // TODO Needs to be verified + $outDirectory = false; } $outDirectory = ltrim((string)$outDirectory, DS); - $outDirectory = $outDirectory ? DS . $outDirectory : ''; + $outDirectory = $outDirectory ? '/' . $outDirectory : ''; - return $outDirectory . DS . $assetLink; + return $outDirectory . '/' . $assetLink; } } diff --git a/src/Utilities/ViteHelperConfig.php b/src/Utilities/ViteHelperConfig.php deleted file mode 100644 index 81ae6d8..0000000 --- a/src/Utilities/ViteHelperConfig.php +++ /dev/null @@ -1,59 +0,0 @@ -config = is_array($config) ? $config : (array)Configure::read($config); - } - - /** - * @param array|string|null $config config array or key - * @return self - */ - public static function create(array|string|null $config = null): self - { - return new self($config); - } - - /** - * @param string $path path to config - * @param mixed $default default value - * @return mixed - */ - public function read(string $path, mixed $default = null): mixed - { - return Hash::get($this->config, $path, $default); - } - - /** - * Merge two configs - * - * @param \ViteHelper\Utilities\ViteHelperConfig $config - * @return self - */ - public function merge(ViteHelperConfig $config): self - { - return self::create(array_merge( - $this->config, - $config->config, - )); - } -} diff --git a/src/Utilities/ViteManifest.php b/src/Utilities/ViteManifest.php index c9f08a9..7d9f6bc 100644 --- a/src/Utilities/ViteManifest.php +++ b/src/Utilities/ViteManifest.php @@ -3,7 +3,6 @@ namespace ViteHelper\Utilities; -use Cake\Core\Plugin; use ViteHelper\Exception\ManifestNotFoundException; /** @@ -14,22 +13,18 @@ class ViteManifest /** * Returns the manifest records as a Collection * - * @param \ViteHelper\Utilities\ViteHelperConfig $config plugin config instance + * @param string $manifestPath + * @param string $outDirectory * @return \ViteHelper\Utilities\ManifestRecords<\ViteHelper\Utilities\ManifestRecord> + * @throws \JsonException * @throws \ViteHelper\Exception\ManifestNotFoundException * @internal */ - public static function getRecords(ViteHelperConfig $config): ManifestRecords + public static function getRecords(string $manifestPath, string $outDirectory): ManifestRecords { - if ($config->read('plugin') && $config->read('build.manifest') === null) { - $manifestPath = static::getPluginManifestPath($config->read('plugin')); - } else { - $manifestPath = $config->read('build.manifest', ConfigDefaults::BUILD_MANIFEST); - } - if (!is_readable($manifestPath)) { throw new ManifestNotFoundException( - "No valid manifest.json found at path {$manifestPath}. Did you build your js?", + "No valid manifest.json found at path $manifestPath. Did you build your js?", ); } @@ -41,9 +36,7 @@ public static function getRecords(ViteHelperConfig $config): ManifestRecords } $json = str_replace( - [ - "\u0000", - ], + "\u0000", '', $json ); @@ -52,13 +45,13 @@ public static function getRecords(ViteHelperConfig $config): ManifestRecords $manifestArray = []; foreach (get_object_vars($manifest) as $property => $value) { - $manifestArray[$property] = new ManifestRecord($property, $value, $config); + $manifestArray[$property] = new ManifestRecord($property, $value, $outDirectory); } /** * Legacy Polyfills must come first. */ - usort($manifestArray, function ($file) { + usort($manifestArray, static function ($file) { /** @var \ViteHelper\Utilities\ManifestRecord $file */ return $file->isPolyfill() ? 0 : 1; }); @@ -66,22 +59,11 @@ public static function getRecords(ViteHelperConfig $config): ManifestRecords /** * ES-module scripts must come last. */ - usort($manifestArray, function ($file) { + usort($manifestArray, static function ($file) { /** @var \ViteHelper\Utilities\ManifestRecord $file */ return !$file->isPolyfill() && !$file->isLegacy() ? 1 : 0; }); return new ManifestRecords($manifestArray, $manifestPath); } - - /** - * Get the default location of a plugin's vite manifest.json - * - * @param string $pluginName e.g. "MyPlugin" - * @return string filesystem path to the Plugin's manifest.json - */ - protected static function getPluginManifestPath(string $pluginName): string - { - return Plugin::path($pluginName) . 'webroot' . DS . 'manifest.json'; - } } diff --git a/src/View/Helper/ViteScriptsHelper.php b/src/View/Helper/ViteScriptsHelper.php index aa91420..54f33b0 100644 --- a/src/View/Helper/ViteScriptsHelper.php +++ b/src/View/Helper/ViteScriptsHelper.php @@ -3,16 +3,18 @@ namespace ViteHelper\View\Helper; +use Cake\Collection\Collection; use Cake\Collection\CollectionInterface; +use Cake\Core\Configure; use Cake\Utility\Text; use Cake\View\Helper; +use ViteHelper\Enum\Environment; +use ViteHelper\Enum\RenderMode; use ViteHelper\Exception\ConfigurationException; -use ViteHelper\Exception\InvalidArgumentException; -use ViteHelper\Utilities\ConfigDefaults; +use ViteHelper\Model\Entity\BuildConfig; +use ViteHelper\Model\Entity\ScriptRecord; +use ViteHelper\Model\Entity\StyleRecord; use ViteHelper\Utilities\ManifestRecord; -use ViteHelper\Utilities\ManifestRecords; -use ViteHelper\Utilities\ViteHelperConfig; -use ViteHelper\Utilities\ViteManifest; /** * ViteScripts helper @@ -21,362 +23,395 @@ */ class ViteScriptsHelper extends Helper { - public array $helpers = ['Html']; - - /** - * Check if the app is currently in development state. - * - * Production mode can be forced in config through `forceProductionMode`, - * or by setting a cookie or a url-parameter. - * - * Otherwise, it will look for a hint that the app - * is in development mode through the `developmentHostNeedles` - * - * @param \ViteHelper\Utilities\ViteHelperConfig|string|null $config config key or instance to use - * @return bool - */ - public function isDev(ViteHelperConfig|string|null $config = null): bool - { - $config = $this->createConfig($config); - if ($config->read('forceProductionMode', ConfigDefaults::FORCE_PRODUCTION_MODE)) { - return false; - } - - $productionHint = $config->read('productionHint', ConfigDefaults::PRODUCTION_HINT); - $hasCookieOrQuery = $this->getView()->getRequest()->getCookie($productionHint) || $this->getView()->getRequest()->getQuery($productionHint); - if ($hasCookieOrQuery) { - return false; - } - - $needles = $config->read('development.hostNeedles', ConfigDefaults::DEVELOPMENT_HOST_NEEDLES); - foreach ($needles as $needle) { - if (str_contains((string)$this->getView()->getRequest()->host(), $needle)) { - return true; - } - } - - return false; - } - - /** - * Adds scripts to the script view block - * - * Options: - * * block (string): name of the view block to render the scripts in - * * files (string[]): files to serve in development and production - overrides prodFilter and devEntries - * * prodFilter (string, array, callable): to filter manifest entries in production mode - * * devEntries (string[]): entry files in development mode - * * other options are rendered as attributes to the html tag - * - * @param array|string $options file entrypoint or script options - * @param \ViteHelper\Utilities\ViteHelperConfig|string|null $config config key or instance to use - * @return void - * @throws \ViteHelper\Exception\ConfigurationException - * @throws \ViteHelper\Exception\ManifestNotFoundException|\ViteHelper\Exception\InvalidArgumentException - */ - public function script(array|string $options = [], ViteHelperConfig|string|null $config = null): void - { - $config = $this->createConfig($config); - if (is_string($options)) { - $options = ['files' => [$options]]; - } - $options['block'] = $options['block'] ?? $config->read('viewBlocks.script', ConfigDefaults::VIEW_BLOCK_SCRIPT); - $options['cssBlock'] = $options['cssBlock'] ?? $config->read('viewBlocks.css', ConfigDefaults::VIEW_BLOCK_CSS); - $options = $this->updateOptionsForFiltersAndEntries($options); - - if ($this->isDev($config)) { - $this->devScript($options, $config); - - return; - } - - $this->productionScript($options, $config); - } - - /** - * Convenience method to render a plugin's scripts - * - * @param string $pluginName e.g. MyPlugin - * @param bool $devMode set to true during development - * @param array $options helper options - * @param \ViteHelper\Utilities\ViteHelperConfig|string|null $config config key or instance to use - * @return void - * @throws \ViteHelper\Exception\ConfigurationException - * @throws \ViteHelper\Exception\InvalidArgumentException - * @throws \ViteHelper\Exception\ManifestNotFoundException - */ - public function pluginScript(string $pluginName, bool $devMode = false, array $options = [], ViteHelperConfig|string|null $config = null): void - { - $config = $this->createConfig($config); - $config = $config->merge(ViteHelperConfig::create([ - 'plugin' => $pluginName, - 'forceProductionMode' => !$devMode, - ])); - - $this->script($options, $config); - } - - /** - * @param array $options passed to script tag - * @param \ViteHelper\Utilities\ViteHelperConfig $config config instance - * @return void - * @throws \ViteHelper\Exception\ConfigurationException - */ - private function devScript(array $options, ViteHelperConfig $config): void - { - $this->Html->script( - $config->read('development.url', ConfigDefaults::DEVELOPMENT_URL) - . '/@vite/client', - [ - 'type' => 'module', - 'block' => $options['cssBlock'], - ] - ); - - $files = $this->getFilesForDevelopment($options, $config, 'scriptEntries'); - - unset($options['cssBlock']); - unset($options['prodFilter']); - unset($options['devEntries']); - $options['type'] = 'module'; - - foreach ($files as $file) { - $this->Html->script(Text::insert(':host/:file', [ - 'host' => $config->read('development.url', ConfigDefaults::DEVELOPMENT_URL), - 'file' => ltrim($file, DS), - ]), $options); - } - } - - /** - * @param array $options will be passed to script tag - * @param \ViteHelper\Utilities\ViteHelperConfig $config config instance - * @return void - * @throws \ViteHelper\Exception\ManifestNotFoundException - * @throws \ViteHelper\Exception\InvalidArgumentException - */ - private function productionScript(array $options, ViteHelperConfig $config): void - { - $pluginPrefix = $config->read('plugin'); - $pluginPrefix = $pluginPrefix ? $pluginPrefix . '.' : null; - - $records = $this->getFilteredRecords(ViteManifest::getRecords($config), $options); - $cssBlock = $options['cssBlock']; - unset($options['prodFilter']); - unset($options['cssBlock']); - unset($options['devEntries']); - - foreach ($records as $record) { - if (!$record->isEntryScript()) { - continue; - } - - unset($options['type']); - unset($options['nomodule']); - if ($record->isModuleEntryScript()) { - $options['type'] = 'module'; - } else { - $options['nomodule'] = 'nomodule'; - } - - $this->Html->script($pluginPrefix . $record->getFileUrl(), $options); - - // the js files has css dependency ? - $cssFiles = $record->getCss(); - if (!count($cssFiles)) { - continue; - } - - foreach ($cssFiles as $cssFile) { - $this->Html->css($pluginPrefix . $cssFile, [ - 'block' => $cssBlock, - ]); - } - } - } - - /** - * Adds CSS tags to the configured block - * - * Note: This method might be unnecessary if you import your css in javascript. - * - * Options: - * * block (string): name of the view block to render the html tags in - * * files (string[]): files to serve in development and production - overrides prodFilter and devEntries - * * prodFilter (string, array, callable): to filter manifest entries in production mode - * * devEntries (string[]): entry files in development mode - * * other options are rendered as attributes to the html tag - * - * @param array|string $options file entrypoint or css options - * @param \ViteHelper\Utilities\ViteHelperConfig|string|null $config config key or instance to use - * @return void - * @throws \ViteHelper\Exception\ManifestNotFoundException - * @throws \ViteHelper\Exception\ConfigurationException - * @throws \ViteHelper\Exception\InvalidArgumentException - */ - public function css(array|string $options = [], ViteHelperConfig|string|null $config = null): void - { - $config = $this->createConfig($config); - if (is_string($options)) { - $options = ['files' => [$options]]; - } - // TODO the default should be css. This is a bug but might break in production. - // So this should be replaced in a major release. - $options['block'] = $options['block'] ?? $config->read('viewBlocks.css', ConfigDefaults::VIEW_BLOCK_SCRIPT); - $options = $this->updateOptionsForFiltersAndEntries($options); - - if ($this->isDev($config)) { - $files = $this->getFilesForDevelopment($options, $config, 'styleEntries'); - unset($options['devEntries']); - foreach ($files as $file) { - $this->Html->css(Text::insert(':host/:file', [ - 'host' => $config->read('development.url', ConfigDefaults::DEVELOPMENT_URL), - 'file' => ltrim($file, '/'), - ]), $options); - } - - return; - } - - $pluginPrefix = $config->read('plugin'); - $pluginPrefix = $pluginPrefix ? $pluginPrefix . '.' : null; - $records = $this->getFilteredRecords(ViteManifest::getRecords($config), $options); - unset($options['prodFilter']); - unset($options['devEntries']); - foreach ($records as $record) { - if (!$record->isEntry() || !$record->isStylesheet() || $record->isLegacy()) { - continue; - } - - $this->Html->css($pluginPrefix . $record->getFileUrl(), $options); - } - } - - /** - * Convenience method to render a plugin's styles - * - * @param string $pluginName e.g. MyPlugin - * @param bool $devMode set to true during development - * @param array $options helper options - * @param \ViteHelper\Utilities\ViteHelperConfig|string|null $config config key or instance to use - * @return void - * @throws \ViteHelper\Exception\ConfigurationException - * @throws \ViteHelper\Exception\InvalidArgumentException - * @throws \ViteHelper\Exception\ManifestNotFoundException - */ - public function pluginCss(string $pluginName, bool $devMode = false, array $options = [], ViteHelperConfig|string|null $config = null): void - { - $config = $this->createConfig($config); - $config = $config->merge(ViteHelperConfig::create([ - 'plugin' => $pluginName, - 'forceProductionMode' => !$devMode, - ])); - - $this->css($options, $config); - } - - /** - * @param array $options entries can be passed through `devEntries` - * @param \ViteHelper\Utilities\ViteHelperConfig $config config instance - * @param string $configOption key of the config - * @return array - * @throws \ViteHelper\Exception\ConfigurationException - */ - private function getFilesForDevelopment(array $options, ViteHelperConfig $config, string $configOption): array - { - $files = $options['devEntries'] ?: $config->read('development.' . $configOption, ConfigDefaults::DEVELOPMENT_SCRIPT_ENTRIES); - - if (empty($files)) { - throw new ConfigurationException( - 'There are no valid entry points for the dev server. ' - . 'Be sure to set the ViteHelper.development.' . $configOption . ' config or pass entries to the helper.' - ); - } - - if (!array_is_list($files)) { - throw new ConfigurationException(sprintf( - 'Expected entryPoints to be a List (array with int-keys) with at least one entry, but got %s.', - gettype($files) === 'array' ? 'a relational array' : gettype($files), - )); - } - - return $files; - } - - /** - * Filter records from vite manifest for production - * - * @param \ViteHelper\Utilities\ManifestRecords $records records to filter - * @param array $options method looks at the `prodFilter`key - * @return \ViteHelper\Utilities\ManifestRecords|\Cake\Collection\CollectionInterface - * @throws \ViteHelper\Exception\InvalidArgumentException - */ - private function getFilteredRecords(ManifestRecords $records, array $options): ManifestRecords|CollectionInterface - { - $filter = $options['prodFilter']; - if (empty($filter)) { - return $records; - } - - if (is_callable($filter)) { - return $records->filter($filter); - } - - if (is_string($filter)) { - $filter = (array)$filter; - } - - if (!is_array($filter)) { - throw new InvalidArgumentException('$options["prodFilter"] must be empty or of type string, array, or callable.'); - } - - return $records->filter(function (ManifestRecord $record) use ($filter) { - foreach ($filter as $property => $file) { - $property = is_string($property) ? $property : 'src'; - if ($record->match($file, $property)) { - return true; - } - } - - return false; - }); - } - - /** - * @param array $options options with `prodFilter`, `devEntries`, or `files` key - * @return array - */ - private function updateOptionsForFiltersAndEntries(array $options): array - { - $options['prodFilter'] = $options['prodFilter'] ?? null; - $options['devEntries'] = $options['devEntries'] ?? null; - $files = $options['files'] ?? null; - if ($files) { - if (!empty($options['devEntries'])) { - trigger_error('"devEntries" passed to ViteHelper will be overridden by "files".'); - } - if (!empty($options['prodFilter'])) { - trigger_error('"prodFilter" passed to ViteHelper will be overridden by "files".'); - } - $options['devEntries'] = $files; - $options['prodFilter'] = $files; - } - - return $options; - } - - /** - * Helper method to create a new config or the defined config - * - * @param \ViteHelper\Utilities\ViteHelperConfig|string|null $config can be a config key, a config instance or null for the default - * @return \ViteHelper\Utilities\ViteHelperConfig - */ - private function createConfig(ViteHelperConfig|string|null $config): ViteHelperConfig - { - if ($config instanceof ViteHelperConfig) { - return $config; - } - - return ViteHelperConfig::create($config); - } + public const VITESCRIPT_DETECTOR_NAME = 'vite_in_production'; + + private const DEFAULT_BUILD_KEY = '_default_build'; + + /** + * @var array|string[] + */ + public array $helpers = ['Html']; + + /** + * The style and script files + * + * @var array + */ + protected array $entries; + + /** + * Build information + * These settings comes from the configuration + * + * @var array<\ViteHelper\Model\Entity\BuildConfig> $builds + */ + protected array $builds; + + /** + * @inheritdoc + */ + protected array $_defaultConfig = [ + 'environment' => Environment::PRODUCTION, + 'development' => [ + 'url' => 'http://localhost:3000', + ], + 'builds' => [], + 'render_mode' => RenderMode::AUTO, + 'viewBlocks' => [ + 'css' => 'css', + 'script' => 'script', + ], + ]; + + /** + * @inheritDoc + * @throws \ViteHelper\Exception\ConfigurationException + */ + public function initialize(array $config): void + { + parent::initialize($config); + $this->setConfig(Configure::read('ViteHelper')); + $this->setConfig($config); + $env = $this->getConfig('environment', 'prod'); + if (is_string($env)) { + $env = Environment::from($env); + } + + if (!($env instanceof Environment)) { + throw new ConfigurationException('Invalid environment config!'); + } + + if ($env === Environment::FROM_DETECTOR) { + $this->setConfig( + 'environment', + $this->getView()->getRequest()->is(self::VITESCRIPT_DETECTOR_NAME) ? + Environment::PRODUCTION : Environment::DEVELOPMENT + ); + $env = $this->getConfig('environment'); + } + + if ($env === Environment::DEVELOPMENT) { + $this->Html->script( + $this->getConfig('development.url') + . '/@vite/client', + [ + 'type' => 'module', + 'block' => $this->getConfig('viewBlocks.css'), + ] + ); + } + + $this->builds = []; + foreach ($this->getConfig('builds') as $build) { + if (isset($build['environment']) && is_string($build['environment'])) { + $environment = Environment::tryFrom($build['environment']) ?? $env; + } else { + $environment = $build['environment'] ?? $env; + } + if (isset($build['plugin'])) { + $this->builds[$build['plugin']] = new BuildConfig( + manifest: $build['manifest'] ?? 'build' . DS . '.vite' . DS . 'manifest.json', + outputDirectory: $build['outputDirectory'] ?? 'build', + plugin: $build['plugin'], + environment: $environment, + ); + } else { + $this->builds[self::DEFAULT_BUILD_KEY] = new BuildConfig( + manifest: $build['manifest'] ?? 'build' . DS . '.vite' . DS . 'manifest.json', + outputDirectory: $build['outputDirectory'] ?? 'build', + plugin: null, + environment: $environment, + ); + } + } + + $this->getView()->getEventManager()->on('Vite.render', [$this, 'render']); + } + + /** + * Adds scripts to the script view block + * + * @param array|string $files files to serve + * @param string|null $block name of the view block to render the scripts in + * @param string|null $plugin + * @param array $elementOptions options to the html tag + * @param \ViteHelper\Enum\Environment|string|null $environment the files will be served only in this environment, null on both + * @return void + */ + public function script( + array|string $files = [], + ?string $block = null, + ?string $plugin = null, + array $elementOptions = [], + Environment|string|null $environment = null, + ): void + { + $env = is_string($environment) ? Environment::tryFrom($environment) : $environment; + $elementOptions['block'] = $block ?? $this->getConfig('viewBlocks.script'); + $files = (array)$files; + foreach ($files as $file) { + $this->entries[] = match ($env) { + Environment::DEVELOPMENT => new ScriptRecord( + $file, + [Environment::DEVELOPMENT], + $block, + $plugin, + $elementOptions, + ), + Environment::PRODUCTION => new ScriptRecord( + $file, + [Environment::PRODUCTION], + $block, + $plugin, + $elementOptions, + ), + default => new ScriptRecord( + $file, + [Environment::PRODUCTION, Environment::DEVELOPMENT], + $block, + $plugin, + $elementOptions, + ), + }; + } + if ($this->getConfig('render_mode') === RenderMode::AUTO) { + $this->render(); + } + } + + /** + * Adds style to the css view block + * + * @param array|string $files files to serve + * @param string|null $block name of the view block to render the scripts in + * @param string|null $plugin + * @param array $elementOptions options to the html tag + * @param \ViteHelper\Enum\Environment|string|null $environment the files will be served only in this environment, null on both + * @return void + */ + public function css( + array|string $files = [], + ?string $block = null, + ?string $plugin = null, + array $elementOptions = [], + Environment|string|null $environment = null, + ): void + { + $env = is_string($environment) ? Environment::tryFrom($environment) : $environment; + $elementOptions['block'] = $block ?? $this->getConfig('viewBlocks.css'); + $files = (array)$files; + foreach ($files as $file) { + $this->entries[] = match ($env) { + Environment::DEVELOPMENT => new StyleRecord( + $file, + [Environment::DEVELOPMENT], + $block, + $plugin, + $elementOptions, + ), + Environment::PRODUCTION => new StyleRecord( + $file, + [Environment::PRODUCTION], + $block, + $plugin, + $elementOptions, + ), + default => new StyleRecord( + $file, + [Environment::PRODUCTION, Environment::DEVELOPMENT], + $block, + $plugin, + $elementOptions, + ), + }; + } + if ($this->getConfig('render_mode') === RenderMode::AUTO) { + $this->render(); + } + } + + /** + * Adds styles and scripts to the blocks + * + * @return void + */ + public function render(): void + { + $currentEnvironment = $this->getConfig('environment', Environment::PRODUCTION); + $records = array_filter($this->entries, static function ($record) { + /** @var \ViteHelper\Model\Entity\ScriptRecord|\ViteHelper\Model\Entity\StyleRecord $record */ + return !$record->is_rendered; + }); + + /** @var \ViteHelper\Model\Entity\ScriptRecord|\ViteHelper\Model\Entity\StyleRecord $record */ + foreach ($records as $record) { + // should we render ?! + if (!$record->isValidEnvironment($currentEnvironment)) { + continue; + } + + // how we should do it ? + $renderEnvironment = $this->getBuildEnvironment($record); + if ($renderEnvironment === Environment::DEVELOPMENT) { + $this->renderDevelopmentRecord($record); + } else { + $this->renderProductionRecord($record); + } + } + } + + /** + * Appends development style/script tag to configured block + * + * @param \ViteHelper\Model\Entity\StyleRecord|\ViteHelper\Model\Entity\ScriptRecord $record + * @return void + */ + private function renderDevelopmentRecord(StyleRecord|ScriptRecord $record): void + { + $record->is_rendered = true; + if ($record instanceof ScriptRecord) { + $record->elementOptions['type'] = 'module'; + $this->Html->script(Text::insert(':host/:file', [ + 'host' => $this->getConfig('development.url'), + 'file' => ltrim($record->file, DS), + ]), $record->elementOptions); + } else { + $this->Html->css(Text::insert(':host/:file', [ + 'host' => $this->getConfig('development.url'), + 'file' => ltrim($record->file, '/'), + ]), $record->elementOptions); + } + } + + /** + * Appends production style/script tag to configured block + * + * @param \ViteHelper\Model\Entity\StyleRecord|\ViteHelper\Model\Entity\ScriptRecord $record + * @return void + */ + private function renderProductionRecord(StyleRecord|ScriptRecord $record): void + { + $record->is_rendered = true; + if ($record instanceof ScriptRecord) { + $this->renderProductionScripts($record); + } else { + $this->renderProductionStyle($record); + } + } + + /** + * Appends production script tags to configured block + * + * @param \ViteHelper\Model\Entity\ScriptRecord $file + * @return void + */ + private function renderProductionScripts(ScriptRecord $file): void + { + $manifestRecords = $this->getManifestRecords($file); + + /** @var \ViteHelper\Utilities\ManifestRecord $manifestRecord */ + foreach ($manifestRecords as $manifestRecord) { + if (!$manifestRecord->isEntryScript()) { + continue; + } + + $options = $manifestRecord->getMetadata(); + if ($manifestRecord->isModuleEntryScript()) { + $options['options']['type'] = 'module'; + } else { + $options['options']['nomodule'] = 'nomodule'; + } + + $recordPluginPrefix = $file->plugin; + if (isset($options['plugin'])) { + $recordPluginPrefix = $options['plugin'] . '.'; + unset($options['plugin']); + } + $this->Html->script($recordPluginPrefix . $manifestRecord->getFileUrl(), $options['options']); + + // the js files has css dependency ? + $cssFiles = $manifestRecord->getCss(); + if (!count($cssFiles)) { + continue; + } + + foreach ($cssFiles as $cssFile) { + $this->Html->css($recordPluginPrefix . $cssFile, [ + 'block' => $this->getConfig('viewBlocks.css'), + ]); + } + unset($recordPluginPrefix); + } + + } + + /** + * Appends production style tags to configured block + * + * @param \ViteHelper\Model\Entity\StyleRecord $file + * @return void + */ + private function renderProductionStyle(StyleRecord $file): void + { + $records = $this->getManifestRecords($file); + foreach ($records as $record) { + if (!$record->isEntry() || !$record->isStylesheet() || $record->isLegacy()) { + continue; + } + + $options = $record->getMetadata(); + $recordPluginPrefix = $file->plugin; + if (isset($options['plugin'])) { + $recordPluginPrefix = $options['plugin'] . '.'; + unset($options['plugin']); + } + + $this->Html->css($recordPluginPrefix . $record->getFileUrl(), $options['options']); + unset($recordPluginPrefix); + } + } + + /** + * Returns manifest records with the correct metadata + * + * @param \ViteHelper\Model\Entity\StyleRecord|\ViteHelper\Model\Entity\ScriptRecord $file + * @return \Cake\Collection\CollectionInterface + */ + private function getManifestRecords(StyleRecord|ScriptRecord $file): CollectionInterface + { + if (!isset($this->builds[$file->plugin ?? self::DEFAULT_BUILD_KEY])) { + trigger_error('Invalid plugin name: ' . $file->plugin, E_USER_WARNING); + + return new Collection([]); + } + $build = $this->builds[$file->plugin ?? self::DEFAULT_BUILD_KEY]; + + return $build->getManifestRecords()->filter(function (ManifestRecord $record) use ($file) { + if ($record->match($file->file)) { + $record->setMetadata([ + 'options' => $file->elementOptions, + 'plugin' => $file->plugin, + ]); + + return $record; + } + + return false; + }); + } + + /** + * Returns a record forced environment + * + * @param \ViteHelper\Model\Entity\StyleRecord|\ViteHelper\Model\Entity\ScriptRecord $record + * @return \ViteHelper\Enum\Environment + */ + private function getBuildEnvironment(StyleRecord|ScriptRecord $record): Environment + { + if (!isset($this->builds[$record->plugin ?? self::DEFAULT_BUILD_KEY])) { + return $this->getConfig('environment', Environment::PRODUCTION); + } + + return $this->builds[$record->plugin ?? self::DEFAULT_BUILD_KEY]->getEnvironment(); + } }