diff --git a/.hazelnut/README.md b/.hazelnut/README.md index 2a53b6c..6e992d8 100644 --- a/.hazelnut/README.md +++ b/.hazelnut/README.md @@ -61,9 +61,16 @@ docker build --platform linux/amd64 -t canvas-env:local image ``` The image stores the Drupal environment at `$HAZELNUT_WORKSPACE_DIR/drupal`. At -runtime, `canvas-env-init` links the Canvas checkout from +runtime, `canvas-env-init` moves the Canvas checkout Hazelnut cloned to `$HAZELNUT_WORKSPACE_DIR/$HAZELNUT_PROJECT_NAME` into Drupal's -`web/modules/contrib/canvas` path. +`web/modules/contrib/canvas` path and leaves a symlink at the workspace path. +The checkout must physically live inside the Drupal tree because Canvas +tooling locates Drupal by walking up parent directories. `canvas-env-start` also runs a virtual +display with a VNC bridge; headed Cypress and Playwright sessions can be watched +at `http://localhost:6080/vnc.html`. The Cypress binary and the Playwright +Chromium browser are baked into the image; their versions are pinned as build +arguments in the Dockerfile and must be kept in sync with what the Canvas +repository resolves. To test with a local Canvas checkout: @@ -73,8 +80,9 @@ docker run -d \ --platform linux/amd64 \ -p 8080:8080 \ -p 5173:5173 \ + -p 6080:6080 \ -e HAZELNUT_PROJECT_NAME=canvas \ - -v "/path/to/canvas:/home/hazelnut/workspace/canvas" \ + -v "/path/to/canvas:/home/hazelnut/workspace/drupal/web/modules/contrib/canvas" \ canvas-env:local \ canvas-env-start sleep infinity @@ -83,6 +91,9 @@ docker exec canvas-env-test site-install --stark docker exec -d canvas-env-test canvas-env-start ``` +Mount a clean clone rather than a working checkout: `node_modules` installed on +the host contain platform-specific binaries that fail inside the container. + Open `http://localhost:8080` and sign in with `admin` as both the username and password. Remove the container when finished: diff --git a/.hazelnut/image/AGENTS.md b/.hazelnut/image/AGENTS.md new file mode 100644 index 0000000..e727350 --- /dev/null +++ b/.hazelnut/image/AGENTS.md @@ -0,0 +1,68 @@ +# Drupal Canvas development container + +Drupal 11 environment for developing the Drupal Canvas module. PHP 8.3, Node.js +24, Composer 2, MariaDB (local socket, database `db`, user `db`, password `db`). + +## Layout + +- Canvas checkout (work here): `~/workspace/canvas` +- Drupal environment root: `~/workspace/drupal` (also `$CANVAS_ENV_ROOT`) +- The checkout is symlinked to `web/modules/contrib/canvas`; never edit files + through the Drupal path. + +## Setup and serving + +Run once per session, from `$CANVAS_ENV_ROOT` (the default directory): + +1. `composer install --no-interaction` +2. `site-install` (alias `si`) — installs Drupal plus Canvas. + - default: sample content, builds the UI (first build is slow) + - `--stark`: minimal theme content + - `--ui`: skips the UI build and enables `canvas_vite`, which serves UI + assets from the Vite dev server; then run `ui` to start it on :5173 + - `--mercury`: Mercury theme demo content +3. `canvas-env-start` — serves the site at http://127.0.0.1:8080 (run in the + background). Credentials: `admin` / `admin`. + +`canvas-env-start` also starts MariaDB and a virtual display with a VNC bridge. +Watch headed browser sessions at http://localhost:6080/vnc.html. + +## Commands + +All commands work from any directory. + +- `phpunit [path]` — PHPUnit with Drupal core's config; paths are relative to + the Canvas module, for example `phpunit tests/src/Unit`. Functional tests need + `canvas-env-start` running. +- `cypress` (alias `cy`) — Cypress e2e run; `--component` for component tests, + `--spec ` to filter, `--open` for interactive mode via VNC. +- `playwright [args]` — `playwright test` in the Canvas root; all Playwright CLI + arguments pass through. +- `phpcs [path]` / `phpstan [path]` — PHPCBF and PHPStan with Canvas's + configuration. +- `ui` — builds the UI, enables `canvas_vite`, and starts the Vite dev server; + `--skip-build` to skip the build. +- `drush`, `composer` — on PATH; Drush is preconfigured with the site URI. + +The Cypress binary and Playwright's Chromium are baked into the image; no +browser installs are needed. + +## npm and Turborepo + +The Canvas repository is an npm workspaces monorepo built with Turborepo. Run +npm scripts from `~/workspace/canvas` (root `package.json`) or a workspace +directory. Builds cache under `.turbo/`; unchanged rebuilds are near-instant. +All workspaces require Node.js `>=22.19.0 <23 || >=24.5.0`; the image ships +24.x. + +## Pitfalls + +- The Canvas checkout physically lives at + `~/workspace/drupal/web/modules/contrib/canvas`; `~/workspace/canvas` is a + symlink to it. Keep it that way: Canvas tooling finds Drupal by walking up + parent directories from the physical path. +- After enabling or disabling modules outside `site-install`, run + `drush cache:rebuild` before using the Canvas editor. +- Environment variables for tests (`SIMPLETEST_*`, `DRUPAL_TEST_*`, `BASE_URL`, + `DRUPAL_ROOT_CORE`) are preset; do not override them unless a test documents + otherwise. diff --git a/.hazelnut/image/Dockerfile b/.hazelnut/image/Dockerfile index 29d6272..71ecf06 100644 --- a/.hazelnut/image/Dockerfile +++ b/.hazelnut/image/Dockerfile @@ -4,6 +4,11 @@ USER root ENV CANVAS_ENV_ROOT=${HAZELNUT_WORKSPACE_DIR}/drupal +# Keep these in sync with the versions the Canvas repository resolves: +# `cypress` in ui/package.json, `@playwright/test` in package.json. +ARG CYPRESS_VERSION=13.17.0 +ARG PLAYWRIGHT_VERSION=1.59.1 + # Drupal Canvas CI runs PHP 8.3, while Debian Trixie provides PHP 8.4. RUN install -d -m 0755 /etc/apt/keyrings \ && curl -fsSL https://packages.sury.org/php/apt.gpg \ @@ -19,6 +24,7 @@ RUN install -d -m 0755 /etc/apt/keyrings \ && apt-get update \ && env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ + fluxbox \ libasound2 \ libgbm1 \ libgtk-3-0 \ @@ -28,6 +34,7 @@ RUN install -d -m 0755 /etc/apt/keyrings \ libxtst6 \ mariadb-server \ nodejs \ + novnc \ patch \ php8.3-cli \ php8.3-curl \ @@ -37,11 +44,13 @@ RUN install -d -m 0755 /etc/apt/keyrings \ php8.3-mysql \ php8.3-opcache \ php8.3-readline \ + php8.3-sqlite3 \ php8.3-xml \ php8.3-zip \ pkg-config \ python3 \ rsync \ + x11vnc \ xauth \ xvfb \ zip \ @@ -57,6 +66,17 @@ RUN EXPECTED_CHECKSUM="$(curl -fsSL https://composer.github.io/installer.sig)" \ && php /tmp/composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer \ && rm /tmp/composer-setup.php +# Bake the Cypress binary and the Playwright browsers into the image so the +# test commands can assume they are available. Both stores are version-keyed, +# so bumping the ARGs above is enough when Canvas upgrades. +ENV CYPRESS_CACHE_FOLDER=/opt/cypress-cache \ + PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright +RUN npm_config_cache=/tmp/npm-cache npx -y "cypress@${CYPRESS_VERSION}" install \ + && npm_config_cache=/tmp/npm-cache npx -y "playwright@${PLAYWRIGHT_VERSION}" \ + install --with-deps chromium \ + && rm -rf /tmp/npm-cache /var/lib/apt/lists/* \ + && chown -R "$USER:$USER" "$CYPRESS_CACHE_FOLDER" "$PLAYWRIGHT_BROWSERS_PATH" + # Keep the local database private to the container and tune PHP for development. RUN printf '%s\n' \ '[mysqld]' \ @@ -68,21 +88,21 @@ RUN printf '%s\n' \ >/etc/php/8.3/cli/conf.d/99-canvas-env.ini \ && install -d -m 0755 "$CANVAS_ENV_ROOT/web/modules/contrib" -# The Drupal environment is baked into the image. Hazelnut clones Canvas into -# the session workspace, which is linked into Composer's expected module path. +# The Drupal environment is baked into the image. At runtime canvas-env-init +# moves the Canvas checkout from the session workspace into Composer's +# expected module path and leaves a symlink at the workspace path. +# composer.json and composer.lock are byte-identical copies of the repository +# root files; scripts/ carries the pre-install-cmd hooks they reference. COPY composer.json composer.lock ${CANVAS_ENV_ROOT}/ COPY recipes/ ${CANVAS_ENV_ROOT}/recipes/ -COPY check-repo.sh ${CANVAS_ENV_ROOT}/scripts/check-repo.sh +COPY scripts/ ${CANVAS_ENV_ROOT}/scripts/ COPY web/sites/default/settings.php ${CANVAS_ENV_ROOT}/web/sites/default/settings.php COPY bin/ /usr/local/bin/ -RUN jq '.scripts["pre-install-cmd"] = ["./scripts/check-repo.sh"]' \ - "$CANVAS_ENV_ROOT/composer.json" >/tmp/composer.json \ - && mv /tmp/composer.json "$CANVAS_ENV_ROOT/composer.json" \ - && chown -R "$USER:$USER" "$CANVAS_ENV_ROOT" "$HOME/.config" \ - && chmod 0755 "$CANVAS_ENV_ROOT/scripts/check-repo.sh" \ +RUN chown -R "$USER:$USER" "$CANVAS_ENV_ROOT" "$HOME/.config" \ + && chmod 0755 "$CANVAS_ENV_ROOT"/scripts/*.sh \ /usr/local/bin/canvas-env-* /usr/local/bin/cypress \ - /usr/local/bin/n /usr/local/bin/phpcs /usr/local/bin/phpstan \ + /usr/local/bin/phpcs /usr/local/bin/phpstan \ /usr/local/bin/phpunit /usr/local/bin/playwright /usr/local/bin/site-install \ /usr/local/bin/ui \ && ln -s cypress /usr/local/bin/cy \ @@ -90,18 +110,29 @@ RUN jq '.scripts["pre-install-cmd"] = ["./scripts/check-repo.sh"]' \ USER ${USER} -ENV COMPOSER_MEMORY_LIMIT=-1 \ - CYPRESS_CACHE_FOLDER=${CANVAS_ENV_ROOT}/.cache/cypress \ +# vendor/bin is appended, not prepended, so the /usr/local/bin wrappers +# (phpunit, phpcs, phpstan) are not shadowed by the Composer-installed +# binaries of the same name once composer install has run. +ENV BASE_URL=http://127.0.0.1:8080 \ + COMPOSER_MEMORY_LIMIT=-1 \ + DB_URL=mysql://db:db@127.0.0.1:3306/db \ + DISPLAY=:0 \ + DRUPAL_ROOT_CORE=${CANVAS_ENV_ROOT}/web/core \ DRUPAL_TEST_BASE_URL=http://127.0.0.1:8080 \ DRUPAL_TEST_DB_URL=mysql://db:db@127.0.0.1:3306/db \ + DRUPAL_TEST_DRUPAL_ROOT=${CANVAS_ENV_ROOT}/web \ DRUSH_OPTIONS_URI=http://127.0.0.1:8080 \ NODE_OPTIONS=--max-old-space-size=4096 \ + PHP_CLI_SERVER_WORKERS=8 \ npm_config_cache=${CANVAS_ENV_ROOT}/.cache/npm \ + npm_config_fund=false \ + npm_config_update_notifier=false \ SIMPLETEST_BASE_URL=http://127.0.0.1:8080 \ SIMPLETEST_DB=mysql://db:db@127.0.0.1:3306/db \ + TURBO_TELEMETRY_DISABLED=1 \ VITE_SERVER_ORIGIN=http://127.0.0.1:5173 \ - PATH=${CANVAS_ENV_ROOT}/vendor/bin:${PATH} + PATH=${PATH}:${CANVAS_ENV_ROOT}/vendor/bin -EXPOSE 8080 5173 +EXPOSE 8080 5173 6080 WORKDIR ${CANVAS_ENV_ROOT} diff --git a/.hazelnut/image/bin/canvas-env-init b/.hazelnut/image/bin/canvas-env-init index 2afbc29..5a32711 100755 --- a/.hazelnut/image/bin/canvas-env-init +++ b/.hazelnut/image/bin/canvas-env-init @@ -6,15 +6,40 @@ WORKSPACE_ROOT="${HAZELNUT_WORKSPACE_DIR:-$HOME/workspace}" ENV_ROOT="${CANVAS_ENV_ROOT:-$WORKSPACE_ROOT/drupal}" CANVAS_ROOT="${CANVAS_ROOT:-$WORKSPACE_ROOT/${HAZELNUT_PROJECT_NAME:?HAZELNUT_PROJECT_NAME must be set when CANVAS_ROOT is not set}}" FILES_DIR="$ENV_ROOT/web/sites/default/files" -MODULE_LINK="$ENV_ROOT/web/modules/contrib/canvas" +MODULE_DIR="$ENV_ROOT/web/modules/contrib/canvas" -if [[ -L "$MODULE_LINK" ]]; then - ln -sfn "$CANVAS_ROOT" "$MODULE_LINK" -elif [[ -e "$MODULE_LINK" ]]; then - echo "Cannot link Canvas because $MODULE_LINK already exists and is not a symlink." >&2 - exit 1 +has_content() { + [[ -d "$1" ]] \ + && [[ -n "$(find "$1/" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]] +} + +# The checkout must physically live inside the Drupal tree: Canvas tooling +# (its Composer scripts, cypress.config.js, @drupal-canvas/test-utils, and +# @drupal/playwright) locates Drupal by walking up parent directories from +# the physical path. The workspace path stays valid as a symlink. +if [[ ! -L "$MODULE_DIR" ]] && has_content "$MODULE_DIR"; then + # The checkout is already in place, for example bind-mounted there. + if [[ -L "$CANVAS_ROOT" || ! -e "$CANVAS_ROOT" ]]; then + ln -sfn "$MODULE_DIR" "$CANVAS_ROOT" + fi +elif [[ ! -L "$CANVAS_ROOT" ]] && has_content "$CANVAS_ROOT"; then + # Hazelnut cloned the checkout into the workspace. Move it into the + # Drupal tree and leave a symlink behind. A bind-mounted workspace + # checkout cannot be moved; fall back to linking it into the Drupal tree, + # which keeps the environment usable, although parent-walking test + # tooling then only works where an environment override exists. + rm -rf "$MODULE_DIR" 2>/dev/null || true + if mv "$CANVAS_ROOT" "$MODULE_DIR" 2>/dev/null; then + ln -s "$MODULE_DIR" "$CANVAS_ROOT" + else + echo "Warning: cannot move $CANVAS_ROOT into the Drupal tree (is it a mount?); linking instead." >&2 + ln -sfn "$CANVAS_ROOT" "$MODULE_DIR" + fi +elif [[ -L "$CANVAS_ROOT" || -L "$MODULE_DIR" ]]; then + : # Already initialized. else - ln -s "$CANVAS_ROOT" "$MODULE_LINK" + echo "Canvas is not available at $CANVAS_ROOT or $MODULE_DIR." >&2 + exit 1 fi if ! sudo mariadb-admin ping --silent >/dev/null 2>&1; then diff --git a/.hazelnut/image/bin/canvas-env-start b/.hazelnut/image/bin/canvas-env-start index 7ebbe97..187c164 100755 --- a/.hazelnut/image/bin/canvas-env-start +++ b/.hazelnut/image/bin/canvas-env-start @@ -4,9 +4,27 @@ set -euo pipefail WORKSPACE_ROOT="${HAZELNUT_WORKSPACE_DIR:-$HOME/workspace}" ENV_ROOT="${CANVAS_ENV_ROOT:-$WORKSPACE_ROOT/drupal}" +VNC_DISPLAY="${DISPLAY:-:0}" canvas-env-init +# Run a virtual display with a VNC bridge so headed Cypress and Playwright +# sessions can be watched at http://localhost:6080/vnc.html via noVNC. +if ! pgrep -x Xvfb >/dev/null 2>&1; then + Xvfb "$VNC_DISPLAY" -screen 0 1920x1080x24 >/dev/null 2>&1 & + sleep 1 +fi +if ! pgrep -x fluxbox >/dev/null 2>&1; then + fluxbox -display "$VNC_DISPLAY" >/dev/null 2>&1 & +fi +if ! pgrep -x x11vnc >/dev/null 2>&1; then + x11vnc -display "$VNC_DISPLAY" -forever -shared -nopw -quiet -bg >/dev/null 2>&1 +fi +if ! pgrep -f novnc_proxy >/dev/null 2>&1; then + /usr/share/novnc/utils/novnc_proxy --web /usr/share/novnc \ + --vnc localhost:5900 --listen 6080 >/dev/null 2>&1 & +fi + if (( $# > 0 )); then exec "$@" fi @@ -16,6 +34,9 @@ if [[ ! -f "$ENV_ROOT/web/.ht.router.php" ]]; then exit 1 fi +# PHP's built-in server does not change its working directory, and some code +# includes files relative to the docroot, so it must run from there. +cd "$ENV_ROOT/web" exec php -S "0.0.0.0:${CANVAS_HTTP_PORT:-8080}" \ -t "$ENV_ROOT/web" \ "$ENV_ROOT/web/.ht.router.php" diff --git a/.hazelnut/image/bin/cypress b/.hazelnut/image/bin/cypress index d2a5898..c57dc84 100755 --- a/.hazelnut/image/bin/cypress +++ b/.hazelnut/image/bin/cypress @@ -6,6 +6,7 @@ WORKSPACE_ROOT="${HAZELNUT_WORKSPACE_DIR:-$HOME/workspace}" CANVAS_ROOT="${CANVAS_ROOT:-$WORKSPACE_ROOT/${HAZELNUT_PROJECT_NAME:?HAZELNUT_PROJECT_NAME must be set when CANVAS_ROOT is not set}}" UI_ROOT="$CANVAS_ROOT/ui" COMPONENT=false +OPEN=false SPEC="" while (( $# > 0 )); do @@ -14,6 +15,10 @@ while (( $# > 0 )); do COMPONENT=true shift ;; + --open|-o) + OPEN=true + shift + ;; --spec) [[ $# -ge 2 ]] || { echo "--spec requires a value" >&2; exit 1; } SPEC="$2" @@ -21,7 +26,7 @@ while (( $# > 0 )); do ;; *) echo "Unknown option: $1" >&2 - echo "Usage: cypress [--component|-c] [--spec ]" >&2 + echo "Usage: cypress [--component|-c] [--open|-o] [--spec ]" >&2 exit 1 ;; esac @@ -29,12 +34,18 @@ done cd "$UI_ROOT" -if [[ ! -x node_modules/.bin/cypress ]]; then - echo "Cypress is not installed. Run: n install" >&2 +# npm workspaces hoist dependencies to the repository root node_modules. +if [[ ! -x "$CANVAS_ROOT/node_modules/.bin/cypress" ]]; then + echo "Node dependencies are not installed. Run: npm install --prefix $CANVAS_ROOT" >&2 exit 1 fi -if ! npm run cy:verify >/dev/null 2>&1; then - npx cypress install + +# Dismiss the Cypress welcome message so open mode starts on the project. +STATE_DIR="$HOME/.config/Cypress/cy/production/projects/__global__" +if [[ ! -f "$STATE_DIR/state.json" ]]; then + mkdir -p "$STATE_DIR" + printf '{"majorVersionWelcomeDismissed": {"13": %s}}\n' "$(date +%s)" \ + >"$STATE_DIR/state.json" fi ARGS=(--browser electron) @@ -47,4 +58,9 @@ if [[ -n "$SPEC" ]]; then ARGS+=(--spec "$SPEC") fi +if [[ "$OPEN" == true ]]; then + echo "Watch the Cypress session at http://localhost:6080/vnc.html" + exec npm run cy:open -- "${ARGS[@]}" +fi + exec npm run cy:run -- "${ARGS[@]}" diff --git a/.hazelnut/image/bin/n b/.hazelnut/image/bin/n deleted file mode 100755 index 15cc7f9..0000000 --- a/.hazelnut/image/bin/n +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -WORKSPACE_ROOT="${HAZELNUT_WORKSPACE_DIR:-$HOME/workspace}" -CANVAS_ROOT="${CANVAS_ROOT:-$WORKSPACE_ROOT/${HAZELNUT_PROJECT_NAME:?HAZELNUT_PROJECT_NAME must be set when CANVAS_ROOT is not set}}" -CANVAS_DIR=ui -NPM_ARGS=() - -for arg in "$@"; do - case "$arg" in - --canvas-dir=*) - CANVAS_DIR="${arg#*=}" - ;; - *) - NPM_ARGS+=("$arg") - ;; - esac -done - -case "$CANVAS_DIR" in - root) TARGET_DIR="$CANVAS_ROOT" ;; - ui) TARGET_DIR="$CANVAS_ROOT/ui" ;; - astro) TARGET_DIR="$CANVAS_ROOT/ui/lib/astro-hydration" ;; - cli) TARGET_DIR="$CANVAS_ROOT/cli" ;; - docs) TARGET_DIR="$CANVAS_ROOT/docs/user" ;; - *) - echo "Unknown Canvas directory: $CANVAS_DIR" >&2 - echo "Available directories: root, ui, astro, cli, docs" >&2 - exit 1 - ;; -esac - -printf 'Running npm in %s…\n' "$TARGET_DIR" -cd "$TARGET_DIR" -exec npm "${NPM_ARGS[@]}" diff --git a/.hazelnut/image/bin/phpcs b/.hazelnut/image/bin/phpcs index 5aa563b..6790294 100755 --- a/.hazelnut/image/bin/phpcs +++ b/.hazelnut/image/bin/phpcs @@ -3,24 +3,34 @@ set -euo pipefail WORKSPACE_ROOT="${HAZELNUT_WORKSPACE_DIR:-$HOME/workspace}" +ENV_ROOT="${CANVAS_ENV_ROOT:-$WORKSPACE_ROOT/drupal}" CANVAS_ROOT="${CANVAS_ROOT:-$WORKSPACE_ROOT/${HAZELNUT_PROJECT_NAME:?HAZELNUT_PROJECT_NAME must be set when CANVAS_ROOT is not set}}" -cd "$CANVAS_ROOT" -if (( $# == 0 )); then - exec composer run phpcbf -fi +# Canvas's own `composer run phpcbf` locates the Drupal root by walking up +# parent directories, which cannot work from the workspace checkout, so the +# environment root's PHPCBF is invoked directly with Canvas's standard. +cd "$CANVAS_ROOT" +mkdir -p .cache/phpcs -ARGS=() +TARGETS=() for arg in "$@"; do if [[ "$arg" == /* || "$arg" == -* ]]; then - ARGS+=("$arg") + TARGETS+=("$arg") else - ARGS+=("$CANVAS_ROOT/$arg") + TARGETS+=("$CANVAS_ROOT/$arg") fi done +if (( ${#TARGETS[@]} == 0 )); then + TARGETS=("$CANVAS_ROOT") +fi -mkdir -p .cache/phpcs -exec composer --working-dir="$(composer run --quiet composer-root)" exec phpcbf -- \ +# PHPCBF exits 1 when it fixed something, which is a success for this command. +STATUS=0 +"$ENV_ROOT/vendor/bin/phpcbf" \ --standard="$CANVAS_ROOT/phpcs.xml" \ --colors \ - "${ARGS[@]}" + "${TARGETS[@]}" || STATUS=$? +if (( STATUS == 0 || STATUS == 1 )); then + exit 0 +fi +exit "$STATUS" diff --git a/.hazelnut/image/bin/phpstan b/.hazelnut/image/bin/phpstan index 8440f10..93db837 100755 --- a/.hazelnut/image/bin/phpstan +++ b/.hazelnut/image/bin/phpstan @@ -3,6 +3,20 @@ set -euo pipefail WORKSPACE_ROOT="${HAZELNUT_WORKSPACE_DIR:-$HOME/workspace}" +ENV_ROOT="${CANVAS_ENV_ROOT:-$WORKSPACE_ROOT/drupal}" CANVAS_ROOT="${CANVAS_ROOT:-$WORKSPACE_ROOT/${HAZELNUT_PROJECT_NAME:?HAZELNUT_PROJECT_NAME must be set when CANVAS_ROOT is not set}}" + +# Canvas's own `composer run phpstan` locates the Drupal root by walking up +# parent directories, which cannot work from the workspace checkout, so the +# environment root's PHPStan is invoked directly with Canvas's configuration. cd "$CANVAS_ROOT" -exec composer run phpstan + +TARGETS=("$@") +if (( ${#TARGETS[@]} == 0 )); then + TARGETS=(.) +fi + +exec "$ENV_ROOT/vendor/bin/phpstan" \ + --configuration=phpstan.neon \ + --autoload-file=phpstan_rules/autoload.php \ + analyze "${TARGETS[@]}" diff --git a/.hazelnut/image/bin/playwright b/.hazelnut/image/bin/playwright index 9fe8c1f..d85aa75 100755 --- a/.hazelnut/image/bin/playwright +++ b/.hazelnut/image/bin/playwright @@ -4,37 +4,14 @@ set -euo pipefail WORKSPACE_ROOT="${HAZELNUT_WORKSPACE_DIR:-$HOME/workspace}" CANVAS_ROOT="${CANVAS_ROOT:-$WORKSPACE_ROOT/${HAZELNUT_PROJECT_NAME:?HAZELNUT_PROJECT_NAME must be set when CANVAS_ROOT is not set}}" -SPEC="" - -while (( $# > 0 )); do - case "$1" in - --spec) - [[ $# -ge 2 ]] || { echo "--spec requires a value" >&2; exit 1; } - SPEC="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" >&2 - echo "Usage: playwright [--spec ]" >&2 - exit 1 - ;; - esac -done cd "$CANVAS_ROOT" if [[ ! -x node_modules/.bin/playwright ]]; then - echo "Playwright is not installed. Run: n --canvas-dir=root install" >&2 + echo "Node dependencies are not installed. Run: npm install --prefix $CANVAS_ROOT" >&2 exit 1 fi -MARKER="$HOME/.playwright_deps_installed" -if [[ ! -f "$MARKER" ]]; then - npx playwright install --with-deps - touch "$MARKER" -fi - -if [[ -n "$SPEC" ]]; then - exec npx playwright test "$SPEC" -fi -exec npx playwright test +# All arguments are passed to `playwright test`, so specs, --headed (watch at +# http://localhost:6080/vnc.html), --grep, and friends work as documented. +exec npx playwright test "$@" diff --git a/.hazelnut/image/bin/site-install b/.hazelnut/image/bin/site-install index d05363b..8e6ab5f 100755 --- a/.hazelnut/image/bin/site-install +++ b/.hazelnut/image/bin/site-install @@ -49,11 +49,7 @@ drush() { } apply_recipe() { - if [[ -x "$ENV_ROOT/vendor/bin/dr" ]]; then - (cd "$DRUPAL_ROOT" && "$ENV_ROOT/vendor/bin/dr" recipe:apply --no-interaction "$1") - else - (cd "$DRUPAL_ROOT" && php core/scripts/drupal recipe --no-interaction "$1") - fi + (cd "$DRUPAL_ROOT" && php core/scripts/drupal recipe --no-interaction "$1") } delete_canvas_folders() { @@ -110,7 +106,9 @@ drush php:eval ' $entity->save(); ' +# npm must run from inside the workspace root: `npm --prefix` through the +# workspace symlink breaks npm's workspace resolution and prunes packages. if [[ "$UI" == false ]]; then - npm install --prefix "$CANVAS_ROOT" - npm run build --prefix "$CANVAS_ROOT/ui" + (cd "$CANVAS_ROOT" && npm install) + (cd "$CANVAS_ROOT/ui" && npm run build) fi diff --git a/.hazelnut/image/bin/ui b/.hazelnut/image/bin/ui index e8d5605..a5ee0be 100755 --- a/.hazelnut/image/bin/ui +++ b/.hazelnut/image/bin/ui @@ -26,11 +26,14 @@ while (( $# > 0 )); do shift done -if [[ "$INSTALL" == true ]]; then - npm install --prefix "$CANVAS_ROOT" +# npm must run from inside the workspace root: `npm --prefix` through the +# workspace symlink breaks npm's workspace resolution and prunes packages. +# site-install --ui skips npm entirely, so install dependencies when absent. +if [[ "$INSTALL" == true || ! -d "$CANVAS_ROOT/node_modules" ]]; then + (cd "$CANVAS_ROOT" && npm install) fi if [[ "$SKIP_BUILD" == false ]]; then - npm run build --prefix "$UI_ROOT" + (cd "$UI_ROOT" && npm run build) fi "$ENV_ROOT/vendor/bin/drush" \ @@ -38,4 +41,5 @@ fi --uri="${DRUSH_OPTIONS_URI:-http://127.0.0.1:8080}" \ pm:install canvas_vite --yes -exec npm run drupaldev --prefix "$UI_ROOT" -- --host 0.0.0.0 +cd "$UI_ROOT" +exec npm run drupaldev -- --host 0.0.0.0 diff --git a/.hazelnut/image/scripts/check-repo-mercury.sh b/.hazelnut/image/scripts/check-repo-mercury.sh new file mode 100644 index 0000000..eb89eae --- /dev/null +++ b/.hazelnut/image/scripts/check-repo-mercury.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# The repository root composer.json runs this hook before composer install. +# The Mercury theme is not part of the Hazelnut environment, so this is a +# no-op that exists to keep composer.json byte-identical to the root copy. +exit 0 diff --git a/.hazelnut/image/check-repo.sh b/.hazelnut/image/scripts/check-repo.sh similarity index 100% rename from .hazelnut/image/check-repo.sh rename to .hazelnut/image/scripts/check-repo.sh