livelocale 0.9.0 → 0.10.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.
@@ -1120,7 +1120,7 @@ new file mode 100644
1120
1120
  }
1121
1121
  };
1122
1122
 
1123
- // src/server/github-gateway.ts
1123
+ // src/server/github-rest.ts
1124
1124
  var GitHubApiError = class extends Error {
1125
1125
  constructor(message, status) {
1126
1126
  super(message);
@@ -1129,27 +1129,26 @@ var GitHubApiError = class extends Error {
1129
1129
  }
1130
1130
  status;
1131
1131
  };
1132
- var MAX_WRITE_ATTEMPTS = 4;
1133
- var isWriteConflict = (err) => err instanceof GitHubApiError && (err.status === 409 || err.status === 422 && /sha/i.test(err.message));
1134
- var encodePath = (p) => p.split("/").map(encodeURIComponent).join("/");
1135
- var normalizeDir = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
1136
- function mapCompareStatus(status) {
1137
- switch (status) {
1138
- case "added":
1139
- case "copied":
1140
- return "added";
1141
- case "removed":
1142
- return "deleted";
1143
- case "renamed":
1144
- return "renamed";
1145
- default:
1146
- return "modified";
1147
- }
1132
+ var CHUNK_BYTES = 32768;
1133
+ function encodeBase64Utf8(text) {
1134
+ const bytes = new TextEncoder().encode(text);
1135
+ let binary = "";
1136
+ for (let i = 0; i < bytes.length; i += CHUNK_BYTES) {
1137
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_BYTES));
1138
+ }
1139
+ return btoa(binary);
1148
1140
  }
