Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,21 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J
- ESLint config mode:
- `ESLINT_CONFIG_MODE=nearest` (default) groups by nearest config file.
- `ESLINT_CONFIG_MODE=fixed` forces `.eslintrc.passing.json`.
- ESLint warning visibility (GitLab CI parity):
- `DCQ_ESLINT_QUIET=1` (default) adds `--quiet` to `ddev eslint` and
`ddev eslint-fix`, so warnings are suppressed.
- Set `DCQ_ESLINT_QUIET=0` to include warnings in CLI output. Persist this in
`.ddev/config.yaml` (or `.ddev/config.yml`):
```yaml
web_environment:
- DCQ_ESLINT_QUIET=0
```
- VS Code uses its own setting for extension diagnostics. Set
`"eslint.quiet": false` in `.vscode/settings.json` to include warnings in
the IDE.
- Installer behavior: `overwrite` regenerates IDE settings from template;
`merge` only adds missing keys and will not change an existing
`eslint.quiet` value.
- 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.
Expand All @@ -161,14 +176,17 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J
- If no project `phpstan.neon*` exists, the wrapper uses the GitLab template
config shipped with the add-on.
- PHPStan level:
- GitLab CI template defaults use level 0. The installer can set a local default level (0-10).
- GitLab CI template defaults use level 0. The installer can set a local default level (0-10).

## Installer environment variables

- `DCQ_INSTALL_MODE`: `replace`, `skip`, or `abort` for conflict handling.
- `DCQ_NONINTERACTIVE=true`: disable prompts; if no overrides are set, applies
the recommended settings automatically.
- `DCQ_PHPSTAN_LEVEL`: set `phpstan.neon` level (0-10) without prompting.
- `DCQ_ESLINT_QUIET`: `1`/unset to suppress ESLint warnings by default
(GitLab CI parity), `0` to include warnings. This can be set in
`.ddev/config.yaml` under `web_environment`.
- `DCQ_INSTALL_DEPS`: `install`/`true` to auto-install missing `drupal/core-dev`,
`skip`/`false` to skip, or unset to prompt when interactive.
- `DCQ_INSTALL_NODE_DEPS`: `root` to install JS deps in the project root (creates
Expand Down
18 changes: 16 additions & 2 deletions commands/web/eslint
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ print_help() {
Usage: ddev eslint [args]

Runs ESLint inside the DDEV web container with Drupal.org GitLab CI template defaults.

Defaults:
- Uses --quiet (GitLab CI parity). Set DCQ_ESLINT_QUIET=0 to include warnings.
USAGE
}

Expand All @@ -28,6 +31,11 @@ NODE_PATH=""
RESOLVE_PLUGINS_DIR=""
ESLINT_TOOLCHAIN="${ESLINT_TOOLCHAIN:-auto}"
ESLINT_CONFIG_MODE="${ESLINT_CONFIG_MODE:-nearest}"
DCQ_ESLINT_QUIET="${DCQ_ESLINT_QUIET:-1}"
QUIET_ENABLED=1
case "$DCQ_ESLINT_QUIET" in
0|false|FALSE|False|no|NO|off|OFF) QUIET_ENABLED=0 ;;
esac

for arg in "$@"; do
if [ "$seen_double_dash" = true ]; then
Expand Down Expand Up @@ -110,7 +118,11 @@ if [ "$has_version" = true ]; then
exit $?
fi

CMD=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --quiet)
CMD=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN")
DEFAULT_ARGS=()
if [ "$QUIET_ENABLED" -eq 1 ]; then
DEFAULT_ARGS+=(--quiet)
fi
if [ -n "$RESOLVE_PLUGINS_DIR" ]; then
# Ensure ESLint resolves plugins from the selected toolchain.
CMD+=(--resolve-plugins-relative-to "$RESOLVE_PLUGINS_DIR")
Expand All @@ -125,6 +137,7 @@ if [ "$has_config" = false ] && [ "$ESLINT_CONFIG_MODE" != "nearest" ]; then
fi
fi
CMD+=(--ext .js,.yml --ignore-pattern "**/node_modules/**")
CMD+=("${DEFAULT_ARGS[@]}")

find_nearest_config() {
local file_path="$1"
Expand Down Expand Up @@ -312,11 +325,12 @@ if [ "$ESLINT_CONFIG_MODE" = "nearest" ]; then
config_dir="$(dirname "$config_path")"
plugins_dir="$(resolve_plugins_dir "$config_dir")"
echo "ESLint nearest-mode: config ${config_path} (${#group_files[@]} file(s))." >&2
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --quiet --config="$config_path")
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --config="$config_path")
if [ -n "$plugins_dir" ]; then
group_cmd+=(--resolve-plugins-relative-to "$plugins_dir")
fi
group_cmd+=(--ext .js,.yml --ignore-pattern "**/node_modules/**")
group_cmd+=("${DEFAULT_ARGS[@]}")
"${group_cmd[@]}" "${FLAGS_ARGS[@]}" "${filtered_files[@]}"
status=$?
if [ $status -ne 0 ]; then
Expand Down
24 changes: 20 additions & 4 deletions commands/web/eslint-fix
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Usage: ddev eslint-fix [args]

