@blocofy/cli 0.1.0 → 0.1.1
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 +40 -23
- package/bin/blocofy.mjs +61 -117
- package/lib/credentials.mjs +10 -10
- package/lib/dev-server.mjs +19 -18
- package/lib/local-theme.mjs +8 -9
- package/lib/theme-sync.mjs +5 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,48 +1,65 @@
|
|
|
1
1
|
# @blocofy/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
(npm/Vite/Tailwind)
|
|
3
|
+
Develop your [Blocofy](https://blocofy.com) theme locally against **live data**, see an
|
|
4
|
+
instant preview, and publish. The CLI does **not** build assets — generate them with your
|
|
5
|
+
own tools (npm/Vite/Tailwind); the platform serves plain Liquid + static assets.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @blocofy/cli login # site URL + dev token (admin
|
|
9
|
-
cd path/to/
|
|
10
|
-
npx @blocofy/cli theme dev # http://localhost:3030 — local
|
|
8
|
+
npx @blocofy/cli login # site URL + dev token (admin panel: Settings → Theme CLI tokens)
|
|
9
|
+
cd path/to/theme
|
|
10
|
+
npx @blocofy/cli theme dev # http://localhost:3030 — local theme + live data, livereload
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Commands
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
blocofy login [--url <url>] [--token <bcf_…>]
|
|
17
|
-
#
|
|
18
|
-
blocofy theme dev [dir] # Local dev
|
|
19
|
-
# --port <n> (
|
|
20
|
-
blocofy theme pull [dir] #
|
|
21
|
-
blocofy theme push [dir] #
|
|
17
|
+
# Save your platform URL + dev token (~/.blocofy/credentials.json, 0600).
|
|
18
|
+
blocofy theme dev [dir] # Local dev server: edit a file and the browser reloads automatically.
|
|
19
|
+
# --port <n> (default 3030)
|
|
20
|
+
blocofy theme pull [dir] # Download the live theme to disk.
|
|
21
|
+
blocofy theme push [dir] # Write the local theme to the live site (create/update; no delete).
|
|
22
22
|
blocofy --version
|
|
23
23
|
blocofy --help
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## How it works
|
|
27
27
|
|
|
28
|
-
`theme dev`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
`theme dev` starts a local HTTP server. For each page request it reads your local theme
|
|
29
|
+
files and sends them to the platform's dev-render endpoint (`/api/dev/render`). The platform
|
|
30
|
+
renders them with the site's **live data** and returns HTML — so the CLI ships no rendering
|
|
31
|
+
engine and you see exactly the production output. A file watcher reloads the browser over
|
|
32
|
+
Server-Sent Events when you save.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
Credentials come from `~/.blocofy/credentials.json` (written by `blocofy login`) or the
|
|
35
|
+
`BLOCOFY_URL` + `BLOCOFY_TOKEN` environment variables (for CI/automation).
|
|
35
36
|
|
|
36
|
-
##
|
|
37
|
+
## Theme structure
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
A theme is a directory of files grouped by top-level folder:
|
|
40
|
+
|
|
41
|
+
| Folder | Contents |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `layout/` | Page shell (`theme.liquid`) |
|
|
44
|
+
| `section/` | Page sections (`Hero.liquid`, `FeaturedCards.liquid`, …) |
|
|
45
|
+
| `block/` | Repeatable pieces used inside sections |
|
|
46
|
+
| `partial/` | Shared snippets (header/footer, …) |
|
|
47
|
+
| `asset/` | CSS and static files (`theme.css`) |
|
|
48
|
+
| `pages/<slug>.json` | Page content (`/` → `pages/index.json`) |
|
|
49
|
+
| `config/settings.json` | Theme settings + color schemes |
|
|
50
|
+
|
|
51
|
+
Liquid templates use the `.liquid` extension; files under `asset/` are served as-is.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
Zero runtime dependencies (Node built-ins only). Run the tests with:
|
|
39
56
|
|
|
40
57
|
```bash
|
|
41
58
|
node --test
|
|
42
59
|
```
|
|
43
60
|
|
|
44
|
-
Node ≥ 18
|
|
61
|
+
Requires Node ≥ 18.
|
|
45
62
|
|
|
46
|
-
##
|
|
63
|
+
## License
|
|
47
64
|
|
|
48
65
|
[MIT](./LICENSE)
|
package/bin/blocofy.mjs
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Blocofy
|
|
4
|
-
* preview,
|
|
3
|
+
* Blocofy theme CLI. Develop your theme locally against live data with instant
|
|
4
|
+
* preview, then publish.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* livereload. Monorepo GEREKMEZ → external müşteri `npx @blocofy/cli` ile.
|
|
11
|
-
* - Monorepo (Faz 2 v1): login yoksa, monorepo içindeyse renderer'ı THEME_DEV_DIR
|
|
12
|
-
* ile spawn eder (fallback).
|
|
6
|
+
* `blocofy login` stores your platform URL + dev token, then `blocofy theme dev`
|
|
7
|
+
* starts a local server that proxies each request to the platform's
|
|
8
|
+
* `/api/dev/render` endpoint (local theme files + live data) with file-watch
|
|
9
|
+
* livereload. No monorepo required.
|
|
13
10
|
*/
|
|
14
|
-
import { spawn } from "node:child_process";
|
|
15
11
|
import { existsSync } from "node:fs";
|
|
16
|
-
import {
|
|
12
|
+
import { resolve } from "node:path";
|
|
17
13
|
import { createInterface } from "node:readline/promises";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
14
|
|
|
20
15
|
import { credentialsPath, loadCredentials, saveCredentials } from "../lib/credentials.mjs";
|
|
21
16
|
import { startDevServer } from "../lib/dev-server.mjs";
|
|
22
17
|
import { pullTheme, pushTheme } from "../lib/theme-sync.mjs";
|
|
23
18
|
|
|
24
|
-
const VERSION = "0.1.
|
|
19
|
+
const VERSION = "0.1.1";
|
|
25
20
|
const args = process.argv.slice(2);
|
|
26
21
|
|
|
27
|
-
/** `--key value`
|
|
22
|
+
/** Parse `--key value` and `--flag` (boolean) arguments. */
|
|
28
23
|
function parseFlags(rest) {
|
|
29
24
|
const flags = {};
|
|
30
25
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -42,36 +37,23 @@ function parseFlags(rest) {
|
|
|
42
37
|
return flags;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
function findMonorepoRoot(start) {
|
|
46
|
-
let dir = start;
|
|
47
|
-
for (let i = 0; i < 8; i++) {
|
|
48
|
-
if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) return dir;
|
|
49
|
-
const parent = resolve(dir, "..");
|
|
50
|
-
if (parent === dir) break;
|
|
51
|
-
dir = parent;
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
40
|
function printHelp() {
|
|
57
|
-
console.log(`blocofy — Blocofy
|
|
41
|
+
console.log(`blocofy — Blocofy theme development CLI (v${VERSION})
|
|
58
42
|
|
|
59
|
-
|
|
43
|
+
Usage:
|
|
60
44
|
blocofy login [--url <url>] [--token <bcf_…>]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
blocofy theme dev [dir] Local
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
blocofy theme
|
|
68
|
-
blocofy theme push [dir] Local temayı canlı siteye yaz (create/update; v1 silme yok).
|
|
45
|
+
Save your platform URL + dev token (~/.blocofy/credentials.json).
|
|
46
|
+
Get a token from your admin panel: Settings → Theme CLI tokens.
|
|
47
|
+
blocofy theme dev [dir] Local dev server — local theme + live data with instant preview.
|
|
48
|
+
Edit a file and the browser reloads automatically. dir defaults to cwd.
|
|
49
|
+
--port <n> (default 3030)
|
|
50
|
+
blocofy theme pull [dir] Download the live theme to disk. dir defaults to cwd.
|
|
51
|
+
blocofy theme push [dir] Write the local theme to the live site (create/update; no delete).
|
|
69
52
|
blocofy --version
|
|
70
53
|
blocofy --help
|
|
71
54
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
platform Liquid + statik asset olarak kalır.`);
|
|
55
|
+
The CLI does not build assets — generate them with your own tools (npm/Vite/Tailwind);
|
|
56
|
+
the platform serves plain Liquid + static assets.`);
|
|
75
57
|
}
|
|
76
58
|
|
|
77
59
|
async function login(rest) {
|
|
@@ -82,7 +64,7 @@ async function login(rest) {
|
|
|
82
64
|
if (!url || !token) {
|
|
83
65
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
84
66
|
try {
|
|
85
|
-
if (!url) url = (await rl.question("Platform/site URL (
|
|
67
|
+
if (!url) url = (await rl.question("Platform/site URL (e.g. https://store.myblocofy.com): ")).trim();
|
|
86
68
|
if (!token) token = (await rl.question("Dev token (bcf_…): ")).trim();
|
|
87
69
|
} finally {
|
|
88
70
|
rl.close();
|
|
@@ -91,24 +73,24 @@ async function login(rest) {
|
|
|
91
73
|
|
|
92
74
|
url = url.replace(/\/+$/, "");
|
|
93
75
|
if (!/^https?:\/\//.test(url)) {
|
|
94
|
-
console.error("
|
|
76
|
+
console.error("Invalid URL — must start with http(s)://");
|
|
95
77
|
process.exit(1);
|
|
96
78
|
}
|
|
97
79
|
if (!token.startsWith("bcf_")) {
|
|
98
|
-
console.error("
|
|
80
|
+
console.error("Invalid token — must start with bcf_ (admin panel: Settings → Theme CLI tokens).");
|
|
99
81
|
process.exit(1);
|
|
100
82
|
}
|
|
101
83
|
|
|
102
84
|
saveCredentials({ url, token });
|
|
103
|
-
console.log(`
|
|
104
|
-
console.log(`
|
|
85
|
+
console.log(`Saved credentials: ${credentialsPath()}`);
|
|
86
|
+
console.log(`Now run, in your theme directory: blocofy theme dev`);
|
|
105
87
|
}
|
|
106
88
|
|
|
107
|
-
/**
|
|
89
|
+
/** Resolve credentials (URL + token) or exit with guidance. */
|
|
108
90
|
function requireCreds() {
|
|
109
91
|
const creds = loadCredentials();
|
|
110
92
|
if (!creds || !creds.url || !creds.token) {
|
|
111
|
-
console.error("
|
|
93
|
+
console.error("Login required: run `blocofy login` (or set BLOCOFY_URL + BLOCOFY_TOKEN).");
|
|
112
94
|
process.exit(1);
|
|
113
95
|
}
|
|
114
96
|
return creds;
|
|
@@ -119,22 +101,22 @@ async function themePull(rest) {
|
|
|
119
101
|
const dir = resolve(positional[0] ?? process.cwd());
|
|
120
102
|
const creds = requireCreds();
|
|
121
103
|
const { count } = await pullTheme({ dir, url: creds.url, token: creds.token });
|
|
122
|
-
console.log(
|
|
104
|
+
console.log(`Downloaded ${count} theme files → ${dir}`);
|
|
123
105
|
}
|
|
124
106
|
|
|
125
107
|
async function themePush(rest) {
|
|
126
108
|
const positional = rest.filter((a) => !a.startsWith("--"));
|
|
127
109
|
const dir = resolve(positional[0] ?? process.cwd());
|
|
128
110
|
if (!existsSync(dir)) {
|
|
129
|
-
console.error(`
|
|
111
|
+
console.error(`Theme directory not found: ${dir}`);
|
|
130
112
|
process.exit(1);
|
|
131
113
|
}
|
|
132
114
|
const creds = requireCreds();
|
|
133
115
|
const result = await pushTheme({ dir, url: creds.url, token: creds.token });
|
|
134
116
|
const extra = result.skippedDeletes
|
|
135
|
-
? `, ${result.skippedDeletes}
|
|
117
|
+
? `, ${result.skippedDeletes} remote file(s) absent locally (not deleted)`
|
|
136
118
|
: "";
|
|
137
|
-
console.log(`Push: ${result.created}
|
|
119
|
+
console.log(`Push: ${result.created} created, ${result.updated} updated${extra}.`);
|
|
138
120
|
}
|
|
139
121
|
|
|
140
122
|
function themeDev(rest) {
|
|
@@ -143,83 +125,45 @@ function themeDev(rest) {
|
|
|
143
125
|
const themeDir = resolve(positional[0] ?? process.cwd());
|
|
144
126
|
|
|
145
127
|
if (!existsSync(themeDir)) {
|
|
146
|
-
console.error(`
|
|
128
|
+
console.error(`Theme directory not found: ${themeDir}`);
|
|
147
129
|
process.exit(1);
|
|
148
130
|
}
|
|
149
131
|
|
|
150
|
-
const creds =
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
// Standalone: giriş yapıldıysa (ya da --standalone zorlanırsa).
|
|
154
|
-
if (canStandalone || flags.standalone) {
|
|
155
|
-
if (!canStandalone) {
|
|
156
|
-
console.error("Standalone için giriş gerekli: `blocofy login` (ya da BLOCOFY_URL + BLOCOFY_TOKEN).");
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
const port = Number(flags.port) || 3030;
|
|
160
|
-
console.log(`blocofy theme dev (standalone)`);
|
|
161
|
-
console.log(` tema dizini : ${themeDir}`);
|
|
162
|
-
console.log(` platform : ${creds.url}`);
|
|
163
|
-
console.log(` token : ${creds.token.slice(0, 8)}… (${creds.source})`);
|
|
164
|
-
console.log(` önizleme : http://localhost:${port}`);
|
|
165
|
-
console.log(` → tema dosyasını düzenle, kaydet = tarayıcı otomatik yenilenir (livereload)`);
|
|
166
|
-
|
|
167
|
-
if (flags.dry) {
|
|
168
|
-
console.log("(--dry: sunucu başlatılmadı)");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const handle = startDevServer({
|
|
173
|
-
dir: themeDir,
|
|
174
|
-
url: creds.url,
|
|
175
|
-
token: creds.token,
|
|
176
|
-
port,
|
|
177
|
-
onError: (err) => {
|
|
178
|
-
if (err && err.code === "EADDRINUSE") {
|
|
179
|
-
console.error(`Port ${port} kullanımda. Farklı port: blocofy theme dev --port <n>`);
|
|
180
|
-
} else {
|
|
181
|
-
console.error(`Sunucu hatası: ${err?.message ?? err}`);
|
|
182
|
-
}
|
|
183
|
-
process.exit(1);
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
const shutdown = () => {
|
|
187
|
-
handle.close();
|
|
188
|
-
process.exit(0);
|
|
189
|
-
};
|
|
190
|
-
process.on("SIGINT", shutdown);
|
|
191
|
-
process.on("SIGTERM", shutdown);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Monorepo fallback (Faz 2 v1): renderer'ı THEME_DEV_DIR ile spawn et.
|
|
196
|
-
const root = findMonorepoRoot(dirname(fileURLToPath(import.meta.url)));
|
|
197
|
-
if (!root) {
|
|
198
|
-
console.error(
|
|
199
|
-
"Ne giriş yapıldı ne de monorepo bulundu.\n" +
|
|
200
|
-
"External kullanım için: `blocofy login` (admin'den dev token al).\n" +
|
|
201
|
-
"Monorepo içinde çalışıyorsan pnpm-workspace.yaml görünür bir dizinden çalıştır.",
|
|
202
|
-
);
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
132
|
+
const creds = requireCreds();
|
|
133
|
+
const port = Number(flags.port) || 3030;
|
|
205
134
|
|
|
206
|
-
console.log(`blocofy theme dev
|
|
207
|
-
console.log(`
|
|
208
|
-
console.log(`
|
|
209
|
-
console.log(`
|
|
210
|
-
console.log(`
|
|
135
|
+
console.log(`blocofy theme dev`);
|
|
136
|
+
console.log(` theme dir : ${themeDir}`);
|
|
137
|
+
console.log(` platform : ${creds.url}`);
|
|
138
|
+
console.log(` token : ${creds.token.slice(0, 8)}… (${creds.source})`);
|
|
139
|
+
console.log(` preview : http://localhost:${port}`);
|
|
140
|
+
console.log(` → edit a theme file and save = the browser reloads automatically (livereload)`);
|
|
211
141
|
|
|
212
142
|
if (flags.dry) {
|
|
213
|
-
console.log("(--dry:
|
|
143
|
+
console.log("(--dry: server not started)");
|
|
214
144
|
return;
|
|
215
145
|
}
|
|
216
146
|
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
147
|
+
const handle = startDevServer({
|
|
148
|
+
dir: themeDir,
|
|
149
|
+
url: creds.url,
|
|
150
|
+
token: creds.token,
|
|
151
|
+
port,
|
|
152
|
+
onError: (err) => {
|
|
153
|
+
if (err && err.code === "EADDRINUSE") {
|
|
154
|
+
console.error(`Port ${port} is in use. Try a different port: blocofy theme dev --port <n>`);
|
|
155
|
+
} else {
|
|
156
|
+
console.error(`Server error: ${err?.message ?? err}`);
|
|
157
|
+
}
|
|
158
|
+
process.exit(1);
|
|
159
|
+
},
|
|
221
160
|
});
|
|
222
|
-
|
|
161
|
+
const shutdown = () => {
|
|
162
|
+
handle.close();
|
|
163
|
+
process.exit(0);
|
|
164
|
+
};
|
|
165
|
+
process.on("SIGINT", shutdown);
|
|
166
|
+
process.on("SIGTERM", shutdown);
|
|
223
167
|
}
|
|
224
168
|
|
|
225
169
|
const [first, ...rest] = args;
|
|
@@ -246,7 +190,7 @@ if (first === "--version" || first === "-v") {
|
|
|
246
190
|
process.exit(1);
|
|
247
191
|
});
|
|
248
192
|
} else {
|
|
249
|
-
console.error(`
|
|
193
|
+
console.error(`Unknown command: ${args.join(" ")}\n`);
|
|
250
194
|
printHelp();
|
|
251
195
|
process.exit(1);
|
|
252
196
|
}
|
package/lib/credentials.mjs
CHANGED
|
@@ -3,11 +3,11 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Blocofy CLI
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Blocofy CLI credentials. `blocofy login` writes the platform URL + dev token to
|
|
7
|
+
* `~/.blocofy/credentials.json` (0600); `blocofy theme dev` reads it. Environment
|
|
8
|
+
* variables (BLOCOFY_URL/BLOCOFY_TOKEN) take precedence over the file (CI/agents).
|
|
9
|
+
* The token is stored in plaintext on disk (the usual dev-CLI pattern), so the file
|
|
10
|
+
* is locked down to 0600.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const DIR = join(homedir(), ".blocofy");
|
|
@@ -17,16 +17,16 @@ export function credentialsPath() {
|
|
|
17
17
|
return FILE;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** URL + token
|
|
20
|
+
/** Write URL + token with 0600 permissions. */
|
|
21
21
|
export function saveCredentials({ url, token }) {
|
|
22
22
|
mkdirSync(DIR, { recursive: true, mode: 0o700 });
|
|
23
23
|
writeFileSync(FILE, JSON.stringify({ url, token }, null, 2) + "\n", { mode: 0o600 });
|
|
24
|
-
chmodSync(FILE, 0o600); //
|
|
24
|
+
chmodSync(FILE, 0o600); // create-mode is ignored if the file already exists → enforce it
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Resolve credentials: environment first (both vars), then the file. Returns
|
|
29
|
+
* `{ url, token, source }` or null. Trailing slashes are stripped from the URL.
|
|
30
30
|
*/
|
|
31
31
|
export function loadCredentials() {
|
|
32
32
|
const envUrl = process.env.BLOCOFY_URL;
|
|
@@ -40,7 +40,7 @@ export function loadCredentials() {
|
|
|
40
40
|
return { url: stripSlash(data.url), token: data.token, source: "file" };
|
|
41
41
|
}
|
|
42
42
|
} catch {
|
|
43
|
-
//
|
|
43
|
+
// missing/corrupt file → null
|
|
44
44
|
}
|
|
45
45
|
return null;
|
|
46
46
|
}
|
package/lib/dev-server.mjs
CHANGED
|
@@ -10,13 +10,13 @@ import {
|
|
|
10
10
|
} from "./local-theme.mjs";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Standalone dev
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Standalone dev server. For each page request it reads the local theme files and
|
|
14
|
+
* posts them to the platform's `/api/dev/render` endpoint, then injects a livereload
|
|
15
|
+
* script into the returned HTML. `fs.watch` reloads the browser over SSE on change.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Assets are usually inlined into the HTML via `{% render 'asset/x.css' %}` (no
|
|
18
|
+
* separate request); direct `/asset/*` references are served from disk. Images/JS
|
|
19
|
+
* are remote (Directus/CDN), so no local serving is needed for them.
|
|
20
20
|
*/
|
|
21
21
|
export function startDevServer({ dir, url, token, port, onError }) {
|
|
22
22
|
const base = url.replace(/\/+$/, "");
|
|
@@ -35,7 +35,7 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
35
35
|
try {
|
|
36
36
|
message = JSON.parse(text).error ?? text;
|
|
37
37
|
} catch {
|
|
38
|
-
/*
|
|
38
|
+
/* raw text */
|
|
39
39
|
}
|
|
40
40
|
return { status: res.status, html: errorHtml(res.status, message) };
|
|
41
41
|
}
|
|
@@ -50,7 +50,7 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
50
50
|
try {
|
|
51
51
|
const pathname = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
|
|
52
52
|
|
|
53
|
-
// Livereload SSE
|
|
53
|
+
// Livereload SSE channel.
|
|
54
54
|
if (pathname === LIVERELOAD_PATH) {
|
|
55
55
|
res.writeHead(200, {
|
|
56
56
|
"content-type": "text/event-stream",
|
|
@@ -63,7 +63,7 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// Direct /asset/* reference → serve from disk (if present).
|
|
67
67
|
const assetFile = safeAssetFile(dir, pathname);
|
|
68
68
|
if (assetFile) {
|
|
69
69
|
res.writeHead(200, { "content-type": contentTypeFor(assetFile), "cache-control": "no-store" });
|
|
@@ -71,7 +71,7 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// Page → render on the platform.
|
|
75
75
|
const { status, html } = await renderPath(pathname);
|
|
76
76
|
res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
77
77
|
res.end(injectLivereload(html));
|
|
@@ -95,7 +95,7 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
95
95
|
try {
|
|
96
96
|
watcher?.close();
|
|
97
97
|
} catch {
|
|
98
|
-
/*
|
|
98
|
+
/* ignore */
|
|
99
99
|
}
|
|
100
100
|
for (const client of clients) client.end();
|
|
101
101
|
server.close();
|
|
@@ -104,8 +104,9 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* `dir
|
|
108
|
-
*
|
|
107
|
+
* Watch `dir`; if recursive watching is unsupported (older Node/Linux), fall back to
|
|
108
|
+
* a non-recursive watch. Calls `onChange` debounced. Returns null if unwatchable
|
|
109
|
+
* (livereload disabled).
|
|
109
110
|
*/
|
|
110
111
|
function startWatch(dir, onChange) {
|
|
111
112
|
let timer = null;
|
|
@@ -124,7 +125,7 @@ function startWatch(dir, onChange) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
/** `/asset/*`
|
|
128
|
+
/** Map a `/asset/*` path to a disk file (path-traversal safe); null if missing. */
|
|
128
129
|
function safeAssetFile(dir, pathname) {
|
|
129
130
|
if (!pathname.startsWith("/asset/")) return null;
|
|
130
131
|
const root = normalize(dir);
|
|
@@ -133,7 +134,7 @@ function safeAssetFile(dir, pathname) {
|
|
|
133
134
|
try {
|
|
134
135
|
if (existsSync(full) && statSync(full).isFile()) return full;
|
|
135
136
|
} catch {
|
|
136
|
-
/*
|
|
137
|
+
/* ignore */
|
|
137
138
|
}
|
|
138
139
|
return null;
|
|
139
140
|
}
|
|
@@ -144,12 +145,12 @@ function escapeHtml(value) {
|
|
|
144
145
|
|
|
145
146
|
function errorHtml(status, message) {
|
|
146
147
|
return (
|
|
147
|
-
`<!doctype html><html lang="
|
|
148
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8">` +
|
|
148
149
|
`<title>blocofy dev — ${status}</title></head>` +
|
|
149
150
|
`<body style="font-family:system-ui,sans-serif;padding:2rem;max-width:48rem;margin:0 auto">` +
|
|
150
|
-
`<h1 style="color:#b91c1c">Render
|
|
151
|
+
`<h1 style="color:#b91c1c">Render error (${status})</h1>` +
|
|
151
152
|
`<pre style="white-space:pre-wrap;background:#f4f4f5;padding:1rem;border-radius:.5rem;color:#333">${escapeHtml(message)}</pre>` +
|
|
152
|
-
`<p style="color:#666">
|
|
153
|
+
`<p style="color:#666">Fix the theme file — this page reloads automatically when you save.</p>` +
|
|
153
154
|
`</body></html>`
|
|
154
155
|
);
|
|
155
156
|
}
|
package/lib/local-theme.mjs
CHANGED
|
@@ -2,25 +2,24 @@ import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { extname, join, relative, sep } from "node:path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* üst-dizini THEME_DIRS olan dosyalar, key = göreli yol, `.liquid` strip'li.
|
|
5
|
+
* Reading local theme files + livereload helpers. `readLocalTemplates` mirrors the
|
|
6
|
+
* platform's own theme loader exactly: files whose top-level directory is one of
|
|
7
|
+
* THEME_DIRS, keyed by relative path with the `.liquid` extension stripped.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
const THEME_DIRS = new Set(["layout", "section", "block", "partial", "asset", "template"]);
|
|
12
11
|
const LIQUID_KINDS = new Set(["layout", "section", "block", "partial", "template"]);
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
* Stripped key → disk
|
|
16
|
-
*
|
|
14
|
+
* Stripped key → on-disk path: re-adds `.liquid` for Liquid kinds, leaves assets
|
|
15
|
+
* raw. Used by `pull` when writing to disk (the inverse of readLocalTemplates).
|
|
17
16
|
*/
|
|
18
17
|
export function localPathFor(key) {
|
|
19
18
|
const top = key.split("/")[0] ?? "";
|
|
20
19
|
return LIQUID_KINDS.has(top) && !key.endsWith(".liquid") ? `${key}.liquid` : key;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
/**
|
|
22
|
+
/** Turn a theme directory into a `{ path: content }` map (sent to dev-render). */
|
|
24
23
|
export function readLocalTemplates(dir) {
|
|
25
24
|
const out = {};
|
|
26
25
|
const walk = (current) => {
|
|
@@ -48,7 +47,7 @@ const LIVERELOAD_SNIPPET = `<script>
|
|
|
48
47
|
|
|
49
48
|
export { LIVERELOAD_PATH };
|
|
50
49
|
|
|
51
|
-
/**
|
|
50
|
+
/** Inject the livereload script into rendered HTML (before </body>). */
|
|
52
51
|
export function injectLivereload(html) {
|
|
53
52
|
if (typeof html !== "string") return html;
|
|
54
53
|
return html.includes("</body>")
|
|
@@ -76,7 +75,7 @@ const CONTENT_TYPES = {
|
|
|
76
75
|
".map": "application/json",
|
|
77
76
|
};
|
|
78
77
|
|
|
79
|
-
/**
|
|
78
|
+
/** Content-type from a file extension (for static asset serving). */
|
|
80
79
|
export function contentTypeFor(p) {
|
|
81
80
|
return CONTENT_TYPES[extname(p).toLowerCase()] ?? "application/octet-stream";
|
|
82
81
|
}
|
package/lib/theme-sync.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { localPathFor, readLocalTemplates } from "./local-theme.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* `blocofy theme pull/push
|
|
8
|
-
*
|
|
9
|
-
* → POST (create/update;
|
|
7
|
+
* `blocofy theme pull/push`. Talks to the platform's `/api/dev/theme` endpoint
|
|
8
|
+
* (Bearer token). pull = GET → write to disk (re-adding `.liquid`); push =
|
|
9
|
+
* readLocalTemplates → POST (create/update; no delete).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
async function errorText(res) {
|
|
@@ -18,7 +18,7 @@ async function errorText(res) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/** Download the live theme to disk. `{ path: content }` (stripped) → `.liquid` files. */
|
|
22
22
|
export async function pullTheme({ dir, url, token }) {
|
|
23
23
|
const base = url.replace(/\/+$/, "");
|
|
24
24
|
const res = await fetch(`${base}/api/dev/theme`, { headers: { authorization: `Bearer ${token}` } });
|
|
@@ -34,7 +34,7 @@ export async function pullTheme({ dir, url, token }) {
|
|
|
34
34
|
return { count };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
37
|
+
/** Write the local theme to the live site (create/update; no delete). */
|
|
38
38
|
export async function pushTheme({ dir, url, token }) {
|
|
39
39
|
const base = url.replace(/\/+$/, "");
|
|
40
40
|
const files = readLocalTemplates(dir);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocofy/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Blocofy
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Develop your Blocofy theme locally against live data with instant preview — login, theme dev, pull, push.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Blocofy",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"theme",
|
|
19
19
|
"cli",
|
|
20
20
|
"liquid",
|
|
21
|
-
"
|
|
21
|
+
"theme-development"
|
|
22
22
|
],
|
|
23
23
|
"bin": {
|
|
24
24
|
"blocofy": "./bin/blocofy.mjs"
|