Problem/Motivation

Drupal attempts reads under node_modules and emits warnings from StaticReflectionParser (file_get_contents()), even though ignore list includes node_modules. This did not happen up to 11.2.10.

Steps to reproduce

  1. Drupal 11.3.1 site.
  2. Custom theme themes/custom/mytheme contains a node_modules/ directory.
  3. Ensure in settings.php:
  4. $settings['file_scan_ignore_directories'] = ['node_modules', 'bower_components'];
  5. Run drush cr (or rebuild caches).
  6. Observe many warnings like:
  7. file_get_contents(themes/custom/mytheme/node_modules/...) Permission denied … StaticReflectionParser.php:183

Proposed resolution

add node module's to the disallow list.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3564112

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

dotoree created an issue. See original summary.

quietone’s picture

Version: 11.3.x-dev » 11.x-dev
Issue summary: View changes

Hi, in Drupal core changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies. Thanks.

I am restoring standard issue template so we can track progress on this issue.

@dotoree, There are instructions for rebasing a merge request. on drupal.org.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

berdir’s picture

Do you have a backtrace for this error to see where it originates?

11.3 supports OOP hooks for themes, so that could be related, but that's only attributes, not annotations?

Instead of this, it should probably be higher up and respect the scan setting

dotoree’s picture

@berdir here is the backtrace:

