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.
@@ -1067,7 +1067,7 @@ new file mode 100644
1067
1067
  }
1068
1068
  };
1069
1069
 
1070
- // src/server/github-gateway.ts
1070
+ // src/server/github-rest.ts
1071
1071
  var GitHubApiError = class extends Error {
1072
1072
  constructor(message, status) {
1073
1073
  super(message);
@@ -1076,27 +1076,26 @@ var GitHubApiError = class extends Error {
1076
1076
  }
1077
1077
  status;
1078
1078
  };
1079
- var MAX_WRITE_ATTEMPTS = 4;
1080
- var isWriteConflict = (err) => err instanceof GitHubApiError && (err.status === 409 || err.status === 422 && /sha/i.test(err.message));
1081
- var encodePath = (p) => p.split("/").map(encodeURIComponent).join("/");
1082
- var normalizeDir = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
1083
- function mapCompareStatus(status) {
1084
- switch (status) {
1085
- case "added":
1086
- case "copied":
1087
- return "added";
1088
- case "removed":
1089
- return "deleted";
1090
- case "renamed":
1091
- return "renamed";
1092
- default:
1093
- return "modified";
1094
- }
1079
+ var CHUNK_BYTES = 32768;
1080
+ function encodeBase64Utf8(text) {
1081
+ const bytes = new TextEncoder().encode(text);
1082
+ let binary = "";
1083
+ for (let i = 0; i < bytes.length; i += CHUNK_BYTES) {
1084
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_BYTES));
1085
+ }
1086
+ return btoa(binary);
1095
1087
  }
