Aller au contenu

#Le bac à sable

Un thème est du code, ce code est modifiable par le client, et il s’exécute sur notre serveur — celui qui rend aussi les sites de tous les autres. Une boucle qui s’emballe dans le thème d’un artisan ne doit pas éteindre la boulangerie d’à côté. Cette page dit dans quelles bornes un thème s’exécute, ce qui arrive quand il les dépasse, et ce qui est simplement hors de portée.

Toutes les valeurs ci-dessous sont celles du code, pas des intentions : src/lib/liquid/moteur.ts pour le rendu, src/lib/liquid/format.ts pour le format.

#Les trois garde-fous

Ils ne sont pas négociables, et ils ne se remplacent pas l’un l’autre.

Aucun accès au disque. LiquidJS lit normalement les fichiers avec fs. Ici, le système de fichiers qu’on lui donne est une carte en mémoire : celle des fichiers du thème, et rien d’autre. render et include ne peuvent donc atteindre ni /etc/passwd, ni les variables d’environnement, ni le thème d’un autre client.

Pas de code arbitraire. Liquid n’a ni eval, ni appel de fonction. Ce n’est pas une restriction qu’on a ajoutée : c’est la raison d’être du langage, et c’est pour cela que Shopify l’a écrit plutôt que d’exposer un moteur de gabarit généraliste.

Des bornes. Temps, mémoire, taille de source, profondeur d’imbrication. Sans elles, une boucle sur dix millions d’entiers immobilise le processus, et le processus sert tout le monde.

#Les chiffres

BorneValeurOù elle s’applique
Temps de rendu3 secondesPar page, et par section
Source analysée512 KoPar appel de rendu, inclusions comprises
Objets créés10⁸Par rendu
Imbrication de sections6 niveauxTag section uniquement
Taille d’un fichier512 KoÀ l’écriture comme à l’import
Nombre de fichiers120Par thème
Attention

Les 3 secondes sont appliquées deux fois, et ce n’est pas une redondance inutile. LiquidJS les fait respecter de l’intérieur, y compris au milieu d’une boucle ; une course contre un minuteur les fait respecter de l’extérieur, au cas où un rendu se bloquerait ailleurs que dans Liquid. La première version du moteur n’avait que la seconde — or une course contre un minuteur ne coupe rien quand le rendu est synchrone : la boucle bloquait le fil d’exécution de Node, donc le minuteur ne s’exécutait jamais.

#Ce que « 512 Ko » veut dire exactement

La taille d’un fichier est comparée à la longueur de la chaîne, pas à son poids en octets — soit 524 288 unités de code UTF-16. Un fichier plein d’accents et d’emoji passera donc la barre un peu plus tard qu’un ls -l ne le laisse croire. C’est sans conséquence pratique : aucun fichier de thème sensé n’approche de cette taille, et la borne existe pour arrêter une archive malveillante, pas pour discipliner une feuille de style.

Le même plafond sert de parseLimit au moteur, mais il y compte la source cumulée d’un rendu : une coquille de 100 Ko qui inclut cinq snippets de 100 Ko dépasse, alors qu’aucun fichier pris isolément ne dépassait.

#Ce qui se passe au dépassement

Rien ne tombe en page blanche. C’est un choix, et il se défend : un thème en cours d’édition contient forcément des erreurs passagères, et rendre une page blanche à chaque virgule mal placée rendrait l’édition impossible.

Une section qui échoue est remplacée par un commentaire HTML — invisible pour le visiteur, lisible par qui édite le thème :

Rendu de la pagehtml
<!-- section prestations : Le rendu du thème a pris trop de temps. -->
<!-- sections trop imbriquées à « entete » -->
<!-- section absente : bandeau-promo -->
Astuce

Ces commentaires sont le premier endroit à regarder quand un bloc de page a disparu sans explication. Ouvre le code source de la page rendue — pas l’inspecteur, qui ne montre pas toujours les commentaires — et cherche <!-- section.

#L’imbrication des sections

Le tag section rend une section depuis la coquille. Six niveaux suffisent largement : au-delà, c’est qu’une section s’appelle elle-même.

layout/theme.liquidliquid
<body>
  {% section 'entete' %}
  {{ content_for_layout }}
  {% section 'pied' %}
</body>

Le compteur de profondeur vit dans une fermeture, hors de portée du thème. Il a vécu un temps dans le contexte Liquid, ce qui suffisait à le neutraliser :

Ce qui ne fonctionne plusliquid
{% assign _profondeur = 0 %}
{% section 'moi-meme' %}

Un compteur qui surveille du code ne doit pas être écrit par ce code. C’est la règle générale, et elle vaut au-delà de ce cas précis.

