From 379d152d6fca68e9c14f3b2e9e4c8da39b47f25f Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Mon, 2 Mar 2026 08:50:28 -0800 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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/8] 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 fef0112e0d8ac6c720404faa1b8e5ecca3e9191a Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Tue, 3 Mar 2026 23:20:15 -0800 Subject: [PATCH 6/8] Add host-path alias symlink entrypoint and coverage --- README.md | 11 ++++ install.yaml | 3 + tests/test.bats | 70 ++++++++++++++++++++++ web-entrypoint.d/90-dcq-host-path-alias.sh | 70 ++++++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 web-entrypoint.d/90-dcq-host-path-alias.sh diff --git a/README.md b/README.md index 3607a78..d927bea 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,14 @@ 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. + - On macOS paths under `/private/...`, it also creates a `/...` companion + alias (for example `/tmp/...`) to cover common host-path forms. + - To disable, add `DCQ_HOST_PATH_ALIAS=0` under `web_environment` in + `.ddev/config.yaml`, then run `ddev restart`. - ESLint toolchain selection: - `ESLINT_TOOLCHAIN=auto` (default) prefers root toolchain when root configs exist. - `ESLINT_TOOLCHAIN=core` forces Drupal core JS toolchain. @@ -224,6 +232,9 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J - `DCQ_INSTALL_IDE_SETTINGS`: `merge` to add missing VS Code settings and extension recommendations, `overwrite` to back up and replace, `skip` to handle manually, or unset to prompt. +- `DCQ_HOST_PATH_ALIAS`: `1`/unset (default) keeps host-path alias symlinks + enabled at web-container startup; set `0`/`false`/`off` to disable and remove + add-on-managed aliases on restart. ## Uninstall 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 382e255..16b2414 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -938,6 +938,76 @@ PY assert_output "/var/www/html/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 can be disabled via DCQ_HOST_PATH_ALIAS" { + 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; if [ -L '${approot}' ]; then exit 1; fi" + assert_success + + case "$approot" in + /private/*) + local alt="${approot#/private}" + run ddev exec bash -lc "set -eu; if [ -L '${alt}' ]; then exit 1; fi" + assert_success + ;; + esac +} + @test "install from directory with non-web docroot" { set -u -o pipefail mkdir -p docroot 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..fb7cc46 --- /dev/null +++ b/web-entrypoint.d/90-dcq-host-path-alias.sh @@ -0,0 +1,70 @@ +#!/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 + +alias_disabled() { + case "${DCQ_HOST_PATH_ALIAS:-1}" in + 0|false|FALSE|False|no|NO|No|off|OFF|Off) + return 0 + ;; + esac + return 1 +} + +cleanup_alias() { + local alias_path="$1" + local current_target="" + if [ -z "$alias_path" ]; then + return + fi + if ! sudo test -L "$alias_path"; then + return + fi + current_target="$(sudo readlink "$alias_path" || true)" + if [ "$current_target" = "$TARGET_PATH" ]; then + sudo rm -f "$alias_path" + fi +} + +ensure_alias() { + local alias_path="$1" + if [ -z "$alias_path" ]; then + return + fi + + if sudo test -e "$alias_path" && ! sudo test -L "$alias_path"; then + # Never replace real directories/files outside our managed symlink case. + return + fi + + sudo mkdir -p "$(dirname "$alias_path")" + sudo ln -sfn "$TARGET_PATH" "$alias_path" +} + +if alias_disabled; then + cleanup_alias "$APPROOT" + cleanup_alias "$ALT_APPROOT" + return 0 2>/dev/null || exit 0 +fi + +ensure_alias "$APPROOT" +ensure_alias "$ALT_APPROOT" From 76007df8f32dd2f364070c81781a42315917bc3c Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Wed, 4 Mar 2026 00:05:44 -0800 Subject: [PATCH 7/8] Simplify wrapper path normalization via shared helper --- commands/helpers/path-map.sh | 22 +++++++++++- commands/web/cspell | 5 +-- commands/web/cspell-suggest | 5 +-- commands/web/eslint | 10 ++---- commands/web/eslint-fix | 15 ++------ commands/web/phpstan | 3 -- commands/web/prettier | 5 +-- commands/web/prettier-fix | 15 ++------ commands/web/stylelint | 5 +-- commands/web/stylelint-fix | 21 +++--------- tests/test.bats | 66 ++++++++++++++++++++++++++++++++++++ 11 files changed, 104 insertions(+), 68 deletions(-) diff --git a/commands/helpers/path-map.sh b/commands/helpers/path-map.sh index 4a48587..2b023f5 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 @@ -39,9 +40,28 @@ map_path() { fi # If the path is a host path under the project root, map it into the container. if [ -n "$HOST_ROOT" ] && [ "${path#${HOST_ROOT}/}" != "$path" ]; then + # Prefer direct host-path alias usage when it is available in the container. + if [ -e "$path" ] || [ -L "$path" ]; then + echo "$path" + return + fi echo "${CONTAINER_ROOT}${path#${HOST_ROOT}}" return fi # Unknown path; return as-is to avoid breaking user inputs. 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 + echo "$path" +} diff --git a/commands/web/cspell b/commands/web/cspell index 1bde9f1..fcc9ede 100755 --- a/commands/web/cspell +++ b/commands/web/cspell @@ -104,10 +104,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 diff --git a/commands/web/cspell-suggest b/commands/web/cspell-suggest index 593b68e..a657fd5 100755 --- a/commands/web/cspell-suggest +++ b/commands/web/cspell-suggest @@ -126,10 +126,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 diff --git a/commands/web/eslint b/commands/web/eslint index 51237b1..ea8aa2a 100755 --- a/commands/web/eslint +++ b/commands/web/eslint @@ -196,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 @@ -220,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 diff --git a/commands/web/eslint-fix b/commands/web/eslint-fix index e5f0244..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 @@ -272,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 @@ -288,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 diff --git a/commands/web/phpstan b/commands/web/phpstan index fd93bfe..8640f2b 100755 --- a/commands/web/phpstan +++ b/commands/web/phpstan @@ -223,9 +223,6 @@ if [ -n "$CONFIG_TO_USE" ]; then 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 b0ba50a..52435e8 100755 --- a/commands/web/prettier +++ b/commands/web/prettier @@ -109,10 +109,7 @@ 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 diff --git a/commands/web/prettier-fix b/commands/web/prettier-fix index 143184b..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 @@ -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 diff --git a/commands/web/stylelint b/commands/web/stylelint index e4141d1..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 diff --git a/commands/web/stylelint-fix b/commands/web/stylelint-fix index 7d39592..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 @@ -131,10 +128,8 @@ normalize_path() { find_stylelint_config() { local path="$1" - local dir="$path" - if [[ "$dir" == /var/www/html/* ]]; then - dir="${dir#/var/www/html/}" - fi + local dir + dir="$(map_to_project_relative "$path")" if [ -f "${PROJECT_ROOT}/${dir}" ]; then dir="$(dirname "$dir")" fi @@ -196,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 @@ -208,10 +200,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 diff --git a/tests/test.bats b/tests/test.bats index 16b2414..a0a4394 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -938,6 +938,72 @@ PY 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 "path map falls back to container path when alias is disabled" { + set -u -o pipefail + local approot="$TESTDIR" + run ddev add-on get "${DIR}" + assert_success + touch web/index.php + + 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; source /mnt/ddev_config/commands/helpers/path-map.sh; map_path '${approot}/web/index.php'" + assert_success + assert_output "/var/www/html/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" From e2af138210f710b231958a2f7809439c81620aef Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Wed, 4 Mar 2026 10:14:22 -0800 Subject: [PATCH 8/8] Enforce host alias and remove fallback path rewrites --- README.md | 10 +- commands/helpers/path-map.sh | 12 +-- commands/web/cspell | 4 +- commands/web/cspell-suggest | 4 +- commands/web/phpcbf | 39 +------- commands/web/phpcs | 39 +------- commands/web/phpstan | 12 +-- commands/web/prettier | 2 - tests/test.bats | 103 +++++++++++---------- web-entrypoint.d/90-dcq-host-path-alias.sh | 43 +++------ 10 files changed, 83 insertions(+), 185 deletions(-) diff --git a/README.md b/README.md index d927bea..203248b 100644 --- a/README.md +++ b/README.md @@ -156,10 +156,13 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J - 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. - - To disable, add `DCQ_HOST_PATH_ALIAS=0` under `web_environment` in - `.ddev/config.yaml`, then run `ddev restart`. + - 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. @@ -232,9 +235,6 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J - `DCQ_INSTALL_IDE_SETTINGS`: `merge` to add missing VS Code settings and extension recommendations, `overwrite` to back up and replace, `skip` to handle manually, or unset to prompt. -- `DCQ_HOST_PATH_ALIAS`: `1`/unset (default) keeps host-path alias symlinks - enabled at web-container startup; set `0`/`false`/`off` to disable and remove - add-on-managed aliases on restart. ## Uninstall diff --git a/commands/helpers/path-map.sh b/commands/helpers/path-map.sh index 2b023f5..83a2e69 100755 --- a/commands/helpers/path-map.sh +++ b/commands/helpers/path-map.sh @@ -38,17 +38,13 @@ 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 - # Prefer direct host-path alias usage when it is available in the container. - if [ -e "$path" ] || [ -L "$path" ]; then - echo "$path" - return - fi - echo "${CONTAINER_ROOT}${path#${HOST_ROOT}}" + echo "$path" return fi - # Unknown path; return as-is to avoid breaking user inputs. + # Unknown path; return as-is. echo "$path" } diff --git a/commands/web/cspell b/commands/web/cspell index fcc9ede..3bcbf2c 100755 --- a/commands/web/cspell +++ b/commands/web/cspell @@ -139,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") diff --git a/commands/web/cspell-suggest b/commands/web/cspell-suggest index a657fd5..c32ed5a 100755 --- a/commands/web/cspell-suggest +++ b/commands/web/cspell-suggest @@ -161,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") diff --git a/commands/web/phpcbf b/commands/web/phpcbf index 5c72d3d..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,44 +18,8 @@ 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 diff --git a/commands/web/phpcs b/commands/web/phpcs index 4aaf8bc..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,44 +17,8 @@ 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 diff --git a/commands/web/phpstan b/commands/web/phpstan index 8640f2b..d201fe6 100755 --- a/commands/web/phpstan +++ b/commands/web/phpstan @@ -94,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" @@ -188,7 +180,7 @@ 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 @@ -207,7 +199,7 @@ while [ "$index" -lt "$arg_count" ]; do FINAL_ARGS+=("$arg") ;; *) - FINAL_ARGS+=("$(map_path "$arg")") + FINAL_ARGS+=("$arg") ;; esac index=$((index + 1)) diff --git a/commands/web/prettier b/commands/web/prettier index 52435e8..0e42fd8 100755 --- a/commands/web/prettier +++ b/commands/web/prettier @@ -145,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 @@ -153,7 +152,6 @@ if [ "$explicit_paths" = true ]; then --config=*|--ignore-path=*) key="${arg%%=*}" value="${arg#*=}" - value="$(map_path "$value")" FINAL_ARGS+=("${key}=${value}") ;; -*) diff --git a/tests/test.bats b/tests/test.bats index a0a4394..ff410ef 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -932,7 +932,7 @@ 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" @@ -957,53 +957,6 @@ PY assert_output "web/index.php" } -@test "path map falls back to container path when alias is disabled" { - set -u -o pipefail - local approot="$TESTDIR" - run ddev add-on get "${DIR}" - assert_success - touch web/index.php - - 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; source /mnt/ddev_config/commands/helpers/path-map.sh; map_path '${approot}/web/index.php'" - assert_success - assert_output "/var/www/html/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" @@ -1025,7 +978,7 @@ PY esac } -@test "host-path alias can be disabled via DCQ_HOST_PATH_ALIAS" { +@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}" @@ -1062,13 +1015,13 @@ PY retry_ddev_command ddev restart -y assert_success - run ddev exec bash -lc "set -eu; if [ -L '${approot}' ]; then exit 1; fi" + 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; if [ -L '${alt}' ]; then exit 1; fi" + run ddev exec bash -lc "set -eu; test -L '${alt}'; [ \"\$(readlink '${alt}')\" = \"/var/www/html\" ]" assert_success ;; esac @@ -1278,6 +1231,54 @@ JS 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 diff --git a/web-entrypoint.d/90-dcq-host-path-alias.sh b/web-entrypoint.d/90-dcq-host-path-alias.sh index fb7cc46..aa27743 100644 --- a/web-entrypoint.d/90-dcq-host-path-alias.sh +++ b/web-entrypoint.d/90-dcq-host-path-alias.sh @@ -21,50 +21,35 @@ case "$APPROOT" in ;; esac -alias_disabled() { - case "${DCQ_HOST_PATH_ALIAS:-1}" in - 0|false|FALSE|False|no|NO|No|off|OFF|Off) - return 0 - ;; - esac - return 1 -} - -cleanup_alias() { +ensure_alias() { local alias_path="$1" local current_target="" if [ -z "$alias_path" ]; then return fi - if ! sudo test -L "$alias_path"; then + + 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 - current_target="$(sudo readlink "$alias_path" || true)" - if [ "$current_target" = "$TARGET_PATH" ]; then - sudo rm -f "$alias_path" - fi -} -ensure_alias() { - local alias_path="$1" - if [ -z "$alias_path" ]; then + 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" && ! sudo test -L "$alias_path"; then - # Never replace real directories/files outside our managed symlink case. - return + 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 -sfn "$TARGET_PATH" "$alias_path" + sudo ln -s "$TARGET_PATH" "$alias_path" } -if alias_disabled; then - cleanup_alias "$APPROOT" - cleanup_alias "$ALT_APPROOT" - return 0 2>/dev/null || exit 0 -fi - ensure_alias "$APPROOT" ensure_alias "$ALT_APPROOT"