Bundled extensions
Your project is 1 extension, and it can bundle others. A theme can bring a module that provides its blocks, and a module can bring a theme for its tests. Either can carry extensions that only its tests use. The generated tool configurations name only the custom directory of your extension's type, so a bundled extension takes a few manual steps. This page covers where bundled extensions go, how to enable them on your site and in tests, and what the tooling checks.
Where they go
Put each bundled extension in a directory of its own. The following table lists the directories, relative to the root of your project:
| Directory | What goes there | In a release |
|---|---|---|
modules/<name>/ | A module that ships with your extension | Yes |
themes/<name>/ | A theme that ships with your extension | Yes |
tests/modules/<name>/ | A module that only your tests use | No |
tests/themes/<name>/ | A theme that only your tests use | No |
While you keep PHPUnit, .gitattributes keeps tests/ out of the release archives that Drupal.org packages. A site that installs a release of yours then gets no test-only extensions. Without PHPUnit, the installer removes tests/ and its .gitattributes entry. A tests/ directory you add later ships in your releases until you add tests export-ignore to .gitattributes.
How Drupal finds them
assemble assembles the site in the build directory. The paths on this page that start with web/ are relative to build. assemble creates your extension's directory at web/modules/custom/<your_extension>, or web/themes/custom/<your_extension> for a theme. The directory holds a symlink to every top-level item of your project apart from build, vendor and hidden items, so a bundled extension arrives inside yours.
Drupal finds extensions at any depth under the site's web/modules and web/themes directories, and it reads each one's type from its own .info.yml. A module inside a theme's directory is found as a module, and a theme inside a module's directory as a theme.
Add no mapping of your own. A second symlink to the same module in web/modules/custom makes Drupal find the module twice and keep 1 copy, with no warning.
A few limits apply:
- Drupal skips hidden directories and directories named
src,lib,vendor,assets,css,files,images,js,misc,templates,includes,fixtures,Drupalorconfig. The site's settings addnode_modulesandbower_componentsto that list. A bundled extension can't live in any of them. - Outside test runs, Drupal skips
tests/directories, unless$settings['extension_discovery_scan_tests']isTRUE. A test-only extension exists for PHPUnit only. Put one that your development site needs too in your project'smodules/orthemes/directory. - Machine names are shared by the whole site, so give a bundled extension a name no other extension uses. Of 2 extensions of the same type that share 1 name, Drupal keeps 1 and drops the other with no warning. When a module and a theme share 1 name, the one that installs second fails.
Enable a bundled extension on your site
provision enables your extension, the drupal/* extensions in the suggest section of composer.json and the extensions of the install profile. A provision-*.sh lifecycle script in scripts/ runs after that, so it can enable a bundled extension.
To enable a bundled module from a lifecycle script:
-
Create
scripts/provision-bundled.shwith the following content:scripts/provision-bundled.sh#!/usr/bin/env bash### Enable the module that the theme bundles.set -eubuild/vendor/bin/drush -l "$(vendor/bin/eddy-info site-url)" pm:enable my_theme_blocks -yFor a bundled theme, use
theme:enablein place ofpm:enable. -
Make the script executable with
chmod +x scripts/provision-bundled.sh. -
Run
provisionagain withmake provision,ahoy provisionorvendor/bin/eddy-provision. The script runs at the end of everyprovision, including the one thatbuildruns.provisionreinstalls the site, so to keep the content you added, runscripts/provision-bundled.shon its own from the project root. -
Confirm that the module is enabled. The following commands list the enabled modules, 1 per setup. The list includes
my_theme_blocks:make drush -- pm:list --status=enabled --type=moduleahoy drush pm:list --status=enabled --type=modulebuild/vendor/bin/drush -l "$(vendor/bin/eddy-info site-url)" pm:list --status=enabled --type=module
Leave a bundled module out of your theme's dependencies. provision enables the theme before your script runs, and Drupal stops with an error when a theme's modules aren't installed yet. Without the dependency, a site that installs a release of your theme gets the module but doesn't enable it. Tell the maintainers of those sites to enable the module, such as in your README.
Use a bundled extension in tests
Test runs find extensions under tests/ too, so a test uses a bundled extension the way it uses any other. A Kernel test lists a bundled module in $modules:
protected static $modules = ['my_theme_blocks'];
To install a bundled theme in a Kernel test:
-
List
systemin$modules. -
Call
$this->installConfig(['system'])insetUp(). -
Install the theme through the theme installer, in
setUp()after theinstallConfig()call or in a test method:$this->container->get('theme_installer')->install(['my_module_test_theme']);
A Functional test can also set $defaultTheme to a bundled theme.
What the tooling covers
The tool configurations name only the custom directory of your extension's type, web/modules/custom or web/themes/custom. Everything you bundle sits under your extension inside that directory. The tools cover bundled extensions as follows:
- PHPCS, PHPStan, Rector, Twig CS Fixer, ESLint and Stylelint check your bundled extensions along with your own code. CSpell checks the whole project apart from hidden paths.
- PHPUnit runs the tests in your extension's
tests/srconly. To run a bundled extension's own tests, add their directories to the testsuites ofphpunit.xml, such asweb/themes/custom/*/modules/*/tests/src/Unit. When you target Drupal 10, add them tophpunit.d10.xmltoo. Also add the matchingtestsdirectory to the<exclude>of the coverage source, so the test classes don't count as uncovered code. - PHPUnit counts the
.phpfiles of a bundled extension toward coverage. It also counts the hook files of a bundled extension of your own type:.moduleand.installin a module,.themein a theme. Other files, such as.incfiles, don't count. For the hook files of the other type, add a<directory suffix=".theme">,<directory suffix=".module">or<directory suffix=".install">line to the coverage source. - Jest runs the
*.test.jsfiles in thejs/directory of each extension directly inside thedirsofjest.config.js. To run a bundled extension's tests, add the directory that holds the extension, such asweb/themes/custom/my_theme/modules, todirs.
assemble copies these configuration files into build, so run assemble again after you edit one (see Build the site).
The PHP and JavaScript tools check only the custom directory of your extension's type, and CSpell checks only your project. So an extension of the other type that suggest installs can't fail your checks.