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
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J
- `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,26 +178,36 @@ 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 scanning `.` when no paths are passed; scope is
controlled by `.cspell.json` (especially `ignorePaths`).
- `.cspell-project-words.txt` is created by the installer (empty) and updated
by `ddev cspell-suggest` when you accept suggested words.
- ESLint / Stylelint / Prettier default scope:
- These wrappers default to scanning the configured docroot when no paths are
passed.
- Scope/exclusions are controlled by visible config files:
`.eslintignore`, `.stylelintignore`, and `.prettierignore`.
- Installer appends DCQ defaults to `.prettierignore` so the file remains the
single source of truth for Prettier scope.
- PHPCS / PHPCBF default scope:
- When a project `.phpcs.xml` is installed by the add-on, `ddev phpcs` and
`ddev phpcbf` with no path default to scanning the configured docroot.
- The generated ruleset excludes `__DOCROOT__/core/**`, `**/contrib/**`,
`**/node_modules/**`, and `__DOCROOT__/sites/*/files/**`.
- You can still pass explicit paths to narrow runs.
- PHP parallel lint scope:
- `ddev php-parallel-lint` remains wrapper-scoped because the tool does not
provide an equivalent project config file for default target paths.
- 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
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
25 changes: 14 additions & 11 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,12 +91,14 @@ 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

Expand Down Expand Up @@ -158,8 +166,3 @@ 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
27 changes: 15 additions & 12 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,14 +111,16 @@ 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

Expand Down Expand Up @@ -189,7 +198,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 +227,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
48 changes: 23 additions & 25 deletions commands/web/eslint
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,21 @@ if [ -n "$RESOLVE_PLUGINS_DIR" ]; then
# Ensure ESLint resolves plugins from the selected toolchain.
CMD+=(--resolve-plugins-relative-to "$RESOLVE_PLUGINS_DIR")
fi
FIXED_CONFIG=""
if [ -f "${PROJECT_ROOT}/.eslintrc.passing.json" ]; then
FIXED_CONFIG="${PROJECT_ROOT}/.eslintrc.passing.json"
elif [ -f "${PROJECT_ROOT}/.eslintrc.json" ]; then
FIXED_CONFIG="${PROJECT_ROOT}/.eslintrc.json"
fi
if [ "$has_config" = false ] && [ "$ESLINT_CONFIG_MODE" != "nearest" ]; then
# Fixed mode: force the passing config for Drupal.org GitLab CI template defaults when requested.
if [ -f "${PROJECT_ROOT}/.eslintrc.passing.json" ]; then
CMD+=(--config="${PROJECT_ROOT}/.eslintrc.passing.json")
else
CMD+=(--config="${DOCROOT_PATH}/core/.eslintrc.passing.json")
echo "Warning: using core ESLint config (${DOCROOT}/core/.eslintrc.passing.json); project config not found." >&2
# Fixed mode: prefer project passing config, then project base config.
if [ -z "$FIXED_CONFIG" ]; then
echo "ESLint config file is missing. Create .eslintrc.passing.json (or .eslintrc.json) in the project root, or pass --config." >&2
exit 2
fi
CMD+=(--config="$FIXED_CONFIG")
fi
CMD+=(--ext .js,.yml --ignore-pattern "**/node_modules/**")
CMD+=(--ext .js,.yml)
CMD+=("${DEFAULT_ARGS[@]}")

find_nearest_config() {
Expand All @@ -146,7 +151,7 @@ find_nearest_config() {
while true; do
# Walk up the directory tree to find the closest ESLint config.
for candidate in .eslintrc.passing.json .eslintrc .eslintrc.json .eslintrc.yaml .eslintrc.yml .eslintrc.js .eslintrc.cjs; do
if [ "$dir" = "web" ] || [ "$dir" = "./web" ]; then
if [ "$dir" = "$DOCROOT" ] || [ "$dir" = "./$DOCROOT" ]; then
# Skip docroot configs so nearest-mode prefers theme/module configs or root passing config.
continue
fi
Expand Down Expand Up @@ -272,31 +277,24 @@ fi

FILES=()
if [ "$explicit_paths" = false ]; then
DEFAULT_FILES=()
for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do
if [ -d "$candidate" ]; then
while IFS= read -r file_path; do
DEFAULT_FILES+=("$file_path")
done < <(find "$candidate" -path '*/node_modules/*' -prune -o -type f \( -name '*.js' -o -name '*.yml' \) -print)
fi
done
if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then
echo "No custom JS/YML files found under modules/custom, themes/custom, or profiles/custom. Nothing to lint." >&2
if [ ! -d "$DOCROOT" ]; then
echo "Configured docroot '$DOCROOT' does not exist. Nothing to lint." >&2
exit 2
fi
FILES=("${DEFAULT_FILES[@]}")
FILES=("$DOCROOT")
else
FILES=("${FINAL_ARGS[@]}")
fi

DEFAULT_CONFIG=""
if [ -f "${PROJECT_ROOT}/.eslintrc.passing.json" ]; then
DEFAULT_CONFIG="${PROJECT_ROOT}/.eslintrc.passing.json"
elif [ -f "${DOCROOT_PATH}/core/.eslintrc.passing.json" ]; then
DEFAULT_CONFIG="${DOCROOT_PATH}/core/.eslintrc.passing.json"
fi
DEFAULT_CONFIG="$FIXED_CONFIG"

if [ "$ESLINT_CONFIG_MODE" = "nearest" ]; then
if [ "$explicit_paths" = false ]; then
"${CMD[@]}" "${FLAGS_ARGS[@]}" "${FILES[@]}"
exit $?
fi

# Group files by nearest config to avoid plugin conflicts across modules/themes.
declare -A ESLINT_GROUPS
for file_path in "${FILES[@]}"; do
Expand Down Expand Up @@ -329,7 +327,7 @@ if [ "$ESLINT_CONFIG_MODE" = "nearest" ]; then
if [ -n "$plugins_dir" ]; then
group_cmd+=(--resolve-plugins-relative-to "$plugins_dir")
fi
group_cmd+=(--ext .js,.yml --ignore-pattern "**/node_modules/**")
group_cmd+=(--ext .js,.yml)
group_cmd+=("${DEFAULT_ARGS[@]}")
"${group_cmd[@]}" "${FLAGS_ARGS[@]}" "${filtered_files[@]}"
status=$?
Expand Down
Loading