@cancia/astro 0.2.1 → 0.3.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/dist/chunk-52URFK5Y.js +580 -0
- package/dist/{chunk-7MPOERVU.js → chunk-AIPRCBJM.js} +16 -3
- package/dist/{chunk-ST44VULL.js → chunk-L2VKQJPY.js} +9 -4
- package/dist/chunk-PIDFNJME.js +19 -0
- package/dist/{chunk-337LJIKX.js → chunk-UR5WC3RA.js} +1 -1
- package/dist/endpoints/publish.js +23 -17
- package/dist/git-backed-DFAB0tzf.d.ts +98 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +53 -23
- package/dist/loader/index.js +2 -2
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +3 -3
- package/dist/storage/index.d.ts +13 -84
- package/dist/storage/index.js +13 -5
- package/package.json +1 -1
- package/dist/chunk-5ELSN6LI.js +0 -244
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hashRev
|
|
3
|
+
} from "./chunk-L2VKQJPY.js";
|
|
4
|
+
import {
|
|
5
|
+
RevConflictError
|
|
6
|
+
} from "./chunk-7IA5B5CF.js";
|
|
7
|
+
|
|
8
|
+
// src/storage/sqlite-v2.ts
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
var require2 = createRequire(import.meta.url);
|
|
12
|
+
function openDB(dbPath) {
|
|
13
|
+
const Database = require2("better-sqlite3");
|
|
14
|
+
const db = new Database(dbPath);
|
|
15
|
+
db.pragma("journal_mode = WAL");
|
|
16
|
+
db.pragma("foreign_keys = ON");
|
|
17
|
+
db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
19
|
+
site TEXT NOT NULL, key TEXT NOT NULL, lang TEXT NOT NULL, value TEXT NOT NULL,
|
|
20
|
+
PRIMARY KEY (site, key, lang)
|
|
21
|
+
);
|
|
22
|
+
CREATE TABLE IF NOT EXISTS pages (
|
|
23
|
+
site TEXT NOT NULL, route TEXT NOT NULL, meta TEXT NOT NULL,
|
|
24
|
+
PRIMARY KEY (site, route)
|
|
25
|
+
);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS list_entries (
|
|
27
|
+
site TEXT NOT NULL, list TEXT NOT NULL, id TEXT NOT NULL, locale TEXT NOT NULL,
|
|
28
|
+
data TEXT NOT NULL,
|
|
29
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
30
|
+
PRIMARY KEY (site, list, id, locale)
|
|
31
|
+
);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS list_order (
|
|
33
|
+
site TEXT NOT NULL, list TEXT NOT NULL, ids TEXT NOT NULL,
|
|
34
|
+
PRIMARY KEY (site, list)
|
|
35
|
+
);
|
|
36
|
+
`);
|
|
37
|
+
return db;
|
|
38
|
+
}
|
|
39
|
+
function makeKVStore(db) {
|
|
40
|
+
const getStmt = db.prepare("SELECT value FROM kv WHERE site=? AND key=? AND lang=?");
|
|
41
|
+
const setStmt = db.prepare(
|
|
42
|
+
`INSERT INTO kv (site, key, lang, value) VALUES (?, ?, ?, ?)
|
|
43
|
+
ON CONFLICT(site, key, lang) DO UPDATE SET value=excluded.value`
|
|
44
|
+
);
|
|
45
|
+
const getAllStmt = db.prepare("SELECT key, lang, value FROM kv WHERE site=?");
|
|
46
|
+
const delStmt = db.prepare("DELETE FROM kv WHERE site=? AND key=? AND lang=?");
|
|
47
|
+
return {
|
|
48
|
+
async get(site, key, lang) {
|
|
49
|
+
const row = getStmt.get(site, key, lang);
|
|
50
|
+
return row?.value ?? null;
|
|
51
|
+
},
|
|
52
|
+
async set(site, key, lang, value) {
|
|
53
|
+
setStmt.run(site, key, lang, value);
|
|
54
|
+
},
|
|
55
|
+
async getAll(site) {
|
|
56
|
+
const rows = getAllStmt.all(site);
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const r of rows) out[`${r.key}.${r.lang}`] = r.value;
|
|
59
|
+
return out;
|
|
60
|
+
},
|
|
61
|
+
async delete(site, key, lang) {
|
|
62
|
+
delStmt.run(site, key, lang);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function makePageStore(db) {
|
|
67
|
+
const getStmt = db.prepare("SELECT meta FROM pages WHERE site=? AND route=?");
|
|
68
|
+
const listStmt = db.prepare("SELECT route, meta FROM pages WHERE site=?");
|
|
69
|
+
const upsertStmt = db.prepare(
|
|
70
|
+
`INSERT INTO pages (site, route, meta) VALUES (?, ?, ?)
|
|
71
|
+
ON CONFLICT(site, route) DO UPDATE SET meta=excluded.meta`
|
|
72
|
+
);
|
|
73
|
+
const delStmt = db.prepare("DELETE FROM pages WHERE site=? AND route=?");
|
|
74
|
+
function parseMeta(raw) {
|
|
75
|
+
return JSON.parse(raw);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
async get(site, route) {
|
|
79
|
+
const row = getStmt.get(site, route);
|
|
80
|
+
if (!row) return null;
|
|
81
|
+
const meta = parseMeta(row.meta);
|
|
82
|
+
return { route, meta, _rev: hashRev(meta) };
|
|
83
|
+
},
|
|
84
|
+
async list(site) {
|
|
85
|
+
const rows = listStmt.all(site);
|
|
86
|
+
return rows.map((r) => {
|
|
87
|
+
const meta = parseMeta(r.meta);
|
|
88
|
+
return { route: r.route, meta, _rev: hashRev(meta) };
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
async set(site, route, meta, rev) {
|
|
92
|
+
const existing = getStmt.get(site, route);
|
|
93
|
+
if (existing) {
|
|
94
|
+
const currentRev = hashRev(parseMeta(existing.meta));
|
|
95
|
+
if (rev !== currentRev) throw new RevConflictError();
|
|
96
|
+
}
|
|
97
|
+
upsertStmt.run(site, route, JSON.stringify(meta));
|
|
98
|
+
return { route, meta, _rev: hashRev(meta) };
|
|
99
|
+
},
|
|
100
|
+
async delete(site, route) {
|
|
101
|
+
delStmt.run(site, route);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function rowToEntry(row) {
|
|
106
|
+
const data = JSON.parse(row.data);
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
locale: row.locale,
|
|
110
|
+
data,
|
|
111
|
+
createdAt: row.created_at,
|
|
112
|
+
updatedAt: row.updated_at,
|
|
113
|
+
_rev: hashRev(data)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function applyOrder(order, ids) {
|
|
117
|
+
const result = [];
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const id of order) {
|
|
120
|
+
if (ids.has(id)) {
|
|
121
|
+
result.push(id);
|
|
122
|
+
seen.add(id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const extras = [...ids].filter((id) => !seen.has(id)).sort();
|
|
126
|
+
return [...result, ...extras];
|
|
127
|
+
}
|
|
128
|
+
function makeListStore(db) {
|
|
129
|
+
const getOrderStmt = db.prepare("SELECT ids FROM list_order WHERE site=? AND list=?");
|
|
130
|
+
const upsertOrderStmt = db.prepare(
|
|
131
|
+
`INSERT INTO list_order (site, list, ids) VALUES (?, ?, ?)
|
|
132
|
+
ON CONFLICT(site, list) DO UPDATE SET ids=excluded.ids`
|
|
133
|
+
);
|
|
134
|
+
const getEntryStmt = db.prepare(
|
|
135
|
+
"SELECT id, locale, data, created_at, updated_at FROM list_entries WHERE site=? AND list=? AND id=? AND locale=?"
|
|
136
|
+
);
|
|
137
|
+
const allEntriesStmt = db.prepare(
|
|
138
|
+
"SELECT id, locale, data, created_at, updated_at FROM list_entries WHERE site=? AND list=? ORDER BY locale"
|
|
139
|
+
);
|
|
140
|
+
const localeEntriesStmt = db.prepare(
|
|
141
|
+
"SELECT id, locale, data, created_at, updated_at FROM list_entries WHERE site=? AND list=? AND locale=?"
|
|
142
|
+
);
|
|
143
|
+
const insertEntryStmt = db.prepare(
|
|
144
|
+
`INSERT INTO list_entries (site, list, id, locale, data, created_at, updated_at)
|
|
145
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
146
|
+
);
|
|
147
|
+
const updateEntryStmt = db.prepare(
|
|
148
|
+
"UPDATE list_entries SET data=?, updated_at=? WHERE site=? AND list=? AND id=? AND locale=?"
|
|
149
|
+
);
|
|
150
|
+
const deleteEntryStmt = db.prepare(
|
|
151
|
+
"DELETE FROM list_entries WHERE site=? AND list=? AND id=? AND locale=?"
|
|
152
|
+
);
|
|
153
|
+
const deleteEntryAllLocalesStmt = db.prepare(
|
|
154
|
+
"DELETE FROM list_entries WHERE site=? AND list=? AND id=?"
|
|
155
|
+
);
|
|
156
|
+
const distinctIdsStmt = db.prepare(
|
|
157
|
+
"SELECT DISTINCT id FROM list_entries WHERE site=? AND list=?"
|
|
158
|
+
);
|
|
159
|
+
function readOrder(site, listName) {
|
|
160
|
+
const row = getOrderStmt.get(site, listName);
|
|
161
|
+
if (!row) return [];
|
|
162
|
+
return JSON.parse(row.ids);
|
|
163
|
+
}
|
|
164
|
+
function writeOrder(site, listName, ids) {
|
|
165
|
+
upsertOrderStmt.run(site, listName, JSON.stringify(ids));
|
|
166
|
+
}
|
|
167
|
+
function distinctIds(site, listName) {
|
|
168
|
+
const rows = distinctIdsStmt.all(site, listName);
|
|
169
|
+
return new Set(rows.map((r) => r.id));
|
|
170
|
+
}
|
|
171
|
+
const createTx = db.transaction(
|
|
172
|
+
(site, listName, id, locale, dataJson, now) => {
|
|
173
|
+
insertEntryStmt.run(site, listName, id, locale, dataJson, now, now);
|
|
174
|
+
const order = readOrder(site, listName);
|
|
175
|
+
if (!order.includes(id)) {
|
|
176
|
+
writeOrder(site, listName, [...order, id]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
const deleteTx = db.transaction((site, listName, id, locale) => {
|
|
181
|
+
deleteEntryStmt.run(site, listName, id, locale);
|
|
182
|
+
const stillExists = distinctIds(site, listName).has(id);
|
|
183
|
+
if (!stillExists) {
|
|
184
|
+
const order = readOrder(site, listName);
|
|
185
|
+
const next = order.filter((existingId) => existingId !== id);
|
|
186
|
+
if (next.length !== order.length) {
|
|
187
|
+
writeOrder(site, listName, next);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const reorderTx = db.transaction((site, listName, ids) => {
|
|
192
|
+
const all = distinctIds(site, listName);
|
|
193
|
+
const supplied = new Set(ids);
|
|
194
|
+
for (const id of all) {
|
|
195
|
+
if (!supplied.has(id)) {
|
|
196
|
+
deleteEntryAllLocalesStmt.run(site, listName, id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
writeOrder(site, listName, ids);
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
async list(site, listName, locale) {
|
|
203
|
+
const order = readOrder(site, listName);
|
|
204
|
+
if (locale !== void 0) {
|
|
205
|
+
const rows2 = localeEntriesStmt.all(site, listName, locale);
|
|
206
|
+
const byId2 = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const r of rows2) byId2.set(r.id, r);
|
|
208
|
+
const sorted = applyOrder(order, new Set(byId2.keys()));
|
|
209
|
+
const entries2 = [];
|
|
210
|
+
for (const id of sorted) {
|
|
211
|
+
const r = byId2.get(id);
|
|
212
|
+
if (r) entries2.push(rowToEntry(r));
|
|
213
|
+
}
|
|
214
|
+
return entries2;
|
|
215
|
+
}
|
|
216
|
+
const rows = allEntriesStmt.all(site, listName);
|
|
217
|
+
const byId = /* @__PURE__ */ new Map();
|
|
218
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const r of rows) {
|
|
220
|
+
allIds.add(r.id);
|
|
221
|
+
const list = byId.get(r.id) ?? [];
|
|
222
|
+
list.push(r);
|
|
223
|
+
byId.set(r.id, list);
|
|
224
|
+
}
|
|
225
|
+
const sortedIds = applyOrder(order, allIds);
|
|
226
|
+
const entries = [];
|
|
227
|
+
for (const id of sortedIds) {
|
|
228
|
+
const group = byId.get(id) ?? [];
|
|
229
|
+
for (const r of group) entries.push(rowToEntry(r));
|
|
230
|
+
}
|
|
231
|
+
return entries;
|
|
232
|
+
},
|
|
233
|
+
async get(site, listName, id, locale) {
|
|
234
|
+
const row = getEntryStmt.get(site, listName, id, locale);
|
|
235
|
+
return row ? rowToEntry(row) : null;
|
|
236
|
+
},
|
|
237
|
+
async create(site, listName, data, locale, id) {
|
|
238
|
+
const finalId = id ?? randomUUID();
|
|
239
|
+
const existing = getEntryStmt.get(site, listName, finalId, locale);
|
|
240
|
+
if (existing) {
|
|
241
|
+
throw new Error(`List entry "${finalId}" already exists in "${listName}" (${locale})`);
|
|
242
|
+
}
|
|
243
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
244
|
+
createTx(site, listName, finalId, locale, JSON.stringify(data), now);
|
|
245
|
+
return {
|
|
246
|
+
id: finalId,
|
|
247
|
+
locale,
|
|
248
|
+
data,
|
|
249
|
+
createdAt: now,
|
|
250
|
+
updatedAt: now,
|
|
251
|
+
_rev: hashRev(data)
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
async update(site, listName, id, locale, data, rev) {
|
|
255
|
+
const existing = getEntryStmt.get(site, listName, id, locale);
|
|
256
|
+
if (!existing) {
|
|
257
|
+
throw new Error(`List entry "${id}" not found in "${listName}" (${locale})`);
|
|
258
|
+
}
|
|
259
|
+
const existingData = JSON.parse(existing.data);
|
|
260
|
+
if (hashRev(existingData) !== rev) throw new RevConflictError();
|
|
261
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
262
|
+
updateEntryStmt.run(JSON.stringify(data), now, site, listName, id, locale);
|
|
263
|
+
return {
|
|
264
|
+
id,
|
|
265
|
+
locale,
|
|
266
|
+
data,
|
|
267
|
+
createdAt: existing.created_at,
|
|
268
|
+
updatedAt: now,
|
|
269
|
+
_rev: hashRev(data)
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
async delete(site, listName, id, locale) {
|
|
273
|
+
const existing = getEntryStmt.get(site, listName, id, locale);
|
|
274
|
+
if (!existing) return;
|
|
275
|
+
deleteTx(site, listName, id, locale);
|
|
276
|
+
},
|
|
277
|
+
async reorder(site, listName, ids) {
|
|
278
|
+
const all = distinctIds(site, listName);
|
|
279
|
+
for (const id of ids) {
|
|
280
|
+
if (!all.has(id)) {
|
|
281
|
+
throw new Error(`Cannot reorder: entry "${id}" not found in "${listName}"`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
reorderTx(site, listName, ids);
|
|
285
|
+
},
|
|
286
|
+
async translations(site, listName) {
|
|
287
|
+
const rows = allEntriesStmt.all(site, listName);
|
|
288
|
+
const idToLocales = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const r of rows) {
|
|
290
|
+
const list = idToLocales.get(r.id) ?? [];
|
|
291
|
+
list.push(r.locale);
|
|
292
|
+
idToLocales.set(r.id, list);
|
|
293
|
+
}
|
|
294
|
+
const order = readOrder(site, listName);
|
|
295
|
+
const orderedIds = applyOrder(order, new Set(idToLocales.keys()));
|
|
296
|
+
return orderedIds.map((id) => ({
|
|
297
|
+
id,
|
|
298
|
+
locales: idToLocales.get(id) ?? []
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var _connections = /* @__PURE__ */ new Map();
|
|
304
|
+
function getConnection(dbPath) {
|
|
305
|
+
let db = _connections.get(dbPath);
|
|
306
|
+
if (!db) {
|
|
307
|
+
db = openDB(dbPath);
|
|
308
|
+
_connections.set(dbPath, db);
|
|
309
|
+
}
|
|
310
|
+
return db;
|
|
311
|
+
}
|
|
312
|
+
function createSqliteAdapterV2(opts = {}) {
|
|
313
|
+
const root = opts.projectRoot ?? process.cwd();
|
|
314
|
+
const dbPath = opts.dbPath ?? `${root}/cancia.db`;
|
|
315
|
+
const db = getConnection(dbPath);
|
|
316
|
+
return {
|
|
317
|
+
kv: makeKVStore(db),
|
|
318
|
+
pages: makePageStore(db),
|
|
319
|
+
lists: makeListStore(db)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function closeSqliteAdapterV2(dbPath) {
|
|
323
|
+
if (dbPath === void 0) {
|
|
324
|
+
for (const db2 of _connections.values()) db2.close();
|
|
325
|
+
_connections.clear();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const db = _connections.get(dbPath);
|
|
329
|
+
if (db) {
|
|
330
|
+
db.close();
|
|
331
|
+
_connections.delete(dbPath);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/storage/github-client.ts
|
|
336
|
+
function toBase64(text) {
|
|
337
|
+
if (typeof Buffer !== "undefined") {
|
|
338
|
+
return Buffer.from(text, "utf-8").toString("base64");
|
|
339
|
+
}
|
|
340
|
+
const bytes = new TextEncoder().encode(text);
|
|
341
|
+
let binary = "";
|
|
342
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
343
|
+
return btoa(binary);
|
|
344
|
+
}
|
|
345
|
+
function createGitHubClient(opts) {
|
|
346
|
+
const { repo, branch, token, committer } = opts;
|
|
347
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
348
|
+
const apiBase = (opts.apiBase ?? "https://api.github.com").replace(/\/$/, "");
|
|
349
|
+
if (!doFetch) {
|
|
350
|
+
throw new Error("createGitHubClient: no fetch available (pass opts.fetch)");
|
|
351
|
+
}
|
|
352
|
+
const headers = () => ({
|
|
353
|
+
Authorization: `Bearer ${token}`,
|
|
354
|
+
Accept: "application/vnd.github+json",
|
|
355
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
356
|
+
});
|
|
357
|
+
const contentsUrl = (path) => {
|
|
358
|
+
const encoded = path.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
359
|
+
return `${apiBase}/repos/${repo}/contents/${encoded}`;
|
|
360
|
+
};
|
|
361
|
+
async function getFileSha(path) {
|
|
362
|
+
const url = `${contentsUrl(path)}?ref=${encodeURIComponent(branch)}`;
|
|
363
|
+
const res = await doFetch(url, { method: "GET", headers: headers() });
|
|
364
|
+
if (res.status === 404) return null;
|
|
365
|
+
if (!res.ok) {
|
|
366
|
+
const detail = await res.text().catch(() => "");
|
|
367
|
+
throw new Error(`GitHub getFileSha ${path} failed: ${res.status} ${detail}`);
|
|
368
|
+
}
|
|
369
|
+
const body = await res.json();
|
|
370
|
+
return body.sha ?? null;
|
|
371
|
+
}
|
|
372
|
+
async function putFile(file, message) {
|
|
373
|
+
const sha = await getFileSha(file.path);
|
|
374
|
+
const payload = {
|
|
375
|
+
message,
|
|
376
|
+
content: toBase64(file.content),
|
|
377
|
+
branch
|
|
378
|
+
};
|
|
379
|
+
if (sha) payload.sha = sha;
|
|
380
|
+
if (committer) payload.committer = committer;
|
|
381
|
+
const res = await doFetch(contentsUrl(file.path), {
|
|
382
|
+
method: "PUT",
|
|
383
|
+
headers: headers(),
|
|
384
|
+
body: JSON.stringify(payload)
|
|
385
|
+
});
|
|
386
|
+
if (!res.ok) {
|
|
387
|
+
const detail = await res.text().catch(() => "");
|
|
388
|
+
throw new Error(`GitHub commit ${file.path} failed: ${res.status} ${detail}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function commitFiles(files, message) {
|
|
392
|
+
for (const file of files) {
|
|
393
|
+
await putFile(file, message);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return { getFileSha, commitFiles };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/storage/git-backed.ts
|
|
400
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
401
|
+
import { join, relative } from "path";
|
|
402
|
+
function createGitBackedAdapter(opts) {
|
|
403
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
404
|
+
const branch = opts.branch ?? "main";
|
|
405
|
+
const debounceMs = opts.debounceMs ?? 3e3;
|
|
406
|
+
const commitMessage = opts.commitMessage ?? "Cancia: content update";
|
|
407
|
+
const warn = opts.warn ?? ((m) => console.warn(m));
|
|
408
|
+
const onError = opts.onError ?? ((m, e) => console.error(m, e));
|
|
409
|
+
const kvPath = opts.contentPaths?.kvPath ?? join(projectRoot, "cancia-content.json");
|
|
410
|
+
const pagesPath = opts.contentPaths?.pagesPath ?? join(projectRoot, ".cancia", "pages.json");
|
|
411
|
+
const listsDir = opts.contentPaths?.listsDir ?? join(projectRoot, ".cancia", "lists");
|
|
412
|
+
const token = opts.token ?? process.env.CANCIA_GITHUB_TOKEN ?? "";
|
|
413
|
+
let client = null;
|
|
414
|
+
if (opts.client) {
|
|
415
|
+
client = opts.client;
|
|
416
|
+
} else if (token) {
|
|
417
|
+
client = createGitHubClient({
|
|
418
|
+
repo: opts.repo,
|
|
419
|
+
branch,
|
|
420
|
+
token,
|
|
421
|
+
committer: opts.committer,
|
|
422
|
+
fetch: opts.fetch,
|
|
423
|
+
apiBase: opts.apiBase
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
const gitEnabled = client !== null;
|
|
427
|
+
if (!gitEnabled) {
|
|
428
|
+
warn(
|
|
429
|
+
"[cancia] Git-backed storage: no GitHub token (CANCIA_GITHUB_TOKEN) \u2014 running local-only. Edits save to disk but are NOT committed/pushed."
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
433
|
+
let timer = null;
|
|
434
|
+
let flushing = null;
|
|
435
|
+
let rerunRequested = false;
|
|
436
|
+
function toRepoPath(absPath) {
|
|
437
|
+
return relative(projectRoot, absPath).split("\\").join("/");
|
|
438
|
+
}
|
|
439
|
+
function markDirty(absPath) {
|
|
440
|
+
dirty.add(absPath);
|
|
441
|
+
}
|
|
442
|
+
function markListDirty(listName, site) {
|
|
443
|
+
const siteDir = join(listsDir, listName, site);
|
|
444
|
+
if (!existsSync(siteDir)) return;
|
|
445
|
+
const walk = (dir) => {
|
|
446
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
447
|
+
const full = join(dir, entry.name);
|
|
448
|
+
if (entry.isDirectory()) walk(full);
|
|
449
|
+
else if (entry.isFile()) markDirty(full);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
walk(siteDir);
|
|
453
|
+
}
|
|
454
|
+
function scheduleFlush() {
|
|
455
|
+
if (!gitEnabled) return;
|
|
456
|
+
if (timer) clearTimeout(timer);
|
|
457
|
+
timer = setTimeout(() => {
|
|
458
|
+
timer = null;
|
|
459
|
+
void runFlush();
|
|
460
|
+
}, debounceMs);
|
|
461
|
+
}
|
|
462
|
+
async function runFlush() {
|
|
463
|
+
if (flushing) {
|
|
464
|
+
rerunRequested = true;
|
|
465
|
+
return flushing;
|
|
466
|
+
}
|
|
467
|
+
flushing = doFlush().finally(() => {
|
|
468
|
+
flushing = null;
|
|
469
|
+
if (rerunRequested) {
|
|
470
|
+
rerunRequested = false;
|
|
471
|
+
void runFlush();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
return flushing;
|
|
475
|
+
}
|
|
476
|
+
async function doFlush() {
|
|
477
|
+
if (!client || dirty.size === 0) return;
|
|
478
|
+
const batch = [...dirty];
|
|
479
|
+
const files = [];
|
|
480
|
+
for (const abs of batch) {
|
|
481
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) continue;
|
|
482
|
+
files.push({ path: toRepoPath(abs), content: readFileSync(abs, "utf-8") });
|
|
483
|
+
}
|
|
484
|
+
if (files.length === 0) {
|
|
485
|
+
for (const abs of batch) dirty.delete(abs);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
await client.commitFiles(files, commitMessage);
|
|
490
|
+
for (const abs of batch) dirty.delete(abs);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
onError(
|
|
493
|
+
"[cancia] Git-backed storage: commit failed \u2014 data saved locally, will retry on next flush.",
|
|
494
|
+
err
|
|
495
|
+
);
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function flush() {
|
|
500
|
+
if (!gitEnabled) return;
|
|
501
|
+
if (timer) {
|
|
502
|
+
clearTimeout(timer);
|
|
503
|
+
timer = null;
|
|
504
|
+
}
|
|
505
|
+
await runFlush();
|
|
506
|
+
}
|
|
507
|
+
const kv = {
|
|
508
|
+
get: (site, key, lang) => opts.local.kv.get(site, key, lang),
|
|
509
|
+
getAll: (site) => opts.local.kv.getAll(site),
|
|
510
|
+
async set(site, key, lang, value) {
|
|
511
|
+
await opts.local.kv.set(site, key, lang, value);
|
|
512
|
+
markDirty(kvPath);
|
|
513
|
+
scheduleFlush();
|
|
514
|
+
},
|
|
515
|
+
async delete(site, key, lang) {
|
|
516
|
+
await opts.local.kv.delete(site, key, lang);
|
|
517
|
+
markDirty(kvPath);
|
|
518
|
+
scheduleFlush();
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const pages = {
|
|
522
|
+
get: (site, route) => opts.local.pages.get(site, route),
|
|
523
|
+
list: (site) => opts.local.pages.list(site),
|
|
524
|
+
async set(site, route, meta, rev) {
|
|
525
|
+
const result = await opts.local.pages.set(site, route, meta, rev);
|
|
526
|
+
markDirty(pagesPath);
|
|
527
|
+
scheduleFlush();
|
|
528
|
+
return result;
|
|
529
|
+
},
|
|
530
|
+
async delete(site, route) {
|
|
531
|
+
await opts.local.pages.delete(site, route);
|
|
532
|
+
markDirty(pagesPath);
|
|
533
|
+
scheduleFlush();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const lists = {
|
|
537
|
+
list: (site, listName, locale) => opts.local.lists.list(site, listName, locale),
|
|
538
|
+
get: (site, listName, id, locale) => opts.local.lists.get(site, listName, id, locale),
|
|
539
|
+
translations: (site, listName) => opts.local.lists.translations(site, listName),
|
|
540
|
+
async create(site, listName, data, locale, id) {
|
|
541
|
+
const entry = await opts.local.lists.create(site, listName, data, locale, id);
|
|
542
|
+
markListDirty(listName, site);
|
|
543
|
+
scheduleFlush();
|
|
544
|
+
return entry;
|
|
545
|
+
},
|
|
546
|
+
async update(site, listName, id, locale, data, rev) {
|
|
547
|
+
const entry = await opts.local.lists.update(site, listName, id, locale, data, rev);
|
|
548
|
+
markListDirty(listName, site);
|
|
549
|
+
scheduleFlush();
|
|
550
|
+
return entry;
|
|
551
|
+
},
|
|
552
|
+
async delete(site, listName, id, locale) {
|
|
553
|
+
await opts.local.lists.delete(site, listName, id, locale);
|
|
554
|
+
markListDirty(listName, site);
|
|
555
|
+
scheduleFlush();
|
|
556
|
+
},
|
|
557
|
+
async reorder(site, listName, ids) {
|
|
558
|
+
await opts.local.lists.reorder(site, listName, ids);
|
|
559
|
+
markListDirty(listName, site);
|
|
560
|
+
scheduleFlush();
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
const git = {
|
|
564
|
+
flush,
|
|
565
|
+
get gitEnabled() {
|
|
566
|
+
return gitEnabled;
|
|
567
|
+
},
|
|
568
|
+
pendingPaths() {
|
|
569
|
+
return [...dirty].map(toRepoPath);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
return { kv, pages, lists, git };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export {
|
|
576
|
+
createSqliteAdapterV2,
|
|
577
|
+
closeSqliteAdapterV2,
|
|
578
|
+
createGitHubClient,
|
|
579
|
+
createGitBackedAdapter
|
|
580
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createGitBackedAdapter
|
|
3
|
-
|
|
2
|
+
createGitBackedAdapter,
|
|
3
|
+
createSqliteAdapterV2
|
|
4
|
+
} from "./chunk-52URFK5Y.js";
|
|
4
5
|
import {
|
|
5
6
|
createJsonFileAdapter,
|
|
6
7
|
createJsonFileAdapterV2
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-L2VKQJPY.js";
|
|
8
9
|
import {
|
|
9
10
|
detectImageType,
|
|
10
11
|
isValidSite
|
|
@@ -156,6 +157,7 @@ function makeR2UploadHandler(opts) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
// src/runtime.ts
|
|
160
|
+
import { isAbsolute, join as join2 } from "path";
|
|
159
161
|
var _runtime = null;
|
|
160
162
|
var _bakedConfig = null;
|
|
161
163
|
function setBakedConfig(config) {
|
|
@@ -169,6 +171,7 @@ function buildRuntimeFromBaked(baked) {
|
|
|
169
171
|
const token = process.env.CANCIA_TOKEN?.trim() || "";
|
|
170
172
|
const secret = baked.public ? void 0 : token || void 0;
|
|
171
173
|
const deployHook = process.env.CANCIA_DEPLOY_HOOK || void 0;
|
|
174
|
+
const deployHookToken = process.env.CANCIA_DEPLOY_HOOK_TOKEN || void 0;
|
|
172
175
|
const storageV2 = buildStorageV2(baked, projectRoot);
|
|
173
176
|
const storage = storageV2 ? storageV2.kv : lazyJsonFileAdapter(projectRoot);
|
|
174
177
|
const uploadHandler = buildUploadHandler(baked, projectRoot);
|
|
@@ -182,12 +185,18 @@ function buildRuntimeFromBaked(baked) {
|
|
|
182
185
|
secret,
|
|
183
186
|
uploadHandler,
|
|
184
187
|
deployHook,
|
|
188
|
+
deployHookToken,
|
|
189
|
+
deployHookMethod: baked.deployHookMethod,
|
|
190
|
+
deployHookHeaders: baked.deployHookHeaders,
|
|
185
191
|
maxUploadMB: baked.maxUploadMB
|
|
186
192
|
};
|
|
187
193
|
}
|
|
188
194
|
function lazyJsonFileAdapter(projectRoot) {
|
|
189
195
|
return createJsonFileAdapter(projectRoot + "/cancia-content.json");
|
|
190
196
|
}
|
|
197
|
+
function resolveDbPath(dbPath, projectRoot) {
|
|
198
|
+
return isAbsolute(dbPath) ? dbPath : join2(projectRoot, dbPath);
|
|
199
|
+
}
|
|
191
200
|
function buildStorageV2(baked, projectRoot) {
|
|
192
201
|
const desc = baked.storage;
|
|
193
202
|
if (!desc) return void 0;
|
|
@@ -204,6 +213,10 @@ function buildStorageV2(baked, projectRoot) {
|
|
|
204
213
|
commitMessage: desc.commitMessage
|
|
205
214
|
});
|
|
206
215
|
}
|
|
216
|
+
if (desc.kind === "sqlite-v2") {
|
|
217
|
+
const dbPath = desc.dbPath ? resolveDbPath(desc.dbPath, projectRoot) : `${projectRoot}/cancia.db`;
|
|
218
|
+
return createSqliteAdapterV2({ dbPath });
|
|
219
|
+
}
|
|
207
220
|
return createJsonFileAdapterV2({ projectRoot });
|
|
208
221
|
}
|
|
209
222
|
function buildUploadHandler(baked, projectRoot) {
|
|
@@ -48,10 +48,8 @@ function createJsonFileAdapter(filePath) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// src/storage/
|
|
52
|
-
import {
|
|
53
|
-
import { dirname as dirname2, join } from "path";
|
|
54
|
-
import { createHash, randomUUID } from "crypto";
|
|
51
|
+
// src/storage/rev.ts
|
|
52
|
+
import { createHash } from "crypto";
|
|
55
53
|
function canonicalize(value) {
|
|
56
54
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
57
55
|
if (Array.isArray(value)) {
|
|
@@ -64,6 +62,11 @@ function canonicalize(value) {
|
|
|
64
62
|
function hashRev(value) {
|
|
65
63
|
return createHash("sha256").update(canonicalize(value)).digest("hex").slice(0, 16);
|
|
66
64
|
}
|
|
65
|
+
|
|
66
|
+
// src/storage/json-file-v2.ts
|
|
67
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmSync } from "fs";
|
|
68
|
+
import { dirname as dirname2, join } from "path";
|
|
69
|
+
import { randomUUID } from "crypto";
|
|
67
70
|
function readJsonFile(filePath, fallback) {
|
|
68
71
|
if (!existsSync2(filePath)) return fallback;
|
|
69
72
|
try {
|
|
@@ -315,5 +318,7 @@ function createJsonFileAdapterV2(opts = {}) {
|
|
|
315
318
|
|
|
316
319
|
export {
|
|
317
320
|
createJsonFileAdapter,
|
|
321
|
+
canonicalize,
|
|
322
|
+
hashRev,
|
|
318
323
|
createJsonFileAdapterV2
|
|
319
324
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/publish-hook.ts
|
|
2
|
+
async function firePublish(hook, opts = {}) {
|
|
3
|
+
if (!hook) return { ok: false, kind: "no-hook" };
|
|
4
|
+
const headers = { ...opts.headers ?? {} };
|
|
5
|
+
if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
|
|
6
|
+
const init = { method: opts.method ?? "POST" };
|
|
7
|
+
if (Object.keys(headers).length > 0) init.headers = headers;
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(hook, init);
|
|
10
|
+
if (!res.ok) return { ok: false, kind: "bad-response", status: res.status };
|
|
11
|
+
return { ok: true };
|
|
12
|
+
} catch {
|
|
13
|
+
return { ok: false, kind: "unreachable" };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
firePublish
|
|
19
|
+
};
|