Skip to content
Open
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
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,25 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J
- `dcq-reports/` is created at the project root when running `checks`
or the `*-fix` commands (logs + patch previews).
- Add `dcq-reports/` to `.gitignore` if you do not want to track it.
- Host-path parity alias:
- The add-on installs `.ddev/web-entrypoint.d/90-dcq-host-path-alias.sh`.
- On container start, it creates a host-style project-path symlink to
`/var/www/html` so absolute host paths can resolve inside the container.
- After first install/upgrade that adds this hook, run `ddev restart` once
so the alias is established in the running container.
- On macOS paths under `/private/...`, it also creates a `/...` companion
alias (for example `/tmp/...`) to cover common host-path forms.
- The alias is enforced at startup; if startup cannot safely establish the
alias, startup emits an error and wrappers should be considered unavailable
until the conflict is resolved.
- ESLint toolchain selection:
- `ESLINT_TOOLCHAIN=auto` (default) prefers root toolchain when root configs exist.
- `ESLINT_TOOLCHAIN=core` forces Drupal core JS toolchain.
- `ESLINT_TOOLCHAIN=root` forces project root toolchain.
- ESLint config mode:
- `ESLINT_CONFIG_MODE=nearest` (default) groups by nearest config file.
- `ESLINT_CONFIG_MODE=fixed` forces `.eslintrc.passing.json`.
- `ESLINT_CONFIG_MODE=fixed` prefers `.eslintrc.passing.json`, then
`.eslintrc.json` in the project root.
- ESLint warning visibility (GitLab CI parity):
- `DCQ_ESLINT_QUIET=1` (default) adds `--quiet` to `ddev eslint` and
`ddev eslint-fix`, so warnings are suppressed.
Expand All @@ -177,8 +189,8 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J
- CSpell parity:
- Run `ddev exec php /mnt/ddev_config/drupal-code-quality/tooling/scripts/prepare-cspell.php -s .prepared` once and
replace `.cspell.json` after reviewing the diff.
- `ddev cspell` runs from the repo root (`.`) by default; scope is controlled
by `.cspell.json` `ignorePaths`. Narrow the scan by passing explicit paths.
- `ddev cspell` defaults to custom code plus `sites` under the configured
docroot, excluding `sites/*/files/**`, when no paths are passed.
- `.cspell-project-words.txt` is created by the installer (empty) and updated
by `ddev cspell-suggest` when you accept suggested words.
- PHPCS / PHPCBF default scope:
Expand All @@ -189,14 +201,14 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J
- You can still pass explicit paths to narrow runs.
- PHPStan baseline:
- Generate a baseline with `ddev phpstan --generate-baseline`.
- This writes `phpstan-baseline.neon` at the project root; the wrapper will
include it automatically when present.
- This writes `phpstan-baseline.neon` at the project root and updates
`phpstan.neon` to include it.
- Use a baseline to suppress known issues in legacy code or core defaults
(for example, the shipped `settings.php` files), then work it down over
time. Avoid using it to hide new regressions.
- PHPStan config fallback:
- If no project `phpstan.neon*` exists, the wrapper uses the GitLab template
config shipped with the add-on.
- PHPStan config requirement:
- `ddev phpstan` requires project config (`phpstan.neon*`) unless you pass
`--configuration <path>`.
- PHPStan level:
- GitLab CI template defaults use level 0. The installer can set a local default level (0-10).

Expand Down
24 changes: 20 additions & 4 deletions commands/helpers/path-map.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ fi

# Fall back to compose metadata when the env var is not set.
if [ -z "$HOST_ROOT" ] && [ -f /mnt/ddev_config/.ddev-docker-compose-full.yaml ]; then
HOST_ROOT="$(awk -F': ' '/com\.ddev\.approot:/ {print $2; exit}' /mnt/ddev_config/.ddev-docker-compose-full.yaml)"
HOST_ROOT="$(awk -F': ' '/com\.ddev\.approot:/ {print $2; exit}' /mnt/ddev_config/.ddev-docker-compose-full.yaml | tr -d '"')"
fi
HOST_ROOT="${HOST_ROOT%/}"

