story-theme-mcp 1.7.2 → 1.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/index.js +3 -2
- package/src/knowledge/brancher.js +29 -1
- package/src/knowledge/demos.js +1 -1
- package/src/knowledge/guides.js +2 -2
- package/src/migrate/extract.js +16 -4
- package/src/migrate/source.js +77 -11
- package/src/migrate/v3.js +812 -125
- package/src/server.js +39 -18
- package/src/shops/app-tools.js +1 -1
- package/src/shops/direct.js +11 -3
- package/src/shops/format.js +1 -1
- package/src/shops/tools.js +5 -3
- package/src/theme/source.js +7 -2
- package/src/validate/design.js +22 -2
- package/src/validate/values.js +7 -3
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Votre assistant IA connaît le [Story Thème](https://story-theme.com) par cœur — chaque section, chaque bloc, chaque réglage — et construit des boutiques Shopify propres et professionnelles, sans jamais produire un fichier que Shopify refuserait.
|
|
4
4
|
|
|
5
|
-
Réservé aux titulaires d'une licence Story Thème. Le jeton se génère dans l'espace membre : **Story Thème > MCP
|
|
5
|
+
Réservé aux titulaires d'une licence Story Thème. Le jeton se génère dans l'espace membre : **Story Thème > MCP**.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
Vous : « Crée la boutique Maison Lumen : bougies parfumées coulées à la main à Lyon. »
|
|
@@ -15,7 +15,7 @@ IA : une seule salve de questions (photos, collection, conditions… ou « vas
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
**Le plus simple — l'extension Claude.** Téléchargez `story-theme-mcp.mcpb` depuis votre espace membre (page MCP
|
|
18
|
+
**Le plus simple — l'extension Claude.** Téléchargez `story-theme-mcp.mcpb` depuis votre espace membre (page MCP) et ouvrez le fichier : Claude installe le serveur et vous demande votre jeton dans un formulaire. Rien d'autre à installer, Node compris.
|
|
19
19
|
|
|
20
20
|
Les deux méthodes ci-dessous restent valables si vous préférez configurer à la main. Elles demandent Node 20 ou plus ([nodejs.org](https://nodejs.org)).
|
|
21
21
|
|
|
@@ -82,7 +82,7 @@ Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-t
|
|
|
82
82
|
|
|
83
83
|
| Variable | Défaut | Rôle |
|
|
84
84
|
|---|---|---|
|
|
85
|
-
| `STORY_MCP_TOKEN` | — | Jeton personnel (espace membre > MCP
|
|
85
|
+
| `STORY_MCP_TOKEN` | — | Jeton personnel (espace membre > MCP). Vérifie la licence et donne accès au thème publié. |
|
|
86
86
|
| `STORY_MCP_HOME` | `~/Documents/Story Theme MCP` | Projets, historique, exports, cache du thème. |
|
|
87
87
|
| `STORY_MCP_LANGUAGE` | `fr` | Langue par défaut des projets et des textes. |
|
|
88
88
|
| `STORY_MCP_CANAL` | `stable` | `test` pour la version en test du thème (comptes testeurs). |
|
|
@@ -117,7 +117,7 @@ npm run fonts # recharge la bibliothèque de polices Shopify (shopify.dev)
|
|
|
117
117
|
npm run bundle # fabrique build/story-theme-mcp-<version>.mcpb (extension Claude, un clic)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
Après `npm run bundle`, déposer le `.mcpb` dans le dossier `mcp/` de l'espace membre : la page MCP
|
|
120
|
+
Après `npm run bundle`, déposer le `.mcpb` dans le dossier `mcp/` de l'espace membre : la page MCP sert automatiquement la version la plus récente qu'elle y trouve.
|
|
121
121
|
|
|
122
122
|
Avec `STORY_THEME_DIR` sur le dépôt du thème, le MCP lit le thème en cours d'écriture et n'appelle pas l'espace membre. Les connaissances rédigées à la main vivent dans `src/knowledge/` (descriptions de chaque section et bloc, compositions par métier, directions artistiques, guides) ; tout le reste vient du thème. `npm run check` signale une section ou un bloc public sans description.
|
|
123
123
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "story-theme-mcp",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
4
4
|
"description": "Serveur MCP du Story Thème : votre assistant IA connaît le thème Shopify Story par cœur et construit des boutiques propres et professionnelles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/config.js
CHANGED
|
@@ -11,7 +11,7 @@ export function loadConfig(env = process.env) {
|
|
|
11
11
|
const themeDir = env.STORY_THEME_DIR || '';
|
|
12
12
|
return {
|
|
13
13
|
version: VERSION,
|
|
14
|
-
// Jeton personnel du client (espace membre > MCP
|
|
14
|
+
// Jeton personnel du client (espace membre > MCP). Sans lui, pas de thème à distance :
|
|
15
15
|
// le MCP retombe sur son cache, puis sur un dossier local.
|
|
16
16
|
token: (env.STORY_MCP_TOKEN || '').trim(),
|
|
17
17
|
apiBase: (env.STORY_MCP_API || DEFAULT_API).trim(),
|
package/src/index.js
CHANGED
|
@@ -25,7 +25,7 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
|
25
25
|
'construit des boutiques Shopify prêtes à importer. Il ne s’utilise pas à la main.',
|
|
26
26
|
'',
|
|
27
27
|
'Variables d’environnement :',
|
|
28
|
-
' STORY_MCP_TOKEN jeton personnel (espace membre Story Thème > MCP
|
|
28
|
+
' STORY_MCP_TOKEN jeton personnel (espace membre Story Thème > MCP) — indispensable',
|
|
29
29
|
' STORY_MCP_HOME dossier des projets et exports (défaut : ~/Documents/Story Theme MCP)',
|
|
30
30
|
' STORY_MCP_LANGUAGE langue des textes (défaut : fr)',
|
|
31
31
|
' STORY_MCP_CANAL « test » pour la version en test du thème (comptes testeurs)',
|
|
@@ -57,6 +57,7 @@ try {
|
|
|
57
57
|
// l'interface de Claude.
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// L'appli Story Theme Apps n'est proposée que si l'espace membre la dit disponible pour ce compte.
|
|
61
|
+
const server = createServer({ ...config, source, appli: source.appli });
|
|
61
62
|
await server.connect(new StdioServerTransport());
|
|
62
63
|
journal(`prêt (projets : ${config.homeDir})`);
|
|
@@ -18,7 +18,7 @@ const ETAPES = [
|
|
|
18
18
|
'Dans l’administration Shopify, installez l’application gratuite Theme Access (éditée par Shopify) : https://apps.shopify.com/theme-access',
|
|
19
19
|
'Dans Theme Access : « Créer un mot de passe », avec votre propre adresse e-mail.',
|
|
20
20
|
'Ouvrez l’e-mail reçu et affichez le mot de passe : il commence par shptka_ et ne s’affiche qu’une fois.',
|
|
21
|
-
`Collez-le dans votre espace membre Story Thème, page MCP
|
|
21
|
+
`Collez-le dans votre espace membre Story Thème, page MCP > « Brancher une boutique » (${PAGE_BRANCHEMENT}). Jamais dans la conversation.`,
|
|
22
22
|
];
|
|
23
23
|
|
|
24
24
|
/** La marche à suivre complète, prête à recopier au marchand. */
|
|
@@ -33,3 +33,31 @@ export const TUTO_BRANCHEMENT = [
|
|
|
33
33
|
|
|
34
34
|
/** Une ligne pour les réponses où la boutique n'est pas (ou peut-être pas) branchée. */
|
|
35
35
|
export const INVITATION_BRANCHEMENT = `Boutique pas encore branchée ? Proposez Theme Access au marchand (deux minutes, une seule fois) : list_shops donne la marche à suivre${VIDEO_BRANCHEMENT ? ` et la vidéo (${VIDEO_BRANCHEMENT})` : ''}. Sans lui, le zip reste la solution de repli, à importer à chaque version.`;
|
|
36
|
+
|
|
37
|
+
/* --------------------------------------------- le connecteur Shopify officiel */
|
|
38
|
+
|
|
39
|
+
// Theme Access n'ouvre que les thèmes. Le connecteur Shopify officiel, édité par Shopify pour
|
|
40
|
+
// ChatGPT et Claude (mai 2026), ouvre le reste : produits, prix, stocks, collections, réductions,
|
|
41
|
+
// commandes. Avec lui, l'assistant remplit les pages des vrais produits et crée les réductions des
|
|
42
|
+
// offres au lieu de laisser une liste au marchand : le MCP le propose donc, surtout au début.
|
|
43
|
+
|
|
44
|
+
/** La fiche du connecteur dans l'annuaire de Claude (vérifiée le 20/10/2026). */
|
|
45
|
+
export const LIEN_CONNECTEUR_SHOPIFY_CLAUDE = 'https://claude.com/connectors/shopify';
|
|
46
|
+
|
|
47
|
+
/** Le conseil, tel que l'assistant le lit dans un résultat d'outil. */
|
|
48
|
+
// Le MCP ne voit pas les autres connecteurs de l'assistant ; l'assistant, lui, voit ses outils :
|
|
49
|
+
// c'est à lui de vérifier, et de se taire si le connecteur est déjà là.
|
|
50
|
+
export const CONSEIL_CONNECTEUR_SHOPIFY = `Connecteur Shopify officiel — seulement s'il n'est pas déjà en place. Vérifiez d'abord vos outils : si vous avez déjà ceux du connecteur Shopify (produits, commandes, réductions…), n'en parlez pas. S'il est installé mais pas actif dans cette conversation, dites simplement de l'activer (ChatGPT : @Shopify). Sinon, proposez-le au marchand en une phrase : édité par Shopify, il vous ouvre, en plus du thème, ses produits, prix, stocks, collections, réductions et commandes (pages remplies de ses vrais produits, réductions des offres créées pour lui, photos déposées dans Fichiers). Où l'ajouter : Claude, ${LIEN_CONNECTEUR_SHOPIFY_CLAUDE} ; ChatGPT, Applis › « Shopify » ; ailleurs, l'annuaire de connecteurs de son IA. S'il l'a refusé ou dit l'avoir déjà, n'y revenez plus.`;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Surtout au début, pas tout le temps : un compteur par conversation (une session MCP) qui
|
|
54
|
+
* rend le conseil aux deux premiers résultats, puis à un sur cinq. Chaque appel compte.
|
|
55
|
+
* @returns {() => string} le conseil, ou une chaîne vide
|
|
56
|
+
*/
|
|
57
|
+
export function rappelConnecteurShopify() {
|
|
58
|
+
let appels = 0;
|
|
59
|
+
return () => {
|
|
60
|
+
appels += 1;
|
|
61
|
+
return appels <= 2 || appels % 5 === 0 ? CONSEIL_CONNECTEUR_SHOPIFY : '';
|
|
62
|
+
};
|
|
63
|
+
}
|
package/src/knowledge/demos.js
CHANGED
|
@@ -22,7 +22,7 @@ export function creerDemos(config) {
|
|
|
22
22
|
const appel = async (chemin) => {
|
|
23
23
|
if (!config.token) {
|
|
24
24
|
throw new Error(
|
|
25
|
-
'Les boutiques démo demandent un jeton : ajoutez STORY_MCP_TOKEN (espace membre Story Thème > MCP
|
|
25
|
+
'Les boutiques démo demandent un jeton : ajoutez STORY_MCP_TOKEN (espace membre Story Thème > MCP).',
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
const reponse = await fetch(`${config.apiBase.replace(/\/+$/, '')}${chemin}`, {
|
package/src/knowledge/guides.js
CHANGED
|
@@ -217,8 +217,8 @@ Formats conseillés :
|
|
|
217
217
|
limites: {
|
|
218
218
|
title: 'Ce que le MCP (V1) ne fait pas',
|
|
219
219
|
body: `- Il ne publie jamais un thème : il écrit dans un thème NON publié (push_to_shop) ou fabrique un zip à importer (export_theme) ; le marchand vérifie et publie lui-même.
|
|
220
|
-
-
|
|
221
|
-
- Il ne crée pas de lui-même produits, collections, menus, pages, réductions ni images :
|
|
220
|
+
- Il n'ouvre que les THÈMES d'une boutique branchée par Theme Access (list_shops le dit) : ni ses produits, ni ses commandes, ni ses clients.
|
|
221
|
+
- Il ne crée pas de lui-même produits, collections, menus, pages, réductions ni images : le connecteur Shopify officiel du marchand le permet ; sinon la check-list les liste.
|
|
222
222
|
- Il ne modifie jamais le code du thème (Liquid, CSS, JS) : seulement les modèles, les groupes de sections et les réglages, comme l'éditeur de thème.
|
|
223
223
|
- Il ne garantit pas le rendu visuel : prévisualisez toujours dans l'éditeur Shopify avant de publier.`,
|
|
224
224
|
},
|
package/src/migrate/extract.js
CHANGED
|
@@ -53,7 +53,9 @@ const ROLES = [
|
|
|
53
53
|
['title', /^(heading|title|image_heading|review_title|tab_label|headline|heading_text|question)$/],
|
|
54
54
|
['eyebrow', /^(subheading|image_subheading|subtitle|label|tagline|eyebrow|caption|announcment)$/],
|
|
55
55
|
['text', /^(description|text|row_content|content|text_footer|caption_text|form_text|text_above|message|body|paragraph|info|tooltip|answer|text_before|text_after|helper_text)$/],
|
|
56
|
-
|
|
56
|
+
// L'image qui remplace un pictogramme (3.x : image_custom, icon_N_custom_icon) reste un pictogramme.
|
|
57
|
+
['icon_image', /^(image_custom|custom_icon|icon_custom|icon_image)$/],
|
|
58
|
+
['image', /^(image|avatar|cover_image|background_image|image_before|image_after|logo|photo)$/],
|
|
57
59
|
['image_mobile', /^(image_mobile|background_image_mobile)$/],
|
|
58
60
|
['video', /^(video|video_url|video_link|video_mobile|video_link_mobile)$/],
|
|
59
61
|
];
|
|
@@ -92,9 +94,14 @@ const texteVide = (valeur) => !plainText(isHtml(valeur) ? valeur.html : valeur).
|
|
|
92
94
|
// (« Écrire un avis », « Voir plus », jours/heures du compte à rebours…), que la 4.0 traduit elle-même.
|
|
93
95
|
const INTERFACE = /^(section_id|custom_class|anchor|write_review_\w+|success_message|plus|less|button_show_all_text|placeholder|error_message|default_value|days|hours|mins|secs|free_label|reviews_based_on_text|reviews_suffix_text|reviews_count|cart_\w+|sale_badge|share_label|size_guide_trigger|product_trigger|trigger|final_message|track)$/;
|
|
94
96
|
|
|
97
|
+
// Le pictogramme choisi dans la bibliothèque du thème (« truck », « leaf ») : la 4.0 a la même.
|
|
98
|
+
const PICTO = /(^|_)icon(_type)?$/;
|
|
99
|
+
|
|
95
100
|
/** Un réglage lu : sa clé, son rôle (titre, texte, lien…), son genre et sa valeur. */
|
|
96
101
|
function lireReglage(cle, valeur, type) {
|
|
97
102
|
if (INTERFACE.test(cle)) return null;
|
|
103
|
+
// « none » compte aussi : la colonne n'avait pas de pictogramme, la 4.0 n'en devine pas un.
|
|
104
|
+
if (PICTO.test(cle) && typeof valeur === 'string' && /^[a-z_]+$/.test(valeur)) return { cle, genre: 'icon', valeur };
|
|
98
105
|
const genre = type ? SCHEMA_GENRE[type] ?? null : genreDeviné(cle, valeur);
|
|
99
106
|
if (!genre || valeur === '' || valeur == null) return null;
|
|
100
107
|
if (genre === 'html' || genre === 'text') {
|
|
@@ -119,11 +126,13 @@ function ranger(contenu, reglages) {
|
|
|
119
126
|
const { genre, valeur } = reglage;
|
|
120
127
|
|
|
121
128
|
if (genre === 'palette') contenu.palette ??= valeur;
|
|
129
|
+
else if (genre === 'icon') contenu.icon ??= valeur;
|
|
122
130
|
else if (genre === 'code') contenu.code.push(valeur);
|
|
123
131
|
else if (['product', 'collection', 'blog', 'page', 'menu', 'article'].includes(genre)) contenu[genre] ??= valeur;
|
|
124
132
|
else if (genre === 'products' || genre === 'collections') contenu[genre] = [...(contenu[genre] || []), ...(Array.isArray(valeur) ? valeur : [valeur])];
|
|
125
133
|
else if (genre === 'image') {
|
|
126
|
-
if (role === '
|
|
134
|
+
if (role === 'icon_image') contenu.icon_image ??= valeur;
|
|
135
|
+
else if (role === 'image_mobile') contenu.image_mobile ??= valeur;
|
|
127
136
|
else if (/after/.test(base)) contenu.image_after ??= valeur;
|
|
128
137
|
else if (/before/.test(base)) contenu.image_before ??= valeur;
|
|
129
138
|
else if (!contenu.image) contenu.image = valeur;
|
|
@@ -191,7 +200,7 @@ function element(reglages, genre = null) {
|
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
const utile = (item) =>
|
|
194
|
-
item.title !== undefined || item.text !== undefined || item.name !== undefined || item.image || item.video || item.product || item.collection || item.buttons.length || item.eyebrow !== undefined;
|
|
203
|
+
item.title !== undefined || item.text !== undefined || item.name !== undefined || item.image || item.icon_image || item.video || item.product || item.collection || item.buttons.length || item.eyebrow !== undefined;
|
|
195
204
|
|
|
196
205
|
/**
|
|
197
206
|
* Le contenu d'une section 3.x.
|
|
@@ -244,13 +253,16 @@ export function extraireSection(section, schema, { elements: typesElements = nul
|
|
|
244
253
|
}
|
|
245
254
|
if (lu.elements.length && !lu.simples.some((r) => ['text', 'html'].includes(r.genre) && /^(heading|title|name|description|text)$/.test(r.cle))) {
|
|
246
255
|
// Un bloc qui porte plusieurs éléments numérotés (icônes, questions, avis).
|
|
247
|
-
contenu.items.push(...lu.elements.map((el) => ({ ...el, kind: bloc.type })));
|
|
256
|
+
contenu.items.push(...lu.elements.map((el) => ({ ...el, kind: bloc.type, bloc: id })));
|
|
248
257
|
continue;
|
|
249
258
|
}
|
|
250
259
|
const item = element(lu.simples, bloc.type);
|
|
260
|
+
item.bloc = id;
|
|
251
261
|
if (lu.elements.length) item.sous = lu.elements;
|
|
252
262
|
if (bloc.blocks) item.enfants = bloc;
|
|
253
263
|
if (utile(item) || item.enfants) contenu.items.push(item);
|
|
264
|
+
// Un bloc qui ne porte que du code (Liquid, HTML) : le code de la section, dans l'ordre.
|
|
265
|
+
else if (item.code.length) contenu.code.push(...item.code);
|
|
254
266
|
}
|
|
255
267
|
return contenu;
|
|
256
268
|
}
|
package/src/migrate/source.js
CHANGED
|
@@ -15,26 +15,80 @@
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { unzipSync, strFromU8 } from 'fflate';
|
|
18
|
-
import {
|
|
18
|
+
import { stripJsonComment } from '../theme/loader.js';
|
|
19
19
|
import { fileKind } from '../validate/template.js';
|
|
20
20
|
import { devinerLangue, textesDesModeles } from '../validate/design.js';
|
|
21
21
|
|
|
22
22
|
const SCHEMA_RE = /\{%-?\s*schema\s*-?%\}([\s\S]*?)\{%-?\s*endschema\s*-?%\}/;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Le JSON tel que Shopify l'accepte : commentaires et virgule finale permis. Le thème 3.x d'origine
|
|
26
|
+
* en a (schéma de main-product, cart-drawer…), et un settings_schema.json retouché à la main aussi :
|
|
27
|
+
* un JSON.parse strict perdait la version du thème et le type de chaque réglage.
|
|
28
|
+
*/
|
|
29
|
+
export function parseJsonSouple(texte) {
|
|
30
|
+
const source = stripJsonComment(String(texte));
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(source);
|
|
33
|
+
} catch (erreur) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(sansVirguleFinale(sansCommentaires(source)));
|
|
36
|
+
} catch {
|
|
37
|
+
throw erreur;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parcourt le texte hors des chaînes : `quoi(c, i)` rend ce qu'il faut écrire et où reprendre.
|
|
43
|
+
function horsChaines(source, quoi) {
|
|
44
|
+
let sortie = '';
|
|
45
|
+
for (let i = 0; i < source.length; i++) {
|
|
46
|
+
if (source[i] === '"') {
|
|
47
|
+
let j = i + 1;
|
|
48
|
+
while (j < source.length && source[j] !== '"') j += source[j] === '\\' ? 2 : 1;
|
|
49
|
+
sortie += source.slice(i, j + 1);
|
|
50
|
+
i = j;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const [texte, suite] = quoi(source, i);
|
|
54
|
+
sortie += texte;
|
|
55
|
+
i = suite;
|
|
56
|
+
}
|
|
57
|
+
return sortie;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sansCommentaires = (source) =>
|
|
61
|
+
horsChaines(source, (t, i) => {
|
|
62
|
+
if (t[i] === '/' && t[i + 1] === '/') {
|
|
63
|
+
const fin = t.indexOf('\n', i);
|
|
64
|
+
return ['\n', fin === -1 ? t.length : fin];
|
|
65
|
+
}
|
|
66
|
+
if (t[i] === '/' && t[i + 1] === '*') {
|
|
67
|
+
const fin = t.indexOf('*/', i + 2);
|
|
68
|
+
return [' ', fin === -1 ? t.length : fin + 1];
|
|
69
|
+
}
|
|
70
|
+
return [t[i], i];
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const sansVirguleFinale = (source) => horsChaines(source, (t, i) => (t[i] === ',' && /^\s*[}\]]/.test(t.slice(i + 1, i + 200)) ? ['', i] : [t[i], i]));
|
|
74
|
+
|
|
24
75
|
function lireSchema(liquid) {
|
|
25
76
|
const match = SCHEMA_RE.exec(liquid);
|
|
26
77
|
if (!match) return null;
|
|
27
78
|
try {
|
|
28
|
-
return
|
|
79
|
+
return parseJsonSouple(match[1]);
|
|
29
80
|
} catch {
|
|
30
81
|
return null;
|
|
31
82
|
}
|
|
32
83
|
}
|
|
33
84
|
|
|
85
|
+
// La langue par défaut du thème 3.x : locales/da.default.json → « da ». Seul le nom compte.
|
|
86
|
+
const LOCALE_DEFAUT = /^locales\/([a-z]{2}(?:-[A-Za-z]{2,4})?)\.default\.json$/;
|
|
87
|
+
|
|
34
88
|
/** Les fichiers utiles d'un thème : chemin relatif → texte. */
|
|
35
89
|
function lireFichiers(source) {
|
|
36
90
|
const fichiers = new Map();
|
|
37
|
-
const garder = (rel) => fileKind(rel) || rel === 'config/settings_schema.json' || /^(sections|blocks)\/[^/]+\.liquid$/.test(rel);
|
|
91
|
+
const garder = (rel) => fileKind(rel) || rel === 'config/settings_schema.json' || /^(sections|blocks)\/[^/]+\.liquid$/.test(rel) || LOCALE_DEFAUT.test(rel);
|
|
38
92
|
|
|
39
93
|
if (fs.statSync(source).isDirectory()) {
|
|
40
94
|
const parcourir = (dossier) => {
|
|
@@ -46,7 +100,7 @@ function lireFichiers(source) {
|
|
|
46
100
|
else if (garder(rel)) fichiers.set(rel, fs.readFileSync(path.join(source, rel), 'utf8'));
|
|
47
101
|
}
|
|
48
102
|
};
|
|
49
|
-
for (const dossier of ['templates', 'sections', 'blocks', 'config']) parcourir(dossier);
|
|
103
|
+
for (const dossier of ['templates', 'sections', 'blocks', 'config', 'locales']) parcourir(dossier);
|
|
50
104
|
return fichiers;
|
|
51
105
|
}
|
|
52
106
|
|
|
@@ -81,7 +135,14 @@ export function lireThemeV3DepuisFichiers(fichiers, origine = 'boutique') {
|
|
|
81
135
|
const schemas = { sections: new Map(), blocks: new Map() };
|
|
82
136
|
let version = null;
|
|
83
137
|
let nom = null;
|
|
138
|
+
let langueParDefaut = null;
|
|
139
|
+
let reglagesSchema = [];
|
|
84
140
|
for (const [rel, texte] of fichiers) {
|
|
141
|
+
const locale = LOCALE_DEFAUT.exec(rel);
|
|
142
|
+
if (locale) {
|
|
143
|
+
langueParDefaut = locale[1];
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
85
146
|
const liquid = /^(sections|blocks)\/([^/]+)\.liquid$/.exec(rel);
|
|
86
147
|
if (liquid) {
|
|
87
148
|
const schema = lireSchema(texte);
|
|
@@ -90,28 +151,33 @@ export function lireThemeV3DepuisFichiers(fichiers, origine = 'boutique') {
|
|
|
90
151
|
}
|
|
91
152
|
if (rel === 'config/settings_schema.json') {
|
|
92
153
|
try {
|
|
93
|
-
const
|
|
154
|
+
const groupes = parseJsonSouple(texte);
|
|
155
|
+
const info = groupes.find((groupe) => groupe.name === 'theme_info');
|
|
94
156
|
version = info?.theme_version || null;
|
|
95
157
|
nom = info?.theme_name || null;
|
|
158
|
+
reglagesSchema = groupes.flatMap((groupe) => groupe.settings || []).filter((reglage) => reglage?.id);
|
|
96
159
|
} catch {
|
|
97
160
|
/* version inconnue */
|
|
98
161
|
}
|
|
99
162
|
continue;
|
|
100
163
|
}
|
|
101
164
|
try {
|
|
102
|
-
fichiersJson.set(rel,
|
|
165
|
+
fichiersJson.set(rel, parseJsonSouple(texte));
|
|
103
166
|
} catch (error) {
|
|
104
167
|
erreurs.push(`${rel} : JSON illisible (${error.message})`);
|
|
105
168
|
}
|
|
106
169
|
}
|
|
107
170
|
|
|
108
|
-
// La langue de la boutique se lit dans SES textes : le fichier de traductions par défaut du
|
|
109
|
-
// thème (en.default.json) est l'anglais pour toutes les boutiques Story Thème.
|
|
110
|
-
const langue = devinerLangue(textesDesModeles([...fichiersJson].filter(([rel]) => rel.startsWith('templates/') || rel.startsWith('sections/')).map(([, data]) => data)));
|
|
111
|
-
|
|
112
171
|
const reglages = fichiersJson.get('config/settings_data.json');
|
|
113
172
|
const courant = typeof reglages?.current === 'string' ? reglages.presets?.[reglages.current] : reglages?.current;
|
|
114
|
-
|
|
173
|
+
|
|
174
|
+
// La langue de la boutique se lit dans SES textes (modèles, groupes, et sections fixes comme le
|
|
175
|
+
// panier). Le fichier de traductions par défaut du thème (en.default.json) est l'anglais pour
|
|
176
|
+
// toutes les boutiques Story Thème : il ne départage que s'il a été changé (da.default.json…).
|
|
177
|
+
const modeles = [...fichiersJson].filter(([rel]) => rel.startsWith('templates/') || rel.startsWith('sections/')).map(([, data]) => data);
|
|
178
|
+
const textes = textesDesModeles([...modeles, { sections: courant?.sections || {} }]);
|
|
179
|
+
const langue = devinerLangue(textes) || (langueParDefaut && langueParDefaut !== 'en' ? langueParDefaut : null);
|
|
180
|
+
return { origine, version, nom, langue, langueParDefaut, fichiers: fichiersJson, schemas, reglages: courant || {}, reglagesSchema, erreurs };
|
|
115
181
|
}
|
|
116
182
|
|
|
117
183
|
/** La version majeure d'un numéro « 3.7.0 », ou null. */
|