Passed
Push — v1 ( 9d6af6...e71600 )
by Andrew
11:38 queued 04:02
created

Manifest::getCssModuleTags()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 3
dl 0
loc 15
rs 9.9332
c 0
b 0
f 0
1
<?php
2
/**
3
 * Twigpack plugin for Craft CMS 3.x
4
 *
5
 * Twigpack is the conduit between Twig and webpack, with manifest.json &
6
 * webpack-dev-server HMR support
7
 *
8
 * @link      https://nystudio107.com/
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
9
 * @copyright Copyright (c) 2018 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
10
 */
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...
11
12
namespace nystudio107\transcoder\helpers;
13
14
use Craft;
15
use craft\helpers\Json as JsonHelper;
16
use craft\helpers\UrlHelper;
17
18
use yii\base\Exception;
19
use yii\caching\TagDependency;
20
use yii\web\NotFoundHttpException;
21
22
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
23
 * @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 indented incorrectly; expected 2 spaces but found 4
Loading history...
24
 * @package   Twigpack
0 ignored issues
show
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 3
Loading history...
25
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 3 spaces but found 5
Loading history...
26
 */
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...
27
class Manifest
28
{
29
    // Constants
30
    // =========================================================================
31
32
    const CACHE_KEY = 'twigpack';
33
    const CACHE_TAG = 'twigpack';
34
35
    const DEVMODE_CACHE_DURATION = 1;
36
37
    // Protected Static Properties
38
    // =========================================================================
39
40
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
41
     * @var array
42
     */
43
    protected static $files;
44
45
    // Public Static Methods
46
    // =========================================================================
47
48
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
49
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
50
     * @param string $moduleName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
51
     * @param bool   $async
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
52
     *
53
     * @return null|string
54
     * @throws NotFoundHttpException
55
     */
56
    public static function getCssModuleTags(array $config, string $moduleName, bool $async)
57
    {
58
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
59
        if ($legacyModule === null) {
60
            return '';
61
        }
62
        $lines = [];
63
        if ($async) {
64
            $lines[] = "<link rel=\"preload\" href=\"{$legacyModule}\" as=\"style\" onload=\"this.onload=null;this.rel='stylesheet'\" />";
65
            $lines[] = "<noscript><link rel=\"stylesheet\" href=\"{$legacyModule}\"></noscript>";
66
        } else {
67
            $lines[] = "<link rel=\"stylesheet\" href=\"{$legacyModule}\" />";
68
        }
69
70
        return implode("\r\n", $lines);
71
    }
72
73
    /**
74
     * Returns the uglified loadCSS rel=preload Polyfill as per:
75
     * https://github.com/filamentgroup/loadCSS#how-to-use-loadcss-recommended-example
76
     *
77
     * @return string
78
     */
79
    public static function getCssRelPreloadPolyfill(): string
80
    {
81
        return <<<EOT
82
<script>
83
/*! loadCSS. [c]2017 Filament Group, Inc. MIT License */
84
!function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={};if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")}catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){var e=t.media||"all";function a(){t.media=e}t.addEventListener?t.addEventListener("load",a):t.attachEvent&&t.attachEvent("onload",a),setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(a,3e3)},e.poly=function(){if(!e.support())for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel||"style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")||(o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500);t.addEventListener?t.addEventListener("load",function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload",function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS}("undefined"!=typeof global?global:this);
85
</script>
86
EOT;
87
    }
88
89
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
90
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
91
     * @param string $moduleName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
92
     * @param bool   $async
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
93
     *
94
     * @return null|string
95
     * @throws NotFoundHttpException
96
     */
97
    public static function getJsModuleTags(array $config, string $moduleName, bool $async)
