Wie ich Ordnung in ein Projekt gebracht habe, wo es ein Chaos an direkter Handhabung gab (Einstellungen für tslint, prettier usw.)

Und wieder hallo. Am Apparat Omelnytskyj Sergey. Heute werde ich mit Ihnen eines meiner Kopfschmerzthemen teilen, nämlich – was zu tun ist, wenn viele Programmierer unterschiedlichen Niveaus an einem Projekt arbeiten, anhand eines Beispiels einer Angular-Anwendung.

Wie ich Ordnung in ein Projekt gebracht habe, wo es ein Chaos an direkter Handhabung gab (Einstellungen für tslint, prettier usw.)

Es hat sich so ergeben, dass ich lange Zeit nur mit meinem Team gearbeitet habe, wo wir längst Regeln für das Formatieren, Kommentieren, Einrücken usw. abgestimmt hatten. Wir hatten uns daran gewöhnt und lebten glücklich und zufrieden. Aus Freude habe ich sogar einen Artikel auf Habr veröffentlicht über unseren Code-Stil. Deshalb haben wir irgendwie nur TSLint beim Pre-Commit verwendet.

Und hier sind wir gewachsen. Ein neues Projekt mit geerbtem Code ist entstanden, und dazu kamen vier neue Entwickler. Und dann ging alles nicht nach Plan.

Wie ich Ordnung in ein Projekt gebracht habe, wo es ein Chaos an direkter Handhabung gab (Einstellungen für tslint, prettier usw.)

Ich denke, viele wissen, dass es kein Vergnügen ist, mit geerbtem Code zu arbeiten. Mir fällt nur ein Projekt ein, von dem ich begeistert war, und der Rest… Worüber war ich gerade?) Ach ja.

Um ehrlich zu sein, ließ die Architektur des Projekts zu wünschen übrig, und Kommentare und Typisierung waren uns nur ein Traum. Irgendwann war ich frustriert, weil unsere Dokumentation über die Formatierungsregeln nicht funktionierte, Kommentare nicht verfasst wurden, und was Typen angeht – was ist das?). Damit musste etwas gemacht werden.

Für diejenigen, die alles sofort erfahren möchten:

  • Wir haben TSLint in weiche Regeln (für Pre-Commit) und strenge Regeln (für die IDE, um daran zu erinnern, was die Entwickler vergessen haben) unterteilt.

  • Wir haben beim Pre-Commit die automatische Behebung möglicher Verstöße durch strenge TSLint-Regeln aktiviert.

  • Wir haben Regeln für Prettier geschrieben.

  • Wir haben mit einem Ritual gearbeitet, um ng lint mit lint-staged zu starten.

Schritt eins – teile und herrsche.

Als mir die Idee kam, die Regeln des Linters zu verschärfen, dachte ich, wir würden daran scheitern. Der Code ist schließlich geerbt. Man muss sich damit auseinandersetzen, und bei solchem Umfang kann man leicht ins Schleudern kommen. Es wurde beschlossen, einen zweiten Linter für die IDE zu erstellen, der die Augen beleidigt und die Entwickler zwingt, JSDoc für Methoden und Eigenschaften zu schreiben, Schnittstellen oder das gefürchtete onPush usw.

Und so lagen im Wurzelverzeichnis zwei TSLint-Dateien:

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": "Verwenden Sie cloneDeep (lodash) zum Kopieren von Objekten"
      }
    ],
    "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
  }
}

In der Datei src/tslint Wir haben den Standard-tslint durch ide ersetzt

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`

Nach dem haben wir angefangen, uns an den unterstrichenen Dingen, die angepasst werden müssen, festzuhalten.

Schritt zwei – ein paar Punkte anpassen

Wie ich Ordnung in ein Projekt gebracht habe, wo es ein Chaos an direkter Handhabung gab (Einstellungen für tslint, prettier usw.)

TsLint hat Regeln mit hat fixer. Lass uns das nutzen.

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

Hier führen wir die strengen Linter-Regeln mit der automatischen Korrektur der verfügbaren Parameter aus und sagen, dass dieser Befehl keine Fehler zurückgeben soll (schließlich ist unser Ziel die automatische Korrektur).

Schritt drei – schreibe schön

Wenn jeder in seinem eigenen Stil schreibt, wird es letztendlich ermüdend. Der Code sollte so geschrieben werden, dass es den Anschein hat, als würde ihn eine Person erstellen. Dazu habe ich Prettier mit den folgenden Einstellungen hinzugefügt:

.prettierr.yaml

printWidth: 200     # Maximale Anzahl von Zeichen in einer Zeile
tabWidth: 2         # Leerzeichen im Tab
singleQuote: true   # Einfachapostrophe verwenden
trailingComma: all  # Kommata wo möglich verwenden
arrowParens: always # Pfeilfunktionen sehen aus wie (x) => x
overrides:
  - files: "*.ts"   # Prüfung von *.ts-Dateien
    options:
      parser: typescript  # Sprache in *.ts-Dateien

Und ich habe den Befehl hinzugefügt: prettier --write --config .prettierr.yaml

Schritt vier – und wie befehlen Sie, all dies auszuführen?

Lassen Sie uns nun genauer betrachten, wie Sie all dies ausführen können. Damit dies funktioniert, müssen wir die folgenden Bibliotheken herunterladen:

npm i -D prettier lint-staged husky

Mit Husky werden wir den Start unserer Befehle an den Git-Hook – pre-commit hängen. lint-staged wird unsere Befehle je nach den geänderten Dateien ausführen (diese Dateien auch in unsere Befehle einfügen).

Ich möchte sofort das Problem skizzieren, mit dem ich konfrontiert war. In unserem Projekt verwenden wir ng lint. Wenn wir es zusammen mit lint-staged verwenden, werden die geänderten Dateien zu unserem Befehl hinzugefügt. Für ng lint gibt es hierfür einen Schlüssel --files, aber wie ich verstanden habe, sieht er nicht die Sammlung von Dateien und benötigt diesen Schlüssel für jede Datei. Daher musste ich die Datei erstellen:

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

Um diese Datei auszuführen, müssen wir den Namen des Projekts übergeben. Es befindet sich in der Datei angular.json im Attribut project. In meinem Fall ist das partner-account und partner-account-e2e. Ich benötige die erste.

Ich komme zurück zur Konfiguration. Unser package.json sieht jetzt so aus:

  "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"
    ]
  },

Achten Sie darauf, dass lint-staged --relative. Der Parameter --relative ist erforderlich. Jetzt wird beim Commit lint-staged. Es wählt die Dateien aus und führt je nach ihnen eine Liste von Befehlen aus.

Leider hebt das nicht die Code-Überprüfung auf, aber es ist viel sauberer geworden. Ich möchte anmerken, dass ich seltener die Entwickler an Zugriffsmodifizierer, Methodendeklarationen und Eigenschaften erinnern musste, und ihre Kreativität wurde in einem einheitlichen Stil (nahezu 😀 ) niedergeschrieben.

P.S. — Danke für die Bilder an unseren PM.

Quelle: habr.com

60GB SSD 8Gb DDR4