This document captures the design for letting users extend Quick with
their own starterkit themes and recipes — the same way the first-party
dq_starterkit and bundled recipes work — without editing anything inside
vendor/.
It is a design note, not yet implemented. It also lists the groundwork that must land before any of this work begins.
A user should be able to publish a recipe or starterkit as a normal Composer
package and have Quick pick it up — ideally just by composer require-ing
it and naming it in config.dq.yml, with no edits to Quick's own files.
Status: done. The starterkit (
drupal-quick/dq_starterkit) and the recipes (recipe-blog,recipe-project) now live in their own repos/packages, and external recipe path resolution is fixed. The steps below are kept as the record of that work; the design that follows builds on it.
This design assumes the theme and the non-bundled recipes already live in their own repositories/packages. The following were the prerequisites:
- Create a new repository, e.g.
github.com/Drupal-Quick/dq-starterkit. - Move
starterkits/dq_starterkit/*into it as acomposer.jsonof"type": "drupal-theme"nameddrupal-quick/dq-starterkit. - Keep the starter kit conventions already established: the
dq_starterkit.starterkit.ymlmarker, a concreteversion:indq_starterkit.info.yml(sogenerate-themeruns non-interactively), and the machine-name token (dq_starterkit) used throughout for substitution. - Decide where the design presets live — most naturally in the starterkit
package itself (as
presets/, declared in itspackage.jsondq.presets), so preset discovery reads them from the installed theme rather than from Quick. - Tag a release (e.g.
1.0.0) and publish to Packagist, or document the VCS install. A tag avoids thedev-mainconstraint consumers currently need. - In Quick, add the starterkit as a
require(or document requiring it). Composer's installer places it atweb/themes/contrib/dq_starterkit/, where Drupal discovers it natively — lettingdq:scaffolddrop the temporary web-root staging step and pointgenerate-themestraight at it.
- Create a repository per recipe, e.g.
github.com/Drupal-Quick/recipe-blog. - Move
recipes/blog/*into it as acomposer.jsonof"type": "drupal-recipe"nameddrupal-quick/recipe-blog, keepingrecipe.yml,config/, andtheme-assets/. - Tag a release and publish (or document the VCS URL).
- Update
templates/recipe-registry.json: remove"bundled": trueand set the realurl.dq-installalready handles VCS registration +composer requirefor non-bundled entries.
When the first non-bundled recipe ships, update resolvePath() in
ScaffoldCommand.php so external entries return an absolute path (built
from the project root) instead of a project-root-relative one. Bundled recipes
already resolve to absolute paths; external ones must too, or the path handed to
the drush recipe subprocess is fragile.
Once these three are done, Quick no longer ships a theme or recipes — it orchestrates installed ones. That is the foundation the rest of this design builds on.
Recipes that are not in the registry are passed straight through to
drush recipe as literal paths (resolvePath() returns the key unchanged). So a
user can already install their own recipe package with composer require and
reference its path in config.dq.yml — no registry entry required.
Two conveniences are currently gated on registry membership:
- Auto-install —
dq-installonly registers a VCS repo and runscomposer requirefor entries it finds in the registry. - Theme-asset injection —
copyThemeAssets()only runs whenisset($registry[$recipe]) && registry[$recipe]['theme_assets']. A user's recipe that ships atheme-assets/directory is silently ignored unless it is in the registry.
So a custom recipe technically works, but it is a second-class citizen.
The root problem is that templates/recipe-registry.json lives inside the
package. Users cannot add to it without editing vendor/, which is overwritten
on update. Two complementary mechanisms solve this.
The simple, transparent escape hatch:
sources:
my_recipe:
package: "acme/recipe-events"
url: "https://github.com/acme/recipe-events"
theme_assets: true
recipes:
- "my_recipe"dq-install and dq:scaffold would load the built-in registry and merge
these entries on top. Minimal change; users own their own entries.
The more Composer/Drupal-native approach — it mirrors how Drush commands and Drupal modules self-register:
{
"type": "drupal-recipe",
"extra": {
"drupal-quick": { "recipe": { "label": "Events", "theme_assets": true } }
}
}Quick discovers these by scanning vendor/composer/installed.json, which
is readable without a service container — so it fits the constraint
dq:scaffold already operates under (no reliable container; see why we use
Drush::bootstrapManager()->getRoot() instead of \Drupal::root()). With this,
no central registry entry is needed at all: the package is its own entry.
Mechanism (B) can only see packages that are already installed. If you want
Quick to auto-install a recipe from a short key (today's external flow),
it needs the package + url before installation — which only (A) or the
built-in registry can provide. The clean split:
- User runs
composer require acme/recipe-eventsthemselves → (B) auto-discovers it for application + theme-assets. Zero registry config. This is the truest "extend the same way." - User wants Quick to fetch it from a key → needs (A) or the curated built-in registry.
Recommendation: lean on (B) as the primary path and demote the built-in registry to a curated, first-party convenience. That removes the central file as a bottleneck entirely.
Same shape as recipes, with two specific changes:
-
Make the starterkit selectable.
dq:scaffoldcurrently hardcodes$starterkitId = 'dq_starterkit'. Expose it in config:theme: starterkit: "acme_starterkit" # any installed theme with a .starterkit.yml
generate-themealready accepts--starterkit, so this is a near-trivial wiring change. Once starterkits are installeddrupal-themepackages (see prerequisites), user starterkits land inweb/themes/contrib/and are discoverable identically — dropping even the staging step. -
Preset discovery should read from the selected starterkit's package (
package.jsondq.presets) rather than a Quick-owned directory.
Independent of everything above: change copyThemeAssets() to inject whenever
the resolved recipe path contains a theme-assets/ directory, regardless of
registry membership. This single change makes every recipe — first-party or
user-supplied — behave consistently.
- Skeleton repos or a generator for the two package types:
- a recipe package —
recipe.yml+ optionalconfig/+ optionaltheme-assets/, plus thecomposer.jsontype/extrafrom (B); - a starterkit theme package —
*.starterkit.yml+ the conventions already established fordq_starterkit. Drupal core already providesgenerate-themefor consuming starterkits; a documented template repo (or adrush dq:recipe-skeleton) covers the producing side.
- a recipe package —
config.dq.ymltemplate updates — a commentedsources:block and thetheme.starterkitkey, surfaced bydq-init(including the interactive wizard, which could list discovered starterkits/recipes frominstalled.json).- Optional
drush dq:registerhelper that writes asources:entry for an already-installed package, so users do not hand-edit YAML.
- Prerequisites (theme package, recipe packages, external path fix) — see above.
- Un-gate theme-asset injection from the registry (smallest, highest-value change; benefits first-party recipes immediately).
- Parameterize the starterkit (
theme.starterkitin config) and read preset discovery from the selected starterkit package. - Merge registry sources — add the
config.dq.ymlsources:section (A). - Auto-discovery via
composer.jsonextra+installed.json(B); make it the primary path and demote the built-in registry to curated convenience. - Templates / generators /
dq-initconfig additions to make authoring new packages turnkey.
None of this requires fighting the container constraint, since package discovery
reads installed.json. The biggest single unlock is self-describing packages
via composer.json extra — that is what turns "edit our central registry" into
"publish a package and it just works."