Passed
Branch develop (7cbc6e)
by Andrew
07:57 queued 03:21
created

ViteService   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 339
Duplicated Lines 0 %

Importance

Changes 16
Bugs 0 Features 1
Metric Value
eloc 112
c 16
b 0
f 1
dl 0
loc 339
rs 9.44
wmc 37

11 Methods

Rating   Name   Duplication   Size   Complexity  
A fetch() 0 3 1
B injectErrorEntry() 0 28 9
A devServerRegister() 0 9 1
B manifestScript() 0 30 7
A init() 0 11 3
A devServerScript() 0 10 1
A register() 0 9 2
A script() 0 7 2
A devServerRunning() 0 14 3
A invalidateCaches() 0 5 1
B manifestRegister() 0 32 7
1
<?php
2
/**
3
 * Vite plugin for Craft CMS 3.x
4
 *
5
 * Allows the use of the Vite.js next generation frontend tooling with Craft CMS
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2021 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\pluginvite\services;
12
13
use nystudio107\pluginvite\helpers\FileHelper;
14
use nystudio107\pluginvite\helpers\ManifestHelper;
15
use nystudio107\pluginvite\helpers\UrlHelper;
16
17
use Craft;
18
use craft\base\Component;
19
use craft\helpers\Html as HtmlHelper;
20
use craft\helpers\Json as JsonHelper;
21
use craft\web\View;
22
23
use yii\base\InvalidConfigException;
24
use yii\caching\TagDependency;
25
26
use Throwable;
27
28
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
29
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
30
 * @package   Vite
0 ignored issues
show
Coding Style introduced by
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
31
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
32
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
33
class ViteService extends Component
34
{
35
    // Constants
36
    // =========================================================================
37
38
    const VITE_CLIENT = '@vite/client.js';
39
    const LEGACY_POLYFILLS = 'vite/legacy-polyfills';
40
41
    const SAFARI_NOMODULE_FIX = '!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();';
42
43
    // Public Properties
44
    // =========================================================================
45
46
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
47
     * @var bool Should the dev server be used for?
48
     */
49
    public $useDevServer;
50
51
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
52
     * @var string File system path (or URL) to the Vite-built manifest.json
53
     */
54
    public $manifestPath;
