Completed
Push — master ( 61e32e...af20bf )
by Craig
06:24 queued 12s
created

RequireJsProcess::aggregateScripts()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 9
nop 3
dl 0
loc 23
rs 9.5555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Zikula package.
7
 *
8
 * Copyright Zikula Foundation - https://ziku.la/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Zikula\ExtensionsModule\Composer\Process;
15
16
use Assetic\Asset\AssetCollection;
17
use Assetic\Asset\FileAsset;
18
use Assetic\Asset\StringAsset;
19
use Assetic\Filter\FilterInterface;
20
use ComponentInstaller\Process\Process;
21
use Composer\Json\JsonFile;
22
use ReflectionClass;
23
24
/**
25
 * Builds the require.js configuration.
26
 */
27
class RequireJsProcess extends Process
28
{
29
    /**
30
     * The base URL for the require.js configuration.
31
     */
32
    protected $baseUrl = 'components';
33
34
    public function init()
35
    {
36
        $output = parent::init();
37
        if ($this->config->has('component-baseurl')) {
38
            $this->baseUrl = $this->config->get('component-baseurl');
39
        }
40
41
        return $output;
42
    }
43
44
    public function process()
45
    {
46
        // Construct the require.js and stick it in the destination.
47
        $json = $this->requireJson($this->packages);
48
        $requireConfig = $this->requireJs($json);
49
//        $vendorPath = str_replace('build/Composer', 'vendor/robloach/component-installer/src/ComponentInstaller', dirname(__DIR__));
50
        $vendorPath = $this->config->get('vendor-dir') . '/robloach/component-installer/src/ComponentInstaller';
51
52
        // Attempt to write the require.config.js file.
53
        $destination = $this->componentDir . '/require.config.js';
54
        $this->fs->ensureDirectoryExists(dirname($destination));
55
        if (false === file_put_contents($destination, $requireConfig)) {
56
            $this->io->write('<error>Error writing require.config.js</error>');
57
58
            return false;
59
        }
60
61
        // Read in require.js to prepare the final require.js.
62
        if (!file_exists($vendorPath . '/Resources/require.js')) {
63
            $this->io->write('<error>Error reading in require.js</error>');
64
65
            return false;
66
        }
67
68
        $assets = $this->newAssetCollection();
69
        $assets->add(new FileAsset($vendorPath . '/Resources/require.js'));
70
        $assets->add(new StringAsset($requireConfig));
71
72
        // Append the config to the require.js and write it.
73
        if (false === file_put_contents($this->componentDir . '/require.js', $assets->dump())) {
74
            $this->io->write('<error>Error writing require.js to the components directory</error>');
75
76
            return false;
77
        }
78
79
        return true;
80
    }
81
82
    /**
83
     * Creates a require.js configuration (JSON array) based on an array of packages from the composer.lock file.
84
     */
85
    public function requireJson(array $packages = []): array
86
    {
87
        $json = [];
88
89
        // Construct the packages configuration.
90
        foreach ($packages as $package) {
91
            // Retrieve information from the extra options.
92
            $extra = $package['extra'] ?? [];
93
            $options = $extra['component'] ?? [];
94
95
            // Construct the base details.
96
            $name = $this->getComponentName($package['name'], $extra);
97
            $component = [
98
                'name' => $name,
99
            ];
100
101
            // Build the "main" directive.
102
            $scripts = $options['scripts'] ?? [];
103
            if (!empty($scripts)) {
104
                // Put all scripts into a build.js file.
105
                $result = $this->aggregateScripts($package, $scripts, $name . DIRECTORY_SEPARATOR . $name . '-built.js');
106
                if (false !== $result) {
107
                    // If the aggregation was successful, add the script to the
108
                    // packages array.
109
                    $component['main'] = $name . '-built.js';
110
111
                    // Add the component to the packages array.
112
                    $json['packages'][] = $component;
113
                }
114
            }
115
116
            // Add the shim definition for the package.
117
            $shim = $options['shim'] ?? [];
118
            if (!empty($shim)) {
119
                $json['shim'][$name] = $shim;
120
            }
121
122
            // Add the config definition for the package.
123
            $packageConfig = $options['config'] ?? [];
124
            if (!empty($packageConfig)) {
125
                $json['config'][$name] = $packageConfig;
126
            }
127
        }
128
129
        // Provide the baseUrl.
130
        $json['baseUrl'] = $this->baseUrl;
131
132
        // Merge in configuration options from the root.
133
        if ($this->config->has('component')) {
134
            $config = $this->config->get('component');
135
            if (isset($config) && is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always false.
Loading history...
136
                // Use a recursive, distict array merge.
137
                $json = $this->arrayMergeRecursiveDistinct($json, $config);
138
            }
139
        }
140
141
        return $json;
142
    }
143
144
    /**
145
     * Concatenate all scripts together into one destination file.
146
     */
147
    public function aggregateScripts(array $package, array $scripts, string $file): bool
148
    {
149
        $assets = $this->newAssetCollection();
150
151
        foreach ($scripts as $script) {
152
            // Collect each candidate from a glob file search.
153
            $path = $this->getVendorDir($package) . DIRECTORY_SEPARATOR . $script;
154
            $matches = $this->fs->recursiveGlobFiles($path);
155
            foreach ($matches as $match) {
156
                $assets->add(new FileAsset($match));
157
            }
158
        }
159
        $js = $assets->dump();
160
161
        // Write the file if there are any JavaScript assets.
162
        if (!empty($js)) {
163
            $destination = $this->componentDir . DIRECTORY_SEPARATOR . $file;
164
            $this->fs->ensureDirectoryExists(dirname($destination));
165
166
            return file_put_contents($destination, $js) ? true : false;
167
        }
168
169
        return false;
170
    }
171
172
    /**
173
     * Constructs the require.js file from the provided require.js JSON array.
174
     */
175
    public function requireJs(array $json = []): string
176
    {
177
        // Encode the array to a JSON array.
178
        $js = JsonFile::encode($json);
179
180
        // Construct the JavaScript output.
181
        $output = <<<EOT
182
var components = ${js};
183
components.baseUrl = Zikula.Config.baseURL + "web";                
184
if (typeof require !== "undefined" && require.config) {
185
    require.config(components);
186
} else {
187
    var require = components;
188
}
189
if (typeof exports !== "undefined" && typeof module !== "undefined") {
190
    module.exports = components;
191
}
192
EOT;
193
194
        return $output;
195
    }
196
197
    /**
198
     * Merges two arrays without changing string array keys. Appends to array if keys are numeric.
199
     *
200
     * @see array_merge()
201
     * @see array_merge_recursive()
202
     */
203
    protected function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
204
    {
205
        $merged = $array1;
206
207
        foreach ($array2 as $key => &$value) {
208
            if (is_numeric($key)) {
209
                $merged[] = $value;
210
            } elseif (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
211
                $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value);
212
            } else {
213
                $merged[$key] = $value;
214
            }
215
        }
216
217
        return $merged;
218
    }
219
220
    protected function newAssetCollection(): AssetCollection
221
    {
222
        // Aggregate all the assets into one file.
223
        $assets = new AssetCollection();
224
        if ($this->config->has('component-scriptFilters')) {
225
            $filters = $this->config->get('component-scriptFilters');
226
            if (isset($filters) && is_array($filters)) {
0 ignored issues
show
introduced by
The condition is_array($filters) is always false.
Loading history...
227
                foreach ($filters as $filter => $filterParams) {
228
                    $reflection = new ReflectionClass($filter);
229
                    /** @var FilterInterface $filter */
230
                    $filter = $reflection->newInstanceArgs($filterParams);
231
                    $assets->ensureFilter($filter);
232
                }
233
            }
234
        }
235
236
        return $assets;
237
    }
238
}
239