‪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 
53  public function ‪includeAllImports(): void
54  {
55  $this->extensionsToLoad['*'] = true;
56  }
57 
58  public function ‪includeTaggedImports(string $tag): void
59  {
60  if (isset($this->extensionsToLoad['*'])) {
61  return;
62  }
63 
64  foreach ($this->‪getImportMaps() as $package => $config) {
65  $tags = $config['tags'] ?? [];
66  if (in_array($tag, $tags, true)) {
67  $this->‪loadDependency($package);
68  }
69  }
70  }
71 
72  public function ‪includeImportsFor(string $specifier): void
73  {
74  if (!isset($this->extensionsToLoad['*'])) {
75  $this->‪resolveImport($specifier, true);
76  } else {
77  $this->‪dispatchResolveJavaScriptImportEvent($specifier, true);
78  }
79  }
80 
81  public function ‪resolveImport(
82  string $specifier,
83  bool $loadImportConfiguration = true
84  ): ?string {
85  $resolution = $this->‪dispatchResolveJavaScriptImportEvent($specifier, $loadImportConfiguration);
86  if ($resolution !== null) {
87  return $resolution;
88  }
89 
90  foreach (array_reverse($this->‪getImportMaps()) as $package => $config) {
91  $imports = $config['imports'] ?? [];
92  if (isset($imports[$specifier])) {
93  if ($loadImportConfiguration) {
94  $this->‪loadDependency($package);
95  }
96  return $imports[$specifier];
97  }
98 
99  $specifierParts = explode('/', $specifier);
100  $specifierPartCount = count($specifierParts);
101  for ($i = 1; $i < $specifierPartCount; ++$i) {
102  $prefix = implode('/', array_slice($specifierParts, 0, $i)) . '/';
103  if (isset($imports[$prefix])) {
104  if ($loadImportConfiguration) {
105  $this->‪loadDependency($package);
106  }
107  return $imports[$prefix] . implode(array_slice($specifierParts, $i));
108  }
109  }
110  }
111 
112  return null;
113  }
114 
115  public function ‪render(
116  string $urlPrefix,
117  null|string|‪ConsumableNonce $nonce
118  ): string {
119  if (count($this->extensionsToLoad) === 0 || count($this->‪getImportMaps()) === 0) {
120  return '';
121  }
122 
123  $html = [];
124 
125  $importMap = $this->‪composeImportMap($urlPrefix);
126  $json = json_encode(
127  $importMap,
128  JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_THROW_ON_ERROR
129  );
130  $nonceAttr = $nonce !== null ? ' nonce="' . htmlspecialchars((string)$nonce) . '"' : '';
131  $html[] = sprintf('<script type="importmap"%s>%s</script>', $nonceAttr, $json);
132 
133  return implode(PHP_EOL, $html) . PHP_EOL;
134  }
135 
136  public function ‪warmupCaches(): void
137  {
138  $this->‪computeImportMaps();
139  }
140 
141  protected function ‪getImportMaps(): array
142  {
143  return $this->importMaps ?? $this->‪getFromCache() ?? $this->‪computeImportMaps();
144  }
145 
146  protected function ‪getFromCache(): ?array
147  {
148  if ($this->cache === null) {
149  return null;
150  }
151  if (!$this->cache->has($this->cacheIdentifier)) {
152  return null;
153  }
154  $this->importMaps = $this->cache->get($this->cacheIdentifier);
155  return ‪$this->importMaps;
156  }
157 
158  protected function ‪computeImportMaps(): array
159  {
160  $extensionVersions = [];
161  ‪$importMaps = [];
162  foreach ($this->packages as $package) {
163  $configurationFile = $package->getPackagePath() . 'Configuration/JavaScriptModules.php';
164  if (!is_readable($configurationFile)) {
165  continue;
166  }
167  $extensionVersions[$package->getPackageKey()] = implode(':', [
168  $package->getPackageKey(),
169  $package->getPackageMetadata()->getVersion(),
170  ]);
171  $packageConfiguration = require($configurationFile);
172  ‪$importMaps[$package->getPackageKey()] = $packageConfiguration ?? [];
173  }
174 
175  $isDevelopment = ‪Environment::getContext()->isDevelopment();
176  if ($isDevelopment) {
177  $bust = (string)‪$GLOBALS['EXEC_TIME'];
178  } else {
179  $bust = ‪GeneralUtility::hmac(
180  ‪Environment::getProjectPath() . implode('|', $extensionVersions)
181  );
182  }
183 
184  foreach (‪$importMaps as $packageName => $config) {
185  ‪$importMaps[$packageName]['imports'] = $this->‪resolvePaths(
186  $config['imports'] ?? [],
187  $this->bustSuffix ? $bust : null
188  );
189  }
190 
191  $this->importMaps = ‪$importMaps;
192  if ($this->cache !== null) {
193  $this->cache->set($this->cacheIdentifier, ‪$importMaps);
194  }
195  return ‪$importMaps;
196  }
197 
198  protected function ‪resolveRecursiveImportMap(
199  string $prefix,
200  string $path,
201  array $exclude,
202  string $bust
203  ): array {
204  $path = GeneralUtility::getFileAbsFileName($path);
205  if (!$path || @!is_dir($path)) {
206  return [];
207  }
208  $exclude = array_map(
209  static fn(string $excludePath): string => GeneralUtility::getFileAbsFileName($excludePath),
210  $exclude
211  );
212 
213  $fileIterator = new \RegexIterator(
214  new \RecursiveIteratorIterator(
215  new \RecursiveDirectoryIterator($path)
216  ),
217  '#^' . preg_quote($path, '#') . '(.+\.js)$#',
218  \RegexIterator::GET_MATCH
219  );
220 
221  $map = [];
222  foreach ($fileIterator as $match) {
223  $fileName = $match[0];
224  $specifier = $prefix . ($match[1] ?? '');
225 
226  // @todo: Abstract into an iterator?
227  foreach ($exclude as $excludedPath) {
228  if (str_starts_with($fileName, $excludedPath)) {
229  continue 2;
230  }
231  }
232 
233  $webPath = ‪PathUtility::getAbsoluteWebPath($fileName, false) . '?bust=' . $bust;
234 
235  $map[$specifier] = $webPath;
236  }
237 
238  return $map;
239  }
240 
241  protected function ‪resolvePaths(
242  array $imports,
243  string $bust = null
244  ): array {
245  $cacheBustingSpecifiers = [];
246  foreach ($imports as $specifier => $address) {
247  if (str_ends_with($specifier, '/')) {
248  $path = is_array($address) ? ($address['path'] ?? '') : $address;
249  $exclude = is_array($address) ? ($address['exclude'] ?? []) : [];
250 
252  $cacheBusted = preg_match('#[^/]@#', $path) === 1;
253  if ($bust !== null && !$cacheBusted) {
254  // Resolve recursive importmap in order to add a bust suffix
255  // to each file.
256  $cacheBustingSpecifiers[] = $this->‪resolveRecursiveImportMap($specifier, $path, $exclude, $bust);
257  }
258  } else {
260  $cacheBusted = preg_match('#[^/]@#', $address) === 1;
261  if ($bust !== null && !$cacheBusted) {
262  ‪$url .= '?bust=' . $bust;
263  }
264  }
265  $imports[$specifier] = ‪$url;
266  }
267 
268  return $imports + array_merge(...$cacheBustingSpecifiers);
269  }
270 
271  protected function ‪loadDependency(string $packageName): void
272  {
273  if (isset($this->extensionsToLoad[$packageName])) {
274  return;
275  }
276 
277  $this->extensionsToLoad[$packageName] = true;
278  $dependencies = $this->‪getImportMaps()[$packageName]['dependencies'] ?? [];
279  foreach ($dependencies as $dependency) {
280  $this->‪loadDependency($dependency);
281  }
282  }
283 
284  protected function ‪composeImportMap(string $urlPrefix): array
285  {
286  ‪$importMaps = $this->‪getImportMaps();
287 
288  if (!isset($this->extensionsToLoad['*'])) {
289  ‪$importMaps = array_intersect_key(‪$importMaps, $this->extensionsToLoad);
290  }
291 
292  $importMap = [];
293  foreach (‪$importMaps as $singleImportMap) {
294  ArrayUtility::mergeRecursiveWithOverrule($importMap, $singleImportMap);
295  }
296  unset($importMap['dependencies']);
297  unset($importMap['tags']);
298 
299  foreach ($importMap['imports'] ?? [] as $specifier => ‪$url) {
300  $importMap['imports'][$specifier] = $urlPrefix . ‪$url;
301  }
302 
303  return $importMap;
304  }
305 
307  string $specifier,
308  bool $loadImportConfiguration = true
309  ): ?string {
310  if ($this->eventDispatcher === null) {
311  return null;
312  }
313 
314  return $this->eventDispatcher->dispatch(
315  new ‪ResolveJavaScriptImportEvent($specifier, $loadImportConfiguration, $this)
316  )->resolution;
317  }
318 
322  public function ‪updateState(array $state): void
323  {
324  $this->extensionsToLoad = $state['extensionsToLoad'] ?? [];
325  }
326 
330  public function ‪getState(): array
331  {
332  return [
333  'extensionsToLoad' => ‪$this->extensionsToLoad,
334  ];
335  }
336 }
‪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\Security\ContentSecurityPolicy\ConsumableNonce
Definition: ConsumableNonce.php:24
‪TYPO3\CMS\Core\Page\ImportMap\render
‪render(string $urlPrefix, null|string|ConsumableNonce $nonce)
Definition: ImportMap.php:115
‪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:284
‪TYPO3\CMS\Core\Page\ImportMap\includeTaggedImports
‪includeTaggedImports(string $tag)
Definition: ImportMap.php:58
‪TYPO3\CMS\Core\Package\PackageInterface
Definition: PackageInterface.php:24
‪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:241
‪TYPO3\CMS\Core\Core\Environment\getProjectPath
‪static string getProjectPath()
Definition: Environment.php:160
‪TYPO3\CMS\Core\Page\ImportMap\warmupCaches
‪warmupCaches()
Definition: ImportMap.php:136
‪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:81
‪TYPO3\CMS\Core\Page\ImportMap\getFromCache
‪getFromCache()
Definition: ImportMap.php:146
‪TYPO3\CMS\Core\Utility\GeneralUtility\hmac
‪static string hmac($input, $additionalSecret='')
Definition: GeneralUtility.php:474
‪TYPO3\CMS\Core\Page\ImportMap\computeImportMaps
‪computeImportMaps()
Definition: ImportMap.php:158
‪TYPO3\CMS\Core\Page\ImportMap\getState
‪getState()
Definition: ImportMap.php:330
‪TYPO3\CMS\Core\Page\ImportMap\resolveRecursiveImportMap
‪resolveRecursiveImportMap(string $prefix, string $path, array $exclude, string $bust)
Definition: ImportMap.php:198
‪TYPO3\CMS\Core\Page\ImportMap\includeImportsFor
‪includeImportsFor(string $specifier)
Definition: ImportMap.php:72
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Page\ImportMap\updateState
‪updateState(array $state)
Definition: ImportMap.php:322
‪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:306
‪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:271
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Page\ImportMap\getImportMaps
‪getImportMaps()
Definition: ImportMap.php:141
‪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\includeAllImports
‪includeAllImports()
Definition: ImportMap.php:53