# Read the docroot detected during install for non-standard Drupal layouts.
if [ -f /mnt/ddev_config/.dcq-docroot ]; then
Expand All @@ -37,11 +38,26 @@ map_path() {
echo "$path"
return
fi
# If the path is a host path under the project root, map it into the container.
# If the path is a host path under the project root, keep it host-native.
# Host-path alias parity is expected to make this resolvable in the container.
if [ -n "$HOST_ROOT" ] && [ "${path#${HOST_ROOT}/}" != "$path" ]; then
echo "${CONTAINER_ROOT}${path#${HOST_ROOT}}"
echo "$path"
return
fi
# Unknown path; return as-is.
echo "$path"
}

map_to_project_relative() {
local path="$1"
path="$(map_path "$path")"
if [ "${path#${CONTAINER_ROOT}/}" != "$path" ]; then
echo "${path#${CONTAINER_ROOT}/}"
return
fi
if [ -n "$HOST_ROOT" ] && [ "${path#${HOST_ROOT}/}" != "$path" ]; then
echo "${path#${HOST_ROOT}/}"
return
fi
# Unknown path; return as-is to avoid breaking user inputs.
echo "$path"
}
21 changes: 1 addition & 20 deletions commands/web/checks
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ if [ -z "$DOCROOT" ]; then
DOCROOT="web"
fi

# Collect custom code paths once so tools can reuse them.
CUSTOM_PATHS=()
for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do
if [ -d "$candidate" ]; then
CUSTOM_PATHS+=("$candidate")
fi
done