55
56
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
57
     * @var string The public URL to the dev server (what appears in `<script src="">` tags
58
     */
59
    public $devServerPublic;
60
61
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
62
     * @var string The public URL to use when not using the dev server
63
     */
64
    public $serverPublic;
65
66
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
67
     * @var string The JavaScript entry from the manifest.json to inject on Twig error pages
68
     *              This can be a string or an array of strings
69
     */
70
    public $errorEntry = '';
71
72
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
73
     * @var string String to be appended to the cache key
74
     */
75
    public $cacheKeySuffix = '';
76
77
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
78
     * @var string The internal URL to the dev server, when accessed from the environment in which PHP is executing
79
     *              This can be the same as `$devServerPublic`, but may be different in containerized or VM setups.
80
     *              ONLY used if $checkDevServer = true
81
     */
82
    public $devServerInternal;
83
84
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
85
     * @var bool Should we check for the presence of the dev server by pinging $devServerInternal to make sure it's running?
86
     */
87
    public $checkDevServer = false;
88
89
    // Protected Properties
90
    // =========================================================================
91
92
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
93
     * @var bool Whether the legacy polyfill has been included yet or not
94
     */
95
    protected $legacyPolyfillIncluded = false;
96
97
    // Public Methods
98
    // =========================================================================
99
100
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
101
     * @inheritDoc
102
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
103
    public function init()
104
    {
105
        parent::init();
106
        // Do nothing for console requests
107
        $request = Craft::$app->getRequest();
108
        if ($request->getIsConsoleRequest()) {
109
            return;
110
        }
111
        // Our component is lazily loaded, so the View will be instantiated by now
112
        if (Craft::$app->getConfig()->getGeneral()->devMode) {
113
            Craft::$app->getView()->on(View::EVENT_END_BODY, [$this, 'injectErrorEntry']);
114
        }
115
    }
116
117
    /**
118
     * Return the appropriate tags to load the Vite script, either via the dev server or
119
     * extracting it from the manifest.json file
120
     *
121
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
122
     * @param bool $asyncCss
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
123
     * @param array $scriptTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
124
     * @param array $cssTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
125
     *
126
     * @return string
127
     */
128
    public function script(string $path, bool $asyncCss = true, array $scriptTagAttrs = [], array $cssTagAttrs = []): string
129
    {
130
        if ($this->devServerRunning()) {
131
            return $this->devServerScript($path, $scriptTagAttrs);
132
        }
133
134
        return $this->manifestScript($path, $asyncCss, $scriptTagAttrs, $cssTagAttrs);
135
    }
136
137
    /**
138
     * Return the script tag to load the script from the Vite dev server
139
     *
140
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
141
     * @param array $scriptTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
142
     *
143
     * @return string
144
     */
145
    public function devServerScript(string $path, array $scriptTagAttrs = []): string
146
    {
147
        $lines = [];
148
        // Include the entry script
149
        $url = UrlHelper::createUrl($this->devServerPublic, $path);
150
        $lines[] = HtmlHelper::jsFile($url, array_merge([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
151
            'type' => 'module',
152
        ], $scriptTagAttrs));
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
153
154
        return implode("\r\n", $lines);
155
    }
156
157
    /**
158
     * Return the script, module link, and CSS link tags for the script from the manifest.json file
159
     *
160
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
161
     * @param bool $asyncCss
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
162
     * @param array $scriptTagAttrs
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
163
     * @param array $cssTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
164
     *
165
     * @return string
166
     */
167
    public function manifestScript(string $path, bool $asyncCss = true, array $scriptTagAttrs = [], array $cssTagAttrs = []): string
168
    {
169
        $lines = [];
170
        ManifestHelper::fetchManifest($this->manifestPath);
171
        $tags = ManifestHelper::manifestTags($path, $asyncCss, $scriptTagAttrs, $cssTagAttrs);
172
        $legacyTags = ManifestHelper::legacyManifestTags($path, $asyncCss, $scriptTagAttrs, $cssTagAttrs);
173
        // Handle any legacy polyfills
174
        if (!empty($legacyTags) && !$this->legacyPolyfillIncluded) {
175
            $lines[] = HtmlHelper::script(self::SAFARI_NOMODULE_FIX, []);
176
            $legacyPolyfillTags = ManifestHelper::extractManifestTags(self::LEGACY_POLYFILLS, $asyncCss, $scriptTagAttrs, $cssTagAttrs, true);
177
            $tags = array_merge($legacyPolyfillTags, $tags);
178
            $this->legacyPolyfillIncluded = true;
179
        }
180
        foreach(array_merge($tags, $legacyTags) as $tag) {
0 ignored issues
show
Coding Style introduced by
Expected "foreach (...) {\n"; found "foreach(...) {\n"
Loading history...
181
            if (!empty($tag)) {
182
                $url = UrlHelper::createUrl($this->serverPublic, $tag['url']);
183
                switch ($tag['type']) {
184
                    case 'file':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
185
                        $lines[] = HtmlHelper::jsFile($url, $tag['options']);
186
                        break;
187
                    case 'css':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
188
                        $lines[] = HtmlHelper::cssFile($url, $tag['options']);
189
                        break;
190
                    default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
191
                        break;
192
                }
193
            }
194
        }
195
196
        return implode("\r\n", $lines);
197
    }
198
199
    /**
200
     * Register the appropriate tags to the Craft View to load the Vite script, either via the dev server or
201
     * extracting it from the manifest.json file
202
     *
203
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
204
     * @param bool $asyncCss
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
205
     * @param array $scriptTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
206
     * @param array $cssTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
207
     *
208
     * @return void
209
     * @throws InvalidConfigException
210
     */
211
    public function register(string $path, bool $asyncCss = true, array $scriptTagAttrs = [], array $cssTagAttrs = [])
212
    {
213
        if ($this->devServerRunning()) {
214
            $this->devServerRegister($path, $scriptTagAttrs);
215
216
            return;
217
        }
218
219
        $this->manifestRegister($path, $asyncCss, $scriptTagAttrs, $cssTagAttrs);
220
    }
221
222
    /**
223
     * Register the script tag to the Craft View to load the script from the Vite dev server
224
     *
225
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
226
     * @param array $scriptTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
227
     *
228
     * @return void
229
     * @throws InvalidConfigException
230
     */
231
    public function devServerRegister(string $path, array $scriptTagAttrs = [])
232
    {
233
        $view = Craft::$app->getView();
234
        // Include the entry script
235
        $url = UrlHelper::createUrl($this->devServerPublic, $path);
236
        $view->registerJsFile(
237
            $url,
238
            array_merge(['type' => 'module'], $scriptTagAttrs),
239
            md5($url . JsonHelper::encode($scriptTagAttrs))
240
        );
241
    }
242
243
    /**
244
     * Register the script, module link, and CSS link tags to the Craft View for the script from the manifest.json file
245
     *
246
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
247
     * @param bool $asyncCss
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
248
     * @param array $scriptTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
249
     * @param array $cssTagAttrs
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
250
     *
251
     * @return void
252
     * @throws InvalidConfigException
253
     */
254
    public function manifestRegister(string $path, bool $asyncCss = true, array $scriptTagAttrs = [], array $cssTagAttrs = [])
255
    {
256
        $view = Craft::$app->getView();
257
        ManifestHelper::fetchManifest($this->manifestPath);
258
        $tags = ManifestHelper::manifestTags($path, $asyncCss, $scriptTagAttrs, $cssTagAttrs);
259
        $legacyTags = ManifestHelper::legacyManifestTags($path, $asyncCss, $scriptTagAttrs, $cssTagAttrs);
260
        // Handle any legacy polyfills
261
        if (!empty($legacyTags) && !$this->legacyPolyfillIncluded) {
262
            $view->registerScript(self::SAFARI_NOMODULE_FIX, $view::POS_HEAD, [], 'SAFARI_NOMODULE_FIX');
263
            $legacyPolyfillTags = ManifestHelper::extractManifestTags(self::LEGACY_POLYFILLS, $asyncCss, $scriptTagAttrs, $cssTagAttrs, true);
264
            $tags = array_merge($legacyPolyfillTags, $tags);
265
            $this->legacyPolyfillIncluded = true;
266
        }
267
        foreach(array_merge($tags, $legacyTags) as $tag) {
0 ignored issues
show
Coding Style introduced by
Expected "foreach (...) {\n"; found "foreach(...) {\n"
Loading history...
268
            if (!empty($tag)) {
269
                $url = UrlHelper::createUrl($this->serverPublic, $tag['url']);
270
                switch ($tag['type']) {
271
                    case 'file':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
272
                        $view->registerJsFile(
273
                            $url,
274
                            $tag['options'],
275
                            md5($url . JsonHelper::encode($tag['options']))
276
                        );
277
                        break;
278
                    case 'css':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
279
                        $view->registerCssFile(
280
                            $url,
281
                            $tag['options']
282
                        );
283
                        break;
284
                    default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
285
                        break;
286
                }
287
            }
288
        }
289
    }
290
291
    /**
292
     * Return the contents of a local file (via path) or remote file (via URL),
293
     * or null if the file doesn't exist or couldn't be fetched
294
     * Yii2 aliases and/or environment variables may be used
295
     *
296
     * @param string $pathOrUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
297
     * @param callable|null $callback
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
298
     *
299
     * @return string|array|null
300
     */
301
    public function fetch(string $pathOrUrl, callable $callback = null)
302
    {
303
        return FileHelper::fetch($pathOrUrl, $callback, $this->cacheKeySuffix);
304
    }
305
306
    /**
307
     * Determine whether the Vite dev server is running
308
     *
309
     * @return bool
310
     */
311
    public function devServerRunning(): bool
312
    {
313
        // If the dev server is turned off via config, say it's not running
314
        if (!$this->useDevServer) {
315
            return false;
316
        }
317
        // If we're not supposed to check that the dev server is actually running, just assume it is
318
        if (!$this->checkDevServer) {
319
            return true;
320
        }
321
        // Check to see if the dev server is actually running by pinging it
322
        $url = UrlHelper::createUrl($this->devServerInternal, self::VITE_CLIENT);
323
324
        return !($this->fetch($url) === null);
325
    }
326
327
    /**
328
     * Invalidate all of the Vite caches
329
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
330
    public function invalidateCaches()
331
    {
332
        $cache = Craft::$app->getCache();
333
        TagDependency::invalidate($cache, FileHelper::CACHE_TAG . $this->cacheKeySuffix);
334
        Craft::info('All Vite caches cleared', __METHOD__);
335
    }
336
337
    // Protected Methods
338
    // =========================================================================
339
340
    /**
341
     * Inject the error entry point JavaScript for auto-reloading of Twig error
342
     * pages
343
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
344
    protected function injectErrorEntry()
345
    {
346
        // If there's no error entry provided, return
347
        if (empty($this->errorEntry)) {
348
            return;
349
        }
350
        // If it's not a server error or a client error, return
351
        $response = Craft::$app->getResponse();
352
        if (!($response->isServerError || $response->isClientError)) {
353
            return;
354
        }
355
        // If the dev server isn't running, return
356
        if (!$this->devServerRunning()) {
357
            return;
358
        }
359
        // Inject the errorEntry script tags to enable HMR on this page
360
        try {
361
            $errorEntry = $this->errorEntry;
362
            if (is_string($errorEntry)) {
0 ignored issues
show
introduced by
The condition is_string($errorEntry) is always true.
Loading history...
363
                $errorEntry = [$errorEntry];
364
            }
365
            foreach ($errorEntry as $entry) {
366
                $tag = $this->script($entry);
367
                if ($tag !== null) {
368
                    echo $tag;
369
                }
370
            }
371
        } catch (Throwable $e) {
372
            // That's okay, Vite will have already logged the error
373
        }
374
    }
375
}
376