uvako-cli 0.5.0 → 0.6.0

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 CHANGED
@@ -75,6 +75,11 @@ uvako app dev --store <geliştirme mağazası>
75
75
  `http://localhost:3000` yapar, o mağazanın webhook'larını bilgisayarınızdaki alıcıya aktarır (imzasıyla) ve
76
76
  kurulum bağlantısını yazar. Tünel gerekmez.
77
77
 
78
+ Örnek uygulama panele gömülü açılır (`"embedded": true`, `@uvako/app-bridge`, bir dakikalık oturum
79
+ belirteci), `blocks/` klasöründe bir vitrin bloğu taşır ve ücretlendirme API'siyle aylık test ücreti
80
+ oluşturur. `deploy` ve `dev` `blocks/<anahtar>/block.{json,html,css}` dosyalarını yapılandırmayla gönderir;
81
+ blokta kod çalışmaz, sunucu tema paketleriyle aynı kurallarla denetler.
82
+
78
83
  | Komut | Ne yapar |
79
84
  |---|---|
80
85
  | `uvako login` | Partner hesabıyla cihaz girişi (`--no-browser`: tarayıcıyı açmaz). |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uvako-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Uvako tema ve uygulama geliştirme aracı. Tema başlatır, denetler ve canlı önizletir; herkese açık uygulama başlatır, yerelde çalıştırıp webhook aktarır ve dağıtır. https://uvako.com",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -49,6 +49,11 @@
49
49
  "node": ">=22"
50
50
  },
51
51
  "scripts": {
52
+ "rules": "node scripts/generate-theme-rules.js",
53
+ "pretest": "node scripts/generate-theme-rules.js --check",
52
54
  "test": "node --test"
55
+ },
56
+ "dependencies": {
57
+ "sax": "1.6.1"
53
58
  }
54
59
  }
package/src/api.js CHANGED
@@ -3,21 +3,22 @@ import { t } from "./i18n.js";
3
3
  // gönderilir: yönlendirmeler izlenmez, böylece anahtar başka bir adrese taşınmaz.
4
4
 
