Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

E ancora buongiorno. Qui è Sergei Omel'niytskiy. Oggi condividerò con te una delle mie questioni più complicate, ovvero cosa fare quando un progetto è sviluppato da molti programmatori di diversi livelli, prendendo come esempio un'applicazione Angular.

Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

Per lungo tempo ho lavorato solo con il mio team, dove avevamo già concordato da tempo le regole di formattazione, commento, rientri, ecc. Ci siamo abituati a queste pratiche e abbiamo lavorato bene e felici insieme. Per celebrare, ho persino pubblicato un articolo su Habr riguardo al nostro codice stilistico. Pertanto, in modo magico, usavamo solo tslint nei pre-commit.

E qui siamo cresciuti. È emerso un nuovo progetto con codice ereditato, e ad esso si sono uniti nuovi sviluppatori, per un totale di 4 abili ragazzi. E qualcosa è andato storto.

Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

Credo che molti sappiano che lavorare con codice ereditato non è piacevole. Nella mia esperienza, ho ricevuto solo un progetto che mi ha entusiasmato, e per il resto... Di cosa stavo parlando? Ah già.

A dire dire la verità, l'architettura nel progetto lasciava molto a desiderare, e commenti e tipizzazione sembravano solo un sogno. A un certo punto, mi sono sentito demoralizzato dal fatto che la nostra documentazione sulle regole di formattazione non funzionava, i commenti non venivano scritti, e il tipo - che cos'è?). Su questo bisognava fare qualcosa.

Per coloro che non vedono l'ora di conoscere tutti i passaggi subito:

  • Abbiamo diviso tslint in regole soft (per pre-commit) e regole hard (per ide, per ricordare ai programmatori ciò che hanno dimenticato di fare)

  • Abbiamo implementato la correzione automatica delle possibili regole da tslint rigido su pre-commit

  • Abbiamo scritto regole per prettier

  • Abbiamo danzato per avviare ng lint con lint-staged

Passo uno - dividi e conquista

Quando mi è venuta l'idea di irrigidire le regole del linter, ho pensato che ci saremmo impiccati. Il codice è ereditato. Bisogna capirlo, e in un tale volume si può affondare. Si è deciso di creare un secondo linter per l'ide, che non darebbe pace e costringerebbe a scrivere jsdoc per i metodi e le proprietà, scrivere interfacce o il maledetto onPush, ecc.

Quindi, nella radice, avevamo due file tslin:

tsconfig.json

{
  "rulesDirectory": [
    "node_modules/codelyzer"
  ],
  "rules": {
    "arrow-return-shorthand": true,
    "callable-types": true,
    "class-name": true,
    "comment-format": [
      true,
      "check-space"
    ],
    "curly": true,
    "deprecation": {
      "severity": "warn"
    },
    "eofline": true,
    "forin": true,
    "import-blacklist": [
      true,
      "rxjs/Rx"
    ],
    "import-spacing": true,
    "indent": [
      true,
      "spaces"
    ],
    "interface-over-type-literal": true,
    "label-position": true,
    "max-line-length": [
      true,
      200
    ],
    "member-access": false,
    "member-ordering": [
      true,
      {
        "order": [
          "static-field",
          "instance-field",
          "static-method",
          "instance-method"
        ]
      }
    ],
    "no-arg": true,
    "no-bitwise": true,
    "no-console": [
      true,
      "debug",
      "info",
      "time",
      "timeEnd",
      "trace"
    ],
    "no-construct": true,
    "no-debugger": true,
    "no-duplicate-super": true,
    "no-empty": false,
    "no-empty-interface": true,
    "no-eval": true,
    "no-inferrable-types": [
      false,
      "ignore-params"
    ],
    "no-duplicate-imports": true,
    "no-misused-new": true,
    "no-non-null-assertion": true,
    "no-redundant-jsdoc": true,
    "no-shadowed-variable": false,
    "no-string-literal": false,
    "no-string-throw": true,
    "no-switch-case-fall-through": true,
    "no-trailing-whitespace": [
      true,
      "ignore-comments",
      "ignore-jsdoc"
    ],
    "no-unnecessary-initializer": true,
    "no-unused-expression": true,
    "no-use-before-declare": false,
    "no-var-keyword": true,
    "object-literal-sort-keys": false,
    "one-line": [
      true,
      "check-open-brace",
      "check-catch",
      "check-else",
      "check-whitespace"
    ],
    "prefer-const": true,
    "quotemark": [
      true,
      "single"
    ],
    "radix": false,
    "semicolon": [
      true,
      "always"
    ],
    "triple-equals": [
      true,
      "allow-null-check"
    ],
    "typedef-whitespace": [
      true,
      {
        "call-signature": "nospace",
        "index-signature": "nospace",
        "parameter": "nospace",
        "property-declaration": "nospace",
        "variable-declaration": "nospace"
      }
    ],
    "unified-signatures": true,
    "variable-name": false,
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ],
    "directive-selector": [
      true,
      "attribute",
      "app",
      "camelCase"
    ],
    "component-selector": [
      true,
      "element",
      "app",
      "kebab-case"
    ],
    "no-output-on-prefix": false,
    "no-inputs-metadata-property": true,
    "no-outputs-metadata-property": true,
    "no-host-metadata-property": true,
    "no-input-rename": false,
    "no-output-rename": true,
    "use-lifecycle-interface": true,
    "use-pipe-transform-interface": true,
    "component-class-suffix": true,
    "directive-class-suffix": true,
    "no-consecutive-blank-lines": true
  }
}

