@villedemontreal/scripting

Scripting core utilities

Usage no npm install needed!

<script type="module">
  import villedemontrealScripting from 'https://cdn.skypack.dev/@villedemontreal/scripting';
</script>

README

Librairie pour la création et utilisation de scripts dans un projet Node.js

Cette librairie, basée sur Caporal, offre une manière standardisée de créer et de lancer des scripts localement dans un projet d'API ou dans une librairie utilisant Node.js.

Les scripts sont développés en Typescript, ce qui vous permet d'utiliser des librairies npm, de déboguer avec breakpoints, etc.

Un ensemble de scripts core sont fournis et sont disponibles automatiquement. Vous ajoutez dans votre projet les scripts supplémentaires dont vous avez besoin.

Une aide est automatiquement créée (et affichable) pour les scripts et leurs options sont automatiquement validées au runtime.

Notez que cette librairie est conçue pour ne fonctionner que lorsque les dépendances dev sont disponibles! Ce que cela signifie, c'est que dans vos Dockerfile, lorsque seules les dépendances production sont disponibles, vous devrez lancer node src/start pour démarrer l'application et non utiliser le script start! De lancer ./run start ne fonctionnerait pas car des dépendances dev manqueraient.

Ceci apporte le bénéfice à votre application nodejs de recevoir les signaux émis par Kubernetes quand il s'apprête à fermer un conteneur (SIGTERM), ce qui permet à l'application de faire du cleanup avant de mourir (Voir pod-lifecycle/pod-termination)

Utilisation de la librairie

Installation

Un projet doit contenir quelques dépendances pour être en mesure d'utiliser certains scripts fournis par cette librairie:

  • typescript
  • @villemontreal/core-utils-scripting-core-nodejs-lib, la librairie elle-même.
  • @villemontreal/lint-config-villemontreal, pour être en mesure d'utiliser les scripts core de lintage.
  • mocha, pour être en mesure d'utiliser les scripts core test et test-units.
  • mocha-jenkins-reporter, pour être en mesure d'utiliser les scripts core test et test-units, avec l'option --jenkins.
  • nyc, pour être en mesure d'utiliser le script core show-coverage. Remarquez aussi que si cette dépendance n'est pas trouvée dans votre projet, les tests seront exécutés uniquement avec Mocha, sans code coverage (un avertissement sera affiché).

Il faut aussi créer deux fichiers à la racine du projet:

  1. run.cmd contenant:
@echo off
node "%~dp0\run" %*
  1. run contenant:
#!/usr/bin/env node
const caporal = require('@caporal/core').program;

// Here, you could add custom global options, or tweak
// the Caporal instance, if required.

// Then it is run:
require(`${__dirname}/node_modules/@villemontreal/core-utils-scripting-core-nodejs-lib/dist/src/run`).run({
  caporal,
  projectRoot: __dirname,
  scriptsIndexModule: `./scripts/index`,
  testsLocations: [`./src/**/*.test.js`, `./tests/**/*.test.js`]
});

Note: si vous êtes sur Linux/Mac, vous devrez aussi lancer chmod +x run pour rendre exécutable le fichier ./run.

Configurations

Il est possible de configurer certains aspects de la librairie dans le fichier run du projet.

  • Une fois l'instance de caporal obtenue (const caporal), vous pouvez la configurer. Par exemple, il est possible d'y ajouter des options globales customs avec caporal.option(...).Mais, en règle général, vous ne devriez pas avoir à configurer cette instance directement.

  • scriptsIndexModule : qui est le chemin vers l'index (le fichier Typescript) exportant les scripts custom de votre projet. Dans un projet d'API, ce chemin ne devrait probablement pas être changé. Notez que si votre projet ne définie pas de scripts custom, vous pouvez complètement enlever ce paramètre ou encore le mettre à null.

  • testsLocations : qui liste les endroits où des fichiers de tests peuvent être trouvés dans votre application.

Lancer un script

Un script se lance en ligne de commande avec:

  • Windows : > run [scriptName] [options]
  • Linux/Mac: > ./run [scriptName] [options]

Par exemple:

> run test --jenkins

Obtenir de l'aide sur les scripts

  • Pour obtenir la liste complète des scripts disponibles et leurs options, lancez: > run
    ou
    > run help

  • Pour obtenir de l'aide sur un script en particulier, lancez: > run [scriptName] --help
    ou
    > run help [scriptName]

Lancez un script en utilisant les configurations de tests

Un script dont le nom:

  • est "test"
  • est "validate"
  • débute par "test-"
  • débute par "testing:"

sera exécuté avec la variable d'environnement NODE_APP_INSTANCE automatiquement mise à tests. Ceci fera en sorte que les configurations -tests seront utilisées.

Autrement, vous pouvez spécifier l'option globale --testing pour forcer cette valeur.