#Ce qui n’est PAS protégé

Deux points à connaître avant d’écrire, parce qu’ils surprennent.

Liquid n’échappe pas le HTML par défaut. Pas plus chez Shopify. Un thème peut donc écrire une balise script. C’est sans conséquence tant que le thème vient de nous ou du propriétaire du site — il ne peut s’attaquer qu’à lui-même. Le jour où l’on acceptera des thèmes de tiers, il faudra assainir le HTML rendu, et ce sera une décision à prendre avant d’ouvrir cette porte. Pour du contenu saisi par le client, utilise escape, ou lignes qui échappe aussi.

Une variable absente rend du vide. Le moteur tourne avec strictVariables et strictFilters désactivés : une faute de frappe dans un coin de pied de page ne bloque pas la page entière, elle rend une chaîne vide. Pratique en production, traître en développement — c’est la première chose à soupçonner quand un réglage « ne marche pas ».

#Ce qu’un thème ne peut pas contenir

Le dossier détermine les extensions admises. La liste est fermée, et c’est une mesure de sécurité, pas de rangement.

DossierExtensions admises
layout/.liquid
sections/.liquid
snippets/.liquid
assets/.css, .svg
config/.json
templates/.json

Il n’y a pas de .js dans assets/, et l’absence est délibérée. Un thème importé est du contenu qui vient de l’extérieur ; un .js déposé dans assets/ serait du script exécuté dans le navigateur de tous les visiteurs du site, sous son domaine, avec accès à ses cookies. Le JavaScript de thème viendra quand on saura le relire. En attendant, un thème est du HTML et du CSS.

Danger

Un chemin est refusé s’il contient .., s’il commence par une barre oblique, s’il contient une barre oblique inversée, ou s’il n’a pas exactement deux segments (dossier/fichier). C’est la faille classique des archives — celle qui permet d’écrire ailleurs que là où on croit — et elle est vérifiée à l’entrée, pas au moment d’écrire.

Le nom de fichier lui-même doit commencer par une lettre ou un chiffre, et ne contenir ensuite que des lettres, des chiffres, des points, des tirets et des tirets bas.

#Vérifier avant d’envoyer

Le CLI applique les mêmes bornes que le serveur, en local, avant tout envoi. C’est ce qui évite d’apprendre qu’un fichier est trop gros après un aller-retour réseau.

  1. Compter les fichiers et leur taille

    Le CLI refuse de pousser au-delà de 120 fichiers, et signale tout fichier au-dessus de 512 Ko avant de tenter l’écriture.

    node scripts/theme.mjs pousser ↑ modifié assets/theme.css + nouveau sections/temoignages.liquid ✓ 1 nouveau, 1 modifié, 3 inchangés
  2. Lire l’erreur de syntaxe, s’il y en a une

    Une erreur d’analyse Liquid est signalée avec son fichier, sa ligne et sa colonne, et interrompt l’envoi — un thème à moitié poussé est pire qu’un thème non poussé.

    Sortie du CLIbash
     sections/hero.liquid tag "for" not closed, line:14, col:1 [syntaxe]
  3. Regarder le rendu, pas seulement le code

    Les dépassements de bornes ne sont pas des erreurs de syntaxe : ils n’apparaissent qu’au rendu, sous forme de commentaires HTML. Une page qui s’analyse parfaitement peut ne rendre que trois sections sur cinq.

#Les bornes, en un coup d’œil

renderLimitmillisecondesdéfaut : 3000

Temps de rendu maximal, appliqué par LiquidJS lui-même — donc y compris au milieu d’une boucle. Au-delà, le rendu est interrompu et la section concernée est remplacée par un commentaire.

parseLimitoctetsdéfaut : 524288

Source analysée par appel de rendu, inclusions comprises. Le format autorise déjà moins par fichier ; cette borne plafonne la somme.

memoryLimitobjetsdéfaut : 100000000

Nombre d’objets que le rendu peut créer. Une page de site vitrine en fabrique quelques milliers : ce plafond n’arrête qu’une boucle qui s’emballe.

PROFONDEUR_MAXentierdéfaut : 6

Niveaux d’imbrication du tag section. Le compteur est hors de portée du thème, et il est rendu même quand une section échoue.

TAILLE_MAXcaractèresdéfaut : 524288

Longueur maximale du contenu d’un fichier, vérifiée à l’import, à l’écriture par l’API, et par le CLI avant l’envoi.

FICHIERS_MAXentierdéfaut : 120

Nombre de fichiers d’un thème. L’API refuse la création du 121ᵉ avec trop_de_fichiers.