5
5
  export class ApiError extends Error {
6
- constructor(message, { status = 0, code = "", problems = [] } = {}) {
6
+ constructor(message, { status = 0, code = "", problems = [], retryAfter = 0 } = {}) {
7
7
  super(message);
8
8
  this.status = status;
9
9
  this.code = code;
10
10
  this.problems = problems;
11
+ this.retryAfter = retryAfter;
11
12
  }
12
13
  }
13
14
 
14
- export async function call({ origin, token }, method, route, { body, raw = false, timeout = 120000 } = {}) {
15
+ export async function call({ origin, token }, method, route, { body, raw = false, timeout = 120000, headers = {}, contentType = "application/zip" } = {}) {
15
16
  let response;
16
17
  try {
17
18
  response = await fetch(`${origin}/api/theme-cli${route}`, {
18
19
  method,
19
20
  redirect: "error",
20
- headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "User-Agent": "uvako-cli", ...(body ? { "Content-Type": "application/zip" } : {}) },
21
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "User-Agent": "uvako-cli", ...(body ? { "Content-Type": contentType } : {}), ...headers },
21
22
  body,
22
23
  signal: AbortSignal.timeout(timeout),
23
24
  });
@@ -33,7 +34,9 @@ export async function call({ origin, token }, method, route, { body, raw = false
33
34
  }
34
35
  const problem = payload?.error ?? {};
35
36
  const fallback = response.status === 404 ? t("Bu adreste Uvako tema uç noktası yok. Mağaza adresini kontrol edin.") : t("Sunucu {0} yanıtı verdi.", response.status);
36
- throw new ApiError(problem.message || fallback, { status: response.status, code: problem.code || "", problems: Array.isArray(problem.problems) ? problem.problems : [] });
37
+ const retry = response.headers.get("Retry-After") ?? "";
38
+ const retryAfter = /^\d+$/.test(retry) ? Number(retry) : Math.max(0, Math.ceil((Date.parse(retry) - Date.now()) / 1000)) || 0;
39
+ throw new ApiError(problem.message || fallback, { status: response.status, code: problem.code || "", problems: Array.isArray(problem.problems) ? problem.problems : [], retryAfter });
37
40
  }
38
41
 
39
42
  /** Herkese açık uç nokta: temel temanın başlangıç ayarları (giriş gerektirmez). */
package/src/check.js CHANGED
@@ -2,6 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { walk } from "./files.js";
4
4
  import { t } from "./i18n.js";
5
+ import { checkSVG } from "./svg.js";
6
+ import { rules } from "./rules.generated.js";
7
+ import { checkPageTemplate, checkSectionSchema, checkSources, platformSchemas, sectionFile, templateTexts } from "./sections.js";
5
8
 
6
9
  // Tema denetimi. Mağazaya yüklenen paketi platform aynı kurallarla yeniden denetler; buradaki
7
10
  // denetim geliştiriciye hatayı yüklemeden önce göstermek içindir, güvenlik kararı sunucudadır.
@@ -11,7 +14,8 @@ import { t } from "./i18n.js";
11
14
  // üstüne ayar, bölüm sırası, SCSS ve görsel getirir.
12
15
  // depo — platform deposundaki katalog teması (themes/<anahtar>).
13
16
 
14
- export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600, styleBytes: 512 * 1024, archiveBytes: 40 * 1024 * 1024, localeBytes: 256 * 1024, localeEntries: 2000 };
17
+ // Sınırlar sunucuyla ortaktır (backend/internal/theme/rules/rules.json).
18
+ export const limits = { fileBytes: rules.limits.fileBytes, totalBytes: rules.limits.totalBytes, files: rules.limits.files, styleBytes: rules.limits.styleBytes, archiveBytes: rules.limits.archiveBytes, localeBytes: rules.limits.localeBytes, localeEntries: rules.limits.localeEntries };
15
19
 
16
20
  // Temanın kendi metinlerinin çevirisi: locales/<dil>.json, {"temadaki metin": "çevirisi"}.
17
21
  // Vitrin ve panel bu sözlükle temanın varsayılan metinlerini ziyaretçinin diline çevirir.
@@ -135,6 +139,10 @@ function scanText(rel, text, report) {
135
139
  for (const { pattern, text: what } of scriptPatterns) {
136
140
  if (pattern.test(text)) report.errors.push(t("{0}: {1} içeriyor. Tema içinde betik çalıştırılamaz.", rel, what));
137
141
  }
142
+ scanSecrets(rel, text, report);
143
+ }
144
+
145
+ function scanSecrets(rel, text, report) {
138
146
  for (const pattern of secretPatterns) {
139
147
  if (pattern.test(text)) report.errors.push(t("{0}: gizli anahtar ya da parola gibi görünen bir değer içeriyor.", rel));
140
148
  }
@@ -260,6 +268,42 @@ function checkImages(data, assets, report) {
260
268
  }
261
269
  }
262
270
 
271
+ // ── bölüm motoru ("engine": "sections") ───────────────────────────────────────────────────────
272
+
273
+ function checkSectionsEngine({ dir, pages, sectionSchemas, sources, index, legacy, locales, report }) {
274
+ for (const file of walk(path.join(dir, "styles")).files) {
275
+ const source = readFileSync(file.full, "utf8");
276
+ if (/fonts\.googleapis\.com|fonts\.gstatic\.com|fonts\.bunny\.net/.test(source)) report.errors.push(t("styles/{0}: yazı tipleri assets/ içinde pakete gömülmeli.", file.rel));
277
+ }
278
+ checkSources(sources, report);
279
+ const schemas = new Map(platformSchemas);
280
+ for (const [handle, schema] of Object.entries(sectionSchemas)) {
281
+ const rel = `sections/${handle}.schema.json`;
282
+ if (!/^[a-z][a-z0-9_-]{0,63}$/.test(handle)) {
283
+ report.errors.push(t("{0}: geçersiz bölüm adı.", rel));
284
+ continue;
285
+ }
286
+ checkSectionSchema(rel, schema, report);
287
+ const kind = platformSchemas.has(handle) ? handle : `pkg:${handle}`;
288
+ schemas.set(kind, schema);
289
+ if (kind.startsWith("pkg:") && !(`sections/${handle}.html.tmpl` in sources)) report.errors.push(t("sections/{0}.schema.json: bölümün şablonu (sections/{0}.html.tmpl) yok.", handle));
290
+ if (schema?.fallback && !platformSchemas.has(schema.fallback) && legacy && !legacy.has(schema.fallback)) report.errors.push(t("sections/{0}.schema.json: \"fallback\" platformda ya da temel temada olan bir bölüm türü olmalı.", handle));
291
+ }
292
+ for (const rel of Object.keys(sources)) {
293
+ const handle = rel.startsWith("sections/") ? rel.slice("sections/".length, -".html.tmpl".length) : "";
294
+ if (handle && !platformSchemas.has(handle) && !schemas.has(`pkg:${handle}`)) schemas.set(`pkg:${handle}`, { name: handle, settings: [] });
295
+ }
296
+ const all = { ...pages, ...(index ? { index } : {}) };
297
+ for (const [name, tpl] of Object.entries(all)) checkPageTemplate(`templates/${name}.json`, tpl, { schemas, legacy }, report);
298
+ // Şablonlardaki {{ t "…" }} metinleri: çevirisi olmayan metin yazıldığı gibi gösterilir.
299
+ const texts = templateTexts(sources);
300
+ for (const [locale, dict] of Object.entries(locales)) {
301
+ if (!dict) continue;
302
+ const missing = [...texts].filter((text) => !(text in dict));
303
+ if (missing.length) report.warnings.push(t("locales/{0}.json: şablonlardaki {1} metnin çevirisi yok (ör. \"{2}\").", locale, missing.length, missing[0].slice(0, 60)));
304
+ }
305
+ }
306
+
263
307
  // ── paket ──────────────────────────────────────────────────────────────────────────────────────
264
308
 
265
309
  function checkPackage(dir, meta, report) {
@@ -276,6 +320,10 @@ function checkPackage(dir, meta, report) {
276
320
  let data = null;
277
321
  let index = null;
278
322
  const locales = {};
323
+ // "engine": "sections" dosyaları: sayfa şablonları, bölüm şemaları ve şablon kaynakları.
324
+ const pages = {};
325
+ const sectionSchemas = {};
326
+ const sources = {};
279
327
  for (const file of files) {
280
328
  const rel = file.rel;
281
329
  if (localOnly(rel)) continue;
@@ -314,9 +362,30 @@ function checkPackage(dir, meta, report) {
314
362
  }
315
363
  if (file.size > limits.fileBytes) report.errors.push(t("{0}: {1} MB; tek dosya sınırı {2} MB.", rel, (file.size / 1024 / 1024).toFixed(1), limits.fileBytes / 1024 / 1024));
316
364
  else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(t("{0}: {1} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.", rel, (file.size / 1024 / 1024).toFixed(1)));
317
- if (ext === ".svg" && file.size <= limits.fileBytes) scanText(rel, text(), report);
365
+ if (ext === ".svg" && file.size <= limits.fileBytes) {
366
+ const body = readFileSync(file.full);
367
+ const problem = checkSVG(body);
368
+ if (problem) report.errors.push(`${rel}: ${problem}`);
369
+ scanSecrets(rel, body.toString("utf8"), report);
370
+ }
371
+ } else if (sectionFile(rel)) {
372
+ const kind = sectionFile(rel);
373
+ if (kind === "source") {
374
+ sources[rel] = text();
375
+ continue;
376
+ }
377
+ if (file.size > rules.limits.jsonBytes) {
378
+ report.errors.push(t("{0}: 1 MB sınırını aşıyor.", rel));
379
+ continue;
380
+ }
381
+ const body = text();
382
+ scanText(rel, body, report);
383
+ let parsed = null;
384
+ try { parsed = JSON.parse(body.replace(/^\uFEFF/, "")); } catch (error) { report.errors.push(t("{0} geçerli JSON değil: {1}", rel, error.message)); continue; }
385
+ if (kind === "template") pages[parts[1].slice(0, -5)] = parsed;
386
+ else sectionSchemas[parts[1].slice(0, -".schema.json".length)] = parsed;
318
387
  } else {
319
- report.errors.push(t("{0}: bu dosya pakette bulunamaz. İzin verilenler: theme.json, config/settings_schema.json, config/settings_data.json, templates/index.json, locales/tr.json, locales/en.json, locales/de.json, styles/*.scss, assets/**.", rel));
388
+ report.errors.push(t("{0}: bu dosya pakette bulunamaz. İzin verilenler: {1}.", rel, rules.paths.join(", ")));
320
389
  }
321
390
  }
322
391
  if (totals.style > limits.styleBytes) report.errors.push(t("Stil dosyaları toplam {0} KB sınırını aşıyor.", limits.styleBytes / 1024));
@@ -332,7 +401,12 @@ function checkPackage(dir, meta, report) {
332
401
  index = readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json", false);
333
402
  // Bölüm türleri temel temadan gelir; `theme init` bunları reference/sections.json dosyasına yazar.
334
403
  const reference = existsSync(path.join(dir, "reference", "sections.json")) ? readJson(path.join(dir, "reference", "sections.json"), { errors: [] }, "") : null;
335
- checkTemplate(index, reference?.sections ? new Set(Object.keys(reference.sections)) : null, report, t("{0} temasında", base));
404
+ const legacy = reference?.sections ? new Set(Object.keys(reference.sections)) : null;
405
+ const engine = String(meta?.engine ?? "").trim();
406
+ if (meta && !rules.engines.includes(engine)) report.errors.push(t("theme.json: bilinmeyen \"engine\" değeri."));
407
+ if (engine !== "sections" && (Object.keys(pages).length || Object.keys(sectionSchemas).length || Object.keys(sources).length)) report.errors.push(t("Yeni sayfa ve bölüm dosyaları için theme.json içinde \"engine\":\"sections\" gerekli."));
408
+ if (engine === "sections") checkSectionsEngine({ dir, pages, sectionSchemas, sources, index, legacy, locales, report });
409
+ else checkTemplate(index, legacy, report, t("{0} temasında", base));
336
410
  checkImages(data, path.join(dir, "assets"), report);
337
411
  checkProductTabs(data, report);
338
412
  checkTranslations(locales, meta, data, report);
package/src/files.js CHANGED
@@ -2,7 +2,8 @@ import { lstatSync, readdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  // Pakete hiç girmeyen adlar (sürüm kontrolü, bağımlılıklar, işletim sistemi dosyaları).
5
- const skipped = new Set([".git", "node_modules", ".DS_Store", "Thumbs.db", ".idea", ".vscode"]);
5
+ // .uvako: CLI'ın yerel durumu (son push/pull revizyonu); pakete girmez.
6
+ const skipped = new Set([".git", "node_modules", ".DS_Store", "Thumbs.db", ".idea", ".vscode", ".uvako"]);
6
7
 
7
8
  /** Klasördeki dosyaları (alt klasörlerle) "a/b.c" biçiminde, sıralı döndürür. Sembolik bağlar ayrıca bildirilir. */
8
9
  export function walk(root) {
package/src/init.js CHANGED
@@ -19,7 +19,8 @@ function logo(name) {
19
19
  * `starter` temel temanın ayarlarıdır (platformdan okunur: şema, varsayılanlar, bölüm türleri, ana
20
20
  * sayfa şablonu). Verilmezse iskelet şablonsuz kurulur; tema temel temanın bölüm sırasını kullanır.
21
21
  */
22
- export function initTheme(key, { name, author, cwd, base = "linea", starter = null }) {
22
+ export function initTheme(key, { name, author, cwd, base = "linea", starter = null, engine = "" }) {
23
+ if (engine && engine !== "sections") throw new Error(t("Bilinmeyen tema motoru: {0} (kullanılabilir: sections)", engine));
23
24
  if (!keyPattern.test(key) || key.length > 40) throw new Error(t("Tema anahtarı küçük harf, rakam ve tireden oluşmalı (ör. \"atolye\"): {0}", key));
24
25
  if (!keyPattern.test(base)) throw new Error(t("Temel tema adı geçersiz: {0}", base));
25
26
  const dir = path.join(cwd, key);
@@ -32,12 +33,13 @@ export function initTheme(key, { name, author, cwd, base = "linea", starter = nu
32
33
  };
33
34
 
34
35
  const description = t("{0} teması.", title);
35
- write("theme.json", json({ key, name: title, version: "0.1.0", base, author: String(author || ""), description, screenshot: "", industries: [], features: [], price: 0 }));
36
+ write("theme.json", json({ key, name: title, version: "0.1.0", base, ...(engine ? { engine } : {}), author: String(author || ""), description, screenshot: "", industries: [], features: [], price: 0 }));
36
37
 
37
38
  // Yalnızca temel temadan farklı olan değerler yazılır; gerisi temel temadan gelir.
38
39
  write("config/settings_data.json", json({ logo: "uploads/logo.svg" }));
39
40
 
40
- if (starter?.template) write("templates/index.json", json(starter.template));
41
+ if (engine === "sections") writeSections(write, getLocale() === "en");
42
+ else if (starter?.template) write("templates/index.json", json(starter.template));
41
43
  if (starter) {
42
44
  // Başvuru dosyaları: pakete girmez. Hangi ayarların ve bölüm türlerinin olduğunu gösterir.
43
45
  write("reference/settings_schema.json", json(starter.schema ?? {}));
@@ -49,10 +51,20 @@ export function initTheme(key, { name, author, cwd, base = "linea", starter = nu
49
51
 
50
52
  // Temanın kendi metinlerinin çevirisi: anahtar temadaki metin, değer o dildeki karşılığıdır.
51
53
  // İskelet tek bir örnekle gelir; settings_data.json'a yazdığınız başlık ve buton metinlerini ekleyin.
52
- if (en) write("locales/tr.json", json({ [description]: `${title} teması.` }));
53
- else write("locales/en.json", json({ [description]: `${title} theme.` }));
54
+ const extra = engine === "sections" ? sectionTexts(en) : {};
55
+ if (en) write("locales/tr.json", json({ [description]: `${title} teması.`, ...extra }));
56
+ else write("locales/en.json", json({ [description]: `${title} theme.`, ...extra }));
54
57
 
55
- write("styles/_tokens.scss", en ? `// Overrides the base theme's variables: the values you set here spread across the whole theme.
58
+ if (engine === "sections") write("styles/_tokens.scss", en ? `// Overrides the base theme's variables. Section themes embed their fonts in assets/fonts
59
+ // (KVKK: no font services are called); declare them with @font-face in styles/theme.scss.
60
+ // $font-family: 'Inter', sans-serif;
61
+ // $color-text: #111111;
62
+ ` : `// Temel temanın değişkenlerinin üstüne yazılır. Bölüm temaları yazı tiplerini assets/fonts içine
63
+ // gömer (KVKK: yazı tipi servisi çağrılmaz); styles/theme.scss içinde @font-face ile tanımlayın.
64
+ // $font-family: 'Inter', sans-serif;
65
+ // $color-text: #111111;
66
+ `);
67
+ else write("styles/_tokens.scss", en ? `// Overrides the base theme's variables: the values you set here spread across the whole theme.
56
68
  // This file may contain only variable assignments and font @imports.
57
69
  //
58
70
  // @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap');
@@ -150,3 +162,82 @@ metinleri listeler.
150
162
  ${commands}`);
151
163
  return dir;
152
164
  }
165
+
166
+ /**
167
+ * "engine": "sections" iskeleti: ana sayfa ve ürün sayfası şablonları, iki örnek paket bölümü ve
168
+ * bir ürün kartı snippet'i. Şablonlar sunucuda sandbox içinde çizilir; betik içeremez. Etkileşim
169
+ * (sepete ekle, yan sepet) platformun data-uvako-* öznitelikleriyle gelir.
170
+ */
171
+ function writeSections(write, en) {
172
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
173
+ write("templates/index.json", json({
174
+ order: ["hero", "rack"],
175
+ sections: {
176
+ hero: { type: "pkg:hero", settings: { title: en ? "Pages that fill up" : "Sayfası dolacak defterler", text: en ? "Pick by paper, binding and size." : "Kâğıdını, cildini ve boyutunu görerek seç.", link: "/search" } },
177
+ rack: { type: "plate-rack", settings: { title: en ? "Best sellers" : "Bu hafta çok satanlar", source: "popular", limit: 8 } },
178
+ },
179
+ }));
180
+ write("templates/product.json", json({
181
+ order: ["gallery", "info", "usp", "details", "related"],
182
+ sections: {
183
+ gallery: { type: "product-gallery", settings: { layout: "slider", show_thumbnails: true } },
184
+ info: { type: "product-info", settings: { show_sku: true, show_stock: true, show_unit_price: true } },
185
+ usp: { type: "pkg:usp", settings: {} },
186
+ details: { type: "product-details", settings: { show_specs: true } },
187
+ related: { type: "product-related", settings: { limit: 4 } },
188
+ },
189
+ }));
190
+ write("sections/hero.schema.json", json({
191
+ name: en ? "Hero" : "Karşılama",
192
+ fallback: "plate-rack",
193
+ settings: [
194
+ { id: "title", type: "text", label: en ? "Title" : "Başlık", default: "" },
195
+ { id: "text", type: "textarea", label: en ? "Text" : "Metin", default: "" },
196
+ { id: "link", type: "url", label: en ? "Button link" : "Düğme bağlantısı", default: "/search" },
197
+ { id: "image", type: "image", label: en ? "Image" : "Görsel", default: "" },
198
+ ],
199
+ }));
200
+ write("sections/hero.html.tmpl", `<section class="hero">
201
+ <div class="hero-copy">
202
+ <h1>{{ .section.settings.title }}</h1>
203
+ {{ with .section.settings.text }}<p>{{ . }}</p>{{ end }}
204
+ <a class="btn btn-primary" href="{{ default "/search" .section.settings.link }}">{{ t "Ürünlere göz at" }}</a>
205
+ </div>
206
+ {{ with .section.settings.image }}<img class="hero-image" src="{{ image_url . }}" alt="">{{ end }}
207
+ <div class="hero-cards">
208
+ {{ range .shop.featured }}{{ template "snippets/product-card" . }}{{ end }}
209
+ </div>
210
+ </section>
211
+ `);
212
+ write("sections/usp.schema.json", json({
213
+ name: en ? "Shipping and returns" : "Kargo ve iade",
214
+ fallback: "product-details",
215
+ settings: [
216
+ { id: "shipping", type: "text", label: en ? "Shipping" : "Kargo", default: en ? "Ships today on weekdays before 15:00" : "Hafta içi 15.00'e kadar aynı gün kargoda" },
217
+ { id: "returns", type: "text", label: en ? "Returns" : "İade", default: en ? "14-day returns" : "14 gün iade" },
218
+ { id: "show_cart_button", type: "checkbox", label: en ? "Open cart button" : "Sepeti aç düğmesi", default: true },
219
+ ],
220
+ }));
221
+ write("sections/usp.html.tmpl", `<section class="usp">
222
+ <ul>
223
+ <li>{{ .section.settings.shipping }}</li>
224
+ <li>{{ .section.settings.returns }}</li>
225
+ </ul>
226
+ {{ if .section.settings.show_cart_button }}<button type="button" class="btn" data-uvako-cart-open>{{ t "Sepeti aç" }}</button>{{ end }}
227
+ </section>
228
+ `);
229
+ write("snippets/product-card.html.tmpl", `<article class="card">
230
+ <a href="{{ url_for "product" .handle }}">
231
+ {{ with .image }}<img src="{{ image_url . }}" alt="" loading="lazy">{{ end }}
232
+ <h3>{{ .title }}</h3>
233
+ </a>
234
+ <p class="price"><strong>{{ money .price }}</strong>{{ with .compare_at_price }} <del>{{ money . }}</del>{{ end }}</p>
235
+ <button type="button" class="btn" data-uvako-add-to-cart="{{ .handle }}" data-name="{{ .title }}" data-price="{{ .price }}" data-image="{{ .image }}" data-url="{{ url_for "product" .handle }}" data-stock="{{ .stock }}">{{ t "Sepete ekle" }}</button>
236
+ </article>
237
+ `);
238
+ }
239
+
240
+ /** Örnek bölümlerin {{ t "…" }} metinleri ve karşılıkları (locales/ dosyasına eklenir). */
241
+ export const sectionTexts = (en) => (en
242
+ ? { "Ürünlere göz at": "Ürünlere göz at", "Sepeti aç": "Sepeti aç", "Sepete ekle": "Sepete ekle" }
243
+ : { "Ürünlere göz at": "Browse products", "Sepeti aç": "Open cart", "Sepete ekle": "Add to cart" });
package/src/locales/en.js CHANGED
@@ -265,6 +265,70 @@ export const en = {
265
265
  "{0}: şablonda yalnızca {{ .settings.ad }} ve if/else kullanılabilir.": "{0}: the template may only use {{ .settings.name }} and if/else.",
266
266
  "{0}: block.css içinde url() ve @import kullanılamaz.": "{0}: block.css cannot use url() or @import.",
267
267
  "{0}: block.json \"settings\" bir liste olmalı.": "{0}: block.json \"settings\" must be a list.",
268
+ // Bölüm motoru ("engine": "sections"), sunucu denetimi, push revizyonu.
269
+ "styles/{0}: yazı tipleri assets/ içinde pakete gömülmeli.": "styles/{0}: fonts must be embedded in the package under assets/.",
270
+ "{0}: geçersiz bölüm adı.": "{0}: invalid section name.",
271
+ "sections/{0}.schema.json: bölümün şablonu (sections/{0}.html.tmpl) yok.": "sections/{0}.schema.json: the section has no template (sections/{0}.html.tmpl).",
272
+ "sections/{0}.schema.json: \"fallback\" platformda ya da temel temada olan bir bölüm türü olmalı.": "sections/{0}.schema.json: \"fallback\" must be a section type of the platform or the base theme.",
273
+ "locales/{0}.json: şablonlardaki {1} metnin çevirisi yok (ör. \"{2}\").": "locales/{0}.json: {1} texts in the templates have no translation (e.g. \"{2}\").",
274
+ "{0}: bu dosya pakette bulunamaz. İzin verilenler: {1}.": "{0}: this file cannot be in a package. Allowed: {1}.",
275
+ "theme.json: bilinmeyen \"engine\" değeri.": "theme.json: unknown \"engine\" value.",
276
+ "Yeni sayfa ve bölüm dosyaları için theme.json içinde \"engine\":\"sections\" gerekli.": "Page and section files need \"engine\":\"sections\" in theme.json.",
277
+ "Bilinmeyen tema motoru: {0} (kullanılabilir: sections)": "Unknown theme engine: {0} (available: sections)",
278
+ "Sunucu denetimi için giriş yapın (uvako login --store <mağaza>) ya da --offline ekleyin.": "Log in to check on the server too (uvako login --store <store>), or add --offline.",
279
+ "✖ Sunucu denetimi ({0}): {1}": "✖ Server check ({0}): {1}",
280
+ "Sunucu denetimi ({0}): geçti.": "Server check ({0}): passed.",
281
+ " Yükleme sınırı aşıldı. {0} saniye sonra tekrar deneyin.": " Upload limit reached. Try again in {0} seconds.",
282
+ " Sunucudaki sürümü almak için: uvako theme pull <anahtar> --force. Üzerine yazmak için: uvako theme push --force": " To get the server's version: uvako theme pull <key> --force. To overwrite it: uvako theme push --force",
283
+ "Sunucu denetimi yapılamadı: {0}": "The server check could not run: {0}",
284
+ "kapanmamış şablon aksiyonu": "unterminated template action",
285
+ "izin verilmeyen öznitelik: {0}": "attribute not allowed: {0}",
286
+ "{0} içinde izin verilmeyen adres şeması": "URL scheme not allowed in {0}",
287
+ "srcset içinde izin verilmeyen adres şeması": "URL scheme not allowed in srcset",
288
+ "style özniteliğinde izin verilmeyen ifade: {0}": "expression not allowed in the style attribute: {0}",
289
+ "kapanmamış HTML yorumu": "unterminated HTML comment",
290
+ "işaretleme bildirimi kullanılamaz": "markup declarations are not allowed",
291
+ "şablon aksiyonu etiket adı oluşturamaz": "a template action cannot form a tag name",
292
+ "izin verilmeyen etiket: {0}": "element not allowed: {0}",
293
+ "kapanmamış etiket: {0}": "unterminated tag: {0}",
294
+ "şablon aksiyonu öznitelik adı oluşturamaz": "a template action cannot form an attribute name",
295
+ "{0} etiketinde kapanmamış öznitelik değeri": "unterminated attribute value on {0}",
296
+ "izin verilmeyen aksiyon: {0}": "action not allowed: {0}",
297
+ "bilinmeyen fonksiyon: {0}": "unknown function: {0}",
298
+ "{0} yalnızca paketin snippets/ şablonlarını adlandırabilir: {1}": "{0} may only name the package's snippets/ templates: {1}",
299
+ "snippet bulunamadı: {0}": "snippet not found: {0}",
300
+ "Pakette en çok 128 bölüm şablonu olabilir.": "A package may contain at most 128 section templates.",
301
+ "{0}: şablon adında yalnızca küçük harf, rakam, tire ve alt çizgi olabilir.": "{0}: a template name may contain only lowercase letters, digits, hyphens and underscores.",
302
+ "{0}: platform bölümünün adı kullanılamaz; başka bir ad verin.": "{0}: the name of a platform section cannot be used; choose another name.",
303
+ "{0}: bölüm ayar şeması geçersiz ({1}).": "{0}: invalid section setting schema ({1}).",
304
+ "nesne olmalı": "must be an object",
305
+ "\"name\" gerekli, en çok 100 karakter": "\"name\" is required, at most 100 characters",
306
+ "\"settings\" en çok {0} ayarlık bir liste olmalı": "\"settings\" must be a list of at most {0} settings",
307
+ "\"fallback\" bir bölüm türü olmalı": "\"fallback\" must be a section type",
308
+ "\"{0}\" ayar adı geçersiz ya da tekrarlı": "setting name \"{0}\" is invalid or repeated",
309
+ "\"{0}\" ayarının \"label\" alanı gerekli": "setting \"{0}\" needs a \"label\"",
310
+ "\"{0}\" ayarının türü ({1}) bilinmiyor; kullanılabilir: {2}": "setting \"{0}\" has an unknown type ({1}); available: {2}",
311
+ "\"{0}\" ayarının min/max değerleri geçersiz": "setting \"{0}\" has invalid min/max values",
312
+ "\"{0}\" ayarının step değeri geçersiz": "setting \"{0}\" has an invalid step",
313
+ "\"{0}\" (range) için min ve max gerekli": "\"{0}\" (range) needs min and max",
314
+ "\"{0}\" (select) için 1-50 seçenek gerekli": "\"{0}\" (select) needs 1-50 options",
315
+ "\"{0}\" ayarının seçenekleri geçersiz": "setting \"{0}\" has invalid options",
316
+ "\"{0}\" ayarının varsayılan değeri türüne uymuyor": "the default of setting \"{0}\" does not match its type",
317
+ "geçerli bir şablon değil ({\"order\": […], \"sections\": {…}}).": "not a valid template ({\"order\": […], \"sections\": {…}}).",
318
+ "en çok 100 bölüm olabilir.": "may contain at most 100 sections.",
319
+ "\"{0}\" bölüm kimliği geçersiz ya da tekrarlı.": "section id \"{0}\" is invalid or repeated.",
320
+ "\"order\" içindeki \"{0}\" bölümü \"sections\" altında yok.": "section \"{0}\" in \"order\" is not under \"sections\".",
321
+ "\"{0}\" bölüm kimliği geçersiz.": "section id \"{0}\" is invalid.",
322
+ "\"{0}\" bölümünün türü ({1}) ne platformda ne pakette ne de temel temada var.": "the type of section \"{0}\" ({1}) is neither a platform, package nor base theme section.",
323
+ "\"{0}\" bölümünün türü ({1}) pakette yok: sections/{2}.html.tmpl ekleyin.": "the type of section \"{0}\" ({1}) is not in the package: add sections/{2}.html.tmpl.",
324
+ "\"{0}\" bölümünün \"{1}\" ayarı betik ya da izin verilmeyen bir değer içeriyor.": "setting \"{1}\" of section \"{0}\" contains a script or a value that is not allowed.",
325
+ "\"{0}\" bölümünün \"{1}\" ayarı şemada yok ya da türüne uymuyor.": "setting \"{1}\" of section \"{0}\" is not in the schema or does not match its type.",
326
+ "Bu mağazada bölüm motoru etkin değil.": "The section engine is not enabled for this store.",
327
+ "Paket sunucuda sizin kopyanızdan sonra değişti. `uvako theme pull` ile güncelleyin ya da --force ile üzerine yazın.": "The package changed on the server after your copy. Update with `uvako theme pull` or overwrite with --force.",
328
+ "Paket yolda bozuldu (SHA-256 uyuşmuyor). Tekrar deneyin.": "The package was damaged in transit (SHA-256 mismatch). Try again.",
329
+ "Tema yükleme sınırı aşıldı. Belirtilen süre sonra tekrar deneyin.": "Theme upload limit reached. Try again after the given time.",
330
+ "Dosya boyut sınırını aşıyor.": "The file exceeds the size limit.",
331
+ "Bölüm şablonları derlenemedi: {0}": "Section templates could not be compiled: {0}",
268
332
  };
269
333
 
270
334
  export const help = (version) => `uvako-cli ${version} — Uvako theme and app development tool
@@ -287,22 +351,26 @@ App (public apps, with the partner sign-in):
287
351
  uvako app list [--json] Lists your registered apps
288
352
 
289
353
  Theme:
290
- uvako theme init <key> [--base linea] [--name "Name"] [--author "Author"]
354
+ uvako theme init <key> [--base linea] [--name "Name"] [--author "Author"] [--engine sections]
291
355
  A new theme scaffold built on a catalog theme
292
- uvako theme check [folder] Checks the theme (structure, settings, translations, security)
293
- uvako theme dev [folder] Live development: uploads to the store as you save, the preview refreshes
356
+ --engine sections: a theme that brings its own sections (with examples)
357
+ uvako theme check [folder] [--offline] Checks the theme (structure, settings, translations, security, section templates);
358
+ when you are logged in the server checks it with the same rules too
359
+ uvako theme dev [folder] Live development: uploads only the file you changed, the preview refreshes
294
360
  The store returns to its previous state when you stop (--keep: leave in library)
295
- uvako theme push [folder] Uploads the theme to the store's theme library
296
- uvako theme pull <key> [--dir folder] Downloads a theme you uploaded
361
+ uvako theme push [folder] [--force] Uploads the theme to the store's theme library (with SHA-256 and revision;
362
+ refused if the package changed after your copy, --force overwrites)
363
+ uvako theme pull <key> [--dir folder] Downloads a theme you uploaded (including sections/, snippets/, templates/)
297
364
  uvako theme list Lists the themes in the store and the packages you uploaded
298
365
  uvako theme submit <key> Submits the theme for review for themes.uvako.com
299
366
  uvako theme delete <key> --yes Deletes a package you uploaded
300
367
  uvako theme package [folder] [--out x.zip] Checks the theme and builds a zip package
301
368
  uvako version Prints the version
302
369
 
303
- A theme is a package that runs no code: settings, section order, SCSS and images.
304
- Packages containing JavaScript, templates or server code are rejected. An uploaded theme is
305
- visible only in your own store; publishing it in the theme store depends on Uvako's review.
370
+ A theme is a package that runs no code: settings, page templates, SCSS and images. "engine": "sections"
371
+ themes may bring their own sections (sections/*.html.tmpl); they are drawn only on the server, in a
372
+ restricted template language. Packages containing JavaScript or server code are rejected. An uploaded
373
+ theme is visible only in your own store; publishing it in the theme store depends on Uvako's review.
306
374
 
307
375
  For CI: the UVAKO_STORE and UVAKO_TOKEN (theme) and UVAKO_PARTNER_TOKEN (app) environment variables take the place of signing in.
308
376
  Language: --lang tr or UVAKO_LANG=tr prints the output in Turkish.
package/src/main.js CHANGED
@@ -6,10 +6,10 @@ import { checkTheme, printReport } from "./check.js";
6
6
  import { session, siteOrigin } from "./config.js";
7
7
  import { initTheme } from "./init.js";
8
8
  import { packageTheme } from "./package.js";
9
- import { dev, list, login, logout, pull, push, remove, submit, whoami } from "./remote.js";
9
+ import { dev, list, login, logout, pull, push, remoteCheck, remove, submit, whoami } from "./remote.js";
10
10
  import { appCommand } from "./app.js";
11
11
  import { partnerLogin, partnerLogout, partnerWhoami } from "./partner.js";
12
- import { getLocale, t } from "./i18n.js";
12
+ import { getLocale, t, tm } from "./i18n.js";
13
13
  import { help as helpEn } from "./locales/en.js";
14
14
 
15
15
  const version = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
@@ -34,22 +34,26 @@ Uygulama (herkese açık uygulamalar, partner girişiyle):
34
34
  uvako app list [--json] Kayıtlı uygulamalarınızı listeler
35
35
 
36
36
  Tema:
37
- uvako theme init <anahtar> [--base linea] [--name "Ad"] [--author "Yazar"]
37
+ uvako theme init <anahtar> [--base linea] [--name "Ad"] [--author "Yazar"] [--engine sections]
38
38
  Bir katalog temasını temel alan yeni tema iskeleti
39
- uvako theme check [klasör] Temayı denetler (yapı, ayarlar, güvenlik)
40
- uvako theme dev [klasör] Canlı geliştirme: kaydettikçe mağazaya yükler, önizleme yenilenir
39
+ --engine sections: kendi bölümlerini getiren tema (örnek bölümlerle)
40
+ uvako theme check [klasör] [--offline] Temayı denetler (yapı, ayarlar, güvenlik, bölüm şablonları);
41
+ giriş yapılmışsa sunucu da aynı kurallarla denetler
42
+ uvako theme dev [klasör] Canlı geliştirme: yalnızca değişen dosyayı yükler, önizleme yenilenir
41
43
  Durdurunca mağaza eski hâline döner (--keep: kütüphanede bırak)
42
- uvako theme push [klasör] Temayı mağazanın tema kütüphanesine yükler
43
- uvako theme pull <anahtar> [--dir klasör] Yüklediğiniz temayı indirir
44
+ uvako theme push [klasör] [--force] Temayı mağazanın tema kütüphanesine yükler (SHA-256 ve revizyonla;
45
+ paket sizden sonra değiştiyse reddedilir, --force üzerine yazar)
46
+ uvako theme pull <anahtar> [--dir klasör] Yüklediğiniz temayı indirir (sections/, snippets/, templates/ dahil)
44
47
  uvako theme list Mağazadaki temaları ve yüklediğiniz paketleri listeler
45
48
  uvako theme submit <anahtar> Temayı themes.uvako.com için incelemeye gönderir
46
49
  uvako theme delete <anahtar> --yes Yüklediğiniz paketi siler
47
50
  uvako theme package [klasör] [--out x.zip] Denetleyip zip paketi üretir
48
51
  uvako version Sürümü yazar
49
52
 
50
- Tema, kod çalıştırmayan bir pakettir: ayarlar, bölüm sırası, SCSS ve görseller.
51
- JavaScript, şablon ya da sunucu kodu içeren paketler reddedilir. Yüklenen tema yalnızca kendi
52
- mağazanızda görünür; tema mağazasında yayınlanması Uvako incelemesine bağlıdır.
53
+ Tema, kod çalıştırmayan bir pakettir: ayarlar, sayfa şablonları, SCSS ve görseller. "engine": "sections"
54
+ temaları kendi bölümlerini (sections/*.html.tmpl) getirebilir; bunlar yalnızca sunucuda, kısıtlı bir
55
+ şablon dilinde çizilir. JavaScript ya da sunucu kodu içeren paketler reddedilir. Yüklenen tema yalnızca
56
+ kendi mağazanızda görünür; tema mağazasında yayınlanması Uvako incelemesine bağlıdır.
53
57
 
54
58
  CI için: UVAKO_STORE ve UVAKO_TOKEN (tema), UVAKO_PARTNER_TOKEN (uygulama) ortam değişkenleri girişin yerini tutar.
55
59
  Dil: --lang en ya da UVAKO_LANG=en çıktıyı İngilizce verir.
@@ -138,16 +142,33 @@ export async function main(argv) {
138
142
  if (command === "init") {
139
143
  if (!target) throw new Error(t("Tema anahtarı gerekli: uvako theme init <anahtar>"));
140
144
  const base = typeof options.base === "string" ? options.base : "linea";
141
- const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd(), base, starter: await fetchStarter(base, options) });
145
+ const engine = typeof options.engine === "string" ? options.engine : "";
146
+ const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd(), base, engine, starter: await fetchStarter(base, options) });
142
147
  const rel = path.relative(process.cwd(), dir) || ".";
143
148
  console.log(t("Tema oluşturuldu: {0} (temel: {1})", rel, base));
144
149
  console.log(t("Sıradaki adım: uvako login --store <mağaza>, sonra: uvako theme dev {0}", rel));
145
150
  return 0;
146
151
  }
147
152
  if (command === "check") {
148
- const report = checkTheme(path.resolve(target ?? "."));
153
+ const dir = path.resolve(target ?? ".");
154
+ const report = checkTheme(dir);
149
155
  printReport(report);
150
- return report.errors.length ? 1 : 0;
156
+ if (report.errors.length) return 1;
157
+ // Giriş yapılmışsa sunucu da aynı paketi kuru olarak denetler: yerelde geçen sunucuda da geçer.
158
+ if (options.offline || report.layout !== "package") return 0;
159
+ const remote = await remoteCheck(dir, options);
160
+ if (!remote) {
161
+ console.log(t("Sunucu denetimi için giriş yapın (uvako login --store <mağaza>) ya da --offline ekleyin."));
162
+ return 0;
163
+ }
164
+ if (!remote.ok) {
165
+ console.error(t("✖ Sunucu denetimi ({0}): {1}", remote.origin, tm(remote.message)));
166
+ for (const problem of remote.problems) console.error(` ✖ ${tm(problem)}`);
167
+ return 1;
168
+ }
169
+ for (const warning of remote.warnings) console.warn(` ! ${tm(warning)}`);
170
+ console.log(t("Sunucu denetimi ({0}): geçti.", remote.origin));
171
+ return 0;
151
172
  }
152
173
  if (command === "package") {
153
174
  const result = packageTheme(path.resolve(target ?? "."), { out: typeof options.out === "string" ? options.out : undefined });