Applies ESLint fixes directly to files (matches standard eslint --fix behavior).

Defaults:
- Uses --quiet (GitLab CI parity). Set DCQ_ESLINT_QUIET=0 to include warnings.

Use --preview to see a patch preview and confirm before applying fixes.
This generates a patch in dcq-reports/ and prompts for confirmation.

Expand All @@ -31,6 +34,11 @@ preview_mode=false
clean_args=()
ESLINT_TOOLCHAIN="${ESLINT_TOOLCHAIN:-auto}"
ESLINT_CONFIG_MODE="${ESLINT_CONFIG_MODE:-nearest}"
DCQ_ESLINT_QUIET="${DCQ_ESLINT_QUIET:-1}"
QUIET_ENABLED=1
case "$DCQ_ESLINT_QUIET" in
0|false|FALSE|False|no|NO|off|OFF) QUIET_ENABLED=0 ;;
esac

for arg in "$@"; do
if [ "$arg" = "--preview" ]; then
Expand Down Expand Up @@ -191,7 +199,11 @@ else
done
fi

CMD_BASE=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --quiet)
CMD_BASE=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN")
DEFAULT_ARGS=()
if [ "$QUIET_ENABLED" -eq 1 ]; then
DEFAULT_ARGS+=(--quiet)
fi
if [ -n "$RESOLVE_PLUGINS_DIR" ]; then
CMD_BASE+=(--resolve-plugins-relative-to "$RESOLVE_PLUGINS_DIR")
fi
Expand All @@ -204,6 +216,7 @@ if [ "$has_config" = false ] && [ "$ESLINT_CONFIG_MODE" != "nearest" ]; then
fi
fi
CMD_BASE+=(--ext .js,.yml --ignore-pattern "**/node_modules/**")
CMD_BASE+=("${DEFAULT_ARGS[@]}")

find_nearest_config() {
local file_path="$1"
Expand Down Expand Up @@ -410,11 +423,12 @@ if [ "$preview_mode" = true ]; then
config_dir="$(dirname "$config_path_abs")"
plugins_dir="$(resolve_plugins_dir "$config_dir")"
echo "ESLint-fix preview: config ${config_path_abs} (${#filtered_files[@]} file(s))." >&2
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --quiet --config="$config_path_run")
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --config="$config_path_run")
if [ -n "$plugins_dir" ]; then
group_cmd+=(--resolve-plugins-relative-to "$plugins_dir")
fi
group_cmd+=(--ext .js,.yml --ignore-pattern "**/node_modules/**" --fix)
group_cmd+=("${DEFAULT_ARGS[@]}")
(cd "$tmp_root" && "${group_cmd[@]}" "${filtered_files[@]}")
status=$?
if [ $status -ne 0 ]; then
Expand Down Expand Up @@ -496,11 +510,12 @@ if [ "$preview_mode" = true ]; then
config_dir="$(dirname "$config_path_abs")"
plugins_dir="$(resolve_plugins_dir "$config_dir")"
echo "Applying ESLint fixes: config ${config_path_abs} (${#filtered_files[@]} file(s))." >&2
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --quiet --config="$config_path_run")
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --config="$config_path_run")
if [ -n "$plugins_dir" ]; then
group_cmd+=(--resolve-plugins-relative-to "$plugins_dir")
fi
group_cmd+=(--ext .js,.yml --ignore-pattern "**/node_modules/**" --fix)
group_cmd+=("${DEFAULT_ARGS[@]}")
"${group_cmd[@]}" "${filtered_files[@]}"
status=$?
if [ $status -ne 0 ]; then
Expand Down Expand Up @@ -567,11 +582,12 @@ if [ "$ESLINT_CONFIG_MODE" = "nearest" ]; then
fi
config_dir="$(dirname "$config_path_abs")"
plugins_dir="$(resolve_plugins_dir "$config_dir")"
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --quiet --config="$config_path_run")
group_cmd=(env "NODE_PATH=$NODE_PATH" node "$TOOLCHAIN_BIN" --config="$config_path_run")
if [ -n "$plugins_dir" ]; then
group_cmd+=(--resolve-plugins-relative-to "$plugins_dir")
fi
group_cmd+=(--ext .js,.yml --ignore-pattern "**/node_modules/**" --fix)
group_cmd+=("${DEFAULT_ARGS[@]}")
"${group_cmd[@]}" "${filtered_files[@]}"
status=$?
if [ $status -ne 0 ]; then
Expand Down
2 changes: 1 addition & 1 deletion commands/web/prettier-fix
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ if [ "$explicit_paths" = false ]; then
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' \) -print)
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)
fi
done
if [ "${#DEFAULT_FILES[@]}" -eq 0 ]; then
Expand Down
59 changes: 45 additions & 14 deletions dcq-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -948,30 +948,22 @@ expand_cspell_config() {
emit 'Expanding .cspell.json with project-specific settings...\n'
local ddev_cmd="${DDEV_EXECUTABLE:-ddev}"

# Copy prepare-cspell.php to project root for execution
local project_script="${app_root%/}/.prepare-cspell-tmp.php"
cp "$prepare_script" "$project_script" || {
emit 'Failed to copy prepare-cspell.php; skipping expansion.\n'
return 0
}

# Run the script in container from project root
# Capture both stdout and stderr, but don't fail the installer if it errors
# Pass the docroot via _WEB_ROOT environment variable
local output
if output=$("$ddev_cmd" exec bash -c "export _WEB_ROOT='${DCQ_DOCROOT:-web}' && php .prepare-cspell-tmp.php" 2>&1); then
if output=$("$ddev_cmd" exec bash -c "cd /var/www/html && export _WEB_ROOT='${DCQ_DOCROOT:-web}' && php .ddev/drupal-code-quality/tooling/scripts/prepare-cspell.php" 2>&1); then
if echo "$output" | grep -q "Writing json"; then
emit 'Successfully expanded .cspell.json\n'
else
emit 'CSpell expansion completed (no changes needed)\n'
fi
else
# Script failed - likely no Drupal core installed yet
emit 'Skipping CSpell expansion (Drupal core may not be installed yet)\n'
emit 'Skipping CSpell expansion: prepare-cspell.php failed.\n'
if [ -n "$output" ]; then
emit '%s\n' "$output"
fi
fi

# Clean up temp script
rm -f "$project_script"
else
emit 'DDEV not available; skipping CSpell expansion.\n'
fi
Expand Down Expand Up @@ -1074,6 +1066,40 @@ escape_sed_replacement() {
printf '%s' "${1:-}" | sed 's/[&|]/\\&/g'
}

eslint_quiet_disabled() {
case "${1:-}" in
0|false|FALSE|False|no|NO|off|OFF)
return 0
;;
esac
return 1
}