[13-Feb-2026 12:10:16 UTC] file_get_contents() FAILED for: themes/custom/my_site/node_modules/.pnpm/@dabh+diagnostics@2.0.8/node_modules/@so-ric/colorspace
[13-Feb-2026 12:10:16 UTC] Array
(
    [0] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\lib\Drupal\Component\Annotation\Doctrine\StaticReflectionParser.php
            [line] => 334
            [function] => parse
            [class] => Drupal\Component\Annotation\Doctrine\StaticReflectionParser
            [type] => ->
        )

    [1] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\lib\Drupal\Core\Hook\ThemeHookCollectorPass.php
            [line] => 264
            [function] => getMethodAttributes
            [class] => Drupal\Component\Annotation\Doctrine\StaticReflectionParser
            [type] => ->
        )

    [2] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\lib\Drupal\Core\Hook\ThemeHookCollectorPass.php
            [line] => 170
            [function] => collectThemeHookImplementations
            [class] => Drupal\Core\Hook\ThemeHookCollectorPass
            [type] => ->
        )

    [3] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\lib\Drupal\Core\Hook\ThemeHookCollectorPass.php
            [line] => 77
            [function] => collectAllHookImplementations
            [class] => Drupal\Core\Hook\ThemeHookCollectorPass
            [type] => ::
        )

    [4] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\symfony\dependency-injection\Compiler\Compiler.php
            [line] => 73
            [function] => process
            [class] => Drupal\Core\Hook\ThemeHookCollectorPass
            [type] => ->
        )

    [5] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\symfony\dependency-injection\ContainerBuilder.php
            [line] => 820
            [function] => compile
            [class] => Symfony\Component\DependencyInjection\Compiler\Compiler
            [type] => ->
        )

    [6] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\lib\Drupal\Core\DrupalKernel.php
            [line] => 1519
            [function] => compile
            [class] => Symfony\Component\DependencyInjection\ContainerBuilder
            [type] => ->
        )

    [7] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\lib\Drupal\Core\DrupalKernel.php
            [line] => 1028
            [function] => compileContainer
            [class] => Drupal\Core\DrupalKernel
            [type] => ->
        )

    [8] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\lib\Drupal\Core\DrupalKernel.php
            [line] => 524
            [function] => initializeContainer
            [class] => Drupal\Core\DrupalKernel
            [type] => ->
        )

    [9] => Array
        (
            [file] => C:\laragon\www\my_site\public_html\core\includes\utility.inc
            [line] => 34
            [function] => boot
            [class] => Drupal\Core\DrupalKernel
            [type] => ->
        )

    [10] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\drush\drush\src\Commands\core\CacheRebuildCommands.php
            [line] => 65
            [function] => drupal_rebuild
        )

    [11] => Array
        (
            [function] => rebuild
            [class] => Drush\Commands\core\CacheRebuildCommands
            [type] => ->
        )

    [12] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\consolidation\annotated-command\src\CommandProcessor.php
            [line] => 276
            [function] => call_user_func_array
        )

    [13] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\consolidation\annotated-command\src\CommandProcessor.php
            [line] => 212
            [function] => runCommandCallback
            [class] => Consolidation\AnnotatedCommand\CommandProcessor
            [type] => ->
        )

    [14] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\consolidation\annotated-command\src\CommandProcessor.php
            [line] => 175
            [function] => validateRunAndAlter
            [class] => Consolidation\AnnotatedCommand\CommandProcessor
            [type] => ->
        )

    [15] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\consolidation\annotated-command\src\AnnotatedCommand.php
            [line] => 387
            [function] => process
            [class] => Consolidation\AnnotatedCommand\CommandProcessor
            [type] => ->
        )

    [16] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\symfony\console\Command\Command.php
            [line] => 341
            [function] => execute
            [class] => Consolidation\AnnotatedCommand\AnnotatedCommand
            [type] => ->
        )

    [17] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\symfony\console\Application.php
            [line] => 1102
            [function] => run
            [class] => Symfony\Component\Console\Command\Command
            [type] => ->
        )

    [18] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\drush\drush\src\Application.php
            [line] => 201
            [function] => doRunCommand
            [class] => Symfony\Component\Console\Application
            [type] => ->
        )

    [19] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\symfony\console\Application.php
            [line] => 356
            [function] => doRunCommand
            [class] => Drush\Application
            [type] => ->
        )

    [20] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\symfony\console\Application.php
            [line] => 195
            [function] => doRun
            [class] => Symfony\Component\Console\Application
            [type] => ->
        )

    [21] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\drush\drush\src\Runtime\Runtime.php
            [line] => 113
            [function] => run
            [class] => Symfony\Component\Console\Application
            [type] => ->
        )

    [22] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\drush\drush\src\Runtime\Runtime.php
            [line] => 40
            [function] => doRun
            [class] => Drush\Runtime\Runtime
            [type] => ->
        )

    [23] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\drush\drush\drush.php
            [line] => 140
            [function] => run
            [class] => Drush\Runtime\Runtime
            [type] => ->
        )

    [24] => Array
        (
            [file] => C:\laragon\www\my_site\vendor\bin\drush.php
            [line] => 119
            [args] => Array
                (
                    [0] => C:\laragon\www\my_site\vendor\drush\drush\drush.php
                )

            [function] => include
        )

)

and some errors:

[warning] file_get_contents(themes/custom/my_site/node_modules/.pnpm/yargs@17.7.2/node_modules/cliui): Failed to open stream: Permission denied StaticReflectionParser.php:184
[warning] file_get_contents(themes/custom/my_site/node_modules/.pnpm/yargs@17.7.2/node_modules/escalade): Failed to open stream: Permission denied StaticReflectionParser.php:184
[warning] file_get_contents(themes/custom/my_site/node_modules/@alpinejs/intersect): Failed to open stream: Permission denied StaticReflectionParser.php:184
[warning] file_get_contents(themes/custom/my_site/node_modules/@tailwindcss/forms): Failed to open stream: Permission denied StaticReflectionParser.php:184
[warning] file_get_contents(themes/custom/my_site/node_modules/@tailwindcss/typography): Failed to open stream: Permission denied StaticReflectionParser.php:184
berdir’s picture

Assigned: Unassigned » nicxvan

Ok, so it is confirmed to be Drupal\Core\Hook\ThemeHookCollectorPass.

That's where we should look into this. \Drupal\Core\Hook\ThemeHookCollectorPass::filterIterator and the ModuleHookCollectorPass() should respect the file scan setting. Probably doesn't hurt to add your extra check too, but I think that alone won't fix the possibly considerably performance issue here.