# Ordered list of Drupal.org GitLab CI template default tools to run.
TOOLS=(
"composer-validate"
Expand Down Expand Up @@ -66,18 +58,7 @@ for tool in "${TOOLS[@]}"; do
continue
fi

if [ "$tool" = "phpcs" ] && [ "${#CUSTOM_PATHS[@]}" -eq 0 ]; then
# Avoid failing when there is no custom code to lint.
echo "SKIP: no custom code directories found for phpcs." | tee -a "$log_file"
STATUS["$tool"]=SKIP
continue
fi

if [ "$tool" = "phpcs" ]; then
"$tool_path" "${CUSTOM_PATHS[@]}" >"$log_file" 2>&1
else
"$tool_path" >"$log_file" 2>&1
fi
"$tool_path" >"$log_file" 2>&1
exit_code=$?
if [ "$exit_code" -eq 0 ]; then
STATUS["$tool"]=PASS
Expand Down
21 changes: 1 addition & 20 deletions commands/web/checks-full
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ if [ -z "$DOCROOT" ]; then
DOCROOT="web"
fi

# Collect custom code paths once so tools can reuse them.
CUSTOM_PATHS=()
for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do
if [ -d "$candidate" ]; then
CUSTOM_PATHS+=("$candidate")
fi
done

# Ordered list of baseline Drupal.org GitLab CI template default tools.
TOOLS=(
"composer-validate"
Expand Down Expand Up @@ -82,18 +74,7 @@ for tool in "${ALL_TOOLS[@]}"; do
continue
fi

if [ "$tool" = "phpcs" ] && [ "${#CUSTOM_PATHS[@]}" -eq 0 ]; then
# Avoid failing when there is no custom code to lint.
echo "SKIP: no custom code directories found for phpcs." | tee -a "$log_file"
STATUS["$tool"]=SKIP
continue
fi

if [ "$tool" = "phpcs" ]; then
"$tool_path" "${CUSTOM_PATHS[@]}" >"$log_file" 2>&1
else
"$tool_path" >"$log_file" 2>&1
fi
"$tool_path" >"$log_file" 2>&1
exit_code=$?
if [ "$exit_code" -eq 0 ]; then
STATUS["$tool"]=PASS
Expand Down
46 changes: 28 additions & 18 deletions commands/web/cspell
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ CORE_CSPELL="${DOCROOT_PATH}/core/node_modules/.bin/cspell"
ROOT_CSPELL="${PROJECT_ROOT}/node_modules/.bin/cspell"
CSPELL_BIN=""

if ! command -v node >/dev/null 2>&1; then
echo "Node.js is not available in the DDEV web container." >&2
echo "Install the Drupal core JS toolchain (${DOCROOT}/core) to run CSpell." >&2
exit 127
fi

if [ -x "$ROOT_CSPELL" ]; then
CSPELL_BIN="$ROOT_CSPELL"
elif [ -x "$CORE_CSPELL" ]; then
Expand All @@ -85,21 +91,20 @@ fi

CMD=("$CSPELL_BIN")
if [ "$has_config" = false ]; then
# Prefer project config; warn when falling back to core.
if [ -f "${PROJECT_ROOT}/.cspell.json" ]; then
CMD+=(-c "${PROJECT_ROOT}/.cspell.json")
else
CMD+=(-c "${DOCROOT_PATH}/core/.cspell.json")
echo "Warning: using core CSpell config (${DOCROOT}/core/.cspell.json); project config not found." >&2
if [ ! -f "${PROJECT_ROOT}/.cspell.json" ]; then
echo "CSpell config file is missing. Create .cspell.json in the project root (for example by reinstalling the add-on)." >&2
exit 2
fi
CMD+=(-c "${PROJECT_ROOT}/.cspell.json")
project_words_file="${PROJECT_ROOT}/.cspell-project-words.txt"
if [ ! -f "$project_words_file" ]; then
: > "$project_words_file"
fi
fi

normalize_path() {
local path="$1"
path="$(map_path "$path")"
if [[ "$path" == /var/www/html/* ]]; then
path="${path#/var/www/html/}"
fi
path="$(map_to_project_relative "$path")"
if [ "$DOCROOT" != "web" ] && [[ "$path" == web/* ]]; then
path="${DOCROOT}/${path#web/}"
fi
Expand Down Expand Up @@ -134,12 +139,12 @@ while [ "$index" -lt "$arg_count" ]; do
;;
-c|--config)
next_arg="${args[$((index + 1))]:-}"
FLAG_ARGS+=("$arg" "$(map_path "$next_arg")")
FLAG_ARGS+=("$arg" "$next_arg")
index=$((index + 1))
;;
--config=*)
config_value="${arg#*=}"
FLAG_ARGS+=("--config=$(map_path "$config_value")")
FLAG_ARGS+=("--config=${config_value}")
;;
-*)
FLAG_ARGS+=("$arg")
Expand All @@ -152,14 +157,19 @@ while [ "$index" -lt "$arg_count" ]; do
done

if [ "$explicit_paths" = false ]; then
"${CMD[@]}" "${FLAG_ARGS[@]}" "."
DEFAULT_PATHS=()
for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do
if [ -d "${PROJECT_ROOT}/${candidate}" ]; then
DEFAULT_PATHS+=("$candidate")
fi
done
if [ "${#DEFAULT_PATHS[@]}" -eq 0 ]; then
echo "No custom code or sites directories found under ${DOCROOT}. Nothing to check." >&2
exit 0
fi
"${CMD[@]}" "${FLAG_ARGS[@]}" --exclude "${DOCROOT}/sites/*/files/**" "${DEFAULT_PATHS[@]}"
exit $?
fi

"${CMD[@]}" "${FLAG_ARGS[@]}" "${POSITIONAL_ARGS[@]}"
exit $?
if ! command -v node >/dev/null 2>&1; then
echo "Node.js is not available in the DDEV web container." >&2
echo "Install the Drupal core JS toolchain (${DOCROOT}/core) to run CSpell." >&2
exit 127
fi
49 changes: 30 additions & 19 deletions commands/web/cspell-suggest
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ DOCROOT_PATH="${PROJECT_ROOT}/${DOCROOT}"
CORE_CSPELL="${DOCROOT_PATH}/core/node_modules/.bin/cspell"
ROOT_CSPELL="${PROJECT_ROOT}/node_modules/.bin/cspell"
CSPELL_BIN=""

if ! command -v node >/dev/null 2>&1; then
echo "Node.js is not available in the DDEV web container." >&2
echo "Install the Drupal core JS toolchain (${DOCROOT}/core) to run CSpell." >&2
exit 127
fi

REPORT_DIR="${PROJECT_ROOT}/dcq-reports"
if ! mkdir -p "$REPORT_DIR"; then
echo "Unable to create report directory: $REPORT_DIR" >&2
Expand Down Expand Up @@ -104,23 +111,22 @@ if [ "$has_version" = true ]; then
exit $?
fi

PROJECT_DICTIONARY="${PROJECT_ROOT}/.cspell-project-words.txt"
CMD=("$CSPELL_BIN")
if [ "$has_config" = false ]; then
# Prefer project config; warn when falling back to core.
if [ -f "${PROJECT_ROOT}/.cspell.json" ]; then
CMD+=(-c "${PROJECT_ROOT}/.cspell.json")
else
CMD+=(-c "${DOCROOT_PATH}/core/.cspell.json")
echo "Warning: using core CSpell config (${DOCROOT}/core/.cspell.json); project config not found." >&2
if [ ! -f "${PROJECT_ROOT}/.cspell.json" ]; then
echo "CSpell config file is missing. Create .cspell.json in the project root (for example by reinstalling the add-on)." >&2
exit 2
fi
CMD+=(-c "${PROJECT_ROOT}/.cspell.json")
if [ ! -f "$PROJECT_DICTIONARY" ]; then
: > "$PROJECT_DICTIONARY"
fi
fi

normalize_path() {
local path="$1"
path="$(map_path "$path")"
if [[ "$path" == /var/www/html/* ]]; then
path="${path#/var/www/html/}"
fi
path="$(map_to_project_relative "$path")"
if [ "$DOCROOT" != "web" ] && [[ "$path" == web/* ]]; then
path="${DOCROOT}/${path#web/}"
fi
Expand Down Expand Up @@ -155,12 +161,12 @@ while [ "$index" -lt "$arg_count" ]; do
;;
-c|--config)
next_arg="${args[$((index + 1))]:-}"
FLAG_ARGS+=("$arg" "$(map_path "$next_arg")")
FLAG_ARGS+=("$arg" "$next_arg")
index=$((index + 1))
;;
--config=*)
config_value="${arg#*=}"
FLAG_ARGS+=("--config=$(map_path "$config_value")")
FLAG_ARGS+=("--config=${config_value}")
;;
-*)
FLAG_ARGS+=("$arg")
Expand All @@ -173,7 +179,18 @@ while [ "$index" -lt "$arg_count" ]; do
done

if [ "$explicit_paths" = false ]; then
POSITIONAL_ARGS=(".")
DEFAULT_PATHS=()
for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do
if [ -d "${PROJECT_ROOT}/${candidate}" ]; then
DEFAULT_PATHS+=("$candidate")
fi
done
if [ "${#DEFAULT_PATHS[@]}" -eq 0 ]; then
echo "No custom code or sites directories found under ${DOCROOT}. Nothing to check." >&2
exit 0
fi
POSITIONAL_ARGS=("${DEFAULT_PATHS[@]}")
FLAG_ARGS+=(--exclude "${DOCROOT}/sites/*/files/**")
fi

if [ "$explicit_paths" = false ]; then
Expand All @@ -189,7 +206,6 @@ else
: > "$UNRECOGNIZED_FILE"
fi

PROJECT_DICTIONARY="${PROJECT_ROOT}/.cspell-project-words.txt"
if [ -f "$PROJECT_DICTIONARY" ]; then
# Merge existing dictionary with new suggestions.
cat "$PROJECT_DICTIONARY" "$UNRECOGNIZED_FILE" | sort -u > "$UPDATED_WORDS_FILE"
Expand Down Expand Up @@ -219,8 +235,3 @@ if [ -s "$UNRECOGNIZED_FILE" ]; then
fi

exit 0
if ! command -v node >/dev/null 2>&1; then
echo "Node.js is not available in the DDEV web container." >&2
echo "Install the Drupal core JS toolchain (${DOCROOT}/core) to run CSpell." >&2
exit 127
fi
Loading