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. Eddy doesn't set this up for you: the configurations the installer generates name only your extension's type, so bundling another extension takes a few manual steps, which this page walks through.
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, so a site that installs a release of yours doesn't get the test-only extensions. Without PHPUnit, the installer removes tests/ and its .gitattributes entry, so 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, and the paths on this page that start with web/ are relative to it. It 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 below 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. Drupal core relies on this too: the System module holds dozens of test themes.
So there's no mapping step. Adding one anyway, such as 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 doesn't look inside hidden directories or 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 but not for your development site, so put one you also need on the site in your project'smodules/orthemes/directory instead. - 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 suggest and the extensions of the install profile, and nothing else. To enable a bundled extension, add a provision-*.sh lifecycle script to scripts/, which runs after your extension is enabled:
#!/usr/bin/env bash
##
# Enable the module that the theme bundles.
set -eu
build/vendor/bin/drush -l "$(vendor/bin/eddy-info site-url)" pm:enable my_theme_blocks -y
Make the script executable with chmod +x. For a bundled theme, use theme:enable instead of pm:enable.
A theme can't list a bundled module in its dependencies. provision enables the theme before your script runs, and Drupal stops with an error when a theme's modules aren't installed yet. Leave the module out of dependencies and enable it from the script. Without the dependency, a site that installs a release of your theme doesn't get the module enabled either, so 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_test'];
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, and everything you bundle sits under your extension inside it. That decides what runs without changes:
- PHPCS, PHPStan, Rector, Twig CS Fixer, ESLint and Stylelint check your bundled extensions along with your own code, and CSpell checks the whole project apart from hidden paths.
- PHPUnit runs the tests in your extension's
tests/srconly. A bundled extension's own tests run after you add their directories to the testsuites ofphpunit.xml, such asweb/themes/custom/*/modules/*/tests/src/Unit, and tophpunit.d10.xmlwhen you target Drupal 10. Add the matchingtestsdirectory to the<exclude>of the coverage source as well, so the test classes don't count as uncovered code. - PHPUnit counts the
.phpfiles of a bundled extension toward coverage, and 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. A bundled theme's.themefile in a module, or a bundled module's.moduleand.installfiles in a theme, count only after you add a<directory suffix=".theme">,<directory suffix=".module">or<directory suffix=".install">line for them. - 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 it, such asweb/themes/custom/my_theme/modules, todirs.
assemble copies these configuration files into build, so run it again after you edit one.
The PHP and JavaScript tools check nothing outside your extension's type directory in the site, and CSpell checks only your project, so an extension of the other type that comes in through suggest can't fail your checks.