From 379d152d6fca68e9c14f3b2e9e4c8da39b47f25f Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Mon, 2 Mar 2026 08:50:28 -0800 Subject: [PATCH 1/6] Require project PHPStan config and remove hidden fallback behavior --- README.md | 10 ++-- commands/web/phpstan | 129 +++---------------------------------------- tests/test.bats | 27 +++++++++ 3 files changed, 40 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index b94bad9..d2ae9a3 100644 --- a/README.md +++ b/README.md @@ -189,14 +189,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 `. - PHPStan level: - GitLab CI template defaults use level 0. The installer can set a local default level (0-10). diff --git a/commands/web/phpstan b/commands/web/phpstan index 9e64765..fd93bfe 100755 --- a/commands/web/phpstan +++ b/commands/web/phpstan @@ -3,17 +3,15 @@ set -u PHPSTAN_BIN="vendor/bin/phpstan" -CI_CONFIG="/mnt/ddev_config/drupal-code-quality/assets/phpstan.neon" TMP_FILES=() source /mnt/ddev_config/commands/helpers/path-map.sh -DOCROOT="${DCQ_DOCROOT:-web}" print_help() { cat <<'USAGE' Usage: ddev phpstan [args] Runs PHPStan inside the DDEV web container. This wrapper forwards all arguments -and applies Drupal CI config discovery when no explicit configuration is given. +and requires a project PHPStan config unless --configuration is provided. USAGE } @@ -31,18 +29,6 @@ cleanup() { done } -config_has_paths() { - local config_file="$1" - if [ -z "$config_file" ] || [ ! -f "$config_file" ]; then - return 1 - fi - # Check if config defines parameters.paths - if grep -q '^[[:space:]]*paths:' "$config_file"; then - return 0 - fi - return 1 -} - ensure_baseline_include() { local config_path="$1" local tmp @@ -81,9 +67,7 @@ ensure_baseline_include() { has_help=false has_version=false has_config=false -has_level=false has_json_format=false -explicit_paths=false has_generate_baseline=false config_value="" @@ -94,7 +78,6 @@ seen_double_dash=false while [ "$index" -lt "$arg_count" ]; do arg="${args[$index]}" if [ "$seen_double_dash" = true ]; then - explicit_paths=true break fi case "$arg" in @@ -144,19 +127,11 @@ while [ "$index" -lt "$arg_count" ]; do ;; --error-format=*) ;; - -l|--level) - has_level=true - index=$((index + 1)) - ;; - --level=*) - has_level=true - ;; analyze|analyse) ;; -*) ;; *) - explicit_paths=true ;; esac if [ "$has_help" = true ] || [ "$has_version" = true ]; then @@ -177,14 +152,7 @@ if [ "$has_version" = true ]; then fi CONFIG_FILE="" -DEFAULT_PATHS=() -EXCLUDE_PATHS=( - "${DOCROOT}/modules/contrib" - "${DOCROOT}/themes/contrib" - "${DOCROOT}/sites/*/files/*" - "sites/*/files/*" -) -# Prefer explicit project configs before falling back to CI template config. +# Prefer explicit project configs. for config_file in phpstan.neon phpstan.neon.dist phpstan.dist.neon; do if [ -f "$config_file" ]; then CONFIG_FILE="$config_file" @@ -192,32 +160,17 @@ for config_file in phpstan.neon phpstan.neon.dist phpstan.dist.neon; do fi done -if [ "$explicit_paths" = false ]; then - # Default to custom code only unless the user passes explicit paths. - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do - if [ -d "$candidate" ]; then - DEFAULT_PATHS+=("$candidate") - fi - done - if [ -d "${DOCROOT}/sites" ]; then - while IFS= read -r site_file; do - DEFAULT_PATHS+=("$site_file") - done < <(find "${DOCROOT}/sites" -type f \( -name '*.php' -o -name '*.inc' -o -name '*.module' -o -name '*.install' -o -name '*.theme' -o -name '*.profile' \) ! -path '*/files/*') - fi -fi - BASE_CONFIG="" CONFIG_TO_USE="" BASE_CONFIG_ABS="" -BASE_CONFIG_FOR_USE="" -BASELINE_PATH="/var/www/html/phpstan-baseline.neon" if [ "$has_config" = true ]; then BASE_CONFIG="$config_value" elif [ -n "$CONFIG_FILE" ]; then BASE_CONFIG="$CONFIG_FILE" -elif [ -f "$CI_CONFIG" ]; then - BASE_CONFIG="$CI_CONFIG" +else + echo "PHPStan config file is missing. Create phpstan.neon in the project root (for example by reinstalling the add-on) or pass --configuration ." >&2 + exit 2 fi if [ -n "$BASE_CONFIG" ]; then @@ -229,24 +182,6 @@ if [ -n "$BASE_CONFIG" ]; then fi fi -if [ -f "$BASELINE_PATH" ]; then - BASELINE_INCLUDE="$BASELINE_PATH" -elif [ -f phpstan-baseline.neon ]; then - BASELINE_INCLUDE="/var/www/html/phpstan-baseline.neon" -else - BASELINE_INCLUDE="" -fi - -BASE_CONFIG_FOR_USE="$BASE_CONFIG_ABS" -if [ -n "$BASE_CONFIG_ABS" ] && [ -n "$BASELINE_INCLUDE" ]; then - if ! grep -q 'phpstan-baseline\.neon' "$BASE_CONFIG_ABS"; then - BASE_CONFIG_FOR_USE="$(mktemp /tmp/phpstan-config.XXXXXX.neon)" - TMP_FILES+=("$BASE_CONFIG_FOR_USE") - printf "includes:\n - %s\n - %s\n" "$BASE_CONFIG_ABS" "$BASELINE_INCLUDE" > "$BASE_CONFIG_FOR_USE" - trap cleanup EXIT - fi -fi - FINAL_ARGS=() index=0 seen_double_dash=false @@ -265,19 +200,11 @@ while [ "$index" -lt "$arg_count" ]; do analyze|analyse) ;; -c|--configuration) - if [ "$has_config" = true ] && [ -n "$BASE_CONFIG_FOR_USE" ]; then - FINAL_ARGS+=("$arg" "$BASE_CONFIG_FOR_USE") - else - FINAL_ARGS+=("$arg" "${args[$((index + 1))]:-}") - fi + FINAL_ARGS+=("$arg" "${args[$((index + 1))]:-}") index=$((index + 1)) ;; --configuration=*) - if [ "$has_config" = true ] && [ -n "$BASE_CONFIG_FOR_USE" ]; then - FINAL_ARGS+=("--configuration=$BASE_CONFIG_FOR_USE") - else - FINAL_ARGS+=("$arg") - fi + FINAL_ARGS+=("$arg") ;; *) FINAL_ARGS+=("$(map_path "$arg")") @@ -287,35 +214,7 @@ while [ "$index" -lt "$arg_count" ]; do done if [ "$has_config" = false ]; then - if [ "$explicit_paths" = false ]; then - # Only inject scope/excludes if the config doesn't already define paths - if config_has_paths "$BASE_CONFIG_FOR_USE"; then - # Config already has paths defined; use it as-is - CONFIG_TO_USE="$BASE_CONFIG_FOR_USE" - else - # Build a minimal config that scopes analysis and excludes contrib/files. - CONFIG_TO_USE="$(mktemp /tmp/phpstan-scope.XXXXXX.neon)" - TMP_FILES+=("$CONFIG_TO_USE") - if [ -n "$BASE_CONFIG_FOR_USE" ]; then - printf "includes:\n - %s\n" "$BASE_CONFIG_FOR_USE" > "$CONFIG_TO_USE" - fi - project_root="$(pwd)" - cat <<'EOF' >> "$CONFIG_TO_USE" -parameters: - excludePaths: - analyseAndScan: -EOF - for exclude_path in "${EXCLUDE_PATHS[@]}"; do - if [ "${exclude_path#/}" = "$exclude_path" ]; then - exclude_path="${project_root}/${exclude_path}" - fi - printf " - %s (?)\n" "$exclude_path" >> "$CONFIG_TO_USE" - done - trap cleanup EXIT - fi - else - CONFIG_TO_USE="$BASE_CONFIG_FOR_USE" - fi + CONFIG_TO_USE="$BASE_CONFIG_ABS" fi CMD=("$PHPSTAN_BIN" analyze) @@ -323,18 +222,6 @@ if [ -n "$CONFIG_TO_USE" ]; then CMD+=(--configuration "$CONFIG_TO_USE") fi -if [ "$has_level" = false ] && [ -z "$CONFIG_FILE" ] && [ "$has_config" = false ] && [ ! -f "$CI_CONFIG" ]; then - # Match PHPStan's default when no config is present at all. - CMD+=(--level=0) -fi - -# Only add DEFAULT_PATHS if config doesn't define paths -if [ "$explicit_paths" = false ] && [ "${#DEFAULT_PATHS[@]}" -gt 0 ]; then - if ! config_has_paths "$CONFIG_TO_USE" && ! config_has_paths "$BASE_CONFIG_FOR_USE"; then - FINAL_ARGS+=("${DEFAULT_PATHS[@]}") - fi -fi - if [ "$has_json_format" = true ]; then if [ -n "${DDEV_HOST_PROJECT_ROOT:-}" ]; then HOST_ROOT="$DDEV_HOST_PROJECT_ROOT" diff --git a/tests/test.bats b/tests/test.bats index 7c03422..2c9e335 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -1040,6 +1040,33 @@ PHP esac } +@test "phpstan fails with helpful message when project config is missing" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + run rm -f phpstan.neon phpstan.neon.dist phpstan.dist.neon + assert_success + + mkdir -p vendor/bin + cat > vendor/bin/phpstan <<'SH' +#!/bin/sh +echo "stub phpstan" +exit 0 +SH + chmod +x vendor/bin/phpstan + + run wait_for_container_path "/var/www/html/vendor/bin/phpstan" + assert_success + + run ddev phpstan + assert_failure + assert_output --partial "PHPStan config file is missing." + assert_output --partial "Create phpstan.neon in the project root" +} + @test "cspell config is expanded during installation" { set -u -o pipefail From 0e2c4cf880ba80d69ffcd19c002169133771089b Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Mon, 2 Mar 2026 11:42:20 -0800 Subject: [PATCH 2/6] Fix stylelint-fix path rewrite for non-web docroots --- commands/web/stylelint-fix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/web/stylelint-fix b/commands/web/stylelint-fix index 6aa795e..212ea6e 100755 --- a/commands/web/stylelint-fix +++ b/commands/web/stylelint-fix @@ -192,7 +192,7 @@ normalize_docroot_arg() { arg="${arg#/var/www/html/}" fi if [ "$DOCROOT" != "web" ] && [[ "$arg" == web/* ]]; then - arg="${DOCROOT}/${path#web/}" + arg="${DOCROOT}/${arg#web/}" fi if [[ "$arg" == "${DOCROOT}/"* ]]; then arg="${arg#${DOCROOT}/}" From 89e87387f0b62ebabf2bdb5e612fddea64ac684a Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Mon, 2 Mar 2026 11:45:11 -0800 Subject: [PATCH 3/6] Add regression test for stylelint-fix non-web docroot path rewrite --- tests/test.bats | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test.bats b/tests/test.bats index 2c9e335..092ba4f 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -975,6 +975,40 @@ PY assert_success } +@test "stylelint-fix rewrites explicit web paths with non-web docroot" { + set -u -o pipefail + mkdir -p docroot + run ddev config --docroot=docroot + assert_success + retry_ddev_command ddev restart -y + assert_success + + run ddev add-on get "${DIR}" + assert_success + + mkdir -p node_modules/stylelint/bin + cat > node_modules/stylelint/bin/stylelint.mjs <<'JS' +#!/usr/bin/env node +process.exit(0); +JS + chmod +x node_modules/stylelint/bin/stylelint.mjs + + mkdir -p docroot/themes/custom/dcq_theme/css + cat > docroot/themes/custom/dcq_theme/css/fixable.css <<'CSS' +.dcq-test { + color: red; +} +CSS + + run wait_for_container_path "/var/www/html/node_modules/stylelint/bin/stylelint.mjs" + assert_success + run wait_for_container_path "/var/www/html/docroot/themes/custom/dcq_theme/css/fixable.css" + assert_success + + run ddev stylelint-fix web/themes/custom/dcq_theme/css/fixable.css + assert_success +} + @test "install from directory with phpstan level override" { set -u -o pipefail export DCQ_PHPSTAN_LEVEL=3 From 4cbd8afc5fb2db54f3bc82f23408dfb9c6a49f12 Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Mon, 2 Mar 2026 12:17:38 -0800 Subject: [PATCH 4/6] Align wrappers with project config and scope decisions --- README.md | 7 +- commands/web/checks | 21 +----- commands/web/checks-full | 21 +----- commands/web/cspell | 37 ++++++--- commands/web/cspell-suggest | 40 ++++++---- commands/web/eslint | 31 ++++---- commands/web/eslint-fix | 34 +++++---- commands/web/php-parallel-lint | 6 +- commands/web/phpcbf | 7 +- commands/web/phpcs | 7 +- commands/web/prettier | 19 ++--- commands/web/prettier-fix | 13 ++-- commands/web/stylelint | 21 ++++-- commands/web/stylelint-fix | 69 +++++++++++++---- tests/test.bats | 133 +++++++++++++++++++++++++++++++++ 15 files changed, 315 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index d2ae9a3..3607a78 100644 --- a/README.md +++ b/README.md @@ -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. @@ -177,8 +178,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: diff --git a/commands/web/checks b/commands/web/checks index 3567e37..a4a73de 100755 --- a/commands/web/checks +++ b/commands/web/checks @@ -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" @@ -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 diff --git a/commands/web/checks-full b/commands/web/checks-full index b44f297..f79cf80 100755 --- a/commands/web/checks-full +++ b/commands/web/checks-full @@ -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" @@ -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 diff --git a/commands/web/cspell b/commands/web/cspell index 82edf04..1bde9f1 100755 --- a/commands/web/cspell +++ b/commands/web/cspell @@ -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 @@ -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 @@ -152,14 +160,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 diff --git a/commands/web/cspell-suggest b/commands/web/cspell-suggest index 9c09785..593b68e 100755 --- a/commands/web/cspell-suggest +++ b/commands/web/cspell-suggest @@ -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 @@ -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 @@ -173,7 +182,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 @@ -189,7 +209,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" @@ -219,8 +238,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 diff --git a/commands/web/eslint b/commands/web/eslint index 98ba236..51237b1 100755 --- a/commands/web/eslint +++ b/commands/web/eslint @@ -127,14 +127,19 @@ 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+=("${DEFAULT_ARGS[@]}") @@ -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 @@ -273,15 +278,15 @@ fi FILES=() if [ "$explicit_paths" = false ]; then DEFAULT_FILES=() - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do + for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; 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) + done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -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 + echo "No default JS/YML files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to lint." >&2 exit 2 fi FILES=("${DEFAULT_FILES[@]}") @@ -290,11 +295,7 @@ else 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 # Group files by nearest config to avoid plugin conflicts across modules/themes. diff --git a/commands/web/eslint-fix b/commands/web/eslint-fix index bd76440..e5f0244 100755 --- a/commands/web/eslint-fix +++ b/commands/web/eslint-fix @@ -192,7 +192,7 @@ if [ "${#RAW_PATHS[@]}" -gt 0 ]; then done else # Default to custom code when no explicit paths are passed. - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do + for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do if [ -d "$candidate" ]; then TARGET_PREFIXES+=("$candidate") fi @@ -207,13 +207,18 @@ fi if [ -n "$RESOLVE_PLUGINS_DIR" ]; then CMD_BASE+=(--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 - if [ -f "${PROJECT_ROOT}/.eslintrc.passing.json" ]; then - CMD_BASE+=(--config="${PROJECT_ROOT}/.eslintrc.passing.json") - else - CMD_BASE+=(--config="${DOCROOT_PATH}/core/.eslintrc.passing.json") - echo "Warning: using core ESLint config (${DOCROOT}/core/.eslintrc.passing.json); project config not found." >&2 + 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_BASE+=(--config="$FIXED_CONFIG") fi CMD_BASE+=(--ext .js,.yml --ignore-pattern "**/node_modules/**") CMD_BASE+=("${DEFAULT_ARGS[@]}") @@ -223,7 +228,10 @@ find_nearest_config() { local dir dir="$(dirname "$file_path")" while true; do - for candidate in .eslintrc .eslintrc.json .eslintrc.yaml .eslintrc.yml .eslintrc.js .eslintrc.cjs; do + for candidate in .eslintrc.passing.json .eslintrc .eslintrc.json .eslintrc.yaml .eslintrc.yml .eslintrc.js .eslintrc.cjs; do + if [ "$dir" = "$DOCROOT" ] || [ "$dir" = "./$DOCROOT" ]; then + continue + fi if [ -f "$dir/$candidate" ]; then echo "$dir/$candidate" return 0 @@ -335,15 +343,15 @@ fi DEFAULT_FILES=() if [ "$explicit_paths" = false ]; then - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do + for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; 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) + done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -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 fix." >&2 + echo "No default JS/YML files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to fix." >&2 exit 2 fi fi @@ -356,11 +364,7 @@ else 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" # ============================================================================ # PREVIEW MODE: Generate patch, show preview, prompt to apply diff --git a/commands/web/php-parallel-lint b/commands/web/php-parallel-lint index 5b79bf2..1879cf8 100755 --- a/commands/web/php-parallel-lint +++ b/commands/web/php-parallel-lint @@ -46,17 +46,17 @@ if [ -z "$DOCROOT" ]; then fi DEFAULT_FILES=() -for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do +for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do if [ -d "$candidate" ]; then while IFS= read -r file_path; do DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" -type f \( -name '*.php' -o -name '*.inc' -o -name '*.module' -o -name '*.install' -o -name '*.theme' -o -name '*.profile' \)) + done < <(find "$candidate" \( -path '*/sites/*/files/*' -o -path '*/node_modules/*' \) -prune -o -type f \( -name '*.php' -o -name '*.inc' -o -name '*.module' -o -name '*.install' -o -name '*.theme' -o -name '*.profile' \) -print) fi done if [ "${#DEFAULT_FILES[@]}" -eq 0 ] && [ "$#" -eq 0 ]; then # Skip quietly if no custom PHP files are present. - echo "No custom PHP files found under ${DOCROOT}/modules/custom, ${DOCROOT}/themes/custom, or ${DOCROOT}/profiles/custom. Nothing to check." >&2 + echo "No default PHP files found under ${DOCROOT}/modules/custom, ${DOCROOT}/themes/custom, ${DOCROOT}/profiles/custom, or ${DOCROOT}/sites (excluding sites/*/files). Nothing to check." >&2 exit 0 fi diff --git a/commands/web/phpcbf b/commands/web/phpcbf index c9e826e..5c72d3d 100755 --- a/commands/web/phpcbf +++ b/commands/web/phpcbf @@ -60,8 +60,5 @@ if [ "$has_config" = true ]; then exit $? fi -"$PHPCBF_BIN" \ - --standard=Drupal \ - --extensions=php,module,inc,install,profile,theme,engine,yml \ - "${rewrite_args[@]}" -exit $? +echo "PHPCS config file is missing. Create .phpcs.xml in the project root (for example by reinstalling the add-on)." >&2 +exit 2 diff --git a/commands/web/phpcs b/commands/web/phpcs index 19a7150..4aaf8bc 100755 --- a/commands/web/phpcs +++ b/commands/web/phpcs @@ -59,8 +59,5 @@ if [ "$has_config" = true ]; then exit $? fi -"$PHPCS_BIN" \ - --standard=Drupal \ - --extensions=php,module,inc,install,profile,theme,engine,yml \ - "${rewrite_args[@]}" -exit $? +echo "PHPCS config file is missing. Create .phpcs.xml in the project root (for example by reinstalling the add-on)." >&2 +exit 2 diff --git a/commands/web/prettier b/commands/web/prettier index c41c396..b0ba50a 100755 --- a/commands/web/prettier +++ b/commands/web/prettier @@ -97,18 +97,13 @@ cd "$DOCROOT" CMD=(env "NODE_PATH=$NODE_PATH" "$PRETTIER_BIN" --check) if [ "$has_config" = false ]; then - # Prefer project configs; warn when falling back to core. - if [ -f "${PROJECT_ROOT}/.prettierrc.json" ]; then - CMD+=(--config="${PROJECT_ROOT}/.prettierrc.json") - else - CMD+=(--config="${DOCROOT_PATH}/core/.prettierrc.json") - echo "Warning: using core Prettier config (${DOCROOT}/core/.prettierrc.json); project config not found." >&2 + if [ ! -f "${PROJECT_ROOT}/.prettierrc.json" ]; then + echo "Prettier config file is missing. Create .prettierrc.json in the project root (for example by reinstalling the add-on)." >&2 + exit 2 fi + CMD+=(--config="${PROJECT_ROOT}/.prettierrc.json") if [ -f "${PROJECT_ROOT}/.prettierignore" ]; then CMD+=(--ignore-path="${PROJECT_ROOT}/.prettierignore") - elif [ -f "${DOCROOT_PATH}/core/.prettierignore" ]; then - CMD+=(--ignore-path="${DOCROOT_PATH}/core/.prettierignore") - echo "Warning: using core Prettier ignore (${DOCROOT}/core/.prettierignore); project ignore not found." >&2 fi fi @@ -178,15 +173,15 @@ fi if [ "$explicit_paths" = false ]; then # Default to custom code only when no explicit paths are passed. DEFAULT_FILES=() - for candidate in modules/custom themes/custom profiles/custom; do + for candidate in modules/custom themes/custom profiles/custom sites; do if [ -d "$candidate" ]; then while IFS= read -r file_path; do DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" -type f \( -name '*.js' -o -name '*.yml' -o -name '*.yaml' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \)) + done < <(find "$candidate" \( -path '*/node_modules/*' -o -path 'sites/*/files/*' \) -prune -o -type f \( -name '*.js' -o -name '*.yml' -o -name '*.yaml' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) fi done if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No custom files found under modules/custom, themes/custom, or profiles/custom. Nothing to check." >&2 + echo "No default files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to check." >&2 exit 0 fi "${CMD[@]}" "$@" "${DEFAULT_FILES[@]}" diff --git a/commands/web/prettier-fix b/commands/web/prettier-fix index 5b1a6de..143184b 100755 --- a/commands/web/prettier-fix +++ b/commands/web/prettier-fix @@ -165,7 +165,7 @@ if [ "${#RAW_PATHS[@]}" -gt 0 ]; then TARGET_PREFIXES+=("$(normalize_path "$raw")") done else - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do + for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do if [ -d "$candidate" ]; then TARGET_PREFIXES+=("$candidate") fi @@ -242,15 +242,15 @@ fi DEFAULT_FILES=() if [ "$explicit_paths" = false ]; then - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do + for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; 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' -o -name '*.yaml' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) + done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.js' -o -name '*.yml' -o -name '*.yaml' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) fi done if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No custom files found under modules/custom, themes/custom, or profiles/custom. Nothing to format." >&2 + echo "No default files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to format." >&2 exit 2 fi fi @@ -265,8 +265,9 @@ fi DEFAULT_CONFIG="" if [ -f "${PROJECT_ROOT}/.prettierrc.json" ]; then DEFAULT_CONFIG="${PROJECT_ROOT}/.prettierrc.json" -elif [ -f "${DOCROOT_PATH}/core/.prettierrc.json" ]; then - DEFAULT_CONFIG="${DOCROOT_PATH}/core/.prettierrc.json" +elif [ "$has_config" = false ]; then + echo "Prettier config file is missing. Create .prettierrc.json in the project root (for example by reinstalling the add-on)." >&2 + exit 2 fi # ============================================================================ diff --git a/commands/web/stylelint b/commands/web/stylelint index d9fcb4c..e4141d1 100755 --- a/commands/web/stylelint +++ b/commands/web/stylelint @@ -176,12 +176,10 @@ DEFAULT_CONFIG_PATH="" if [ "$has_config" = false ]; then if [ -f "${PROJECT_ROOT}/.stylelintrc.json" ]; then CONFIG_PATH="${PROJECT_ROOT}/.stylelintrc.json" - else - CONFIG_PATH="${DOCROOT_PATH}/core/.stylelintrc.json" - echo "Warning: using core Stylelint config (${DOCROOT}/core/.stylelintrc.json); project config not found." >&2 + config_dir="$(dirname "$CONFIG_PATH")" + CMD=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --config="$CONFIG_PATH" --config-basedir="$config_dir") fi DEFAULT_CONFIG_PATH="$CONFIG_PATH" - CMD+=(--config="$CONFIG_PATH") fi uses_scss=false @@ -262,15 +260,15 @@ fi if [ "$explicit_paths" = false ]; then DEFAULT_FILES=() - for candidate in modules/custom themes/custom profiles/custom; do + for candidate in modules/custom themes/custom profiles/custom sites; do if [ -d "$candidate" ]; then while IFS= read -r file_path; do DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" -type f \( -name '*.css' -o -name '*.scss' -o -name '*.sass' \)) + done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) fi done if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No custom style files found under modules/custom, themes/custom, or profiles/custom. Nothing to lint." >&2 + echo "No default style files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to lint." >&2 exit 2 fi if [ "$has_config" = false ] && [ "$CONFIG_PATH" = "$DEFAULT_CONFIG_PATH" ]; then @@ -289,6 +287,10 @@ if [ "$explicit_paths" = false ]; then fi done fi + if [ "$has_config" = false ] && [ -z "$CONFIG_PATH" ]; then + echo "Stylelint config file is missing. Create .stylelintrc.json in the project root (or a nearest config in the target paths), or pass --config." >&2 + exit 2 + fi for file_path in "${DEFAULT_FILES[@]}"; do if [[ "$file_path" == *.scss || "$file_path" == *.sass ]]; then uses_scss=true @@ -336,6 +338,11 @@ if [ "$explicit_paths" = false ]; then exit $? fi +if [ "$has_config" = false ] && [ -z "$CONFIG_PATH" ]; then + echo "Stylelint config file is missing. Create .stylelintrc.json in the project root (or a nearest config in the target paths), or pass --config." >&2 + exit 2 +fi + if [ "$uses_scss" = true ] && config_supports_scss "$CONFIG_PATH"; then scss_allowed=true fi diff --git a/commands/web/stylelint-fix b/commands/web/stylelint-fix index 212ea6e..7d39592 100755 --- a/commands/web/stylelint-fix +++ b/commands/web/stylelint-fix @@ -129,6 +129,27 @@ normalize_path() { echo "$path" } +find_stylelint_config() { + local path="$1" + local dir="$path" + if [[ "$dir" == /var/www/html/* ]]; then + dir="${dir#/var/www/html/}" + fi + if [ -f "${PROJECT_ROOT}/${dir}" ]; then + dir="$(dirname "$dir")" + fi + while [ "$dir" != "." ] && [ "$dir" != "/" ]; do + for candidate in .stylelintrc .stylelintrc.json .stylelintrc.yaml .stylelintrc.yml .stylelintrc.js; do + if [ -f "${PROJECT_ROOT}/${dir}/${candidate}" ]; then + echo "${PROJECT_ROOT}/${dir}/${candidate}" + return 0 + fi + done + dir="$(dirname "$dir")" + done + return 1 +} + TARGET_PREFIXES=() RAW_PATHS=() seen_double_dash=false @@ -165,7 +186,7 @@ if [ "${#RAW_PATHS[@]}" -gt 0 ]; then TARGET_PREFIXES+=("$(normalize_path "$raw")") done else - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do + for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do if [ -d "$candidate" ]; then TARGET_PREFIXES+=("$candidate") fi @@ -242,15 +263,15 @@ fi DEFAULT_FILES=() if [ "$explicit_paths" = false ]; then - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom"; do + for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; 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 '*.css' -o -name '*.scss' \) -print) + done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) fi done if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No custom CSS/SCSS files found under modules/custom, themes/custom, or profiles/custom. Nothing to fix." >&2 + echo "No default CSS/SCSS/Sass files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to fix." >&2 exit 2 fi fi @@ -262,11 +283,29 @@ else FILES=("${FINAL_ARGS[@]}") fi -DEFAULT_CONFIG="" -if [ -f "${PROJECT_ROOT}/.stylelintrc.json" ]; then - DEFAULT_CONFIG="${PROJECT_ROOT}/.stylelintrc.json" -elif [ -f "${DOCROOT_PATH}/core/.stylelintrc.json" ]; then - DEFAULT_CONFIG="${DOCROOT_PATH}/core/.stylelintrc.json" +CONFIG_PATH="" +if [ "$has_config" = false ]; then + if [ -f "${PROJECT_ROOT}/.stylelintrc.json" ]; then + CONFIG_PATH="${PROJECT_ROOT}/.stylelintrc.json" + else + for file_path in "${FILES[@]}"; do + if [[ "$file_path" == -* ]] || [ "$file_path" = "--" ]; then + continue + fi + if config_candidate="$(find_stylelint_config "$file_path")"; then + CONFIG_PATH="$config_candidate" + break + fi + done + fi + if [ -z "$CONFIG_PATH" ]; then + echo "Stylelint config file is missing. Create .stylelintrc.json in the project root (or a nearest config in the target paths), or pass --config." >&2 + exit 2 + fi + config_dir="$(dirname "$CONFIG_PATH")" + if [ -d "${config_dir}/node_modules" ]; then + NODE_PATH="${NODE_PATH}:${config_dir}/node_modules" + fi fi # ============================================================================ @@ -293,8 +332,8 @@ if [ "$preview_mode" = true ]; then . | (cd "$tmp_root" && tar -xf -) preview_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN") - if [ -n "$DEFAULT_CONFIG" ] && [ "$has_config" = false ]; then - preview_cmd+=(--config "$DEFAULT_CONFIG") + if [ "$has_config" = false ]; then + preview_cmd+=(--config "$CONFIG_PATH" --config-basedir "$(dirname "$CONFIG_PATH")") fi preview_cmd+=(--fix) @@ -349,8 +388,8 @@ if [ "$preview_mode" = true ]; then # Apply fixes cd "$PROJECT_ROOT" fix_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN") - if [ -n "$DEFAULT_CONFIG" ] && [ "$has_config" = false ]; then - fix_cmd+=(--config "$DEFAULT_CONFIG") + if [ "$has_config" = false ]; then + fix_cmd+=(--config "$CONFIG_PATH" --config-basedir "$(dirname "$CONFIG_PATH")") fi fix_cmd+=(--fix) @@ -375,8 +414,8 @@ fi cd "$PROJECT_ROOT" fix_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN") -if [ -n "$DEFAULT_CONFIG" ] && [ "$has_config" = false ]; then - fix_cmd+=(--config "$DEFAULT_CONFIG") +if [ "$has_config" = false ]; then + fix_cmd+=(--config "$CONFIG_PATH" --config-basedir "$(dirname "$CONFIG_PATH")") fi fix_cmd+=(--fix) diff --git a/tests/test.bats b/tests/test.bats index 092ba4f..e74b0d7 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -1009,6 +1009,139 @@ CSS assert_success } +@test "eslint fixed mode falls back to .eslintrc.json when passing config is missing" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + run rm -f .eslintrc.passing.json + assert_success + + mkdir -p web/modules/custom/dcq_test/js + cat > web/modules/custom/dcq_test/js/fixed-mode.js <<'JS' +const x = 1; +JS + + mkdir -p node_modules/eslint/bin + cat > node_modules/eslint/bin/eslint.js <<'JS' +#!/usr/bin/env node +process.stdout.write(process.argv.slice(2).join("\n")); +JS + chmod +x node_modules/eslint/bin/eslint.js + + run wait_for_container_path "/var/www/html/node_modules/eslint/bin/eslint.js" + assert_success + + run ddev exec bash -lc 'cd /var/www/html && ESLINT_CONFIG_MODE=fixed ./.ddev/commands/web/eslint web/modules/custom/dcq_test/js/fixed-mode.js' + assert_success + assert_output --partial "--config=/var/www/html/.eslintrc.json" +} + +@test "stylelint-fix fails with helpful message when project config is missing" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + run rm -f .stylelintrc.json .stylelintrc .stylelintrc.yaml .stylelintrc.yml .stylelintrc.js + assert_success + + mkdir -p node_modules/stylelint/bin + cat > node_modules/stylelint/bin/stylelint.mjs <<'JS' +#!/usr/bin/env node +process.exit(0); +JS + chmod +x node_modules/stylelint/bin/stylelint.mjs + + run wait_for_container_path "/var/www/html/node_modules/stylelint/bin/stylelint.mjs" + assert_success + + run ddev stylelint-fix web/themes/custom/dcq_theme/css/fixable.css + assert_failure + assert_output --partial "Stylelint config file is missing." +} + +@test "stylelint-fix uses nearest config when root config is missing" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + run rm -f .stylelintrc.json .stylelintrc .stylelintrc.yaml .stylelintrc.yml .stylelintrc.js + assert_success + + mkdir -p web/themes/custom/dcq_theme/css + cat > web/themes/custom/dcq_theme/.stylelintrc.json <<'JSON' +{ + "rules": {} +} +JSON + cat > web/themes/custom/dcq_theme/css/fixable.css <<'CSS' +a { + color: RED; +} +CSS + + mkdir -p node_modules/stylelint/bin + cat > node_modules/stylelint/bin/stylelint.mjs <<'JS' +#!/usr/bin/env node +process.stdout.write(process.argv.slice(2).join("\n")); +JS + chmod +x node_modules/stylelint/bin/stylelint.mjs + + run wait_for_container_path "/var/www/html/node_modules/stylelint/bin/stylelint.mjs" + assert_success + + run ddev stylelint-fix web/themes/custom/dcq_theme/css/fixable.css + assert_success + assert_output --partial "--config" + assert_output --partial "/var/www/html/web/themes/custom/dcq_theme/.stylelintrc.json" +} + +@test "prettier-fix fails with helpful message when project config is missing" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + run rm -f .prettierrc.json + assert_success + + mkdir -p node_modules/prettier/bin + cat > node_modules/prettier/bin/prettier.cjs <<'JS' +#!/usr/bin/env node +process.exit(0); +JS + chmod +x node_modules/prettier/bin/prettier.cjs + + run wait_for_container_path "/var/www/html/node_modules/prettier/bin/prettier.cjs" + assert_success + + run ddev prettier-fix web/themes/custom/dcq_theme/js/prettier.js + assert_failure + assert_output --partial "Prettier config file is missing." +} + +@test "checks runs phpcs without forcing explicit paths" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + run ddev exec bash -lc $'for cmd in composer-validate php-parallel-lint phpstan eslint stylelint prettier cspell; do\ncat > "/var/www/html/.ddev/commands/web/\\${cmd}" <<\'SH\'\n#!/usr/bin/env bash\nexit 0\nSH\nchmod +x "/var/www/html/.ddev/commands/web/\\${cmd}"\ndone\ncat > /var/www/html/.ddev/commands/web/phpcs <<\'SH\'\n#!/usr/bin/env bash\nif [ "$#" -ne 0 ]; then\n echo "unexpected phpcs args: $*" >&2\n exit 23\nfi\nexit 0\nSH\nchmod +x /var/www/html/.ddev/commands/web/phpcs' + assert_success + + run ddev checks + assert_success + assert_output --partial "- phpcs: PASS" +} + @test "install from directory with phpstan level override" { set -u -o pipefail export DCQ_PHPSTAN_LEVEL=3 From 72fdaaf1ddc23185a4128939c89e927b073b731e Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Mon, 2 Mar 2026 15:18:12 -0800 Subject: [PATCH 5/6] Align fresh-install cspell expectation with default scope --- tests/test.bats | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test.bats b/tests/test.bats index e74b0d7..382e255 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -1463,7 +1463,12 @@ SH run ./.ddev/drupal-code-quality/tooling/bin/cspell assert_failure assert_output --partial "modlue" - assert_output --partial "roottypo" + case "$output" in + *"roottypo"*) + echo "Expected default cspell scope to exclude project-root files like cspell-test.md." + return 1 + ;; + esac before_phpcbf="$(read_container_file /var/www/html/web/modules/custom/dcq_test/dcq_fixable.php)" run ./.ddev/drupal-code-quality/tooling/bin/phpcbf web/modules/custom/dcq_test/dcq_fixable.php From 53a261bfb3cb3ab3525d94b7beca34aa65ce201f Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Tue, 3 Mar 2026 23:18:34 -0800 Subject: [PATCH 6/6] Move scan scope control into visible config files --- README.md | 14 +++- commands/web/cspell | 12 +-- commands/web/cspell-suggest | 13 +-- commands/web/eslint | 23 +++-- commands/web/eslint-fix | 33 +++----- commands/web/prettier | 15 +--- commands/web/prettier-fix | 18 +--- commands/web/stylelint | 22 ++++- commands/web/stylelint-fix | 47 ++++++++--- dcq-install.sh | 47 +++++++++++ drupal-code-quality/assets/.eslintignore | 9 ++ drupal-code-quality/assets/.stylelintignore | 9 ++ .../config-amendments/.prettierignore.dcq | 9 ++ tests/test.bats | 84 +++++++++++++++++-- 14 files changed, 245 insertions(+), 110 deletions(-) create mode 100644 drupal-code-quality/assets/.eslintignore create mode 100644 drupal-code-quality/assets/.stylelintignore create mode 100644 drupal-code-quality/config-amendments/.prettierignore.dcq diff --git a/README.md b/README.md index 3607a78..860d96b 100644 --- a/README.md +++ b/README.md @@ -178,16 +178,26 @@ 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` defaults to custom code plus `sites` under the configured - docroot, excluding `sites/*/files/**`, when no paths are passed. + - `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 and updates diff --git a/commands/web/cspell b/commands/web/cspell index 1bde9f1..bdb4dab 100755 --- a/commands/web/cspell +++ b/commands/web/cspell @@ -160,17 +160,7 @@ while [ "$index" -lt "$arg_count" ]; do done if [ "$explicit_paths" = false ]; then - 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[@]}" + "${CMD[@]}" "${FLAG_ARGS[@]}" "." exit $? fi diff --git a/commands/web/cspell-suggest b/commands/web/cspell-suggest index 593b68e..d9ce221 100755 --- a/commands/web/cspell-suggest +++ b/commands/web/cspell-suggest @@ -182,18 +182,7 @@ while [ "$index" -lt "$arg_count" ]; do done if [ "$explicit_paths" = false ]; then - 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/**") + POSITIONAL_ARGS=(".") fi if [ "$explicit_paths" = false ]; then diff --git a/commands/web/eslint b/commands/web/eslint index 51237b1..ec60d08 100755 --- a/commands/web/eslint +++ b/commands/web/eslint @@ -141,7 +141,7 @@ if [ "$has_config" = false ] && [ "$ESLINT_CONFIG_MODE" != "nearest" ]; then fi CMD+=(--config="$FIXED_CONFIG") fi -CMD+=(--ext .js,.yml --ignore-pattern "**/node_modules/**") +CMD+=(--ext .js,.yml) CMD+=("${DEFAULT_ARGS[@]}") find_nearest_config() { @@ -277,19 +277,11 @@ fi FILES=() if [ "$explicit_paths" = false ]; then - DEFAULT_FILES=() - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do - if [ -d "$candidate" ]; then - while IFS= read -r file_path; do - DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.js' -o -name '*.yml' \) -print) - fi - done - if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No default JS/YML files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). 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 @@ -298,6 +290,11 @@ DEFAULT_CONFIG="" 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 @@ -330,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=$? diff --git a/commands/web/eslint-fix b/commands/web/eslint-fix index e5f0244..dd225d5 100755 --- a/commands/web/eslint-fix +++ b/commands/web/eslint-fix @@ -191,12 +191,7 @@ if [ "${#RAW_PATHS[@]}" -gt 0 ]; then TARGET_PREFIXES+=("$(normalize_path "$raw")") done else - # Default to custom code when no explicit paths are passed. - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do - if [ -d "$candidate" ]; then - TARGET_PREFIXES+=("$candidate") - fi - done + TARGET_PREFIXES+=("$DOCROOT") fi CMD_BASE=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN") @@ -220,7 +215,7 @@ if [ "$has_config" = false ] && [ "$ESLINT_CONFIG_MODE" != "nearest" ]; then fi CMD_BASE+=(--config="$FIXED_CONFIG") fi -CMD_BASE+=(--ext .js,.yml --ignore-pattern "**/node_modules/**") +CMD_BASE+=(--ext .js,.yml) CMD_BASE+=("${DEFAULT_ARGS[@]}") find_nearest_config() { @@ -343,17 +338,11 @@ fi DEFAULT_FILES=() if [ "$explicit_paths" = false ]; then - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do - if [ -d "$candidate" ]; then - while IFS= read -r file_path; do - DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.js' -o -name '*.yml' \) -print) - fi - done - if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No default JS/YML files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to fix." >&2 + if [ ! -d "$DOCROOT" ]; then + echo "Configured docroot '$DOCROOT' does not exist. Nothing to fix." >&2 exit 2 fi + DEFAULT_FILES=("$DOCROOT") fi FILES=() @@ -390,7 +379,7 @@ if [ "$preview_mode" = true ]; then --exclude='*/.git/*' \ . | (cd "$tmp_root" && tar -xf -) - if [ "$ESLINT_CONFIG_MODE" = "nearest" ]; then + if [ "$ESLINT_CONFIG_MODE" = "nearest" ] && [ "$explicit_paths" = true ]; then # Group files by nearest config to avoid plugin conflicts across modules/themes. declare -A ESLINT_GROUPS for file_path in "${FILES[@]}"; do @@ -431,7 +420,7 @@ if [ "$preview_mode" = true ]; then if [ -n "$plugins_dir" ]; then group_cmd+=(--resolve-plugins-relative-to "$plugins_dir") fi - group_cmd+=(--ext .js,.yml --ignore-pattern "**/node_modules/**" --fix) + group_cmd+=(--ext .js,.yml --fix) group_cmd+=("${DEFAULT_ARGS[@]}") (cd "$tmp_root" && "${group_cmd[@]}" "${filtered_files[@]}") status=$? @@ -491,7 +480,7 @@ if [ "$preview_mode" = true ]; then # Apply fixes cd "$PROJECT_ROOT" - if [ "$ESLINT_CONFIG_MODE" = "nearest" ]; then + if [ "$ESLINT_CONFIG_MODE" = "nearest" ] && [ "$explicit_paths" = true ]; then fix_exit=0 for config_path in "${!ESLINT_GROUPS[@]}"; do mapfile -t group_files <<< "${ESLINT_GROUPS[$config_path]}" @@ -518,7 +507,7 @@ if [ "$preview_mode" = true ]; then if [ -n "$plugins_dir" ]; then group_cmd+=(--resolve-plugins-relative-to "$plugins_dir") fi - group_cmd+=(--ext .js,.yml --ignore-pattern "**/node_modules/**" --fix) + group_cmd+=(--ext .js,.yml --fix) group_cmd+=("${DEFAULT_ARGS[@]}") "${group_cmd[@]}" "${filtered_files[@]}" status=$? @@ -550,7 +539,7 @@ fi # ============================================================================ cd "$PROJECT_ROOT" -if [ "$ESLINT_CONFIG_MODE" = "nearest" ]; then +if [ "$ESLINT_CONFIG_MODE" = "nearest" ] && [ "$explicit_paths" = true ]; then # Group files by nearest config to avoid plugin conflicts across modules/themes. declare -A ESLINT_GROUPS for file_path in "${FILES[@]}"; do @@ -590,7 +579,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/**" --fix) + group_cmd+=(--ext .js,.yml --fix) group_cmd+=("${DEFAULT_ARGS[@]}") "${group_cmd[@]}" "${filtered_files[@]}" status=$? diff --git a/commands/web/prettier b/commands/web/prettier index b0ba50a..e48eb26 100755 --- a/commands/web/prettier +++ b/commands/web/prettier @@ -171,20 +171,7 @@ if [ "$explicit_paths" = true ]; then fi if [ "$explicit_paths" = false ]; then - # Default to custom code only when no explicit paths are passed. - DEFAULT_FILES=() - for candidate in modules/custom themes/custom profiles/custom sites; do - if [ -d "$candidate" ]; then - while IFS= read -r file_path; do - DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" \( -path '*/node_modules/*' -o -path 'sites/*/files/*' \) -prune -o -type f \( -name '*.js' -o -name '*.yml' -o -name '*.yaml' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) - fi - done - if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No default files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to check." >&2 - exit 0 - fi - "${CMD[@]}" "$@" "${DEFAULT_FILES[@]}" + "${CMD[@]}" "$@" "." exit $? fi diff --git a/commands/web/prettier-fix b/commands/web/prettier-fix index 143184b..02cc737 100755 --- a/commands/web/prettier-fix +++ b/commands/web/prettier-fix @@ -165,11 +165,7 @@ if [ "${#RAW_PATHS[@]}" -gt 0 ]; then TARGET_PREFIXES+=("$(normalize_path "$raw")") done else - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do - if [ -d "$candidate" ]; then - TARGET_PREFIXES+=("$candidate") - fi - done + TARGET_PREFIXES+=("$DOCROOT") fi FINAL_ARGS=() @@ -242,17 +238,11 @@ fi DEFAULT_FILES=() if [ "$explicit_paths" = false ]; then - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do - if [ -d "$candidate" ]; then - while IFS= read -r file_path; do - DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.js' -o -name '*.yml' -o -name '*.yaml' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) - fi - done - if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No default files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to format." >&2 + if [ ! -d "$DOCROOT" ]; then + echo "Configured docroot '$DOCROOT' does not exist. Nothing to format." >&2 exit 2 fi + DEFAULT_FILES=("$DOCROOT") fi FILES=() diff --git a/commands/web/stylelint b/commands/web/stylelint index e4141d1..68debf0 100755 --- a/commands/web/stylelint +++ b/commands/web/stylelint @@ -24,6 +24,7 @@ has_help=false has_version=false has_positional=false has_config=false +has_ignore_path=false seen_double_dash=false explicit_paths=false @@ -46,6 +47,9 @@ for arg in "$@"; do -c|--config|--config=*) has_config=true ;; + --ignore-path|--ignore-path=*) + has_ignore_path=true + ;; --) ;; -* ) @@ -181,6 +185,9 @@ if [ "$has_config" = false ]; then fi DEFAULT_CONFIG_PATH="$CONFIG_PATH" fi +if [ "$has_ignore_path" = false ] && [ -f "${PROJECT_ROOT}/.stylelintignore" ]; then + CMD+=(--ignore-path="${PROJECT_ROOT}/.stylelintignore") +fi uses_scss=false has_scss_parser=false @@ -211,11 +218,20 @@ if [ "$explicit_paths" = true ]; then FINAL_ARGS+=("$arg" "$next_arg") index=$((index + 1)) ;; + --ignore-path) + next_arg="${args[$((index + 1))]:-}" + FINAL_ARGS+=("$arg" "$(map_path "$next_arg")") + index=$((index + 1)) + ;; --config=*) config_value="${arg#*=}" config_value="$(normalize_docroot_arg "$config_value")" FINAL_ARGS+=("--config=$config_value") ;; + --ignore-path=*) + ignore_value="${arg#*=}" + FINAL_ARGS+=("--ignore-path=$(map_path "$ignore_value")") + ;; -* ) FINAL_ARGS+=("$arg") ;; @@ -260,15 +276,15 @@ fi if [ "$explicit_paths" = false ]; then DEFAULT_FILES=() - for candidate in modules/custom themes/custom profiles/custom sites; do + for candidate in .; do if [ -d "$candidate" ]; then while IFS= read -r file_path; do DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) + done < <(find "$candidate" -path '*/node_modules/*' -prune -o -type f \( -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) fi done if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No default style files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to lint." >&2 + echo "No style files found under ${DOCROOT}. Nothing to lint." >&2 exit 2 fi if [ "$has_config" = false ] && [ "$CONFIG_PATH" = "$DEFAULT_CONFIG_PATH" ]; then diff --git a/commands/web/stylelint-fix b/commands/web/stylelint-fix index 7d39592..009393c 100755 --- a/commands/web/stylelint-fix +++ b/commands/web/stylelint-fix @@ -33,6 +33,7 @@ has_help=false has_version=false has_positional=false has_config=false +has_ignore_path=false seen_double_dash=false explicit_paths=false preview_mode=false @@ -65,6 +66,9 @@ for arg in "${clean_args[@]}"; do -c|--config|--config=*) has_config=true ;; + --ignore-path|--ignore-path=*) + has_ignore_path=true + ;; -* ) ;; *) @@ -172,6 +176,11 @@ while [ "$index" -lt "$arg_count" ]; do ;; --config=*) ;; + --ignore-path) + index=$((index + 1)) + ;; + --ignore-path=*) + ;; -*) ;; *) @@ -186,11 +195,7 @@ if [ "${#RAW_PATHS[@]}" -gt 0 ]; then TARGET_PREFIXES+=("$(normalize_path "$raw")") done else - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do - if [ -d "$candidate" ]; then - TARGET_PREFIXES+=("$candidate") - fi - done + TARGET_PREFIXES+=("$DOCROOT") fi FINAL_ARGS=() @@ -246,10 +251,19 @@ if [ "$explicit_paths" = true ]; then FINAL_ARGS+=("$arg" "$(normalize_config_path "$next_arg")") index=$((index + 1)) ;; + --ignore-path) + next_arg="${args[$((index + 1))]:-}" + FINAL_ARGS+=("$arg" "$(map_path "$next_arg")") + index=$((index + 1)) + ;; --config=*) config_value="${arg#*=}" FINAL_ARGS+=("--config=$(normalize_config_path "$config_value")") ;; + --ignore-path=*) + ignore_value="${arg#*=}" + FINAL_ARGS+=("--ignore-path=$(map_path "$ignore_value")") + ;; -*) FINAL_ARGS+=("$arg") ;; @@ -263,15 +277,15 @@ fi DEFAULT_FILES=() if [ "$explicit_paths" = false ]; then - for candidate in "${DOCROOT}/modules/custom" "${DOCROOT}/themes/custom" "${DOCROOT}/profiles/custom" "${DOCROOT}/sites"; do - if [ -d "$candidate" ]; then - while IFS= read -r file_path; do - DEFAULT_FILES+=("$file_path") - done < <(find "$candidate" \( -path '*/node_modules/*' -o -path '*/sites/*/files/*' \) -prune -o -type f \( -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) - fi - done + if [ ! -d "$DOCROOT" ]; then + echo "Configured docroot '$DOCROOT' does not exist. Nothing to fix." >&2 + exit 2 + fi + while IFS= read -r file_path; do + DEFAULT_FILES+=("$file_path") + done < <(find "$DOCROOT" -path '*/node_modules/*' -prune -o -type f \( -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -print) if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then - echo "No default CSS/SCSS/Sass files found under modules/custom, themes/custom, profiles/custom, or sites (excluding sites/*/files). Nothing to fix." >&2 + echo "No CSS/SCSS/Sass files found under ${DOCROOT}. Nothing to fix." >&2 exit 2 fi fi @@ -307,6 +321,10 @@ if [ "$has_config" = false ]; then NODE_PATH="${NODE_PATH}:${config_dir}/node_modules" fi fi +IGNORE_PATH_ARGS=() +if [ "$has_ignore_path" = false ] && [ -f "${PROJECT_ROOT}/.stylelintignore" ]; then + IGNORE_PATH_ARGS=(--ignore-path "${PROJECT_ROOT}/.stylelintignore") +fi # ============================================================================ # PREVIEW MODE: Generate patch, show preview, prompt to apply @@ -335,6 +353,7 @@ if [ "$preview_mode" = true ]; then if [ "$has_config" = false ]; then preview_cmd+=(--config "$CONFIG_PATH" --config-basedir "$(dirname "$CONFIG_PATH")") fi + preview_cmd+=("${IGNORE_PATH_ARGS[@]}") preview_cmd+=(--fix) (cd "$tmp_root" && "${preview_cmd[@]}" "${FILES[@]}") @@ -391,6 +410,7 @@ if [ "$preview_mode" = true ]; then if [ "$has_config" = false ]; then fix_cmd+=(--config "$CONFIG_PATH" --config-basedir "$(dirname "$CONFIG_PATH")") fi + fix_cmd+=("${IGNORE_PATH_ARGS[@]}") fix_cmd+=(--fix) "${fix_cmd[@]}" "${FILES[@]}" @@ -417,6 +437,7 @@ fix_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN") if [ "$has_config" = false ]; then fix_cmd+=(--config "$CONFIG_PATH" --config-basedir "$(dirname "$CONFIG_PATH")") fi +fix_cmd+=("${IGNORE_PATH_ARGS[@]}") fix_cmd+=(--fix) "${fix_cmd[@]}" "${FILES[@]}" diff --git a/dcq-install.sh b/dcq-install.sh index 4e83b9b..a137870 100644 --- a/dcq-install.sh +++ b/dcq-install.sh @@ -930,6 +930,40 @@ merge_phpcs_config() { rm -f "$tmp" "$merged" } +append_unique_lines_from_file() { + local target="$1" + local source="$2" + local tmp + local line + + if [ ! -f "$source" ]; then + return 0 + fi + + tmp="$(mktemp "${TMPDIR:-/tmp}/dcq-lines-XXXXXX")" + if [ -f "$target" ]; then + cat "$target" >"$tmp" + else + : >"$tmp" + fi + + while IFS= read -r line || [ -n "$line" ]; do + if [ "$line" = "#ddev-generated" ]; then + continue + fi + if grep -Fxq "$line" "$tmp"; then + continue + fi + printf '%s\n' "$line" >>"$tmp" + done <"$source" + + if [ ! -f "$target" ] || ! cmp -s "$target" "$tmp"; then + cat "$tmp" >"$target" + emit_copy 'WRITE: %s\n' "$target" + fi + rm -f "$tmp" +} + expand_cspell_config() { local app_root="$1" local ddev_approot="${DDEV_APPROOT:-$app_root}" @@ -1541,7 +1575,10 @@ node_toolchain_present() { run_command() { # Echo and execute a command (simple transparency for users). + # In interactive installs we route output through a small sanitizer to drop + # terminal query/response noise (OSC/DSR bytes) that can appear as garbage. local arg + local status emit 'Running:' for arg in "$@"; do emit ' %q' "$arg" @@ -1550,6 +1587,11 @@ run_command() { if [ "${non_interactive:-0}" -eq 1 ] || [ "${PROMPT_AVAILABLE:-0}" -ne 1 ]; then "$@" else + if command_available perl; then + "$@" 2>&1 | perl -pe 's/\e\][^\a\x1b]*(?:\a|\e\\)//g; s/\e\[[0-9;?]*R//g;' >&"$PROMPT_OUT_FD" + status=${PIPESTATUS[0]} + return "$status" + fi "$@" >&"$PROMPT_OUT_FD" fi } @@ -1974,6 +2016,11 @@ while IFS= read -r -d '' source; do copy_changed=$((copy_changed + 1)) done < <(find "$addon_root" -type f -print0) +# Append DCQ scope defaults to the project prettier ignore file. +append_unique_lines_from_file \ + "${app_root%/}/.prettierignore" \ + "${addon_root}/config-amendments/.prettierignore.dcq" + if [ "$copy_changed" -eq 0 ] && [ "$copy_skipped" -eq 0 ]; then emit 'All files already match; no changes.\n' else diff --git a/drupal-code-quality/assets/.eslintignore b/drupal-code-quality/assets/.eslintignore new file mode 100644 index 0000000..ae2a761 --- /dev/null +++ b/drupal-code-quality/assets/.eslintignore @@ -0,0 +1,9 @@ +#ddev-generated +# DCQ default exclusions for local linting scope. +# Keep custom code and site config in scope by excluding dependency/generated trees. +**/core/** +**/contrib/** +**/libraries/** +**/node_modules/** +vendor/** +**/sites/*/files/** diff --git a/drupal-code-quality/assets/.stylelintignore b/drupal-code-quality/assets/.stylelintignore new file mode 100644 index 0000000..ae2a761 --- /dev/null +++ b/drupal-code-quality/assets/.stylelintignore @@ -0,0 +1,9 @@ +#ddev-generated +# DCQ default exclusions for local linting scope. +# Keep custom code and site config in scope by excluding dependency/generated trees. +**/core/** +**/contrib/** +**/libraries/** +**/node_modules/** +vendor/** +**/sites/*/files/** diff --git a/drupal-code-quality/config-amendments/.prettierignore.dcq b/drupal-code-quality/config-amendments/.prettierignore.dcq new file mode 100644 index 0000000..36b7fb4 --- /dev/null +++ b/drupal-code-quality/config-amendments/.prettierignore.dcq @@ -0,0 +1,9 @@ +#ddev-generated +# DCQ default exclusions for local formatting scope. +# Keep custom code and site config in scope by excluding dependency/generated trees. +**/core/** +**/contrib/** +**/libraries/** +**/node_modules/** +vendor/** +**/sites/*/files/** diff --git a/tests/test.bats b/tests/test.bats index 382e255..b8b6e6d 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -1039,6 +1039,34 @@ JS assert_output --partial "--config=/var/www/html/.eslintrc.json" } +@test "eslint default run targets configured docroot when no paths are provided" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + mkdir -p node_modules/eslint/bin + cat > node_modules/eslint/bin/eslint.js <<'JS' +#!/usr/bin/env node +process.stdout.write(process.argv.slice(2).join("\n")); +JS + chmod +x node_modules/eslint/bin/eslint.js + + run wait_for_container_path "/var/www/html/node_modules/eslint/bin/eslint.js" + assert_success + + run ddev exec bash -lc 'cd /var/www/html && ESLINT_CONFIG_MODE=nearest ./.ddev/commands/web/eslint' + assert_success + assert_output --partial "web" + case "$output" in + *"modules/custom"*) + echo "Expected no hardcoded custom-directory target list in eslint wrapper default run." + return 1 + ;; + esac +} + @test "stylelint-fix fails with helpful message when project config is missing" { set -u -o pipefail export DCQ_INSTALL_DEPS=skip @@ -1234,6 +1262,55 @@ SH assert_output --partial "Create phpstan.neon in the project root" } +@test "default ignore configs include DCQ scope exclusions after install" { + set -u -o pipefail + run ddev add-on get "${DIR}" + assert_success + + assert_file_exist ".eslintignore" + assert_file_exist ".stylelintignore" + assert_file_exist ".prettierignore" + + run grep -q '\*\*/core/\*\*' .eslintignore + assert_success + run grep -q '\*\*/sites/\*/files/\*\*' .eslintignore + assert_success + + run grep -q '\*\*/core/\*\*' .stylelintignore + assert_success + run grep -q '\*\*/sites/\*/files/\*\*' .stylelintignore + assert_success + + run grep -q '^\*\.yml$' .prettierignore + assert_success + run grep -q '\*\*/core/\*\*' .prettierignore + assert_success + run grep -q '\*\*/sites/\*/files/\*\*' .prettierignore + assert_success +} + +@test "cspell default run targets current directory when no paths are provided" { + set -u -o pipefail + export DCQ_INSTALL_DEPS=skip + export DCQ_INSTALL_NODE_DEPS=skip + run ddev add-on get "${DIR}" + assert_success + + mkdir -p node_modules/.bin + cat > node_modules/.bin/cspell <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$@" +SH + chmod +x node_modules/.bin/cspell + + run wait_for_container_path "/var/www/html/node_modules/.bin/cspell" + assert_success + + run ddev exec bash -lc 'cd /var/www/html && ./.ddev/commands/web/cspell' + assert_success + assert_output --partial "." +} + @test "cspell config is expanded during installation" { set -u -o pipefail @@ -1463,12 +1540,7 @@ SH run ./.ddev/drupal-code-quality/tooling/bin/cspell assert_failure assert_output --partial "modlue" - case "$output" in - *"roottypo"*) - echo "Expected default cspell scope to exclude project-root files like cspell-test.md." - return 1 - ;; - esac + assert_output --partial "roottypo" before_phpcbf="$(read_container_file /var/www/html/web/modules/custom/dcq_test/dcq_fixable.php)" run ./.ddev/drupal-code-quality/tooling/bin/phpcbf web/modules/custom/dcq_test/dcq_fixable.php