98
    {
99
        $legacyModule = self::getModule($config, $moduleName, 'legacy');
100
        if ($legacyModule === null) {
101
            return '';
102
        }
103
        if ($async) {
104
            $modernModule = self::getModule($config, $moduleName, 'modern');
105
            if ($modernModule === null) {
106
                return '';
107
            }
108
        }
109
        $lines = [];
110
        if ($async) {
111
            $lines[] = "<script type=\"module\" src=\"{$modernModule}\"></script>";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $modernModule does not seem to be defined for all execution paths leading up to this point.
Loading history...
112
            $lines[] = "<script nomodule src=\"{$legacyModule}\"></script>";
113
        } else {
114
            $lines[] = "<script src=\"{$legacyModule}\"></script>";
115
        }
116
117
        return implode("\r\n", $lines);
118
    }
119
120
    /**
121
     * Safari 10.1 supports modules, but does not support the `nomodule`
122
     * attribute - it will load <script nomodule> anyway. This snippet solve
123
     * this problem, but only for script tags that load external code, e.g.:
124
     * <script nomodule src="nomodule.js"></script>
125
     *
126
     * Again: this will **not* # prevent inline script, e.g.:
127
     * <script nomodule>alert('no modules');</script>.
128
     *
129
     * This workaround is possible because Safari supports the non-standard
130
     * 'beforeload' event. This allows us to trap the module and nomodule load.
131
     *
132
     * Note also that `nomodule` is supported in later versions of Safari -
133
     * it's just 10.1 that omits this attribute.
134
     *
135
     * c.f.: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
136
     *
137
     * @return string
138
     */
139
    public static function getSafariNomoduleFix(): string
140
    {
141
        return <<<EOT
142
<script>
143
!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()}}();
144
</script>
145
EOT;
146
    }
147
148
    /**
149
     * Return the URI to a module
150
     *
151
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
152
     * @param string $moduleName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
153
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
154
     * @param bool   $soft
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
155
     *
156
     * @return null|string
157
     * @throws NotFoundHttpException
158
     */
159
    public static function getModule(array $config, string $moduleName, string $type = 'modern', bool $soft = false)
160
    {
161
        $module = null;
162
        // Determine whether we should use the devServer for HMR or not
163
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
164
        $isHot = ($devMode && $config['useDevServer']);
165
        // Get the manifest file
166
        $manifest = self::getManifestFile($config, $isHot, $type);
167
        if ($manifest !== null) {
168
            // Make sure it exists in the manifest
169
            if (empty($manifest[$moduleName])) {
170
                self::reportError(Craft::t(
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...
171
                    'transcoder',
172
                    'Module does not exist in the manifest: {moduleName}',
173
                    ['moduleName' => $moduleName]
174
                ), $soft);
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...
175
176
                return null;
177
            }
178
            $module = $manifest[$moduleName];
179
            $prefix = $isHot
180
                ? $config['devServer']['publicPath']
181
                : $config['server']['publicPath'];
182
            // If the module isn't a full URL, prefix it
183
            if (!UrlHelper::isAbsoluteUrl($module)) {
184
                $module = self::combinePaths($prefix, $module);
185
            }
186
            // Make sure it's a full URL
187
            if (!UrlHelper::isAbsoluteUrl($module)) {
188
                try {
189
                    $module = UrlHelper::siteUrl($module);
190
                } catch (Exception $e) {
191
                    Craft::error($e->getMessage(), __METHOD__);
192
                }
193
            }
194
        }
195
196
        return $module;
197
    }
198
199
    /**
200
     * Return a JSON-decoded manifest file
201
     *
202
     * @param array  $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
203
     * @param bool   $isHot
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
204
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
205
     *
206
     * @return null|array
207
     * @throws NotFoundHttpException
208
     */
209
    public static function getManifestFile(array $config, bool &$isHot, string $type = 'modern')
210
    {
211
        $manifest = null;
212
        // Try to get the manifest
213
        while ($manifest === null) {
214
            $manifestPath = $isHot
215
                ? $config['devServer']['manifestPath']
216
                : $config['server']['manifestPath'];
217
            // Normalize the path
218
            $path = self::combinePaths($manifestPath, $config['manifest'][$type]);
219
            $manifest = self::getJsonFileFromUri($path);
220
            // If the manifest isn't found, and it was hot, fall back on non-hot
221
            if ($manifest === null) {
222
                // We couldn't find a manifest; throw an error
223
                self::reportError(Craft::t(
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...
224
                    'transcoder',
225
                    'Manifest file not found at: {manifestPath}',
226
                    ['manifestPath' => $manifestPath]
227
                ), true);
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...
228
                if ($isHot) {
229
                    // Try again, but not with home module replacement
230
                    $isHot = false;
231
                } else {
232
                    // Give up and return null
233
                    return null;
234
                }
235
            }
236
        }
237
238
        return $manifest;
239
    }
