diff --git a/README.md b/README.md index b94bad9..203248b 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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: @@ -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 `. - PHPStan level: - GitLab CI template defaults use level 0. The installer can set a local default level (0-10). diff --git a/commands/helpers/path-map.sh b/commands/helpers/path-map.sh index 4a48587..83a2e69 100755 --- a/commands/helpers/path-map.sh +++ b/commands/helpers/path-map.sh @@ -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 @@ -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" } 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..3bcbf2c 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,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 @@ -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") @@ -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 diff --git a/commands/web/cspell-suggest b/commands/web/cspell-suggest index 9c09785..c32ed5a 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,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 @@ -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") @@ -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 @@ -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" @@ -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 diff --git a/commands/web/eslint b/commands/web/eslint index 98ba236..ea8aa2a 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 @@ -191,10 +196,7 @@ resolve_plugins_dir() { normalize_path() { local path="$1" # Convert container/short paths into repo-relative paths for consistent matching. - 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 @@ -215,10 +217,7 @@ normalize_path() { normalize_config_path() { local path="$1" # ESLint --config paths need to be relative to the repo root in the container. - 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 @@ -273,15 +272,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 +289,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..e72be8e 100755 --- a/commands/web/eslint-fix +++ b/commands/web/eslint-fix @@ -134,10 +134,7 @@ fi normalize_path() { local path="$1" # Normalize paths into repo-relative paths for matching target prefixes. - 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 @@ -192,7 +189,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 +204,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 +225,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 @@ -264,10 +269,7 @@ resolve_plugins_dir() { FINAL_ARGS=() normalize_config_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 @@ -280,10 +282,7 @@ normalize_config_path() { normalize_docroot_arg() { local arg="$1" - arg="$(map_path "$arg")" - if [[ "$arg" == /var/www/html/* ]]; then - arg="${arg#/var/www/html/}" - fi + arg="$(map_to_project_relative "$arg")" if [ "$DOCROOT" != "web" ] && [[ "$arg" == web/* ]]; then arg="${DOCROOT}/${arg#web/}" fi @@ -335,15 +334,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 +355,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..13d6c19 100755 --- a/commands/web/phpcbf +++ b/commands/web/phpcbf @@ -4,7 +4,6 @@ set -u PHPCBF_BIN="vendor/bin/phpcbf" -source /mnt/ddev_config/commands/helpers/path-map.sh if [ ! -x "$PHPCBF_BIN" ]; then echo "phpcbf not found at $PHPCBF_BIN. Run 'ddev composer install' to install vendor/bin/phpcbf." >&2 @@ -19,49 +18,10 @@ for config_file in .phpcs.xml phpcs.xml phpcs.xml.dist .phpcs.xml.dist; do fi done -rewrite_args=() -args=("$@") -arg_count=${#args[@]} -index=0 -while [ "$index" -lt "$arg_count" ]; do - arg="${args[$index]}" - case "$arg" in - --standard=*) - value="${arg#*=}" - # Rewrite host paths for standards into container paths. - rewrite_args+=("--standard=$(map_path "$value")") - ;; - --standard) - next_arg="${args[$((index + 1))]:-}" - # Rewrite host paths for standards into container paths. - rewrite_args+=("--standard" "$(map_path "$next_arg")") - index=$((index + 1)) - ;; - --stdin-path=*) - value="${arg#*=}" - # Rewrite stdin file paths so PHPCBF can resolve them in the container. - rewrite_args+=("--stdin-path=$(map_path "$value")") - ;; - --stdin-path) - next_arg="${args[$((index + 1))]:-}" - # Rewrite stdin file paths so PHPCBF can resolve them in the container. - rewrite_args+=("--stdin-path" "$(map_path "$next_arg")") - index=$((index + 1)) - ;; - *) - rewrite_args+=("$(map_path "$arg")") - ;; - esac - index=$((index + 1)) -done - if [ "$has_config" = true ]; then - "$PHPCBF_BIN" "${rewrite_args[@]}" + "$PHPCBF_BIN" "$@" 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..69d12fd 100755 --- a/commands/web/phpcs +++ b/commands/web/phpcs @@ -3,7 +3,6 @@ set -u PHPCS_BIN="vendor/bin/phpcs" -source /mnt/ddev_config/commands/helpers/path-map.sh if [ ! -x "$PHPCS_BIN" ]; then echo "phpcs not found at $PHPCS_BIN. Run 'ddev composer install' to install vendor/bin/phpcs." >&2 @@ -18,49 +17,10 @@ for config_file in .phpcs.xml phpcs.xml phpcs.xml.dist .phpcs.xml.dist; do fi done -rewrite_args=() -args=("$@") -arg_count=${#args[@]} -index=0 -while [ "$index" -lt "$arg_count" ]; do - arg="${args[$index]}" - case "$arg" in - --standard=*) - value="${arg#*=}" - # Rewrite host paths for standards into container paths. - rewrite_args+=("--standard=$(map_path "$value")") - ;; - --standard) - next_arg="${args[$((index + 1))]:-}" - # Rewrite host paths for standards into container paths. - rewrite_args+=("--standard" "$(map_path "$next_arg")") - index=$((index + 1)) - ;; - --stdin-path=*) - value="${arg#*=}" - # Rewrite stdin file paths so PHPCS can resolve them in the container. - rewrite_args+=("--stdin-path=$(map_path "$value")") - ;; - --stdin-path) - next_arg="${args[$((index + 1))]:-}" - # Rewrite stdin file paths so PHPCS can resolve them in the container. - rewrite_args+=("--stdin-path" "$(map_path "$next_arg")") - index=$((index + 1)) - ;; - *) - rewrite_args+=("$(map_path "$arg")") - ;; - esac - index=$((index + 1)) -done - if [ "$has_config" = true ]; then - "$PHPCS_BIN" "${rewrite_args[@]}" + "$PHPCS_BIN" "$@" 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/phpstan b/commands/web/phpstan index 9e64765..d201fe6 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 @@ -111,19 +94,11 @@ while [ "$index" -lt "$arg_count" ]; do has_config=true next_index=$((index + 1)) config_value="${args[$next_index]:-}" - if [ -n "$config_value" ]; then - config_value="$(map_path "$config_value")" - args[$next_index]="$config_value" - fi index=$((index + 1)) ;; --configuration=*) has_config=true config_value="${arg#*=}" - if [ -n "$config_value" ]; then - config_value="$(map_path "$config_value")" - args[$index]="--configuration=$config_value" - fi ;; --generate-baseline) args[$index]="--generate-baseline=phpstan-baseline.neon" @@ -144,19 +119,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 +144,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 +152,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,31 +174,13 @@ 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 while [ "$index" -lt "$arg_count" ]; do arg="${args[$index]}" if [ "$seen_double_dash" = true ]; then - FINAL_ARGS+=("$(map_path "$arg")") + FINAL_ARGS+=("$arg") index=$((index + 1)) continue fi @@ -265,57 +192,21 @@ 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")") + FINAL_ARGS+=("$arg") ;; esac index=$((index + 1)) 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,22 +214,7 @@ 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" - fi JSON_OUTPUT="$(mktemp /tmp/phpstan-json.XXXXXX)" TMP_FILES+=("$JSON_OUTPUT") "${CMD[@]}" "${FINAL_ARGS[@]}" > "$JSON_OUTPUT" diff --git a/commands/web/prettier b/commands/web/prettier index c41c396..0e42fd8 100755 --- a/commands/web/prettier +++ b/commands/web/prettier @@ -97,27 +97,19 @@ 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 normalize_prettier_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 @@ -153,7 +145,6 @@ if [ "$explicit_paths" = true ]; then FINAL_ARGS+=("$arg") next_arg="${args[$((index + 1))]:-}" if [ -n "$next_arg" ]; then - next_arg="$(map_path "$next_arg")" FINAL_ARGS+=("$next_arg") index=$((index + 1)) fi @@ -161,7 +152,6 @@ if [ "$explicit_paths" = true ]; then --config=*|--ignore-path=*) key="${arg%%=*}" value="${arg#*=}" - value="$(map_path "$value")" FINAL_ARGS+=("${key}=${value}") ;; -*) @@ -178,15 +168,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..1164a85 100755 --- a/commands/web/prettier-fix +++ b/commands/web/prettier-fix @@ -111,10 +111,7 @@ 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 @@ -165,7 +162,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 @@ -175,10 +172,7 @@ fi FINAL_ARGS=() normalize_config_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 @@ -187,10 +181,7 @@ normalize_config_path() { normalize_docroot_arg() { local arg="$1" - arg="$(map_path "$arg")" - if [[ "$arg" == /var/www/html/* ]]; then - arg="${arg#/var/www/html/}" - fi + arg="$(map_to_project_relative "$arg")" if [ "$DOCROOT" != "web" ] && [[ "$arg" == web/* ]]; then arg="${DOCROOT}/${arg#web/}" fi @@ -242,15 +233,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 +256,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..5260a2f 100755 --- a/commands/web/stylelint +++ b/commands/web/stylelint @@ -155,10 +155,7 @@ config_supports_scss() { normalize_docroot_arg() { local arg="$1" - arg="$(map_path "$arg")" - if [[ "$arg" == /var/www/html/* ]]; then - arg="${arg#/var/www/html/}" - fi + arg="$(map_to_project_relative "$arg")" if [ "$DOCROOT" != "web" ] && [[ "$arg" == web/* ]]; then arg="${DOCROOT}/${arg#web/}" fi @@ -176,12 +173,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 +257,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 +284,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 +335,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 6aa795e..e4f1f51 100755 --- a/commands/web/stylelint-fix +++ b/commands/web/stylelint-fix @@ -111,10 +111,7 @@ 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 @@ -129,6 +126,25 @@ normalize_path() { echo "$path" } +find_stylelint_config() { + local path="$1" + local dir + dir="$(map_to_project_relative "$path")" + 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 +181,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 @@ -175,10 +191,7 @@ fi FINAL_ARGS=() normalize_config_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 @@ -187,12 +200,9 @@ normalize_config_path() { normalize_docroot_arg() { local arg="$1" - arg="$(map_path "$arg")" - if [[ "$arg" == /var/www/html/* ]]; then - arg="${arg#/var/www/html/}" - fi + arg="$(map_to_project_relative "$arg")" if [ "$DOCROOT" != "web" ] && [[ "$arg" == web/* ]]; then - arg="${DOCROOT}/${path#web/}" + arg="${DOCROOT}/${arg#web/}" fi if [[ "$arg" == "${DOCROOT}/"* ]]; then arg="${arg#${DOCROOT}/}" @@ -242,15 +252,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 +272,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 +321,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 +377,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 +403,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/install.yaml b/install.yaml index 2324ff5..e3da0ba 100644 --- a/install.yaml +++ b/install.yaml @@ -4,6 +4,7 @@ name: drupal-code-quality # Files copied into the project's .ddev directory. project_files: + - web-entrypoint.d/90-dcq-host-path-alias.sh - commands/helpers/path-map.sh - commands/web/checks - commands/web/composer-validate @@ -50,3 +51,5 @@ removal_actions: fi rm -f .dcq-docroot rm -rf drupal-code-quality + rm -f web-entrypoint.d/90-dcq-host-path-alias.sh + rmdir web-entrypoint.d 2>/dev/null || true diff --git a/tests/test.bats b/tests/test.bats index 7c03422..ff410ef 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -932,12 +932,101 @@ PY assert_success run ddev exec bash -lc 'export DDEV_HOST_PROJECT_ROOT="/tmp/dcq-host-root"; source /mnt/ddev_config/commands/helpers/path-map.sh; map_path "/tmp/dcq-host-root/path/to/file.php"' assert_success - assert_output "/var/www/html/path/to/file.php" + assert_output "/tmp/dcq-host-root/path/to/file.php" run ddev exec bash -lc 'source /mnt/ddev_config/commands/helpers/path-map.sh; map_path "/var/www/html/web/index.php"' assert_success assert_output "/var/www/html/web/index.php" } +@test "path map keeps host absolute paths when alias exists" { + set -u -o pipefail + local approot="$TESTDIR" + run ddev add-on get "${DIR}" + assert_success + touch web/index.php + + retry_ddev_command ddev restart -y + assert_success + + run ddev exec bash -lc "set -eu; source /mnt/ddev_config/commands/helpers/path-map.sh; map_path '${approot}/web/index.php'" + assert_success + assert_output "${approot}/web/index.php" + + run ddev exec bash -lc "set -eu; source /mnt/ddev_config/commands/helpers/path-map.sh; map_to_project_relative '${approot}/web/index.php'" + assert_success + assert_output "web/index.php" +} + +@test "host-path alias symlink is created on restart" { + set -u -o pipefail + local approot="$TESTDIR" + run ddev add-on get "${DIR}" + assert_success + + retry_ddev_command ddev restart -y + assert_success + + run ddev exec bash -lc "set -eu; test -L '${approot}'; [ \"\$(readlink '${approot}')\" = \"/var/www/html\" ]" + assert_success + + case "$approot" in + /private/*) + local alt="${approot#/private}" + run ddev exec bash -lc "set -eu; test -L '${alt}'; [ \"\$(readlink '${alt}')\" = \"/var/www/html\" ]" + assert_success + ;; + esac +} + +@test "host-path alias remains enforced even when DCQ_HOST_PATH_ALIAS=0 is set" { + set -u -o pipefail + local approot="$TESTDIR" + run ddev add-on get "${DIR}" + assert_success + + python3 - <<'PY' +from pathlib import Path + +path = Path(".ddev/config.yaml") +lines = path.read_text(encoding="utf-8").splitlines() + +for idx, line in enumerate(lines): + if line.strip() == "web_environment: []": + lines[idx] = "web_environment:" + lines.insert(idx + 1, " - DCQ_HOST_PATH_ALIAS=0") + break +else: + inserted = False + for idx, line in enumerate(lines): + if line.strip() == "web_environment:": + if idx + 1 < len(lines) and lines[idx + 1].strip().startswith("- "): + lines.insert(idx + 1, " - DCQ_HOST_PATH_ALIAS=0") + else: + lines.insert(idx + 1, " - DCQ_HOST_PATH_ALIAS=0") + inserted = True + break + if not inserted: + lines.append("web_environment:") + lines.append(" - DCQ_HOST_PATH_ALIAS=0") + +path.write_text("\n".join(lines) + "\n", encoding="utf-8") +PY + + retry_ddev_command ddev restart -y + assert_success + + run ddev exec bash -lc "set -eu; test -L '${approot}'; [ \"\$(readlink '${approot}')\" = \"/var/www/html\" ]" + assert_success + + case "$approot" in + /private/*) + local alt="${approot#/private}" + run ddev exec bash -lc "set -eu; test -L '${alt}'; [ \"\$(readlink '${alt}')\" = \"/var/www/html\" ]" + assert_success + ;; + esac +} + @test "install from directory with non-web docroot" { set -u -o pipefail mkdir -p docroot @@ -975,6 +1064,221 @@ 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 "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 "phpcs forwards host absolute stdin and standard paths unchanged" { + set -u -o pipefail + local approot="$TESTDIR" + run ddev add-on get "${DIR}" + assert_success + + mkdir -p vendor/bin + cat > vendor/bin/phpcs <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$@" +exit 0 +SH + chmod +x vendor/bin/phpcs + + retry_ddev_command ddev restart -y + assert_success + + run ddev exec bash -lc ".ddev/commands/web/phpcs --stdin-path '${approot}/web/index.php' --standard='${approot}/.phpcs.xml' '${approot}/web/index.php'" + assert_success + assert_output --partial "--stdin-path" + assert_output --partial "${approot}/web/index.php" + assert_output --partial "--standard=${approot}/.phpcs.xml" +} + +@test "phpcbf forwards host absolute stdin and standard paths unchanged" { + set -u -o pipefail + local approot="$TESTDIR" + run ddev add-on get "${DIR}" + assert_success + + mkdir -p vendor/bin + cat > vendor/bin/phpcbf <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$@" +exit 0 +SH + chmod +x vendor/bin/phpcbf + + retry_ddev_command ddev restart -y + assert_success + + run ddev exec bash -lc ".ddev/commands/web/phpcbf --stdin-path '${approot}/web/index.php' --standard='${approot}/.phpcs.xml' '${approot}/web/index.php'" + assert_success + assert_output --partial "--stdin-path" + assert_output --partial "${approot}/web/index.php" + assert_output --partial "--standard=${approot}/.phpcs.xml" +} + @test "install from directory with phpstan level override" { set -u -o pipefail export DCQ_PHPSTAN_LEVEL=3 @@ -1040,6 +1344,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 @@ -1269,7 +1600,12 @@ PHP 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 diff --git a/web-entrypoint.d/90-dcq-host-path-alias.sh b/web-entrypoint.d/90-dcq-host-path-alias.sh new file mode 100644 index 0000000..aa27743 --- /dev/null +++ b/web-entrypoint.d/90-dcq-host-path-alias.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +#ddev-generated +set -eu + +APPROOT_FILE="/mnt/ddev_config/.ddev-docker-compose-full.yaml" +TARGET_PATH="/var/www/html" + +if [ ! -f "$APPROOT_FILE" ]; then + return 0 2>/dev/null || exit 0 +fi + +APPROOT="$(awk -F': ' '/com\.ddev\.approot:/ {print $2; exit}' "$APPROOT_FILE" | tr -d '"')" +if [ -z "$APPROOT" ]; then + return 0 2>/dev/null || exit 0 +fi + +ALT_APPROOT="" +case "$APPROOT" in + /private/*) + ALT_APPROOT="${APPROOT#/private}" + ;; +esac + +ensure_alias() { + local alias_path="$1" + local current_target="" + if [ -z "$alias_path" ]; then + return + fi + + if sudo test -L "$alias_path"; then + current_target="$(sudo readlink "$alias_path" || true)" + if [ "$current_target" = "$TARGET_PATH" ]; then + return + fi + sudo ln -sfn "$TARGET_PATH" "$alias_path" + return + fi + + if sudo test -d "$alias_path"; then + # Existing directories (for example user-managed bind mounts) already satisfy parity. + return + fi + + if sudo test -e "$alias_path"; then + echo "DCQ host-path alias conflict at ${alias_path}: existing non-directory path cannot be replaced safely." >&2 + return 1 + fi + + sudo mkdir -p "$(dirname "$alias_path")" + sudo ln -s "$TARGET_PATH" "$alias_path" +} + +ensure_alias "$APPROOT" +ensure_alias "$ALT_APPROOT"