1096
- function createGitHubGateway(config) {
1088
+ function decodeBase64Utf8(base64) {
1089
+ const binary = atob(base64.replace(/\s/g, ""));
1090
+ const bytes = new Uint8Array(binary.length);
1091
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1092
+ return new TextDecoder().decode(bytes);
1093
+ }
1094
+ function encodeRefPath(ref) {
1095
+ return ref.split("/").map(encodeURIComponent).join("/");
1096
+ }
1097
+ function createGitHubRest(config) {
1097
1098
  const repo = config.repo;
1098
- const branch = config.branch;
1099
- const baseBranch = config.baseBranch ?? "main";
1100
1099
  const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
1101
1100
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1102
1101
  const owner = repo.split("/")[0] ?? "";
@@ -1106,17 +1105,18 @@ function createGitHubGateway(config) {
1106
1105
  if (typeof fetchImpl !== "function") {
1107
1106
  throw new Error("[livelocale] The GitHub gateway needs a global fetch (Node 18+).");
1108
1107
  }
1108
+ const headers = (hasBody) => ({
1109
+ Authorization: `Bearer ${config.token}`,
1110
+ Accept: "application/vnd.github+json",
1111
+ "X-GitHub-Api-Version": "2022-11-28",
1112
+ ...hasBody ? { "Content-Type": "application/json" } : {}
1113
+ });
1109
1114
  async function api(method, path5, options = {}) {
1110
1115
  const url = new URL(`${apiBase}/repos/${repo}${path5}`);
1111
1116
  for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
1112
1117
  const response = await fetchImpl(url.toString(), {
1113
1118
  method,
1114
- headers: {
1115
- Authorization: `Bearer ${config.token}`,
1116
- Accept: "application/vnd.github+json",
1117
- "X-GitHub-Api-Version": "2022-11-28",
1118
- ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
1119
- },
1119
+ headers: headers(options.body !== void 0),
1120
1120
  body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
1121
1121
  });
1122
1122
  let data = null;
@@ -1138,12 +1138,7 @@ function createGitHubGateway(config) {
1138
1138
  async function probeStatus(method, path5, body) {
1139
1139
  const response = await fetchImpl(`${apiBase}/repos/${repo}${path5}`, {
1140
1140
  method,
1141
- headers: {
1142
- Authorization: `Bearer ${config.token}`,
1143
- Accept: "application/vnd.github+json",
1144
- "X-GitHub-Api-Version": "2022-11-28",
1145
- ...body !== void 0 ? { "Content-Type": "application/json" } : {}
1146
- },
1141
+ headers: headers(body !== void 0),
1147
1142
  body: body !== void 0 ? JSON.stringify(body) : void 0
1148
1143
  });
1149
1144
  try {
@@ -1152,14 +1147,60 @@ function createGitHubGateway(config) {
1152
1147
  }
1153
1148
  return response.status;
1154
1149
  }
1155
- async function refSha(name) {
1150
+ async function refSha(branch) {
1156
1151
  const res = await api(
1157
1152
  "GET",
1158
- `/git/ref/heads/${encodeURIComponent(name)}`,
1153
+ `/git/ref/heads/${encodeRefPath(branch)}`,
1159
1154
  { allow404: true }
1160
1155
  );
1161
1156
  return res.data?.object?.sha ?? null;
1162
1157
  }
1158
+ async function findOpenPullRequest(head, base) {
1159
+ const res = await api("GET", "/pulls", {
1160
+ query: { head: `${owner}:${head}`, base, state: "open" }
1161
+ });
1162
+ const pull = res.data?.[0];
1163
+ return pull ? { number: pull.number, url: pull.html_url } : null;
1164
+ }
1165
+ async function openOrReusePullRequest(input) {
1166
+ const existing = await findOpenPullRequest(input.head, input.base);
1167
+ if (existing) {
1168
+ if (input.updateBodyIfReused) {
1169
+ await api("PATCH", `/pulls/${existing.number}`, { body: { body: input.body } });
1170
+ }
1171
+ return { ...existing, reused: true };
1172
+ }
1173
+ const res = await api("POST", "/pulls", {
1174
+ body: { title: input.title, head: input.head, base: input.base, body: input.body }
1175
+ });
1176
+ return { number: res.data.number, url: res.data.html_url, reused: false };
1177
+ }
1178
+ return { repo, api, probeStatus, refSha, findOpenPullRequest, openOrReusePullRequest };
1179
+ }
1180
+
1181
+ // src/server/github-gateway.ts
1182
+ var MAX_WRITE_ATTEMPTS = 4;
1183
+ var isWriteConflict = (err) => err instanceof GitHubApiError && (err.status === 409 || err.status === 422 && /sha/i.test(err.message));
1184
+ var encodePath = (p) => p.split("/").map(encodeURIComponent).join("/");
1185
+ var normalizeDir = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
1186
+ function mapCompareStatus(status) {
1187
+ switch (status) {
1188
+ case "added":
1189
+ case "copied":
1190
+ return "added";
1191
+ case "removed":
1192
+ return "deleted";
1193
+ case "renamed":
1194
+ return "renamed";
1195
+ default:
1196
+ return "modified";
1197
+ }
1198
+ }
1199
+ function createGitHubGateway(config) {
1200
+ const rest = createGitHubRest(config);
1201
+ const { repo, api, probeStatus, refSha } = rest;
1202
+ const branch = config.branch;
1203
+ const baseBranch = config.baseBranch ?? "main";
1163
1204
  let branchReady = null;
1164
1205
  function ensureBranch() {
1165
1206
  if (!branchReady) {
@@ -1186,7 +1227,7 @@ function createGitHubGateway(config) {
1186
1227
  async function readFile(path5) {
1187
1228
  const contents = await getContents(path5);
1188
1229
  if (!contents || Array.isArray(contents) || contents.type !== "file") return null;
1189
- const text = Buffer.from(contents.content.replace(/\n/g, ""), "base64").toString("utf-8");
1230
+ const text = decodeBase64Utf8(contents.content);
1190
1231
  return { text, sha: contents.sha };
1191
1232
  }
1192
1233
  async function putFile(path5, text, message, sha) {
@@ -1194,7 +1235,7 @@ function createGitHubGateway(config) {
1194
1235
  await api("PUT", `/contents/${encodePath(path5)}`, {
1195
1236
  body: {
1196
1237
  message,
1197
- content: Buffer.from(text, "utf-8").toString("base64"),
1238
+ content: encodeBase64Utf8(text),
1198
1239
  branch,
1199
1240
  ...sha ? { sha } : {}
1200
1241
  }
@@ -1399,10 +1440,7 @@ function createGitHubGateway(config) {
1399
1440
  return (res.data?.files ?? []).filter((f) => f.filename.startsWith(prefix));
1400
1441
  }
1401
1442
  async function openPullRequestUrl() {
1402
- const res = await api("GET", "/pulls", {
1403
- query: { head: `${owner}:${branch}`, base: baseBranch, state: "open" }
1404
- });
1405
- return res.data?.[0]?.html_url ?? null;
1443
+ return (await rest.findOpenPullRequest(branch, baseBranch))?.url ?? null;
1406
1444
  }
1407
1445
  async function status(basePath) {
1408
1446
  const files = await compareFiles(basePath);
@@ -1433,17 +1471,14 @@ ${f.patch ?? "(binary or too large)"}
1433
1471
  if (files.length === 0) {
1434
1472
  throw new Error(`No changes to propose under ${normalizeDir(basePath)} on ${branch}.`);
1435
1473
  }
1436
- const existing = await openPullRequestUrl();
1437
- if (existing) return { hash: existing };
1438
- const res = await api("POST", "/pulls", {
1439
- body: {
1440
- title: message,
1441
- head: branch,
1442
- base: baseBranch,
1443
- body: "Translation changes proposed from the LiveLocale Studio."
1444
- }
1474
+ const pull = await rest.openOrReusePullRequest({
1475
+ head: branch,
1476
+ base: baseBranch,
1477
+ title: message,
1478
+ body: "Translation changes proposed from the LiveLocale Studio.",
1479
+ updateBodyIfReused: false
1445
1480
  });
1446
- return { hash: res.data.html_url };
1481
+ return { hash: pull.url };
1447
1482
  }
1448
1483
  async function discard() {
1449
1484
  const base = await refSha(baseBranch);