ischenko/yii2-jsloader-systemjs

View on GitHub
src/SystemJs.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * @copyright Copyright (c) 2020 Roman Ishchenko
 * @license https://github.com/ischenko/yii2-jsloader-requirejs/blob/master/LICENSE
 * @link https://github.com/ischenko/yii2-jsloader-requirejs#readme
 */

namespace ischenko\yii2\jsloader;

use ischenko\yii2\jsloader\base\Loader;
use ischenko\yii2\jsloader\helpers\FileHelper;
use ischenko\yii2\jsloader\helpers\JsExpression;
use ischenko\yii2\jsloader\systemjs\Config;
use ischenko\yii2\jsloader\systemjs\InlineRenderer;
use yii\base\InvalidConfigException;
use yii\di\Instance;
use yii\helpers\Json;
use yii\web\View;

/**
 * SystemJs implementation
 *
 * @author Roman Ishchenko <roman@ishchenko.ck.ua>
 * @since 1.0
 */
class SystemJs extends Loader
{
    /**
     * A list of allowed extras
     */
    const AVAILABLE_EXTRAS = [
        'system' => ['amd', 'transform', 'named-exports', 'named-register'],
        's' => ['amd', 'transform', 'named-exports', 'named-register', 'global', 'module-types']
    ];

    /**
     * Supported extras:
     *  - amd
     *  - transform
     *  - named-exports
     *  - named-register
     *  - global (ignored if `minimal` set to false)
     *  - module-types (ignored if `minimal` set to false)
     *
     * @var array a list of extras to be loaded
     */
    public $extras = [];

    /**
     * @var bool use minimal loader s.js instead of system.js loader
     */
    public $minimal = false;

    /**
     * @var int|null
     */
    public $position;

    /**
     * @var string|array|JsRendererInterface
     */
    public $renderer = InlineRenderer::class;

    /**
     * @var Config
     */
    private $config;

    /**
     * {@inheritDoc}
     * @throws InvalidConfigException
     */
    public function init()
    {
        parent::init();

        $this->renderer = Instance::ensure($this->renderer, JsRendererInterface::class);
    }

    /**
     * {@inheritDoc}
     *
     * @return ConfigInterface
     */
    public function getConfig(): ConfigInterface
    {
        if ($this->config === null) {
            $this->config = new Config();
        }

        return $this->config;
    }

    /**
     * {@inheritDoc}
     *
     * @param JsExpression[] $expressions
     *
     * @return void
     * @throws InvalidConfigException
     */
    protected function renderJs(array $expressions): void
    {
        $this->registerImportMap();
        $this->registerLibraryFiles();
        $this->registerJsCode($expressions);
    }

    /**
     * Registers a JSON file with import map
     * @throws InvalidConfigException
     */
    protected function registerImportMap(): void
    {
        if (($importMap = $this->getConfig()->toArray()) === []) {
            return;
        }

        $options = 320 | JSON_FORCE_OBJECT;

        if (YII_DEBUG) {
            $options |= JSON_PRETTY_PRINT;
        }

        $importMap = Json::encode($importMap, $options);

        $filePath = $this->getRuntimePath() . DIRECTORY_SEPARATOR . md5($importMap) . '.json';
        $filePath = FileHelper::normalizePath($filePath);

        if (!file_exists($filePath)) {
            file_put_contents($filePath, $importMap, LOCK_EX);
        }

        $view = $this->getView();

        list(, $url) = $view->getAssetManager()->publish($filePath);

        $view->registerJsFile($url, [
            'type' => 'systemjs-importmap',
            'position' => $this->getPosition()
        ]);
    }

    /**
     * Registers JS code from expressions
     *
     * @param JsExpression[] $expressions
     */
    protected function registerJsCode(array $expressions): void
    {
        $jsCode = '';

        krsort($expressions);

        foreach ($expressions as $pos => $expression) {
            $this->appendJsCode($jsCode, $expression, $pos);
            $jsCode = $expression->render($this->renderer);
        }

        // register JS code at the load position
        $this->getView()->registerJs($jsCode, View::POS_END);
    }

    /**
     * Register SystemJs files according to the configuration
     * @throws InvalidConfigException
     */
    protected function registerLibraryFiles(): void
    {
        $view = $this->getView();

        list(, $url) = $view->getAssetManager()->publish('@bower/system.js/dist');

        // resolve script files
        $libFile = $this->minimal ? 's' : 'system';

        if ($this->minimal) {
            $this->extras[] = 'module-types';
        }

        $scripts = array_intersect($this->extras, self::AVAILABLE_EXTRAS[$libFile]);
        $scripts = array_map(function ($script) {
            return "extras/{$script}";
        }, array_unique($scripts));

        $jsExt = YII_DEBUG ? 'js' : 'min.js';
        $options = ['position' => $this->getPosition()];

        array_unshift($scripts, $libFile);

        foreach ($scripts as $script) {
            $view->registerJsFile("{$url}/{$script}.{$jsExt}", $options);
        }
    }

    /**
     * @param string $js
     * @param JsExpression $expression
     * @param int $pos
     */
    private function appendJsCode(string $js, JsExpression $expression, int $pos)
    {
        while (($code = $expression->getExpression()) instanceof JsExpression) {
            $expression = $code;
        }

        if ($pos === View::POS_READY && !empty($code)) {
            $code = "jQuery(function() {\n{$code}\n})";
        }

        if (!empty($js)) {
            $code .= ";\n{$js}";
        }

        $expression->setExpression($code);
    }

    /**
     * Resolves position for script tags
     *
     * @return int
     */
    private function getPosition(): int
    {
        // resolve position
        $position = $this->position ?? View::POS_HEAD;

        if ($position < View::POS_HEAD) {
            $position = View::POS_HEAD;
        } elseif ($position > View::POS_END) {
            $position = View::POS_END;
        }

        return $position;
    }
}