An opinionated development environment for brewing Drupal Canvas, built with DDEV. I maintain this project to use for my day-to-day development work, with workflows and tools I prefer.
- Clone the repository
- Run:
ddev start \ && ddev clone-repo \ && ddev clone-repo-mercury \ && ddev composer install \ && ddev site-install \ && ddev launch \ && ddev ui --install
Use scripts/create-worktree.sh to work on a Canvas branch from
web/modules/contrib/canvas as a standalone project. Pass a branch name, and
the script derives everything else:
scripts/create-worktree.sh 880922-my-branchThis creates the branch from origin/1.x and a worktree at
../canvas-worktrees/880922-my-branch/canvas when they do not exist yet, and
reuses them when they do. The same command works for creating, resuming, and
repairing a setup. When the branch already exists locally or is checked out in
another worktree, the script uses it as is.
The default Canvas starting point is origin/1.x. Pass --base-ref=<ref> to
use another ref. The base ref can be a remote branch, local branch, tag, or
commit SHA. Pass --worktrees-root=<path> to change where derived worktrees
are created. Run the script with --help for the full list of options.
The script also accepts a Drupal.org merge request URL:
scripts/create-worktree.sh https://git.drupalcode.org/project/canvas/-/merge_requests/1353This resolves the merge request to its issue-fork branch, adds the fork as a
Git remote named canvas-<issue-id> when needed, and creates the branch
tracking it. When the branch already exists locally, it is reused as is.
By default, the script also runs the DDEV setup:
ddev start
ddev composer install
ddev site-installPass --ui to run ddev site-install --ui instead, which also builds the UI.
Note that ddev site-install reinstalls the Drupal site.
When a coding tool creates the Canvas worktree, use the same script with
--canvas-worktree in the tool setup hook, replacing SOURCE_TREE_PATH and
WORKTREE_PATH with the environment variables provided by the tool:
set -euo pipefail
SOURCE_ENV="$(cd "$SOURCE_TREE_PATH/../../../.." && pwd)"
"$SOURCE_ENV/scripts/create-worktree.sh" \
--canvas-worktree="$WORKTREE_PATH"After the Canvas worktree exists, the script creates an environment
wrapper next to it, writes a unique ignored DDEV project configuration with an
isolated Turbo cache, mounts the Canvas worktree into DDEV at
web/modules/contrib/canvas, and creates .ddev-env inside the Canvas worktree
as a symlink back to the environment wrapper.
To create and wrap the worktree without running DDEV setup, pass
--skip-ddev.
After setup, the environment wrapper is available from the Canvas worktree:
cd .ddev-envWhen done with a branch, use scripts/destroy-worktree.sh to tear the
setup down. This deletes the DDEV project including its containers and
database, then removes the environment wrapper, the Mercury worktree, and the
Canvas worktree. The branch is kept by default; pass --delete-branch to
delete it as well:
scripts/destroy-worktree.sh 880922-my-branchThe DDEV project name is derived from the checkout directory because
.ddev/config.yaml does not set name. This keeps Git worktrees independent: a
checkout in canvas-env uses https://canvas-env.ddev.site, and a worktree in
canvas-env-issue-123 uses https://canvas-env-issue-123.ddev.site.
Use ignored local overrides in .ddev/config.local.yaml only when a checkout
needs a specific project name.
This project uses two agent configuration scopes:
- Environment scope for root-level tooling and infrastructure tasks.
- Canvas scope for module code work in
web/modules/contrib/canvas.
For details on workspace entrypoints and skill locations, see
agents/README.md.
Running ddev clone-repo also wires the Canvas agent files in the nested
repository by creating symlinks for AGENTS.md, .agents/skills, and .codex,
and by adding matching local-only nested-repo excludes in .git/info/exclude.
| Command | Description |
|---|---|
n |
Run npm inside the UI directory.--canvas-dir=<directory>: runs npm inside specified directory( root, ui, astro, cli, or docs) |
ui |
Build the UI code and start the dev server.--install -i: runs npm install before--skip-build -s: skips the build step |
playwright |
Run Playwright in UI mode. (Accessible via VNC at https://<project-name>.ddev.site:6081/vnc.html)--spec <path-to-spec>: runs a spec in headless mode |
cy,cypress |
Run the Cypress UI in end-to-end testing mode (Accessible via VNC at https://<project-name>.ddev.site:6081/vnc.html)--spec <path-to-spec>: runs a spec in headless mode--component -c: use component testing mode |
phpunit [path] |
Run PHPUnit tests in the module's codebase[path]: narrows to the path, relative to the module directory |
phpcs [path] |
Run PHP Code Beautifier and Fixer in the module's codebase[path]: narrows to the path, relative to the module directory |
phpstan |
Run PHPStan in the module's codebase |
si,site-install |
Install Drupal site, install and configure modules: Canvas, Canvas Headless, and Canvas OAuth.--ui: also installs Canvas Vite for UI development--mercury -m: applies the Mercury recipe instead of the Stark and Humanify recipes--stark: skips the Humanify recipe, and creates an empty page |
clone-repo |
Clone the Canvas module's repository, add local nested-repo excludes for the agent and Codex paths, and set their symlinks. (Runs on the host.) |
clone-repo-mercury |
Clone the Mercury theme's repository. (Runs on the host.) |
See the list of commands provided by DDEV out-of-the box.
| Script | Description |
|---|---|
scripts/create-worktree.sh |
Create or set up a Canvas worktree with an environment wrapper and DDEV bind mount. |
scripts/destroy-worktree.sh |
Tear down a Canvas worktree setup, including its DDEV project, containers, and database. |
scripts/wrap-canvas-worktree.sh |
Wrap an existing Canvas worktree with a sibling environment worktree and DDEV bind mount. |
scripts/wire-canvas-agents.sh |
Wire local agent and Codex configuration into a Canvas checkout or worktree. |
scripts/install-canvas-packages.sh |
Build and install local Canvas package tarballs into a target project. |
Use ddev drush dcer to export specific entities to recipe content folders.
Export a canvas_page entity:
ddev drush dcer canvas_page 1 --folder=../recipes/canvas_dev_humanify/contentExport a menu_link_content entity:
ddev drush dcer menu_link_content 1 --folder=../../recipes/canvas_dev_humanify/contentMy work on Drupal Canvas is made possible by Acquia.