1149
- function createGitHubGateway(config) {
1141
+ function decodeBase64Utf8(base64) {
1142
+ const binary = atob(base64.replace(/\s/g, ""));
1143
+ const bytes = new Uint8Array(binary.length);
1144
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1145
+ return new TextDecoder().decode(bytes);
1146
+ }
1147
+ function encodeRefPath(ref) {
1148
+ return ref.split("/").map(encodeURIComponent).join("/");
1149
+ }
1150
+ function createGitHubRest(config) {
1150
1151
  const repo = config.repo;
1151
- const branch = config.branch;
1152
- const baseBranch = config.baseBranch ?? "main";
1153
1152
  const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
1154
1153
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1155
1154
  const owner = repo.split("/")[0] ?? "";
@@ -1159,17 +1158,18 @@ function createGitHubGateway(config) {
1159
1158
  if (typeof fetchImpl !== "function") {
1160
1159
  throw new Error("[livelocale] The GitHub gateway needs a global fetch (Node 18+).");
1161
1160
  }
1161
+ const headers = (hasBody) => ({
1162
+ Authorization: `Bearer ${config.token}`,
1163
+ Accept: "application/vnd.github+json",
1164
+ "X-GitHub-Api-Version": "2022-11-28",
1165
+ ...hasBody ? { "Content-Type": "application/json" } : {}
1166
+ });
1162
1167
  async function api(method, path5, options = {}) {
1163
1168
  const url = new URL(`${apiBase}/repos/${repo}${path5}`);
1164
1169
  for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
1165
1170
  const response = await fetchImpl(url.toString(), {
1166
1171
  method,
1167
- headers: {
1168
- Authorization: `Bearer ${config.token}`,
1169
- Accept: "application/vnd.github+json",
1170
- "X-GitHub-Api-Version": "2022-11-28",
1171
- ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
1172
- },
1172
+ headers: headers(options.body !== void 0),
1173
1173
  body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
1174
1174
  });
1175
1175
  let data = null;
@@ -1191,12 +1191,7 @@ function createGitHubGateway(config) {
1191
1191
  async function probeStatus(method, path5, body) {
1192
1192
  const response = await fetchImpl(`${apiBase}/repos/${repo}${path5}`, {
1193
1193
  method,
1194
- headers: {
1195
- Authorization: `Bearer ${config.token}`,
1196
- Accept: "application/vnd.github+json",
1197
- "X-GitHub-Api-Version": "2022-11-28",
1198
- ...body !== void 0 ? { "Content-Type": "application/json" } : {}
1199
- },
1194
+ headers: headers(body !== void 0),
1200
1195
  body: body !== void 0 ? JSON.stringify(body) : void 0
1201
1196
  });
1202
1197
  try {
@@ -1205,14 +1200,60 @@ function createGitHubGateway(config) {
1205
1200
  }
1206
1201
  return response.status;
1207
1202
  }
1208
- async function refSha(name) {
1203
+ async function refSha(branch) {
1209
1204
  const res = await api(
1210
1205
  "GET",
1211
- `/git/ref/heads/${encodeURIComponent(name)}`,
1206
+ `/git/ref/heads/${encodeRefPath(branch)}`,
1212
1207
  { allow404: true }
1213
1208
  );
1214
1209
  return res.data?.object?.sha ?? null;
1215
1210
  }
1211
+ async function findOpenPullRequest(head, base) {
1212
+ const res = await api("GET", "/pulls", {
1213
+ query: { head: `${owner}:${head}`, base, state: "open" }
1214
+ });
1215
+ const pull = res.data?.[0];
1216
+ return pull ? { number: pull.number, url: pull.html_url } : null;
1217
+ }
1218
+ async function openOrReusePullRequest(input) {
1219
+ const existing = await findOpenPullRequest(input.head, input.base);
1220
+ if (existing) {
1221
+ if (input.updateBodyIfReused) {
1222
+ await api("PATCH", `/pulls/${existing.number}`, { body: { body: input.body } });
1223
+ }
1224
+ return { ...existing, reused: true };
1225
+ }
1226
+ const res = await api("POST", "/pulls", {
1227
+ body: { title: input.title, head: input.head, base: input.base, body: input.body }
1228
+ });
1229
+ return { number: res.data.number, url: res.data.html_url, reused: false };
1230
+ }
1231
+ return { repo, api, probeStatus, refSha, findOpenPullRequest, openOrReusePullRequest };
1232
+ }
1233
+
1234
+ // src/server/github-gateway.ts
1235
+ var MAX_WRITE_ATTEMPTS = 4;
1236
+ var isWriteConflict = (err) => err instanceof GitHubApiError && (err.status === 409 || err.status === 422 && /sha/i.test(err.message));
1237
+ var encodePath = (p) => p.split("/").map(encodeURIComponent).join("/");
1238
+ var normalizeDir = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
1239
+ function mapCompareStatus(status) {
1240
+ switch (status) {
1241
+ case "added":
1242
+ case "copied":
1243
+ return "added";
1244
+ case "removed":
1245
+ return "deleted";
1246
+ case "renamed":
1247
+ return "renamed";
1248
+ default:
1249
+ return "modified";
1250
+ }
1251
+ }
1252
+ function createGitHubGateway(config) {
1253
+ const rest = createGitHubRest(config);
1254
+ const { repo, api, probeStatus, refSha } = rest;
1255
+ const branch = config.branch;
1256
+ const baseBranch = config.baseBranch ?? "main";
1216
1257
  let branchReady = null;
1217
1258
  function ensureBranch() {
1218
1259
  if (!branchReady) {
@@ -1239,7 +1280,7 @@ function createGitHubGateway(config) {
1239
1280
  async function readFile(path5) {
1240
1281
  const contents = await getContents(path5);
1241
1282
  if (!contents || Array.isArray(contents) || contents.type !== "file") return null;
1242
- const text = Buffer.from(contents.content.replace(/\n/g, ""), "base64").toString("utf-8");
1283
+ const text = decodeBase64Utf8(contents.content);
1243
1284
  return { text, sha: contents.sha };
1244
1285
  }
1245
1286
  async function putFile(path5, text, message, sha) {
@@ -1247,7 +1288,7 @@ function createGitHubGateway(config) {
1247
1288
  await api("PUT", `/contents/${encodePath(path5)}`, {
1248
1289
  body: {
1249
1290
  message,
1250
- content: Buffer.from(text, "utf-8").toString("base64"),
1291
+ content: encodeBase64Utf8(text),
1251
1292
  branch,
1252
1293
  ...sha ? { sha } : {}
1253
1294
  }
@@ -1452,10 +1493,7 @@ function createGitHubGateway(config) {
1452
1493
  return (res.data?.files ?? []).filter((f) => f.filename.startsWith(prefix));
1453
1494
  }
1454
1495
  async function openPullRequestUrl() {
1455
- const res = await api("GET", "/pulls", {
1456
- query: { head: `${owner}:${branch}`, base: baseBranch, state: "open" }
1457
- });
1458
- return res.data?.[0]?.html_url ?? null;
1496
+ return (await rest.findOpenPullRequest(branch, baseBranch))?.url ?? null;
1459
1497
  }
1460
1498
  async function status(basePath) {
1461
1499
  const files = await compareFiles(basePath);
@@ -1486,17 +1524,14 @@ ${f.patch ?? "(binary or too large)"}
1486
1524
  if (files.length === 0) {
1487
1525
  throw new Error(`No changes to propose under ${normalizeDir(basePath)} on ${branch}.`);
1488
1526
  }
1489
- const existing = await openPullRequestUrl();
1490
- if (existing) return { hash: existing };
1491
- const res = await api("POST", "/pulls", {
1492
- body: {
1493
- title: message,
1494
- head: branch,
1495
- base: baseBranch,
1496
- body: "Translation changes proposed from the LiveLocale Studio."
1497
- }
1527
+ const pull = await rest.openOrReusePullRequest({
1528
+ head: branch,
1529
+ base: baseBranch,
1530
+ title: message,
1531
+ body: "Translation changes proposed from the LiveLocale Studio.",
1532
+ updateBodyIfReused: false
1498
1533
  });
1499
- return { hash: res.data.html_url };
1534
+ return { hash: pull.url };
1500
1535
  }
1501
1536
  async function discard() {
1502
1537
  const base = await refSha(baseBranch);