240
241
    /**
242
     * Invalidate all of the manifest caches
243
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
244
    public static function invalidateCaches()
245
    {
246
        $cache = Craft::$app->getCache();
247
        TagDependency::invalidate($cache, self::CACHE_TAG);
248
        Craft::info('All manifest caches cleared', __METHOD__);
249
    }
250
251
    // Protected Static Methods
252
    // =========================================================================
253
254
    /**
255
     * Return the contents of a file from a URI path
256
     *
257
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
258
     *
259
     * @return mixed
260
     */
261
    protected static function getJsonFileFromUri(string $path)
262
    {
263
        // Make sure it's a full URL
264
        if (!UrlHelper::isAbsoluteUrl($path) && !is_file($path)) {
265
            try {
266
                $path = UrlHelper::siteUrl($path);
267
            } catch (Exception $e) {
268
                Craft::error($e->getMessage(), __METHOD__);
269
            }
270
        }
271
272
        return self::getJsonFileContents($path);
273
    }
274
275
    /**
276
     * Return the contents of a file from the passed in path
277
     *
278
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
279
     *
280
     * @return mixed
281
     */
282
    protected static function getJsonFileContents(string $path)
283
    {
284
        // Return the memoized manifest if it exists
285
        if (!empty(self::$files[$path])) {
286
            return self::$files[$path];
287
        }
288
        // Create the dependency tags
289
        $dependency = new TagDependency([
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...
290
            'tags' => [
291
                self::CACHE_TAG,
292
                self::CACHE_TAG.$path,
293
            ],
294
        ]);
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...
295
        // Set the cache duration based on devMode
296
        $cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
297
            ? self::DEVMODE_CACHE_DURATION
298
            : null;
299
        // Get the result from the cache, or parse the file
300
        $cache = Craft::$app->getCache();
301
        $file = $cache->getOrSet(
302
            self::CACHE_KEY.$path,
303
            function () use ($path) {
304
                $result = null;
305
                $string = @file_get_contents($path);
306
                if ($string) {
307
                    $result = JsonHelper::decodeIfJson($string);
308
                }
309
310
                return $result;
311
            },
312
            $cacheDuration,
313
            $dependency
314
        );
315
        self::$files[$path] = $file;
316
317
        return $file;
318
    }
319
320
    /**
321
     * Combined the passed in paths, whether file system or URL
322
     *
323
     * @param string ...$paths
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
324
     *
325
     * @return string
326
     */
327
    protected static function combinePaths(string ...$paths): string
328
    {
329
        $last_key = \count($paths) - 1;
330
        array_walk($paths, function (&$val, $key) use ($last_key) {
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...
331
            switch ($key) {
332
                case 0:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
333
                    $val = rtrim($val, '/ ');
334
                    break;
335
                case $last_key:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
336
                    $val = ltrim($val, '/ ');
337
                    break;
338
                default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
339
                    $val = trim($val, '/ ');
340
                    break;
341
            }
342
        });
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...
343
344
        $first = array_shift($paths);
345
        $last = array_pop($paths);
346
        $paths = array_filter($paths);
347
        array_unshift($paths, $first);
348
        $paths[] = $last;
349
350
        return implode('/', $paths);
351
    }
352
353
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
354
     * @param string $error
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
355
     * @param bool   $soft
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
356
     *
357
     * @throws NotFoundHttpException
358
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
359
    protected static function reportError(string $error, $soft = false)
360
    {
361
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
362
        if ($devMode && !$soft) {
363
            throw new NotFoundHttpException($error);
364
        }
365
        Craft::error($error, __METHOD__);
366
    }
367
}
368