Redémarrer la machine virtuelle de Local by Flywheel photo

Local by Flywheel ne démarre plus à cause du renouvellement du certificat TLS de la machine virtuelle (docker) : une solution

J’utilise quotidiennement Local by Flywheel pour développer ou debugger des problèmes sur certains sites.

C’est une bonne alternative lorsque les hébergeurs ne proposent pas de site staging à leurs clients (les meilleurs hébergeurs proposent évidemment un staging, c’est la base).

L’autre jour, tournée de mises à jour suivie d’un reboot, je lance Local et patatras: il ne veut plus démarrer et visiblement reste bloqué sur une tentative de renouvellement de certificat TLS pour la machine virtuelle qui tourne sous Docker.

Si cela vous arrive, voici la marche à suivre. Il suffit de copier ces lignes d’instructions dans votre terminal. Concrètement, nous allons télécharger une nouvelle version du fichier ISO Boot2Docker et laisser le système se ré-provisionner.

Le processus implique de créer un alias (local-docker-machine) pour la machine virtuelle docker “Local by Flywheel”, et ensuite de lancer la série de commandes suivantes sur cet alias.

Voici les commandes à lancer dans le terminal:

alias local-docker-machine="/Applications/Local\ by\ Flywheel.app/Contents/Resources/extraResources/virtual-machine/vendor/docker/osx/docker-machine"
local-docker-machine stop local-by-flywheel
rm -rf ~/.docker/machine/certs
local-docker-machine create local-cert-gen
local-docker-machine start local-by-flywheel
local-docker-machine regenerate-certs -f local-by-flywheel
local-docker-machine rm -f local-cert-genCode language: JavaScript (javascript)

Voici le résultat de ces commandes:

Creating CA: /Users/matt/.docker/machine/certs/ca.pem
Creating client certificate: /Users/matt/.docker/machine/certs/cert.pem
Running pre-create checks...
(local-cert-gen) No default Boot2Docker ISO found locally, downloading the latest release...
(local-cert-gen) Latest release for github.com/boot2docker/boot2docker is v19.03.5
(local-cert-gen) Downloading /Users/matt/.docker/machine/cache/boot2docker.iso from https://github.com/boot2docker/boot2docker/releases/download/v19.03.5/boot2docker.iso...
(local-cert-gen) 0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%
Creating machine...
(local-cert-gen) Copying /Users/matt/.docker/machine/cache/boot2docker.iso to /Users/matt/.docker/machine/machines/local-cert-gen/boot2docker.iso...
(local-cert-gen) Creating VirtualBox VM...
(local-cert-gen) Creating SSH key...
(local-cert-gen) Starting the VM...
(local-cert-gen) Check network to re-create if needed...
(local-cert-gen) Found a new host-only adapter: "vboxnet1"
(local-cert-gen) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: /Applications/Local by Flywheel.app/Contents/Resources/extraResources/virtual-machine/vendor/docker/osx/docker-machine env local-cert-gen
Docker machine "local-by-flywheel" does not exist. Use "docker-machine ls" to list machines. Use "docker-machine create" to add a new one.
Regenerating TLS certificates
Docker machine "local-by-flywheel" does not exist. Use "docker-machine ls" to list machines. Use "docker-machine create" to add a new one.
About to remove local-cert-gen
WARNING: This action will delete both local reference and remote instance.
Successfully removed local-cert-genCode language: JavaScript (javascript)

Vous n’avez plus qu’à lancer Local by Flywheel: lancement maintenant impeccable et toutes les machines virtuelles sont bien là.

jQuery : sélectionner un élément dont l'ID ou la classe commence ou finit par une chaîne photo 1

JavaScript et jQuery : ajouter !important en CSS proprement

jQuery possède une limitation qui peut vite devenir agaçante : on ne peut pas ajouter correctement !important à une propriété CSS avec la méthode .css().

Par exemple, ce code ne fonctionne pas comme on pourrait l’espérer :

jQuery('.foo').css('border', '4px #000 solid !important');Code language: JavaScript (javascript)

En revanche, cette déclaration fonctionne bien :

jQuery('.foo').css('border', '4px #000 solid');Code language: JavaScript (javascript)

Le problème vient du fait que !important n’est pas une simple partie de la valeur CSS. C’est une priorité de déclaration dans la cascade CSS. La méthode jQuery .css() sert à lire ou définir des propriétés CSS, mais elle ne fournit pas d’argument séparé pour la priorité important. La documentation jQuery présente .css() comme une méthode pratique pour obtenir ou définir des propriétés de style, pas comme une API complète de gestion des priorités CSS.

Voici donc plusieurs solutions, de la plus propre à la plus directe.

Avant de commencer : faut-il vraiment utiliser !important ?

!important doit rester une exception, pas une stratégie de mise en page. Dans beaucoup de cas, si l’on a besoin de !important, c’est que la cascade CSS, l’ordre de chargement ou la spécificité des sélecteurs posent problème.

MDN rappelle que la spécificité détermine quelles valeurs CSS s’appliquent lorsqu’un élément est ciblé par plusieurs règles. MDN conseille aussi d’éviter !important lorsque c’est possible, et de privilégier des sélecteurs mieux pensés, des règles moins lourdes ou des cascade layers.

