Passed
Push — develop ( 4e3d41...c96be8 )
by Andrew
03:14
created

Manifest::getFileContents()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 27
nc 9
nop 1
dl 0
loc 44
rs 8.8657
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/
9
 * @copyright Copyright (c) 2018 nystudio107
10
 */
11
12
namespace nystudio107\twigpack\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
/**
23
 * @author    nystudio107
24
 * @package   Twigpack
25
 * @since     1.0.0
26
 */
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
    /**
41
     * @var array
42
     */
43
    protected static $files;
44
45
    // Public Static Methods
46
    // =========================================================================
47
48
    /**
49
     * @param array  $config
50
     * @param string $moduleName
51
     * @param bool   $async
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');
59
        if ($legacyModule === null) {
60
            return null;
61
        }
62
        $lines = [];
63
        if ($async) {
64
            $lines[] = "<link rel=\"preload\" href=\"{$legacyModule}\" as=\"style\" onload=\"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
     * @param array  $config
75
     * @param string $moduleName
76
     * @param bool   $async
77
     *
78
     * @return null|string
79
     * @throws NotFoundHttpException
80
     */
81
    public static function getJsModuleTags(array $config, string $moduleName, bool $async)
82
    {
83
        $legacyModule = self::getModule($config, $moduleName, 'legacy');
84
        if ($legacyModule === null) {
85
            return null;
86
        }
87
        if ($async) {
88
            $modernModule = self::getModule($config, $moduleName, 'modern');
89
            if ($modernModule === null) {
90
                return null;
91
            }
92
        }
93
        $lines = [];
94
        if ($async) {
95
            $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...
96
            $lines[] = "<script nomodule src=\"{$legacyModule}\"></script>";
97
        } else {
98
            $lines[] = "<script src=\"{$legacyModule}\"></script>";
99
        }
100
101
        return implode("\r\n", $lines);
102
    }
103
104
    /**
105
     * Safari 10.1 supports modules, but does not support the `nomodule`
106
     * attribute - it will load <script nomodule> anyway. This snippet solve
107
     * this problem, but only for script tags that load external code, e.g.:
108
     * <script nomodule src="nomodule.js"></script>
109
     *
110
     * Again: this will **not* # prevent inline script, e.g.:
111
     * <script nomodule>alert('no modules');</script>.
112
     *
113
     * This workaround is possible because Safari supports the non-standard
114
     * 'beforeload' event. This allows us to trap the module and nomodule load.
115
     *
116
     * Note also that `nomodule` is supported in later versions of Safari -
117
     * it's just 10.1 that omits this attribute.
118
     *
119
     * c.f.: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
120
     *
121
     * @return string
122
     */
123
    public static function getSafariNomoduleFix(): string
124
    {
125
        return <<<EOT
126
<script>
127
!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()}}();
128
</script>
129
EOT;
130
    }
131
132
    /**
133
     * @param array  $config
134
     * @param string $moduleName
135
     * @param string $type
136
     *
137
     * @return null|string
138
     * @throws NotFoundHttpException
139
     */
140
    public static function getModule(array $config, string $moduleName, string $type = 'modern')
141
    {
142
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
143
        $isHot = ($devMode && $config['useDevServer']);
144
        $manifest = null;
145
        // Try to get the manifest
146
        while ($manifest === null) {
147
            $manifestPath = $isHot
148
                ? $config['devServer']['manifestPath']
149
                : $config['server']['manifestPath'];
150
            $manifest = self::getManifestFile($config['manifest'][$type], $manifestPath);
151
            // If the manigest isn't found, and it was hot, fall back on non-hot
152
            if ($manifest === null) {
153
                Craft::error(
154
                    Craft::t(
155
                        'twigpack',
156
                        'Manifest file not found at: {manifestPath}',
157
                        ['manifestPath' => $manifestPath]
158
                    ),
159
                    __METHOD__
160
                );
161
                if ($isHot) {
162
                    // Try again, but not with home module replacement
163
                    $isHot = false;
164
                } else {
165
                    if ($devMode) {
166
                        // We couldn't find a manifest; throw an error
167
                        throw new NotFoundHttpException(
168
                            Craft::t(
169
                                'twigpack',
170
                                'Manifest file not found at: {manifestPath}',
171
                                ['manifestPath' => $manifestPath]
172
                            )
173
                        );
174
                    }
175
176
                    return null;
177
                }
178
            }
179
        }
180
        $module = $manifest[$moduleName];
181
        $prefix = $isHot
182
            ? $config['devServer']['publicPath']
183
            : $config['server']['publicPath'];
184
        // If the module isn't a full URL, prefix it
185
        if (!UrlHelper::isAbsoluteUrl($module)) {
186
            $module = self::combinePaths($prefix, $module);
187
        }
188
        // Make sure it's a full URL
189
        if (!UrlHelper::isAbsoluteUrl($module)) {
190
            try {
191
                $module = UrlHelper::siteUrl($module);
192
            } catch (Exception $e) {
193
                Craft::error($e->getMessage(), __METHOD__);
194
            }
195
        }
196
197
        return $module;
198
    }
