diff --git a/.agents/skills/dq-add-recipe/SKILL.md b/.agents/skills/dq-add-recipe/SKILL.md index 2985b9b..c80e485 100644 --- a/.agents/skills/dq-add-recipe/SKILL.md +++ b/.agents/skills/dq-add-recipe/SKILL.md @@ -85,7 +85,7 @@ final class BlogHooks { ``` - The module needs a `dq_.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 diff --git a/.agents/skills/dq-conventions/SKILL.md b/.agents/skills/dq-conventions/SKILL.md index a6d23e1..fa32ef9 100644 --- a/.agents/skills/dq-conventions/SKILL.md +++ b/.agents/skills/dq-conventions/SKILL.md @@ -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). diff --git a/README.md b/README.md index b1fd200..4d25bb4 100644 --- a/README.md +++ b/README.md @@ -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) --- diff --git a/bin/dq-install b/bin/dq-install index c2b255b..39749d4 100644 --- a/bin/dq-install +++ b/bin/dq-install @@ -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" diff --git a/docs/presets.md b/docs/presets.md index 3527446..5398cf8 100644 --- a/docs/presets.md +++ b/docs/presets.md @@ -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 diff --git a/docs/structured-data.md b/docs/structured-data.md index adbf87e..5ca8d01 100644 --- a/docs/structured-data.md +++ b/docs/structured-data.md @@ -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. diff --git a/src/Drush/Commands/DrupalQuickHelpers.php b/src/Drush/Commands/DrupalQuickHelpers.php index dc80a8b..40afd6a 100644 --- a/src/Drush/Commands/DrupalQuickHelpers.php +++ b/src/Drush/Commands/DrupalQuickHelpers.php @@ -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', ] 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. * diff --git a/src/Drush/Commands/ScaffoldCommand.php b/src/Drush/Commands/ScaffoldCommand.php index 852d5ba..defbcce 100644 --- a/src/Drush/Commands/ScaffoldCommand.php +++ b/src/Drush/Commands/ScaffoldCommand.php @@ -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.', @@ -266,20 +270,33 @@ private function runBuild(array $config, array $registry): int { // The entry's options map to native recipe inputs: each becomes // --input=.= (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). @@ -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..."); @@ -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'", '', ]));