Dans un projet propre, on essaiera donc d’abord :

  • d’ajouter une classe CSS ;
  • de charger la feuille de style dans le bon ordre ;
  • d’utiliser un sélecteur plus précis ;
  • de réduire la spécificité des règles existantes ;
  • d’éviter les styles inline lorsque c’est possible.

Cela dit, il existe des cas réels où !important est nécessaire : surcharge d’un plugin, script tiers, style inline imposé, bannière publicitaire, widget externe ou correctif rapide sur une interface que l’on ne contrôle pas totalement.

Première solution : ajouter une classe CSS

C’est la solution la plus propre dans la majorité des cas. Au lieu d’ajouter directement du style inline en JavaScript, on ajoute une classe avec jQuery, puis on écrit la règle CSS dans une feuille de style.

jQuery('.foo').addClass('has-black-border');Code language: JavaScript (javascript)

Et côté CSS :

.has-black-border {
  border: 4px solid #000 !important;
}Code language: CSS (css)

Cette méthode a plusieurs avantages :

  • le style reste dans le CSS ;
  • le JavaScript ne gère que le comportement ;
  • la règle est plus facile à retrouver ;
  • on peut la modifier sans toucher au script ;
  • on évite d’empiler du style inline.

C’est presque toujours ma solution préférée. Elle est plus maintenable et plus lisible.

On peut aussi retirer la classe plus tard :

jQuery('.foo').removeClass('has-black-border');Code language: JavaScript (javascript)

Ou basculer la classe selon un état :

jQuery('.foo').toggleClass('has-black-border');Code language: JavaScript (javascript)

Deuxième solution : utiliser style.setProperty()

Si vous devez vraiment appliquer une déclaration inline avec !important, la meilleure solution consiste à utiliser JavaScript natif avec style.setProperty().

Cette méthode accepte trois arguments : le nom de la propriété, sa valeur, puis la priorité. Pour définir !important, on passe 'important' en troisième argument.

jQuery('.foo').each(function () {
  this.style.setProperty('border', '4px solid #000', 'important');
});Code language: JavaScript (javascript)

Notez bien la différence : on ne met pas !important dans la valeur.

// Mauvais.
this.style.setProperty('border', '4px solid #000 !important');

// Bon.
this.style.setProperty('border', '4px solid #000', 'important');Code language: JavaScript (javascript)

C’est la méthode la plus précise lorsqu’on veut modifier directement le style inline d’un élément.

Version vanilla JavaScript

Si vous n’avez pas besoin de jQuery, la version moderne en JavaScript pur est très simple :

document.querySelectorAll('.foo').forEach((element) => {
  element.style.setProperty('border', '4px solid #000', 'important');
});Code language: JavaScript (javascript)

Et pour cibler un seul élément :

const element = document.querySelector('.foo');

if (element) {
  element.style.setProperty('border', '4px solid #000', 'important');
}Code language: JavaScript (javascript)

Cette version évite de charger jQuery uniquement pour modifier une classe ou une propriété CSS. Sur un site moderne, c’est souvent le meilleur choix.

Troisième solution : utiliser attr() pour modifier style

Une autre solution consiste à modifier directement l’attribut style avec attr(). On peut concaténer le style existant pour ne pas l’écraser.

jQuery('.foo').attr('style', function (index, style) {
  return (style || '') + '; border: 4px solid #000 !important;';
});Code language: JavaScript (javascript)

Cette méthode fonctionne, mais elle est moins propre. Elle manipule une chaîne complète de CSS inline, ce qui peut produire des doublons ou rendre les styles plus difficiles à maintenir.

Exemple de résultat possible :

<div class="foo" style="color: red; border: 4px solid #000 !important;">
  Contenu
</div>Code language: HTML, XML (xml)

Je la réserverais à un correctif rapide ou à un environnement où l’on ne peut pas accéder facilement à l’objet DOM.

Quatrième solution : utiliser cssText

On peut aussi modifier toute la déclaration inline via cssText. L’idée ressemble à la méthode précédente : on récupère le style existant, puis on ajoute une déclaration CSS à la fin.

jQuery('.foo').each(function () {
  this.style.cssText += '; border: 4px solid #000 !important;';
});Code language: JavaScript (javascript)

Cette version est plus directe que l’appel à attr('style'), mais elle garde le même défaut : on manipule une chaîne CSS complète, avec un risque de doublons et de règles contradictoires.

Par exemple, si l’élément possède déjà une bordure inline, vous pouvez finir avec ceci :

style="border: 1px solid red; border: 4px solid #000 !important;"Code language: JavaScript (javascript)

Le navigateur appliquera la dernière déclaration importante, mais le HTML devient vite peu lisible.

Supprimer ensuite la propriété !important

Si vous avez utilisé setProperty(), vous pouvez supprimer la propriété avec removeProperty() :

jQuery('.foo').each(function () {
  this.style.removeProperty('border');
});Code language: JavaScript (javascript)

En vanilla JavaScript :

