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.
- package/README.md +192 -7
- package/dist/codemod/index.cjs +1692 -593
- package/dist/codemod/index.cjs.map +1 -1
- package/dist/codemod/index.js +1675 -576
- package/dist/codemod/index.js.map +1 -1
- package/dist/server/index.cjs +85 -50
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +85 -50
- package/dist/server/index.js.map +1 -1
- package/dist/studio/assets/index-FFC_lDHv.css +1 -0
- package/dist/studio/assets/index-yI6iF4xN.js +14 -0
- package/dist/studio/index.html +2 -2
- package/dist/vite/index.cjs +25 -24
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.js +7 -6
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/studio/assets/index-Bvbtkry-.css +0 -1
- package/dist/studio/assets/index-DC_jRsox.js +0 -13
package/dist/codemod/index.js
CHANGED
|
@@ -292,11 +292,12 @@ var init_transform = __esm({
|
|
|
292
292
|
});
|
|
293
293
|
|
|
294
294
|
// src/codemod/index.ts
|
|
295
|
-
import * as
|
|
296
|
-
import * as
|
|
295
|
+
import * as fs12 from "fs";
|
|
296
|
+
import * as path18 from "path";
|
|
297
297
|
import * as net from "net";
|
|
298
298
|
|
|
299
299
|
// src/server/module-dir.ts
|
|
300
|
+
import { readFileSync } from "fs";
|
|
300
301
|
import * as path from "path";
|
|
301
302
|
import { fileURLToPath } from "url";
|
|
302
303
|
function moduleDir() {
|
|
@@ -305,6 +306,15 @@ function moduleDir() {
|
|
|
305
306
|
}
|
|
306
307
|
return path.dirname(fileURLToPath(import.meta.url));
|
|
307
308
|
}
|
|
309
|
+
function readOwnVersion() {
|
|
310
|
+
const file = path.resolve(moduleDir(), "../../package.json");
|
|
311
|
+
const pkg = JSON.parse(readFileSync(file, "utf-8"));
|
|
312
|
+
const version = pkg?.version;
|
|
313
|
+
if (typeof version !== "string" || version === "") {
|
|
314
|
+
throw new Error(`No version in ${file}.`);
|
|
315
|
+
}
|
|
316
|
+
return version;
|
|
317
|
+
}
|
|
308
318
|
|
|
309
319
|
// src/codemod/studio-server.ts
|
|
310
320
|
import * as fs from "fs";
|
|
@@ -729,7 +739,7 @@ function isShareManifest(value) {
|
|
|
729
739
|
const layout = own2(value, "layout");
|
|
730
740
|
const locales = own2(value, "locales");
|
|
731
741
|
const sourceLocale = own2(value, "sourceLocale");
|
|
732
|
-
return own2(value, "format") === SHARE_MANIFEST_FORMAT && (repo === void 0 || typeof repo === "string" && isValidRepoSlug(repo)) && isNullOr(own2(value, "defaultBranch"), isValidBranchName) && isNullOr(own2(value, "branch"), isValidBranchName) && isNullOr(own2(value, "base"), (base) => BASE_SHA_RE2.test(base)) && typeof own2(value, "localesPath") === "string" && (layout === "flat" || layout === "namespaced") && isArrayOf(locales, isLocaleCode) && typeof sourceLocale === "string" && locales.includes(sourceLocale) && isNullOr(own2(value, "basePath"), (
|
|
742
|
+
return own2(value, "format") === SHARE_MANIFEST_FORMAT && (repo === void 0 || typeof repo === "string" && isValidRepoSlug(repo)) && isNullOr(own2(value, "defaultBranch"), isValidBranchName) && isNullOr(own2(value, "branch"), isValidBranchName) && isNullOr(own2(value, "base"), (base) => BASE_SHA_RE2.test(base)) && typeof own2(value, "localesPath") === "string" && (layout === "flat" || layout === "namespaced") && isArrayOf(locales, isLocaleCode) && typeof sourceLocale === "string" && locales.includes(sourceLocale) && isNullOr(own2(value, "basePath"), (path19) => path19.startsWith("/") && path19.endsWith("/")) && isArrayOf(own2(value, "codeKeys"), isString);
|
|
733
743
|
}
|
|
734
744
|
|
|
735
745
|
// src/codemod/doctor.ts
|
|
@@ -2233,23 +2243,23 @@ function formatCheckReport(report, limit = 25) {
|
|
|
2233
2243
|
|
|
2234
2244
|
// src/server/pack.ts
|
|
2235
2245
|
import { spawn } from "child_process";
|
|
2236
|
-
import { existsSync as existsSync5, readFileSync as
|
|
2246
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4, promises as fs5 } from "fs";
|
|
2237
2247
|
import * as path8 from "path";
|
|
2238
2248
|
|
|
2239
2249
|
// src/server/build-info.ts
|
|
2240
2250
|
import { execFile } from "child_process";
|
|
2241
|
-
import { readFileSync as
|
|
2251
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2242
2252
|
import * as path6 from "path";
|
|
2243
|
-
var runGit = (args, cwd) => new Promise((
|
|
2253
|
+
var runGit = (args, cwd) => new Promise((resolve16) => {
|
|
2244
2254
|
try {
|
|
2245
2255
|
execFile(
|
|
2246
2256
|
"git",
|
|
2247
2257
|
args,
|
|
2248
2258
|
{ cwd, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
|
|
2249
|
-
(error, stdout) =>
|
|
2259
|
+
(error, stdout) => resolve16(error ? null : String(stdout).trim())
|
|
2250
2260
|
);
|
|
2251
2261
|
} catch {
|
|
2252
|
-
|
|
2262
|
+
resolve16(null);
|
|
2253
2263
|
}
|
|
2254
2264
|
});
|
|
2255
2265
|
var SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
|
|
@@ -2268,7 +2278,7 @@ var asSlug = (value) => isValidRepoSlug(value) ? value : null;
|
|
|
2268
2278
|
function readEvent(eventPath) {
|
|
2269
2279
|
if (!eventPath) return {};
|
|
2270
2280
|
try {
|
|
2271
|
-
const parsed = JSON.parse(
|
|
2281
|
+
const parsed = JSON.parse(readFileSync4(eventPath, "utf-8"));
|
|
2272
2282
|
return isRecord(parsed) ? parsed : {};
|
|
2273
2283
|
} catch {
|
|
2274
2284
|
return {};
|
|
@@ -2367,7 +2377,7 @@ function parseGitHubSlug(remote) {
|
|
|
2367
2377
|
}
|
|
2368
2378
|
|
|
2369
2379
|
// src/server/build-target.ts
|
|
2370
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2380
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2371
2381
|
import * as path7 from "path";
|
|
2372
2382
|
var NEXT_CONFIG_FILES = [
|
|
2373
2383
|
"next.config.js",
|
|
@@ -2389,7 +2399,7 @@ var VITE_BASE = /\bbase\s*:\s*['"`](\/[^'"`]*|\.\/?|)['"`]/;
|
|
|
2389
2399
|
var EXPORTS_POST = /export\s+(?:const|let|var|async\s+function|function)\s+POST\b|export\s*\{[^}]*\bPOST\b[^}]*\}/;
|
|
2390
2400
|
function readText2(file) {
|
|
2391
2401
|
try {
|
|
2392
|
-
return
|
|
2402
|
+
return readFileSync5(file, "utf-8");
|
|
2393
2403
|
} catch {
|
|
2394
2404
|
return null;
|
|
2395
2405
|
}
|
|
@@ -2541,8 +2551,8 @@ var NO_REPO_WARNING = "exposeRepo is on but no GitHub repository was found; the
|
|
|
2541
2551
|
function isPlainObject2(value) {
|
|
2542
2552
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2543
2553
|
}
|
|
2544
|
-
function toPosix(
|
|
2545
|
-
return
|
|
2554
|
+
function toPosix(relative9) {
|
|
2555
|
+
return relative9.split(path8.sep).join("/") || ".";
|
|
2546
2556
|
}
|
|
2547
2557
|
function isInside(parent, child) {
|
|
2548
2558
|
const rel = path8.relative(parent, child);
|
|
@@ -2841,7 +2851,7 @@ function quoteWindowsArg(arg) {
|
|
|
2841
2851
|
}
|
|
2842
2852
|
return out + "\\".repeat(backslashes * 2) + '"';
|
|
2843
2853
|
}
|
|
2844
|
-
var spawnInherit = (command, args, { cwd, env }) => new Promise((
|
|
2854
|
+
var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve16, reject2) => {
|
|
2845
2855
|
const child = process.platform === "win32" ? spawn([command, ...args].map(quoteWindowsArg).join(" "), {
|
|
2846
2856
|
cwd,
|
|
2847
2857
|
env,
|
|
@@ -2849,10 +2859,10 @@ var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve14, reje
|
|
|
2849
2859
|
shell: true
|
|
2850
2860
|
}) : spawn(command, args, { cwd, env, stdio: "inherit" });
|
|
2851
2861
|
child.on("error", (error) => {
|
|
2852
|
-
if (error.code === "ENOENT")
|
|
2862
|
+
if (error.code === "ENOENT") resolve16(127);
|
|
2853
2863
|
else reject2(error);
|
|
2854
2864
|
});
|
|
2855
|
-
child.on("close", (code) =>
|
|
2865
|
+
child.on("close", (code) => resolve16(code ?? 1));
|
|
2856
2866
|
});
|
|
2857
2867
|
var BUILD_USAGE = "Usage: livelocale build [--preview] -- <your build command>";
|
|
2858
2868
|
var NEXT_PREVIEW_WARNING = "Next.js pages have no watermarks without --preview; the Studio will open but nothing is editable.";
|
|
@@ -2861,7 +2871,7 @@ var FRESH_SLACK_MS = 2e3;
|
|
|
2861
2871
|
function gitignoresShareDir(projectDir) {
|
|
2862
2872
|
let content;
|
|
2863
2873
|
try {
|
|
2864
|
-
content =
|
|
2874
|
+
content = readFileSync6(path8.join(projectDir, ".gitignore"), "utf-8");
|
|
2865
2875
|
} catch {
|
|
2866
2876
|
return false;
|
|
2867
2877
|
}
|
|
@@ -2929,122 +2939,12 @@ async function runBuildCommand(options) {
|
|
|
2929
2939
|
}
|
|
2930
2940
|
|
|
2931
2941
|
// src/codemod/apply/cli.ts
|
|
2932
|
-
import * as
|
|
2942
|
+
import * as path16 from "path";
|
|
2933
2943
|
|
|
2934
|
-
// src/codemod/apply/
|
|
2935
|
-
import { execFile as execFile2 } from "child_process";
|
|
2944
|
+
// src/codemod/apply/github-event.ts
|
|
2936
2945
|
import * as fs6 from "fs";
|
|
2937
|
-
import * as path9 from "path";
|
|
2938
|
-
var MAX_APPLY_INPUT_BYTES = 1048576;
|
|
2939
|
-
var TOO_LARGE_MESSAGE2 = "The input is larger than 1 MB.";
|
|
2940
|
-
var NO_INPUT_MESSAGE = 'Pass the change link, a file that holds it, "-" to read stdin, or --clipboard. (cmd.exe cuts arguments at 8,191 characters; use --clipboard or a file for long links.)';
|
|
2941
|
-
var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
|
|
2942
|
-
var ApplyInputError = class extends Error {
|
|
2943
|
-
constructor(message) {
|
|
2944
|
-
super(message);
|
|
2945
|
-
this.name = "ApplyInputError";
|
|
2946
|
-
}
|
|
2947
|
-
};
|
|
2948
|
-
function checkSize(text) {
|
|
2949
|
-
if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
|
|
2950
|
-
throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
2951
|
-
}
|
|
2952
|
-
return text;
|
|
2953
|
-
}
|
|
2954
|
-
async function readStream(stream) {
|
|
2955
|
-
const chunks = [];
|
|
2956
|
-
let size = 0;
|
|
2957
|
-
for await (const chunk of stream) {
|
|
2958
|
-
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
|
|
2959
|
-
size += bytes.length;
|
|
2960
|
-
if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
2961
|
-
chunks.push(bytes);
|
|
2962
|
-
}
|
|
2963
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
2964
|
-
}
|
|
2965
|
-
function existingFile(arg, cwd) {
|
|
2966
|
-
try {
|
|
2967
|
-
const resolved = path9.resolve(cwd, arg);
|
|
2968
|
-
return fs6.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
|
|
2969
|
-
} catch {
|
|
2970
|
-
return null;
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
async function readApplyInput(arg, options) {
|
|
2974
|
-
if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
|
|
2975
|
-
if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
|
|
2976
|
-
if (arg === "-") return readStream(options.stdin ?? process.stdin);
|
|
2977
|
-
const file = existingFile(arg, options.cwd);
|
|
2978
|
-
if (file === null) return checkSize(arg);
|
|
2979
|
-
if (fs6.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
2980
|
-
return checkSize(fs6.readFileSync(file, "utf8"));
|
|
2981
|
-
}
|
|
2982
|
-
function clipboardCommands(platform) {
|
|
2983
|
-
if (platform === "win32") {
|
|
2984
|
-
return [
|
|
2985
|
-
["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
|
|
2986
|
-
];
|
|
2987
|
-
}
|
|
2988
|
-
if (platform === "darwin") return [["pbpaste", []]];
|
|
2989
|
-
return [
|
|
2990
|
-
["wl-paste", ["--no-newline"]],
|
|
2991
|
-
["xclip", ["-selection", "clipboard", "-o"]],
|
|
2992
|
-
["xsel", ["--clipboard", "--output"]]
|
|
2993
|
-
];
|
|
2994
|
-
}
|
|
2995
|
-
var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
|
|
2996
|
-
function run(execFileImpl, [command, args]) {
|
|
2997
|
-
return new Promise((resolve14) => {
|
|
2998
|
-
try {
|
|
2999
|
-
execFileImpl(
|
|
3000
|
-
command,
|
|
3001
|
-
args,
|
|
3002
|
-
{ encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
|
|
3003
|
-
(error, stdout) => {
|
|
3004
|
-
const code = error?.code;
|
|
3005
|
-
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve14(TOO_LARGE);
|
|
3006
|
-
else resolve14(error ? null : String(stdout));
|
|
3007
|
-
}
|
|
3008
|
-
);
|
|
3009
|
-
} catch {
|
|
3010
|
-
resolve14(null);
|
|
3011
|
-
}
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
async function readClipboard(execFileImpl = execFile2, platform = process.platform) {
|
|
3015
|
-
for (const command of clipboardCommands(platform)) {
|
|
3016
|
-
const text = await run(execFileImpl, command);
|
|
3017
|
-
if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
3018
|
-
if (text !== null) return checkSize(text);
|
|
3019
|
-
}
|
|
3020
|
-
throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
|
|
3021
|
-
}
|
|
3022
|
-
|
|
3023
|
-
// src/codemod/apply/local.ts
|
|
3024
|
-
import { execFile as execFile4, spawn as spawn2 } from "child_process";
|
|
3025
|
-
import * as fs9 from "fs";
|
|
3026
2946
|
import * as os from "os";
|
|
3027
|
-
import * as
|
|
3028
|
-
|
|
3029
|
-
// src/core/change-link-node.ts
|
|
3030
|
-
import { inflateSync } from "zlib";
|
|
3031
|
-
var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
|
|
3032
|
-
var TOO_LARGE_MESSAGE3 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
|
|
3033
|
-
var nodeInflate = async (bytes, maxOutputLength) => {
|
|
3034
|
-
let result;
|
|
3035
|
-
try {
|
|
3036
|
-
result = inflateSync(bytes, { maxOutputLength, info: true });
|
|
3037
|
-
} catch (err) {
|
|
3038
|
-
if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
|
|
3039
|
-
throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE3);
|
|
3040
|
-
}
|
|
3041
|
-
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3042
|
-
}
|
|
3043
|
-
if (result.engine.bytesWritten !== bytes.length) {
|
|
3044
|
-
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3045
|
-
}
|
|
3046
|
-
return result.buffer;
|
|
3047
|
-
};
|
|
2947
|
+
import * as path10 from "path";
|
|
3048
2948
|
|
|
3049
2949
|
// src/client/watermark.ts
|
|
3050
2950
|
var WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
|
|
@@ -3112,6 +3012,11 @@ function detectJsonIndent(content) {
|
|
|
3112
3012
|
const ws = match[1];
|
|
3113
3013
|
return ws.includes(" ") ? " " : ws;
|
|
3114
3014
|
}
|
|
3015
|
+
function stringifyLike(data, content) {
|
|
3016
|
+
const indent = content === null ? " " : detectJsonIndent(content);
|
|
3017
|
+
const trailing = content === null || content.endsWith("\n") ? "\n" : "";
|
|
3018
|
+
return JSON.stringify(data, null, indent) + trailing;
|
|
3019
|
+
}
|
|
3115
3020
|
|
|
3116
3021
|
// src/core/save-rules.ts
|
|
3117
3022
|
var reject = (status, code, error) => ({
|
|
@@ -3317,113 +3222,1064 @@ function planApply(input) {
|
|
|
3317
3222
|
return { edits, counts };
|
|
3318
3223
|
}
|
|
3319
3224
|
|
|
3320
|
-
// src/
|
|
3321
|
-
import {
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3225
|
+
// src/core/change-link-node.ts
|
|
3226
|
+
import { inflateSync } from "zlib";
|
|
3227
|
+
var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
|
|
3228
|
+
var TOO_LARGE_MESSAGE2 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
|
|
3229
|
+
var nodeInflate = async (bytes, maxOutputLength) => {
|
|
3230
|
+
let result;
|
|
3231
|
+
try {
|
|
3232
|
+
result = inflateSync(bytes, { maxOutputLength, info: true });
|
|
3233
|
+
} catch (err) {
|
|
3234
|
+
if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
|
|
3235
|
+
throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE2);
|
|
3236
|
+
}
|
|
3237
|
+
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3329
3238
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
if (!filePath.startsWith(resolvedBasePath)) {
|
|
3333
|
-
throw new Error("Path traversal attempt");
|
|
3239
|
+
if (result.engine.bytesWritten !== bytes.length) {
|
|
3240
|
+
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3334
3241
|
}
|
|
3335
|
-
return
|
|
3336
|
-
}
|
|
3242
|
+
return result.buffer;
|
|
3243
|
+
};
|
|
3337
3244
|
|
|
3338
|
-
// src/server/
|
|
3339
|
-
var
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
this.
|
|
3344
|
-
() => {
|
|
3345
|
-
},
|
|
3346
|
-
() => {
|
|
3347
|
-
}
|
|
3348
|
-
);
|
|
3349
|
-
return next;
|
|
3245
|
+
// src/server/github-rest.ts
|
|
3246
|
+
var GitHubApiError = class extends Error {
|
|
3247
|
+
constructor(message, status) {
|
|
3248
|
+
super(message);
|
|
3249
|
+
this.status = status;
|
|
3250
|
+
this.name = "GitHubApiError";
|
|
3350
3251
|
}
|
|
3252
|
+
status;
|
|
3351
3253
|
};
|
|
3352
|
-
var
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
this.requireEnabled = options.requireEnabled ?? true;
|
|
3359
|
-
this.quiet = options.quiet ?? false;
|
|
3360
|
-
this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
|
|
3361
|
-
}
|
|
3362
|
-
assertEnabled() {
|
|
3363
|
-
if (!this.requireEnabled) return;
|
|
3364
|
-
const reason = getDisabledReason();
|
|
3365
|
-
if (reason !== null) {
|
|
3366
|
-
throw new Error(
|
|
3367
|
-
`[livelocale] FileMutator can only be executed in development mode (${reason}).`
|
|
3368
|
-
);
|
|
3369
|
-
}
|
|
3254
|
+
var CHUNK_BYTES = 32768;
|
|
3255
|
+
function encodeBase64Utf8(text) {
|
|
3256
|
+
const bytes = new TextEncoder().encode(text);
|
|
3257
|
+
let binary = "";
|
|
3258
|
+
for (let i = 0; i < bytes.length; i += CHUNK_BYTES) {
|
|
3259
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_BYTES));
|
|
3370
3260
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3261
|
+
return btoa(binary);
|
|
3262
|
+
}
|
|
3263
|
+
function decodeBase64Utf8(base64) {
|
|
3264
|
+
const binary = atob(base64.replace(/\s/g, ""));
|
|
3265
|
+
const bytes = new Uint8Array(binary.length);
|
|
3266
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
3267
|
+
return new TextDecoder().decode(bytes);
|
|
3268
|
+
}
|
|
3269
|
+
function isPullRequestCreationForbidden(err) {
|
|
3270
|
+
return err instanceof GitHubApiError && err.status === 403 && /not permitted to create or approve pull requests/i.test(err.message);
|
|
3271
|
+
}
|
|
3272
|
+
function encodeRefPath(ref) {
|
|
3273
|
+
return ref.split("/").map(encodeURIComponent).join("/");
|
|
3274
|
+
}
|
|
3275
|
+
function createGitHubRest(config) {
|
|
3276
|
+
const repo = config.repo;
|
|
3277
|
+
const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
|
|
3278
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
3279
|
+
const owner = repo.split("/")[0] ?? "";
|
|
3280
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
3281
|
+
throw new Error(`[livelocale] github.repo must be "owner/name", got "${repo}".`);
|
|
3282
|
+
}
|
|
3283
|
+
if (typeof fetchImpl !== "function") {
|
|
3284
|
+
throw new Error("[livelocale] The GitHub gateway needs a global fetch (Node 18+).");
|
|
3285
|
+
}
|
|
3286
|
+
const headers = (hasBody) => ({
|
|
3287
|
+
Authorization: `Bearer ${config.token}`,
|
|
3288
|
+
Accept: "application/vnd.github+json",
|
|
3289
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
3290
|
+
...hasBody ? { "Content-Type": "application/json" } : {}
|
|
3291
|
+
});
|
|
3292
|
+
async function api(method, path19, options = {}) {
|
|
3293
|
+
const url = new URL(`${apiBase}/repos/${repo}${path19}`);
|
|
3294
|
+
for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
|
|
3295
|
+
const response = await fetchImpl(url.toString(), {
|
|
3296
|
+
method,
|
|
3297
|
+
headers: headers(options.body !== void 0),
|
|
3298
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
3299
|
+
});
|
|
3300
|
+
let data = null;
|
|
3301
|
+
try {
|
|
3302
|
+
data = await response.json();
|
|
3303
|
+
} catch {
|
|
3379
3304
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
async updateLocaleKey(basePath, locale, keyPath, newValue) {
|
|
3388
|
-
this.assertEnabled();
|
|
3389
|
-
if (!isSafeLocaleSegment(locale)) {
|
|
3390
|
-
throw new Error(
|
|
3391
|
-
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
3305
|
+
if (response.status === 404 && options.allow404) return { status: 404, data: null };
|
|
3306
|
+
if (!response.ok) {
|
|
3307
|
+
const detail = data?.message || "request failed";
|
|
3308
|
+
const hint = /not accessible by/i.test(detail) ? ' The token lacks a permission: give it "Contents" and "Pull requests" with "Read and write" on this repository.' : "";
|
|
3309
|
+
throw new GitHubApiError(
|
|
3310
|
+
`GitHub API ${response.status} on ${method} ${path19}: ${detail}.${hint}`,
|
|
3311
|
+
response.status
|
|
3392
3312
|
);
|
|
3393
3313
|
}
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3314
|
+
return { status: response.status, data };
|
|
3315
|
+
}
|
|
3316
|
+
async function probeStatus(method, path19, body) {
|
|
3317
|
+
const response = await fetchImpl(`${apiBase}/repos/${repo}${path19}`, {
|
|
3318
|
+
method,
|
|
3319
|
+
headers: headers(body !== void 0),
|
|
3320
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
3321
|
+
});
|
|
3399
3322
|
try {
|
|
3400
|
-
|
|
3401
|
-
if (stats.isDirectory()) {
|
|
3402
|
-
isNamespaceDir = true;
|
|
3403
|
-
}
|
|
3323
|
+
await response.arrayBuffer();
|
|
3404
3324
|
} catch {
|
|
3405
3325
|
}
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3326
|
+
return response.status;
|
|
3327
|
+
}
|
|
3328
|
+
async function refSha(branch) {
|
|
3329
|
+
const res = await api(
|
|
3330
|
+
"GET",
|
|
3331
|
+
`/git/ref/heads/${encodeRefPath(branch)}`,
|
|
3332
|
+
{ allow404: true }
|
|
3333
|
+
);
|
|
3334
|
+
return res.data?.object?.sha ?? null;
|
|
3335
|
+
}
|
|
3336
|
+
async function findOpenPullRequest(head, base) {
|
|
3337
|
+
const res = await api("GET", "/pulls", {
|
|
3338
|
+
query: { head: `${owner}:${head}`, base, state: "open" }
|
|
3339
|
+
});
|
|
3340
|
+
const pull = res.data?.[0];
|
|
3341
|
+
return pull ? { number: pull.number, url: pull.html_url } : null;
|
|
3342
|
+
}
|
|
3343
|
+
async function openOrReusePullRequest(input) {
|
|
3344
|
+
const existing = await findOpenPullRequest(input.head, input.base);
|
|
3345
|
+
if (existing) {
|
|
3346
|
+
if (input.updateBodyIfReused) {
|
|
3347
|
+
await api("PATCH", `/pulls/${existing.number}`, { body: { body: input.body } });
|
|
3420
3348
|
}
|
|
3421
|
-
|
|
3422
|
-
filePath = path11.resolve(resolvedBasePath, `${locale}.json`);
|
|
3349
|
+
return { ...existing, reused: true };
|
|
3423
3350
|
}
|
|
3424
|
-
|
|
3351
|
+
const res = await api("POST", "/pulls", {
|
|
3352
|
+
body: { title: input.title, head: input.head, base: input.base, body: input.body }
|
|
3353
|
+
});
|
|
3354
|
+
return { number: res.data.number, url: res.data.html_url, reused: false };
|
|
3355
|
+
}
|
|
3356
|
+
return { repo, api, probeStatus, refSha, findOpenPullRequest, openOrReusePullRequest };
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// src/codemod/apply/github-commit.ts
|
|
3360
|
+
import * as path9 from "path";
|
|
3361
|
+
var BRANCH_PREFIX = "livelocale/";
|
|
3362
|
+
function normalizeRepoDir(p) {
|
|
3363
|
+
const posix2 = p.replace(/\\/g, "/");
|
|
3364
|
+
if (posix2.startsWith("/") || /^[A-Za-z]:/.test(posix2)) {
|
|
3365
|
+
throw new Error(`[livelocale] The locales path must be relative to the repository: "${p}".`);
|
|
3366
|
+
}
|
|
3367
|
+
const segments = posix2.split("/").filter((s) => s !== "" && s !== ".");
|
|
3368
|
+
if (segments.includes("..")) {
|
|
3369
|
+
throw new Error(`[livelocale] The locales path must stay inside the repository: "${p}".`);
|
|
3370
|
+
}
|
|
3371
|
+
return segments.join("/");
|
|
3372
|
+
}
|
|
3373
|
+
function joinRepoPath(dir, rest) {
|
|
3374
|
+
return dir === "" ? rest : `${dir}/${rest}`;
|
|
3375
|
+
}
|
|
3376
|
+
function localeFileFor(localesDir, layout, locale, key) {
|
|
3377
|
+
const dir = normalizeRepoDir(localesDir);
|
|
3378
|
+
if (layout === "flat") return { file: joinRepoPath(dir, `${locale}.json`), innerKey: key };
|
|
3379
|
+
const dot = key.indexOf(".");
|
|
3380
|
+
if (dot <= 0 || dot === key.length - 1) {
|
|
3381
|
+
throw new Error(`[livelocale] "${key}" names no key inside a namespace file.`);
|
|
3382
|
+
}
|
|
3383
|
+
return {
|
|
3384
|
+
file: joinRepoPath(dir, `${locale}/${key.slice(0, dot)}.json`),
|
|
3385
|
+
innerKey: key.slice(dot + 1)
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
function isPlainObject4(value) {
|
|
3389
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3390
|
+
}
|
|
3391
|
+
function applyEditsToText(text, edits) {
|
|
3392
|
+
const data = JSON.parse(text);
|
|
3393
|
+
if (!isPlainObject4(data)) throw new Error("[livelocale] A locale file is not a JSON object.");
|
|
3394
|
+
for (const edit of edits) setNestedProperty(data, edit.innerKey, edit.value);
|
|
3395
|
+
return stringifyLike(data, text);
|
|
3396
|
+
}
|
|
3397
|
+
async function listDir(rest, dir, ref) {
|
|
3398
|
+
const res = await rest.api("GET", `/contents/${encodeRefPath(dir)}`, {
|
|
3399
|
+
query: { ref },
|
|
3400
|
+
allow404: true
|
|
3401
|
+
});
|
|
3402
|
+
return Array.isArray(res.data) ? res.data : null;
|
|
3403
|
+
}
|
|
3404
|
+
async function readBlob(rest, sha) {
|
|
3405
|
+
const res = await rest.api(
|
|
3406
|
+
"GET",
|
|
3407
|
+
`/git/blobs/${encodeURIComponent(sha)}`
|
|
3408
|
+
);
|
|
3409
|
+
return decodeBase64Utf8(res.data?.content ?? "");
|
|
3410
|
+
}
|
|
3411
|
+
function parseLocaleFile(text, file) {
|
|
3412
|
+
let data;
|
|
3413
|
+
try {
|
|
3414
|
+
data = JSON.parse(text);
|
|
3415
|
+
} catch {
|
|
3416
|
+
throw new Error(`[livelocale] ${file} is not valid JSON.`);
|
|
3417
|
+
}
|
|
3418
|
+
if (!isPlainObject4(data)) throw new Error(`[livelocale] ${file} is not a JSON object.`);
|
|
3419
|
+
return data;
|
|
3420
|
+
}
|
|
3421
|
+
async function readRemoteLocales(rest, commitSha, localesDir, locales) {
|
|
3422
|
+
const dir = normalizeRepoDir(localesDir);
|
|
3423
|
+
const entries = await listDir(rest, dir, commitSha);
|
|
3424
|
+
if (entries === null) {
|
|
3425
|
+
throw new Error(`[livelocale] No locales folder at ${dir || "."} on the target.`);
|
|
3426
|
+
}
|
|
3427
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3428
|
+
const files = /* @__PURE__ */ new Map();
|
|
3429
|
+
for (const locale of locales) {
|
|
3430
|
+
if (targets.has(locale)) continue;
|
|
3431
|
+
const flat = entries.find((e) => e.type === "file" && e.name === `${locale}.json`);
|
|
3432
|
+
const folder = entries.find((e) => e.type === "dir" && e.name === locale);
|
|
3433
|
+
if (!isValidLocaleFormat(locale) || !flat && !folder) {
|
|
3434
|
+
targets.set(locale, null);
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
if (flat) {
|
|
3438
|
+
const file = joinRepoPath(dir, flat.name);
|
|
3439
|
+
const text = await readBlob(rest, flat.sha);
|
|
3440
|
+
files.set(file, text);
|
|
3441
|
+
targets.set(locale, { layout: "flat", dict: parseLocaleFile(text, file) });
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
const dict = {};
|
|
3445
|
+
for (const entry of await listDir(rest, joinRepoPath(dir, locale), commitSha) ?? []) {
|
|
3446
|
+
if (entry.type !== "file" || !entry.name.endsWith(".json")) continue;
|
|
3447
|
+
const file = joinRepoPath(dir, `${locale}/${entry.name}`);
|
|
3448
|
+
const text = await readBlob(rest, entry.sha);
|
|
3449
|
+
files.set(file, text);
|
|
3450
|
+
dict[entry.name.slice(0, -".json".length)] = parseLocaleFile(text, file);
|
|
3451
|
+
}
|
|
3452
|
+
targets.set(locale, { layout: "namespaced", dict });
|
|
3453
|
+
}
|
|
3454
|
+
return { sha: commitSha, targets, files };
|
|
3455
|
+
}
|
|
3456
|
+
function buildFileChanges(localesDir, plan, remote, baseFiles) {
|
|
3457
|
+
const dir = normalizeRepoDir(localesDir);
|
|
3458
|
+
const prefix = dir === "" ? "" : `${dir}/`;
|
|
3459
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
3460
|
+
for (const edit of plan.edits) {
|
|
3461
|
+
if (edit.status !== "apply") continue;
|
|
3462
|
+
const layout = remote.targets.get(edit.locale)?.layout ?? "flat";
|
|
3463
|
+
const { file, innerKey } = localeFileFor(dir, layout, edit.locale, edit.key);
|
|
3464
|
+
const normalized = path9.posix.normalize(file);
|
|
3465
|
+
if (normalized !== file || normalized.startsWith("../") || normalized.startsWith("/") || !normalized.startsWith(prefix)) {
|
|
3425
3466
|
throw new Error(
|
|
3426
|
-
`[livelocale]
|
|
3467
|
+
`[livelocale] Refusing to write ${file}: it is outside the locales folder ${dir || "."}.`
|
|
3468
|
+
);
|
|
3469
|
+
}
|
|
3470
|
+
const list = byFile.get(file) ?? [];
|
|
3471
|
+
list.push({ innerKey, value: edit.to });
|
|
3472
|
+
byFile.set(file, list);
|
|
3473
|
+
}
|
|
3474
|
+
const changes = /* @__PURE__ */ new Map();
|
|
3475
|
+
for (const [file, edits] of byFile) {
|
|
3476
|
+
const text = baseFiles?.get(file) ?? remote.files.get(file);
|
|
3477
|
+
if (text === void 0) {
|
|
3478
|
+
throw new Error(`[livelocale] ${file} was not read from the target.`);
|
|
3479
|
+
}
|
|
3480
|
+
const next = applyEditsToText(text, edits);
|
|
3481
|
+
if (next !== text) changes.set(file, next);
|
|
3482
|
+
}
|
|
3483
|
+
return changes;
|
|
3484
|
+
}
|
|
3485
|
+
async function prepareBranch(rest, baseName, base, targetSha, runId) {
|
|
3486
|
+
const head = await rest.refSha(baseName);
|
|
3487
|
+
if (head === null) return { branch: baseName, parentSha: targetSha, mode: "new" };
|
|
3488
|
+
if (await rest.findOpenPullRequest(baseName, base)) {
|
|
3489
|
+
return { branch: baseName, parentSha: head, mode: "append" };
|
|
3490
|
+
}
|
|
3491
|
+
return { branch: `${baseName}-${runId}`, parentSha: targetSha, mode: "new" };
|
|
3492
|
+
}
|
|
3493
|
+
async function commitFiles(rest, input) {
|
|
3494
|
+
const { branch, parentSha, mode } = input.branchPlan;
|
|
3495
|
+
if (!branch.startsWith(BRANCH_PREFIX)) {
|
|
3496
|
+
throw new Error(`[livelocale] Refusing to write "${branch}": only ${BRANCH_PREFIX}* branches.`);
|
|
3497
|
+
}
|
|
3498
|
+
if (input.files.size === 0) return { sha: null, unchanged: true };
|
|
3499
|
+
const parent = await rest.api(
|
|
3500
|
+
"GET",
|
|
3501
|
+
`/git/commits/${encodeURIComponent(parentSha)}`
|
|
3502
|
+
);
|
|
3503
|
+
const baseTree = parent.data.tree.sha;
|
|
3504
|
+
const entries = [];
|
|
3505
|
+
for (const [file, text] of input.files) {
|
|
3506
|
+
const blob = await rest.api("POST", "/git/blobs", {
|
|
3507
|
+
body: { content: encodeBase64Utf8(text), encoding: "base64" }
|
|
3508
|
+
});
|
|
3509
|
+
entries.push({ path: file, mode: "100644", type: "blob", sha: blob.data.sha });
|
|
3510
|
+
}
|
|
3511
|
+
const tree = await rest.api("POST", "/git/trees", {
|
|
3512
|
+
body: { base_tree: baseTree, tree: entries }
|
|
3513
|
+
});
|
|
3514
|
+
const treeSha = tree.data.sha;
|
|
3515
|
+
if (treeSha === baseTree) return { sha: null, unchanged: true };
|
|
3516
|
+
const commit = await rest.api("POST", "/git/commits", {
|
|
3517
|
+
body: { message: input.message, tree: treeSha, parents: [parentSha], author: input.author }
|
|
3518
|
+
});
|
|
3519
|
+
const sha = commit.data.sha;
|
|
3520
|
+
if (mode === "new") {
|
|
3521
|
+
await rest.api("POST", "/git/refs", { body: { ref: `refs/heads/${branch}`, sha } });
|
|
3522
|
+
} else {
|
|
3523
|
+
await rest.api("PATCH", `/git/refs/heads/${encodeRefPath(branch)}`, {
|
|
3524
|
+
body: { sha, force: false }
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
return { sha };
|
|
3528
|
+
}
|
|
3529
|
+
function noreplyAuthor(actor) {
|
|
3530
|
+
return { name: actor.login, email: `${actor.id}+${actor.login}@users.noreply.github.com` };
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
// src/codemod/apply/github-trigger.ts
|
|
3534
|
+
var APPLY_COMMAND_RE = /^\/livelocale\s+apply(\s+--force)?\s*$/;
|
|
3535
|
+
var COMMAND_PREFIX = "/livelocale";
|
|
3536
|
+
var ISSUE_BRANCH_RE = /^livelocale\/issue-(\d+)(?:-\d+)?$/;
|
|
3537
|
+
var TRUSTED_ASSOCIATIONS = /* @__PURE__ */ new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
|
3538
|
+
var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["admin", "write"]);
|
|
3539
|
+
var TRUSTED_ROLES = /* @__PURE__ */ new Set(["admin", "maintain", "write"]);
|
|
3540
|
+
var ignore = (reason) => ({ kind: "ignore", reason });
|
|
3541
|
+
function actorOf(user) {
|
|
3542
|
+
return { login: String(user?.login ?? ""), id: Number(user?.id ?? 0) };
|
|
3543
|
+
}
|
|
3544
|
+
function textOf(value) {
|
|
3545
|
+
return typeof value === "string" ? value : "";
|
|
3546
|
+
}
|
|
3547
|
+
function holdsChangeToken(text) {
|
|
3548
|
+
try {
|
|
3549
|
+
findChangeToken(text);
|
|
3550
|
+
return true;
|
|
3551
|
+
} catch {
|
|
3552
|
+
return false;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
function parseTrigger(eventName, event) {
|
|
3556
|
+
const repo = textOf(event?.repository?.full_name);
|
|
3557
|
+
const defaultBranch = textOf(event?.repository?.default_branch);
|
|
3558
|
+
switch (eventName) {
|
|
3559
|
+
case "issues": {
|
|
3560
|
+
if (event?.action !== "opened") return ignore("issue not opened");
|
|
3561
|
+
const issue = event.issue;
|
|
3562
|
+
const body = textOf(issue?.body);
|
|
3563
|
+
if (!body.includes(CHANGE_BLOCK_OPEN)) return ignore("no change block");
|
|
3564
|
+
return {
|
|
3565
|
+
kind: "apply",
|
|
3566
|
+
via: "issue",
|
|
3567
|
+
issueNumber: Number(issue.number),
|
|
3568
|
+
actor: actorOf(issue.user),
|
|
3569
|
+
association: issue.author_association ?? null,
|
|
3570
|
+
text: body,
|
|
3571
|
+
force: false,
|
|
3572
|
+
repo,
|
|
3573
|
+
defaultBranch
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
case "issue_comment": {
|
|
3577
|
+
if (event?.action !== "created") return ignore("comment not created");
|
|
3578
|
+
const issue = event.issue;
|
|
3579
|
+
const comment = event.comment;
|
|
3580
|
+
if (issue?.pull_request) return ignore("comment on a pull request");
|
|
3581
|
+
if (comment?.user?.type === "Bot") return ignore("comment by a bot");
|
|
3582
|
+
const body = textOf(comment?.body);
|
|
3583
|
+
const firstLine = body.split(/\r\n|\r|\n/)[0] ?? "";
|
|
3584
|
+
if (!firstLine.startsWith(COMMAND_PREFIX)) return ignore("not a livelocale command");
|
|
3585
|
+
const command = APPLY_COMMAND_RE.exec(firstLine);
|
|
3586
|
+
if (!command) return ignore("unknown command");
|
|
3587
|
+
return {
|
|
3588
|
+
kind: "apply",
|
|
3589
|
+
via: "comment",
|
|
3590
|
+
issueNumber: Number(issue.number),
|
|
3591
|
+
// The gate checks the commenter, never the issue author.
|
|
3592
|
+
actor: actorOf(comment.user),
|
|
3593
|
+
association: comment.author_association ?? null,
|
|
3594
|
+
text: holdsChangeToken(body) ? body : textOf(issue?.body),
|
|
3595
|
+
force: command[1] !== void 0,
|
|
3596
|
+
repo,
|
|
3597
|
+
defaultBranch
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
case "workflow_dispatch":
|
|
3601
|
+
return {
|
|
3602
|
+
kind: "apply",
|
|
3603
|
+
via: "dispatch",
|
|
3604
|
+
issueNumber: null,
|
|
3605
|
+
actor: actorOf(event?.sender),
|
|
3606
|
+
association: null,
|
|
3607
|
+
text: textOf(event?.inputs?.link),
|
|
3608
|
+
force: false,
|
|
3609
|
+
repo,
|
|
3610
|
+
defaultBranch
|
|
3611
|
+
};
|
|
3612
|
+
case "pull_request": {
|
|
3613
|
+
if (event?.action !== "closed") return ignore("pull request not closed");
|
|
3614
|
+
const pull = event.pull_request;
|
|
3615
|
+
const branch = ISSUE_BRANCH_RE.exec(textOf(pull?.head?.ref));
|
|
3616
|
+
if (!branch) return ignore("not a livelocale issue branch");
|
|
3617
|
+
if (textOf(pull?.head?.repo?.full_name) !== repo || repo === "") {
|
|
3618
|
+
return ignore("pull request from another repository");
|
|
3619
|
+
}
|
|
3620
|
+
return {
|
|
3621
|
+
kind: "close-issue",
|
|
3622
|
+
issueNumber: Number(branch[1]),
|
|
3623
|
+
prUrl: textOf(pull.html_url),
|
|
3624
|
+
merged: pull.merged === true,
|
|
3625
|
+
repo
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
default:
|
|
3629
|
+
return ignore(`unsupported event ${eventName}`);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
async function isTrustedActor(rest, login, association) {
|
|
3633
|
+
if (association !== null && TRUSTED_ASSOCIATIONS.has(association)) return true;
|
|
3634
|
+
try {
|
|
3635
|
+
const res = await rest.api(
|
|
3636
|
+
"GET",
|
|
3637
|
+
`/collaborators/${encodeURIComponent(login)}/permission`
|
|
3638
|
+
);
|
|
3639
|
+
const permission = res.data?.permission ?? "";
|
|
3640
|
+
const role = res.data?.role_name ?? "";
|
|
3641
|
+
return TRUSTED_PERMISSIONS.has(permission) || TRUSTED_ROLES.has(role);
|
|
3642
|
+
} catch (err) {
|
|
3643
|
+
if (err instanceof GitHubApiError && (err.status === 403 || err.status === 404)) return false;
|
|
3644
|
+
throw err;
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
function chooseTargetBranch(requested, allowList, defaultBranch) {
|
|
3648
|
+
if (!requested) return { branch: defaultBranch, reason: "default" };
|
|
3649
|
+
const allowed = allowList.length > 0 ? allowList : [defaultBranch];
|
|
3650
|
+
if (allowed.includes(requested)) return { branch: requested, reason: "link" };
|
|
3651
|
+
return { branch: defaultBranch, reason: "not-allowed" };
|
|
3652
|
+
}
|
|
3653
|
+
function branchBaseName(t, runId) {
|
|
3654
|
+
return t.issueNumber !== null ? `livelocale/issue-${t.issueNumber}` : `livelocale/run-${runId}`;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
// src/codemod/apply/report.ts
|
|
3658
|
+
var DEFAULT_SPAN_LENGTH = 120;
|
|
3659
|
+
var DEFAULT_TABLE_ROWS = 100;
|
|
3660
|
+
var MAX_BODY_CHARS = 6e4;
|
|
3661
|
+
var MAX_NOTE_CHARS2 = 2e3;
|
|
3662
|
+
var MAX_NAME_CHARS = 64;
|
|
3663
|
+
var MAX_CHECK_LINES = 50;
|
|
3664
|
+
var MAX_CHECK_SUMMARY_CHARS = 1e4;
|
|
3665
|
+
var BODY_MARGIN = 200;
|
|
3666
|
+
var STATUS_LABELS = {
|
|
3667
|
+
apply: "applied",
|
|
3668
|
+
already: "already done",
|
|
3669
|
+
conflict: "conflict",
|
|
3670
|
+
"needs-force": "needs --force",
|
|
3671
|
+
invalid: "rejected"
|
|
3672
|
+
};
|
|
3673
|
+
var BIDI_CONTROL_RE = /[--]/g;
|
|
3674
|
+
var NEWLINE_RE = /\r\n|\r|\n/g;
|
|
3675
|
+
var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
3676
|
+
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
3677
|
+
function cut(text, max) {
|
|
3678
|
+
if (text.length <= max) return text;
|
|
3679
|
+
let out = text.slice(0, max);
|
|
3680
|
+
const last = out.charCodeAt(out.length - 1);
|
|
3681
|
+
if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
|
|
3682
|
+
return out;
|
|
3683
|
+
}
|
|
3684
|
+
function cutLines(text, max) {
|
|
3685
|
+
if (text.length <= max) return text;
|
|
3686
|
+
const kept = [];
|
|
3687
|
+
let length = 0;
|
|
3688
|
+
for (const line of text.split("\n")) {
|
|
3689
|
+
const added = (kept.length > 0 ? 1 : 0) + line.length;
|
|
3690
|
+
if (length + added + 2 > max) break;
|
|
3691
|
+
kept.push(line);
|
|
3692
|
+
length += added;
|
|
3693
|
+
}
|
|
3694
|
+
return [...kept, "\u2026"].join("\n");
|
|
3695
|
+
}
|
|
3696
|
+
function longestBacktickRun(text) {
|
|
3697
|
+
let longest = 0;
|
|
3698
|
+
for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
|
|
3699
|
+
return longest;
|
|
3700
|
+
}
|
|
3701
|
+
function codePointLabel(ch) {
|
|
3702
|
+
return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
|
|
3703
|
+
}
|
|
3704
|
+
function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
|
|
3705
|
+
const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
|
|
3706
|
+
const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
|
|
3707
|
+
if (content === "") return "` `";
|
|
3708
|
+
const fence = "`".repeat(longestBacktickRun(content) + 1);
|
|
3709
|
+
const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
|
|
3710
|
+
return `${fence}${pad}${content}${pad}${fence}`;
|
|
3711
|
+
}
|
|
3712
|
+
function sanitizeName(name) {
|
|
3713
|
+
if (name === void 0) return void 0;
|
|
3714
|
+
const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
|
|
3715
|
+
const result = cut(cleaned, MAX_NAME_CHARS).trim();
|
|
3716
|
+
return result === "" ? void 0 : result;
|
|
3717
|
+
}
|
|
3718
|
+
var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
|
|
3719
|
+
function summarizeCheck(report, edited) {
|
|
3720
|
+
const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
|
|
3721
|
+
const mine = report.issues.filter(
|
|
3722
|
+
(issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
|
|
3723
|
+
);
|
|
3724
|
+
if (mine.length === 0) return NO_CHECK_PROBLEMS;
|
|
3725
|
+
const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
|
|
3726
|
+
for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
|
|
3727
|
+
lines.push(
|
|
3728
|
+
`- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
|
|
3729
|
+
);
|
|
3730
|
+
}
|
|
3731
|
+
if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
|
|
3732
|
+
const others = report.issues.length - mine.length;
|
|
3733
|
+
if (others > 0) {
|
|
3734
|
+
lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
|
|
3735
|
+
}
|
|
3736
|
+
return lines.join("\n");
|
|
3737
|
+
}
|
|
3738
|
+
function checksCell(edit) {
|
|
3739
|
+
const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
|
|
3740
|
+
if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
|
|
3741
|
+
parts.push(codeSpan(edit.message));
|
|
3742
|
+
}
|
|
3743
|
+
return parts.join("; ");
|
|
3744
|
+
}
|
|
3745
|
+
function renderRow(edit) {
|
|
3746
|
+
const before = edit.from === null ? "\u2014" : codeSpan(edit.from);
|
|
3747
|
+
const after = edit.status === "conflict" ? `now: ${edit.current === void 0 ? "(removed)" : codeSpan(edit.current)}` : codeSpan(edit.to);
|
|
3748
|
+
const cells = [
|
|
3749
|
+
STATUS_LABELS[edit.status],
|
|
3750
|
+
codeSpan(edit.locale),
|
|
3751
|
+
codeSpan(edit.key),
|
|
3752
|
+
before,
|
|
3753
|
+
after,
|
|
3754
|
+
checksCell(edit)
|
|
3755
|
+
];
|
|
3756
|
+
return `| ${cells.join(" | ")} |`;
|
|
3757
|
+
}
|
|
3758
|
+
function renderTable(plan, limit, maxChars) {
|
|
3759
|
+
const lines = [
|
|
3760
|
+
"| Status | Locale | Key | Before | After | Checks |",
|
|
3761
|
+
"| --- | --- | --- | --- | --- | --- |"
|
|
3762
|
+
];
|
|
3763
|
+
const moreLine = (n) => `
|
|
3764
|
+
|
|
3765
|
+
\u2026and ${n} more`;
|
|
3766
|
+
let length = lines.join("\n").length + moreLine(plan.edits.length).length;
|
|
3767
|
+
let shown = 0;
|
|
3768
|
+
for (const edit of plan.edits) {
|
|
3769
|
+
if (shown >= limit) break;
|
|
3770
|
+
const row = renderRow(edit);
|
|
3771
|
+
if (length + row.length + 1 > maxChars) break;
|
|
3772
|
+
lines.push(row);
|
|
3773
|
+
length += row.length + 1;
|
|
3774
|
+
shown += 1;
|
|
3775
|
+
}
|
|
3776
|
+
const rest = plan.edits.length - shown;
|
|
3777
|
+
return lines.join("\n") + (rest > 0 ? moreLine(rest) : "");
|
|
3778
|
+
}
|
|
3779
|
+
function fencedNote(note) {
|
|
3780
|
+
const text = cut(note, MAX_NOTE_CHARS2);
|
|
3781
|
+
const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
|
|
3782
|
+
return `${fence}text
|
|
3783
|
+
${text}
|
|
3784
|
+
${fence}`;
|
|
3785
|
+
}
|
|
3786
|
+
function closingLine(ctx) {
|
|
3787
|
+
if (ctx.issueNumber === void 0) return void 0;
|
|
3788
|
+
if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
|
|
3789
|
+
return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
|
|
3790
|
+
}
|
|
3791
|
+
function targetNote(target) {
|
|
3792
|
+
if (target?.reason !== "not-allowed" || target.requested === void 0) return void 0;
|
|
3793
|
+
return `The link was made from ${codeSpan(target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(target.branch)}.`;
|
|
3794
|
+
}
|
|
3795
|
+
function creditLines(ctx, withTarget) {
|
|
3796
|
+
const lines = [];
|
|
3797
|
+
if (ctx.by?.trim()) lines.push(`Translated by ${codeSpan(ctx.by)}`);
|
|
3798
|
+
const target = withTarget ? targetNote(ctx.target) : void 0;
|
|
3799
|
+
if (target) lines.push(target);
|
|
3800
|
+
if (ctx.note?.trim()) lines.push(fencedNote(ctx.note));
|
|
3801
|
+
return lines;
|
|
3802
|
+
}
|
|
3803
|
+
function renderPullRequestBody(ctx) {
|
|
3804
|
+
const head = [
|
|
3805
|
+
"Applies translations sent with a LiveLocale change link.",
|
|
3806
|
+
...creditLines(ctx, true)
|
|
3807
|
+
];
|
|
3808
|
+
const tail = [];
|
|
3809
|
+
if (ctx.checkSummary) tail.push(cutLines(ctx.checkSummary, MAX_CHECK_SUMMARY_CHARS));
|
|
3810
|
+
if (ctx.runUrl) tail.push(`Run log: ${ctx.runUrl}`);
|
|
3811
|
+
const closing = closingLine(ctx);
|
|
3812
|
+
if (closing) tail.push(closing);
|
|
3813
|
+
const fixed = [...head, ...tail].join("\n\n").length;
|
|
3814
|
+
const table = renderTable(ctx.plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
|
|
3815
|
+
return [...head, table, ...tail].join("\n\n");
|
|
3816
|
+
}
|
|
3817
|
+
function renderCommitMessage(input) {
|
|
3818
|
+
let subject = "i18n: apply LiveLocale changes";
|
|
3819
|
+
if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
|
|
3820
|
+
else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
|
|
3821
|
+
const applied = input.plan.edits.filter((edit) => edit.status === "apply");
|
|
3822
|
+
const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
|
|
3823
|
+
const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
|
|
3824
|
+
const name = sanitizeName(input.by);
|
|
3825
|
+
if (name) parts.push(`Translated-by: ${name}`);
|
|
3826
|
+
return parts.join("\n\n");
|
|
3827
|
+
}
|
|
3828
|
+
function renderTerminalSummary(plan) {
|
|
3829
|
+
const c = plan.counts;
|
|
3830
|
+
const lines = [
|
|
3831
|
+
`Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
|
|
3832
|
+
];
|
|
3833
|
+
for (const edit of plan.edits) {
|
|
3834
|
+
lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
|
|
3835
|
+
}
|
|
3836
|
+
return lines.join("\n");
|
|
3837
|
+
}
|
|
3838
|
+
var MAX_ERROR_MESSAGE_CHARS = 2e3;
|
|
3839
|
+
function needsForceHint(plan) {
|
|
3840
|
+
const n = plan?.counts["needs-force"] ?? 0;
|
|
3841
|
+
if (n === 0) return void 0;
|
|
3842
|
+
return `${n} change(s) were saved with "Save anyway". A maintainer can apply them with \`/livelocale apply --force\`.`;
|
|
3843
|
+
}
|
|
3844
|
+
function withTable(head, plan, tail) {
|
|
3845
|
+
if (!plan) return [...head, ...tail].join("\n\n");
|
|
3846
|
+
const fixed = [...head, ...tail].join("\n\n").length;
|
|
3847
|
+
const table = renderTable(plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
|
|
3848
|
+
return [...head, table, ...tail].join("\n\n");
|
|
3849
|
+
}
|
|
3850
|
+
function checkTail(checkSummary) {
|
|
3851
|
+
return checkSummary ? [cutLines(checkSummary, MAX_CHECK_SUMMARY_CHARS)] : [];
|
|
3852
|
+
}
|
|
3853
|
+
function renderIssueComment(ctx) {
|
|
3854
|
+
const { outcome } = ctx;
|
|
3855
|
+
switch (outcome.kind) {
|
|
3856
|
+
case "waiting-for-maintainer":
|
|
3857
|
+
return "Waiting for a maintainer: comment `/livelocale apply`";
|
|
3858
|
+
case "wrong-repo":
|
|
3859
|
+
return `This change link was made for ${codeSpan(outcome.linkRepo)}, not ${codeSpan(outcome.repo)}. Nothing was applied.`;
|
|
3860
|
+
case "error": {
|
|
3861
|
+
const message = cut(outcome.message, MAX_ERROR_MESSAGE_CHARS).replace(/\.$/, "");
|
|
3862
|
+
const runLog = ctx.runUrl ? ` Run log: ${ctx.runUrl}` : "";
|
|
3863
|
+
return `LiveLocale could not apply this link: ${message}.${runLog}`;
|
|
3864
|
+
}
|
|
3865
|
+
case "nothing-to-apply": {
|
|
3866
|
+
const hint = needsForceHint(ctx.plan);
|
|
3867
|
+
return withTable(
|
|
3868
|
+
["Nothing to apply.", ...creditLines(ctx, false)],
|
|
3869
|
+
ctx.plan,
|
|
3870
|
+
hint ? [hint] : []
|
|
3871
|
+
);
|
|
3872
|
+
}
|
|
3873
|
+
case "pr": {
|
|
3874
|
+
const headline = `${outcome.reused ? "Updated pull request" : "Pull request"}: ${outcome.url}`;
|
|
3875
|
+
return withTable(
|
|
3876
|
+
[headline, ...creditLines(ctx, true)],
|
|
3877
|
+
ctx.plan,
|
|
3878
|
+
checkTail(ctx.checkSummary)
|
|
3879
|
+
);
|
|
3880
|
+
}
|
|
3881
|
+
case "compare": {
|
|
3882
|
+
const headline = [
|
|
3883
|
+
`This repository does not let GitHub Actions open pull requests, so the branch ${codeSpan(outcome.branch)} was pushed instead. Create the pull request with one click: ${outcome.url}`,
|
|
3884
|
+
'(Or allow it once: Settings \u2192 Actions \u2192 General \u2192 "Allow GitHub Actions to create and approve pull requests".)'
|
|
3885
|
+
];
|
|
3886
|
+
return withTable(
|
|
3887
|
+
[...headline, ...creditLines(ctx, true)],
|
|
3888
|
+
ctx.plan,
|
|
3889
|
+
checkTail(ctx.checkSummary)
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// src/codemod/apply/github-event.ts
|
|
3896
|
+
var PR_TITLE = "i18n: LiveLocale changes";
|
|
3897
|
+
var NOT_FOUND_MESSAGE = "No LiveLocale change link (lc1.\u2026) found.";
|
|
3898
|
+
var ExpectedFailure = class extends Error {
|
|
3899
|
+
};
|
|
3900
|
+
function errorMessage(err) {
|
|
3901
|
+
return err instanceof Error ? err.message : String(err);
|
|
3902
|
+
}
|
|
3903
|
+
function required(env, name) {
|
|
3904
|
+
const value = env[name];
|
|
3905
|
+
if (!value) throw new Error(`[livelocale] ${name} is not set; run this inside GitHub Actions.`);
|
|
3906
|
+
return value;
|
|
3907
|
+
}
|
|
3908
|
+
async function closeIssue(rest, trigger, log) {
|
|
3909
|
+
const n = trigger.issueNumber;
|
|
3910
|
+
const issue = await rest.api("GET", `/issues/${n}`, {
|
|
3911
|
+
allow404: true
|
|
3912
|
+
});
|
|
3913
|
+
const body = typeof issue.data?.body === "string" ? issue.data.body : "";
|
|
3914
|
+
if (issue.data?.state !== "open" || !body.includes(CHANGE_BLOCK_OPEN)) {
|
|
3915
|
+
log(`Issue #${n} is not an open LiveLocale issue; nothing to close.`);
|
|
3916
|
+
return "ignored";
|
|
3917
|
+
}
|
|
3918
|
+
await rest.api("PATCH", `/issues/${n}`, {
|
|
3919
|
+
body: { state: "closed", state_reason: trigger.merged ? "completed" : "not_planned" }
|
|
3920
|
+
});
|
|
3921
|
+
await rest.api("POST", `/issues/${n}/comments`, {
|
|
3922
|
+
body: { body: `Closed because ${trigger.prUrl} was ${trigger.merged ? "merged" : "closed"}.` }
|
|
3923
|
+
});
|
|
3924
|
+
log(`Closed issue #${n}.`);
|
|
3925
|
+
return "closed-issue";
|
|
3926
|
+
}
|
|
3927
|
+
async function checkChanges(options, workspace, remote, changes, plan) {
|
|
3928
|
+
const dir = normalizeRepoDir(options.localesPath);
|
|
3929
|
+
const prefix = dir === "" ? "" : `${dir}/`;
|
|
3930
|
+
const tmp = fs6.mkdtempSync(path10.join(os.tmpdir(), "livelocale-check-"));
|
|
3931
|
+
try {
|
|
3932
|
+
for (const [file, text] of new Map([...remote.files, ...changes])) {
|
|
3933
|
+
const target = path10.join(tmp, ...file.slice(prefix.length).split("/"));
|
|
3934
|
+
fs6.mkdirSync(path10.dirname(target), { recursive: true });
|
|
3935
|
+
fs6.writeFileSync(target, text, "utf-8");
|
|
3936
|
+
}
|
|
3937
|
+
const report = await runCheck({
|
|
3938
|
+
projectDir: path10.join(workspace, options.dir ?? "."),
|
|
3939
|
+
localesPath: tmp,
|
|
3940
|
+
sourceLocale: options.sourceLocale,
|
|
3941
|
+
unused: false
|
|
3942
|
+
});
|
|
3943
|
+
const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
|
|
3944
|
+
return summarizeCheck(report, edited);
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
return `Check could not run: ${errorMessage(err)}`;
|
|
3947
|
+
} finally {
|
|
3948
|
+
fs6.rmSync(tmp, { recursive: true, force: true });
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
async function runGitHubEventApply(options) {
|
|
3952
|
+
const { env } = options;
|
|
3953
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
3954
|
+
const token = required(env, "GITHUB_TOKEN");
|
|
3955
|
+
const eventPath = required(env, "GITHUB_EVENT_PATH");
|
|
3956
|
+
const event = JSON.parse(fs6.readFileSync(eventPath, "utf-8"));
|
|
3957
|
+
const trigger = parseTrigger(env.GITHUB_EVENT_NAME ?? "", event);
|
|
3958
|
+
if (trigger.kind === "ignore") {
|
|
3959
|
+
log(`Nothing to do: ${trigger.reason}.`);
|
|
3960
|
+
return "ignored";
|
|
3961
|
+
}
|
|
3962
|
+
const repo = env.GITHUB_REPOSITORY || trigger.repo;
|
|
3963
|
+
const rest = createGitHubRest({
|
|
3964
|
+
repo,
|
|
3965
|
+
token,
|
|
3966
|
+
apiBase: env.GITHUB_API_URL || void 0,
|
|
3967
|
+
fetchImpl: options.fetchImpl
|
|
3968
|
+
});
|
|
3969
|
+
if (trigger.kind === "close-issue") return closeIssue(rest, trigger, log);
|
|
3970
|
+
const server = (env.GITHUB_SERVER_URL || "https://github.com").replace(/\/+$/, "");
|
|
3971
|
+
const runId = env.GITHUB_RUN_ID ?? "0";
|
|
3972
|
+
const runUrl = `${server}/${repo}/actions/runs/${runId}`;
|
|
3973
|
+
const issueNumber = trigger.issueNumber ?? void 0;
|
|
3974
|
+
const report = async (outcome, ctx = {}) => {
|
|
3975
|
+
const text = renderIssueComment({ ...ctx, runUrl, outcome });
|
|
3976
|
+
if (issueNumber !== void 0) {
|
|
3977
|
+
await rest.api("POST", `/issues/${issueNumber}/comments`, { body: { body: text } });
|
|
3978
|
+
} else if (env.GITHUB_STEP_SUMMARY) {
|
|
3979
|
+
fs6.appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}
|
|
3980
|
+
`, "utf-8");
|
|
3981
|
+
}
|
|
3982
|
+
log(`Result: ${outcome.kind}`);
|
|
3983
|
+
return outcome.kind;
|
|
3984
|
+
};
|
|
3985
|
+
if (trigger.via !== "dispatch") {
|
|
3986
|
+
if (!await isTrustedActor(rest, trigger.actor.login, trigger.association)) {
|
|
3987
|
+
return report({ kind: "waiting-for-maintainer" });
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
try {
|
|
3991
|
+
return await applyLink(options, rest, trigger, { repo, runId, runUrl, server, log, report });
|
|
3992
|
+
} catch (err) {
|
|
3993
|
+
if (err instanceof ExpectedFailure) return report({ kind: "error", message: err.message });
|
|
3994
|
+
await report({ kind: "error", message: errorMessage(err) });
|
|
3995
|
+
throw err;
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
async function applyLink(options, rest, trigger, run3) {
|
|
3999
|
+
const { repo, runId, runUrl, log, report } = run3;
|
|
4000
|
+
const issueNumber = trigger.issueNumber ?? void 0;
|
|
4001
|
+
let set;
|
|
4002
|
+
try {
|
|
4003
|
+
set = await decodeChangeLink(trigger.text, nodeInflate);
|
|
4004
|
+
} catch (err) {
|
|
4005
|
+
if (!(err instanceof ChangeLinkError)) throw err;
|
|
4006
|
+
throw new ExpectedFailure(err.code === "NOT_FOUND" ? NOT_FOUND_MESSAGE : err.message);
|
|
4007
|
+
}
|
|
4008
|
+
if (set.repo !== void 0 && set.repo.toLowerCase() !== repo.toLowerCase()) {
|
|
4009
|
+
return report({ kind: "wrong-repo", linkRepo: set.repo, repo });
|
|
4010
|
+
}
|
|
4011
|
+
const defaultBranch = trigger.defaultBranch;
|
|
4012
|
+
const chosen = chooseTargetBranch(set.branch, options.branches ?? [defaultBranch], defaultBranch);
|
|
4013
|
+
const target = {
|
|
4014
|
+
branch: chosen.branch,
|
|
4015
|
+
isDefault: chosen.branch === defaultBranch,
|
|
4016
|
+
...set.branch ? { requested: set.branch } : {},
|
|
4017
|
+
reason: chosen.reason
|
|
4018
|
+
};
|
|
4019
|
+
const targetSha = await rest.refSha(target.branch);
|
|
4020
|
+
if (targetSha === null) {
|
|
4021
|
+
throw new Error(`[livelocale] The target branch ${target.branch} does not exist.`);
|
|
4022
|
+
}
|
|
4023
|
+
const locales = [.../* @__PURE__ */ new Set([...set.edits.map((e) => e.locale), options.sourceLocale])];
|
|
4024
|
+
const remote = await readRemoteLocales(rest, targetSha, options.localesPath, locales);
|
|
4025
|
+
if (!remote.targets.get(options.sourceLocale)) {
|
|
4026
|
+
throw new ExpectedFailure(
|
|
4027
|
+
`Source locale \`${options.sourceLocale}\` has no locale file on \`${target.branch}\`.`
|
|
4028
|
+
);
|
|
4029
|
+
}
|
|
4030
|
+
const plan = planApply({
|
|
4031
|
+
edits: set.edits,
|
|
4032
|
+
sourceLocale: options.sourceLocale,
|
|
4033
|
+
targets: remote.targets,
|
|
4034
|
+
allowForce: trigger.force
|
|
4035
|
+
});
|
|
4036
|
+
log(renderTerminalSummary(plan));
|
|
4037
|
+
const credits = { plan, by: set.by, note: set.note, target };
|
|
4038
|
+
if (plan.counts.apply === 0) return report({ kind: "nothing-to-apply" }, credits);
|
|
4039
|
+
const branchPlan = await prepareBranch(
|
|
4040
|
+
rest,
|
|
4041
|
+
branchBaseName(trigger, runId),
|
|
4042
|
+
target.branch,
|
|
4043
|
+
targetSha,
|
|
4044
|
+
runId
|
|
4045
|
+
);
|
|
4046
|
+
const base = branchPlan.mode === "append" ? await readRemoteLocales(rest, branchPlan.parentSha, options.localesPath, locales) : remote;
|
|
4047
|
+
const changes = buildFileChanges(options.localesPath, plan, remote, base.files);
|
|
4048
|
+
const commit = await commitFiles(rest, {
|
|
4049
|
+
branchPlan,
|
|
4050
|
+
files: changes,
|
|
4051
|
+
message: renderCommitMessage({ plan, by: set.by, issueNumber, runId }),
|
|
4052
|
+
author: noreplyAuthor(trigger.actor)
|
|
4053
|
+
});
|
|
4054
|
+
log(
|
|
4055
|
+
commit.sha === null ? `${branchPlan.branch} already holds these changes.` : `Committed to ${branchPlan.branch}.`
|
|
4056
|
+
);
|
|
4057
|
+
const workspace = options.env.GITHUB_WORKSPACE || process.cwd();
|
|
4058
|
+
const checkSummary = await checkChanges(options, workspace, base, changes, plan);
|
|
4059
|
+
const title = issueNumber !== void 0 ? `${PR_TITLE} (#${issueNumber})` : `${PR_TITLE} (run ${runId})`;
|
|
4060
|
+
const body = renderPullRequestBody({ ...credits, issueNumber, checkSummary, runUrl });
|
|
4061
|
+
let outcome;
|
|
4062
|
+
try {
|
|
4063
|
+
const pull = await rest.openOrReusePullRequest({
|
|
4064
|
+
head: branchPlan.branch,
|
|
4065
|
+
base: target.branch,
|
|
4066
|
+
title,
|
|
4067
|
+
body,
|
|
4068
|
+
updateBodyIfReused: true
|
|
4069
|
+
});
|
|
4070
|
+
outcome = { kind: "pr", url: pull.url, reused: pull.reused };
|
|
4071
|
+
} catch (err) {
|
|
4072
|
+
if (!isPullRequestCreationForbidden(err)) throw err;
|
|
4073
|
+
const closing = closingLine({ target, issueNumber });
|
|
4074
|
+
const url = `${run3.server}/${repo}/compare/${encodeRefPath(target.branch)}...${encodeRefPath(branchPlan.branch)}?quick_pull=1&title=${encodeURIComponent(title)}` + (closing ? `&body=${encodeURIComponent(closing)}` : "");
|
|
4075
|
+
outcome = { kind: "compare", url, branch: branchPlan.branch };
|
|
4076
|
+
}
|
|
4077
|
+
log(`Pull request: ${outcome.url}`);
|
|
4078
|
+
return report(outcome, { ...credits, checkSummary });
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
// src/codemod/apply/link-input.ts
|
|
4082
|
+
import { execFile as execFile2 } from "child_process";
|
|
4083
|
+
import * as fs7 from "fs";
|
|
4084
|
+
import * as path11 from "path";
|
|
4085
|
+
var MAX_APPLY_INPUT_BYTES = 1048576;
|
|
4086
|
+
var TOO_LARGE_MESSAGE3 = "The input is larger than 1 MB.";
|
|
4087
|
+
var NO_INPUT_MESSAGE = 'Pass the change link, a file that holds it, "-" to read stdin, or --clipboard. (cmd.exe cuts arguments at 8,191 characters; use --clipboard or a file for long links.)';
|
|
4088
|
+
var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
|
|
4089
|
+
var ApplyInputError = class extends Error {
|
|
4090
|
+
constructor(message) {
|
|
4091
|
+
super(message);
|
|
4092
|
+
this.name = "ApplyInputError";
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
function checkSize(text) {
|
|
4096
|
+
if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
|
|
4097
|
+
throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4098
|
+
}
|
|
4099
|
+
return text;
|
|
4100
|
+
}
|
|
4101
|
+
async function readStream(stream) {
|
|
4102
|
+
const chunks = [];
|
|
4103
|
+
let size = 0;
|
|
4104
|
+
for await (const chunk of stream) {
|
|
4105
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
|
|
4106
|
+
size += bytes.length;
|
|
4107
|
+
if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4108
|
+
chunks.push(bytes);
|
|
4109
|
+
}
|
|
4110
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
4111
|
+
}
|
|
4112
|
+
function existingFile(arg, cwd) {
|
|
4113
|
+
try {
|
|
4114
|
+
const resolved = path11.resolve(cwd, arg);
|
|
4115
|
+
return fs7.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
|
|
4116
|
+
} catch {
|
|
4117
|
+
return null;
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
async function readApplyInput(arg, options) {
|
|
4121
|
+
if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
|
|
4122
|
+
if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
|
|
4123
|
+
if (arg === "-") return readStream(options.stdin ?? process.stdin);
|
|
4124
|
+
const file = existingFile(arg, options.cwd);
|
|
4125
|
+
if (file === null) return checkSize(arg);
|
|
4126
|
+
if (fs7.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4127
|
+
return checkSize(fs7.readFileSync(file, "utf8"));
|
|
4128
|
+
}
|
|
4129
|
+
function clipboardCommands(platform) {
|
|
4130
|
+
if (platform === "win32") {
|
|
4131
|
+
return [
|
|
4132
|
+
["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
|
|
4133
|
+
];
|
|
4134
|
+
}
|
|
4135
|
+
if (platform === "darwin") return [["pbpaste", []]];
|
|
4136
|
+
return [
|
|
4137
|
+
["wl-paste", ["--no-newline"]],
|
|
4138
|
+
["xclip", ["-selection", "clipboard", "-o"]],
|
|
4139
|
+
["xsel", ["--clipboard", "--output"]]
|
|
4140
|
+
];
|
|
4141
|
+
}
|
|
4142
|
+
var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
|
|
4143
|
+
function run(execFileImpl, [command, args]) {
|
|
4144
|
+
return new Promise((resolve16) => {
|
|
4145
|
+
try {
|
|
4146
|
+
execFileImpl(
|
|
4147
|
+
command,
|
|
4148
|
+
args,
|
|
4149
|
+
{ encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
|
|
4150
|
+
(error, stdout) => {
|
|
4151
|
+
const code = error?.code;
|
|
4152
|
+
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve16(TOO_LARGE);
|
|
4153
|
+
else resolve16(error ? null : String(stdout));
|
|
4154
|
+
}
|
|
4155
|
+
);
|
|
4156
|
+
} catch {
|
|
4157
|
+
resolve16(null);
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
async function readClipboard(execFileImpl = execFile2, platform = process.platform) {
|
|
4162
|
+
for (const command of clipboardCommands(platform)) {
|
|
4163
|
+
const text = await run(execFileImpl, command);
|
|
4164
|
+
if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4165
|
+
if (text !== null) return checkSize(text);
|
|
4166
|
+
}
|
|
4167
|
+
throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
// src/codemod/apply/local.ts
|
|
4171
|
+
import { execFile as execFile4, spawn as spawn2 } from "child_process";
|
|
4172
|
+
import * as fs10 from "fs";
|
|
4173
|
+
import * as os2 from "os";
|
|
4174
|
+
import * as path15 from "path";
|
|
4175
|
+
|
|
4176
|
+
// src/server/file-mutator.ts
|
|
4177
|
+
import { promises as fs8 } from "fs";
|
|
4178
|
+
import * as path13 from "path";
|
|
4179
|
+
|
|
4180
|
+
// src/core/safe-path.ts
|
|
4181
|
+
import * as path12 from "path";
|
|
4182
|
+
function resolveSafeLocalePath(basePath, locale) {
|
|
4183
|
+
if (!isSafeLocaleSegment(locale)) {
|
|
4184
|
+
throw new Error("Path traversal attempt");
|
|
4185
|
+
}
|
|
4186
|
+
const resolvedBasePath = path12.resolve(basePath);
|
|
4187
|
+
const filePath = path12.resolve(resolvedBasePath, `${locale}.json`);
|
|
4188
|
+
if (!filePath.startsWith(resolvedBasePath)) {
|
|
4189
|
+
throw new Error("Path traversal attempt");
|
|
4190
|
+
}
|
|
4191
|
+
return filePath;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
// src/server/file-mutator.ts
|
|
4195
|
+
var FileMutex = class {
|
|
4196
|
+
queue = Promise.resolve();
|
|
4197
|
+
async acquire(fn) {
|
|
4198
|
+
const next = this.queue.then(fn);
|
|
4199
|
+
this.queue = next.then(
|
|
4200
|
+
() => {
|
|
4201
|
+
},
|
|
4202
|
+
() => {
|
|
4203
|
+
}
|
|
4204
|
+
);
|
|
4205
|
+
return next;
|
|
4206
|
+
}
|
|
4207
|
+
};
|
|
4208
|
+
var FileMutator = class {
|
|
4209
|
+
fileLocks = /* @__PURE__ */ new Map();
|
|
4210
|
+
requireEnabled;
|
|
4211
|
+
quiet;
|
|
4212
|
+
scanner;
|
|
4213
|
+
constructor(options = {}) {
|
|
4214
|
+
this.requireEnabled = options.requireEnabled ?? true;
|
|
4215
|
+
this.quiet = options.quiet ?? false;
|
|
4216
|
+
this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
|
|
4217
|
+
}
|
|
4218
|
+
assertEnabled() {
|
|
4219
|
+
if (!this.requireEnabled) return;
|
|
4220
|
+
const reason = getDisabledReason();
|
|
4221
|
+
if (reason !== null) {
|
|
4222
|
+
throw new Error(
|
|
4223
|
+
`[livelocale] FileMutator can only be executed in development mode (${reason}).`
|
|
4224
|
+
);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
/**
|
|
4228
|
+
* Helper to serialize execution per file path
|
|
4229
|
+
*/
|
|
4230
|
+
async withLock(filePath, fn) {
|
|
4231
|
+
let lock = this.fileLocks.get(filePath);
|
|
4232
|
+
if (!lock) {
|
|
4233
|
+
lock = new FileMutex();
|
|
4234
|
+
this.fileLocks.set(filePath, lock);
|
|
4235
|
+
}
|
|
4236
|
+
return lock.acquire(fn);
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* Updates a locale JSON file on disk atomically and safely.
|
|
4240
|
+
* Enforces development mode checks, path traversal mitigation, key depth restrictions,
|
|
4241
|
+
* structural JSON validation, and concurrency serialization.
|
|
4242
|
+
*/
|
|
4243
|
+
async updateLocaleKey(basePath, locale, keyPath, newValue) {
|
|
4244
|
+
this.assertEnabled();
|
|
4245
|
+
if (!isSafeLocaleSegment(locale)) {
|
|
4246
|
+
throw new Error(
|
|
4247
|
+
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
4248
|
+
);
|
|
4249
|
+
}
|
|
4250
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4251
|
+
const localeDir = path13.resolve(resolvedBasePath, locale);
|
|
4252
|
+
let filePath = "";
|
|
4253
|
+
let mutationKeyPath = keyPath;
|
|
4254
|
+
let isNamespaceDir = false;
|
|
4255
|
+
try {
|
|
4256
|
+
const stats = await fs8.stat(localeDir);
|
|
4257
|
+
if (stats.isDirectory()) {
|
|
4258
|
+
isNamespaceDir = true;
|
|
4259
|
+
}
|
|
4260
|
+
} catch {
|
|
4261
|
+
}
|
|
4262
|
+
if (isNamespaceDir) {
|
|
4263
|
+
const firstDot = keyPath.indexOf(".");
|
|
4264
|
+
if (firstDot !== -1) {
|
|
4265
|
+
const namespace = keyPath.substring(0, firstDot);
|
|
4266
|
+
if (DANGEROUS_KEY_SEGMENTS.has(namespace)) {
|
|
4267
|
+
throw new Error(
|
|
4268
|
+
`[livelocale] Forbidden namespace segment in path: "${namespace}". Segment names like __proto__, constructor, and prototype are not allowed.`
|
|
4269
|
+
);
|
|
4270
|
+
}
|
|
4271
|
+
mutationKeyPath = keyPath.substring(firstDot + 1);
|
|
4272
|
+
filePath = path13.resolve(localeDir, `${namespace}.json`);
|
|
4273
|
+
} else {
|
|
4274
|
+
filePath = path13.resolve(localeDir, `${keyPath}.json`);
|
|
4275
|
+
mutationKeyPath = "";
|
|
4276
|
+
}
|
|
4277
|
+
} else {
|
|
4278
|
+
filePath = path13.resolve(resolvedBasePath, `${locale}.json`);
|
|
4279
|
+
}
|
|
4280
|
+
if (!filePath.startsWith(resolvedBasePath)) {
|
|
4281
|
+
throw new Error(
|
|
4282
|
+
`[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
|
|
3427
4283
|
);
|
|
3428
4284
|
}
|
|
3429
4285
|
await this.withLock(filePath, async () => {
|
|
@@ -3431,7 +4287,7 @@ var FileMutator = class {
|
|
|
3431
4287
|
let indent = " ";
|
|
3432
4288
|
let trailingNewline = "\n";
|
|
3433
4289
|
try {
|
|
3434
|
-
const fileContent = await
|
|
4290
|
+
const fileContent = await fs8.readFile(filePath, "utf-8");
|
|
3435
4291
|
indent = detectJsonIndent(fileContent);
|
|
3436
4292
|
trailingNewline = fileContent.endsWith("\n") ? "\n" : "";
|
|
3437
4293
|
try {
|
|
@@ -3471,17 +4327,17 @@ var FileMutator = class {
|
|
|
3471
4327
|
}
|
|
3472
4328
|
const tempPath = `${filePath}.tmp`;
|
|
3473
4329
|
try {
|
|
3474
|
-
await
|
|
4330
|
+
await fs8.writeFile(tempPath, stringified, "utf-8");
|
|
3475
4331
|
} catch (err) {
|
|
3476
4332
|
throw new Error(
|
|
3477
4333
|
`[livelocale] Failed to write temp file at ${tempPath}. Operation: write. Original error: ${err.message}`
|
|
3478
4334
|
);
|
|
3479
4335
|
}
|
|
3480
4336
|
try {
|
|
3481
|
-
await
|
|
4337
|
+
await fs8.rename(tempPath, filePath);
|
|
3482
4338
|
} catch (err) {
|
|
3483
4339
|
try {
|
|
3484
|
-
await
|
|
4340
|
+
await fs8.unlink(tempPath);
|
|
3485
4341
|
} catch {
|
|
3486
4342
|
}
|
|
3487
4343
|
throw new Error(
|
|
@@ -3499,19 +4355,19 @@ var FileMutator = class {
|
|
|
3499
4355
|
*/
|
|
3500
4356
|
async readLocale(basePath, locale) {
|
|
3501
4357
|
this.assertEnabled();
|
|
3502
|
-
const localePath =
|
|
4358
|
+
const localePath = path13.resolve(basePath, locale);
|
|
3503
4359
|
let isDir = false;
|
|
3504
4360
|
try {
|
|
3505
|
-
isDir = (await
|
|
4361
|
+
isDir = (await fs8.stat(localePath)).isDirectory();
|
|
3506
4362
|
} catch {
|
|
3507
4363
|
}
|
|
3508
4364
|
if (isDir) {
|
|
3509
4365
|
const merged = {};
|
|
3510
|
-
for (const file of await
|
|
4366
|
+
for (const file of await fs8.readdir(localePath)) {
|
|
3511
4367
|
if (!file.endsWith(".json")) continue;
|
|
3512
4368
|
try {
|
|
3513
|
-
merged[
|
|
3514
|
-
await
|
|
4369
|
+
merged[path13.basename(file, ".json")] = JSON.parse(
|
|
4370
|
+
await fs8.readFile(path13.join(localePath, file), "utf-8")
|
|
3515
4371
|
);
|
|
3516
4372
|
} catch {
|
|
3517
4373
|
}
|
|
@@ -3519,15 +4375,15 @@ var FileMutator = class {
|
|
|
3519
4375
|
return merged;
|
|
3520
4376
|
}
|
|
3521
4377
|
const filePath = resolveSafeLocalePath(basePath, locale);
|
|
3522
|
-
return JSON.parse(await
|
|
4378
|
+
return JSON.parse(await fs8.readFile(filePath, "utf-8"));
|
|
3523
4379
|
}
|
|
3524
4380
|
/**
|
|
3525
4381
|
* Lists all locale codes dynamically scanned from the locales directory.
|
|
3526
4382
|
*/
|
|
3527
4383
|
async listLocales(basePath) {
|
|
3528
4384
|
this.assertEnabled();
|
|
3529
|
-
const resolvedBasePath =
|
|
3530
|
-
const items = await
|
|
4385
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4386
|
+
const items = await fs8.readdir(resolvedBasePath, { withFileTypes: true });
|
|
3531
4387
|
const localesSet = /* @__PURE__ */ new Set();
|
|
3532
4388
|
for (const item of items) {
|
|
3533
4389
|
if (item.isDirectory()) {
|
|
@@ -3535,7 +4391,7 @@ var FileMutator = class {
|
|
|
3535
4391
|
localesSet.add(item.name);
|
|
3536
4392
|
}
|
|
3537
4393
|
} else if (item.isFile() && item.name.endsWith(".json")) {
|
|
3538
|
-
const name =
|
|
4394
|
+
const name = path13.basename(item.name, ".json");
|
|
3539
4395
|
if (LOCALE_RE.test(name)) {
|
|
3540
4396
|
localesSet.add(name);
|
|
3541
4397
|
}
|
|
@@ -3556,8 +4412,8 @@ var FileMutator = class {
|
|
|
3556
4412
|
if (!LOCALE_RE.test(locale)) {
|
|
3557
4413
|
throw new Error(`[livelocale] Invalid locale format: "${locale}"`);
|
|
3558
4414
|
}
|
|
3559
|
-
const resolvedBasePath =
|
|
3560
|
-
const localePath =
|
|
4415
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4416
|
+
const localePath = path13.resolve(resolvedBasePath, locale);
|
|
3561
4417
|
if (!localePath.startsWith(resolvedBasePath)) {
|
|
3562
4418
|
throw new Error(
|
|
3563
4419
|
`[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
|
|
@@ -3565,19 +4421,19 @@ var FileMutator = class {
|
|
|
3565
4421
|
}
|
|
3566
4422
|
let exists = false;
|
|
3567
4423
|
try {
|
|
3568
|
-
await
|
|
4424
|
+
await fs8.stat(localePath);
|
|
3569
4425
|
exists = true;
|
|
3570
4426
|
} catch {
|
|
3571
4427
|
}
|
|
3572
4428
|
try {
|
|
3573
|
-
await
|
|
4429
|
+
await fs8.stat(`${localePath}.json`);
|
|
3574
4430
|
exists = true;
|
|
3575
4431
|
} catch {
|
|
3576
4432
|
}
|
|
3577
4433
|
if (exists) {
|
|
3578
4434
|
throw new Error(`[livelocale] Locale "${locale}" already exists.`);
|
|
3579
4435
|
}
|
|
3580
|
-
const items = await
|
|
4436
|
+
const items = await fs8.readdir(resolvedBasePath, { withFileTypes: true });
|
|
3581
4437
|
let referenceDir = null;
|
|
3582
4438
|
for (const item of items) {
|
|
3583
4439
|
if (item.isDirectory() && LOCALE_RE.test(item.name)) {
|
|
@@ -3586,16 +4442,16 @@ var FileMutator = class {
|
|
|
3586
4442
|
}
|
|
3587
4443
|
}
|
|
3588
4444
|
if (referenceDir) {
|
|
3589
|
-
await
|
|
3590
|
-
const refPath =
|
|
3591
|
-
const files = await
|
|
4445
|
+
await fs8.mkdir(localePath, { recursive: true });
|
|
4446
|
+
const refPath = path13.resolve(resolvedBasePath, referenceDir);
|
|
4447
|
+
const files = await fs8.readdir(refPath);
|
|
3592
4448
|
for (const file of files) {
|
|
3593
4449
|
if (file.endsWith(".json")) {
|
|
3594
|
-
await
|
|
4450
|
+
await fs8.writeFile(path13.join(localePath, file), "{}", "utf-8");
|
|
3595
4451
|
}
|
|
3596
4452
|
}
|
|
3597
4453
|
} else {
|
|
3598
|
-
await
|
|
4454
|
+
await fs8.writeFile(`${localePath}.json`, "{}", "utf-8");
|
|
3599
4455
|
}
|
|
3600
4456
|
console.log(`[livelocale] Added locale '${locale}'`);
|
|
3601
4457
|
}
|
|
@@ -3610,16 +4466,16 @@ var FileMutator = class {
|
|
|
3610
4466
|
if (!LOCALE_RE.test(oldLocale) || !LOCALE_RE.test(newLocale)) {
|
|
3611
4467
|
throw new Error(`[livelocale] Invalid locale format`);
|
|
3612
4468
|
}
|
|
3613
|
-
const resolvedBasePath =
|
|
3614
|
-
const oldPath =
|
|
3615
|
-
const newPath =
|
|
4469
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4470
|
+
const oldPath = path13.resolve(resolvedBasePath, oldLocale);
|
|
4471
|
+
const newPath = path13.resolve(resolvedBasePath, newLocale);
|
|
3616
4472
|
if (!oldPath.startsWith(resolvedBasePath) || !newPath.startsWith(resolvedBasePath)) {
|
|
3617
4473
|
throw new Error("[livelocale] Path traversal attempt");
|
|
3618
4474
|
}
|
|
3619
4475
|
let isDir = false;
|
|
3620
4476
|
let exists = false;
|
|
3621
4477
|
try {
|
|
3622
|
-
const stats = await
|
|
4478
|
+
const stats = await fs8.stat(oldPath);
|
|
3623
4479
|
isDir = stats.isDirectory();
|
|
3624
4480
|
exists = true;
|
|
3625
4481
|
} catch {
|
|
@@ -3627,7 +4483,7 @@ var FileMutator = class {
|
|
|
3627
4483
|
let isFile = false;
|
|
3628
4484
|
if (!exists) {
|
|
3629
4485
|
try {
|
|
3630
|
-
await
|
|
4486
|
+
await fs8.stat(`${oldPath}.json`);
|
|
3631
4487
|
isFile = true;
|
|
3632
4488
|
exists = true;
|
|
3633
4489
|
} catch {
|
|
@@ -3638,12 +4494,12 @@ var FileMutator = class {
|
|
|
3638
4494
|
}
|
|
3639
4495
|
let newExists = false;
|
|
3640
4496
|
try {
|
|
3641
|
-
await
|
|
4497
|
+
await fs8.stat(newPath);
|
|
3642
4498
|
newExists = true;
|
|
3643
4499
|
} catch {
|
|
3644
4500
|
}
|
|
3645
4501
|
try {
|
|
3646
|
-
await
|
|
4502
|
+
await fs8.stat(`${newPath}.json`);
|
|
3647
4503
|
newExists = true;
|
|
3648
4504
|
} catch {
|
|
3649
4505
|
}
|
|
@@ -3651,9 +4507,9 @@ var FileMutator = class {
|
|
|
3651
4507
|
throw new Error(`[livelocale] Target locale "${newLocale}" already exists.`);
|
|
3652
4508
|
}
|
|
3653
4509
|
if (isDir) {
|
|
3654
|
-
await
|
|
4510
|
+
await fs8.rename(oldPath, newPath);
|
|
3655
4511
|
} else if (isFile) {
|
|
3656
|
-
await
|
|
4512
|
+
await fs8.rename(`${oldPath}.json`, `${newPath}.json`);
|
|
3657
4513
|
}
|
|
3658
4514
|
console.log(`[livelocale] Renamed locale '${oldLocale}' to '${newLocale}'`);
|
|
3659
4515
|
}
|
|
@@ -3668,15 +4524,15 @@ var FileMutator = class {
|
|
|
3668
4524
|
if (!LOCALE_RE.test(locale)) {
|
|
3669
4525
|
throw new Error(`[livelocale] Invalid locale format`);
|
|
3670
4526
|
}
|
|
3671
|
-
const resolvedBasePath =
|
|
3672
|
-
const localePath =
|
|
4527
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4528
|
+
const localePath = path13.resolve(resolvedBasePath, locale);
|
|
3673
4529
|
if (!localePath.startsWith(resolvedBasePath)) {
|
|
3674
4530
|
throw new Error("[livelocale] Path traversal attempt");
|
|
3675
4531
|
}
|
|
3676
4532
|
let isDir = false;
|
|
3677
4533
|
let exists = false;
|
|
3678
4534
|
try {
|
|
3679
|
-
const stats = await
|
|
4535
|
+
const stats = await fs8.stat(localePath);
|
|
3680
4536
|
isDir = stats.isDirectory();
|
|
3681
4537
|
exists = true;
|
|
3682
4538
|
} catch {
|
|
@@ -3684,7 +4540,7 @@ var FileMutator = class {
|
|
|
3684
4540
|
let isFile = false;
|
|
3685
4541
|
if (!exists) {
|
|
3686
4542
|
try {
|
|
3687
|
-
await
|
|
4543
|
+
await fs8.stat(`${localePath}.json`);
|
|
3688
4544
|
isFile = true;
|
|
3689
4545
|
exists = true;
|
|
3690
4546
|
} catch {
|
|
@@ -3694,9 +4550,9 @@ var FileMutator = class {
|
|
|
3694
4550
|
throw new Error(`[livelocale] Locale "${locale}" does not exist.`);
|
|
3695
4551
|
}
|
|
3696
4552
|
if (isDir) {
|
|
3697
|
-
await
|
|
4553
|
+
await fs8.rm(localePath, { recursive: true, force: true });
|
|
3698
4554
|
} else if (isFile) {
|
|
3699
|
-
await
|
|
4555
|
+
await fs8.unlink(`${localePath}.json`);
|
|
3700
4556
|
}
|
|
3701
4557
|
console.log(`[livelocale] Deleted locale '${locale}'`);
|
|
3702
4558
|
}
|
|
@@ -3724,8 +4580,8 @@ var FileMutator = class {
|
|
|
3724
4580
|
// src/server/git-client.ts
|
|
3725
4581
|
import { execFile as execFile3 } from "child_process";
|
|
3726
4582
|
import { promisify } from "util";
|
|
3727
|
-
import * as
|
|
3728
|
-
import * as
|
|
4583
|
+
import * as fs9 from "fs";
|
|
4584
|
+
import * as path14 from "path";
|
|
3729
4585
|
var execFileAsync = promisify(execFile3);
|
|
3730
4586
|
var MAX_DIFF_BYTES = 2e5;
|
|
3731
4587
|
function mapStatus(xy) {
|
|
@@ -3756,15 +4612,15 @@ var GitClient = class {
|
|
|
3756
4612
|
}
|
|
3757
4613
|
/** Resolves the repository root and the locales path relative to it (forward slashes). */
|
|
3758
4614
|
async locate(localesPath) {
|
|
3759
|
-
const absolute =
|
|
3760
|
-
const cwd =
|
|
3761
|
-
if (!
|
|
4615
|
+
const absolute = path14.resolve(localesPath);
|
|
4616
|
+
const cwd = fs9.existsSync(absolute) ? absolute : path14.dirname(absolute);
|
|
4617
|
+
if (!fs9.existsSync(cwd)) return null;
|
|
3762
4618
|
try {
|
|
3763
4619
|
const root = (await this.git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
3764
4620
|
if (!root) return null;
|
|
3765
|
-
const rel =
|
|
4621
|
+
const rel = path14.relative(root, absolute).split(path14.sep).join("/");
|
|
3766
4622
|
if (rel.startsWith("..")) return null;
|
|
3767
|
-
return { root:
|
|
4623
|
+
return { root: path14.resolve(root), rel: rel || ".", absolute };
|
|
3768
4624
|
} catch {
|
|
3769
4625
|
return null;
|
|
3770
4626
|
}
|
|
@@ -3796,301 +4652,127 @@ var GitClient = class {
|
|
|
3796
4652
|
if (file.startsWith('"') && file.endsWith('"')) file = file.slice(1, -1);
|
|
3797
4653
|
changes.push({ path: file, status: mapStatus(xy) });
|
|
3798
4654
|
}
|
|
3799
|
-
} catch {
|
|
3800
|
-
}
|
|
3801
|
-
return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
|
|
3802
|
-
}
|
|
3803
|
-
async diff(localesPath) {
|
|
3804
|
-
const loc = await this.locate(localesPath);
|
|
3805
|
-
if (!loc) return "";
|
|
3806
|
-
let out = "";
|
|
3807
|
-
try {
|
|
3808
|
-
out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
|
|
3809
|
-
} catch {
|
|
3810
|
-
}
|
|
3811
|
-
try {
|
|
3812
|
-
const untracked = await this.git(loc.root, [
|
|
3813
|
-
"ls-files",
|
|
3814
|
-
"--others",
|
|
3815
|
-
"--exclude-standard",
|
|
3816
|
-
"--",
|
|
3817
|
-
loc.rel
|
|
3818
|
-
]);
|
|
3819
|
-
for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
3820
|
-
const full = path12.join(loc.root, file);
|
|
3821
|
-
let content = "";
|
|
3822
|
-
try {
|
|
3823
|
-
content = fs8.readFileSync(full, "utf-8");
|
|
3824
|
-
} catch {
|
|
3825
|
-
continue;
|
|
3826
|
-
}
|
|
3827
|
-
const lines = content.split("\n");
|
|
3828
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
3829
|
-
out += `diff --git a/${file} b/${file}
|
|
3830
|
-
new file mode 100644
|
|
3831
|
-
--- /dev/null
|
|
3832
|
-
+++ b/${file}
|
|
3833
|
-
@@ -0,0 +1,${lines.length} @@
|
|
3834
|
-
` + lines.map((l) => `+${l}`).join("\n") + "\n";
|
|
3835
|
-
}
|
|
3836
|
-
} catch {
|
|
3837
|
-
}
|
|
3838
|
-
if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
|
|
3839
|
-
out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
|
|
3840
|
-
}
|
|
3841
|
-
return out;
|
|
3842
|
-
}
|
|
3843
|
-
async commit(localesPath, message) {
|
|
3844
|
-
const loc = await this.locate(localesPath);
|
|
3845
|
-
if (!loc) throw new Error("Not a git repository.");
|
|
3846
|
-
const status = await this.status(localesPath);
|
|
3847
|
-
if (status.changes.length === 0) {
|
|
3848
|
-
throw new Error(`No changes to commit under ${loc.rel}.`);
|
|
3849
|
-
}
|
|
3850
|
-
await this.git(loc.root, ["add", "-A", "--", loc.rel]);
|
|
3851
|
-
await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
|
|
3852
|
-
const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
|
|
3853
|
-
return { hash };
|
|
3854
|
-
}
|
|
3855
|
-
async discard(localesPath) {
|
|
3856
|
-
const loc = await this.locate(localesPath);
|
|
3857
|
-
if (!loc) throw new Error("Not a git repository.");
|
|
3858
|
-
try {
|
|
3859
|
-
await this.git(loc.root, ["checkout", "--", loc.rel]);
|
|
3860
|
-
} catch {
|
|
3861
|
-
}
|
|
3862
|
-
await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
|
|
3863
|
-
}
|
|
3864
|
-
};
|
|
3865
|
-
|
|
3866
|
-
// src/codemod/apply/report.ts
|
|
3867
|
-
var DEFAULT_SPAN_LENGTH = 120;
|
|
3868
|
-
var DEFAULT_TABLE_ROWS = 100;
|
|
3869
|
-
var MAX_BODY_CHARS = 6e4;
|
|
3870
|
-
var MAX_NOTE_CHARS2 = 2e3;
|
|
3871
|
-
var MAX_NAME_CHARS = 64;
|
|
3872
|
-
var MAX_CHECK_LINES = 50;
|
|
3873
|
-
var MAX_CHECK_SUMMARY_CHARS = 1e4;
|
|
3874
|
-
var BODY_MARGIN = 200;
|
|
3875
|
-
var STATUS_LABELS = {
|
|
3876
|
-
apply: "applied",
|
|
3877
|
-
already: "already done",
|
|
3878
|
-
conflict: "conflict",
|
|
3879
|
-
"needs-force": "needs --force",
|
|
3880
|
-
invalid: "rejected"
|
|
3881
|
-
};
|
|
3882
|
-
var BIDI_CONTROL_RE = /[--]/g;
|
|
3883
|
-
var NEWLINE_RE = /\r\n|\r|\n/g;
|
|
3884
|
-
var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
3885
|
-
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
3886
|
-
function cut(text, max) {
|
|
3887
|
-
if (text.length <= max) return text;
|
|
3888
|
-
let out = text.slice(0, max);
|
|
3889
|
-
const last = out.charCodeAt(out.length - 1);
|
|
3890
|
-
if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
|
|
3891
|
-
return out;
|
|
3892
|
-
}
|
|
3893
|
-
function cutLines(text, max) {
|
|
3894
|
-
if (text.length <= max) return text;
|
|
3895
|
-
const kept = [];
|
|
3896
|
-
let length = 0;
|
|
3897
|
-
for (const line of text.split("\n")) {
|
|
3898
|
-
const added = (kept.length > 0 ? 1 : 0) + line.length;
|
|
3899
|
-
if (length + added + 2 > max) break;
|
|
3900
|
-
kept.push(line);
|
|
3901
|
-
length += added;
|
|
3902
|
-
}
|
|
3903
|
-
return [...kept, "\u2026"].join("\n");
|
|
3904
|
-
}
|
|
3905
|
-
function longestBacktickRun(text) {
|
|
3906
|
-
let longest = 0;
|
|
3907
|
-
for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
|
|
3908
|
-
return longest;
|
|
3909
|
-
}
|
|
3910
|
-
function codePointLabel(ch) {
|
|
3911
|
-
return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
|
|
3912
|
-
}
|
|
3913
|
-
function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
|
|
3914
|
-
const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
|
|
3915
|
-
const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
|
|
3916
|
-
if (content === "") return "` `";
|
|
3917
|
-
const fence = "`".repeat(longestBacktickRun(content) + 1);
|
|
3918
|
-
const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
|
|
3919
|
-
return `${fence}${pad}${content}${pad}${fence}`;
|
|
3920
|
-
}
|
|
3921
|
-
function sanitizeName(name) {
|
|
3922
|
-
if (name === void 0) return void 0;
|
|
3923
|
-
const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
|
|
3924
|
-
const result = cut(cleaned, MAX_NAME_CHARS).trim();
|
|
3925
|
-
return result === "" ? void 0 : result;
|
|
3926
|
-
}
|
|
3927
|
-
var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
|
|
3928
|
-
function summarizeCheck(report, edited) {
|
|
3929
|
-
const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
|
|
3930
|
-
const mine = report.issues.filter(
|
|
3931
|
-
(issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
|
|
3932
|
-
);
|
|
3933
|
-
if (mine.length === 0) return NO_CHECK_PROBLEMS;
|
|
3934
|
-
const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
|
|
3935
|
-
for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
|
|
3936
|
-
lines.push(
|
|
3937
|
-
`- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
|
|
3938
|
-
);
|
|
3939
|
-
}
|
|
3940
|
-
if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
|
|
3941
|
-
const others = report.issues.length - mine.length;
|
|
3942
|
-
if (others > 0) {
|
|
3943
|
-
lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
|
|
3944
|
-
}
|
|
3945
|
-
return lines.join("\n");
|
|
3946
|
-
}
|
|
3947
|
-
function checksCell(edit) {
|
|
3948
|
-
const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
|
|
3949
|
-
if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
|
|
3950
|
-
parts.push(codeSpan(edit.message));
|
|
4655
|
+
} catch {
|
|
4656
|
+
}
|
|
4657
|
+
return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
|
|
3951
4658
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
4659
|
+
async diff(localesPath) {
|
|
4660
|
+
const loc = await this.locate(localesPath);
|
|
4661
|
+
if (!loc) return "";
|
|
4662
|
+
let out = "";
|
|
4663
|
+
try {
|
|
4664
|
+
out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
|
|
4665
|
+
} catch {
|
|
4666
|
+
}
|
|
4667
|
+
try {
|
|
4668
|
+
const untracked = await this.git(loc.root, [
|
|
4669
|
+
"ls-files",
|
|
4670
|
+
"--others",
|
|
4671
|
+
"--exclude-standard",
|
|
4672
|
+
"--",
|
|
4673
|
+
loc.rel
|
|
4674
|
+
]);
|
|
4675
|
+
for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
4676
|
+
const full = path14.join(loc.root, file);
|
|
4677
|
+
let content = "";
|
|
4678
|
+
try {
|
|
4679
|
+
content = fs9.readFileSync(full, "utf-8");
|
|
4680
|
+
} catch {
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
const lines = content.split("\n");
|
|
4684
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
4685
|
+
out += `diff --git a/${file} b/${file}
|
|
4686
|
+
new file mode 100644
|
|
4687
|
+
--- /dev/null
|
|
4688
|
+
+++ b/${file}
|
|
4689
|
+
@@ -0,0 +1,${lines.length} @@
|
|
4690
|
+
` + lines.map((l) => `+${l}`).join("\n") + "\n";
|
|
4691
|
+
}
|
|
4692
|
+
} catch {
|
|
4693
|
+
}
|
|
4694
|
+
if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
|
|
4695
|
+
out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
|
|
4696
|
+
}
|
|
4697
|
+
return out;
|
|
3984
4698
|
}
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
if (ctx.issueNumber === void 0) return void 0;
|
|
3997
|
-
if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
|
|
3998
|
-
return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
|
|
3999
|
-
}
|
|
4000
|
-
function renderPullRequestBody(ctx) {
|
|
4001
|
-
const head = ["Applies translations sent with a LiveLocale change link."];
|
|
4002
|
-
if (ctx.by?.trim()) head.push(`Translated by ${codeSpan(ctx.by)}`);
|
|
4003
|
-
if (ctx.target.reason === "not-allowed" && ctx.target.requested !== void 0) {
|
|
4004
|
-
head.push(
|
|
4005
|
-
`The link was made from ${codeSpan(ctx.target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(ctx.target.branch)}.`
|
|
4006
|
-
);
|
|
4699
|
+
async commit(localesPath, message) {
|
|
4700
|
+
const loc = await this.locate(localesPath);
|
|
4701
|
+
if (!loc) throw new Error("Not a git repository.");
|
|
4702
|
+
const status = await this.status(localesPath);
|
|
4703
|
+
if (status.changes.length === 0) {
|
|
4704
|
+
throw new Error(`No changes to commit under ${loc.rel}.`);
|
|
4705
|
+
}
|
|
4706
|
+
await this.git(loc.root, ["add", "-A", "--", loc.rel]);
|
|
4707
|
+
await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
|
|
4708
|
+
const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
|
|
4709
|
+
return { hash };
|
|
4007
4710
|
}
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
return [...head, table, ...tail].join("\n\n");
|
|
4017
|
-
}
|
|
4018
|
-
function renderCommitMessage(input) {
|
|
4019
|
-
let subject = "i18n: apply LiveLocale changes";
|
|
4020
|
-
if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
|
|
4021
|
-
else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
|
|
4022
|
-
const applied = input.plan.edits.filter((edit) => edit.status === "apply");
|
|
4023
|
-
const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
|
|
4024
|
-
const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
|
|
4025
|
-
const name = sanitizeName(input.by);
|
|
4026
|
-
if (name) parts.push(`Translated-by: ${name}`);
|
|
4027
|
-
return parts.join("\n\n");
|
|
4028
|
-
}
|
|
4029
|
-
function renderTerminalSummary(plan) {
|
|
4030
|
-
const c = plan.counts;
|
|
4031
|
-
const lines = [
|
|
4032
|
-
`Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
|
|
4033
|
-
];
|
|
4034
|
-
for (const edit of plan.edits) {
|
|
4035
|
-
lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
|
|
4711
|
+
async discard(localesPath) {
|
|
4712
|
+
const loc = await this.locate(localesPath);
|
|
4713
|
+
if (!loc) throw new Error("Not a git repository.");
|
|
4714
|
+
try {
|
|
4715
|
+
await this.git(loc.root, ["checkout", "--", loc.rel]);
|
|
4716
|
+
} catch {
|
|
4717
|
+
}
|
|
4718
|
+
await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
|
|
4036
4719
|
}
|
|
4037
|
-
|
|
4038
|
-
}
|
|
4720
|
+
};
|
|
4039
4721
|
|
|
4040
4722
|
// src/codemod/apply/local.ts
|
|
4041
|
-
function
|
|
4723
|
+
function isPlainObject5(value) {
|
|
4042
4724
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4043
4725
|
}
|
|
4044
4726
|
async function readTarget(mutator, localesDir, locale) {
|
|
4045
4727
|
if (!isValidLocaleFormat(locale)) return null;
|
|
4046
|
-
const dir =
|
|
4728
|
+
const dir = path15.join(localesDir, locale);
|
|
4047
4729
|
let layout;
|
|
4048
|
-
if (
|
|
4730
|
+
if (fs10.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
4049
4731
|
layout = "namespaced";
|
|
4050
|
-
} else if (
|
|
4732
|
+
} else if (fs10.statSync(`${dir}.json`, { throwIfNoEntry: false })?.isFile()) {
|
|
4051
4733
|
layout = "flat";
|
|
4052
4734
|
} else {
|
|
4053
4735
|
return null;
|
|
4054
4736
|
}
|
|
4055
4737
|
const dict = await mutator.readLocale(localesDir, locale);
|
|
4056
|
-
if (!
|
|
4738
|
+
if (!isPlainObject5(dict)) {
|
|
4057
4739
|
throw new Error(`The "${locale}" locale file is not a JSON object.`);
|
|
4058
4740
|
}
|
|
4059
4741
|
return { layout, dict };
|
|
4060
4742
|
}
|
|
4061
4743
|
function fileFor(localesPath, layout, locale, key) {
|
|
4062
|
-
const rel = layout === "flat" ?
|
|
4063
|
-
return
|
|
4744
|
+
const rel = layout === "flat" ? path15.join(localesPath, `${locale}.json`) : path15.join(localesPath, locale, `${key.slice(0, key.indexOf("."))}.json`);
|
|
4745
|
+
return path15.normalize(rel).split(path15.sep).join("/");
|
|
4064
4746
|
}
|
|
4065
|
-
var
|
|
4747
|
+
var PR_TITLE2 = "i18n: LiveLocale changes";
|
|
4066
4748
|
var GITHUB_REMOTE_RE = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/;
|
|
4067
4749
|
var defaultGit = (args, cwd, opts) => {
|
|
4068
4750
|
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
4069
4751
|
if (opts?.inherit) {
|
|
4070
|
-
return new Promise((
|
|
4752
|
+
return new Promise((resolve16, reject2) => {
|
|
4071
4753
|
const child = spawn2("git", args, { cwd, env, stdio: "inherit", windowsHide: true });
|
|
4072
4754
|
child.on("error", reject2);
|
|
4073
4755
|
child.on(
|
|
4074
4756
|
"close",
|
|
4075
|
-
(code) => code === 0 ?
|
|
4757
|
+
(code) => code === 0 ? resolve16("") : reject2(new Error(`git ${args[0]} failed (exit ${code}).`))
|
|
4076
4758
|
);
|
|
4077
4759
|
});
|
|
4078
4760
|
}
|
|
4079
|
-
return new Promise((
|
|
4761
|
+
return new Promise((resolve16, reject2) => {
|
|
4080
4762
|
execFile4(
|
|
4081
4763
|
"git",
|
|
4082
4764
|
args,
|
|
4083
4765
|
{ cwd, env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 },
|
|
4084
|
-
(error, stdout) => error ? reject2(error) :
|
|
4766
|
+
(error, stdout) => error ? reject2(error) : resolve16(String(stdout))
|
|
4085
4767
|
);
|
|
4086
4768
|
});
|
|
4087
4769
|
};
|
|
4088
|
-
var defaultGh = (args, cwd) => new Promise((
|
|
4770
|
+
var defaultGh = (args, cwd) => new Promise((resolve16, reject2) => {
|
|
4089
4771
|
execFile4(
|
|
4090
4772
|
"gh",
|
|
4091
4773
|
args,
|
|
4092
4774
|
{ cwd, windowsHide: true },
|
|
4093
|
-
(error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) :
|
|
4775
|
+
(error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) : resolve16(String(stdout))
|
|
4094
4776
|
);
|
|
4095
4777
|
});
|
|
4096
4778
|
function utcStamp(date) {
|
|
@@ -4122,7 +4804,7 @@ async function pickBranchName(git, root, stamp) {
|
|
|
4122
4804
|
for (let n = 2; await taken(name); n++) name = `${base}-${n}`;
|
|
4123
4805
|
return name;
|
|
4124
4806
|
}
|
|
4125
|
-
function
|
|
4807
|
+
function errorMessage2(err) {
|
|
4126
4808
|
return err instanceof Error ? err.message : String(err);
|
|
4127
4809
|
}
|
|
4128
4810
|
async function writeEdits(mutator, plan, targets, localesDir, localesPath) {
|
|
@@ -4142,12 +4824,12 @@ function exitCodeFor(plan) {
|
|
|
4142
4824
|
async function preparePullRequest(git, projectDir, localesDir) {
|
|
4143
4825
|
const top = (await tryRun(() => git(["rev-parse", "--show-toplevel"], projectDir)))?.trim();
|
|
4144
4826
|
if (!top) throw new ApplyInputError("apply --pr needs a git repository.");
|
|
4145
|
-
const root =
|
|
4827
|
+
const root = path15.resolve(top);
|
|
4146
4828
|
const original = (await tryRun(() => git(["symbolic-ref", "--quiet", "--short", "HEAD"], root)))?.trim();
|
|
4147
4829
|
if (!original) throw new ApplyInputError("Check out a branch first.");
|
|
4148
4830
|
const status = await new GitClient().status(localesDir);
|
|
4149
4831
|
if (status.changes.length > 0) {
|
|
4150
|
-
const rel =
|
|
4832
|
+
const rel = path15.relative(root, localesDir).split(path15.sep).join("/") || ".";
|
|
4151
4833
|
throw new ApplyInputError(
|
|
4152
4834
|
`Commit or stash the changes under ${rel} first; apply --pr commits everything in that folder.`
|
|
4153
4835
|
);
|
|
@@ -4155,10 +4837,10 @@ async function preparePullRequest(git, projectDir, localesDir) {
|
|
|
4155
4837
|
return { root, original };
|
|
4156
4838
|
}
|
|
4157
4839
|
async function createPullRequest(log, gh, root, base, head, body) {
|
|
4158
|
-
const tmpDir =
|
|
4159
|
-
const bodyFile =
|
|
4840
|
+
const tmpDir = fs10.mkdtempSync(path15.join(os2.tmpdir(), "livelocale-pr-"));
|
|
4841
|
+
const bodyFile = path15.join(tmpDir, "body.md");
|
|
4160
4842
|
try {
|
|
4161
|
-
|
|
4843
|
+
fs10.writeFileSync(bodyFile, body, "utf-8");
|
|
4162
4844
|
const out = await gh(
|
|
4163
4845
|
[
|
|
4164
4846
|
"pr",
|
|
@@ -4168,7 +4850,7 @@ async function createPullRequest(log, gh, root, base, head, body) {
|
|
|
4168
4850
|
"--head",
|
|
4169
4851
|
head,
|
|
4170
4852
|
"--title",
|
|
4171
|
-
|
|
4853
|
+
PR_TITLE2,
|
|
4172
4854
|
"--body-file",
|
|
4173
4855
|
bodyFile
|
|
4174
4856
|
],
|
|
@@ -4179,20 +4861,20 @@ async function createPullRequest(log, gh, root, base, head, body) {
|
|
|
4179
4861
|
} catch (err) {
|
|
4180
4862
|
if (err?.code !== "ENOENT") {
|
|
4181
4863
|
const stderr = err?.stderr;
|
|
4182
|
-
const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() :
|
|
4864
|
+
const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() : errorMessage2(err);
|
|
4183
4865
|
log(`gh could not open the pull request: ${reason}`);
|
|
4184
4866
|
}
|
|
4185
4867
|
return void 0;
|
|
4186
4868
|
} finally {
|
|
4187
|
-
|
|
4869
|
+
fs10.rmSync(tmpDir, { recursive: true, force: true });
|
|
4188
4870
|
}
|
|
4189
4871
|
}
|
|
4190
4872
|
async function runLocalApply(options) {
|
|
4191
4873
|
const log = options.log ?? ((line) => console.log(line));
|
|
4192
4874
|
const set = await decodeChangeLink(options.input, nodeInflate);
|
|
4193
4875
|
const localesPath = options.localesPath ?? "locales";
|
|
4194
|
-
const localesDir =
|
|
4195
|
-
if (!
|
|
4876
|
+
const localesDir = path15.resolve(options.projectDir, localesPath);
|
|
4877
|
+
if (!fs10.statSync(localesDir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
4196
4878
|
throw new ApplyInputError(`No locales folder at ${localesPath}.`);
|
|
4197
4879
|
}
|
|
4198
4880
|
const pr = options.pr === true && options.dryRun !== true;
|
|
@@ -4248,7 +4930,7 @@ async function runLocalApply(options) {
|
|
|
4248
4930
|
const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
|
|
4249
4931
|
checkSummary = summarizeCheck(report, edited);
|
|
4250
4932
|
} catch (err) {
|
|
4251
|
-
checkSummary = `Check could not run: ${
|
|
4933
|
+
checkSummary = `Check could not run: ${errorMessage2(err)}`;
|
|
4252
4934
|
}
|
|
4253
4935
|
await new GitClient().commit(localesDir, renderCommitMessage({ plan, by: set.by }));
|
|
4254
4936
|
await git(["push", "-u", "origin", branch], root, { inherit: true });
|
|
@@ -4292,7 +4974,8 @@ var FLAG_OPTIONS = {
|
|
|
4292
4974
|
"--clipboard": "clipboard",
|
|
4293
4975
|
"--pr": "pr",
|
|
4294
4976
|
"--force": "force",
|
|
4295
|
-
"--dry-run": "dryRun"
|
|
4977
|
+
"--dry-run": "dryRun",
|
|
4978
|
+
"--github-event": "githubEvent"
|
|
4296
4979
|
};
|
|
4297
4980
|
function parseApplyArgs(argv) {
|
|
4298
4981
|
const args = {
|
|
@@ -4303,11 +4986,20 @@ function parseApplyArgs(argv) {
|
|
|
4303
4986
|
dryRun: false,
|
|
4304
4987
|
dir: ".",
|
|
4305
4988
|
locales: void 0,
|
|
4306
|
-
source: void 0
|
|
4989
|
+
source: void 0,
|
|
4990
|
+
githubEvent: false,
|
|
4991
|
+
branches: void 0
|
|
4307
4992
|
};
|
|
4308
4993
|
for (let i = 0; i < argv.length; i++) {
|
|
4309
4994
|
const arg = argv[i];
|
|
4310
|
-
if (
|
|
4995
|
+
if (arg === "--branches") {
|
|
4996
|
+
const value = argv[i + 1];
|
|
4997
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
4998
|
+
throw new ApplyInputError(`${arg} needs a value.`);
|
|
4999
|
+
}
|
|
5000
|
+
args.branches = value.split(",").map((branch) => branch.trim()).filter(Boolean);
|
|
5001
|
+
i++;
|
|
5002
|
+
} else if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS2, arg)) {
|
|
4311
5003
|
const value = argv[i + 1];
|
|
4312
5004
|
if (value === void 0 || value.startsWith("--")) {
|
|
4313
5005
|
throw new ApplyInputError(`${arg} needs a value.`);
|
|
@@ -4327,17 +5019,43 @@ function parseApplyArgs(argv) {
|
|
|
4327
5019
|
if (args.clipboard && args.input !== void 0) {
|
|
4328
5020
|
throw new ApplyInputError("Pass the link or --clipboard, not both.");
|
|
4329
5021
|
}
|
|
5022
|
+
if (args.githubEvent) {
|
|
5023
|
+
if (args.force) {
|
|
5024
|
+
throw new ApplyInputError(
|
|
5025
|
+
"--force is not allowed with --github-event; a maintainer comments `/livelocale apply --force`."
|
|
5026
|
+
);
|
|
5027
|
+
}
|
|
5028
|
+
if (args.input !== void 0) {
|
|
5029
|
+
throw new ApplyInputError(
|
|
5030
|
+
"--github-event reads the link from the event file; do not pass one."
|
|
5031
|
+
);
|
|
5032
|
+
}
|
|
5033
|
+
if (args.locales === void 0 || args.source === void 0) {
|
|
5034
|
+
throw new ApplyInputError("--github-event needs --locales and --source.");
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
4330
5037
|
return args;
|
|
4331
5038
|
}
|
|
4332
5039
|
async function runApplyCommand(argv) {
|
|
4333
5040
|
try {
|
|
4334
5041
|
const args = parseApplyArgs(argv);
|
|
5042
|
+
if (args.githubEvent) {
|
|
5043
|
+
const result2 = await runGitHubEventApply({
|
|
5044
|
+
env: process.env,
|
|
5045
|
+
localesPath: args.locales,
|
|
5046
|
+
sourceLocale: args.source,
|
|
5047
|
+
branches: args.branches,
|
|
5048
|
+
dir: args.dir
|
|
5049
|
+
});
|
|
5050
|
+
process.exitCode = result2 === "error" || result2 === "wrong-repo" ? 1 : 0;
|
|
5051
|
+
return;
|
|
5052
|
+
}
|
|
4335
5053
|
const input = await readApplyInput(args.input, {
|
|
4336
5054
|
cwd: process.cwd(),
|
|
4337
5055
|
clipboard: args.clipboard
|
|
4338
5056
|
});
|
|
4339
5057
|
const result = await runLocalApply({
|
|
4340
|
-
projectDir:
|
|
5058
|
+
projectDir: path16.resolve(args.dir),
|
|
4341
5059
|
localesPath: args.locales,
|
|
4342
5060
|
sourceLocale: args.source,
|
|
4343
5061
|
input,
|
|
@@ -4352,6 +5070,327 @@ async function runApplyCommand(argv) {
|
|
|
4352
5070
|
}
|
|
4353
5071
|
}
|
|
4354
5072
|
|
|
5073
|
+
// src/codemod/init-share.ts
|
|
5074
|
+
import * as fs11 from "fs";
|
|
5075
|
+
import * as path17 from "path";
|
|
5076
|
+
var CHECKOUT_ACTION = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2";
|
|
5077
|
+
var WORKFLOW_PATH = ".github/workflows/livelocale.yml";
|
|
5078
|
+
var LOCALES_CANDIDATES = [
|
|
5079
|
+
"locales",
|
|
5080
|
+
"src/locales",
|
|
5081
|
+
"public/locales",
|
|
5082
|
+
"src/i18n/locales",
|
|
5083
|
+
"messages"
|
|
5084
|
+
];
|
|
5085
|
+
var SAFE_ARG = /^[A-Za-z0-9_./-]+$/;
|
|
5086
|
+
var VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
5087
|
+
var MAX_BRANCHES = 10;
|
|
5088
|
+
var SHARE_GITIGNORE = "public/livelocale/";
|
|
5089
|
+
function readText3(file) {
|
|
5090
|
+
try {
|
|
5091
|
+
return fs11.readFileSync(file, "utf-8");
|
|
5092
|
+
} catch {
|
|
5093
|
+
return null;
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
function parsePackageJson(text) {
|
|
5097
|
+
if (text === null) return null;
|
|
5098
|
+
try {
|
|
5099
|
+
const parsed = JSON.parse(text);
|
|
5100
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
5101
|
+
} catch {
|
|
5102
|
+
return null;
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
function hasDependency2(pkg, name) {
|
|
5106
|
+
return Boolean(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]);
|
|
5107
|
+
}
|
|
5108
|
+
function detectShareStack(projectDir) {
|
|
5109
|
+
const target = detectBuildTarget(projectDir);
|
|
5110
|
+
if (target.kind === "next") {
|
|
5111
|
+
return target.staticExport ? { stack: "next-export", evidence: "Next.js with output: 'export'" } : { stack: "next-server", evidence: "Next.js without output: 'export'" };
|
|
5112
|
+
}
|
|
5113
|
+
if (target.kind === "vite") return { stack: "vite", evidence: "Vite" };
|
|
5114
|
+
if (target.kind === "cra") return { stack: "cra", evidence: "react-scripts" };
|
|
5115
|
+
const pkg = parsePackageJson(readText3(path17.join(projectDir, "package.json")));
|
|
5116
|
+
for (const name of ["webpack", "@rsbuild/core"]) {
|
|
5117
|
+
if (hasDependency2(pkg, name)) return { stack: "webpack", evidence: `${name} dependency` };
|
|
5118
|
+
}
|
|
5119
|
+
const config = fs11.readdirSync(projectDir).find((name) => /^webpack\.config\.[A-Za-z]+$/.test(name));
|
|
5120
|
+
if (config) return { stack: "webpack", evidence: config };
|
|
5121
|
+
return { stack: "unknown", evidence: "no Vite, Next.js, CRA, webpack or Rsbuild setup found" };
|
|
5122
|
+
}
|
|
5123
|
+
function checkSafePath(name, value) {
|
|
5124
|
+
const segments = value.split("/");
|
|
5125
|
+
if (!SAFE_ARG.test(value) || value.startsWith("/") || segments.includes("..")) {
|
|
5126
|
+
throw new Error(`\`${name}\` may only contain letters, digits, ".", "_", "-" and "/".`);
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
function validateShareArgs(o) {
|
|
5130
|
+
checkSafePath("--locales", o.localesPath);
|
|
5131
|
+
if (o.dir !== void 0) checkSafePath("--dir", o.dir);
|
|
5132
|
+
if (!LOCALE_RE.test(o.sourceLocale)) {
|
|
5133
|
+
throw new Error("`--source` must be a locale code such as en or pt-BR.");
|
|
5134
|
+
}
|
|
5135
|
+
if (o.branches.length === 0) throw new Error("`--branches` needs at least one branch.");
|
|
5136
|
+
if (o.branches.length > MAX_BRANCHES) {
|
|
5137
|
+
throw new Error(`\`--branches\` takes at most ${MAX_BRANCHES} branches.`);
|
|
5138
|
+
}
|
|
5139
|
+
for (const branch of o.branches) checkSafePath("--branches", branch);
|
|
5140
|
+
}
|
|
5141
|
+
function renderWorkflow(o) {
|
|
5142
|
+
validateShareArgs(o);
|
|
5143
|
+
if (!VERSION_RE.test(o.version)) throw new Error(`Not a livelocale version: ${o.version}`);
|
|
5144
|
+
const dirArg = o.dir === void 0 ? "" : ` --dir ${o.dir}`;
|
|
5145
|
+
return [
|
|
5146
|
+
"# Written by `npx livelocale init --share`. Turns LiveLocale change links into pull requests.",
|
|
5147
|
+
"# livelocale reads the link from the event file; it never reaches a shell command.",
|
|
5148
|
+
"name: LiveLocale",
|
|
5149
|
+
"on:",
|
|
5150
|
+
" issues: { types: [opened] }",
|
|
5151
|
+
" issue_comment: { types: [created] }",
|
|
5152
|
+
" pull_request: { types: [closed] }",
|
|
5153
|
+
" workflow_dispatch: { inputs: { link: { description: 'LiveLocale change link', required: true } } }",
|
|
5154
|
+
"permissions: { contents: write, pull-requests: write, issues: write }",
|
|
5155
|
+
"concurrency: { group: 'livelocale-${{ github.event.issue.number || github.run_id }}', queue: max }",
|
|
5156
|
+
"jobs:",
|
|
5157
|
+
" apply:",
|
|
5158
|
+
" if: >-",
|
|
5159
|
+
" github.event_name == 'workflow_dispatch' ||",
|
|
5160
|
+
" (github.event_name == 'issues' && contains(github.event.issue.body, '```livelocale')) ||",
|
|
5161
|
+
" (github.event_name == 'issue_comment' && !github.event.issue.pull_request &&",
|
|
5162
|
+
" startsWith(github.event.comment.body, '/livelocale')) ||",
|
|
5163
|
+
" (github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'livelocale/issue-') &&",
|
|
5164
|
+
" github.event.pull_request.base.ref != github.event.repository.default_branch)",
|
|
5165
|
+
" runs-on: ubuntu-latest",
|
|
5166
|
+
" steps:",
|
|
5167
|
+
` - uses: ${CHECKOUT_ACTION}`,
|
|
5168
|
+
" with: { persist-credentials: false }",
|
|
5169
|
+
` - run: npx --yes livelocale@${o.version} apply --github-event --locales ${o.localesPath} --source ${o.sourceLocale} --branches ${o.branches.join(",")}${dirArg}`,
|
|
5170
|
+
" env: { GITHUB_TOKEN: '${{ github.token }}' }",
|
|
5171
|
+
""
|
|
5172
|
+
].join("\n");
|
|
5173
|
+
}
|
|
5174
|
+
function toPosix2(p) {
|
|
5175
|
+
return p.split(path17.sep).join("/");
|
|
5176
|
+
}
|
|
5177
|
+
function normalizeArgPath(p) {
|
|
5178
|
+
return p.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
|
|
5179
|
+
}
|
|
5180
|
+
function realDir(dir) {
|
|
5181
|
+
try {
|
|
5182
|
+
return fs11.realpathSync(dir);
|
|
5183
|
+
} catch {
|
|
5184
|
+
return path17.resolve(dir);
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
function holdsLocaleJson(dir) {
|
|
5188
|
+
let entries;
|
|
5189
|
+
try {
|
|
5190
|
+
entries = fs11.readdirSync(dir, { withFileTypes: true });
|
|
5191
|
+
} catch {
|
|
5192
|
+
return false;
|
|
5193
|
+
}
|
|
5194
|
+
return entries.some((entry) => {
|
|
5195
|
+
if (entry.isFile())
|
|
5196
|
+
return entry.name.endsWith(".json") && LOCALE_RE.test(entry.name.slice(0, -5));
|
|
5197
|
+
if (!entry.isDirectory() || !LOCALE_RE.test(entry.name)) return false;
|
|
5198
|
+
try {
|
|
5199
|
+
return fs11.readdirSync(path17.join(dir, entry.name)).some((name) => name.endsWith(".json"));
|
|
5200
|
+
} catch {
|
|
5201
|
+
return false;
|
|
5202
|
+
}
|
|
5203
|
+
});
|
|
5204
|
+
}
|
|
5205
|
+
function findLocalesPath(projectDir) {
|
|
5206
|
+
const found = LOCALES_CANDIDATES.find((rel) => holdsLocaleJson(path17.join(projectDir, rel)));
|
|
5207
|
+
if (!found) throw new Error("Pass --locales <path>.");
|
|
5208
|
+
return found;
|
|
5209
|
+
}
|
|
5210
|
+
async function defaultBranchOf(git, cwd) {
|
|
5211
|
+
const ref = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
5212
|
+
if (ref?.startsWith("origin/") && ref.length > "origin/".length) {
|
|
5213
|
+
return ref.slice("origin/".length);
|
|
5214
|
+
}
|
|
5215
|
+
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
5216
|
+
if (head && head !== "HEAD") return head;
|
|
5217
|
+
return "main";
|
|
5218
|
+
}
|
|
5219
|
+
function planWrite(projectDir, rel, content) {
|
|
5220
|
+
const current = readText3(path17.join(projectDir, rel));
|
|
5221
|
+
if (current === null) return { path: rel, action: "create", content };
|
|
5222
|
+
return current === content ? { path: rel, action: "unchanged" } : { path: rel, action: "update", content };
|
|
5223
|
+
}
|
|
5224
|
+
function planViteConfig(c) {
|
|
5225
|
+
const addNote = `Add liveLocaleVite({ localesPath: '${c.localesPath}', share: true }) to the plugins array in vite.config.`;
|
|
5226
|
+
const config = VITE_CONFIG_FILES.find((name) => fs11.existsSync(path17.join(c.projectDir, name)));
|
|
5227
|
+
if (!config) {
|
|
5228
|
+
c.notes.push(addNote);
|
|
5229
|
+
return [];
|
|
5230
|
+
}
|
|
5231
|
+
const source = readText3(path17.join(c.projectDir, config)) ?? "";
|
|
5232
|
+
if (!source.includes("liveLocaleVite(")) {
|
|
5233
|
+
c.notes.push(addNote);
|
|
5234
|
+
return [{ path: config, action: "skipped", reason: "no liveLocaleVite(...) call" }];
|
|
5235
|
+
}
|
|
5236
|
+
if (/\bshare\s*:/.test(source)) return [{ path: config, action: "unchanged" }];
|
|
5237
|
+
const call = /liveLocaleVite\(\{\s*/;
|
|
5238
|
+
if (!call.test(source)) {
|
|
5239
|
+
c.notes.push(addNote);
|
|
5240
|
+
return [{ path: config, action: "skipped", reason: "liveLocaleVite(...) has no { options }" }];
|
|
5241
|
+
}
|
|
5242
|
+
const source2 = c.sourceLocale === "en" ? "" : `sourceLocale: '${c.sourceLocale}', `;
|
|
5243
|
+
const next = source.replace(call, () => `liveLocaleVite({ share: true, ${source2}`);
|
|
5244
|
+
return [{ path: config, action: "update", content: next }];
|
|
5245
|
+
}
|
|
5246
|
+
function planPackageScripts(c) {
|
|
5247
|
+
const text = readText3(path17.join(c.projectDir, "package.json"));
|
|
5248
|
+
const pkg = parsePackageJson(text);
|
|
5249
|
+
const args = `--locales ${c.localesPath} --source ${c.sourceLocale}`;
|
|
5250
|
+
const isNext = c.stack === "next-export" || c.stack === "next-server";
|
|
5251
|
+
const outDir = c.stack === "cra" ? "build" : "dist";
|
|
5252
|
+
const packCommand = `livelocale pack ${outDir} ${args}`;
|
|
5253
|
+
if (!pkg) {
|
|
5254
|
+
c.notes.push(
|
|
5255
|
+
isNext ? `Add "build:preview": "livelocale build --preview ${args} -- next build" to package.json.` : `Run "${packCommand}" after your build.`
|
|
5256
|
+
);
|
|
5257
|
+
return [{ path: "package.json", action: "skipped", reason: "no readable package.json" }];
|
|
5258
|
+
}
|
|
5259
|
+
const scripts = { ...pkg.scripts ?? {} };
|
|
5260
|
+
if (isNext) {
|
|
5261
|
+
const wanted = `livelocale build --preview ${args} -- next build`;
|
|
5262
|
+
const current = scripts["build:preview"];
|
|
5263
|
+
if (current === wanted || typeof current === "string" && current.includes("livelocale build")) {
|
|
5264
|
+
return [{ path: "package.json", action: "unchanged" }];
|
|
5265
|
+
}
|
|
5266
|
+
if (current !== void 0) {
|
|
5267
|
+
c.notes.push(`Set "build:preview" to "${wanted}" in package.json.`);
|
|
5268
|
+
return [
|
|
5269
|
+
{
|
|
5270
|
+
path: "package.json",
|
|
5271
|
+
action: "skipped",
|
|
5272
|
+
reason: 'scripts["build:preview"] already exists'
|
|
5273
|
+
}
|
|
5274
|
+
];
|
|
5275
|
+
}
|
|
5276
|
+
scripts["build:preview"] = wanted;
|
|
5277
|
+
} else {
|
|
5278
|
+
const build = scripts.build;
|
|
5279
|
+
if (typeof build !== "string" || build.trim() === "") {
|
|
5280
|
+
c.notes.push(`Run "${packCommand}" after your build.`);
|
|
5281
|
+
return [{ path: "package.json", action: "skipped", reason: "no scripts.build" }];
|
|
5282
|
+
}
|
|
5283
|
+
if (build.includes("livelocale pack")) return [{ path: "package.json", action: "unchanged" }];
|
|
5284
|
+
scripts.build = `${build} && ${packCommand}`;
|
|
5285
|
+
}
|
|
5286
|
+
return [
|
|
5287
|
+
{
|
|
5288
|
+
path: "package.json",
|
|
5289
|
+
action: "update",
|
|
5290
|
+
content: stringifyLike({ ...pkg, scripts }, text)
|
|
5291
|
+
}
|
|
5292
|
+
];
|
|
5293
|
+
}
|
|
5294
|
+
function planGitignore(projectDir) {
|
|
5295
|
+
const current = readText3(path17.join(projectDir, ".gitignore"));
|
|
5296
|
+
if (current === null) {
|
|
5297
|
+
return { path: ".gitignore", action: "create", content: `${SHARE_GITIGNORE}
|
|
5298
|
+
` };
|
|
5299
|
+
}
|
|
5300
|
+
if (parseGitignore(current).ignores("public/livelocale", true)) {
|
|
5301
|
+
return { path: ".gitignore", action: "unchanged" };
|
|
5302
|
+
}
|
|
5303
|
+
const sep9 = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
5304
|
+
return { path: ".gitignore", action: "update", content: `${current}${sep9}${SHARE_GITIGNORE}
|
|
5305
|
+
` };
|
|
5306
|
+
}
|
|
5307
|
+
function planBuild(c) {
|
|
5308
|
+
switch (c.stack) {
|
|
5309
|
+
case "vite":
|
|
5310
|
+
return planViteConfig(c);
|
|
5311
|
+
case "cra":
|
|
5312
|
+
case "webpack":
|
|
5313
|
+
return planPackageScripts(c);
|
|
5314
|
+
case "next-export":
|
|
5315
|
+
case "next-server": {
|
|
5316
|
+
const files = [...planPackageScripts(c), planGitignore(c.projectDir)];
|
|
5317
|
+
if (c.stack === "next-export") {
|
|
5318
|
+
const route = findExportBlockingRoute(c.projectDir);
|
|
5319
|
+
if (route) {
|
|
5320
|
+
c.notes.push(
|
|
5321
|
+
`Delete ${route}: static export rejects POST routes, and livelocale build stops while it exists.`
|
|
5322
|
+
);
|
|
5323
|
+
}
|
|
5324
|
+
} else {
|
|
5325
|
+
c.notes.push(
|
|
5326
|
+
'Use "npm run build:preview" as the build command of your preview deployments.'
|
|
5327
|
+
);
|
|
5328
|
+
}
|
|
5329
|
+
return files;
|
|
5330
|
+
}
|
|
5331
|
+
case "unknown":
|
|
5332
|
+
c.notes.push(
|
|
5333
|
+
`No supported build setup found. In custom CI, run LIVELOCALE=preview <your build> (only for SSR/SSG), then npx livelocale pack <outDir> --locales ${c.localesPath} --source ${c.sourceLocale}.`
|
|
5334
|
+
);
|
|
5335
|
+
return [];
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
async function planInitShare(o) {
|
|
5339
|
+
const projectDir = realDir(o.projectDir);
|
|
5340
|
+
const git = o.git ?? runGit;
|
|
5341
|
+
const sourceLocale = o.sourceLocale ?? "en";
|
|
5342
|
+
const localesPath = o.localesPath !== void 0 ? normalizeArgPath(o.localesPath) : findLocalesPath(projectDir);
|
|
5343
|
+
validateShareArgs({ localesPath, sourceLocale, branches: o.branches ?? ["main"] });
|
|
5344
|
+
const { stack } = detectShareStack(projectDir);
|
|
5345
|
+
const notes = [];
|
|
5346
|
+
const files = planBuild({ projectDir, stack, localesPath, sourceLocale, notes });
|
|
5347
|
+
const remote = await git(["remote", "get-url", "origin"], projectDir);
|
|
5348
|
+
if (!remote || parseGitHubSlug(remote) === null) {
|
|
5349
|
+
notes.push('One-click pull requests need GitHub. Use: npx livelocale apply "<link>" --pr');
|
|
5350
|
+
return { stack, files, notes };
|
|
5351
|
+
}
|
|
5352
|
+
const top = await git(["rev-parse", "--show-toplevel"], projectDir);
|
|
5353
|
+
const repoRoot = top ? realDir(top) : projectDir;
|
|
5354
|
+
const appRel = toPosix2(path17.relative(repoRoot, projectDir));
|
|
5355
|
+
const dir = appRel === "" ? void 0 : appRel;
|
|
5356
|
+
const repoLocales = dir === void 0 ? localesPath : `${dir}/${localesPath}`;
|
|
5357
|
+
const defaultBranch = await defaultBranchOf(git, projectDir);
|
|
5358
|
+
const branches = o.branches ?? [defaultBranch];
|
|
5359
|
+
const workflow = renderWorkflow({
|
|
5360
|
+
version: o.version,
|
|
5361
|
+
localesPath: repoLocales,
|
|
5362
|
+
sourceLocale,
|
|
5363
|
+
branches,
|
|
5364
|
+
dir
|
|
5365
|
+
});
|
|
5366
|
+
const workflowRel = toPosix2(path17.relative(projectDir, path17.join(repoRoot, WORKFLOW_PATH)));
|
|
5367
|
+
const planned = planWrite(projectDir, workflowRel, workflow);
|
|
5368
|
+
files.push(
|
|
5369
|
+
planned.action === "update" && !o.overwrite ? {
|
|
5370
|
+
path: workflowRel,
|
|
5371
|
+
action: "skipped",
|
|
5372
|
+
reason: "it exists with different content; pass --overwrite to replace it"
|
|
5373
|
+
} : planned
|
|
5374
|
+
);
|
|
5375
|
+
for (const branch of branches) {
|
|
5376
|
+
if (branch === defaultBranch) continue;
|
|
5377
|
+
notes.push(
|
|
5378
|
+
`Also merge ${WORKFLOW_PATH} into \`${branch}\`; pull requests into it run workflows from that branch, so LiveLocale cannot close their issues otherwise.`
|
|
5379
|
+
);
|
|
5380
|
+
}
|
|
5381
|
+
return { stack, files, notes };
|
|
5382
|
+
}
|
|
5383
|
+
function writePlannedFiles(projectDir, files) {
|
|
5384
|
+
for (const file of files) {
|
|
5385
|
+
if (file.action !== "create" && file.action !== "update" || file.content === void 0) {
|
|
5386
|
+
continue;
|
|
5387
|
+
}
|
|
5388
|
+
const target = path17.resolve(projectDir, file.path);
|
|
5389
|
+
fs11.mkdirSync(path17.dirname(target), { recursive: true });
|
|
5390
|
+
fs11.writeFileSync(target, file.content, "utf-8");
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
|
|
4355
5394
|
// src/codemod/index.ts
|
|
4356
5395
|
var DEFAULT_STUDIO_PORT = 3010;
|
|
4357
5396
|
var DEFAULT_APP_ORIGIN = "http://localhost:3000";
|
|
@@ -4379,6 +5418,11 @@ Options for migrate:
|
|
|
4379
5418
|
|
|
4380
5419
|
Options for init:
|
|
4381
5420
|
--dir <path> Target project directory (default: current directory)
|
|
5421
|
+
--share Set up the share link build and the GitHub workflow instead of the API route
|
|
5422
|
+
--locales <path> (--share) Locales directory relative to the project (default: detected)
|
|
5423
|
+
--source <locale> (--share) Source locale (default: en)
|
|
5424
|
+
--branches <a,b> (--share) Branches a change link may target (default: the default branch)
|
|
5425
|
+
--overwrite (--share) Replace an existing .github/workflows/livelocale.yml
|
|
4382
5426
|
|
|
4383
5427
|
Options for studio:
|
|
4384
5428
|
--port <number> Port to run the studio server on (default: ${DEFAULT_STUDIO_PORT})
|
|
@@ -4424,6 +5468,8 @@ Options for apply:
|
|
|
4424
5468
|
--source <locale> Source locale (default: en, else the first locale)
|
|
4425
5469
|
--force Apply edits the translator saved with "Save anyway"
|
|
4426
5470
|
--dry-run Show what would change without writing files
|
|
5471
|
+
--github-event Run inside the generated GitHub workflow; reads the link from the event file
|
|
5472
|
+
--branches <a,b> Branches a link may target (default: the repository's default branch)
|
|
4427
5473
|
|
|
4428
5474
|
Global Options:
|
|
4429
5475
|
-h, --help Show help information
|
|
@@ -4452,11 +5498,11 @@ function normalizeAppArg(value) {
|
|
|
4452
5498
|
}
|
|
4453
5499
|
}
|
|
4454
5500
|
function isPortAnswering(port) {
|
|
4455
|
-
return new Promise((
|
|
5501
|
+
return new Promise((resolve16) => {
|
|
4456
5502
|
const socket = net.connect({ host: "localhost", port });
|
|
4457
5503
|
const done = (answering) => {
|
|
4458
5504
|
socket.destroy();
|
|
4459
|
-
|
|
5505
|
+
resolve16(answering);
|
|
4460
5506
|
};
|
|
4461
5507
|
socket.setTimeout(1e3, () => done(false));
|
|
4462
5508
|
socket.once("connect", () => done(true));
|
|
@@ -4466,8 +5512,8 @@ function isPortAnswering(port) {
|
|
|
4466
5512
|
async function runStudio(port, appOrigin) {
|
|
4467
5513
|
console.log(`
|
|
4468
5514
|
\u{1F680} Starting LiveLocale Studio...`);
|
|
4469
|
-
const studioDir =
|
|
4470
|
-
if (!
|
|
5515
|
+
const studioDir = path18.resolve(moduleDir(), "../studio");
|
|
5516
|
+
if (!fs12.existsSync(path18.join(studioDir, "index.html"))) {
|
|
4471
5517
|
console.error(`Error: Studio assets not found at "${studioDir}".`);
|
|
4472
5518
|
console.error(`Did you build the project? Run "npm run build" first.`);
|
|
4473
5519
|
process.exit(1);
|
|
@@ -4499,48 +5545,48 @@ async function runStudio(port, appOrigin) {
|
|
|
4499
5545
|
function runInit(projectDir) {
|
|
4500
5546
|
console.log(`
|
|
4501
5547
|
\u{1F680} Initializing LiveLocale API handler...`);
|
|
4502
|
-
const absoluteDir =
|
|
4503
|
-
if (!
|
|
5548
|
+
const absoluteDir = path18.resolve(projectDir);
|
|
5549
|
+
if (!fs12.existsSync(absoluteDir)) {
|
|
4504
5550
|
console.error(`Error: Directory "${projectDir}" does not exist.`);
|
|
4505
5551
|
process.exit(1);
|
|
4506
5552
|
}
|
|
4507
|
-
const isTypeScript =
|
|
5553
|
+
const isTypeScript = fs12.existsSync(path18.join(absoluteDir, "tsconfig.json"));
|
|
4508
5554
|
const fileExt = isTypeScript ? "ts" : "js";
|
|
4509
5555
|
console.log(`\u{1F4C1} Target: ${absoluteDir}`);
|
|
4510
5556
|
console.log(`\u{1F527} Language detected: ${isTypeScript ? "TypeScript" : "JavaScript"}`);
|
|
4511
5557
|
let hasNextIntl = false;
|
|
4512
5558
|
let hasReactI18next = false;
|
|
4513
|
-
const targetPkgPath =
|
|
4514
|
-
if (
|
|
5559
|
+
const targetPkgPath = path18.join(absoluteDir, "package.json");
|
|
5560
|
+
if (fs12.existsSync(targetPkgPath)) {
|
|
4515
5561
|
try {
|
|
4516
|
-
const pkg = JSON.parse(
|
|
5562
|
+
const pkg = JSON.parse(fs12.readFileSync(targetPkgPath, "utf8"));
|
|
4517
5563
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4518
5564
|
hasNextIntl = Boolean(allDeps["next-intl"]);
|
|
4519
5565
|
hasReactI18next = Boolean(allDeps["react-i18next"]);
|
|
4520
5566
|
} catch {
|
|
4521
5567
|
}
|
|
4522
5568
|
}
|
|
4523
|
-
const appDirPath =
|
|
4524
|
-
const pagesDirPath =
|
|
4525
|
-
const srcAppDirPath =
|
|
4526
|
-
const srcPagesDirPath =
|
|
5569
|
+
const appDirPath = path18.join(absoluteDir, "app");
|
|
5570
|
+
const pagesDirPath = path18.join(absoluteDir, "pages");
|
|
5571
|
+
const srcAppDirPath = path18.join(absoluteDir, "src", "app");
|
|
5572
|
+
const srcPagesDirPath = path18.join(absoluteDir, "src", "pages");
|
|
4527
5573
|
let targetPath = "";
|
|
4528
5574
|
let routerType = "";
|
|
4529
5575
|
let content = "";
|
|
4530
|
-
if (
|
|
4531
|
-
targetPath =
|
|
5576
|
+
if (fs12.existsSync(appDirPath)) {
|
|
5577
|
+
targetPath = path18.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
|
|
4532
5578
|
routerType = "Next.js App Router (root)";
|
|
4533
|
-
} else if (
|
|
4534
|
-
targetPath =
|
|
5579
|
+
} else if (fs12.existsSync(srcAppDirPath)) {
|
|
5580
|
+
targetPath = path18.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
|
|
4535
5581
|
routerType = "Next.js App Router (src/)";
|
|
4536
|
-
} else if (
|
|
4537
|
-
targetPath =
|
|
5582
|
+
} else if (fs12.existsSync(pagesDirPath)) {
|
|
5583
|
+
targetPath = path18.join(pagesDirPath, "api", `livelocale.${fileExt}`);
|
|
4538
5584
|
routerType = "Next.js Pages Router (root)";
|
|
4539
|
-
} else if (
|
|
4540
|
-
targetPath =
|
|
5585
|
+
} else if (fs12.existsSync(srcPagesDirPath)) {
|
|
5586
|
+
targetPath = path18.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
|
|
4541
5587
|
routerType = "Next.js Pages Router (src/)";
|
|
4542
5588
|
} else {
|
|
4543
|
-
targetPath =
|
|
5589
|
+
targetPath = path18.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
|
|
4544
5590
|
routerType = "Default App Router (app/ not found)";
|
|
4545
5591
|
}
|
|
4546
5592
|
console.log(`\u{1F4CD} Project structure: ${routerType}`);
|
|
@@ -4568,14 +5614,14 @@ const handler = createLiveLocaleHandler({
|
|
|
4568
5614
|
export default handler;
|
|
4569
5615
|
`;
|
|
4570
5616
|
}
|
|
4571
|
-
const targetDir =
|
|
4572
|
-
if (!
|
|
4573
|
-
|
|
5617
|
+
const targetDir = path18.dirname(targetPath);
|
|
5618
|
+
if (!fs12.existsSync(targetDir)) {
|
|
5619
|
+
fs12.mkdirSync(targetDir, { recursive: true });
|
|
4574
5620
|
}
|
|
4575
5621
|
try {
|
|
4576
|
-
|
|
5622
|
+
fs12.writeFileSync(targetPath, content, "utf8");
|
|
4577
5623
|
console.log(`\u2705 Created API Route Handler successfully at:`);
|
|
4578
|
-
console.log(` ${
|
|
5624
|
+
console.log(` ${path18.relative(absoluteDir, targetPath)}`);
|
|
4579
5625
|
if (hasNextIntl) {
|
|
4580
5626
|
console.log(`
|
|
4581
5627
|
\u{1F4A1} Detected 'next-intl' in your project!`);
|
|
@@ -4617,8 +5663,8 @@ async function runMigrate(args) {
|
|
|
4617
5663
|
}
|
|
4618
5664
|
}
|
|
4619
5665
|
}
|
|
4620
|
-
const absoluteDir =
|
|
4621
|
-
if (!
|
|
5666
|
+
const absoluteDir = path18.resolve(dir);
|
|
5667
|
+
if (!fs12.existsSync(absoluteDir)) {
|
|
4622
5668
|
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
4623
5669
|
process.exit(1);
|
|
4624
5670
|
}
|
|
@@ -4642,28 +5688,28 @@ async function runMigrate(args) {
|
|
|
4642
5688
|
let scannedCount = 0;
|
|
4643
5689
|
let migratedCount = 0;
|
|
4644
5690
|
async function walk(currentDir) {
|
|
4645
|
-
const files =
|
|
5691
|
+
const files = fs12.readdirSync(currentDir, { withFileTypes: true });
|
|
4646
5692
|
for (const file of files) {
|
|
4647
|
-
const fullPath =
|
|
5693
|
+
const fullPath = path18.join(currentDir, file.name);
|
|
4648
5694
|
if (file.isDirectory()) {
|
|
4649
5695
|
if (excludes.includes(file.name)) {
|
|
4650
5696
|
continue;
|
|
4651
5697
|
}
|
|
4652
5698
|
await walk(fullPath);
|
|
4653
5699
|
} else if (file.isFile()) {
|
|
4654
|
-
const ext =
|
|
5700
|
+
const ext = path18.extname(file.name);
|
|
4655
5701
|
if ([".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
|
|
4656
5702
|
scannedCount++;
|
|
4657
5703
|
try {
|
|
4658
|
-
const content =
|
|
5704
|
+
const content = fs12.readFileSync(fullPath, "utf8");
|
|
4659
5705
|
const result = await transformReactI18next2(content, fullPath);
|
|
4660
5706
|
if (result.modified) {
|
|
4661
5707
|
migratedCount++;
|
|
4662
|
-
const relativePath =
|
|
5708
|
+
const relativePath = path18.relative(absoluteDir, fullPath);
|
|
4663
5709
|
if (dryRun) {
|
|
4664
5710
|
console.log(`[DRY RUN] Would migrate: ${relativePath}`);
|
|
4665
5711
|
} else {
|
|
4666
|
-
|
|
5712
|
+
fs12.writeFileSync(fullPath, result.code, "utf8");
|
|
4667
5713
|
console.log(`\u2705 Migrated: ${relativePath}`);
|
|
4668
5714
|
}
|
|
4669
5715
|
}
|
|
@@ -4712,8 +5758,8 @@ async function runDoctorCommand(args) {
|
|
|
4712
5758
|
dir = arg;
|
|
4713
5759
|
}
|
|
4714
5760
|
}
|
|
4715
|
-
const projectDir =
|
|
4716
|
-
if (!
|
|
5761
|
+
const projectDir = path18.resolve(dir);
|
|
5762
|
+
if (!fs12.existsSync(projectDir)) {
|
|
4717
5763
|
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
4718
5764
|
process.exit(1);
|
|
4719
5765
|
}
|
|
@@ -4774,8 +5820,8 @@ async function runCheckCommand(args) {
|
|
|
4774
5820
|
dir = arg;
|
|
4775
5821
|
}
|
|
4776
5822
|
}
|
|
4777
|
-
const projectDir =
|
|
4778
|
-
if (!
|
|
5823
|
+
const projectDir = path18.resolve(dir);
|
|
5824
|
+
if (!fs12.existsSync(projectDir)) {
|
|
4779
5825
|
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
4780
5826
|
process.exit(1);
|
|
4781
5827
|
}
|
|
@@ -4816,7 +5862,7 @@ async function runCheckCommand(args) {
|
|
|
4816
5862
|
var PACK_USAGE = "Usage: livelocale pack <outDir> [options]";
|
|
4817
5863
|
function printPackSummary(result, projectDir, warnings) {
|
|
4818
5864
|
for (const warning of warnings) console.log(`\u26A0\uFE0F ${warning}`);
|
|
4819
|
-
const outRel =
|
|
5865
|
+
const outRel = path18.relative(projectDir, path18.dirname(result.shareDir)).split(path18.sep).join("/") || ".";
|
|
4820
5866
|
console.log(
|
|
4821
5867
|
`\u2705 LiveLocale share folder written to ${outRel}/livelocale/ (${result.manifest.locales.length} locales, ${result.manifest.codeKeys.length} code keys, ${result.htmlInjected.length} HTML files linked).`
|
|
4822
5868
|
);
|
|
@@ -4828,7 +5874,7 @@ async function runPackCommand(args) {
|
|
|
4828
5874
|
if (options.positional.length !== 1 || options.out !== void 0 || options.preview || options.command.length > 0) {
|
|
4829
5875
|
throw new PackError("USAGE", PACK_USAGE);
|
|
4830
5876
|
}
|
|
4831
|
-
const projectDir =
|
|
5877
|
+
const projectDir = path18.resolve(options.dir);
|
|
4832
5878
|
const result = await packOutput({
|
|
4833
5879
|
projectDir,
|
|
4834
5880
|
outDir: options.positional[0],
|
|
@@ -4851,7 +5897,7 @@ async function runBuildCli(args) {
|
|
|
4851
5897
|
if (options.positional.length > 0 || !options.inject) {
|
|
4852
5898
|
throw new PackError("USAGE", BUILD_USAGE);
|
|
4853
5899
|
}
|
|
4854
|
-
const projectDir =
|
|
5900
|
+
const projectDir = path18.resolve(options.dir);
|
|
4855
5901
|
const result = await runBuildCommand({
|
|
4856
5902
|
projectDir,
|
|
4857
5903
|
command: options.command,
|
|
@@ -4874,6 +5920,55 @@ async function runBuildCli(args) {
|
|
|
4874
5920
|
process.exitCode = 1;
|
|
4875
5921
|
}
|
|
4876
5922
|
}
|
|
5923
|
+
async function runInitShareCommand(args) {
|
|
5924
|
+
let dir = ".";
|
|
5925
|
+
let localesPath;
|
|
5926
|
+
let sourceLocale;
|
|
5927
|
+
let branches;
|
|
5928
|
+
let overwrite = false;
|
|
5929
|
+
for (let i = 1; i < args.length; i++) {
|
|
5930
|
+
const arg = args[i];
|
|
5931
|
+
if (arg === "--dir") {
|
|
5932
|
+
dir = args[++i] || ".";
|
|
5933
|
+
} else if (arg === "--locales") {
|
|
5934
|
+
localesPath = args[++i];
|
|
5935
|
+
} else if (arg === "--source") {
|
|
5936
|
+
sourceLocale = args[++i];
|
|
5937
|
+
} else if (arg === "--branches") {
|
|
5938
|
+
branches = (args[++i] ?? "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
5939
|
+
} else if (arg === "--overwrite") {
|
|
5940
|
+
overwrite = true;
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
const projectDir = path18.resolve(dir);
|
|
5944
|
+
if (!fs12.existsSync(projectDir)) {
|
|
5945
|
+
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
5946
|
+
process.exit(1);
|
|
5947
|
+
}
|
|
5948
|
+
try {
|
|
5949
|
+
const plan = await planInitShare({
|
|
5950
|
+
projectDir,
|
|
5951
|
+
localesPath,
|
|
5952
|
+
sourceLocale,
|
|
5953
|
+
branches,
|
|
5954
|
+
version: readOwnVersion(),
|
|
5955
|
+
overwrite
|
|
5956
|
+
});
|
|
5957
|
+
writePlannedFiles(projectDir, plan.files);
|
|
5958
|
+
console.log(`
|
|
5959
|
+
\u{1F680} LiveLocale share link setup (${plan.stack})`);
|
|
5960
|
+
for (const file of plan.files) {
|
|
5961
|
+
console.log(
|
|
5962
|
+
` ${file.action.padEnd(9)} ${file.path}${file.reason ? ` (${file.reason})` : ""}`
|
|
5963
|
+
);
|
|
5964
|
+
}
|
|
5965
|
+
for (const note of plan.notes) console.log(`\u{1F4A1} ${note}`);
|
|
5966
|
+
console.log("Commit and push: the workflow must be on the default branch.");
|
|
5967
|
+
} catch (err) {
|
|
5968
|
+
console.error(`\u274C ${err.message}`);
|
|
5969
|
+
process.exitCode = 1;
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
4877
5972
|
async function run2() {
|
|
4878
5973
|
const args = process.argv.slice(2);
|
|
4879
5974
|
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
@@ -4906,6 +6001,10 @@ async function run2() {
|
|
|
4906
6001
|
await runApplyCommand(args.slice(1));
|
|
4907
6002
|
return;
|
|
4908
6003
|
}
|
|
6004
|
+
if (args[0] === "init" && args.includes("--share")) {
|
|
6005
|
+
await runInitShareCommand(args);
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
4909
6008
|
if (args[0] === "init") {
|
|
4910
6009
|
let dir = ".";
|
|
4911
6010
|
for (let i = 1; i < args.length; i++) {
|