@blocofy/cli 0.1.4 → 0.1.6
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/bin/blocofy.mjs +28 -1
- package/lib/dev-server.mjs +81 -20
- package/lib/local-theme.mjs +1 -1
- package/package.json +1 -1
package/bin/blocofy.mjs
CHANGED
|
@@ -14,11 +14,12 @@ import { createInterface } from "node:readline/promises";
|
|
|
14
14
|
|
|
15
15
|
import { credentialsPath, loadCredentials, saveCredentials } from "../lib/credentials.mjs";
|
|
16
16
|
import { startDevServer } from "../lib/dev-server.mjs";
|
|
17
|
+
import { readLocalTemplates } from "../lib/local-theme.mjs";
|
|
17
18
|
import { fetchDevSession, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
|
|
18
19
|
import { hyperlink, openUrl } from "../lib/term.mjs";
|
|
19
20
|
import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
|
|
20
21
|
|
|
21
|
-
const VERSION = "0.1.
|
|
22
|
+
const VERSION = "0.1.6";
|
|
22
23
|
const args = process.argv.slice(2);
|
|
23
24
|
|
|
24
25
|
/** Parse `--key value` and `--flag` (boolean) arguments. */
|
|
@@ -216,6 +217,20 @@ async function themeDev(rest) {
|
|
|
216
217
|
console.log(`\n Press: ${keys}`);
|
|
217
218
|
console.log(` Edit a theme file and save — every open view reloads automatically.\n`);
|
|
218
219
|
|
|
220
|
+
// Tanılama: kaç tema dosyası izleniyor? Boşsa hot-reload mümkün değil — yanlış
|
|
221
|
+
// dizinde ya da `theme pull` yapılmamış demektir; sebebini açıkça söyle.
|
|
222
|
+
const localFiles = readLocalTemplates(themeDir);
|
|
223
|
+
const fileCount = Object.keys(localFiles).length;
|
|
224
|
+
if (fileCount === 0) {
|
|
225
|
+
console.warn(
|
|
226
|
+
` ⚠ No theme files found under ${themeDir}\n` +
|
|
227
|
+
` Expected top-level folders: layout/ section/ partial/ asset/ block/ template/\n` +
|
|
228
|
+
` Run this from your theme root, or fetch it first: blocofy theme pull\n`,
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
console.log(` Watching ${themeDir} — ${fileCount} theme files\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
if (flags.dry) {
|
|
220
235
|
console.log("(--dry: server not started)");
|
|
221
236
|
return;
|
|
@@ -227,6 +242,18 @@ async function themeDev(rest) {
|
|
|
227
242
|
token: creds.token,
|
|
228
243
|
port,
|
|
229
244
|
syncDraft: Boolean(session),
|
|
245
|
+
// Her kaydetmede ne olduğunu bas — "reloaded" = watch tetiklendi; "0 views"
|
|
246
|
+
// = hiçbir tarayıcı sekmesi bağlı değil (yanlış görünüme bakıyorsun); sync
|
|
247
|
+
// hatası = draft güncellenemedi (preview/editör eski kalır, local yine yenilenir).
|
|
248
|
+
onReload: ({ file, synced, clients, error }) => {
|
|
249
|
+
const what = file || "change";
|
|
250
|
+
if (error) {
|
|
251
|
+
console.error(` ↻ ${what} — draft sync failed: ${error} (local view still reloaded)`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const views = clients ? `${clients} view${clients === 1 ? "" : "s"}` : "no views connected";
|
|
255
|
+
console.log(` ↻ ${what} → ${synced ? "synced + " : ""}reloaded (${views})`);
|
|
256
|
+
},
|
|
230
257
|
onError: (err) => {
|
|
231
258
|
if (err && err.code === "EADDRINUSE") {
|
|
232
259
|
console.error(`Port ${port} is in use. Try a different port: blocofy theme dev --port <n>`);
|
package/lib/dev-server.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
-
import { existsSync, readFileSync,
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { join, normalize, sep } from "node:path";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
LIVERELOAD_PATH,
|
|
7
|
+
THEME_DIRS,
|
|
7
8
|
contentTypeFor,
|
|
8
9
|
injectLivereload,
|
|
9
10
|
readLocalTemplates,
|
|
@@ -19,7 +20,7 @@ import { pushTheme } from "./theme-sync.mjs";
|
|
|
19
20
|
* separate request); direct `/asset/*` references are served from disk. Images/JS
|
|
20
21
|
* are remote (Directus/CDN), so no local serving is needed for them.
|
|
21
22
|
*/
|
|
22
|
-
export function startDevServer({ dir, url, token, port, syncDraft = false, onError }) {
|
|
23
|
+
export function startDevServer({ dir, url, token, port, syncDraft = false, onError, onReload }) {
|
|
23
24
|
const base = url.replace(/\/+$/, "");
|
|
24
25
|
const clients = new Set();
|
|
25
26
|
|
|
@@ -90,16 +91,19 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
90
91
|
for (const client of clients) client.write("data: reload\n\n");
|
|
91
92
|
};
|
|
92
93
|
// Değişimde: önce taslağa senkronla (canlı-domain önizleme + editör güncel olsun),
|
|
93
|
-
// SONRA reload yayınla → tüm görünümler en güncel taslağı çeker.
|
|
94
|
-
|
|
94
|
+
// SONRA reload yayınla → tüm görünümler en güncel taslağı çeker. `onReload` log için:
|
|
95
|
+
// hangi dosya, senkron oldu mu, kaç görünüm bağlı (clients.size = açık EventSource'lar).
|
|
96
|
+
const watcher = startWatch(dir, async (file) => {
|
|
97
|
+
let error = null;
|
|
95
98
|
if (syncDraft) {
|
|
96
99
|
try {
|
|
97
100
|
await pushTheme({ dir, url, token, draft: true });
|
|
98
|
-
} catch {
|
|
99
|
-
|
|
101
|
+
} catch (err) {
|
|
102
|
+
error = err?.message ?? String(err); // senkron hatası reload'u engellemesin
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
broadcast();
|
|
106
|
+
onReload?.({ file, synced: syncDraft && !error, clients: clients.size, error });
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
// Açılışta taslağı bir kez doldur → uzak görünümler hemen güncel local'i gösterir.
|
|
@@ -124,25 +128,82 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
131
|
+
* Recursively snapshot theme-file mtimes → Map(relPath → mtimeMs). Only descends
|
|
132
|
+
* into THEME_DIRS at the top level, so node_modules / .git / build tooling don't
|
|
133
|
+
* bloat the scan. Used by the polling watcher.
|
|
134
|
+
*/
|
|
135
|
+
function snapshot(dir) {
|
|
136
|
+
const map = new Map();
|
|
137
|
+
const walk = (current, rel) => {
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (!rel && !THEME_DIRS.has(entry.name)) continue; // top level: theme dirs only
|
|
146
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
147
|
+
const full = join(current, entry.name);
|
|
148
|
+
if (entry.isDirectory()) {
|
|
149
|
+
walk(full, childRel);
|
|
150
|
+
} else {
|
|
151
|
+
try {
|
|
152
|
+
map.set(childRel, statSync(full).mtimeMs);
|
|
153
|
+
} catch {
|
|
154
|
+
/* dosya kayboldu (atomik kaydetme yarış) — sonraki tarama yakalar */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
walk(dir, "");
|
|
160
|
+
return map;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Watch `dir` by polling theme-file mtimes (default 300ms). fs.watch yerine polling:
|
|
165
|
+
* editörün kaydetme şekli (yerinde/atomik), dosya sistemi ve `recursive` desteği
|
|
166
|
+
* olmayan Node build'lerinden bağımsız çalışır — kaybolan event yok. Değişen/eklenen/
|
|
167
|
+
* silinen ilk dosyanın yolunu `onChange`'e geçirir (debounce'lu). `.close()` döner.
|
|
130
168
|
*/
|
|
131
|
-
function startWatch(dir, onChange) {
|
|
169
|
+
function startWatch(dir, onChange, intervalMs = 300) {
|
|
170
|
+
let prev = snapshot(dir);
|
|
132
171
|
let timer = null;
|
|
133
|
-
|
|
172
|
+
let lastFile = null;
|
|
173
|
+
const fire = (file) => {
|
|
174
|
+
if (file) lastFile = file;
|
|
134
175
|
if (timer) clearTimeout(timer);
|
|
135
|
-
timer = setTimeout(onChange,
|
|
176
|
+
timer = setTimeout(() => onChange(lastFile), 80);
|
|
136
177
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
const tick = () => {
|
|
179
|
+
const cur = snapshot(dir);
|
|
180
|
+
let changed = null;
|
|
181
|
+
for (const [key, mtime] of cur) {
|
|
182
|
+
const before = prev.get(key);
|
|
183
|
+
if (before === undefined || before !== mtime) {
|
|
184
|
+
changed = key;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
144
187
|
}
|
|
145
|
-
|
|
188
|
+
if (!changed) {
|
|
189
|
+
for (const key of prev.keys()) {
|
|
190
|
+
if (!cur.has(key)) {
|
|
191
|
+
changed = key;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
prev = cur;
|
|
197
|
+
if (changed) fire(changed);
|
|
198
|
+
};
|
|
199
|
+
const interval = setInterval(tick, intervalMs);
|
|
200
|
+
if (interval.unref) interval.unref();
|
|
201
|
+
return {
|
|
202
|
+
close() {
|
|
203
|
+
clearInterval(interval);
|
|
204
|
+
if (timer) clearTimeout(timer);
|
|
205
|
+
},
|
|
206
|
+
};
|
|
146
207
|
}
|
|
147
208
|
|
|
148
209
|
/** Map a `/asset/*` path to a disk file (path-traversal safe); null if missing. */
|
package/lib/local-theme.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { extname, join, relative, sep } from "node:path";
|
|
|
7
7
|
* THEME_DIRS, keyed by relative path with the `.liquid` extension stripped.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const THEME_DIRS = new Set(["layout", "section", "block", "partial", "asset", "template"]);
|
|
10
|
+
export const THEME_DIRS = new Set(["layout", "section", "block", "partial", "asset", "template"]);
|
|
11
11
|
const LIQUID_KINDS = new Set(["layout", "section", "block", "partial", "template"]);
|
|
12
12
|
|
|
13
13
|
/**
|