tslint.ide_only.json

{
  "rulesDirectory": [
    "node_modules/codelyzer"
  ],
  "rules": {
    "completed-docs": [
      true,
      {
        "properties": true,
        "methods": true
      }
    ],
    "no-angle-bracket-type-assertion": true,
    "no-any": true,
    "prefer-output-readonly": true,
    "prefer-on-push-component-change-detection": true,
    "array-type": [
      true,
      "array"
    ],
    "typedef": [
      true,
      "call-signature",
      "arrow-call-signature"
    ],
    "arrow-return-shorthand": true,
    "callable-types": true,
    "class-name": true,
    "comment-format": [
      true,
      "check-space"
    ],
    "curly": true,
    "deprecation": {
      "severity": "warn"
    },
    "eofline": true,
    "forin": true,
    "import-blacklist": [
      true,
      "rxjs/Rx"
    ],
    "import-spacing": true,
    "indent": [
      true,
      "spaces"
    ],
    "interface-over-type-literal": true,
    "label-position": true,
    "max-line-length": [
      true,
      200
    ],
    "member-access": [
      true,
      "check-parameter-property",
      "check-accessor"
    ],
    "member-ordering": [
      true,
      {
        "order": [
          "public-static-field",
          "protected-static-field",
          "private-static-field",
          "public-instance-field",
          "protected-instance-field",
          "private-instance-field",
          "constructor",
          "public-static-method",
          "protected-static-method",
          "private-static-method",
          "public-instance-method",
          "protected-instance-method",
          "private-instance-method"
        ]
      }
    ],
    "no-arg": true,
    "no-bitwise": true,
    "no-console": true,
    "no-construct": true,
    "no-debugger": true,
    "no-duplicate-super": true,
    "no-empty": false,
    "no-empty-interface": true,
    "no-duplicate-switch-case": true,
    "no-eval": true,
    "no-inferrable-types": [
      false,
      "ignore-params"
    ],
    "no-duplicate-imports": true,
    "one-variable-per-declaration": true,
    "no-misused-new": true,
    "no-non-null-assertion": true,
    "prefer-template": [
      true,
      "allow-single-concat"
    ],
    "ordered-imports": true,
    "no-redundant-jsdoc": true,
    "no-shadowed-variable": false,
    "no-string-literal": false,
    "no-string-throw": true,
    "no-switch-case-fall-through": true,
    "no-trailing-whitespace": [
      true,
      "ignore-comments",
      "ignore-jsdoc"
    ],
    "ban": [
      true,
      {
        "name": [
          "Object",
          "assign"
        ],
        "message": "Utilizza cloneDeep (lodash) per copiare un oggetto"
      }
    ],
    "max-classes-per-file": [
      true,
      1
    ],
    "cyclomatic-complexity": [
      true,
      6
    ],
    "static-this": true,
    "no-unnecessary-initializer": true,
    "no-unused-expression": true,
    "no-var-keyword": true,
    "object-literal-sort-keys": false,
    "one-line": [
      true,
      "check-open-brace",
      "check-catch",
      "check-else",
      "check-whitespace"
    ],
    "prefer-const": true,
    "quotemark": [
      true,
      "single"
    ],
    "radix": false,
    "semicolon": [
      true,
      "always"
    ],
    "triple-equals": [
      true,
      "allow-null-check"
    ],
    "typedef-whitespace": [
      true,
      {
        "call-signature": "nospace",
        "index-signature": "nospace",
        "parameter": "nospace",
        "property-declaration": "nospace",
        "variable-declaration": "nospace"
      }
    ],
    "unified-signatures": true,
    "variable-name": false,
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ],
    "directive-selector": [
      true,
      "attribute",
      "app",
      "camelCase"
    ],
    "component-selector": [
      true,
      "element",
      "app",
      "kebab-case"
    ],
    "no-output-on-prefix": false,
    "no-inputs-metadata-property": true,
    "no-outputs-metadata-property": true,
    "no-host-metadata-property": true,
    "no-input-rename": false,
    "no-output-rename": true,
    "use-lifecycle-interface": true,
    "use-pipe-transform-interface": true,
    "component-class-suffix": true,
    "directive-class-suffix": true,
    "no-consecutive-blank-lines": true
  }
}