Using the Assigned field to ping @nicxvan.

nicxvan’s picture

Component: file system » extension system

Yeah we should add node module's to the disallow list.

https://git.drupalcode.org/project/drupal/-/blob/main/core/lib/Drupal/Co...
https://git.drupalcode.org/project/drupal/-/blob/main/core/lib/Drupal/Co...

Those are the two places we need to update.

nicxvan’s picture

Assigned: nicxvan » Unassigned

nicxvan changed the visibility of the branch 3564112-cache-rebuild-triggers to hidden.

nicxvan changed the visibility of the branch 11.x to hidden.

nicxvan’s picture

Issue summary: View changes

I'm going to clean up the mr since it's against 11.3 and doesn't follow the approach we should take.

nicxvan changed the visibility of the branch 11.3.x to hidden.

richard.thomas made their first commit to this issue’s fork.

richard.thomas’s picture

After updating to 11.3, we noticed significant performance degradation when clearing caches on a development site with a very large node_modules folder inside our site theme.

It looks to be due to the theme hook scanning going through the entire folder structure.

I've pushed up a simple MR https://git.drupalcode.org/project/drupal/-/merge_requests/15629 to make both HookCollectorPass and ThemeHookCollectorPass respect the file_scan_ignore_directories setting by merging that into the default list of excluded directories they had.

Doesn't seem to cause any issues with the existing tests and my local testing shows a big speedup (~30sec down to ~4sec for a cache rebuild with a populated node_modules directory that is configured to be ignored).

I'll see if I can add a specific test for the ignore directories option.

richard.thomas’s picture

Status: Active » Needs review

I've added a couple of tests to https://git.drupalcode.org/project/drupal/-/merge_requests/15629 to check that directories configured in settings are ignored for both theme and module hook collection.

berdir’s picture

Status: Needs review » Needs work

This looks good I think, not sure extensive the tests should be, we're about to deprecate .theme files on main, which would require at least a small change either here.

Maybe we can just add a SkipDeprecations attribute and a todo to remove in D13 to the tests and test modules as I *think* we can remove this when we remove BC as we know all extension directories already so we will _only_ need to look at their src/Hooks folder then, which should significantly simplify this in D13 as we can directly and only process that directory.

nicxvan changed the visibility of the branch main to hidden.

nicxvan’s picture

Yeah, I am tempted to just hardcode node_modules and bower_components.

The ignore file setting is for a different purpose and I'm not sure we should conflate this.

I'm not super convinced as I write this, so I think it's worth discussing.

richard.thomas’s picture

I'd definitely prefer to give developers the option to configure additional exclusions (or remove the defaults). We could add an additional setting I guess, but I feel like the existing file_scan_ignore_directories would cover the same use-case.

The description in default.settings.php is:

The default list of directories that will be ignored by Drupal's file API.

By default ignore node_modules and bower_components folders to avoid issues
with common frontend tools and recursive scanning of directories looking for
extensions.

It does mention extensions, but I think it's serving much the same purpose as what we need here (marking directories that won't have any Drupal code in them so don't bother recursively scanning them).

nicxvan’s picture

I think you're right, but let's also take a moment to update the comment there too then.

richard.thomas’s picture

Status: Needs work » Needs review

I've had a go at updating the comment in the default.settings.php file to better reflect what the setting is doing.

I've also marked the new tests with #[IgnoreDeprecations] for when procedural hooks are deprecated and added a TODO comment to the new test theme/module files to remove them when procedural hooks are removed.

nicxvan’s picture

Status: Needs review » Needs work

I have a few more comments on the MR, this is getting close! Thanks for working on this!

Here is the gist of the changes requested:
The comment should preserve the bit about the file api since that is still accurate
We only need to test the ignore directory bit since we already have tests for collection hooks, so we can drop the included_directory directories
Don't worry about the deprecation ignore, we'll add that when necessary if that issue gets in first, and we would do it by adding the attribute, if this gets in first I'll update the other issue.