‪TYPO3CMS  ‪main
ImportMap.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
18 namespace ‪TYPO3\CMS\Core\Page;
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
29 
34 {
35  protected array ‪$extensionsToLoad = [];
36 
37  private ?array ‪$importMaps = null;
38 
42  public function ‪__construct(
43  protected readonly array $packages,
44  protected readonly ?‪FrontendInterface $cache = null,
45  protected readonly string $cacheIdentifier = '',
46  protected readonly ?EventDispatcherInterface $eventDispatcher = null,
47  protected readonly bool $bustSuffix = true
48  ) {
49  }
50 
54  public function ‪includeAllImports(): void
55  {
56  $this->extensionsToLoad['*'] = true;
57  }
58 
59  public function ‪includeTaggedImports(string $tag): void
60  {
61  if (isset($this->extensionsToLoad['*'])) {
62  return;
63  }
64 
65  foreach ($this->‪getImportMaps() as $package => $config) {
66  $tags = $config['tags'] ?? [];
67  if (in_array($tag, $tags, true)) {
68  $this->‪loadDependency($package);
69  }
70  }
71  }
72 
73  public function ‪includeImportsFor(string $specifier): void
74  {
75  if (!isset($this->extensionsToLoad['*'])) {
76  $this->‪resolveImport($specifier, true);
77  } else {
78  $this->‪dispatchResolveJavaScriptImportEvent($specifier, true);
79  }
80  }
81 
82  public function ‪resolveImport(
83  string $specifier,
84  bool $loadImportConfiguration = true
85  ): ?string {
86  $resolution = $this->‪dispatchResolveJavaScriptImportEvent($specifier, $loadImportConfiguration);
87  if ($resolution !== null) {
88  return $resolution;
89  }
90 
91  foreach (array_reverse($this->‪getImportMaps()) as $package => $config) {
92  $imports = $config['imports'] ?? [];
93  if (isset($imports[$specifier])) {
94  if ($loadImportConfiguration) {
95  $this->‪loadDependency($package);
96  }
97  return $imports[$specifier];
98  }
99 
100  $specifierParts = explode('/', $specifier);
101  $specifierPartCount = count($specifierParts);
102  for ($i = 1; $i < $specifierPartCount; ++$i) {
103  $prefix = implode('/', array_slice($specifierParts, 0, $i)) . '/';
104  if (isset($imports[$prefix])) {
105  if ($loadImportConfiguration) {
106  $this->‪loadDependency($package);
107  }
108  return $imports[$prefix] . implode(array_slice($specifierParts, $i));
109  }
110  }
111  }
112 
113  return null;
114  }
115 
116  public function ‪render(
117  string $urlPrefix,
118  null|string|‪ConsumableString $nonce
119  ): string {
120  if (count($this->extensionsToLoad) === 0 || count($this->‪getImportMaps()) === 0) {
121  return '';
122  }
123 
124  $html = [];
125 
126  $importMap = $this->‪composeImportMap($urlPrefix);
127  $json = json_encode(
128  $importMap,
129  JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_THROW_ON_ERROR
130  );
131  $nonceAttr = $nonce !== null ? ' nonce="' . htmlspecialchars((string)$nonce) . '"' : '';
132  $html[] = sprintf('<script type="importmap"%s>%s</script>', $nonceAttr, $json);
133 
134  return implode(PHP_EOL, $html) . PHP_EOL;
135  }
136 
137  public function ‪warmupCaches(): void
138  {
139  $this->‪computeImportMaps();
140  }
141 
142  protected function ‪getImportMaps(): array
143  {
144  return $this->importMaps ?? $this->‪getFromCache() ?? $this->‪computeImportMaps();
145  }
146 
147  protected function ‪getFromCache(): ?array
148  {
149  if ($this->cache === null) {
150  return null;
151  }
152  if (!$this->cache->has($this->cacheIdentifier)) {
153  return null;
154  }
155  $this->importMaps = $this->cache->get($this->cacheIdentifier);
156  return ‪$this->importMaps;
157  }
158 
159  protected function ‪computeImportMaps(): array
160  {
161  $extensionVersions = [];
162  ‪$importMaps = [];
163  foreach ($this->packages as $package) {
164  $configurationFile = $package->getPackagePath() . 'Configuration/JavaScriptModules.php';
165  if (!is_readable($configurationFile)) {
166  continue;
167  }
168  $extensionVersions[$package->getPackageKey()] = implode(':', [
169  $package->getPackageKey(),
170  $package->getPackageMetadata()->getVersion(),
171  ]);
172  $packageConfiguration = require($configurationFile);
173  ‪$importMaps[$package->getPackageKey()] = $packageConfiguration ?? [];
174  }
175 
176  $isDevelopment = ‪Environment::getContext()->isDevelopment();
177  if ($isDevelopment) {
178  $bust = (string)‪$GLOBALS['EXEC_TIME'];
179  } else {
180  $bust = ‪GeneralUtility::hmac(
181  ‪Environment::getProjectPath() . implode('|', $extensionVersions)
182  );
183  }
184 
185  foreach (‪$importMaps as $packageName => $config) {
186  ‪$importMaps[$packageName]['imports'] = $this->‪resolvePaths(
187  $config['imports'] ?? [],
188  $this->bustSuffix ? $bust : null
189  );
190  }
191 
192  $this->importMaps = ‪$importMaps;
193  if ($this->cache !== null) {
194  $this->cache->set($this->cacheIdentifier, ‪$importMaps);
195  }
196  return ‪$importMaps;
197  }
198 
199  protected function ‪resolveRecursiveImportMap(
200  string $prefix,
201  string $path,
202  array $exclude,
203  string $bust
204  ): array {
205  $path = GeneralUtility::getFileAbsFileName($path);
206  if (!$path || @!is_dir($path)) {
207  return [];
208  }
209  $exclude = array_map(
210  static fn (string $excludePath): string => GeneralUtility::getFileAbsFileName($excludePath),
211  $exclude
212  );
213 
214  $fileIterator = new \RegexIterator(
215  new \RecursiveIteratorIterator(
216  new \RecursiveDirectoryIterator($path)
217  ),
218  '#^' . preg_quote($path, '#') . '(.+\.js)$#',
219  \RegexIterator::GET_MATCH
220  );
221 
222  $map = [];
223  foreach ($fileIterator as $match) {
224  $fileName = $match[0];
225  $specifier = $prefix . ($match[1] ?? '');
226 
227  // @todo: Abstract into an iterator?
228  foreach ($exclude as $excludedPath) {
229  if (str_starts_with($fileName, $excludedPath)) {
230  continue 2;
231  }
232  }
233 
234  $webPath = ‪PathUtility::getAbsoluteWebPath($fileName, false) . '?bust=' . $bust;
235 
236  $map[$specifier] = $webPath;
237  }
238 
239  return $map;
240  }
241 
242  protected function ‪resolvePaths(
243  array $imports,
244  string $bust = null
245  ): array {
246  $cacheBustingSpecifiers = [];
247  foreach ($imports as $specifier => $address) {
248  if (str_ends_with($specifier, '/')) {
249  $path = is_array($address) ? ($address['path'] ?? '') : $address;
250  $exclude = is_array($address) ? ($address['exclude'] ?? []) : [];
251 
253  $cacheBusted = preg_match('#[^/]@#', $path) === 1;
254  if ($bust !== null && !$cacheBusted) {
255  // Resolve recursive importmap in order to add a bust suffix
256  // to each file.
257  $cacheBustingSpecifiers[] = $this->‪resolveRecursiveImportMap($specifier, $path, $exclude, $bust);
258  }
259  } else {
261  $cacheBusted = preg_match('#[^/]@#', $address) === 1;
262  if ($bust !== null && !$cacheBusted) {
263  ‪$url .= '?bust=' . $bust;
264  }
265  }
266  $imports[$specifier] = ‪$url;
267  }
268 
269  return $imports + array_merge(...$cacheBustingSpecifiers);
270  }
271 
272  protected function ‪loadDependency(string $packageName): void
273  {
274  if (isset($this->extensionsToLoad[$packageName])) {
275  return;
276  }
277 
278  $this->extensionsToLoad[$packageName] = true;
279  $dependencies = $this->‪getImportMaps()[$packageName]['dependencies'] ?? [];
280  foreach ($dependencies as $dependency) {
281  $this->‪loadDependency($dependency);
282  }
283  }
284 
285  protected function ‪composeImportMap(string $urlPrefix): array
286  {
287  ‪$importMaps = $this->‪getImportMaps();
288 
289  if (!isset($this->extensionsToLoad['*'])) {
290  ‪$importMaps = array_intersect_key(‪$importMaps, $this->extensionsToLoad);
291  }
292 
293  $importMap = [];
294  foreach (‪$importMaps as $singleImportMap) {
295  ArrayUtility::mergeRecursiveWithOverrule($importMap, $singleImportMap);
296  }
297  unset($importMap['dependencies']);
298  unset($importMap['tags']);
299 
300  foreach ($importMap['imports'] ?? [] as $specifier => ‪$url) {
301  $importMap['imports'][$specifier] = $urlPrefix . ‪$url;
302  }
303 
304  return $importMap;
305  }
306 
308  string $specifier,
309  bool $loadImportConfiguration = true
310  ): ?string {
311  if ($this->eventDispatcher === null) {
312  return null;
313  }
314 
315  return $this->eventDispatcher->dispatch(
316  new ‪ResolveJavaScriptImportEvent($specifier, $loadImportConfiguration, $this)
317  )->resolution;
318  }
319 
323  public function ‪updateState(array $state): void
324  {
325  $this->extensionsToLoad = $state['extensionsToLoad'] ?? [];
326  }
327 
331  public function ‪getState(): array
332  {
333  return [
334  'extensionsToLoad' => ‪$this->extensionsToLoad,
335  ];
336  }
337 }
‪TYPO3\CMS\Core\Page\ImportMap\__construct
‪__construct(protected readonly array $packages, protected readonly ?FrontendInterface $cache=null, protected readonly string $cacheIdentifier='', protected readonly ?EventDispatcherInterface $eventDispatcher=null, protected readonly bool $bustSuffix=true)
Definition: ImportMap.php:42
‪TYPO3\CMS\Core\Page\ImportMap\$extensionsToLoad
‪array $extensionsToLoad
Definition: ImportMap.php:35
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪TYPO3\CMS\Core\Page\ImportMap
Definition: ImportMap.php:34
‪TYPO3\CMS\Core\Page
Definition: AssetCollector.php:18
‪TYPO3\CMS\Core\Page\ImportMap\composeImportMap
‪composeImportMap(string $urlPrefix)
Definition: ImportMap.php:285
‪TYPO3\CMS\Core\Page\ImportMap\includeTaggedImports
‪includeTaggedImports(string $tag)
Definition: ImportMap.php:59
‪TYPO3\CMS\Core\Package\PackageInterface
Definition: PackageInterface.php:22
‪TYPO3\CMS\Core\Utility\PathUtility\getAbsoluteWebPath
‪static string getAbsoluteWebPath(string $targetPath, bool $prefixWithSitePath=true)
Definition: PathUtility.php:52
‪TYPO3\CMS\Core\Page\ImportMap\resolvePaths
‪resolvePaths(array $imports, string $bust=null)
Definition: ImportMap.php:242
‪TYPO3\CMS\Core\Core\Environment\getProjectPath
‪static string getProjectPath()
Definition: Environment.php:160
‪TYPO3\CMS\Core\Page\ImportMap\warmupCaches
‪warmupCaches()
Definition: ImportMap.php:137
‪TYPO3\CMS\Core\Utility\PathUtility\getPublicResourceWebPath
‪static getPublicResourceWebPath(string $resourcePath, bool $prefixWithSitePath=true)
Definition: PathUtility.php:97
‪TYPO3\CMS\Core\Page\ImportMap\resolveImport
‪resolveImport(string $specifier, bool $loadImportConfiguration=true)
Definition: ImportMap.php:82
‪TYPO3\CMS\Core\Page\ImportMap\getFromCache
‪getFromCache()
Definition: ImportMap.php:147
‪TYPO3\CMS\Core\Utility\GeneralUtility\hmac
‪static string hmac($input, $additionalSecret='')
Definition: GeneralUtility.php:475
‪TYPO3\CMS\Core\Page\ImportMap\computeImportMaps
‪computeImportMaps()
Definition: ImportMap.php:159
‪TYPO3\CMS\Core\Domain\ConsumableString
Definition: ConsumableString.php:26
‪TYPO3\CMS\Core\Page\ImportMap\getState
‪getState()
Definition: ImportMap.php:331
‪TYPO3\CMS\Core\Page\ImportMap\resolveRecursiveImportMap
‪resolveRecursiveImportMap(string $prefix, string $path, array $exclude, string $bust)
Definition: ImportMap.php:199
‪TYPO3\CMS\Core\Page\ImportMap\includeImportsFor
‪includeImportsFor(string $specifier)
Definition: ImportMap.php:73
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Page\ImportMap\updateState
‪updateState(array $state)
Definition: ImportMap.php:323
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Core\Page\ImportMap\$importMaps
‪array $importMaps
Definition: ImportMap.php:37
‪TYPO3\CMS\Core\Page\ImportMap\dispatchResolveJavaScriptImportEvent
‪dispatchResolveJavaScriptImportEvent(string $specifier, bool $loadImportConfiguration=true)
Definition: ImportMap.php:307
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Page\ImportMap\loadDependency
‪loadDependency(string $packageName)
Definition: ImportMap.php:272
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:51
‪TYPO3\CMS\Core\Page\ImportMap\getImportMaps
‪getImportMaps()
Definition: ImportMap.php:142
‪TYPO3\CMS\Core\Core\Environment\getContext
‪static getContext()
Definition: Environment.php:128
‪TYPO3\CMS\Core\Page\Event\ResolveJavaScriptImportEvent
Definition: ResolveJavaScriptImportEvent.php:24
‪TYPO3\CMS\Core\Page\ImportMap\render
‪render(string $urlPrefix, null|string|ConsumableString $nonce)
Definition: ImportMap.php:116
‪TYPO3\CMS\Core\Page\ImportMap\includeAllImports
‪includeAllImports()
Definition: ImportMap.php:54