199
200
    /**
201
     * Invalidate all of the manifest caches
202
     */
203
    public static function invalidateCaches()
204
    {
205
        $cache = Craft::$app->getCache();
206
        TagDependency::invalidate($cache, self::CACHE_TAG);
207
        Craft::info('All manifest caches cleared', __METHOD__);
208
    }
209
210
    // Protected Static Methods
211
    // =========================================================================
212
213
    /**
214
     * @param string $name
215
     * @param string $path
216
     *
217
     * @return mixed
218
     */
219
    protected static function getManifestFile(string $name, string $path)
220
    {
221
        // Normalize the path
222
        $path = self::combinePaths($path, $name);
223
224
        return self::getFileContents($path);
225
    }
226
227
    /**
228
     * @param string $path
229
     *
230
     * @return mixed
231
     */
232
    protected static function getFileContents(string $path)
233
    {
234
        // Make sure it's a full URL
235
        if (!UrlHelper::isAbsoluteUrl($path)) {
236
            try {
237
                $path = UrlHelper::siteUrl($path);
238
            } catch (Exception $e) {
239
                Craft::error($e->getMessage(), __METHOD__);
240
            }
241
        }
242
        // Return the memoized manifest if it exists
243
        if (!empty(self::$files[$path])) {
244
            return self::$files[$path];
245
        }
246
        // Create the dependency tags
247
        $dependency = new TagDependency([
248
            'tags' => [
249
                self::CACHE_TAG,
250
                self::CACHE_TAG.$path,
251
            ],
252
        ]);
253
        // Set the cache duraction based on devMode
254
        $cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
255
            ? self::DEVMODE_CACHE_DURATION
256
            : null;
257
        // Get the result from the cache, or parse the file
258
        $cache = Craft::$app->getCache();
259
        $file = $cache->getOrSet(
260
            self::CACHE_KEY.$path,
261
            function () use ($path) {
262
                $result = null;
263
                $string = @file_get_contents($path);
264
                if ($string) {
265
                    $result = JsonHelper::decodeIfJson($string);
266
                }
267
268
                return $result;
269
            },
270
            $cacheDuration,
271
            $dependency
272
        );
273
        self::$files[$path] = $file;
274
275
        return $file;
276
    }
277
278
    /**
279
     * Combined the passed in paths, whether file system or URL
280
     *
281
     * @param string ...$paths
282
     *
283
     * @return string
284
     */
285
    protected static function combinePaths(string ...$paths): string
286
    {
287
        $last_key = \count($paths) - 1;
288
        array_walk($paths, function (&$val, $key) use ($last_key) {
289
            switch ($key) {
290
                case 0:
291
                    $val = rtrim($val, '/ ');
292
                    break;
293
                case $last_key:
294
                    $val = ltrim($val, '/ ');
295
                    break;
296
                default:
297
                    $val = trim($val, '/ ');
298
                    break;
299
            }
300
        });
301
302
        $first = array_shift($paths);
303
        $last = array_pop($paths);
304
        $paths = array_filter($paths);
305
        array_unshift($paths, $first);
306
        $paths[] = $last;
307
308
        return implode('/', $paths);
309
    }
310
}
311