Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/dq-add-recipe/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ final class BlogHooks {
```

- The module needs a `dq_<name>.info.yml` (`type: module`,
`core_version_requirement: ^11.1.8`, `dependencies:` for node/views/etc.).
`core_version_requirement: ^11.3`, `dependencies:` for node/views/etc.).
- The namespace is the **module** machine name (`Drupal\dq_blog\Hook`) — no
`STARTERKIT` token; module namespaces are independent of the theme.
- Multiple recipes may implement the same hook — each submodule is a separate
Expand Down
6 changes: 4 additions & 2 deletions .agents/skills/dq-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ no dispatcher.

- Module namespace = the **module** machine name (`Drupal\dq_blog\Hook`), not the
theme — so no `STARTERKIT` token in module PHP.
- Recipe modules use **module** OOP preprocess hooks — introduced in Drupal
**11.2**, backported to **11.1.8** — so the stack floor is `^11.1.8`.
- Recipe modules use **module** OOP preprocess hooks (introduced in Drupal
11.2, backported to 11.1.8). The **stack floor is `^11.3`** — the tested
range is 11.3 and 11.4 (the scaffold auto-detects 11.4's consolidated `dr`
CLI and its stricter recipe validation is accounted for).
- The **starterkit** keeps its own presentation hooks procedural in `.theme`
(one impl each, no conflict); it does **not** use OOP theme hooks (those are
11.3+, and we don't need them).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ static: # optional, used by `drush dq:static`

## Requirements

PHP 8.1+ · Drupal 11.1.8+ (recipe modules use module preprocess OOP hooks) · Drush 12.5+ · Node.js/npm (for the theme build)
PHP 8.1+ · Drupal 11.3+ (tested on 11.3 and 11.4 — the scaffold auto-detects 11.4's consolidated `dr` CLI) · Drush 12.5+ · Node.js/npm (for the theme build)

---

Expand Down
4 changes: 3 additions & 1 deletion bin/dq-install
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ $rootRequire = array_merge(
);
if (!in_array($peerPackage, $rootRequire, true)) {
echo "📎 Adding {$peerPackage} to project composer.json...\n";
$composer['require'][$peerPackage] = '^1.0';
// core-recipe-unpack version-syncs with Drupal core (11.2+); the early
// standalone 1.x alphas are gone.
$composer['require'][$peerPackage] = '^11.2';
file_put_contents(
$composerFile,
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
Expand Down
6 changes: 5 additions & 1 deletion docs/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ chosen preset ← presets/overrides.css (theme_design from config.dq.yml)
```

`overrides.css` persists, so it survives re-skinning: `npm run preset corporate`
keeps the user's `theme_design` tweaks on top of the new preset.
keeps the user's `theme_design` tweaks on top of the new preset. In a generated
site it is **committed** (the starterkit gitignores it only for its own repo
hygiene, and deliberately withholds its `.gitignore` from `generate-theme` so
the site's root rules govern) — it cannot be regenerated once `dq:cleanup`
removes config.dq.yml.

## The content-scale contract

Expand Down
2 changes: 1 addition & 1 deletion docs/structured-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ a body-summary `description`, and any `field_keywords`.

The blog recipe ships a submodule (`dq_blog`) that `dq:scaffold` assembles under
the umbrella module and the recipe's `install:` enables. Its `BlogHooks` class
implements `#[Hook('preprocess_node')]` natively (OOP hooks, Drupal 11.1.8+) and
implements `#[Hook('preprocess_node')]` natively (module OOP hooks) and
narrows to Article with `$node->bundle() === 'article'`. Because the submodule is
its own extension, its preprocess stacks with the theme's and with other recipes
— no shared dispatcher is needed.
Expand Down
19 changes: 19 additions & 0 deletions src/Drush/Commands/DrupalQuickHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ protected function drupalRoot(): string {
return Drush::bootstrapManager()->getRoot();
}

/**
* Locates Drupal core's consolidated CLI (vendor/bin/dr, Drupal 11.4+).
*
* Returns the command prefix ['php', <path>] or NULL on older cores. Used
* for generate-theme and recipe application: 11.4 moved those off the
* legacy core/scripts/drupal + drush paths (the legacy script resolves the
* autoloader relative to the docroot and breaks on relocated-docroot
* projects, and the drush `recipe` command is gone).
*/
protected function drupalCoreCli(): ?array {
$root = $this->drupalRoot();
foreach ([getcwd() . '/vendor/bin/dr', dirname($root) . '/vendor/bin/dr', "{$root}/vendor/bin/dr"] as $candidate) {
if (is_file($candidate)) {
return ['php', $candidate];
}
}
return NULL;
}

/**
* Runs an external command and streams its output to the console.
*
Expand Down
44 changes: 34 additions & 10 deletions src/Drush/Commands/ScaffoldCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,12 @@ private function runBuild(array $config, array $registry): int {
return self::FAILURE;
}

// Prefer core's consolidated CLI (vendor/bin/dr, 11.4+); fall back to
// the legacy script on older cores (see drupalCoreCli()).
$drupalCli = $this->drupalCoreCli() ?? ['php', "{$drupalRoot}/core/scripts/drupal"];

$genCode = $this->runProcess([
'php', "{$drupalRoot}/core/scripts/drupal", 'generate-theme',
...$drupalCli, 'generate-theme',
$themeName,
"--name={$themeTitle}",
'--description=A custom Drupal theme built with Tailwind CSS and Vite.',
Expand Down Expand Up @@ -266,20 +270,33 @@ private function runBuild(array $config, array $registry): int {

// The entry's options map to native recipe inputs: each becomes
// --input=<recipe-dir>.<name>=<value> (core prefixes input names with
// the recipe directory's basename). Passed as extra args because the
// site-process option serializer can't repeat an option; Symfony
// parses them as options regardless of position. Unset options fall
// back to the recipe.yml input defaults.
$args = [$path];
// the recipe directory's basename). Unset options fall back to the
// recipe.yml input defaults.
$inputFlags = [];
foreach ($options as $key => $value) {
if (!is_scalar($value)) {
$this->io->warning("Skipping non-scalar option '{$key}' for recipe '{$path}'.");
continue;
}
$value = is_bool($value) ? ($value ? '1' : '0') : (string) $value;
$args[] = '--input=' . basename($path) . ".{$key}={$value}";
$value = is_bool($value) ? ($value ? '1' : '0') : (string) $value;
$inputFlags[] = '--input=' . basename($path) . ".{$key}={$value}";
}

// Core 11.4 moved recipe application to `dr recipe:apply` (the drush
// `recipe` command is gone); older cores still expose it via drush.
// The drush path passes --input flags as extra args because the
// site-process option serializer can't repeat an option; Symfony
// parses them as options regardless of position.
if ($dr = $this->drupalCoreCli()) {
$code = $this->runProcess([...$dr, 'recipe:apply', $path, ...$inputFlags, '--no-interaction']);
if ($code !== 0) {
$this->io->error("Applying recipe '{$path}' failed (exit code {$code}).");
return $code;
}
}
else {
Drush::drush(Drush::aliasManager()->getSelf(), 'recipe', [$path, ...$inputFlags], ['yes' => TRUE])->mustRun();
}
Drush::drush(Drush::aliasManager()->getSelf(), 'recipe', $args, ['yes' => TRUE])->mustRun();

// Inject theme assets when the recipe ships a theme-assets/ directory
// (copyThemeAssets no-ops when it doesn't — no registry flag needed).
Expand All @@ -289,6 +306,13 @@ private function runBuild(array $config, array $registry): int {
}
}

// 3.4. Recipes install modules in separate processes; rebuild so every
// subsequent drush call boots a container that knows the new extensions
// (without this, e.g. config:set can hit "entity type does not exist").
if (!empty($recipes)) {
Drush::drush(Drush::aliasManager()->getSelf(), 'cache:rebuild')->mustRun();
}

// 3.5. Set the generated theme as the site default (after recipes).
if ($themeName) {
$this->io->writeln("🎨 Setting '{$themeName}' as the default theme...");
Expand Down Expand Up @@ -535,7 +559,7 @@ private function ensureUmbrellaModule(): string {
"name: 'DQ Hooks'",
'type: module',
"description: 'Umbrella for drupal-quick recipe behaviour submodules (native OOP hooks).'",
'core_version_requirement: ^11.1.8',
'core_version_requirement: ^11.3',
"package: 'DQ'",
'',
]));
Expand Down
Loading