diff --git a/.editorconfig b/.editorconfig index ef71ea28..2d899627 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,6 @@ +# Top-most EditorConfig file +root = true + [*.{js,jsx,ts,tsx,vue,sh}] indent_style = space indent_size = 2 @@ -9,3 +12,8 @@ max_line_length = 100 [{Dockerfile}] indent_style = space indent_size = 4 + +[*.py] +indent_size = 4 # PEP 8 (the official Python style guide) recommends using 4 spaces per indentation level +indent_style = space +max_line_length = 100 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a7c3bf1c..013cc9f6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -16,7 +16,8 @@ // Scripting "timonwong.shellcheck", // Lints bash files. "ms-vscode.powershell", // Lints PowerShell files. - "ms-python.python", // Lints Python files. + "ms-python.python", // Python IntelliSense, debugging, and basic linting. + "ms-python.pylint", // Lints Python files // Distribution "ms-azuretools.vscode-docker" // Adds Docker support. ] diff --git a/docs/development.md b/docs/development.md index 22a3aa9b..b7a1f149 100644 --- a/docs/development.md +++ b/docs/development.md @@ -17,6 +17,7 @@ See [ci-cd.md](./ci-cd.md) for more information. - Refer to [action.yml](./../.github/actions/setup-node/action.yml) for the minimum required version compatible with the automated workflows. - 💡 Recommended: Use [`nvm`](https://github.com/nvm-sh/nvm) CLI to install and switch between Node.js versions. - Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options). +- For Visual Studio Code users, running the configuration script is recommended to optimize the IDE settings, as detailed in [utility scripts](#utility-scripts). ### Testing @@ -79,8 +80,8 @@ See [ci-cd.md](./ci-cd.md) for more information. - [**`npm run install-deps [-- ]`**](../scripts/npm-install.js): - Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features. - For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies. -- [**`./scripts/configure-vscode.sh`**](../scripts/configure-vscode.sh): - - This script checks and sets the necessary configurations for VSCode in `settings.json` file. +- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py): + - Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment. #### Automation scripts @@ -96,3 +97,4 @@ See [ci-cd.md](./ci-cd.md) for more information. You should use EditorConfig to follow project style. For Visual Studio Code, [`.vscode/extensions.json`](./../.vscode/extensions.json) includes list of recommended extensions. +You can use [VSCode configuration script](#utility-scripts) to automatically install those. diff --git a/scripts/configure-vscode.sh b/scripts/configure-vscode.sh deleted file mode 100755 index 1e487699..00000000 --- a/scripts/configure-vscode.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash - -# This script ensures that the '.vscode/settings.json' file exists and is configured correctly for ESLint validation on Vue and JavaScript files. -# See https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code - -declare -r SETTINGS_FILE='.vscode/settings.json' -declare -ra CONFIG_KEYS=('vue' 'javascript' 'typescript') -declare -r TEMP_FILE="tmp.$$.json" - -main() { - ensure_vscode_directory_exists - create_or_update_settings -} - -ensure_vscode_directory_exists() { - local dir_name - dir_name=$(dirname "${SETTINGS_FILE}") - if [[ ! -d ${dir_name} ]]; then - mkdir -p "${dir_name}" - echo "🎉 Created directory: ${dir_name}" - fi -} - -create_or_update_settings() { - if [[ ! -f ${SETTINGS_FILE} ]]; then - create_default_settings - else - add_or_update_eslint_validate - fi -} - -create_default_settings() { - local default_validate - default_validate=$(printf '%s' "${CONFIG_KEYS[*]}" | jq -R -s -c -M 'split(" ")') - echo "{ \"eslint.validate\": ${default_validate} }" | jq '.' > "${SETTINGS_FILE}" - echo "🎉 Created default ${SETTINGS_FILE}" -} - -add_or_update_eslint_validate() { - if ! jq -e '.["eslint.validate"]' "${SETTINGS_FILE}" >/dev/null; then - add_default_eslint_validate - else - update_eslint_validate - fi -} - -add_default_eslint_validate() { - jq --argjson keys "$(printf '%s' "${CONFIG_KEYS[*]}" \ - | jq -R -s -c 'split(" ")')" '. += {"eslint.validate": $keys}' "${SETTINGS_FILE}" > "${TEMP_FILE}" - replace_and_confirm - echo "🎉 Added default 'eslint.validate' to ${SETTINGS_FILE}" -} - -update_eslint_validate() { - local existing_keys - existing_keys=$(jq '.["eslint.validate"]' "${SETTINGS_FILE}") - for key in "${CONFIG_KEYS[@]}"; do - if ! echo "${existing_keys}" | jq 'index("'"${key}"'")' >/dev/null; then - jq '.["eslint.validate"] += ["'"${key}"'"]' "${SETTINGS_FILE}" > "${TEMP_FILE}" - mv "${TEMP_FILE}" "${SETTINGS_FILE}" - echo "🎉 Updated 'eslint.validate' in ${SETTINGS_FILE} for ${key}" - else - echo "âŠī¸ No updated needed for ${key} ${SETTINGS_FILE}." - fi - done -} - -replace_and_confirm() { - if mv "${TEMP_FILE}" "${SETTINGS_FILE}"; then - echo "🎉 Updated ${SETTINGS_FILE}" - fi -} - -main diff --git a/scripts/configure_vscode.py b/scripts/configure_vscode.py new file mode 100755 index 00000000..265e1cfa --- /dev/null +++ b/scripts/configure_vscode.py @@ -0,0 +1,162 @@ +""" +This script configures project-level VSCode settings in '.vscode/settings.json' for +development and installs recommended extensions from '.vscode/extensions.json'. +""" +# pylint: disable=missing-function-docstring + +import os +import json +import subprocess +import sys +import re +from typing import Any +from shutil import which + +VSCODE_SETTINGS_JSON_FILE: str = '.vscode/settings.json' +VSCODE_EXTENSIONS_JSON_FILE: str = '.vscode/extensions.json' + +def main() -> None: + ensure_vscode_directory_exists() + ensure_setting_file_exists() + add_or_update_settings() + install_recommended_extensions() + +def ensure_vscode_directory_exists() -> None: + vscode_directory_path = os.path.dirname(VSCODE_SETTINGS_JSON_FILE) + try: + os.makedirs(vscode_directory_path, exist_ok=True) + print_success(f"Created or verified directory: {vscode_directory_path}") + except OSError as error: + print_error(f"Error handling directory {vscode_directory_path}: {error}") + +def ensure_setting_file_exists() -> None: + try: + if os.path.isfile(VSCODE_SETTINGS_JSON_FILE): + print_success(f"VSCode settings file exists: {VSCODE_SETTINGS_JSON_FILE}") + return + with open(VSCODE_SETTINGS_JSON_FILE, 'w', encoding='utf-8') as file: + json.dump({}, file, indent=4) + print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}") + except IOError as error: + print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}") + print(f"📄 Created empty {VSCODE_SETTINGS_JSON_FILE}") + +def add_or_update_settings() -> None: + configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript']) + # Set ESLint validation for specific file types. + # Details: # pylint: disable-next=line-too-long + # - https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code + + configure_setting_key('terminal.integrated.env.linux', {"GTK_PATH": ""}) + # Unset GTK_PATH on Linux for Electron development in sandboxed environments + # like Snap or Flatpak VSCode installations, enabling script execution. + # Details: # pylint: disable-next=line-too-long + # - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274 + +def configure_setting_key(configuration_key: str, desired_value: Any) -> None: + try: + with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file: + settings: dict = json.load(file) + if configuration_key in settings: + actual_value = settings[configuration_key] + if actual_value == desired_value: + print_skip(f"Already configured as desired: \"{configuration_key}\"") + return + settings[configuration_key] = desired_value + file.seek(0) + json.dump(settings, file, indent=4) + file.truncate() + print_success(f"Added or updated configuration: {configuration_key}") + except json.JSONDecodeError: + print_error(f"Failed to update JSON for key {configuration_key}.") + +def install_recommended_extensions() -> None: + if not os.path.isfile(VSCODE_EXTENSIONS_JSON_FILE): + print_error( + f"The extensions.json file does not exist in the path: {VSCODE_EXTENSIONS_JSON_FILE}." + ) + return + with open(VSCODE_EXTENSIONS_JSON_FILE, 'r', encoding='utf-8') as file: + json_content: str = remove_json_comments(file.read()) + try: + data: dict = json.loads(json_content) + extensions: list[str] = data.get("recommendations", []) + if not extensions: + print_skip(f"No recommendations found in the {VSCODE_EXTENSIONS_JSON_FILE} file.") + return + vscode_cli_path = which('code') # More reliable than using `code`, especially on Windows. + if vscode_cli_path is None: + print_error('Visual Studio Code CLI (`code`) tool not found.') + return + install_vscode_extensions(vscode_cli_path, extensions) + except json.JSONDecodeError: + print_error(f"Invalid JSON in {VSCODE_EXTENSIONS_JSON_FILE}") + +def remove_json_comments(json_like: str) -> str: + pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)' + return re.sub( + pattern, + lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE, + ) + +def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None: + successful_installations = 0 + for ext in extensions: + try: + result = subprocess.run( + [vscode_cli_path, "--install-extension", ext], + check=True, + capture_output=True, + text=True, + ) + if "already installed" in result.stdout: + print_skip(f"Created or verified directory: {ext}") + else: + print_success(f"Installed extension: {ext}") + successful_installations += 1 + print_subprocess_output(result) + except subprocess.CalledProcessError as e: + print_subprocess_output(e) + print_error(f"Failed to install extension: {ext}") + except FileNotFoundError: + print_error(' '.join([ + f"Visual Studio Code CLI tool not found: {vscode_cli_path}." + f"Could not install extension: {ext}", + ])) + total_extensions = len(extensions) + print_installation_results(successful_installations, total_extensions) + +def print_subprocess_output(result: subprocess.CompletedProcess[str]) -> None: + output = '\n'.join([text.strip() for text in [result.stdout, result.stderr] if text]) + if not output: + return + formatted_output = '\t' + output.strip().replace('\n', '\n\t') + print(formatted_output) + +def print_installation_results(successful_installations: int, total_extensions: int) -> None: + if successful_installations == total_extensions: + print_success( + f"Successfully installed or verified all {total_extensions} recommended extensions." + ) + elif successful_installations > 0: + print_warning( + f"Partially successful: Installed or verified {successful_installations} " + f"out of {total_extensions} recommended extensions." + ) + else: + print_error("Failed to install any of the recommended extensions.") + +def print_error(message: str) -> None: + print(f"💀 Error: {message}", file=sys.stderr) + +def print_success(message: str) -> None: + print(f"✅ Success: {message}") + +def print_skip(message: str) -> None: + print(f"⏊ Skipped: {message}") + +def print_warning(message: str) -> None: + print(f"âš ī¸ Warning: {message}", file=sys.stderr) + +if __name__ == "__main__": + main()