Ajouter un script custom dans votre projet

Un script propre à un projet sera ajouté en général ajouté sous un répetoire "scripts" racine. Notez que vous pouvez créer plusieurs niveaux de sous-répertoires.

Vous devez exporter un nouveau script dans l'index de vos scripts (scripts/index.ts).

  • Un Script doit au minimum implémenter 3 méthodes:

    • name(): le nom du script
    • description(): une description pour le script
    • main(): contenant le code du script
  • Si votre script demande plus de configurations qu'un nom et description, vous pouvez également implémenter configure().

  • La classe de base fournit également un logger vous permettant d'afficher des messages en respectant le niveau demandé par les arguments --silent, --quiet et --verbose pouvant être passés lors d'une commande.

Si une erreur survient dans votre script, lancez simplement une erreur régulière (throw new Error(...)).

Note: référez-vous aux scripts core fournis par cette librairie sous scripts/testing comme exemples!

Ajouter des options à un script

Pour ajouter des options ou des arguments à vos scripts:

  1. Vous overridez la méthode configure(command) et vous utilisez l'object command fourni pour ajouter les options. Par exemple:
protected async configure(command: Command): Promise<void> {
  command.option(`-p, --port <number>`, `A port number`, {
    validator: caporal.NUMBER
  });
}
  1. Vous créez une interface Options au dessus de la classe de votre script et définissez les options dans cette interface. Par exemple:
export interface Options {
  port?: number;
}
  1. Vous paramétrisez la classe de base ScriptBase avec cette interface, comme premier argument. Par exemple:
export class MyScript extends ScriptBase<Options> {
  // "this.options.port" est maintenant disponible de manière typée.
}

Ajouter une option globale

Cette fonctionnalité ne devrait pas souvent être requise, mais il est possible d'ajouter une option globale, en plus de celles qui sont ajoutées automatiquement par la librairie.

Pour se faire, vous

  1. Éditez le fichier run de votre projet pour ajouter l'option globale à l'instance caporal. Par exemple:
caporal.option('--custom', 'Custom global option', {
  global: true
});
  1. Créez dans votre projet une interface héritant de IGlobalOptions. Par exemple:
export interface IAppGlobalOptions extends IGlobalOptions {
  something?: boolean;
}
  1. Dans vos scripts, vous paramétrisez la classe de base ScriptBase avec cette interface, comme deuxième argument. Par exemple:
export class MyScript extends ScriptBase<Options, IAppGlobalOptions> {
  // "this.options.something" est maintenant disponible de manière typée.
}

Invoquer un script dans un script

Dans un script, il est possible d'en appeller un (ou plusieurs) autre en utilisant la méthode this.invokeScript(...). Vous passez à cette méthode la classe du script à appeler ainsi que les options et arguments à utiliser.

Notez que les options globales seront automatiquement disponibles par le script appellé! Vous ne les spécifiez que si vous avez besoin d'en changer la valeur.

Lancer une commande shell dans un script

Un utilitaire fourni this.invokeShellCommand(...) permet de lancer une commande shell dans un script. Par exemple:

await this.invokeShellCommand('node', ['some/js/module', '--somearg']);

Le troisième paramètre est un objet options. Une de ces options est useTestsNodeAppInstance que vous pouvez mettre à true pour lancer le nouveau process avec une variable d'environnement "NODE_APP_INSTANCE" égale à "tests" et d'ainsi faire en sorte que les configurations de tests soient utilisées si du code de l'API est exécuté dans le process.

Overrider un script core

Il est possible d'overrider un script core (c'est à dire un script fourni par cette librairie). Par exemple, vous voulez ajouter des validations supplémentaires lorsque run test est exécuté, en addition du lintage et des tests unitaires lancés par le script test core fourni par cette librairie.

Pour se faire, vous créez simplement un nouveau script dans votre projet et vous lui donner le même nom que le script à overrider. Puis, dans la méthode main() de votre nouveau script, vous invoquez le script core en lui passant les options requises et vous ajoutez tout autre code requis par la suite.

Note: Si vous héritez de la classe du script core pour implémenter votre version du script, n'oubliez pas de redéfinir la méthode outputName() pour lui faire retourner le nom du script directement! Autrement il sera affiché "- core" dans les logs! Par exemple:

get outputName(): string {
  return this.name;
}

Note Dans de rares cas où vous devriez overrider des script cores qui ne sont pas compilés automatiquement par défaut, vous devrez spécifier le nom de ces scripts dans le paramètre overridenCoreScripts du fichier run. Ceci indique à la librairie de scripting qu'elle doit effectuer la compilation, malgré qu'il s'agisse de scripts core qui ne le sont pas par défaut. Présentement, les scripts core non compilés par défaut sont:
prettier, prettier-fix, tslint, tslint-fix, lint, lint-fix et watch. Bref, des scripts qui n'ont pas à être overridés en général!

