uvako-cli 0.1.2 → 0.2.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 +64 -15
- package/package.json +2 -2
- package/src/api.js +49 -0
- package/src/check.js +210 -81
- package/src/config.js +100 -0
- package/src/init.js +51 -85
- package/src/main.js +59 -13
- package/src/package.js +20 -61
- package/src/remote.js +264 -0
- package/src/zip.js +91 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uvako-cli
|
|
2
2
|
|
|
3
|
-
Uvako mağazaları için tema geliştirme aracı. Temayı başlatır, denetler ve
|
|
3
|
+
Uvako mağazaları için tema geliştirme aracı. Temayı başlatır, denetler, mağazanıza yükler ve kaydettikçe canlı önizletir.
|
|
4
4
|
|
|
5
5
|
**[Uvako](https://uvako.com)** — Rox Software ürünü.
|
|
6
6
|
|
|
@@ -13,45 +13,94 @@ npm install -g uvako-cli
|
|
|
13
13
|
uvako version
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Hızlı başlangıç
|
|
17
|
+
|
|
18
|
+
1. Mağaza panelinde **Ayarlar → Temalar → Geliştirici erişimi** bölümünden bir erişim anahtarı oluşturun.
|
|
19
|
+
2. Giriş yapın (anahtar sorulur, ekrana yazılmaz):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uvako login --store magazam
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. Bir katalog temasını temel alan yeni tema başlatın:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uvako theme init atolye --base linea
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
4. Canlı geliştirin: dosyaları kaydettikçe tema mağazanıza yüklenir, açık önizleme sayfası kendini yeniler.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvako theme dev atolye
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Önizleme adresi mağazanızın kendi adresidir (`https://magazam.uvoo.store/?preview=…`); tarayıcıda mağaza
|
|
38
|
+
paneline giriş yapmış olmanız gerekir. Tema, panelden yayınlanana kadar vitrininizi değiştirmez.
|
|
39
|
+
|
|
16
40
|
## Komutlar
|
|
17
41
|
|
|
18
42
|
| Komut | Ne yapar |
|
|
19
43
|
|---|---|
|
|
20
|
-
| `uvako
|
|
44
|
+
| `uvako login --store <mağaza>` | Mağazaya tema erişim anahtarıyla bağlanır. |
|
|
45
|
+
| `uvako logout` · `uvako whoami` | Kayıtlı girişi siler · bağlı mağazayı gösterir. |
|
|
46
|
+
| `uvako theme init <anahtar>` | Yeni tema iskeleti (`--base`, `--name`, `--author`). |
|
|
21
47
|
| `uvako theme check [klasör]` | Temayı denetler: yapı, ayarlar, bölümler, görseller, güvenlik. |
|
|
48
|
+
| `uvako theme dev [klasör]` | Canlı geliştirme: kaydettikçe yükler, önizleme yenilenir. |
|
|
49
|
+
| `uvako theme push [klasör]` | Temayı mağazanın tema kütüphanesine yükler. |
|
|
50
|
+
| `uvako theme pull <anahtar>` | Yüklediğiniz temayı indirir (`--dir`, `--force`). |
|
|
51
|
+
| `uvako theme list` | Mağazadaki temaları ve yüklediğiniz paketleri listeler. |
|
|
52
|
+
| `uvako theme submit <anahtar>` | Temayı themes.uvako.com için incelemeye gönderir. |
|
|
53
|
+
| `uvako theme delete <anahtar> --yes` | Yüklediğiniz paketi siler. |
|
|
22
54
|
| `uvako theme package [klasör]` | Denetimden geçen temayı zip paketine çevirir (`--out`). |
|
|
23
55
|
| `uvako version` | Sürümü yazar. |
|
|
24
56
|
|
|
25
|
-
`uvako-cli` aynı komutların uzun adıdır.
|
|
57
|
+
`uvako-cli` aynı komutların uzun adıdır. Hata varsa çıkış kodu 1'dir. CI için `UVAKO_STORE` ve
|
|
58
|
+
`UVAKO_TOKEN` ortam değişkenleri girişin yerini tutar.
|
|
26
59
|
|
|
27
60
|
## Tema paketi
|
|
28
61
|
|
|
29
|
-
Tema kod içermez.
|
|
62
|
+
Tema kod içermez. Bir katalog temasını (`base`) temel alır ve onun üstüne şunları getirir:
|
|
30
63
|
|
|
31
64
|
```
|
|
32
|
-
theme.json kimlik: key, name, version
|
|
33
|
-
config/
|
|
34
|
-
config/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
styles/
|
|
65
|
+
theme.json kimlik: key, name, version, base; mağaza vitrini bilgileri
|
|
66
|
+
config/settings_data.json temel temadan farklı olan ayar değerleri
|
|
67
|
+
config/settings_schema.json (isteğe bağlı) panelde görünen ayarlar
|
|
68
|
+
templates/index.json (isteğe bağlı) ana sayfadaki bölüm sırası
|
|
69
|
+
styles/_tokens.scss renk, yazı tipi, köşe değişkenleri (temanın tamamına yayılır)
|
|
70
|
+
styles/theme.scss temaya özgü SCSS
|
|
38
71
|
assets/ logo, slayt ve banner görselleri, yazı tipleri
|
|
72
|
+
reference/ temel temanın ayarları ve bölüm türleri (pakete girmez)
|
|
39
73
|
```
|
|
40
74
|
|
|
75
|
+
Bölümlerin kendisi (slayt, banner, ürün vitrini…) temel temadan gelir; paket hangilerinin hangi sırayla
|
|
76
|
+
ve hangi görünümle kullanılacağını söyler. Katalog temaları: [themes.uvako.com](https://themes.uvako.com).
|
|
77
|
+
|
|
78
|
+
## Yayınlama
|
|
79
|
+
|
|
80
|
+
- `push` ile yüklenen tema **yalnızca kendi mağazanızda** görünür.
|
|
81
|
+
- `submit` temayı Uvako incelemesine gönderir. Onaylanan sürüm themes.uvako.com'da listelenir ve diğer
|
|
82
|
+
mağazalar kurabilir. Fiyatı (`theme.json` içindeki `price` öneridir) Uvako onaylar.
|
|
83
|
+
- Onaydan sonra yüklediğiniz yeni sürümler yeniden onaylanana kadar yalnızca kendi mağazanızı etkiler.
|
|
84
|
+
|
|
41
85
|
## Güvenlik kuralları
|
|
42
86
|
|
|
43
|
-
`check`
|
|
87
|
+
Mağaza, yüklenen paketi aynı kurallarla **kendisi yeniden denetler**; `check` hatayı önceden görmeniz içindir.
|
|
44
88
|
|
|
45
|
-
- JSON, SCSS
|
|
89
|
+
- Yalnızca yukarıdaki dosyalar kabul edilir: JSON, SCSS, görsel ve yazı tipi. JavaScript, şablon ve
|
|
90
|
+
çalıştırılabilir dosyalar reddedilir.
|
|
46
91
|
- `<script>`, `javascript:` adresleri, `onclick` gibi olay öznitelikleri, `<iframe>` (SVG ve ayar metinleri dahil).
|
|
47
92
|
- Kod çalıştıran CSS (`expression`, `behavior`, `-moz-binding`).
|
|
48
|
-
- SCSS
|
|
93
|
+
- SCSS içinde döngü, fonksiyon ve mixin tanımı (`@while`, `@for`, `@each`, `@function`, `@mixin`, `@use`).
|
|
94
|
+
Temel temanın mixin'leri `@include` ile kullanılabilir.
|
|
95
|
+
- Paketin kendi dosyaları dışındaki `@import`'lar ve yazı tipi sağlayıcıları dışındaki adresler.
|
|
49
96
|
- Sembolik bağlar, gizli dosyalar, 5 MB'tan büyük dosyalar, 60 MB'tan büyük ya da 600 dosyadan fazla paketler.
|
|
50
97
|
- Gizli anahtar ya da parola gibi görünen değerler.
|
|
51
98
|
|
|
52
|
-
|
|
99
|
+
Erişim anahtarı yalnızca tema işlemlerine yetkilidir; sipariş, müşteri ve ayarlara erişemez. Bilgisayarınızda
|
|
100
|
+
kullanıcı klasörünüzde saklanır, tema klasörüne yazılmaz, yalnızca giriş yaptığınız mağazaya ve yalnızca
|
|
101
|
+
https üzerinden gönderilir. Kaybederseniz panelden iptal edin.
|
|
53
102
|
|
|
54
|
-
|
|
103
|
+
Araç platformun kaynak kodunu içermez.
|
|
55
104
|
|
|
56
105
|
## Uvako
|
|
57
106
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uvako-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Uvako tema geliştirme aracı. Tema başlatır, denetler ve
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Uvako tema geliştirme aracı. Tema başlatır, denetler, mağazaya yükler ve canlı önizletir. https://uvako.com",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": {
|
package/src/api.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Mağazanın tema uç noktası (/api/theme-cli). Erişim anahtarı yalnızca girişte seçilen mağazaya
|
|
2
|
+
// gönderilir: yönlendirmeler izlenmez, böylece anahtar başka bir adrese taşınmaz.
|
|
3
|
+
|
|
4
|
+
export class ApiError extends Error {
|
|
5
|
+
constructor(message, { status = 0, code = "", problems = [] } = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.problems = problems;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function call({ origin, token }, method, route, { body, raw = false, timeout = 120000 } = {}) {
|
|
14
|
+
let response;
|
|
15
|
+
try {
|
|
16
|
+
response = await fetch(`${origin}/api/theme-cli${route}`, {
|
|
17
|
+
method,
|
|
18
|
+
redirect: "error",
|
|
19
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "User-Agent": "uvako-cli", ...(body ? { "Content-Type": "application/zip" } : {}) },
|
|
20
|
+
body,
|
|
21
|
+
signal: AbortSignal.timeout(timeout),
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new ApiError(`Mağazaya bağlanılamadı (${origin}): ${error.cause?.code ?? error.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (response.ok) return raw ? Buffer.from(await response.arrayBuffer()) : response.json();
|
|
27
|
+
let payload = null;
|
|
28
|
+
try {
|
|
29
|
+
payload = await response.json();
|
|
30
|
+
} catch {
|
|
31
|
+
// JSON olmayan yanıt (ör. ağ geçidi hatası).
|
|
32
|
+
}
|
|
33
|
+
const problem = payload?.error ?? {};
|
|
34
|
+
const fallback = response.status === 404 ? "Bu adreste Uvako tema uç noktası yok. Mağaza adresini kontrol edin." : `Sunucu ${response.status} yanıtı verdi.`;
|
|
35
|
+
throw new ApiError(problem.message || fallback, { status: response.status, code: problem.code || "", problems: Array.isArray(problem.problems) ? problem.problems : [] });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Herkese açık uç nokta: temel temanın başlangıç ayarları (giriş gerektirmez). */
|
|
39
|
+
export async function starter(site, base) {
|
|
40
|
+
let response;
|
|
41
|
+
try {
|
|
42
|
+
response = await fetch(`${site}/api/site/themes/${encodeURIComponent(base)}/starter`, { headers: { Accept: "application/json", "User-Agent": "uvako-cli" }, signal: AbortSignal.timeout(30000) });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new ApiError(`uvako.com'a bağlanılamadı: ${error.cause?.code ?? error.message}`);
|
|
45
|
+
}
|
|
46
|
+
if (response.status === 404) throw new ApiError(`"${base}" adında bir temel tema yok. Temalar: https://themes.uvako.com`, { status: 404 });
|
|
47
|
+
if (!response.ok) throw new ApiError(`Temel tema okunamadı (${response.status}).`, { status: response.status });
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
package/src/check.js
CHANGED
|
@@ -2,20 +2,27 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { walk } from "./files.js";
|
|
4
4
|
|
|
5
|
-
// Tema
|
|
6
|
-
//
|
|
5
|
+
// Tema denetimi. Mağazaya yüklenen paketi platform aynı kurallarla yeniden denetler; buradaki
|
|
6
|
+
// denetim geliştiriciye hatayı yüklemeden önce göstermek içindir, güvenlik kararı sunucudadır.
|
|
7
|
+
//
|
|
8
|
+
// İki düzen vardır:
|
|
9
|
+
// paket — bağımsız tema klasörü: theme.json içindeki "base" bir katalog temasıdır; paket onun
|
|
10
|
+
// üstüne ayar, bölüm sırası, SCSS ve görsel getirir.
|
|
11
|
+
// depo — platform deposundaki katalog teması (themes/<anahtar>).
|
|
7
12
|
|
|
8
|
-
export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600 };
|
|
13
|
+
export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600, styleBytes: 512 * 1024, archiveBytes: 40 * 1024 * 1024 };
|
|
9
14
|
|
|
10
15
|
const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11
16
|
const versionPattern = /^\d+\.\d+\.\d+$/;
|
|
17
|
+
const stylePattern = /^_?[a-z0-9]+(?:[-_][a-z0-9]+)*\.scss$/;
|
|
18
|
+
const segmentPattern = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
const assetExtensions = new Set([".png", ".jpg", ".jpeg", ".webp", ".avif", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".otf"]);
|
|
21
|
+
// Depodaki katalog temalarında bulunabilecek dosya türleri.
|
|
22
|
+
const repoExtensions = new Set([...assetExtensions, ".json", ".scss", ".css", ".md", ".txt", ".eot"]);
|
|
23
|
+
|
|
24
|
+
/** Pakete girmeyen, yalnızca geliştiriciye yardımcı dosyalar. */
|
|
25
|
+
export const localOnly = (rel) => rel === "README.md" || rel.startsWith("reference/") || rel.endsWith(".zip");
|
|
19
26
|
|
|
20
27
|
// SCSS içinden çağrılabilecek dış adresler (yazı tipleri).
|
|
21
28
|
const allowedStyleHosts = new Set(["fonts.googleapis.com", "fonts.gstatic.com", "fonts.bunny.net"]);
|
|
@@ -24,24 +31,25 @@ const scriptPatterns = [
|
|
|
24
31
|
{ pattern: /<script\b/i, text: "<script> etiketi" },
|
|
25
32
|
{ pattern: /javascript:/i, text: "javascript: adresi" },
|
|
26
33
|
{ pattern: /\son[a-z]+\s*=/i, text: "olay özniteliği (onclick, onload…)" },
|
|
27
|
-
{ pattern: /<iframe\b|<object\b|<embed\b/i, text: "gömülü içerik etiketi" },
|
|
34
|
+
{ pattern: /<iframe\b|<object\b|<embed\b|<foreignObject\b/i, text: "gömülü içerik etiketi" },
|
|
28
35
|
];
|
|
29
36
|
|
|
30
37
|
const secretPatterns = [
|
|
31
38
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
32
39
|
/\b(sk|pk)_(live|test)_[A-Za-z0-9]{16,}\b/,
|
|
33
40
|
/\bAKIA[0-9A-Z]{16}\b/,
|
|
41
|
+
/\buvt_[A-Za-z0-9_-]{20,}\b/,
|
|
34
42
|
// JSON içinde tırnaklar ters eğik çizgiyle kaçışlı olabilir.
|
|
35
43
|
/\b(api[_-]?key|secret|password|token)\\?["']?\s*[:=]\s*\\?["'][^"'\\]{12,}/i,
|
|
36
44
|
];
|
|
37
45
|
|
|
38
|
-
function readJson(file, report, label) {
|
|
46
|
+
function readJson(file, report, label, required = true) {
|
|
39
47
|
if (!existsSync(file)) {
|
|
40
|
-
report.errors.push(`${label} yok.`);
|
|
48
|
+
if (required) report.errors.push(`${label} yok.`);
|
|
41
49
|
return null;
|
|
42
50
|
}
|
|
43
51
|
try {
|
|
44
|
-
return JSON.parse(readFileSync(file, "utf8").replace(
|
|
52
|
+
return JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
|
|
45
53
|
} catch (error) {
|
|
46
54
|
report.errors.push(`${label} geçerli JSON değil: ${error.message}`);
|
|
47
55
|
return null;
|
|
@@ -82,22 +90,175 @@ function scanText(rel, text, report) {
|
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
function hostOf(target) {
|
|
94
|
+
try {
|
|
95
|
+
return new URL(target.startsWith("//") ? `https:${target}` : target).hostname;
|
|
96
|
+
} catch {
|
|
97
|
+
return "?";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const allowedUrl = (target) => /^(https:)?\/\//i.test(target) && allowedStyleHosts.has(hostOf(target));
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stil denetimi. `strict` paketlerde açıktır: döngü, fonksiyon ve mixin tanımı yoktur (paket
|
|
105
|
+
* derleyiciyi kilitleyemez), yalnızca paketin kendi dosyaları içe aktarılır.
|
|
106
|
+
*/
|
|
107
|
+
function scanStyle(rel, source, report, { strict = false, styles = new Set(), tokens = false } = {}) {
|
|
108
|
+
let text = source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[\s;{}])\/\/[^\n]*/gm, "$1");
|
|
109
|
+
const held = [];
|
|
110
|
+
text = text.replace(/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g, (quoted) => `"~${held.push(quoted.slice(1, -1)) - 1}~"`);
|
|
111
|
+
const resolve = (value) => {
|
|
112
|
+
const match = /^"~(\d+)~"$/.exec(value.trim());
|
|
113
|
+
return (match ? held[Number(match[1])] ?? "" : value).trim();
|
|
114
|
+
};
|
|
115
|
+
const all = `${text} ${held.join(" ")}`;
|
|
116
|
+
if (/expression\s*\(|behavior\s*:|-moz-binding|<\/style/i.test(all)) report.errors.push(`${rel}: tarayıcıda kod çalıştıran CSS (expression, behavior, -moz-binding) içeriyor.`);
|
|
117
|
+
if (/javascript:/i.test(all)) report.errors.push(`${rel}: javascript: adresi içeriyor.`);
|
|
118
|
+
if (strict) {
|
|
119
|
+
const rule = /@(while|for|each|function|mixin|use|forward|debug|warn|error|charset)\b/i.exec(text);
|
|
120
|
+
if (rule) report.errors.push(`${rel}: @${rule[1].toLowerCase()} kullanılamaz. Tema stilleri döngü, fonksiyon ve mixin tanımı içeremez; temel temanın mixin'leri @include ile kullanılabilir.`);
|
|
121
|
+
if (text.includes("\\")) report.errors.push(`${rel}: tırnak dışında ters eğik çizgi kullanılamaz.`);
|
|
122
|
+
}
|
|
123
|
+
text = text.replace(/url\(([^)]*)\)/gi, (_, inner) => {
|
|
124
|
+
const target = resolve(inner);
|
|
125
|
+
if (/^data:/i.test(target)) {
|
|
126
|
+
if (strict && !/^data:(image\/(png|jpe?g|gif|webp)|font\/woff2?|application\/font-woff2?);base64,/i.test(target)) report.errors.push(`${rel}: yalnızca görsel ve yazı tipi data: adresleri kullanılabilir.`);
|
|
127
|
+
} else if ((target.includes("//") || (strict && target.includes(":"))) && !allowedUrl(target)) {
|
|
128
|
+
report.errors.push(`${rel}: izin verilmeyen dış adres (${hostOf(target)}). Görsel ve yazı tiplerini assets/ altına koyun; yalnızca şu adresler çağrılabilir: ${[...allowedStyleHosts].join(", ")}.`);
|
|
129
|
+
}
|
|
130
|
+
return "url()";
|
|
131
|
+
});
|
|
132
|
+
for (const match of text.matchAll(/@import\s+([^;]+);/gi)) {
|
|
133
|
+
for (const part of match[1].split(",")) {
|
|
134
|
+
if (/^\s*url\(\)/i.test(part)) continue;
|
|
135
|
+
const target = resolve(part);
|
|
136
|
+
if (target.includes("//") || target.includes(":")) {
|
|
137
|
+
if (!allowedUrl(target)) report.errors.push(`${rel}: izin verilmeyen dış adres (${hostOf(target)}).`);
|
|
138
|
+
} else if (strict && !(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(target) && (styles.has(`${target}.scss`) || styles.has(`_${target}.scss`)))) {
|
|
139
|
+
report.errors.push(`${rel}: @import "${target}" pakette yok. Yalnızca paketin kendi styles/ dosyaları içe aktarılabilir.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (tokens) {
|
|
144
|
+
for (const raw of text.split(";")) {
|
|
145
|
+
const statement = raw.trim();
|
|
146
|
+
if (statement === "" || /^\$[A-Za-z0-9_-]+\s*:\s*[^{}@]+$/.test(statement) || /^@import/i.test(statement)) continue;
|
|
147
|
+
report.errors.push(`${rel}: yalnızca değişken ataması ($ad: değer;) ve yazı tipi @import'u içerebilir.`);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkIdentity(dir, meta, report) {
|
|
154
|
+
const key = typeof meta?.key === "string" ? meta.key.trim() : "";
|
|
155
|
+
report.key = key;
|
|
156
|
+
if (!meta) return key;
|
|
157
|
+
if (!keyPattern.test(key) || key.length > 40) report.errors.push(`theme.json: "key" küçük harf, rakam ve tireden oluşmalı (ör. "linea").`);
|
|
158
|
+
else if (key !== path.basename(dir)) report.errors.push(`theme.json: "key" (${key}) klasör adıyla (${path.basename(dir)}) aynı olmalı.`);
|
|
159
|
+
if (!String(meta.name ?? "").trim()) report.errors.push(`theme.json: "name" gerekli.`);
|
|
160
|
+
if (!versionPattern.test(String(meta.version ?? ""))) report.errors.push(`theme.json: "version" 1.0.0 biçiminde olmalı.`);
|
|
161
|
+
if (!String(meta.description ?? "").trim()) report.warnings.push(`theme.json: "description" boş; tema listesinde açıklama görünmez.`);
|
|
162
|
+
return key;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checkTemplate(index, types, report, source) {
|
|
166
|
+
if (!index) return;
|
|
167
|
+
if (!Array.isArray(index.order) || index.order.length === 0) report.errors.push(`templates/index.json: "order" boş; ana sayfada bölüm görünmez.`);
|
|
168
|
+
for (const id of index.order ?? []) {
|
|
169
|
+
const section = index.sections?.[id];
|
|
170
|
+
if (!section) report.errors.push(`templates/index.json: "order" içindeki "${id}" bölümü "sections" altında tanımlı değil.`);
|
|
171
|
+
else if (types && !types.has(section.type)) report.errors.push(`templates/index.json: "${id}" bölümünün türü (${section.type}) ${source} yok.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function checkImages(data, assets, report) {
|
|
176
|
+
if (!data || typeof data !== "object") return;
|
|
177
|
+
for (const [name, value] of Object.entries(data)) {
|
|
178
|
+
if (typeof value !== "string" || !/^uploads\/[^\s]+\.(png|jpe?g|webp|avif|gif|svg|ico)$/i.test(value)) continue;
|
|
179
|
+
if (!existsSync(path.join(assets, value))) report.errors.push(`config/settings_data.json: "${name}" ayarındaki görsel bulunamadı (assets/${value}).`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── paket ──────────────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function checkPackage(dir, meta, report) {
|
|
186
|
+
const base = String(meta?.base ?? "").trim();
|
|
187
|
+
if (meta && !keyPattern.test(base)) report.errors.push(`theme.json: "base" gerekli: paketin temel aldığı katalog teması (ör. "linea"). Temalar: https://themes.uvako.com`);
|
|
188
|
+
if (meta && (Number(meta.price ?? 0) < 0 || Number(meta.price ?? 0) > 100000 || Number.isNaN(Number(meta.price ?? 0)))) report.errors.push(`theme.json: "price" 0 ile 100.000 arasında olmalı.`);
|
|
189
|
+
if (meta?.demo_url && !/^https:\/\/[^\s]+$/.test(String(meta.demo_url))) report.errors.push(`theme.json: "demo_url" https:// ile başlayan bir adres olmalı.`);
|
|
190
|
+
|
|
191
|
+
const { files, links } = walk(dir);
|
|
192
|
+
for (const link of links) report.errors.push(`${link}: sembolik bağ pakete alınamaz.`);
|
|
193
|
+
const styles = new Set(files.filter((file) => file.rel.startsWith("styles/")).map((file) => file.rel.slice("styles/".length)));
|
|
194
|
+
const totals = { files: 0, bytes: 0, style: 0 };
|
|
195
|
+
let schema = null;
|
|
196
|
+
let data = null;
|
|
197
|
+
let index = null;
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
const rel = file.rel;
|
|
200
|
+
if (localOnly(rel)) continue;
|
|
201
|
+
totals.files += 1;
|
|
202
|
+
totals.bytes += file.size;
|
|
203
|
+
const parts = rel.split("/");
|
|
204
|
+
if (parts.some((part) => !segmentPattern.test(part))) {
|
|
205
|
+
report.errors.push(`${rel}: dosya adında yalnızca harf, rakam, nokta, tire ve alt çizgi olabilir; gizli dosya olamaz.`);
|
|
206
|
+
continue;
|
|
94
207
|
}
|
|
95
|
-
|
|
208
|
+
const ext = path.extname(rel).toLowerCase();
|
|
209
|
+
const text = () => readFileSync(file.full, "utf8");
|
|
210
|
+
if (rel === "theme.json") scanText(rel, text(), report);
|
|
211
|
+
else if (rel === "config/settings_schema.json" || rel === "config/settings_data.json" || rel === "templates/index.json") {
|
|
212
|
+
if (file.size > 1024 * 1024) report.errors.push(`${rel}: 1 MB sınırını aşıyor.`);
|
|
213
|
+
else scanText(rel, text(), report);
|
|
214
|
+
} else if (parts[0] === "styles" && parts.length === 2 && stylePattern.test(parts[1])) {
|
|
215
|
+
totals.style += file.size;
|
|
216
|
+
if (file.size <= limits.styleBytes) {
|
|
217
|
+
const source = text();
|
|
218
|
+
scanText(rel, source, report);
|
|
219
|
+
scanStyle(rel, source, report, { strict: true, styles, tokens: parts[1] === "_tokens.scss" });
|
|
220
|
+
}
|
|
221
|
+
} else if (parts[0] === "assets" && parts.length >= 2) {
|
|
222
|
+
if (!assetExtensions.has(ext)) {
|
|
223
|
+
report.errors.push(`${rel}: bu dosya türü (${ext || "uzantısız"}) temada bulunamaz.`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (file.size > limits.fileBytes) report.errors.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB; tek dosya sınırı ${limits.fileBytes / 1024 / 1024} MB.`);
|
|
227
|
+
else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.`);
|
|
228
|
+
if (ext === ".svg" && file.size <= limits.fileBytes) scanText(rel, text(), report);
|
|
229
|
+
} else {
|
|
230
|
+
report.errors.push(`${rel}: bu dosya pakette bulunamaz. İzin verilenler: theme.json, config/settings_schema.json, config/settings_data.json, templates/index.json, styles/*.scss, assets/**.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (totals.style > limits.styleBytes) report.errors.push(`Stil dosyaları toplam ${limits.styleBytes / 1024} KB sınırını aşıyor.`);
|
|
234
|
+
if (styles.size > 0 && !styles.has("theme.scss") && !(styles.size === 1 && styles.has("_tokens.scss"))) report.errors.push("styles/theme.scss yok: stil dosyaları oradan içe aktarılır.");
|
|
235
|
+
|
|
236
|
+
schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json", false);
|
|
237
|
+
if (schema) {
|
|
238
|
+
if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values) || schema.group.values.length === 0) report.errors.push(`config/settings_schema.json: "group" sekmeli olmalı ({"type":"tab","values":[…]}).`);
|
|
239
|
+
schemaKeys(schema.group, new Set(), report);
|
|
240
|
+
}
|
|
241
|
+
data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json", false);
|
|
242
|
+
if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push("config/settings_data.json: anahtar-değer nesnesi olmalı.");
|
|
243
|
+
index = readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json", false);
|
|
244
|
+
// Bölüm türleri temel temadan gelir; `theme init` bunları reference/sections.json dosyasına yazar.
|
|
245
|
+
const reference = existsSync(path.join(dir, "reference", "sections.json")) ? readJson(path.join(dir, "reference", "sections.json"), { errors: [] }, "") : null;
|
|
246
|
+
checkTemplate(index, reference?.sections ? new Set(Object.keys(reference.sections)) : null, report, `${base} temasında`);
|
|
247
|
+
checkImages(data, path.join(dir, "assets"), report);
|
|
248
|
+
for (const shot of [meta?.screenshot, ...(Array.isArray(meta?.screenshots) ? meta.screenshots : [])]) {
|
|
249
|
+
const rel = String(shot ?? "").trim().replace(/^\//, "").replace(/^assets\//, "");
|
|
250
|
+
if (rel && !existsSync(path.join(dir, "assets", rel))) report.errors.push(`theme.json: ekran görüntüsü pakette yok (assets/${rel}).`);
|
|
96
251
|
}
|
|
252
|
+
report.files = totals.files;
|
|
253
|
+
report.bytes = totals.bytes;
|
|
254
|
+
if (totals.bytes > limits.totalBytes) report.errors.push(`Tema ${(totals.bytes / 1024 / 1024).toFixed(0)} MB; sınır ${limits.totalBytes / 1024 / 1024} MB.`);
|
|
255
|
+
if (totals.files > limits.files) report.errors.push(`Tema ${totals.files} dosya içeriyor; sınır ${limits.files}.`);
|
|
97
256
|
}
|
|
98
257
|
|
|
99
|
-
|
|
100
|
-
|
|
258
|
+
// ── platform deposundaki katalog teması ────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
function scanRepoFiles(root, label, report, totals) {
|
|
261
|
+
if (!existsSync(root)) return;
|
|
101
262
|
const { files, links } = walk(root);
|
|
102
263
|
for (const link of links) report.errors.push(`${label}/${link}: sembolik bağ pakete alınamaz.`);
|
|
103
264
|
for (const file of files) {
|
|
@@ -106,7 +267,7 @@ function scanFiles(root, label, report, totals) {
|
|
|
106
267
|
totals.files += 1;
|
|
107
268
|
totals.bytes += file.size;
|
|
108
269
|
if (file.rel.split("/").some((part) => part === ".." || part.startsWith("."))) report.errors.push(`${rel}: gizli dosya ya da üst klasöre çıkan yol.`);
|
|
109
|
-
if (!
|
|
270
|
+
if (!repoExtensions.has(ext)) {
|
|
110
271
|
report.errors.push(`${rel}: bu dosya türü (${ext || "uzantısız"}) temada bulunamaz.`);
|
|
111
272
|
continue;
|
|
112
273
|
}
|
|
@@ -114,92 +275,60 @@ function scanFiles(root, label, report, totals) {
|
|
|
114
275
|
else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.`);
|
|
115
276
|
if ([".json", ".scss", ".css", ".svg", ".md", ".txt"].includes(ext) && file.size <= limits.fileBytes) {
|
|
116
277
|
const text = readFileSync(file.full, "utf8");
|
|
117
|
-
// Ayar verisindeki HTML alanları (alt bilgi menüsü gibi) platformda temizlenir; betik yine de reddedilir.
|
|
118
278
|
scanText(rel, text, report);
|
|
119
279
|
if (ext === ".scss" || ext === ".css") scanStyle(rel, text, report);
|
|
120
280
|
}
|
|
121
281
|
}
|
|
122
|
-
return files;
|
|
123
282
|
}
|
|
124
283
|
|
|
125
|
-
|
|
126
|
-
const report = { dir, key: "", layout: "", errors: [], warnings: [], files: 0, bytes: 0 };
|
|
127
|
-
if (!existsSync(path.join(dir, "theme.json"))) {
|
|
128
|
-
report.errors.push(`${dir} bir tema klasörü değil: theme.json bulunamadı.`);
|
|
129
|
-
return report;
|
|
130
|
-
}
|
|
131
|
-
const where = locate(dir);
|
|
132
|
-
report.layout = where.layout;
|
|
133
|
-
|
|
134
|
-
// ── kimlik ───────────────────────────────────────────────────────────
|
|
135
|
-
const meta = readJson(path.join(dir, "theme.json"), report, "theme.json");
|
|
136
|
-
const key = typeof meta?.key === "string" ? meta.key.trim() : "";
|
|
137
|
-
report.key = key;
|
|
138
|
-
if (meta) {
|
|
139
|
-
if (!keyPattern.test(key) || key.length > 40) report.errors.push(`theme.json: "key" küçük harf, rakam ve tireden oluşmalı (ör. "linea").`);
|
|
140
|
-
else if (key !== path.basename(dir)) report.errors.push(`theme.json: "key" (${key}) klasör adıyla (${path.basename(dir)}) aynı olmalı.`);
|
|
141
|
-
if (!String(meta.name ?? "").trim()) report.errors.push(`theme.json: "name" gerekli.`);
|
|
142
|
-
if (!versionPattern.test(String(meta.version ?? ""))) report.errors.push(`theme.json: "version" 1.0.0 biçiminde olmalı.`);
|
|
143
|
-
if (!String(meta.description ?? "").trim()) report.warnings.push(`theme.json: "description" boş; tema listesinde açıklama görünmez.`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ── ayar şeması ve değerler ──────────────────────────────────────────
|
|
284
|
+
function checkRepo(dir, meta, key, where, report) {
|
|
147
285
|
const schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json");
|
|
148
|
-
const known = new Set();
|
|
149
286
|
if (schema) {
|
|
150
287
|
if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values)) report.errors.push(`config/settings_schema.json: "group" sekmeli olmalı ({"type":"tab","values":[…]}).`);
|
|
151
|
-
schemaKeys(schema.group,
|
|
288
|
+
schemaKeys(schema.group, new Set(), report);
|
|
152
289
|
}
|
|
153
290
|
const data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json");
|
|
154
291
|
if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push("config/settings_data.json: anahtar-değer nesnesi olmalı.");
|
|
155
292
|
|
|
156
|
-
// ── bölümler ve ana sayfa şablonu ────────────────────────────────────
|
|
157
293
|
const sections = readJson(path.join(dir, "config", "sections.json"), report, "config/sections.json");
|
|
158
294
|
const types = new Set(Object.keys(sections?.sections ?? {}));
|
|
159
295
|
if (sections && types.size === 0) report.errors.push(`config/sections.json: "sections" altında en az bir bölüm türü olmalı.`);
|
|
160
296
|
for (const [type, section] of Object.entries(sections?.sections ?? {})) {
|
|
161
297
|
if (key && !type.startsWith(`${key}-`)) report.warnings.push(`config/sections.json: "${type}" bölüm türü tema anahtarıyla ("${key}-") başlamıyor.`);
|
|
162
298
|
if (!String(section?.name ?? "").trim()) report.errors.push(`config/sections.json: "${type}" bölümünün adı yok.`);
|
|
163
|
-
// "keys" isteğe bağlıdır
|
|
299
|
+
// "keys" isteğe bağlıdır; verilmişse liste olmalı.
|
|
164
300
|
if (section?.keys !== undefined && !Array.isArray(section.keys)) report.errors.push(`config/sections.json: "${type}" bölümünün "keys" alanı liste olmalı.`);
|
|
165
301
|
}
|
|
166
|
-
|
|
167
|
-
if (index) {
|
|
168
|
-
if (!Array.isArray(index.order) || index.order.length === 0) report.errors.push(`templates/index.json: "order" boş; ana sayfada bölüm görünmez.`);
|
|
169
|
-
for (const id of index.order ?? []) {
|
|
170
|
-
const section = index.sections?.[id];
|
|
171
|
-
if (!section) report.errors.push(`templates/index.json: "order" içindeki "${id}" bölümü "sections" altında tanımlı değil.`);
|
|
172
|
-
else if (sections && !types.has(section.type)) report.errors.push(`templates/index.json: "${id}" bölümünün türü (${section.type}) config/sections.json içinde yok.`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
302
|
+
checkTemplate(readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json (ana sayfa şablonu)"), sections ? types : null, report, "config/sections.json içinde");
|
|
175
303
|
|
|
176
|
-
// ── dosyalar: tür, boyut, güvenlik ───────────────────────────────────
|
|
177
304
|
const totals = { files: 0, bytes: 0 };
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
for (const sub of ["config", "templates"]) scanFiles(path.join(dir, sub), sub, report, totals);
|
|
183
|
-
scanFiles(where.styles, "styles", report, totals);
|
|
184
|
-
scanFiles(where.assets, "assets", report, totals);
|
|
185
|
-
if (!existsSync(path.join(where.styles, "theme.scss"))) report.errors.push(`Stil dosyası yok: ${path.join(where.styles, "theme.scss")}`);
|
|
186
|
-
}
|
|
305
|
+
for (const sub of ["config", "templates"]) scanRepoFiles(path.join(dir, sub), sub, report, totals);
|
|
306
|
+
scanRepoFiles(where.styles, "styles", report, totals);
|
|
307
|
+
scanRepoFiles(where.assets, "assets", report, totals);
|
|
308
|
+
if (!existsSync(path.join(where.styles, "theme.scss"))) report.errors.push(`Stil dosyası yok: ${path.join(where.styles, "theme.scss")}`);
|
|
187
309
|
report.files = totals.files;
|
|
188
310
|
report.bytes = totals.bytes;
|
|
189
311
|
if (totals.bytes > limits.totalBytes) report.errors.push(`Tema ${(totals.bytes / 1024 / 1024).toFixed(0)} MB; sınır ${limits.totalBytes / 1024 / 1024} MB.`);
|
|
190
312
|
if (totals.files > limits.files) report.errors.push(`Tema ${totals.files} dosya içeriyor; sınır ${limits.files}.`);
|
|
191
|
-
|
|
192
|
-
// ── ayarlardaki görseller yerinde mi ─────────────────────────────────
|
|
193
|
-
if (data && typeof data === "object") {
|
|
194
|
-
for (const [name, value] of Object.entries(data)) {
|
|
195
|
-
if (typeof value !== "string" || !/^uploads\/[^\s]+\.(png|jpe?g|webp|avif|gif|svg|ico)$/i.test(value)) continue;
|
|
196
|
-
if (!existsSync(path.join(where.assets, value))) report.errors.push(`config/settings_data.json: "${name}" ayarındaki görsel bulunamadı (assets/${value}).`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
313
|
+
checkImages(data, where.assets, report);
|
|
199
314
|
if (meta?.screenshot) {
|
|
200
315
|
const shot = String(meta.screenshot).replace(new RegExp(`^/themes/${key}/assets/`), "");
|
|
201
316
|
if (!existsSync(path.join(where.assets, shot))) report.warnings.push(`theme.json: ekran görüntüsü bulunamadı (assets/${shot}).`);
|
|
202
317
|
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function checkTheme(dir) {
|
|
321
|
+
const report = { dir, key: "", layout: "", errors: [], warnings: [], files: 0, bytes: 0 };
|
|
322
|
+
if (!existsSync(path.join(dir, "theme.json"))) {
|
|
323
|
+
report.errors.push(`${dir} bir tema klasörü değil: theme.json bulunamadı.`);
|
|
324
|
+
return report;
|
|
325
|
+
}
|
|
326
|
+
const where = locate(dir);
|
|
327
|
+
report.layout = where.layout;
|
|
328
|
+
const meta = readJson(path.join(dir, "theme.json"), report, "theme.json");
|
|
329
|
+
const key = checkIdentity(dir, meta, report);
|
|
330
|
+
if (where.layout === "package") checkPackage(dir, meta, report);
|
|
331
|
+
else checkRepo(dir, meta, key, where, report);
|
|
203
332
|
return report;
|
|
204
333
|
}
|
|
205
334
|
|