@@ -50,6 +50,13 @@ final class ThemeNamespaceRegistry {
5050 */
5151 private array $ warnedNamespaces = [];
5252
53+ /**
54+ * Protected default namespaces keyed by namespace.
55+ *
56+ * @var array<string, array{name: string, type: string}>|null
57+ */
58+ private ?array $ protectedNamespaces = NULL ;
59+
5360 /**
5461 * Creates the registry.
5562 */
@@ -316,18 +323,81 @@ private function resolvePath(Extension $theme, string $path): string {
316323 }
317324
318325 /**
319- * Returns whether the namespace would shadow a default Drupal namespace.
326+ * Returns protected default namespaces keyed by namespace.
327+ *
328+ * @return array<string, array{name: string, type: string}>
329+ * Protected default namespace owner metadata.
320330 */
321- private function isProtectedNamespace (string $ namespace , string $ definingThemeName ): bool {
322- if ($ namespace === $ definingThemeName ) {
331+ private function getProtectedNamespaces (): array {
332+ return $ this ->protectedNamespaces ??= $ this ->buildProtectedNamespaces ();
333+ }
334+
335+ /**
336+ * Builds protected default namespaces for installed modules and themes.
337+ *
338+ * Extensions may opt into reuse of their default namespace via
339+ * `components.allow_default_namespace_reuse` or by defining a matching
340+ * default namespace under `components.namespaces`.
341+ *
342+ * @return array<string, array{name: string, type: string}>
343+ * Protected default namespace owner metadata.
344+ */
345+ private function buildProtectedNamespaces (): array {
346+ $ protectedNamespaces = [];
347+
348+ foreach ($ this ->moduleExtensionList ->getList () as $ extensionName => $ extension ) {
349+ if (!$ extension instanceof Extension || $ this ->allowsDefaultNamespaceReuse ($ extension )) {
350+ continue ;
351+ }
352+
353+ $ protectedNamespaces [$ extensionName ] = [
354+ 'name ' => (string ) ($ extension ->info ['name ' ] ?? $ extensionName ),
355+ 'type ' => 'module ' ,
356+ ];
357+ }
358+
359+ // Themes win ties to match Drupal's existing namespace precedence.
360+ foreach ($ this ->themeExtensionList ->getList () as $ extensionName => $ extension ) {
361+ if (!$ extension instanceof Extension || $ this ->allowsDefaultNamespaceReuse ($ extension )) {
362+ continue ;
363+ }
364+
365+ $ protectedNamespaces [$ extensionName ] = [
366+ 'name ' => (string ) ($ extension ->info ['name ' ] ?? $ extensionName ),
367+ 'type ' => 'theme ' ,
368+ ];
369+ }
370+
371+ return $ protectedNamespaces ;
372+ }
373+
374+ /**
375+ * Returns whether an extension allows reuse of its default namespace.
376+ */
377+ private function allowsDefaultNamespaceReuse (Extension $ extension ): bool {
378+ $ components = $ extension ->info ['components ' ] ?? NULL ;
379+ if (!is_array ($ components )) {
323380 return FALSE ;
324381 }
325382
326- if (isset ($ this ->themeExtensionList ->getList ()[$ namespace ])) {
383+ // Mirror drupal/components, where presence of the key opts in.
384+ if (array_key_exists ('allow_default_namespace_reuse ' , $ components )) {
327385 return TRUE ;
328386 }
329387
330- return isset ($ this ->moduleExtensionList ->getList ()[$ namespace ]);
388+ $ definitions = $ components ['namespaces ' ] ?? NULL ;
389+ return is_array ($ definitions ) && !empty ($ definitions [$ extension ->getName ()]);
390+ }
391+
392+ /**
393+ * Returns whether the namespace would shadow a protected default namespace.
394+ */
395+ private function isProtectedNamespace (string $ namespace , string $ definingThemeName ): bool {
396+ if ($ namespace === $ definingThemeName ) {
397+ return FALSE ;
398+ }
399+
400+ return isset ($ this ->getProtectedNamespaces ()[$ namespace ]);
331401 }
332402
333403 /**
@@ -375,14 +445,9 @@ private function logMissingPath(string $themeName, string $namespace, string $pa
375445 * The owner type and human-readable name.
376446 */
377447 private function getProtectedNamespaceOwner (string $ namespace ): array {
378- $ theme = $ this ->themeExtensionList ->getList ()[$ namespace ] ?? NULL ;
379- if ($ theme instanceof Extension) {
380- return ['theme ' , (string ) ($ theme ->info ['name ' ] ?? $ namespace )];
381- }
382-
383- $ module = $ this ->moduleExtensionList ->getList ()[$ namespace ] ?? NULL ;
384- if ($ module instanceof Extension) {
385- return ['module ' , (string ) ($ module ->info ['name ' ] ?? $ namespace )];
448+ $ owner = $ this ->getProtectedNamespaces ()[$ namespace ] ?? NULL ;
449+ if (is_array ($ owner )) {
450+ return [$ owner ['type ' ], $ owner ['name ' ]];
386451 }
387452
388453 return ['extension ' , $ namespace ];
0 commit comments