Ajouter un script en mode "testing"

Pour vos tests, il est possible de spécifier un script, mais en faisant en sorte qu'il ne soit disponible que lorsque vos tests roulent. Pour ce faire, vous devez préfixer le nom du script par TESTING_SCRIPT_NAME_PREFIX qui est une constante exportée par le fichier src/scriptBase.ts de la librairie. Par exemple:

get name(): string {
  return `${TESTING_SCRIPT_NAME_PREFIX}myTestScript`;
}

Script non listé

Pour éviter qu'un script ne se retrouve dans la documentation globale générée par Caporal, vous pouvez appeller command.hide();, dans la méthode configure() overridée:

protected async configure(command: Command): Promise<void> {
  command.hide();
  // ...
}

Scripts npm

Si désiré, vous pouvez aussi déclarer vos scripts dans le fichier package.json de votre projet. Ces scripts npm ne seront que des indirections vers le fichier run bootstrappant la librairie de scripting, en utilisant node. Par exemple:

"scripts": {
  "test": "node run test",
  // ...
}

Notez que si vous utilisez npm pour lancer un script, et que vous avez à passer des options, il vous faudra ajouter l'argument spécial "--" avant les options. Par exemple:

> npm run test -- --nc

Ceci est passablement plus verbeux que la méthode utilisant run directement:

run test --nc

Développement de la librairie

Launch configurations

Ces "launch configurations" sont founies pour développer la librairie dans VSCode :

  • "Debug script" - Lance un script en mode debug. Vous pouvez mettre des breakpoints et ils seront respectés. Pour changer le script exécuté, vous devez modifier la ligne appropriée dans le fichier ".vscode/launch.json".

  • "Debug all tests" - Lance tous les tests en mode debug. Vous pouvez mettre des breakpoints et ils seront respectés.

  • "Debug current test file" - Lance en mode debug le fichier de tests présentement ouvert dans VSCode. Vous pouvez mettre des breakpoints et ils seront respectés.

  • "Debug current tests file - fast" - Lance en mode debug le fichier de tests présentement ouvert dans VSCode. Aucune compilation n'est effectuée au préalable. Cette launch configuration doit être utilisée lorsque la compilation incrémentale roule (en lançant au préalable run watch dans un terminal).

Ajouter un script core à la librairie

  1. Créez le nouveau script sous src/scripts.

  2. Exporter le script dans l'index src/scripts/index.

Test et publication de la librairie sur Nexus

En mergant une pull request dans la branche develop, un artifact "-pre.build" sera créé automatiquement dans Nexus. Vous pouvez utiliser cette version temporaire de la librairie pour bien la tester dans un réel projet.

Une fois mergée dans master, la librairie est définitiement publiée dans Nexus, en utilisant la version spécifiée dans le package.json.

Artifact Nexus privé, lors du développement

Lors du développement d'une nouvelle fonctionnalité, sur une branche feature, il peut parfois être utile de déployer une version temporaire de la librairie dans Nexus. Ceci permet de bien tester l'utilisation de la librairie modifiée dans un vrai projet, ou même dans une autre librairie elle-même par la suite utilisée dans un vrai projet.

Si le code à tester est terminé et prêt à être mis en commun avec d'autres développeurs, la solution de base, comme spécifiée à la section précédante, est de merger sur develop: ceci créera automatiquement un artifact "-pre-build" dans Nexus. Cependant, si le code est encore en développement et vous désirez éviter de polluer la branche commune develop avec du code temporaire, il y a une solution permettant de générer un artifact "[votre prénom]-pre-build" temporaire dans Nexus, à partir d'une branche feature directement:

  1. Checkoutez votre branche feature dans une branche nommée "nexus". Ce nom est important et correspond à une entrée dans le Jenkinsfile.
  2. Une fois sur la branche nexus, ajoutez un suffixe "-[votre prénom]" à la version dans le package.json, par exemple: "5.15.0-roger". Ceci permet d'éviter tout conflit dans Nexus et exprime clairement qu'il s'agit d'une version temporaire pour votre développement privé.
  3. Commitez et poussez la branche nexus.
  4. Une fois le build Jenkins terminé, un artifact pour votre version aura été déployé dans Nexus.

Notez que, lors du développement dans une branche feature, l'utilisation d'un simple npm link local peut souvent être suffisant! Mais cette solution a ses limites, par exemple si vous désirez tester la librairie modifiée dans un container Docker.

Aide / Contributions

Pour obtenir de l'aide avec ce gabarit, vous pouvez poster sur la salle Google Chat dev-discussions.

Notez que les contributions sous forme de pull requests sont bienvenues.