ESLint, un emmerdeur qui a souvent raison

Ceci fait suite à l’article Goo 3 avec Laravel 9, Sail, Inertia, Vue3, tailwindcss, Vite et PrimeVue, car on a oublié un morceau : le linter; analyseur syntaxique aidant à améliorer la qualité de son développement en respectant certains standards et bonnes pratiques. Cet oubli est dû on montage manuel de la solution, alors qu’en passant par la ligne de commande il est proposé parmi une suite d’options.

Mise en place

Pour vue, voici directement le linter et son plugin, en suivant leur guide, s’en suit le fichier de configuration que l’on peut personnaliser pour coller à ses besoins.

npm install --save-dev eslint eslint-plugin-vue
module.exports = {
    extends: [
        // add more generic rulesets here, such as:
        // 'eslint:recommended',
        'plugin:vue/vue3-recommended',
        // 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
    ],
    rules: {
        // override/add rules settings here, such as:
        // 'vue/no-unused-vars': 'error'
    }
}

Ça c’est la doc, mais évidemment ça ne se passera pas forcément bien car on est pas issu d’un projet généré par la ligne de commande, ben oui. J’ai donc cherché à comprendre un peu pourquoi le linter ne comprend pas Typescript, ni même module de son propre fichier de configuration. Ça commence mal.

Du coup, j’ai trouvé le npm @vue/eslint-config-typescript qui nous aide pas mal. J’ai installé et créé le fichier .eslintrc.cjs et j’y ai mis ce qu’ils indiquent :

/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")

module.exports = {
    extends: [
        'eslint:recommended',
        'plugin:vue/vue3-essential',
        '@vue/eslint-config-typescript'
    ]
}

Utilisant InteliJ, il vous faudra surement activer l’usage de la config.

Aménagements pour respecter le linter

C’est parti pour modifier pas mal de code, je vais donc corriger les blocs de code vu dans l’article PrimeVue : rendu et composant de cellule et vous expliquer la raison si elle n’est pas évidente.

Je commence par les modèles du tableau, component et son type Object devient any, du fait que <component> peut accepter un string en paramètre de :is.

Notez également la modification au niveau du prototype de renderer en transformant Object par un type plus précis, un tuple permettant de décrire la structure {clef: {}, clef2: {}, ...}, vous retrouverez cette modification plus loin également.

export interface ColDef {
    field: string
    header: string
    renderer?: (rowData: {[key: string]: any}, field?: string) => string
    sortable?: boolean
    components?: CellComponent[]
}

export interface CellComponent {
    component: any
    props?: Object
    on?: Object
    model?: Object
}

Ensuite, coté composant simple-table, différents points se présentent. D’abord les v-for sur composant inconnu ou <component> aiment avoir une clef d’identification unique (v-bind:key).

...
        <Column v-for="(c, ci) in cols" v-bind:key="ci" :field="c.field" :header="c.header" :sortable="c.sortable">
            <template #body="colProps" v-if="c.components">
                <component v-for="(comp, i) in c.components"
                           v-bind:key="i"
...

Le linter râlera également sur certaines valeurs par défaut des props ou du type de data. default est une fonction et data de rowClass trouve une définition any dans leur code et rien ailleurs.

...
    colsDef: {
        type: Array as PropType<ColDef[]>,
        required: true,
        default: () => []
    },
...
    rowClass: {
        type: Function,
        default: (data: any) => '' // Cf primevue github
    }
...

map râlait déjà que son hôte ne soit pas obligatoirement défini, ce qui est faux car required dans les props, j’imagine qu’il ne fait pas le liens, Angular a ce genre de soucis aussi. Et pour faire propre vu que l’on rend un objet neuf au retour de map, on le type.

...
const cols: ComputedRef<ColDef[]> = computed<ColDef[]>(() => {
    return props.colsDef!.map((c) => ({
...
    }) as ColDef)
})

const interceptEvents = (events: { [key: string]: Function } | undefined, rowData: Object, field: string): Object => {
    if (!events) {
        return {}
    }

    const interceptedEvents: { [key: string]: Function } = {}
    Object.keys(events).forEach((e) => {
        interceptedEvents[e] = (ev: Object) => {
            return events[e](ev, rowData, field)
        }
    })
    return interceptedEvents
}
...

Nous revoilà avec notre tuple, cette fois adapté pour les events. Et vu qu’on pourrait ne pas en donner, mais qu’on l’utilise, il vaut mieux avoir un contrôle et un retour propre. Attention que si on avait utilisé l’écriture events?: il aurait râlé car les paramètres facultatifs doivent se trouver après les impératifs. Et pour bien faire on type ev en Object.

Conclusion

Comme vous l’aurez peut-être expérimenté ou lu ici, si ce n’est pas fait à votre place, ce n’est pas forcément évident à mettre en route et à définir.

Il y a moyen de perdre beaucoup de temps si on ne s’y prend pas correctement dès le départ, c’est un réflexe à avoir. Et ici ce ne sont que quelques broutilles, ça fonctionne MAIS ce n’était pas assez bien. Et même si c’est peu de chose, cela aura pris à un padawan testeur une bonne après-midi.