document.querySelectorAll('.foo').forEach((element) => {
  element.style.removeProperty('border');
});Code language: JavaScript (javascript)

On peut aussi vérifier si une propriété inline possède une priorité important avec getPropertyPriority(). MDN indique que cette méthode retourne la priorité explicite, par exemple "important", ou une chaîne vide s’il n’y en a pas.

const element = document.querySelector('.foo');

if (element) {
  const priority = element.style.getPropertyPriority('border');

  if (priority === 'important') {
    element.style.removeProperty('border');
  }
}Code language: JavaScript (javascript)

Utiliser une variable CSS plutôt que !important

Une approche plus moderne consiste parfois à définir une variable CSS en JavaScript, puis à laisser le CSS gérer le rendu.

Exemple CSS :

.foo {
  border: var(--sky-border, 1px solid transparent);
}Code language: CSS (css)

Ensuite, en JavaScript :

document.querySelectorAll('.foo').forEach((element) => {
  element.style.setProperty('--sky-border', '4px solid #000');
});Code language: JavaScript (javascript)

Cette méthode ne remplace pas tous les cas d’usage de !important, mais elle permet souvent d’éviter le combat de spécificité. On change une valeur, pas toute une déclaration. C’est plus élégant, donc naturellement moins satisfaisant pour les amateurs de chaos.

Cas WordPress : privilégier une classe et une feuille CSS

Dans WordPress, on rencontre souvent ce problème lorsqu’un plugin injecte des styles trop agressifs, ou lorsqu’un thème charge ses styles après les vôtres.

Si vous contrôlez le code, évitez d’injecter du style inline avec jQuery. Ajoutez une classe, puis chargez une feuille CSS proprement.

Exemple JavaScript :

document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('.foo').forEach((element) => {
    element.classList.add('has-black-border');
  });
});Code language: JavaScript (javascript)

Exemple CSS :

.has-black-border {
  border: 4px solid #000 !important;
}Code language: CSS (css)

Et côté WordPress, chargez vos assets depuis un plugin, un mu-plugin ou un thème enfant :

<?php
/**
 * Enqueue custom frontend assets.
 *
 * @return void
 */
function sky_enqueue_custom_frontend_assets(): void {
	if ( is_admin() ) {
		return;
	}

	wp_enqueue_style(
		'sky-custom-style',
		get_stylesheet_directory_uri() . '/assets/css/custom.css',
		array(),
		'1.0.0'
	);

	wp_enqueue_script(
		'sky-custom-script',
		get_stylesheet_directory_uri() . '/assets/js/custom.js',
		array(),
		'1.0.0',
		true
	);
}
add_action( 'wp_enqueue_scripts', 'sky_enqueue_custom_frontend_assets' );Code language: HTML, XML (xml)

Dans un plugin WordPress moderne, je privilégierais même une classe CSS pilotée par JavaScript vanilla. jQuery reste disponible sur beaucoup de sites WordPress, mais ce n’est plus une raison suffisante pour l’utiliser partout.

Quelle solution choisir ?

Si vous pouvez écrire du CSS, utilisez une classe :

jQuery('.foo').addClass('has-black-border');Code language: JavaScript (javascript)

Si vous devez vraiment modifier le style inline avec !important, utilisez setProperty() :

jQuery('.foo').each(function () {
  this.style.setProperty('border', '4px solid #000', 'important');
});Code language: JavaScript (javascript)

Si vous travaillez sans jQuery, utilisez directement JavaScript moderne :

document.querySelectorAll('.foo').forEach((element) => {
  element.style.setProperty('border', '4px solid #000', 'important');
});Code language: JavaScript (javascript)

Et si vous voulez une solution maintenable, évitez attr('style') et cssText, sauf pour un patch rapide et bien isolé.

Mémo rapide

// Ne fonctionne pas correctement.
jQuery('.foo').css('border', '4px solid #000 !important');

// Solution recommandée : classe CSS.
jQuery('.foo').addClass('has-black-border');

// CSS associé.
.has-black-border {
  border: 4px solid #000 !important;
}

// Solution directe avec jQuery + JavaScript natif.
jQuery('.foo').each(function () {
  this.style.setProperty('border', '4px solid #000', 'important');
});

// Version vanilla JavaScript.
document.querySelectorAll('.foo').forEach((element) => {
  element.style.setProperty('border', '4px solid #000', 'important');
});

// Supprimer la propriété inline.
document.querySelectorAll('.foo').forEach((element) => {
  element.style.removeProperty('border');
});Code language: PHP (php)

Conclusion

jQuery ne permet pas d’ajouter proprement !important avec .css(), car !important n’est pas simplement une partie de la valeur CSS. C’est une priorité de déclaration dans la cascade.

La solution la plus propre consiste à ajouter une classe avec addClass(), puis à définir la règle dans une feuille CSS. Si vous devez vraiment poser une déclaration inline prioritaire, utilisez style.setProperty('propriété', 'valeur', 'important').

En pratique : classe CSS d’abord, setProperty() ensuite, attr('style') et cssText seulement si vous avez une très bonne raison. Ou un vieux widget tiers. Ce qui revient parfois au même niveau de malédiction.