resolve_eslint_quiet_setting() {
local app_root="$1"
local raw="${DCQ_ESLINT_QUIET:-}"
local config_file=""
local matched_line=""

if [ -z "$raw" ]; then
for config_file in "${app_root%/}/.ddev/config.yaml" "${app_root%/}/.ddev/config.yml"; do
[ -f "$config_file" ] || continue
matched_line="$(grep -E '^[[:space:]-]*["'"'"']?DCQ_ESLINT_QUIET=' "$config_file" | tail -n 1 || true)"
[ -n "$matched_line" ] || continue
raw="${matched_line#*=}"
raw="${raw%%#*}"
raw="$(printf '%s' "$raw" | tr -d "\"'" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
[ -n "$raw" ] && break
done
fi

if eslint_quiet_disabled "$raw"; then
printf 'false'
return
fi
printf 'true'
}

render_ide_template() {
# Render IDE settings template with resolved shim and tool paths.
local template="$1"
Expand All @@ -1083,17 +1109,20 @@ render_ide_template() {
local prettier_path="$5"
local eslint_node_path="$6"
local eslint_resolve_plugins="$7"
local eslint_quiet="$8"
local escaped_shim
local escaped_stylelint
local escaped_prettier
local escaped_node_path
local escaped_resolve_plugins
local escaped_eslint_quiet

escaped_shim="$(escape_sed_replacement "$shim_setting")"
escaped_stylelint="$(escape_sed_replacement "$stylelint_path")"
escaped_prettier="$(escape_sed_replacement "$prettier_path")"
escaped_node_path="$(escape_sed_replacement "$eslint_node_path")"
escaped_resolve_plugins="$(escape_sed_replacement "$eslint_resolve_plugins")"
escaped_eslint_quiet="$(escape_sed_replacement "$eslint_quiet")"

sed \
-e '1{/^#ddev-generated$/d;}' \
Expand All @@ -1102,6 +1131,7 @@ render_ide_template() {
-e "s|__DCQ_PRETTIER_PATH__|${escaped_prettier}|g" \
-e "s|__DCQ_ESLINT_NODE_PATH__|${escaped_node_path}|g" \
-e "s|__DCQ_ESLINT_RESOLVE_PLUGINS__|${escaped_resolve_plugins}|g" \
-e "s|__DCQ_ESLINT_QUIET__|${escaped_eslint_quiet}|g" \
"$template" >"$output"
}

Expand Down Expand Up @@ -2190,6 +2220,7 @@ if [ -f "$ide_settings_template" ] || [ -f "$ide_extensions_template" ]; then
js_modules=""
eslint_node_path=""
eslint_resolve_plugins=""
eslint_quiet="$(resolve_eslint_quiet_setting "$app_root")"
if [ "$ide_node_mode" = "root" ] && [ "$has_root_node_modules" -eq 1 ]; then
js_modules="./node_modules"
eslint_node_path="node_modules"
Expand All @@ -2205,7 +2236,7 @@ if [ -f "$ide_settings_template" ] || [ -f "$ide_extensions_template" ]; then
fi

render_ide_template "$ide_settings_template" "$ide_tmp" "$shim_setting" \
"$stylelint_path" "$prettier_path" "$eslint_node_path" "$eslint_resolve_plugins"
"$stylelint_path" "$prettier_path" "$eslint_node_path" "$eslint_resolve_plugins" "$eslint_quiet"
if [ "$ide_js_paths_set" -eq 0 ]; then
strip_ide_js_settings "$ide_tmp" || true
emit 'JS tool paths not configured (node_modules missing). Install JS deps and re-run the installer or update settings manually.\n'
Expand Down
12 changes: 11 additions & 1 deletion drupal-code-quality/ide-settings/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@ Stylelint, Prettier, CSpell) use local `node_modules` paths. The installer uses
the Node toolchain choice (prompt or `DCQ_INSTALL_NODE_DEPS`) to configure those
paths when dependencies are available. Override the paths if you prefer a
different location. Run `ddev <tool>` in the terminal to use the containerized
CLI wrappers.
CLI wrappers. The generated settings also set `eslint.quiet` by default to
match GitLab CI behavior; set `DCQ_ESLINT_QUIET=0` before install to disable.
To persist this in DDEV commands, add `DCQ_ESLINT_QUIET=0` under
`web_environment` in `.ddev/config.yaml`.

For existing projects, update `.vscode/settings.json` as well:
- Set `"eslint.quiet": false` to show warnings in the IDE.
- Set `"eslint.quiet": true` to hide warnings in the IDE.

Note: installer `merge` mode only adds missing settings. It does not overwrite
an existing `eslint.quiet` value.
1 change: 1 addition & 0 deletions drupal-code-quality/ide-settings/vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"phpstan.configFile": "./phpstan.neon",
"intelephense.environment.phpVersion": "8.3",
"eslint.enable": true,
"eslint.quiet": __DCQ_ESLINT_QUIET__,
"eslint.workingDirectories": [
{
"directory": ".",
Expand Down
19 changes: 19 additions & 0 deletions tests/test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@ with open(path, encoding="utf-8") as fh:

assert data.get("dcq.customSetting") == "keep"
assert data.get("eslint.nodePath") == "custom"
assert data.get("eslint.quiet") is True
PY
assert_success

Expand Down Expand Up @@ -899,12 +900,29 @@ PY
assert_failure
run grep -q '"eslint.options"' ".vscode/settings.json"
assert_failure
run grep -q '"eslint.quiet": true' ".vscode/settings.json"
assert_success
run grep -q '"stylelint.stylelintPath"' ".vscode/settings.json"
assert_failure
run grep -q '"prettier.prettierPath"' ".vscode/settings.json"
assert_failure
}

@test "VS Code settings respect DCQ_ESLINT_QUIET override" {
set -u -o pipefail
export DCQ_INSTALL_DEPS=skip
export DCQ_INSTALL_NODE_DEPS=skip
export DCQ_INSTALL_IDE_SETTINGS=overwrite
export DCQ_ESLINT_QUIET=0

run ddev add-on get "${DIR}"
assert_success

assert_file_exist ".vscode/settings.json"
run grep -q '"eslint.quiet": false' ".vscode/settings.json"
assert_success
}

@test "path map prefers DDEV_HOST_PROJECT_ROOT" {
set -u -o pipefail
run ddev add-on get "${DIR}"
Expand Down Expand Up @@ -1161,6 +1179,7 @@ PY
assert_log_contains '"stylelint.stylelintPath": "./node_modules/stylelint"' ".vscode/settings.json"
assert_log_contains '"prettier.prettierPath": "./node_modules/prettier"' ".vscode/settings.json"
assert_log_contains '"eslint.nodePath": "node_modules"' ".vscode/settings.json"
assert_log_contains '"eslint.quiet": true' ".vscode/settings.json"
assert_log_contains '"resolvePluginsRelativeTo": "."' ".vscode/settings.json"

run ./.ddev/drupal-code-quality/tooling/bin/phpstan --version
Expand Down