Nel file src/tslint abbiamo sostituito il tslint standard con ide

src/tslint.json

{
    "extends": "../tslint.ide_only.json",
    "rules": {
        "directive-selector": [
            true,
            "attribute",
            "app",
            "camelCase"
        ],
        "component-selector": [
            true,
            "element",
            "app",
            "kebab-case"
        ]
    }
}

И поправил запуск нашего линтера в скритах package.json

ng lint --tslint-config ./tslint.json --fix`

Dopo di che abbiamo cominciato a riscontrare problemi con gli elementi sottolineati che necessitano di correzione.

Passo due — correzione di alcuni aspetti

Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

tslint ha regole con has fixer. Quindi sfruttiamo questa opportunità.

tslint --project tslint.ide_only.json --fix --force

Qui avviamo le regole del linter rigoroso con la correzione automatica delle impostazioni disponibili e diciamo che questo comando non deve restituire errori (dopotutto, il nostro obiettivo è fare correzioni automatiche).

Passo tre — scrivi in modo elegante

Quando ognuno scrive nel proprio stile, alla fine risulta stancante. È fondamentale scrivere il codice in modo da sembrare prodotto da una sola persona. Per questo ho aggiunto prettier, con le seguenti impostazioni:

.prettierr.yaml

printWidth: 200     # Numero massimo di caratteri per riga
tabWidth: 2         # Spazi per tabulazione
singleQuote: true   # Utilizzare le virgolette singole
trailingComma: all  # Utilizzare le virgole dove possibile
arrowParens: always # Le funzioni freccia appaiono come (x) => x
overrides:
  - files: "*.ts"   # Controllo dei file *.ts
    options:
      parser: typescript  # Linguaggio nei file *.ts

E ho aggiunto il comando: prettier --write --config .prettierr.yaml

Passo quattro — E come ci ordini di eseguire tutto questo?

Adesso analizziamo più dettagliatamente come eseguire tutto ciò. Per far funzionare il tutto, dobbiamo scaricare le seguenti librerie:

npm i -D prettier lint-staged husky

Utilizzando husky, collegheremo l'esecuzione dei nostri comandi a un hook git — pre-commit. lint-staged eseguirà i comandi in base ai file modificati (includendo anche questi file nei nostri comandi).

Vorrei anche delineare il problema con cui mi sono imbattuto. Nel nostro progetto utilizziamo ng lint. Quando lo usiamo insieme a lint-staged, i file modificati vengono aggiunti al nostro comando. ng lint ha una chiave per questo --files, ma, come ho capito, non vede un gruppo di file e ha bisogno di aggiungere questa chiave per ogni file. Per questo ho creato un file:

lint.sh

#!/bin/bash

PROJECT=$1
shift
SOURCES=$@
DESTINATIONS=""
DELIMITER=""

for src in $SOURCES
do
    DELIMITER=" --files "
    DESTINATIONS="$DESTINATIONS$DELIMITER${src}"
done

ng lint $PROJECT --tslint-config ./tslint.json $DESTINATIONS

Per avviare questo file, dobbiamo fornire il nome del progetto. Si trova nel file angular.json nella proprietà project. Nel mio caso, è partner-account e partner-account-e2e. Ho bisogno del primo.

Tornerò alla configurazione. Il nostro package.json ora appare così:

  "husky": {
    "hooks": {
      "pre-commit": "lint-staged --relative"
    }
  },
  "lint-staged": {
    "*.{ts,js}": [
      "prettier --write --config .prettierr.yaml",
      "tslint --project tslint.ide_only.json --fix --force",
      "sh lint.sh partner-account",
      "git add"
    ],
    "*.{html,scss,css}": [
      "prettier --write --config .prettierr.yaml",
      "git add"
    ]
  },

Fai attenzione a lint-staged --relative. Il parametro --relative è obbligatorio. Ora, al momento del commit, viene eseguito lint-staged. Questo seleziona i file e avvia la lista di comandi in base a essi.

Purtroppo, non elimina la revisione del codice, ma è diventata molto più pulita. Nota che ora ricordo meno frequentemente agli sviluppatori riguardo ai modificatori di accesso, alla descrizione dei metodi e delle proprietà, e la loro creatività è scritta in uno stile uniforme (beh, quasi 😀 ).

P.S. — Grazie per le immagini al nostro PM.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster