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.cjs
CHANGED
|
@@ -313,11 +313,12 @@ var init_transform = __esm({
|
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
// src/codemod/index.ts
|
|
316
|
-
var
|
|
317
|
-
var
|
|
316
|
+
var fs12 = __toESM(require("fs"), 1);
|
|
317
|
+
var path18 = __toESM(require("path"), 1);
|
|
318
318
|
var net = __toESM(require("net"), 1);
|
|
319
319
|
|
|
320
320
|
// src/server/module-dir.ts
|
|
321
|
+
var import_fs = require("fs");
|
|
321
322
|
var path = __toESM(require("path"), 1);
|
|
322
323
|
var import_url = require("url");
|
|
323
324
|
var import_meta = {};
|
|
@@ -327,6 +328,15 @@ function moduleDir() {
|
|
|
327
328
|
}
|
|
328
329
|
return path.dirname((0, import_url.fileURLToPath)(import_meta.url));
|
|
329
330
|
}
|
|
331
|
+
function readOwnVersion() {
|
|
332
|
+
const file = path.resolve(moduleDir(), "../../package.json");
|
|
333
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)(file, "utf-8"));
|
|
334
|
+
const version = pkg?.version;
|
|
335
|
+
if (typeof version !== "string" || version === "") {
|
|
336
|
+
throw new Error(`No version in ${file}.`);
|
|
337
|
+
}
|
|
338
|
+
return version;
|
|
339
|
+
}
|
|
330
340
|
|
|
331
341
|
// src/codemod/studio-server.ts
|
|
332
342
|
var fs = __toESM(require("fs"), 1);
|
|
@@ -751,7 +761,7 @@ function isShareManifest(value) {
|
|
|
751
761
|
const layout = own2(value, "layout");
|
|
752
762
|
const locales = own2(value, "locales");
|
|
753
763
|
const sourceLocale = own2(value, "sourceLocale");
|
|
754
|
-
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"), (
|
|
764
|
+
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);
|
|
755
765
|
}
|
|
756
766
|
|
|
757
767
|
// src/codemod/doctor.ts
|
|
@@ -1600,7 +1610,7 @@ function validateMessageSyntax(message) {
|
|
|
1600
1610
|
}
|
|
1601
1611
|
|
|
1602
1612
|
// src/server/key-scanner.ts
|
|
1603
|
-
var
|
|
1613
|
+
var import_fs2 = require("fs");
|
|
1604
1614
|
var path4 = __toESM(require("path"), 1);
|
|
1605
1615
|
|
|
1606
1616
|
// src/core/env.ts
|
|
@@ -1885,7 +1895,7 @@ var KeyScanner = class {
|
|
|
1885
1895
|
const gitignorePath = path4.join(projectRoot, ".gitignore");
|
|
1886
1896
|
let stat;
|
|
1887
1897
|
try {
|
|
1888
|
-
stat = await
|
|
1898
|
+
stat = await import_fs2.promises.stat(gitignorePath);
|
|
1889
1899
|
} catch {
|
|
1890
1900
|
this.gitignoreCache = null;
|
|
1891
1901
|
return NO_IGNORES;
|
|
@@ -1895,7 +1905,7 @@ var KeyScanner = class {
|
|
|
1895
1905
|
return hit.value;
|
|
1896
1906
|
}
|
|
1897
1907
|
try {
|
|
1898
|
-
const content = await
|
|
1908
|
+
const content = await import_fs2.promises.readFile(gitignorePath, "utf-8");
|
|
1899
1909
|
const matcher = parseGitignore(content);
|
|
1900
1910
|
this.gitignoreCache = { mtimeMs: stat.mtimeMs, size: stat.size, value: matcher };
|
|
1901
1911
|
return matcher;
|
|
@@ -1920,7 +1930,7 @@ var KeyScanner = class {
|
|
|
1920
1930
|
const scanDir = async (dir) => {
|
|
1921
1931
|
let items;
|
|
1922
1932
|
try {
|
|
1923
|
-
items = await
|
|
1933
|
+
items = await import_fs2.promises.readdir(dir, { withFileTypes: true });
|
|
1924
1934
|
} catch {
|
|
1925
1935
|
return;
|
|
1926
1936
|
}
|
|
@@ -1938,7 +1948,7 @@ var KeyScanner = class {
|
|
|
1938
1948
|
if (gitignore.ignores(toRel(fullPath), false)) continue;
|
|
1939
1949
|
let stat;
|
|
1940
1950
|
try {
|
|
1941
|
-
stat = await
|
|
1951
|
+
stat = await import_fs2.promises.stat(fullPath);
|
|
1942
1952
|
} catch {
|
|
1943
1953
|
continue;
|
|
1944
1954
|
}
|
|
@@ -1950,7 +1960,7 @@ var KeyScanner = class {
|
|
|
1950
1960
|
continue;
|
|
1951
1961
|
}
|
|
1952
1962
|
try {
|
|
1953
|
-
const content = await
|
|
1963
|
+
const content = await import_fs2.promises.readFile(fullPath, "utf-8");
|
|
1954
1964
|
const keys = extractKeys(content);
|
|
1955
1965
|
this.fileCache.set(fullPath, { mtimeMs: stat.mtimeMs, size: stat.size, value: keys });
|
|
1956
1966
|
for (const k of keys) keysSet.add(k);
|
|
@@ -1977,18 +1987,18 @@ var KeyScanner = class {
|
|
|
1977
1987
|
const localePath = path4.resolve(resolvedBasePath, locale);
|
|
1978
1988
|
let isDir = false;
|
|
1979
1989
|
try {
|
|
1980
|
-
const stats = await
|
|
1990
|
+
const stats = await import_fs2.promises.stat(localePath);
|
|
1981
1991
|
isDir = stats.isDirectory();
|
|
1982
1992
|
} catch {
|
|
1983
1993
|
}
|
|
1984
1994
|
let data = {};
|
|
1985
1995
|
if (isDir) {
|
|
1986
|
-
const files = await
|
|
1996
|
+
const files = await import_fs2.promises.readdir(localePath);
|
|
1987
1997
|
for (const file of files) {
|
|
1988
1998
|
if (file.endsWith(".json")) {
|
|
1989
1999
|
const ns = path4.basename(file, ".json");
|
|
1990
2000
|
try {
|
|
1991
|
-
const content = await
|
|
2001
|
+
const content = await import_fs2.promises.readFile(path4.join(localePath, file), "utf-8");
|
|
1992
2002
|
data[ns] = JSON.parse(content);
|
|
1993
2003
|
} catch {
|
|
1994
2004
|
}
|
|
@@ -2001,7 +2011,7 @@ var KeyScanner = class {
|
|
|
2001
2011
|
}
|
|
2002
2012
|
const filePath = path4.resolve(resolvedBasePath, `${locale}.json`);
|
|
2003
2013
|
if (filePath.startsWith(resolvedBasePath)) {
|
|
2004
|
-
const content = await
|
|
2014
|
+
const content = await import_fs2.promises.readFile(filePath, "utf-8");
|
|
2005
2015
|
data = JSON.parse(content);
|
|
2006
2016
|
}
|
|
2007
2017
|
} catch {
|
|
@@ -2255,23 +2265,23 @@ function formatCheckReport(report, limit = 25) {
|
|
|
2255
2265
|
|
|
2256
2266
|
// src/server/pack.ts
|
|
2257
2267
|
var import_child_process2 = require("child_process");
|
|
2258
|
-
var
|
|
2268
|
+
var import_fs5 = require("fs");
|
|
2259
2269
|
var path8 = __toESM(require("path"), 1);
|
|
2260
2270
|
|
|
2261
2271
|
// src/server/build-info.ts
|
|
2262
2272
|
var import_child_process = require("child_process");
|
|
2263
|
-
var
|
|
2273
|
+
var import_fs3 = require("fs");
|
|
2264
2274
|
var path6 = __toESM(require("path"), 1);
|
|
2265
|
-
var runGit = (args, cwd) => new Promise((
|
|
2275
|
+
var runGit = (args, cwd) => new Promise((resolve16) => {
|
|
2266
2276
|
try {
|
|
2267
2277
|
(0, import_child_process.execFile)(
|
|
2268
2278
|
"git",
|
|
2269
2279
|
args,
|
|
2270
2280
|
{ cwd, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
|
|
2271
|
-
(error, stdout) =>
|
|
2281
|
+
(error, stdout) => resolve16(error ? null : String(stdout).trim())
|
|
2272
2282
|
);
|
|
2273
2283
|
} catch {
|
|
2274
|
-
|
|
2284
|
+
resolve16(null);
|
|
2275
2285
|
}
|
|
2276
2286
|
});
|
|
2277
2287
|
var SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
|
|
@@ -2290,7 +2300,7 @@ var asSlug = (value) => isValidRepoSlug(value) ? value : null;
|
|
|
2290
2300
|
function readEvent(eventPath) {
|
|
2291
2301
|
if (!eventPath) return {};
|
|
2292
2302
|
try {
|
|
2293
|
-
const parsed = JSON.parse((0,
|
|
2303
|
+
const parsed = JSON.parse((0, import_fs3.readFileSync)(eventPath, "utf-8"));
|
|
2294
2304
|
return isRecord(parsed) ? parsed : {};
|
|
2295
2305
|
} catch {
|
|
2296
2306
|
return {};
|
|
@@ -2389,7 +2399,7 @@ function parseGitHubSlug(remote) {
|
|
|
2389
2399
|
}
|
|
2390
2400
|
|
|
2391
2401
|
// src/server/build-target.ts
|
|
2392
|
-
var
|
|
2402
|
+
var import_fs4 = require("fs");
|
|
2393
2403
|
var path7 = __toESM(require("path"), 1);
|
|
2394
2404
|
var NEXT_CONFIG_FILES = [
|
|
2395
2405
|
"next.config.js",
|
|
@@ -2411,7 +2421,7 @@ var VITE_BASE = /\bbase\s*:\s*['"`](\/[^'"`]*|\.\/?|)['"`]/;
|
|
|
2411
2421
|
var EXPORTS_POST = /export\s+(?:const|let|var|async\s+function|function)\s+POST\b|export\s*\{[^}]*\bPOST\b[^}]*\}/;
|
|
2412
2422
|
function readText2(file) {
|
|
2413
2423
|
try {
|
|
2414
|
-
return (0,
|
|
2424
|
+
return (0, import_fs4.readFileSync)(file, "utf-8");
|
|
2415
2425
|
} catch {
|
|
2416
2426
|
return null;
|
|
2417
2427
|
}
|
|
@@ -2432,7 +2442,7 @@ function hasDependency(pkg, name) {
|
|
|
2432
2442
|
function firstExisting2(projectDir, names) {
|
|
2433
2443
|
for (const name of names) {
|
|
2434
2444
|
const file = path7.join(projectDir, name);
|
|
2435
|
-
if ((0,
|
|
2445
|
+
if ((0, import_fs4.existsSync)(file)) return file;
|
|
2436
2446
|
}
|
|
2437
2447
|
return null;
|
|
2438
2448
|
}
|
|
@@ -2563,8 +2573,8 @@ var NO_REPO_WARNING = "exposeRepo is on but no GitHub repository was found; the
|
|
|
2563
2573
|
function isPlainObject2(value) {
|
|
2564
2574
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2565
2575
|
}
|
|
2566
|
-
function toPosix(
|
|
2567
|
-
return
|
|
2576
|
+
function toPosix(relative9) {
|
|
2577
|
+
return relative9.split(path8.sep).join("/") || ".";
|
|
2568
2578
|
}
|
|
2569
2579
|
function isInside(parent, child) {
|
|
2570
2580
|
const rel = path8.relative(parent, child);
|
|
@@ -2582,7 +2592,7 @@ async function readJsonObject(file, projectDir) {
|
|
|
2582
2592
|
const shown = toPosix(path8.relative(projectDir, file));
|
|
2583
2593
|
let parsed;
|
|
2584
2594
|
try {
|
|
2585
|
-
parsed = JSON.parse(await
|
|
2595
|
+
parsed = JSON.parse(await import_fs5.promises.readFile(file, "utf-8"));
|
|
2586
2596
|
} catch (error) {
|
|
2587
2597
|
throw new PackError(
|
|
2588
2598
|
"INVALID_LOCALE_FILE",
|
|
@@ -2604,8 +2614,8 @@ async function collectShareData(options) {
|
|
|
2604
2614
|
const warnings = [];
|
|
2605
2615
|
let entries;
|
|
2606
2616
|
try {
|
|
2607
|
-
if (!(await
|
|
2608
|
-
entries = await
|
|
2617
|
+
if (!(await import_fs5.promises.stat(localesDir)).isDirectory()) throw new Error("not a directory");
|
|
2618
|
+
entries = await import_fs5.promises.readdir(localesDir, { withFileTypes: true });
|
|
2609
2619
|
} catch {
|
|
2610
2620
|
throw new PackError(
|
|
2611
2621
|
"LOCALES_NOT_FOUND",
|
|
@@ -2643,7 +2653,7 @@ async function collectShareData(options) {
|
|
|
2643
2653
|
if (dirLocales.has(locale)) {
|
|
2644
2654
|
dictionary = {};
|
|
2645
2655
|
const folder = path8.join(localesDir, locale);
|
|
2646
|
-
const files = (await
|
|
2656
|
+
const files = (await import_fs5.promises.readdir(folder, { withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".json")).map((file) => file.name).sort();
|
|
2647
2657
|
for (const file of files) {
|
|
2648
2658
|
const namespace = file.slice(0, -".json".length);
|
|
2649
2659
|
defineOwn(dictionary, namespace, await readJsonObject(path8.join(folder, file), projectDir));
|
|
@@ -2684,14 +2694,14 @@ function defaultStudioCandidates() {
|
|
|
2684
2694
|
}
|
|
2685
2695
|
function isDirectory(target) {
|
|
2686
2696
|
try {
|
|
2687
|
-
return (0,
|
|
2697
|
+
return (0, import_fs5.statSync)(target).isDirectory();
|
|
2688
2698
|
} catch {
|
|
2689
2699
|
return false;
|
|
2690
2700
|
}
|
|
2691
2701
|
}
|
|
2692
2702
|
function resolveStudioDir(candidates = defaultStudioCandidates()) {
|
|
2693
2703
|
for (const dir of candidates) {
|
|
2694
|
-
if ((0,
|
|
2704
|
+
if ((0, import_fs5.existsSync)(path8.join(dir, "index.html")) && isDirectory(path8.join(dir, "assets"))) {
|
|
2695
2705
|
return dir;
|
|
2696
2706
|
}
|
|
2697
2707
|
}
|
|
@@ -2699,13 +2709,13 @@ function resolveStudioDir(candidates = defaultStudioCandidates()) {
|
|
|
2699
2709
|
}
|
|
2700
2710
|
async function ownsShareDir(shareDir) {
|
|
2701
2711
|
try {
|
|
2702
|
-
await
|
|
2712
|
+
await import_fs5.promises.lstat(shareDir);
|
|
2703
2713
|
} catch {
|
|
2704
2714
|
return true;
|
|
2705
2715
|
}
|
|
2706
2716
|
try {
|
|
2707
2717
|
const manifest = JSON.parse(
|
|
2708
|
-
await
|
|
2718
|
+
await import_fs5.promises.readFile(path8.join(shareDir, SHARE_MANIFEST_FILE), "utf-8")
|
|
2709
2719
|
);
|
|
2710
2720
|
return isPlainObject2(manifest) && typeof manifest["format"] === "number";
|
|
2711
2721
|
} catch {
|
|
@@ -2715,7 +2725,7 @@ async function ownsShareDir(shareDir) {
|
|
|
2715
2725
|
async function findHtmlFiles(outDir, shareDir) {
|
|
2716
2726
|
const found = [];
|
|
2717
2727
|
const walk = async (dir) => {
|
|
2718
|
-
for (const entry of await
|
|
2728
|
+
for (const entry of await import_fs5.promises.readdir(dir, { withFileTypes: true })) {
|
|
2719
2729
|
if (entry.isSymbolicLink()) continue;
|
|
2720
2730
|
const full = path8.join(dir, entry.name);
|
|
2721
2731
|
if (entry.isDirectory()) {
|
|
@@ -2762,30 +2772,30 @@ async function packOutput(options) {
|
|
|
2762
2772
|
`${toPosix(path8.relative(projectDir, outDir))}/${SHARE_DIR}/ already exists and was not written by LiveLocale. Move it away and run again.`
|
|
2763
2773
|
);
|
|
2764
2774
|
}
|
|
2765
|
-
await
|
|
2766
|
-
await
|
|
2767
|
-
await
|
|
2775
|
+
await import_fs5.promises.rm(shareDir, { recursive: true, force: true });
|
|
2776
|
+
await import_fs5.promises.cp(studioDir, shareDir, { recursive: true });
|
|
2777
|
+
await import_fs5.promises.writeFile(
|
|
2768
2778
|
path8.join(shareDir, SHARE_MANIFEST_FILE),
|
|
2769
2779
|
JSON.stringify(data.manifest, null, 2) + "\n",
|
|
2770
2780
|
"utf-8"
|
|
2771
2781
|
);
|
|
2772
2782
|
const localesDir = path8.join(shareDir, SHARE_LOCALES_DIR);
|
|
2773
|
-
await
|
|
2783
|
+
await import_fs5.promises.mkdir(localesDir, { recursive: true });
|
|
2774
2784
|
for (const locale of data.manifest.locales) {
|
|
2775
|
-
await
|
|
2785
|
+
await import_fs5.promises.writeFile(
|
|
2776
2786
|
path8.join(localesDir, `${locale}.json`),
|
|
2777
2787
|
JSON.stringify(data.dictionaries[locale]),
|
|
2778
2788
|
"utf-8"
|
|
2779
2789
|
);
|
|
2780
2790
|
}
|
|
2781
|
-
await
|
|
2782
|
-
const inject = injectHtml === "auto" ? target.kind !== "next" && !(0,
|
|
2791
|
+
await import_fs5.promises.writeFile(path8.join(shareDir, SHARE_FLAG_FILE), FLAG_SCRIPT, "utf-8");
|
|
2792
|
+
const inject = injectHtml === "auto" ? target.kind !== "next" && !(0, import_fs5.existsSync)(path8.join(outDir, "_next")) : injectHtml;
|
|
2783
2793
|
const htmlInjected = inject ? await findHtmlFiles(outDir, shareDir) : [];
|
|
2784
2794
|
for (const rel of htmlInjected) {
|
|
2785
2795
|
const file = path8.join(outDir, ...rel.split("/"));
|
|
2786
|
-
const html = await
|
|
2796
|
+
const html = await import_fs5.promises.readFile(file, "utf-8");
|
|
2787
2797
|
const injected = injectFlagScript(html, flagSrcFor(base, rel));
|
|
2788
|
-
if (injected !== html) await
|
|
2798
|
+
if (injected !== html) await import_fs5.promises.writeFile(file, injected, "utf-8");
|
|
2789
2799
|
}
|
|
2790
2800
|
const warnings = [...data.warnings];
|
|
2791
2801
|
if (base.kind === "relative") warnings.push(RELATIVE_BASE_WARNING);
|
|
@@ -2863,7 +2873,7 @@ function quoteWindowsArg(arg) {
|
|
|
2863
2873
|
}
|
|
2864
2874
|
return out + "\\".repeat(backslashes * 2) + '"';
|
|
2865
2875
|
}
|
|
2866
|
-
var spawnInherit = (command, args, { cwd, env }) => new Promise((
|
|
2876
|
+
var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve16, reject2) => {
|
|
2867
2877
|
const child = process.platform === "win32" ? (0, import_child_process2.spawn)([command, ...args].map(quoteWindowsArg).join(" "), {
|
|
2868
2878
|
cwd,
|
|
2869
2879
|
env,
|
|
@@ -2871,10 +2881,10 @@ var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve14, reje
|
|
|
2871
2881
|
shell: true
|
|
2872
2882
|
}) : (0, import_child_process2.spawn)(command, args, { cwd, env, stdio: "inherit" });
|
|
2873
2883
|
child.on("error", (error) => {
|
|
2874
|
-
if (error.code === "ENOENT")
|
|
2884
|
+
if (error.code === "ENOENT") resolve16(127);
|
|
2875
2885
|
else reject2(error);
|
|
2876
2886
|
});
|
|
2877
|
-
child.on("close", (code) =>
|
|
2887
|
+
child.on("close", (code) => resolve16(code ?? 1));
|
|
2878
2888
|
});
|
|
2879
2889
|
var BUILD_USAGE = "Usage: livelocale build [--preview] -- <your build command>";
|
|
2880
2890
|
var NEXT_PREVIEW_WARNING = "Next.js pages have no watermarks without --preview; the Studio will open but nothing is editable.";
|
|
@@ -2883,7 +2893,7 @@ var FRESH_SLACK_MS = 2e3;
|
|
|
2883
2893
|
function gitignoresShareDir(projectDir) {
|
|
2884
2894
|
let content;
|
|
2885
2895
|
try {
|
|
2886
|
-
content = (0,
|
|
2896
|
+
content = (0, import_fs5.readFileSync)(path8.join(projectDir, ".gitignore"), "utf-8");
|
|
2887
2897
|
} catch {
|
|
2888
2898
|
return false;
|
|
2889
2899
|
}
|
|
@@ -2892,7 +2902,7 @@ function gitignoresShareDir(projectDir) {
|
|
|
2892
2902
|
function freshOutput(projectDir, candidates, startMs) {
|
|
2893
2903
|
for (const candidate of candidates) {
|
|
2894
2904
|
try {
|
|
2895
|
-
const stat = (0,
|
|
2905
|
+
const stat = (0, import_fs5.statSync)(path8.join(projectDir, candidate, "index.html"));
|
|
2896
2906
|
if (stat.isFile() && stat.mtimeMs >= startMs - FRESH_SLACK_MS) return candidate;
|
|
2897
2907
|
} catch {
|
|
2898
2908
|
}
|
|
@@ -2922,7 +2932,7 @@ async function runBuildCommand(options) {
|
|
|
2922
2932
|
`Next.js static export (output: 'export') cannot build ${blocking}: it exports POST, and export mode only allows GET route handlers. The share link does not need this route. Delete ${blocking} (or move it out of this build) and run again.`
|
|
2923
2933
|
);
|
|
2924
2934
|
}
|
|
2925
|
-
await
|
|
2935
|
+
await import_fs5.promises.mkdir(path8.join(projectDir, "public"), { recursive: true });
|
|
2926
2936
|
const pack2 = await packOutput({
|
|
2927
2937
|
...packOptions,
|
|
2928
2938
|
projectDir,
|
|
@@ -2951,122 +2961,12 @@ async function runBuildCommand(options) {
|
|
|
2951
2961
|
}
|
|
2952
2962
|
|
|
2953
2963
|
// src/codemod/apply/cli.ts
|
|
2954
|
-
var
|
|
2964
|
+
var path16 = __toESM(require("path"), 1);
|
|
2955
2965
|
|
|
2956
|
-
// src/codemod/apply/
|
|
2957
|
-
var import_child_process3 = require("child_process");
|
|
2966
|
+
// src/codemod/apply/github-event.ts
|
|
2958
2967
|
var fs6 = __toESM(require("fs"), 1);
|
|
2959
|
-
var path9 = __toESM(require("path"), 1);
|
|
2960
|
-
var MAX_APPLY_INPUT_BYTES = 1048576;
|
|
2961
|
-
var TOO_LARGE_MESSAGE2 = "The input is larger than 1 MB.";
|
|
2962
|
-
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.)';
|
|
2963
|
-
var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
|
|
2964
|
-
var ApplyInputError = class extends Error {
|
|
2965
|
-
constructor(message) {
|
|
2966
|
-
super(message);
|
|
2967
|
-
this.name = "ApplyInputError";
|
|
2968
|
-
}
|
|
2969
|
-
};
|
|
2970
|
-
function checkSize(text) {
|
|
2971
|
-
if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
|
|
2972
|
-
throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
2973
|
-
}
|
|
2974
|
-
return text;
|
|
2975
|
-
}
|
|
2976
|
-
async function readStream(stream) {
|
|
2977
|
-
const chunks = [];
|
|
2978
|
-
let size = 0;
|
|
2979
|
-
for await (const chunk of stream) {
|
|
2980
|
-
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
|
|
2981
|
-
size += bytes.length;
|
|
2982
|
-
if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
2983
|
-
chunks.push(bytes);
|
|
2984
|
-
}
|
|
2985
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
2986
|
-
}
|
|
2987
|
-
function existingFile(arg, cwd) {
|
|
2988
|
-
try {
|
|
2989
|
-
const resolved = path9.resolve(cwd, arg);
|
|
2990
|
-
return fs6.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
|
|
2991
|
-
} catch {
|
|
2992
|
-
return null;
|
|
2993
|
-
}
|
|
2994
|
-
}
|
|
2995
|
-
async function readApplyInput(arg, options) {
|
|
2996
|
-
if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
|
|
2997
|
-
if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
|
|
2998
|
-
if (arg === "-") return readStream(options.stdin ?? process.stdin);
|
|
2999
|
-
const file = existingFile(arg, options.cwd);
|
|
3000
|
-
if (file === null) return checkSize(arg);
|
|
3001
|
-
if (fs6.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
3002
|
-
return checkSize(fs6.readFileSync(file, "utf8"));
|
|
3003
|
-
}
|
|
3004
|
-
function clipboardCommands(platform) {
|
|
3005
|
-
if (platform === "win32") {
|
|
3006
|
-
return [
|
|
3007
|
-
["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
|
|
3008
|
-
];
|
|
3009
|
-
}
|
|
3010
|
-
if (platform === "darwin") return [["pbpaste", []]];
|
|
3011
|
-
return [
|
|
3012
|
-
["wl-paste", ["--no-newline"]],
|
|
3013
|
-
["xclip", ["-selection", "clipboard", "-o"]],
|
|
3014
|
-
["xsel", ["--clipboard", "--output"]]
|
|
3015
|
-
];
|
|
3016
|
-
}
|
|
3017
|
-
var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
|
|
3018
|
-
function run(execFileImpl, [command, args]) {
|
|
3019
|
-
return new Promise((resolve14) => {
|
|
3020
|
-
try {
|
|
3021
|
-
execFileImpl(
|
|
3022
|
-
command,
|
|
3023
|
-
args,
|
|
3024
|
-
{ encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
|
|
3025
|
-
(error, stdout) => {
|
|
3026
|
-
const code = error?.code;
|
|
3027
|
-
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve14(TOO_LARGE);
|
|
3028
|
-
else resolve14(error ? null : String(stdout));
|
|
3029
|
-
}
|
|
3030
|
-
);
|
|
3031
|
-
} catch {
|
|
3032
|
-
resolve14(null);
|
|
3033
|
-
}
|
|
3034
|
-
});
|
|
3035
|
-
}
|
|
3036
|
-
async function readClipboard(execFileImpl = import_child_process3.execFile, platform = process.platform) {
|
|
3037
|
-
for (const command of clipboardCommands(platform)) {
|
|
3038
|
-
const text = await run(execFileImpl, command);
|
|
3039
|
-
if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
|
|
3040
|
-
if (text !== null) return checkSize(text);
|
|
3041
|
-
}
|
|
3042
|
-
throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
// src/codemod/apply/local.ts
|
|
3046
|
-
var import_child_process5 = require("child_process");
|
|
3047
|
-
var fs9 = __toESM(require("fs"), 1);
|
|
3048
2968
|
var os = __toESM(require("os"), 1);
|
|
3049
|
-
var
|
|
3050
|
-
|
|
3051
|
-
// src/core/change-link-node.ts
|
|
3052
|
-
var import_zlib = require("zlib");
|
|
3053
|
-
var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
|
|
3054
|
-
var TOO_LARGE_MESSAGE3 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
|
|
3055
|
-
var nodeInflate = async (bytes, maxOutputLength) => {
|
|
3056
|
-
let result;
|
|
3057
|
-
try {
|
|
3058
|
-
result = (0, import_zlib.inflateSync)(bytes, { maxOutputLength, info: true });
|
|
3059
|
-
} catch (err) {
|
|
3060
|
-
if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
|
|
3061
|
-
throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE3);
|
|
3062
|
-
}
|
|
3063
|
-
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3064
|
-
}
|
|
3065
|
-
if (result.engine.bytesWritten !== bytes.length) {
|
|
3066
|
-
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3067
|
-
}
|
|
3068
|
-
return result.buffer;
|
|
3069
|
-
};
|
|
2969
|
+
var path10 = __toESM(require("path"), 1);
|
|
3070
2970
|
|
|
3071
2971
|
// src/client/watermark.ts
|
|
3072
2972
|
var WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
|
|
@@ -3134,6 +3034,11 @@ function detectJsonIndent(content) {
|
|
|
3134
3034
|
const ws = match[1];
|
|
3135
3035
|
return ws.includes(" ") ? " " : ws;
|
|
3136
3036
|
}
|
|
3037
|
+
function stringifyLike(data, content) {
|
|
3038
|
+
const indent = content === null ? " " : detectJsonIndent(content);
|
|
3039
|
+
const trailing = content === null || content.endsWith("\n") ? "\n" : "";
|
|
3040
|
+
return JSON.stringify(data, null, indent) + trailing;
|
|
3041
|
+
}
|
|
3137
3042
|
|
|
3138
3043
|
// src/core/save-rules.ts
|
|
3139
3044
|
var reject = (status, code, error) => ({
|
|
@@ -3339,109 +3244,1060 @@ function planApply(input) {
|
|
|
3339
3244
|
return { edits, counts };
|
|
3340
3245
|
}
|
|
3341
3246
|
|
|
3342
|
-
// src/
|
|
3343
|
-
var
|
|
3344
|
-
var
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3247
|
+
// src/core/change-link-node.ts
|
|
3248
|
+
var import_zlib = require("zlib");
|
|
3249
|
+
var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
|
|
3250
|
+
var TOO_LARGE_MESSAGE2 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
|
|
3251
|
+
var nodeInflate = async (bytes, maxOutputLength) => {
|
|
3252
|
+
let result;
|
|
3253
|
+
try {
|
|
3254
|
+
result = (0, import_zlib.inflateSync)(bytes, { maxOutputLength, info: true });
|
|
3255
|
+
} catch (err) {
|
|
3256
|
+
if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
|
|
3257
|
+
throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE2);
|
|
3258
|
+
}
|
|
3259
|
+
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3351
3260
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
if (!filePath.startsWith(resolvedBasePath)) {
|
|
3355
|
-
throw new Error("Path traversal attempt");
|
|
3261
|
+
if (result.engine.bytesWritten !== bytes.length) {
|
|
3262
|
+
throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
|
|
3356
3263
|
}
|
|
3357
|
-
return
|
|
3358
|
-
}
|
|
3264
|
+
return result.buffer;
|
|
3265
|
+
};
|
|
3359
3266
|
|
|
3360
|
-
// src/server/
|
|
3361
|
-
var
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
this.
|
|
3366
|
-
() => {
|
|
3367
|
-
},
|
|
3368
|
-
() => {
|
|
3369
|
-
}
|
|
3370
|
-
);
|
|
3371
|
-
return next;
|
|
3267
|
+
// src/server/github-rest.ts
|
|
3268
|
+
var GitHubApiError = class extends Error {
|
|
3269
|
+
constructor(message, status) {
|
|
3270
|
+
super(message);
|
|
3271
|
+
this.status = status;
|
|
3272
|
+
this.name = "GitHubApiError";
|
|
3372
3273
|
}
|
|
3274
|
+
status;
|
|
3373
3275
|
};
|
|
3374
|
-
var
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
this.requireEnabled = options.requireEnabled ?? true;
|
|
3381
|
-
this.quiet = options.quiet ?? false;
|
|
3382
|
-
this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
|
|
3276
|
+
var CHUNK_BYTES = 32768;
|
|
3277
|
+
function encodeBase64Utf8(text) {
|
|
3278
|
+
const bytes = new TextEncoder().encode(text);
|
|
3279
|
+
let binary = "";
|
|
3280
|
+
for (let i = 0; i < bytes.length; i += CHUNK_BYTES) {
|
|
3281
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_BYTES));
|
|
3383
3282
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3283
|
+
return btoa(binary);
|
|
3284
|
+
}
|
|
3285
|
+
function decodeBase64Utf8(base64) {
|
|
3286
|
+
const binary = atob(base64.replace(/\s/g, ""));
|
|
3287
|
+
const bytes = new Uint8Array(binary.length);
|
|
3288
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
3289
|
+
return new TextDecoder().decode(bytes);
|
|
3290
|
+
}
|
|
3291
|
+
function isPullRequestCreationForbidden(err) {
|
|
3292
|
+
return err instanceof GitHubApiError && err.status === 403 && /not permitted to create or approve pull requests/i.test(err.message);
|
|
3293
|
+
}
|
|
3294
|
+
function encodeRefPath(ref) {
|
|
3295
|
+
return ref.split("/").map(encodeURIComponent).join("/");
|
|
3296
|
+
}
|
|
3297
|
+
function createGitHubRest(config) {
|
|
3298
|
+
const repo = config.repo;
|
|
3299
|
+
const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
|
|
3300
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
3301
|
+
const owner = repo.split("/")[0] ?? "";
|
|
3302
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
3303
|
+
throw new Error(`[livelocale] github.repo must be "owner/name", got "${repo}".`);
|
|
3304
|
+
}
|
|
3305
|
+
if (typeof fetchImpl !== "function") {
|
|
3306
|
+
throw new Error("[livelocale] The GitHub gateway needs a global fetch (Node 18+).");
|
|
3307
|
+
}
|
|
3308
|
+
const headers = (hasBody) => ({
|
|
3309
|
+
Authorization: `Bearer ${config.token}`,
|
|
3310
|
+
Accept: "application/vnd.github+json",
|
|
3311
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
3312
|
+
...hasBody ? { "Content-Type": "application/json" } : {}
|
|
3313
|
+
});
|
|
3314
|
+
async function api(method, path19, options = {}) {
|
|
3315
|
+
const url = new URL(`${apiBase}/repos/${repo}${path19}`);
|
|
3316
|
+
for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
|
|
3317
|
+
const response = await fetchImpl(url.toString(), {
|
|
3318
|
+
method,
|
|
3319
|
+
headers: headers(options.body !== void 0),
|
|
3320
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
3321
|
+
});
|
|
3322
|
+
let data = null;
|
|
3323
|
+
try {
|
|
3324
|
+
data = await response.json();
|
|
3325
|
+
} catch {
|
|
3326
|
+
}
|
|
3327
|
+
if (response.status === 404 && options.allow404) return { status: 404, data: null };
|
|
3328
|
+
if (!response.ok) {
|
|
3329
|
+
const detail = data?.message || "request failed";
|
|
3330
|
+
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.' : "";
|
|
3331
|
+
throw new GitHubApiError(
|
|
3332
|
+
`GitHub API ${response.status} on ${method} ${path19}: ${detail}.${hint}`,
|
|
3333
|
+
response.status
|
|
3390
3334
|
);
|
|
3391
3335
|
}
|
|
3336
|
+
return { status: response.status, data };
|
|
3392
3337
|
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3338
|
+
async function probeStatus(method, path19, body) {
|
|
3339
|
+
const response = await fetchImpl(`${apiBase}/repos/${repo}${path19}`, {
|
|
3340
|
+
method,
|
|
3341
|
+
headers: headers(body !== void 0),
|
|
3342
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
3343
|
+
});
|
|
3344
|
+
try {
|
|
3345
|
+
await response.arrayBuffer();
|
|
3346
|
+
} catch {
|
|
3401
3347
|
}
|
|
3402
|
-
return
|
|
3348
|
+
return response.status;
|
|
3403
3349
|
}
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3350
|
+
async function refSha(branch) {
|
|
3351
|
+
const res = await api(
|
|
3352
|
+
"GET",
|
|
3353
|
+
`/git/ref/heads/${encodeRefPath(branch)}`,
|
|
3354
|
+
{ allow404: true }
|
|
3355
|
+
);
|
|
3356
|
+
return res.data?.object?.sha ?? null;
|
|
3357
|
+
}
|
|
3358
|
+
async function findOpenPullRequest(head, base) {
|
|
3359
|
+
const res = await api("GET", "/pulls", {
|
|
3360
|
+
query: { head: `${owner}:${head}`, base, state: "open" }
|
|
3361
|
+
});
|
|
3362
|
+
const pull = res.data?.[0];
|
|
3363
|
+
return pull ? { number: pull.number, url: pull.html_url } : null;
|
|
3364
|
+
}
|
|
3365
|
+
async function openOrReusePullRequest(input) {
|
|
3366
|
+
const existing = await findOpenPullRequest(input.head, input.base);
|
|
3367
|
+
if (existing) {
|
|
3368
|
+
if (input.updateBodyIfReused) {
|
|
3369
|
+
await api("PATCH", `/pulls/${existing.number}`, { body: { body: input.body } });
|
|
3370
|
+
}
|
|
3371
|
+
return { ...existing, reused: true };
|
|
3372
|
+
}
|
|
3373
|
+
const res = await api("POST", "/pulls", {
|
|
3374
|
+
body: { title: input.title, head: input.head, base: input.base, body: input.body }
|
|
3375
|
+
});
|
|
3376
|
+
return { number: res.data.number, url: res.data.html_url, reused: false };
|
|
3377
|
+
}
|
|
3378
|
+
return { repo, api, probeStatus, refSha, findOpenPullRequest, openOrReusePullRequest };
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// src/codemod/apply/github-commit.ts
|
|
3382
|
+
var path9 = __toESM(require("path"), 1);
|
|
3383
|
+
var BRANCH_PREFIX = "livelocale/";
|
|
3384
|
+
function normalizeRepoDir(p) {
|
|
3385
|
+
const posix2 = p.replace(/\\/g, "/");
|
|
3386
|
+
if (posix2.startsWith("/") || /^[A-Za-z]:/.test(posix2)) {
|
|
3387
|
+
throw new Error(`[livelocale] The locales path must be relative to the repository: "${p}".`);
|
|
3388
|
+
}
|
|
3389
|
+
const segments = posix2.split("/").filter((s) => s !== "" && s !== ".");
|
|
3390
|
+
if (segments.includes("..")) {
|
|
3391
|
+
throw new Error(`[livelocale] The locales path must stay inside the repository: "${p}".`);
|
|
3392
|
+
}
|
|
3393
|
+
return segments.join("/");
|
|
3394
|
+
}
|
|
3395
|
+
function joinRepoPath(dir, rest) {
|
|
3396
|
+
return dir === "" ? rest : `${dir}/${rest}`;
|
|
3397
|
+
}
|
|
3398
|
+
function localeFileFor(localesDir, layout, locale, key) {
|
|
3399
|
+
const dir = normalizeRepoDir(localesDir);
|
|
3400
|
+
if (layout === "flat") return { file: joinRepoPath(dir, `${locale}.json`), innerKey: key };
|
|
3401
|
+
const dot = key.indexOf(".");
|
|
3402
|
+
if (dot <= 0 || dot === key.length - 1) {
|
|
3403
|
+
throw new Error(`[livelocale] "${key}" names no key inside a namespace file.`);
|
|
3404
|
+
}
|
|
3405
|
+
return {
|
|
3406
|
+
file: joinRepoPath(dir, `${locale}/${key.slice(0, dot)}.json`),
|
|
3407
|
+
innerKey: key.slice(dot + 1)
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
function isPlainObject4(value) {
|
|
3411
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3412
|
+
}
|
|
3413
|
+
function applyEditsToText(text, edits) {
|
|
3414
|
+
const data = JSON.parse(text);
|
|
3415
|
+
if (!isPlainObject4(data)) throw new Error("[livelocale] A locale file is not a JSON object.");
|
|
3416
|
+
for (const edit of edits) setNestedProperty(data, edit.innerKey, edit.value);
|
|
3417
|
+
return stringifyLike(data, text);
|
|
3418
|
+
}
|
|
3419
|
+
async function listDir(rest, dir, ref) {
|
|
3420
|
+
const res = await rest.api("GET", `/contents/${encodeRefPath(dir)}`, {
|
|
3421
|
+
query: { ref },
|
|
3422
|
+
allow404: true
|
|
3423
|
+
});
|
|
3424
|
+
return Array.isArray(res.data) ? res.data : null;
|
|
3425
|
+
}
|
|
3426
|
+
async function readBlob(rest, sha) {
|
|
3427
|
+
const res = await rest.api(
|
|
3428
|
+
"GET",
|
|
3429
|
+
`/git/blobs/${encodeURIComponent(sha)}`
|
|
3430
|
+
);
|
|
3431
|
+
return decodeBase64Utf8(res.data?.content ?? "");
|
|
3432
|
+
}
|
|
3433
|
+
function parseLocaleFile(text, file) {
|
|
3434
|
+
let data;
|
|
3435
|
+
try {
|
|
3436
|
+
data = JSON.parse(text);
|
|
3437
|
+
} catch {
|
|
3438
|
+
throw new Error(`[livelocale] ${file} is not valid JSON.`);
|
|
3439
|
+
}
|
|
3440
|
+
if (!isPlainObject4(data)) throw new Error(`[livelocale] ${file} is not a JSON object.`);
|
|
3441
|
+
return data;
|
|
3442
|
+
}
|
|
3443
|
+
async function readRemoteLocales(rest, commitSha, localesDir, locales) {
|
|
3444
|
+
const dir = normalizeRepoDir(localesDir);
|
|
3445
|
+
const entries = await listDir(rest, dir, commitSha);
|
|
3446
|
+
if (entries === null) {
|
|
3447
|
+
throw new Error(`[livelocale] No locales folder at ${dir || "."} on the target.`);
|
|
3448
|
+
}
|
|
3449
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3450
|
+
const files = /* @__PURE__ */ new Map();
|
|
3451
|
+
for (const locale of locales) {
|
|
3452
|
+
if (targets.has(locale)) continue;
|
|
3453
|
+
const flat = entries.find((e) => e.type === "file" && e.name === `${locale}.json`);
|
|
3454
|
+
const folder = entries.find((e) => e.type === "dir" && e.name === locale);
|
|
3455
|
+
if (!isValidLocaleFormat(locale) || !flat && !folder) {
|
|
3456
|
+
targets.set(locale, null);
|
|
3457
|
+
continue;
|
|
3458
|
+
}
|
|
3459
|
+
if (flat) {
|
|
3460
|
+
const file = joinRepoPath(dir, flat.name);
|
|
3461
|
+
const text = await readBlob(rest, flat.sha);
|
|
3462
|
+
files.set(file, text);
|
|
3463
|
+
targets.set(locale, { layout: "flat", dict: parseLocaleFile(text, file) });
|
|
3464
|
+
continue;
|
|
3465
|
+
}
|
|
3466
|
+
const dict = {};
|
|
3467
|
+
for (const entry of await listDir(rest, joinRepoPath(dir, locale), commitSha) ?? []) {
|
|
3468
|
+
if (entry.type !== "file" || !entry.name.endsWith(".json")) continue;
|
|
3469
|
+
const file = joinRepoPath(dir, `${locale}/${entry.name}`);
|
|
3470
|
+
const text = await readBlob(rest, entry.sha);
|
|
3471
|
+
files.set(file, text);
|
|
3472
|
+
dict[entry.name.slice(0, -".json".length)] = parseLocaleFile(text, file);
|
|
3473
|
+
}
|
|
3474
|
+
targets.set(locale, { layout: "namespaced", dict });
|
|
3475
|
+
}
|
|
3476
|
+
return { sha: commitSha, targets, files };
|
|
3477
|
+
}
|
|
3478
|
+
function buildFileChanges(localesDir, plan, remote, baseFiles) {
|
|
3479
|
+
const dir = normalizeRepoDir(localesDir);
|
|
3480
|
+
const prefix = dir === "" ? "" : `${dir}/`;
|
|
3481
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
3482
|
+
for (const edit of plan.edits) {
|
|
3483
|
+
if (edit.status !== "apply") continue;
|
|
3484
|
+
const layout = remote.targets.get(edit.locale)?.layout ?? "flat";
|
|
3485
|
+
const { file, innerKey } = localeFileFor(dir, layout, edit.locale, edit.key);
|
|
3486
|
+
const normalized = path9.posix.normalize(file);
|
|
3487
|
+
if (normalized !== file || normalized.startsWith("../") || normalized.startsWith("/") || !normalized.startsWith(prefix)) {
|
|
3412
3488
|
throw new Error(
|
|
3413
|
-
`[livelocale]
|
|
3489
|
+
`[livelocale] Refusing to write ${file}: it is outside the locales folder ${dir || "."}.`
|
|
3414
3490
|
);
|
|
3415
3491
|
}
|
|
3416
|
-
const
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
}
|
|
3426
|
-
} catch {
|
|
3492
|
+
const list = byFile.get(file) ?? [];
|
|
3493
|
+
list.push({ innerKey, value: edit.to });
|
|
3494
|
+
byFile.set(file, list);
|
|
3495
|
+
}
|
|
3496
|
+
const changes = /* @__PURE__ */ new Map();
|
|
3497
|
+
for (const [file, edits] of byFile) {
|
|
3498
|
+
const text = baseFiles?.get(file) ?? remote.files.get(file);
|
|
3499
|
+
if (text === void 0) {
|
|
3500
|
+
throw new Error(`[livelocale] ${file} was not read from the target.`);
|
|
3427
3501
|
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3502
|
+
const next = applyEditsToText(text, edits);
|
|
3503
|
+
if (next !== text) changes.set(file, next);
|
|
3504
|
+
}
|
|
3505
|
+
return changes;
|
|
3506
|
+
}
|
|
3507
|
+
async function prepareBranch(rest, baseName, base, targetSha, runId) {
|
|
3508
|
+
const head = await rest.refSha(baseName);
|
|
3509
|
+
if (head === null) return { branch: baseName, parentSha: targetSha, mode: "new" };
|
|
3510
|
+
if (await rest.findOpenPullRequest(baseName, base)) {
|
|
3511
|
+
return { branch: baseName, parentSha: head, mode: "append" };
|
|
3512
|
+
}
|
|
3513
|
+
return { branch: `${baseName}-${runId}`, parentSha: targetSha, mode: "new" };
|
|
3514
|
+
}
|
|
3515
|
+
async function commitFiles(rest, input) {
|
|
3516
|
+
const { branch, parentSha, mode } = input.branchPlan;
|
|
3517
|
+
if (!branch.startsWith(BRANCH_PREFIX)) {
|
|
3518
|
+
throw new Error(`[livelocale] Refusing to write "${branch}": only ${BRANCH_PREFIX}* branches.`);
|
|
3519
|
+
}
|
|
3520
|
+
if (input.files.size === 0) return { sha: null, unchanged: true };
|
|
3521
|
+
const parent = await rest.api(
|
|
3522
|
+
"GET",
|
|
3523
|
+
`/git/commits/${encodeURIComponent(parentSha)}`
|
|
3524
|
+
);
|
|
3525
|
+
const baseTree = parent.data.tree.sha;
|
|
3526
|
+
const entries = [];
|
|
3527
|
+
for (const [file, text] of input.files) {
|
|
3528
|
+
const blob = await rest.api("POST", "/git/blobs", {
|
|
3529
|
+
body: { content: encodeBase64Utf8(text), encoding: "base64" }
|
|
3530
|
+
});
|
|
3531
|
+
entries.push({ path: file, mode: "100644", type: "blob", sha: blob.data.sha });
|
|
3532
|
+
}
|
|
3533
|
+
const tree = await rest.api("POST", "/git/trees", {
|
|
3534
|
+
body: { base_tree: baseTree, tree: entries }
|
|
3535
|
+
});
|
|
3536
|
+
const treeSha = tree.data.sha;
|
|
3537
|
+
if (treeSha === baseTree) return { sha: null, unchanged: true };
|
|
3538
|
+
const commit = await rest.api("POST", "/git/commits", {
|
|
3539
|
+
body: { message: input.message, tree: treeSha, parents: [parentSha], author: input.author }
|
|
3540
|
+
});
|
|
3541
|
+
const sha = commit.data.sha;
|
|
3542
|
+
if (mode === "new") {
|
|
3543
|
+
await rest.api("POST", "/git/refs", { body: { ref: `refs/heads/${branch}`, sha } });
|
|
3544
|
+
} else {
|
|
3545
|
+
await rest.api("PATCH", `/git/refs/heads/${encodeRefPath(branch)}`, {
|
|
3546
|
+
body: { sha, force: false }
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
return { sha };
|
|
3550
|
+
}
|
|
3551
|
+
function noreplyAuthor(actor) {
|
|
3552
|
+
return { name: actor.login, email: `${actor.id}+${actor.login}@users.noreply.github.com` };
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
// src/codemod/apply/github-trigger.ts
|
|
3556
|
+
var APPLY_COMMAND_RE = /^\/livelocale\s+apply(\s+--force)?\s*$/;
|
|
3557
|
+
var COMMAND_PREFIX = "/livelocale";
|
|
3558
|
+
var ISSUE_BRANCH_RE = /^livelocale\/issue-(\d+)(?:-\d+)?$/;
|
|
3559
|
+
var TRUSTED_ASSOCIATIONS = /* @__PURE__ */ new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
|
3560
|
+
var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["admin", "write"]);
|
|
3561
|
+
var TRUSTED_ROLES = /* @__PURE__ */ new Set(["admin", "maintain", "write"]);
|
|
3562
|
+
var ignore = (reason) => ({ kind: "ignore", reason });
|
|
3563
|
+
function actorOf(user) {
|
|
3564
|
+
return { login: String(user?.login ?? ""), id: Number(user?.id ?? 0) };
|
|
3565
|
+
}
|
|
3566
|
+
function textOf(value) {
|
|
3567
|
+
return typeof value === "string" ? value : "";
|
|
3568
|
+
}
|
|
3569
|
+
function holdsChangeToken(text) {
|
|
3570
|
+
try {
|
|
3571
|
+
findChangeToken(text);
|
|
3572
|
+
return true;
|
|
3573
|
+
} catch {
|
|
3574
|
+
return false;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
function parseTrigger(eventName, event) {
|
|
3578
|
+
const repo = textOf(event?.repository?.full_name);
|
|
3579
|
+
const defaultBranch = textOf(event?.repository?.default_branch);
|
|
3580
|
+
switch (eventName) {
|
|
3581
|
+
case "issues": {
|
|
3582
|
+
if (event?.action !== "opened") return ignore("issue not opened");
|
|
3583
|
+
const issue = event.issue;
|
|
3584
|
+
const body = textOf(issue?.body);
|
|
3585
|
+
if (!body.includes(CHANGE_BLOCK_OPEN)) return ignore("no change block");
|
|
3586
|
+
return {
|
|
3587
|
+
kind: "apply",
|
|
3588
|
+
via: "issue",
|
|
3589
|
+
issueNumber: Number(issue.number),
|
|
3590
|
+
actor: actorOf(issue.user),
|
|
3591
|
+
association: issue.author_association ?? null,
|
|
3592
|
+
text: body,
|
|
3593
|
+
force: false,
|
|
3594
|
+
repo,
|
|
3595
|
+
defaultBranch
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
case "issue_comment": {
|
|
3599
|
+
if (event?.action !== "created") return ignore("comment not created");
|
|
3600
|
+
const issue = event.issue;
|
|
3601
|
+
const comment = event.comment;
|
|
3602
|
+
if (issue?.pull_request) return ignore("comment on a pull request");
|
|
3603
|
+
if (comment?.user?.type === "Bot") return ignore("comment by a bot");
|
|
3604
|
+
const body = textOf(comment?.body);
|
|
3605
|
+
const firstLine = body.split(/\r\n|\r|\n/)[0] ?? "";
|
|
3606
|
+
if (!firstLine.startsWith(COMMAND_PREFIX)) return ignore("not a livelocale command");
|
|
3607
|
+
const command = APPLY_COMMAND_RE.exec(firstLine);
|
|
3608
|
+
if (!command) return ignore("unknown command");
|
|
3609
|
+
return {
|
|
3610
|
+
kind: "apply",
|
|
3611
|
+
via: "comment",
|
|
3612
|
+
issueNumber: Number(issue.number),
|
|
3613
|
+
// The gate checks the commenter, never the issue author.
|
|
3614
|
+
actor: actorOf(comment.user),
|
|
3615
|
+
association: comment.author_association ?? null,
|
|
3616
|
+
text: holdsChangeToken(body) ? body : textOf(issue?.body),
|
|
3617
|
+
force: command[1] !== void 0,
|
|
3618
|
+
repo,
|
|
3619
|
+
defaultBranch
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
case "workflow_dispatch":
|
|
3623
|
+
return {
|
|
3624
|
+
kind: "apply",
|
|
3625
|
+
via: "dispatch",
|
|
3626
|
+
issueNumber: null,
|
|
3627
|
+
actor: actorOf(event?.sender),
|
|
3628
|
+
association: null,
|
|
3629
|
+
text: textOf(event?.inputs?.link),
|
|
3630
|
+
force: false,
|
|
3631
|
+
repo,
|
|
3632
|
+
defaultBranch
|
|
3633
|
+
};
|
|
3634
|
+
case "pull_request": {
|
|
3635
|
+
if (event?.action !== "closed") return ignore("pull request not closed");
|
|
3636
|
+
const pull = event.pull_request;
|
|
3637
|
+
const branch = ISSUE_BRANCH_RE.exec(textOf(pull?.head?.ref));
|
|
3638
|
+
if (!branch) return ignore("not a livelocale issue branch");
|
|
3639
|
+
if (textOf(pull?.head?.repo?.full_name) !== repo || repo === "") {
|
|
3640
|
+
return ignore("pull request from another repository");
|
|
3641
|
+
}
|
|
3642
|
+
return {
|
|
3643
|
+
kind: "close-issue",
|
|
3644
|
+
issueNumber: Number(branch[1]),
|
|
3645
|
+
prUrl: textOf(pull.html_url),
|
|
3646
|
+
merged: pull.merged === true,
|
|
3647
|
+
repo
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
default:
|
|
3651
|
+
return ignore(`unsupported event ${eventName}`);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
async function isTrustedActor(rest, login, association) {
|
|
3655
|
+
if (association !== null && TRUSTED_ASSOCIATIONS.has(association)) return true;
|
|
3656
|
+
try {
|
|
3657
|
+
const res = await rest.api(
|
|
3658
|
+
"GET",
|
|
3659
|
+
`/collaborators/${encodeURIComponent(login)}/permission`
|
|
3660
|
+
);
|
|
3661
|
+
const permission = res.data?.permission ?? "";
|
|
3662
|
+
const role = res.data?.role_name ?? "";
|
|
3663
|
+
return TRUSTED_PERMISSIONS.has(permission) || TRUSTED_ROLES.has(role);
|
|
3664
|
+
} catch (err) {
|
|
3665
|
+
if (err instanceof GitHubApiError && (err.status === 403 || err.status === 404)) return false;
|
|
3666
|
+
throw err;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
function chooseTargetBranch(requested, allowList, defaultBranch) {
|
|
3670
|
+
if (!requested) return { branch: defaultBranch, reason: "default" };
|
|
3671
|
+
const allowed = allowList.length > 0 ? allowList : [defaultBranch];
|
|
3672
|
+
if (allowed.includes(requested)) return { branch: requested, reason: "link" };
|
|
3673
|
+
return { branch: defaultBranch, reason: "not-allowed" };
|
|
3674
|
+
}
|
|
3675
|
+
function branchBaseName(t, runId) {
|
|
3676
|
+
return t.issueNumber !== null ? `livelocale/issue-${t.issueNumber}` : `livelocale/run-${runId}`;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// src/codemod/apply/report.ts
|
|
3680
|
+
var DEFAULT_SPAN_LENGTH = 120;
|
|
3681
|
+
var DEFAULT_TABLE_ROWS = 100;
|
|
3682
|
+
var MAX_BODY_CHARS = 6e4;
|
|
3683
|
+
var MAX_NOTE_CHARS2 = 2e3;
|
|
3684
|
+
var MAX_NAME_CHARS = 64;
|
|
3685
|
+
var MAX_CHECK_LINES = 50;
|
|
3686
|
+
var MAX_CHECK_SUMMARY_CHARS = 1e4;
|
|
3687
|
+
var BODY_MARGIN = 200;
|
|
3688
|
+
var STATUS_LABELS = {
|
|
3689
|
+
apply: "applied",
|
|
3690
|
+
already: "already done",
|
|
3691
|
+
conflict: "conflict",
|
|
3692
|
+
"needs-force": "needs --force",
|
|
3693
|
+
invalid: "rejected"
|
|
3694
|
+
};
|
|
3695
|
+
var BIDI_CONTROL_RE = /[--]/g;
|
|
3696
|
+
var NEWLINE_RE = /\r\n|\r|\n/g;
|
|
3697
|
+
var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
3698
|
+
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
3699
|
+
function cut(text, max) {
|
|
3700
|
+
if (text.length <= max) return text;
|
|
3701
|
+
let out = text.slice(0, max);
|
|
3702
|
+
const last = out.charCodeAt(out.length - 1);
|
|
3703
|
+
if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
|
|
3704
|
+
return out;
|
|
3705
|
+
}
|
|
3706
|
+
function cutLines(text, max) {
|
|
3707
|
+
if (text.length <= max) return text;
|
|
3708
|
+
const kept = [];
|
|
3709
|
+
let length = 0;
|
|
3710
|
+
for (const line of text.split("\n")) {
|
|
3711
|
+
const added = (kept.length > 0 ? 1 : 0) + line.length;
|
|
3712
|
+
if (length + added + 2 > max) break;
|
|
3713
|
+
kept.push(line);
|
|
3714
|
+
length += added;
|
|
3715
|
+
}
|
|
3716
|
+
return [...kept, "\u2026"].join("\n");
|
|
3717
|
+
}
|
|
3718
|
+
function longestBacktickRun(text) {
|
|
3719
|
+
let longest = 0;
|
|
3720
|
+
for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
|
|
3721
|
+
return longest;
|
|
3722
|
+
}
|
|
3723
|
+
function codePointLabel(ch) {
|
|
3724
|
+
return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
|
|
3725
|
+
}
|
|
3726
|
+
function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
|
|
3727
|
+
const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
|
|
3728
|
+
const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
|
|
3729
|
+
if (content === "") return "` `";
|
|
3730
|
+
const fence = "`".repeat(longestBacktickRun(content) + 1);
|
|
3731
|
+
const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
|
|
3732
|
+
return `${fence}${pad}${content}${pad}${fence}`;
|
|
3733
|
+
}
|
|
3734
|
+
function sanitizeName(name) {
|
|
3735
|
+
if (name === void 0) return void 0;
|
|
3736
|
+
const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
|
|
3737
|
+
const result = cut(cleaned, MAX_NAME_CHARS).trim();
|
|
3738
|
+
return result === "" ? void 0 : result;
|
|
3739
|
+
}
|
|
3740
|
+
var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
|
|
3741
|
+
function summarizeCheck(report, edited) {
|
|
3742
|
+
const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
|
|
3743
|
+
const mine = report.issues.filter(
|
|
3744
|
+
(issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
|
|
3745
|
+
);
|
|
3746
|
+
if (mine.length === 0) return NO_CHECK_PROBLEMS;
|
|
3747
|
+
const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
|
|
3748
|
+
for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
|
|
3749
|
+
lines.push(
|
|
3750
|
+
`- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
3753
|
+
if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
|
|
3754
|
+
const others = report.issues.length - mine.length;
|
|
3755
|
+
if (others > 0) {
|
|
3756
|
+
lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
|
|
3757
|
+
}
|
|
3758
|
+
return lines.join("\n");
|
|
3759
|
+
}
|
|
3760
|
+
function checksCell(edit) {
|
|
3761
|
+
const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
|
|
3762
|
+
if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
|
|
3763
|
+
parts.push(codeSpan(edit.message));
|
|
3764
|
+
}
|
|
3765
|
+
return parts.join("; ");
|
|
3766
|
+
}
|
|
3767
|
+
function renderRow(edit) {
|
|
3768
|
+
const before = edit.from === null ? "\u2014" : codeSpan(edit.from);
|
|
3769
|
+
const after = edit.status === "conflict" ? `now: ${edit.current === void 0 ? "(removed)" : codeSpan(edit.current)}` : codeSpan(edit.to);
|
|
3770
|
+
const cells = [
|
|
3771
|
+
STATUS_LABELS[edit.status],
|
|
3772
|
+
codeSpan(edit.locale),
|
|
3773
|
+
codeSpan(edit.key),
|
|
3774
|
+
before,
|
|
3775
|
+
after,
|
|
3776
|
+
checksCell(edit)
|
|
3777
|
+
];
|
|
3778
|
+
return `| ${cells.join(" | ")} |`;
|
|
3779
|
+
}
|
|
3780
|
+
function renderTable(plan, limit, maxChars) {
|
|
3781
|
+
const lines = [
|
|
3782
|
+
"| Status | Locale | Key | Before | After | Checks |",
|
|
3783
|
+
"| --- | --- | --- | --- | --- | --- |"
|
|
3784
|
+
];
|
|
3785
|
+
const moreLine = (n) => `
|
|
3786
|
+
|
|
3787
|
+
\u2026and ${n} more`;
|
|
3788
|
+
let length = lines.join("\n").length + moreLine(plan.edits.length).length;
|
|
3789
|
+
let shown = 0;
|
|
3790
|
+
for (const edit of plan.edits) {
|
|
3791
|
+
if (shown >= limit) break;
|
|
3792
|
+
const row = renderRow(edit);
|
|
3793
|
+
if (length + row.length + 1 > maxChars) break;
|
|
3794
|
+
lines.push(row);
|
|
3795
|
+
length += row.length + 1;
|
|
3796
|
+
shown += 1;
|
|
3797
|
+
}
|
|
3798
|
+
const rest = plan.edits.length - shown;
|
|
3799
|
+
return lines.join("\n") + (rest > 0 ? moreLine(rest) : "");
|
|
3800
|
+
}
|
|
3801
|
+
function fencedNote(note) {
|
|
3802
|
+
const text = cut(note, MAX_NOTE_CHARS2);
|
|
3803
|
+
const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
|
|
3804
|
+
return `${fence}text
|
|
3805
|
+
${text}
|
|
3806
|
+
${fence}`;
|
|
3807
|
+
}
|
|
3808
|
+
function closingLine(ctx) {
|
|
3809
|
+
if (ctx.issueNumber === void 0) return void 0;
|
|
3810
|
+
if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
|
|
3811
|
+
return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
|
|
3812
|
+
}
|
|
3813
|
+
function targetNote(target) {
|
|
3814
|
+
if (target?.reason !== "not-allowed" || target.requested === void 0) return void 0;
|
|
3815
|
+
return `The link was made from ${codeSpan(target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(target.branch)}.`;
|
|
3816
|
+
}
|
|
3817
|
+
function creditLines(ctx, withTarget) {
|
|
3818
|
+
const lines = [];
|
|
3819
|
+
if (ctx.by?.trim()) lines.push(`Translated by ${codeSpan(ctx.by)}`);
|
|
3820
|
+
const target = withTarget ? targetNote(ctx.target) : void 0;
|
|
3821
|
+
if (target) lines.push(target);
|
|
3822
|
+
if (ctx.note?.trim()) lines.push(fencedNote(ctx.note));
|
|
3823
|
+
return lines;
|
|
3824
|
+
}
|
|
3825
|
+
function renderPullRequestBody(ctx) {
|
|
3826
|
+
const head = [
|
|
3827
|
+
"Applies translations sent with a LiveLocale change link.",
|
|
3828
|
+
...creditLines(ctx, true)
|
|
3829
|
+
];
|
|
3830
|
+
const tail = [];
|
|
3831
|
+
if (ctx.checkSummary) tail.push(cutLines(ctx.checkSummary, MAX_CHECK_SUMMARY_CHARS));
|
|
3832
|
+
if (ctx.runUrl) tail.push(`Run log: ${ctx.runUrl}`);
|
|
3833
|
+
const closing = closingLine(ctx);
|
|
3834
|
+
if (closing) tail.push(closing);
|
|
3835
|
+
const fixed = [...head, ...tail].join("\n\n").length;
|
|
3836
|
+
const table = renderTable(ctx.plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
|
|
3837
|
+
return [...head, table, ...tail].join("\n\n");
|
|
3838
|
+
}
|
|
3839
|
+
function renderCommitMessage(input) {
|
|
3840
|
+
let subject = "i18n: apply LiveLocale changes";
|
|
3841
|
+
if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
|
|
3842
|
+
else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
|
|
3843
|
+
const applied = input.plan.edits.filter((edit) => edit.status === "apply");
|
|
3844
|
+
const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
|
|
3845
|
+
const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
|
|
3846
|
+
const name = sanitizeName(input.by);
|
|
3847
|
+
if (name) parts.push(`Translated-by: ${name}`);
|
|
3848
|
+
return parts.join("\n\n");
|
|
3849
|
+
}
|
|
3850
|
+
function renderTerminalSummary(plan) {
|
|
3851
|
+
const c = plan.counts;
|
|
3852
|
+
const lines = [
|
|
3853
|
+
`Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
|
|
3854
|
+
];
|
|
3855
|
+
for (const edit of plan.edits) {
|
|
3856
|
+
lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
|
|
3857
|
+
}
|
|
3858
|
+
return lines.join("\n");
|
|
3859
|
+
}
|
|
3860
|
+
var MAX_ERROR_MESSAGE_CHARS = 2e3;
|
|
3861
|
+
function needsForceHint(plan) {
|
|
3862
|
+
const n = plan?.counts["needs-force"] ?? 0;
|
|
3863
|
+
if (n === 0) return void 0;
|
|
3864
|
+
return `${n} change(s) were saved with "Save anyway". A maintainer can apply them with \`/livelocale apply --force\`.`;
|
|
3865
|
+
}
|
|
3866
|
+
function withTable(head, plan, tail) {
|
|
3867
|
+
if (!plan) return [...head, ...tail].join("\n\n");
|
|
3868
|
+
const fixed = [...head, ...tail].join("\n\n").length;
|
|
3869
|
+
const table = renderTable(plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
|
|
3870
|
+
return [...head, table, ...tail].join("\n\n");
|
|
3871
|
+
}
|
|
3872
|
+
function checkTail(checkSummary) {
|
|
3873
|
+
return checkSummary ? [cutLines(checkSummary, MAX_CHECK_SUMMARY_CHARS)] : [];
|
|
3874
|
+
}
|
|
3875
|
+
function renderIssueComment(ctx) {
|
|
3876
|
+
const { outcome } = ctx;
|
|
3877
|
+
switch (outcome.kind) {
|
|
3878
|
+
case "waiting-for-maintainer":
|
|
3879
|
+
return "Waiting for a maintainer: comment `/livelocale apply`";
|
|
3880
|
+
case "wrong-repo":
|
|
3881
|
+
return `This change link was made for ${codeSpan(outcome.linkRepo)}, not ${codeSpan(outcome.repo)}. Nothing was applied.`;
|
|
3882
|
+
case "error": {
|
|
3883
|
+
const message = cut(outcome.message, MAX_ERROR_MESSAGE_CHARS).replace(/\.$/, "");
|
|
3884
|
+
const runLog = ctx.runUrl ? ` Run log: ${ctx.runUrl}` : "";
|
|
3885
|
+
return `LiveLocale could not apply this link: ${message}.${runLog}`;
|
|
3886
|
+
}
|
|
3887
|
+
case "nothing-to-apply": {
|
|
3888
|
+
const hint = needsForceHint(ctx.plan);
|
|
3889
|
+
return withTable(
|
|
3890
|
+
["Nothing to apply.", ...creditLines(ctx, false)],
|
|
3891
|
+
ctx.plan,
|
|
3892
|
+
hint ? [hint] : []
|
|
3893
|
+
);
|
|
3894
|
+
}
|
|
3895
|
+
case "pr": {
|
|
3896
|
+
const headline = `${outcome.reused ? "Updated pull request" : "Pull request"}: ${outcome.url}`;
|
|
3897
|
+
return withTable(
|
|
3898
|
+
[headline, ...creditLines(ctx, true)],
|
|
3899
|
+
ctx.plan,
|
|
3900
|
+
checkTail(ctx.checkSummary)
|
|
3901
|
+
);
|
|
3902
|
+
}
|
|
3903
|
+
case "compare": {
|
|
3904
|
+
const headline = [
|
|
3905
|
+
`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}`,
|
|
3906
|
+
'(Or allow it once: Settings \u2192 Actions \u2192 General \u2192 "Allow GitHub Actions to create and approve pull requests".)'
|
|
3907
|
+
];
|
|
3908
|
+
return withTable(
|
|
3909
|
+
[...headline, ...creditLines(ctx, true)],
|
|
3910
|
+
ctx.plan,
|
|
3911
|
+
checkTail(ctx.checkSummary)
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
// src/codemod/apply/github-event.ts
|
|
3918
|
+
var PR_TITLE = "i18n: LiveLocale changes";
|
|
3919
|
+
var NOT_FOUND_MESSAGE = "No LiveLocale change link (lc1.\u2026) found.";
|
|
3920
|
+
var ExpectedFailure = class extends Error {
|
|
3921
|
+
};
|
|
3922
|
+
function errorMessage(err) {
|
|
3923
|
+
return err instanceof Error ? err.message : String(err);
|
|
3924
|
+
}
|
|
3925
|
+
function required(env, name) {
|
|
3926
|
+
const value = env[name];
|
|
3927
|
+
if (!value) throw new Error(`[livelocale] ${name} is not set; run this inside GitHub Actions.`);
|
|
3928
|
+
return value;
|
|
3929
|
+
}
|
|
3930
|
+
async function closeIssue(rest, trigger, log) {
|
|
3931
|
+
const n = trigger.issueNumber;
|
|
3932
|
+
const issue = await rest.api("GET", `/issues/${n}`, {
|
|
3933
|
+
allow404: true
|
|
3934
|
+
});
|
|
3935
|
+
const body = typeof issue.data?.body === "string" ? issue.data.body : "";
|
|
3936
|
+
if (issue.data?.state !== "open" || !body.includes(CHANGE_BLOCK_OPEN)) {
|
|
3937
|
+
log(`Issue #${n} is not an open LiveLocale issue; nothing to close.`);
|
|
3938
|
+
return "ignored";
|
|
3939
|
+
}
|
|
3940
|
+
await rest.api("PATCH", `/issues/${n}`, {
|
|
3941
|
+
body: { state: "closed", state_reason: trigger.merged ? "completed" : "not_planned" }
|
|
3942
|
+
});
|
|
3943
|
+
await rest.api("POST", `/issues/${n}/comments`, {
|
|
3944
|
+
body: { body: `Closed because ${trigger.prUrl} was ${trigger.merged ? "merged" : "closed"}.` }
|
|
3945
|
+
});
|
|
3946
|
+
log(`Closed issue #${n}.`);
|
|
3947
|
+
return "closed-issue";
|
|
3948
|
+
}
|
|
3949
|
+
async function checkChanges(options, workspace, remote, changes, plan) {
|
|
3950
|
+
const dir = normalizeRepoDir(options.localesPath);
|
|
3951
|
+
const prefix = dir === "" ? "" : `${dir}/`;
|
|
3952
|
+
const tmp = fs6.mkdtempSync(path10.join(os.tmpdir(), "livelocale-check-"));
|
|
3953
|
+
try {
|
|
3954
|
+
for (const [file, text] of new Map([...remote.files, ...changes])) {
|
|
3955
|
+
const target = path10.join(tmp, ...file.slice(prefix.length).split("/"));
|
|
3956
|
+
fs6.mkdirSync(path10.dirname(target), { recursive: true });
|
|
3957
|
+
fs6.writeFileSync(target, text, "utf-8");
|
|
3958
|
+
}
|
|
3959
|
+
const report = await runCheck({
|
|
3960
|
+
projectDir: path10.join(workspace, options.dir ?? "."),
|
|
3961
|
+
localesPath: tmp,
|
|
3962
|
+
sourceLocale: options.sourceLocale,
|
|
3963
|
+
unused: false
|
|
3964
|
+
});
|
|
3965
|
+
const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
|
|
3966
|
+
return summarizeCheck(report, edited);
|
|
3967
|
+
} catch (err) {
|
|
3968
|
+
return `Check could not run: ${errorMessage(err)}`;
|
|
3969
|
+
} finally {
|
|
3970
|
+
fs6.rmSync(tmp, { recursive: true, force: true });
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
async function runGitHubEventApply(options) {
|
|
3974
|
+
const { env } = options;
|
|
3975
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
3976
|
+
const token = required(env, "GITHUB_TOKEN");
|
|
3977
|
+
const eventPath = required(env, "GITHUB_EVENT_PATH");
|
|
3978
|
+
const event = JSON.parse(fs6.readFileSync(eventPath, "utf-8"));
|
|
3979
|
+
const trigger = parseTrigger(env.GITHUB_EVENT_NAME ?? "", event);
|
|
3980
|
+
if (trigger.kind === "ignore") {
|
|
3981
|
+
log(`Nothing to do: ${trigger.reason}.`);
|
|
3982
|
+
return "ignored";
|
|
3983
|
+
}
|
|
3984
|
+
const repo = env.GITHUB_REPOSITORY || trigger.repo;
|
|
3985
|
+
const rest = createGitHubRest({
|
|
3986
|
+
repo,
|
|
3987
|
+
token,
|
|
3988
|
+
apiBase: env.GITHUB_API_URL || void 0,
|
|
3989
|
+
fetchImpl: options.fetchImpl
|
|
3990
|
+
});
|
|
3991
|
+
if (trigger.kind === "close-issue") return closeIssue(rest, trigger, log);
|
|
3992
|
+
const server = (env.GITHUB_SERVER_URL || "https://github.com").replace(/\/+$/, "");
|
|
3993
|
+
const runId = env.GITHUB_RUN_ID ?? "0";
|
|
3994
|
+
const runUrl = `${server}/${repo}/actions/runs/${runId}`;
|
|
3995
|
+
const issueNumber = trigger.issueNumber ?? void 0;
|
|
3996
|
+
const report = async (outcome, ctx = {}) => {
|
|
3997
|
+
const text = renderIssueComment({ ...ctx, runUrl, outcome });
|
|
3998
|
+
if (issueNumber !== void 0) {
|
|
3999
|
+
await rest.api("POST", `/issues/${issueNumber}/comments`, { body: { body: text } });
|
|
4000
|
+
} else if (env.GITHUB_STEP_SUMMARY) {
|
|
4001
|
+
fs6.appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}
|
|
4002
|
+
`, "utf-8");
|
|
4003
|
+
}
|
|
4004
|
+
log(`Result: ${outcome.kind}`);
|
|
4005
|
+
return outcome.kind;
|
|
4006
|
+
};
|
|
4007
|
+
if (trigger.via !== "dispatch") {
|
|
4008
|
+
if (!await isTrustedActor(rest, trigger.actor.login, trigger.association)) {
|
|
4009
|
+
return report({ kind: "waiting-for-maintainer" });
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
try {
|
|
4013
|
+
return await applyLink(options, rest, trigger, { repo, runId, runUrl, server, log, report });
|
|
4014
|
+
} catch (err) {
|
|
4015
|
+
if (err instanceof ExpectedFailure) return report({ kind: "error", message: err.message });
|
|
4016
|
+
await report({ kind: "error", message: errorMessage(err) });
|
|
4017
|
+
throw err;
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
async function applyLink(options, rest, trigger, run3) {
|
|
4021
|
+
const { repo, runId, runUrl, log, report } = run3;
|
|
4022
|
+
const issueNumber = trigger.issueNumber ?? void 0;
|
|
4023
|
+
let set;
|
|
4024
|
+
try {
|
|
4025
|
+
set = await decodeChangeLink(trigger.text, nodeInflate);
|
|
4026
|
+
} catch (err) {
|
|
4027
|
+
if (!(err instanceof ChangeLinkError)) throw err;
|
|
4028
|
+
throw new ExpectedFailure(err.code === "NOT_FOUND" ? NOT_FOUND_MESSAGE : err.message);
|
|
4029
|
+
}
|
|
4030
|
+
if (set.repo !== void 0 && set.repo.toLowerCase() !== repo.toLowerCase()) {
|
|
4031
|
+
return report({ kind: "wrong-repo", linkRepo: set.repo, repo });
|
|
4032
|
+
}
|
|
4033
|
+
const defaultBranch = trigger.defaultBranch;
|
|
4034
|
+
const chosen = chooseTargetBranch(set.branch, options.branches ?? [defaultBranch], defaultBranch);
|
|
4035
|
+
const target = {
|
|
4036
|
+
branch: chosen.branch,
|
|
4037
|
+
isDefault: chosen.branch === defaultBranch,
|
|
4038
|
+
...set.branch ? { requested: set.branch } : {},
|
|
4039
|
+
reason: chosen.reason
|
|
4040
|
+
};
|
|
4041
|
+
const targetSha = await rest.refSha(target.branch);
|
|
4042
|
+
if (targetSha === null) {
|
|
4043
|
+
throw new Error(`[livelocale] The target branch ${target.branch} does not exist.`);
|
|
4044
|
+
}
|
|
4045
|
+
const locales = [.../* @__PURE__ */ new Set([...set.edits.map((e) => e.locale), options.sourceLocale])];
|
|
4046
|
+
const remote = await readRemoteLocales(rest, targetSha, options.localesPath, locales);
|
|
4047
|
+
if (!remote.targets.get(options.sourceLocale)) {
|
|
4048
|
+
throw new ExpectedFailure(
|
|
4049
|
+
`Source locale \`${options.sourceLocale}\` has no locale file on \`${target.branch}\`.`
|
|
4050
|
+
);
|
|
4051
|
+
}
|
|
4052
|
+
const plan = planApply({
|
|
4053
|
+
edits: set.edits,
|
|
4054
|
+
sourceLocale: options.sourceLocale,
|
|
4055
|
+
targets: remote.targets,
|
|
4056
|
+
allowForce: trigger.force
|
|
4057
|
+
});
|
|
4058
|
+
log(renderTerminalSummary(plan));
|
|
4059
|
+
const credits = { plan, by: set.by, note: set.note, target };
|
|
4060
|
+
if (plan.counts.apply === 0) return report({ kind: "nothing-to-apply" }, credits);
|
|
4061
|
+
const branchPlan = await prepareBranch(
|
|
4062
|
+
rest,
|
|
4063
|
+
branchBaseName(trigger, runId),
|
|
4064
|
+
target.branch,
|
|
4065
|
+
targetSha,
|
|
4066
|
+
runId
|
|
4067
|
+
);
|
|
4068
|
+
const base = branchPlan.mode === "append" ? await readRemoteLocales(rest, branchPlan.parentSha, options.localesPath, locales) : remote;
|
|
4069
|
+
const changes = buildFileChanges(options.localesPath, plan, remote, base.files);
|
|
4070
|
+
const commit = await commitFiles(rest, {
|
|
4071
|
+
branchPlan,
|
|
4072
|
+
files: changes,
|
|
4073
|
+
message: renderCommitMessage({ plan, by: set.by, issueNumber, runId }),
|
|
4074
|
+
author: noreplyAuthor(trigger.actor)
|
|
4075
|
+
});
|
|
4076
|
+
log(
|
|
4077
|
+
commit.sha === null ? `${branchPlan.branch} already holds these changes.` : `Committed to ${branchPlan.branch}.`
|
|
4078
|
+
);
|
|
4079
|
+
const workspace = options.env.GITHUB_WORKSPACE || process.cwd();
|
|
4080
|
+
const checkSummary = await checkChanges(options, workspace, base, changes, plan);
|
|
4081
|
+
const title = issueNumber !== void 0 ? `${PR_TITLE} (#${issueNumber})` : `${PR_TITLE} (run ${runId})`;
|
|
4082
|
+
const body = renderPullRequestBody({ ...credits, issueNumber, checkSummary, runUrl });
|
|
4083
|
+
let outcome;
|
|
4084
|
+
try {
|
|
4085
|
+
const pull = await rest.openOrReusePullRequest({
|
|
4086
|
+
head: branchPlan.branch,
|
|
4087
|
+
base: target.branch,
|
|
4088
|
+
title,
|
|
4089
|
+
body,
|
|
4090
|
+
updateBodyIfReused: true
|
|
4091
|
+
});
|
|
4092
|
+
outcome = { kind: "pr", url: pull.url, reused: pull.reused };
|
|
4093
|
+
} catch (err) {
|
|
4094
|
+
if (!isPullRequestCreationForbidden(err)) throw err;
|
|
4095
|
+
const closing = closingLine({ target, issueNumber });
|
|
4096
|
+
const url = `${run3.server}/${repo}/compare/${encodeRefPath(target.branch)}...${encodeRefPath(branchPlan.branch)}?quick_pull=1&title=${encodeURIComponent(title)}` + (closing ? `&body=${encodeURIComponent(closing)}` : "");
|
|
4097
|
+
outcome = { kind: "compare", url, branch: branchPlan.branch };
|
|
4098
|
+
}
|
|
4099
|
+
log(`Pull request: ${outcome.url}`);
|
|
4100
|
+
return report(outcome, { ...credits, checkSummary });
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
// src/codemod/apply/link-input.ts
|
|
4104
|
+
var import_child_process3 = require("child_process");
|
|
4105
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
4106
|
+
var path11 = __toESM(require("path"), 1);
|
|
4107
|
+
var MAX_APPLY_INPUT_BYTES = 1048576;
|
|
4108
|
+
var TOO_LARGE_MESSAGE3 = "The input is larger than 1 MB.";
|
|
4109
|
+
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.)';
|
|
4110
|
+
var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
|
|
4111
|
+
var ApplyInputError = class extends Error {
|
|
4112
|
+
constructor(message) {
|
|
4113
|
+
super(message);
|
|
4114
|
+
this.name = "ApplyInputError";
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
function checkSize(text) {
|
|
4118
|
+
if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
|
|
4119
|
+
throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4120
|
+
}
|
|
4121
|
+
return text;
|
|
4122
|
+
}
|
|
4123
|
+
async function readStream(stream) {
|
|
4124
|
+
const chunks = [];
|
|
4125
|
+
let size = 0;
|
|
4126
|
+
for await (const chunk of stream) {
|
|
4127
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
|
|
4128
|
+
size += bytes.length;
|
|
4129
|
+
if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4130
|
+
chunks.push(bytes);
|
|
4131
|
+
}
|
|
4132
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
4133
|
+
}
|
|
4134
|
+
function existingFile(arg, cwd) {
|
|
4135
|
+
try {
|
|
4136
|
+
const resolved = path11.resolve(cwd, arg);
|
|
4137
|
+
return fs7.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
|
|
4138
|
+
} catch {
|
|
4139
|
+
return null;
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
async function readApplyInput(arg, options) {
|
|
4143
|
+
if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
|
|
4144
|
+
if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
|
|
4145
|
+
if (arg === "-") return readStream(options.stdin ?? process.stdin);
|
|
4146
|
+
const file = existingFile(arg, options.cwd);
|
|
4147
|
+
if (file === null) return checkSize(arg);
|
|
4148
|
+
if (fs7.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4149
|
+
return checkSize(fs7.readFileSync(file, "utf8"));
|
|
4150
|
+
}
|
|
4151
|
+
function clipboardCommands(platform) {
|
|
4152
|
+
if (platform === "win32") {
|
|
4153
|
+
return [
|
|
4154
|
+
["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
|
|
4155
|
+
];
|
|
4156
|
+
}
|
|
4157
|
+
if (platform === "darwin") return [["pbpaste", []]];
|
|
4158
|
+
return [
|
|
4159
|
+
["wl-paste", ["--no-newline"]],
|
|
4160
|
+
["xclip", ["-selection", "clipboard", "-o"]],
|
|
4161
|
+
["xsel", ["--clipboard", "--output"]]
|
|
4162
|
+
];
|
|
4163
|
+
}
|
|
4164
|
+
var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
|
|
4165
|
+
function run(execFileImpl, [command, args]) {
|
|
4166
|
+
return new Promise((resolve16) => {
|
|
4167
|
+
try {
|
|
4168
|
+
execFileImpl(
|
|
4169
|
+
command,
|
|
4170
|
+
args,
|
|
4171
|
+
{ encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
|
|
4172
|
+
(error, stdout) => {
|
|
4173
|
+
const code = error?.code;
|
|
4174
|
+
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve16(TOO_LARGE);
|
|
4175
|
+
else resolve16(error ? null : String(stdout));
|
|
4176
|
+
}
|
|
4177
|
+
);
|
|
4178
|
+
} catch {
|
|
4179
|
+
resolve16(null);
|
|
4180
|
+
}
|
|
4181
|
+
});
|
|
4182
|
+
}
|
|
4183
|
+
async function readClipboard(execFileImpl = import_child_process3.execFile, platform = process.platform) {
|
|
4184
|
+
for (const command of clipboardCommands(platform)) {
|
|
4185
|
+
const text = await run(execFileImpl, command);
|
|
4186
|
+
if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
|
|
4187
|
+
if (text !== null) return checkSize(text);
|
|
4188
|
+
}
|
|
4189
|
+
throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
// src/codemod/apply/local.ts
|
|
4193
|
+
var import_child_process5 = require("child_process");
|
|
4194
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
4195
|
+
var os2 = __toESM(require("os"), 1);
|
|
4196
|
+
var path15 = __toESM(require("path"), 1);
|
|
4197
|
+
|
|
4198
|
+
// src/server/file-mutator.ts
|
|
4199
|
+
var import_fs6 = require("fs");
|
|
4200
|
+
var path13 = __toESM(require("path"), 1);
|
|
4201
|
+
|
|
4202
|
+
// src/core/safe-path.ts
|
|
4203
|
+
var path12 = __toESM(require("path"), 1);
|
|
4204
|
+
function resolveSafeLocalePath(basePath, locale) {
|
|
4205
|
+
if (!isSafeLocaleSegment(locale)) {
|
|
4206
|
+
throw new Error("Path traversal attempt");
|
|
4207
|
+
}
|
|
4208
|
+
const resolvedBasePath = path12.resolve(basePath);
|
|
4209
|
+
const filePath = path12.resolve(resolvedBasePath, `${locale}.json`);
|
|
4210
|
+
if (!filePath.startsWith(resolvedBasePath)) {
|
|
4211
|
+
throw new Error("Path traversal attempt");
|
|
4212
|
+
}
|
|
4213
|
+
return filePath;
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
// src/server/file-mutator.ts
|
|
4217
|
+
var FileMutex = class {
|
|
4218
|
+
queue = Promise.resolve();
|
|
4219
|
+
async acquire(fn) {
|
|
4220
|
+
const next = this.queue.then(fn);
|
|
4221
|
+
this.queue = next.then(
|
|
4222
|
+
() => {
|
|
4223
|
+
},
|
|
4224
|
+
() => {
|
|
4225
|
+
}
|
|
4226
|
+
);
|
|
4227
|
+
return next;
|
|
4228
|
+
}
|
|
4229
|
+
};
|
|
4230
|
+
var FileMutator = class {
|
|
4231
|
+
fileLocks = /* @__PURE__ */ new Map();
|
|
4232
|
+
requireEnabled;
|
|
4233
|
+
quiet;
|
|
4234
|
+
scanner;
|
|
4235
|
+
constructor(options = {}) {
|
|
4236
|
+
this.requireEnabled = options.requireEnabled ?? true;
|
|
4237
|
+
this.quiet = options.quiet ?? false;
|
|
4238
|
+
this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
|
|
4239
|
+
}
|
|
4240
|
+
assertEnabled() {
|
|
4241
|
+
if (!this.requireEnabled) return;
|
|
4242
|
+
const reason = getDisabledReason();
|
|
4243
|
+
if (reason !== null) {
|
|
4244
|
+
throw new Error(
|
|
4245
|
+
`[livelocale] FileMutator can only be executed in development mode (${reason}).`
|
|
4246
|
+
);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Helper to serialize execution per file path
|
|
4251
|
+
*/
|
|
4252
|
+
async withLock(filePath, fn) {
|
|
4253
|
+
let lock = this.fileLocks.get(filePath);
|
|
4254
|
+
if (!lock) {
|
|
4255
|
+
lock = new FileMutex();
|
|
4256
|
+
this.fileLocks.set(filePath, lock);
|
|
4257
|
+
}
|
|
4258
|
+
return lock.acquire(fn);
|
|
4259
|
+
}
|
|
4260
|
+
/**
|
|
4261
|
+
* Updates a locale JSON file on disk atomically and safely.
|
|
4262
|
+
* Enforces development mode checks, path traversal mitigation, key depth restrictions,
|
|
4263
|
+
* structural JSON validation, and concurrency serialization.
|
|
4264
|
+
*/
|
|
4265
|
+
async updateLocaleKey(basePath, locale, keyPath, newValue) {
|
|
4266
|
+
this.assertEnabled();
|
|
4267
|
+
if (!isSafeLocaleSegment(locale)) {
|
|
4268
|
+
throw new Error(
|
|
4269
|
+
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
4270
|
+
);
|
|
4271
|
+
}
|
|
4272
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4273
|
+
const localeDir = path13.resolve(resolvedBasePath, locale);
|
|
4274
|
+
let filePath = "";
|
|
4275
|
+
let mutationKeyPath = keyPath;
|
|
4276
|
+
let isNamespaceDir = false;
|
|
4277
|
+
try {
|
|
4278
|
+
const stats = await import_fs6.promises.stat(localeDir);
|
|
4279
|
+
if (stats.isDirectory()) {
|
|
4280
|
+
isNamespaceDir = true;
|
|
4281
|
+
}
|
|
4282
|
+
} catch {
|
|
4283
|
+
}
|
|
4284
|
+
if (isNamespaceDir) {
|
|
4285
|
+
const firstDot = keyPath.indexOf(".");
|
|
4286
|
+
if (firstDot !== -1) {
|
|
4287
|
+
const namespace = keyPath.substring(0, firstDot);
|
|
3432
4288
|
if (DANGEROUS_KEY_SEGMENTS.has(namespace)) {
|
|
3433
4289
|
throw new Error(
|
|
3434
4290
|
`[livelocale] Forbidden namespace segment in path: "${namespace}". Segment names like __proto__, constructor, and prototype are not allowed.`
|
|
3435
4291
|
);
|
|
3436
4292
|
}
|
|
3437
4293
|
mutationKeyPath = keyPath.substring(firstDot + 1);
|
|
3438
|
-
filePath =
|
|
4294
|
+
filePath = path13.resolve(localeDir, `${namespace}.json`);
|
|
3439
4295
|
} else {
|
|
3440
|
-
filePath =
|
|
4296
|
+
filePath = path13.resolve(localeDir, `${keyPath}.json`);
|
|
3441
4297
|
mutationKeyPath = "";
|
|
3442
4298
|
}
|
|
3443
4299
|
} else {
|
|
3444
|
-
filePath =
|
|
4300
|
+
filePath = path13.resolve(resolvedBasePath, `${locale}.json`);
|
|
3445
4301
|
}
|
|
3446
4302
|
if (!filePath.startsWith(resolvedBasePath)) {
|
|
3447
4303
|
throw new Error(
|
|
@@ -3453,7 +4309,7 @@ var FileMutator = class {
|
|
|
3453
4309
|
let indent = " ";
|
|
3454
4310
|
let trailingNewline = "\n";
|
|
3455
4311
|
try {
|
|
3456
|
-
const fileContent = await
|
|
4312
|
+
const fileContent = await import_fs6.promises.readFile(filePath, "utf-8");
|
|
3457
4313
|
indent = detectJsonIndent(fileContent);
|
|
3458
4314
|
trailingNewline = fileContent.endsWith("\n") ? "\n" : "";
|
|
3459
4315
|
try {
|
|
@@ -3493,17 +4349,17 @@ var FileMutator = class {
|
|
|
3493
4349
|
}
|
|
3494
4350
|
const tempPath = `${filePath}.tmp`;
|
|
3495
4351
|
try {
|
|
3496
|
-
await
|
|
4352
|
+
await import_fs6.promises.writeFile(tempPath, stringified, "utf-8");
|
|
3497
4353
|
} catch (err) {
|
|
3498
4354
|
throw new Error(
|
|
3499
4355
|
`[livelocale] Failed to write temp file at ${tempPath}. Operation: write. Original error: ${err.message}`
|
|
3500
4356
|
);
|
|
3501
4357
|
}
|
|
3502
4358
|
try {
|
|
3503
|
-
await
|
|
4359
|
+
await import_fs6.promises.rename(tempPath, filePath);
|
|
3504
4360
|
} catch (err) {
|
|
3505
4361
|
try {
|
|
3506
|
-
await
|
|
4362
|
+
await import_fs6.promises.unlink(tempPath);
|
|
3507
4363
|
} catch {
|
|
3508
4364
|
}
|
|
3509
4365
|
throw new Error(
|
|
@@ -3521,19 +4377,19 @@ var FileMutator = class {
|
|
|
3521
4377
|
*/
|
|
3522
4378
|
async readLocale(basePath, locale) {
|
|
3523
4379
|
this.assertEnabled();
|
|
3524
|
-
const localePath =
|
|
4380
|
+
const localePath = path13.resolve(basePath, locale);
|
|
3525
4381
|
let isDir = false;
|
|
3526
4382
|
try {
|
|
3527
|
-
isDir = (await
|
|
4383
|
+
isDir = (await import_fs6.promises.stat(localePath)).isDirectory();
|
|
3528
4384
|
} catch {
|
|
3529
4385
|
}
|
|
3530
4386
|
if (isDir) {
|
|
3531
4387
|
const merged = {};
|
|
3532
|
-
for (const file of await
|
|
4388
|
+
for (const file of await import_fs6.promises.readdir(localePath)) {
|
|
3533
4389
|
if (!file.endsWith(".json")) continue;
|
|
3534
4390
|
try {
|
|
3535
|
-
merged[
|
|
3536
|
-
await
|
|
4391
|
+
merged[path13.basename(file, ".json")] = JSON.parse(
|
|
4392
|
+
await import_fs6.promises.readFile(path13.join(localePath, file), "utf-8")
|
|
3537
4393
|
);
|
|
3538
4394
|
} catch {
|
|
3539
4395
|
}
|
|
@@ -3541,15 +4397,15 @@ var FileMutator = class {
|
|
|
3541
4397
|
return merged;
|
|
3542
4398
|
}
|
|
3543
4399
|
const filePath = resolveSafeLocalePath(basePath, locale);
|
|
3544
|
-
return JSON.parse(await
|
|
4400
|
+
return JSON.parse(await import_fs6.promises.readFile(filePath, "utf-8"));
|
|
3545
4401
|
}
|
|
3546
4402
|
/**
|
|
3547
4403
|
* Lists all locale codes dynamically scanned from the locales directory.
|
|
3548
4404
|
*/
|
|
3549
4405
|
async listLocales(basePath) {
|
|
3550
4406
|
this.assertEnabled();
|
|
3551
|
-
const resolvedBasePath =
|
|
3552
|
-
const items = await
|
|
4407
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4408
|
+
const items = await import_fs6.promises.readdir(resolvedBasePath, { withFileTypes: true });
|
|
3553
4409
|
const localesSet = /* @__PURE__ */ new Set();
|
|
3554
4410
|
for (const item of items) {
|
|
3555
4411
|
if (item.isDirectory()) {
|
|
@@ -3557,7 +4413,7 @@ var FileMutator = class {
|
|
|
3557
4413
|
localesSet.add(item.name);
|
|
3558
4414
|
}
|
|
3559
4415
|
} else if (item.isFile() && item.name.endsWith(".json")) {
|
|
3560
|
-
const name =
|
|
4416
|
+
const name = path13.basename(item.name, ".json");
|
|
3561
4417
|
if (LOCALE_RE.test(name)) {
|
|
3562
4418
|
localesSet.add(name);
|
|
3563
4419
|
}
|
|
@@ -3578,8 +4434,8 @@ var FileMutator = class {
|
|
|
3578
4434
|
if (!LOCALE_RE.test(locale)) {
|
|
3579
4435
|
throw new Error(`[livelocale] Invalid locale format: "${locale}"`);
|
|
3580
4436
|
}
|
|
3581
|
-
const resolvedBasePath =
|
|
3582
|
-
const localePath =
|
|
4437
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4438
|
+
const localePath = path13.resolve(resolvedBasePath, locale);
|
|
3583
4439
|
if (!localePath.startsWith(resolvedBasePath)) {
|
|
3584
4440
|
throw new Error(
|
|
3585
4441
|
`[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
|
|
@@ -3587,19 +4443,19 @@ var FileMutator = class {
|
|
|
3587
4443
|
}
|
|
3588
4444
|
let exists = false;
|
|
3589
4445
|
try {
|
|
3590
|
-
await
|
|
4446
|
+
await import_fs6.promises.stat(localePath);
|
|
3591
4447
|
exists = true;
|
|
3592
4448
|
} catch {
|
|
3593
4449
|
}
|
|
3594
4450
|
try {
|
|
3595
|
-
await
|
|
4451
|
+
await import_fs6.promises.stat(`${localePath}.json`);
|
|
3596
4452
|
exists = true;
|
|
3597
4453
|
} catch {
|
|
3598
4454
|
}
|
|
3599
4455
|
if (exists) {
|
|
3600
4456
|
throw new Error(`[livelocale] Locale "${locale}" already exists.`);
|
|
3601
4457
|
}
|
|
3602
|
-
const items = await
|
|
4458
|
+
const items = await import_fs6.promises.readdir(resolvedBasePath, { withFileTypes: true });
|
|
3603
4459
|
let referenceDir = null;
|
|
3604
4460
|
for (const item of items) {
|
|
3605
4461
|
if (item.isDirectory() && LOCALE_RE.test(item.name)) {
|
|
@@ -3608,16 +4464,16 @@ var FileMutator = class {
|
|
|
3608
4464
|
}
|
|
3609
4465
|
}
|
|
3610
4466
|
if (referenceDir) {
|
|
3611
|
-
await
|
|
3612
|
-
const refPath =
|
|
3613
|
-
const files = await
|
|
4467
|
+
await import_fs6.promises.mkdir(localePath, { recursive: true });
|
|
4468
|
+
const refPath = path13.resolve(resolvedBasePath, referenceDir);
|
|
4469
|
+
const files = await import_fs6.promises.readdir(refPath);
|
|
3614
4470
|
for (const file of files) {
|
|
3615
4471
|
if (file.endsWith(".json")) {
|
|
3616
|
-
await
|
|
4472
|
+
await import_fs6.promises.writeFile(path13.join(localePath, file), "{}", "utf-8");
|
|
3617
4473
|
}
|
|
3618
4474
|
}
|
|
3619
4475
|
} else {
|
|
3620
|
-
await
|
|
4476
|
+
await import_fs6.promises.writeFile(`${localePath}.json`, "{}", "utf-8");
|
|
3621
4477
|
}
|
|
3622
4478
|
console.log(`[livelocale] Added locale '${locale}'`);
|
|
3623
4479
|
}
|
|
@@ -3632,16 +4488,16 @@ var FileMutator = class {
|
|
|
3632
4488
|
if (!LOCALE_RE.test(oldLocale) || !LOCALE_RE.test(newLocale)) {
|
|
3633
4489
|
throw new Error(`[livelocale] Invalid locale format`);
|
|
3634
4490
|
}
|
|
3635
|
-
const resolvedBasePath =
|
|
3636
|
-
const oldPath =
|
|
3637
|
-
const newPath =
|
|
4491
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4492
|
+
const oldPath = path13.resolve(resolvedBasePath, oldLocale);
|
|
4493
|
+
const newPath = path13.resolve(resolvedBasePath, newLocale);
|
|
3638
4494
|
if (!oldPath.startsWith(resolvedBasePath) || !newPath.startsWith(resolvedBasePath)) {
|
|
3639
4495
|
throw new Error("[livelocale] Path traversal attempt");
|
|
3640
4496
|
}
|
|
3641
4497
|
let isDir = false;
|
|
3642
4498
|
let exists = false;
|
|
3643
4499
|
try {
|
|
3644
|
-
const stats = await
|
|
4500
|
+
const stats = await import_fs6.promises.stat(oldPath);
|
|
3645
4501
|
isDir = stats.isDirectory();
|
|
3646
4502
|
exists = true;
|
|
3647
4503
|
} catch {
|
|
@@ -3649,7 +4505,7 @@ var FileMutator = class {
|
|
|
3649
4505
|
let isFile = false;
|
|
3650
4506
|
if (!exists) {
|
|
3651
4507
|
try {
|
|
3652
|
-
await
|
|
4508
|
+
await import_fs6.promises.stat(`${oldPath}.json`);
|
|
3653
4509
|
isFile = true;
|
|
3654
4510
|
exists = true;
|
|
3655
4511
|
} catch {
|
|
@@ -3660,12 +4516,12 @@ var FileMutator = class {
|
|
|
3660
4516
|
}
|
|
3661
4517
|
let newExists = false;
|
|
3662
4518
|
try {
|
|
3663
|
-
await
|
|
4519
|
+
await import_fs6.promises.stat(newPath);
|
|
3664
4520
|
newExists = true;
|
|
3665
4521
|
} catch {
|
|
3666
4522
|
}
|
|
3667
4523
|
try {
|
|
3668
|
-
await
|
|
4524
|
+
await import_fs6.promises.stat(`${newPath}.json`);
|
|
3669
4525
|
newExists = true;
|
|
3670
4526
|
} catch {
|
|
3671
4527
|
}
|
|
@@ -3673,9 +4529,9 @@ var FileMutator = class {
|
|
|
3673
4529
|
throw new Error(`[livelocale] Target locale "${newLocale}" already exists.`);
|
|
3674
4530
|
}
|
|
3675
4531
|
if (isDir) {
|
|
3676
|
-
await
|
|
4532
|
+
await import_fs6.promises.rename(oldPath, newPath);
|
|
3677
4533
|
} else if (isFile) {
|
|
3678
|
-
await
|
|
4534
|
+
await import_fs6.promises.rename(`${oldPath}.json`, `${newPath}.json`);
|
|
3679
4535
|
}
|
|
3680
4536
|
console.log(`[livelocale] Renamed locale '${oldLocale}' to '${newLocale}'`);
|
|
3681
4537
|
}
|
|
@@ -3690,15 +4546,15 @@ var FileMutator = class {
|
|
|
3690
4546
|
if (!LOCALE_RE.test(locale)) {
|
|
3691
4547
|
throw new Error(`[livelocale] Invalid locale format`);
|
|
3692
4548
|
}
|
|
3693
|
-
const resolvedBasePath =
|
|
3694
|
-
const localePath =
|
|
4549
|
+
const resolvedBasePath = path13.resolve(basePath);
|
|
4550
|
+
const localePath = path13.resolve(resolvedBasePath, locale);
|
|
3695
4551
|
if (!localePath.startsWith(resolvedBasePath)) {
|
|
3696
4552
|
throw new Error("[livelocale] Path traversal attempt");
|
|
3697
4553
|
}
|
|
3698
4554
|
let isDir = false;
|
|
3699
4555
|
let exists = false;
|
|
3700
4556
|
try {
|
|
3701
|
-
const stats = await
|
|
4557
|
+
const stats = await import_fs6.promises.stat(localePath);
|
|
3702
4558
|
isDir = stats.isDirectory();
|
|
3703
4559
|
exists = true;
|
|
3704
4560
|
} catch {
|
|
@@ -3706,7 +4562,7 @@ var FileMutator = class {
|
|
|
3706
4562
|
let isFile = false;
|
|
3707
4563
|
if (!exists) {
|
|
3708
4564
|
try {
|
|
3709
|
-
await
|
|
4565
|
+
await import_fs6.promises.stat(`${localePath}.json`);
|
|
3710
4566
|
isFile = true;
|
|
3711
4567
|
exists = true;
|
|
3712
4568
|
} catch {
|
|
@@ -3716,9 +4572,9 @@ var FileMutator = class {
|
|
|
3716
4572
|
throw new Error(`[livelocale] Locale "${locale}" does not exist.`);
|
|
3717
4573
|
}
|
|
3718
4574
|
if (isDir) {
|
|
3719
|
-
await
|
|
4575
|
+
await import_fs6.promises.rm(localePath, { recursive: true, force: true });
|
|
3720
4576
|
} else if (isFile) {
|
|
3721
|
-
await
|
|
4577
|
+
await import_fs6.promises.unlink(`${localePath}.json`);
|
|
3722
4578
|
}
|
|
3723
4579
|
console.log(`[livelocale] Deleted locale '${locale}'`);
|
|
3724
4580
|
}
|
|
@@ -3746,8 +4602,8 @@ var FileMutator = class {
|
|
|
3746
4602
|
// src/server/git-client.ts
|
|
3747
4603
|
var import_child_process4 = require("child_process");
|
|
3748
4604
|
var import_util = require("util");
|
|
3749
|
-
var
|
|
3750
|
-
var
|
|
4605
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
4606
|
+
var path14 = __toESM(require("path"), 1);
|
|
3751
4607
|
var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
|
|
3752
4608
|
var MAX_DIFF_BYTES = 2e5;
|
|
3753
4609
|
function mapStatus(xy) {
|
|
@@ -3778,15 +4634,15 @@ var GitClient = class {
|
|
|
3778
4634
|
}
|
|
3779
4635
|
/** Resolves the repository root and the locales path relative to it (forward slashes). */
|
|
3780
4636
|
async locate(localesPath) {
|
|
3781
|
-
const absolute =
|
|
3782
|
-
const cwd =
|
|
3783
|
-
if (!
|
|
4637
|
+
const absolute = path14.resolve(localesPath);
|
|
4638
|
+
const cwd = fs9.existsSync(absolute) ? absolute : path14.dirname(absolute);
|
|
4639
|
+
if (!fs9.existsSync(cwd)) return null;
|
|
3784
4640
|
try {
|
|
3785
4641
|
const root = (await this.git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
3786
4642
|
if (!root) return null;
|
|
3787
|
-
const rel =
|
|
4643
|
+
const rel = path14.relative(root, absolute).split(path14.sep).join("/");
|
|
3788
4644
|
if (rel.startsWith("..")) return null;
|
|
3789
|
-
return { root:
|
|
4645
|
+
return { root: path14.resolve(root), rel: rel || ".", absolute };
|
|
3790
4646
|
} catch {
|
|
3791
4647
|
return null;
|
|
3792
4648
|
}
|
|
@@ -3818,301 +4674,127 @@ var GitClient = class {
|
|
|
3818
4674
|
if (file.startsWith('"') && file.endsWith('"')) file = file.slice(1, -1);
|
|
3819
4675
|
changes.push({ path: file, status: mapStatus(xy) });
|
|
3820
4676
|
}
|
|
3821
|
-
} catch {
|
|
3822
|
-
}
|
|
3823
|
-
return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
|
|
3824
|
-
}
|
|
3825
|
-
async diff(localesPath) {
|
|
3826
|
-
const loc = await this.locate(localesPath);
|
|
3827
|
-
if (!loc) return "";
|
|
3828
|
-
let out = "";
|
|
3829
|
-
try {
|
|
3830
|
-
out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
|
|
3831
|
-
} catch {
|
|
3832
|
-
}
|
|
3833
|
-
try {
|
|
3834
|
-
const untracked = await this.git(loc.root, [
|
|
3835
|
-
"ls-files",
|
|
3836
|
-
"--others",
|
|
3837
|
-
"--exclude-standard",
|
|
3838
|
-
"--",
|
|
3839
|
-
loc.rel
|
|
3840
|
-
]);
|
|
3841
|
-
for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
3842
|
-
const full = path12.join(loc.root, file);
|
|
3843
|
-
let content = "";
|
|
3844
|
-
try {
|
|
3845
|
-
content = fs8.readFileSync(full, "utf-8");
|
|
3846
|
-
} catch {
|
|
3847
|
-
continue;
|
|
3848
|
-
}
|
|
3849
|
-
const lines = content.split("\n");
|
|
3850
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
3851
|
-
out += `diff --git a/${file} b/${file}
|
|
3852
|
-
new file mode 100644
|
|
3853
|
-
--- /dev/null
|
|
3854
|
-
+++ b/${file}
|
|
3855
|
-
@@ -0,0 +1,${lines.length} @@
|
|
3856
|
-
` + lines.map((l) => `+${l}`).join("\n") + "\n";
|
|
3857
|
-
}
|
|
3858
|
-
} catch {
|
|
3859
|
-
}
|
|
3860
|
-
if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
|
|
3861
|
-
out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
|
|
3862
|
-
}
|
|
3863
|
-
return out;
|
|
3864
|
-
}
|
|
3865
|
-
async commit(localesPath, message) {
|
|
3866
|
-
const loc = await this.locate(localesPath);
|
|
3867
|
-
if (!loc) throw new Error("Not a git repository.");
|
|
3868
|
-
const status = await this.status(localesPath);
|
|
3869
|
-
if (status.changes.length === 0) {
|
|
3870
|
-
throw new Error(`No changes to commit under ${loc.rel}.`);
|
|
3871
|
-
}
|
|
3872
|
-
await this.git(loc.root, ["add", "-A", "--", loc.rel]);
|
|
3873
|
-
await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
|
|
3874
|
-
const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
|
|
3875
|
-
return { hash };
|
|
3876
|
-
}
|
|
3877
|
-
async discard(localesPath) {
|
|
3878
|
-
const loc = await this.locate(localesPath);
|
|
3879
|
-
if (!loc) throw new Error("Not a git repository.");
|
|
3880
|
-
try {
|
|
3881
|
-
await this.git(loc.root, ["checkout", "--", loc.rel]);
|
|
3882
|
-
} catch {
|
|
3883
|
-
}
|
|
3884
|
-
await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
|
|
3885
|
-
}
|
|
3886
|
-
};
|
|
3887
|
-
|
|
3888
|
-
// src/codemod/apply/report.ts
|
|
3889
|
-
var DEFAULT_SPAN_LENGTH = 120;
|
|
3890
|
-
var DEFAULT_TABLE_ROWS = 100;
|
|
3891
|
-
var MAX_BODY_CHARS = 6e4;
|
|
3892
|
-
var MAX_NOTE_CHARS2 = 2e3;
|
|
3893
|
-
var MAX_NAME_CHARS = 64;
|
|
3894
|
-
var MAX_CHECK_LINES = 50;
|
|
3895
|
-
var MAX_CHECK_SUMMARY_CHARS = 1e4;
|
|
3896
|
-
var BODY_MARGIN = 200;
|
|
3897
|
-
var STATUS_LABELS = {
|
|
3898
|
-
apply: "applied",
|
|
3899
|
-
already: "already done",
|
|
3900
|
-
conflict: "conflict",
|
|
3901
|
-
"needs-force": "needs --force",
|
|
3902
|
-
invalid: "rejected"
|
|
3903
|
-
};
|
|
3904
|
-
var BIDI_CONTROL_RE = /[--]/g;
|
|
3905
|
-
var NEWLINE_RE = /\r\n|\r|\n/g;
|
|
3906
|
-
var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
3907
|
-
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
3908
|
-
function cut(text, max) {
|
|
3909
|
-
if (text.length <= max) return text;
|
|
3910
|
-
let out = text.slice(0, max);
|
|
3911
|
-
const last = out.charCodeAt(out.length - 1);
|
|
3912
|
-
if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
|
|
3913
|
-
return out;
|
|
3914
|
-
}
|
|
3915
|
-
function cutLines(text, max) {
|
|
3916
|
-
if (text.length <= max) return text;
|
|
3917
|
-
const kept = [];
|
|
3918
|
-
let length = 0;
|
|
3919
|
-
for (const line of text.split("\n")) {
|
|
3920
|
-
const added = (kept.length > 0 ? 1 : 0) + line.length;
|
|
3921
|
-
if (length + added + 2 > max) break;
|
|
3922
|
-
kept.push(line);
|
|
3923
|
-
length += added;
|
|
3924
|
-
}
|
|
3925
|
-
return [...kept, "\u2026"].join("\n");
|
|
3926
|
-
}
|
|
3927
|
-
function longestBacktickRun(text) {
|
|
3928
|
-
let longest = 0;
|
|
3929
|
-
for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
|
|
3930
|
-
return longest;
|
|
3931
|
-
}
|
|
3932
|
-
function codePointLabel(ch) {
|
|
3933
|
-
return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
|
|
3934
|
-
}
|
|
3935
|
-
function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
|
|
3936
|
-
const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
|
|
3937
|
-
const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
|
|
3938
|
-
if (content === "") return "` `";
|
|
3939
|
-
const fence = "`".repeat(longestBacktickRun(content) + 1);
|
|
3940
|
-
const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
|
|
3941
|
-
return `${fence}${pad}${content}${pad}${fence}`;
|
|
3942
|
-
}
|
|
3943
|
-
function sanitizeName(name) {
|
|
3944
|
-
if (name === void 0) return void 0;
|
|
3945
|
-
const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
|
|
3946
|
-
const result = cut(cleaned, MAX_NAME_CHARS).trim();
|
|
3947
|
-
return result === "" ? void 0 : result;
|
|
3948
|
-
}
|
|
3949
|
-
var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
|
|
3950
|
-
function summarizeCheck(report, edited) {
|
|
3951
|
-
const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
|
|
3952
|
-
const mine = report.issues.filter(
|
|
3953
|
-
(issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
|
|
3954
|
-
);
|
|
3955
|
-
if (mine.length === 0) return NO_CHECK_PROBLEMS;
|
|
3956
|
-
const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
|
|
3957
|
-
for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
|
|
3958
|
-
lines.push(
|
|
3959
|
-
`- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
|
|
3960
|
-
);
|
|
3961
|
-
}
|
|
3962
|
-
if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
|
|
3963
|
-
const others = report.issues.length - mine.length;
|
|
3964
|
-
if (others > 0) {
|
|
3965
|
-
lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
|
|
3966
|
-
}
|
|
3967
|
-
return lines.join("\n");
|
|
3968
|
-
}
|
|
3969
|
-
function checksCell(edit) {
|
|
3970
|
-
const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
|
|
3971
|
-
if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
|
|
3972
|
-
parts.push(codeSpan(edit.message));
|
|
4677
|
+
} catch {
|
|
4678
|
+
}
|
|
4679
|
+
return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
|
|
3973
4680
|
}
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4681
|
+
async diff(localesPath) {
|
|
4682
|
+
const loc = await this.locate(localesPath);
|
|
4683
|
+
if (!loc) return "";
|
|
4684
|
+
let out = "";
|
|
4685
|
+
try {
|
|
4686
|
+
out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
|
|
4687
|
+
} catch {
|
|
4688
|
+
}
|
|
4689
|
+
try {
|
|
4690
|
+
const untracked = await this.git(loc.root, [
|
|
4691
|
+
"ls-files",
|
|
4692
|
+
"--others",
|
|
4693
|
+
"--exclude-standard",
|
|
4694
|
+
"--",
|
|
4695
|
+
loc.rel
|
|
4696
|
+
]);
|
|
4697
|
+
for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
4698
|
+
const full = path14.join(loc.root, file);
|
|
4699
|
+
let content = "";
|
|
4700
|
+
try {
|
|
4701
|
+
content = fs9.readFileSync(full, "utf-8");
|
|
4702
|
+
} catch {
|
|
4703
|
+
continue;
|
|
4704
|
+
}
|
|
4705
|
+
const lines = content.split("\n");
|
|
4706
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
4707
|
+
out += `diff --git a/${file} b/${file}
|
|
4708
|
+
new file mode 100644
|
|
4709
|
+
--- /dev/null
|
|
4710
|
+
+++ b/${file}
|
|
4711
|
+
@@ -0,0 +1,${lines.length} @@
|
|
4712
|
+
` + lines.map((l) => `+${l}`).join("\n") + "\n";
|
|
4713
|
+
}
|
|
4714
|
+
} catch {
|
|
4715
|
+
}
|
|
4716
|
+
if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
|
|
4717
|
+
out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
|
|
4718
|
+
}
|
|
4719
|
+
return out;
|
|
4006
4720
|
}
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
if (ctx.issueNumber === void 0) return void 0;
|
|
4019
|
-
if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
|
|
4020
|
-
return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
|
|
4021
|
-
}
|
|
4022
|
-
function renderPullRequestBody(ctx) {
|
|
4023
|
-
const head = ["Applies translations sent with a LiveLocale change link."];
|
|
4024
|
-
if (ctx.by?.trim()) head.push(`Translated by ${codeSpan(ctx.by)}`);
|
|
4025
|
-
if (ctx.target.reason === "not-allowed" && ctx.target.requested !== void 0) {
|
|
4026
|
-
head.push(
|
|
4027
|
-
`The link was made from ${codeSpan(ctx.target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(ctx.target.branch)}.`
|
|
4028
|
-
);
|
|
4721
|
+
async commit(localesPath, message) {
|
|
4722
|
+
const loc = await this.locate(localesPath);
|
|
4723
|
+
if (!loc) throw new Error("Not a git repository.");
|
|
4724
|
+
const status = await this.status(localesPath);
|
|
4725
|
+
if (status.changes.length === 0) {
|
|
4726
|
+
throw new Error(`No changes to commit under ${loc.rel}.`);
|
|
4727
|
+
}
|
|
4728
|
+
await this.git(loc.root, ["add", "-A", "--", loc.rel]);
|
|
4729
|
+
await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
|
|
4730
|
+
const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
|
|
4731
|
+
return { hash };
|
|
4029
4732
|
}
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
return [...head, table, ...tail].join("\n\n");
|
|
4039
|
-
}
|
|
4040
|
-
function renderCommitMessage(input) {
|
|
4041
|
-
let subject = "i18n: apply LiveLocale changes";
|
|
4042
|
-
if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
|
|
4043
|
-
else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
|
|
4044
|
-
const applied = input.plan.edits.filter((edit) => edit.status === "apply");
|
|
4045
|
-
const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
|
|
4046
|
-
const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
|
|
4047
|
-
const name = sanitizeName(input.by);
|
|
4048
|
-
if (name) parts.push(`Translated-by: ${name}`);
|
|
4049
|
-
return parts.join("\n\n");
|
|
4050
|
-
}
|
|
4051
|
-
function renderTerminalSummary(plan) {
|
|
4052
|
-
const c = plan.counts;
|
|
4053
|
-
const lines = [
|
|
4054
|
-
`Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
|
|
4055
|
-
];
|
|
4056
|
-
for (const edit of plan.edits) {
|
|
4057
|
-
lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
|
|
4733
|
+
async discard(localesPath) {
|
|
4734
|
+
const loc = await this.locate(localesPath);
|
|
4735
|
+
if (!loc) throw new Error("Not a git repository.");
|
|
4736
|
+
try {
|
|
4737
|
+
await this.git(loc.root, ["checkout", "--", loc.rel]);
|
|
4738
|
+
} catch {
|
|
4739
|
+
}
|
|
4740
|
+
await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
|
|
4058
4741
|
}
|
|
4059
|
-
|
|
4060
|
-
}
|
|
4742
|
+
};
|
|
4061
4743
|
|
|
4062
4744
|
// src/codemod/apply/local.ts
|
|
4063
|
-
function
|
|
4745
|
+
function isPlainObject5(value) {
|
|
4064
4746
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4065
4747
|
}
|
|
4066
4748
|
async function readTarget(mutator, localesDir, locale) {
|
|
4067
4749
|
if (!isValidLocaleFormat(locale)) return null;
|
|
4068
|
-
const dir =
|
|
4750
|
+
const dir = path15.join(localesDir, locale);
|
|
4069
4751
|
let layout;
|
|
4070
|
-
if (
|
|
4752
|
+
if (fs10.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
4071
4753
|
layout = "namespaced";
|
|
4072
|
-
} else if (
|
|
4754
|
+
} else if (fs10.statSync(`${dir}.json`, { throwIfNoEntry: false })?.isFile()) {
|
|
4073
4755
|
layout = "flat";
|
|
4074
4756
|
} else {
|
|
4075
4757
|
return null;
|
|
4076
4758
|
}
|
|
4077
4759
|
const dict = await mutator.readLocale(localesDir, locale);
|
|
4078
|
-
if (!
|
|
4760
|
+
if (!isPlainObject5(dict)) {
|
|
4079
4761
|
throw new Error(`The "${locale}" locale file is not a JSON object.`);
|
|
4080
4762
|
}
|
|
4081
4763
|
return { layout, dict };
|
|
4082
4764
|
}
|
|
4083
4765
|
function fileFor(localesPath, layout, locale, key) {
|
|
4084
|
-
const rel = layout === "flat" ?
|
|
4085
|
-
return
|
|
4766
|
+
const rel = layout === "flat" ? path15.join(localesPath, `${locale}.json`) : path15.join(localesPath, locale, `${key.slice(0, key.indexOf("."))}.json`);
|
|
4767
|
+
return path15.normalize(rel).split(path15.sep).join("/");
|
|
4086
4768
|
}
|
|
4087
|
-
var
|
|
4769
|
+
var PR_TITLE2 = "i18n: LiveLocale changes";
|
|
4088
4770
|
var GITHUB_REMOTE_RE = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/;
|
|
4089
4771
|
var defaultGit = (args, cwd, opts) => {
|
|
4090
4772
|
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
4091
4773
|
if (opts?.inherit) {
|
|
4092
|
-
return new Promise((
|
|
4774
|
+
return new Promise((resolve16, reject2) => {
|
|
4093
4775
|
const child = (0, import_child_process5.spawn)("git", args, { cwd, env, stdio: "inherit", windowsHide: true });
|
|
4094
4776
|
child.on("error", reject2);
|
|
4095
4777
|
child.on(
|
|
4096
4778
|
"close",
|
|
4097
|
-
(code) => code === 0 ?
|
|
4779
|
+
(code) => code === 0 ? resolve16("") : reject2(new Error(`git ${args[0]} failed (exit ${code}).`))
|
|
4098
4780
|
);
|
|
4099
4781
|
});
|
|
4100
4782
|
}
|
|
4101
|
-
return new Promise((
|
|
4783
|
+
return new Promise((resolve16, reject2) => {
|
|
4102
4784
|
(0, import_child_process5.execFile)(
|
|
4103
4785
|
"git",
|
|
4104
4786
|
args,
|
|
4105
4787
|
{ cwd, env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 },
|
|
4106
|
-
(error, stdout) => error ? reject2(error) :
|
|
4788
|
+
(error, stdout) => error ? reject2(error) : resolve16(String(stdout))
|
|
4107
4789
|
);
|
|
4108
4790
|
});
|
|
4109
4791
|
};
|
|
4110
|
-
var defaultGh = (args, cwd) => new Promise((
|
|
4792
|
+
var defaultGh = (args, cwd) => new Promise((resolve16, reject2) => {
|
|
4111
4793
|
(0, import_child_process5.execFile)(
|
|
4112
4794
|
"gh",
|
|
4113
4795
|
args,
|
|
4114
4796
|
{ cwd, windowsHide: true },
|
|
4115
|
-
(error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) :
|
|
4797
|
+
(error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) : resolve16(String(stdout))
|
|
4116
4798
|
);
|
|
4117
4799
|
});
|
|
4118
4800
|
function utcStamp(date) {
|
|
@@ -4144,7 +4826,7 @@ async function pickBranchName(git, root, stamp) {
|
|
|
4144
4826
|
for (let n = 2; await taken(name); n++) name = `${base}-${n}`;
|
|
4145
4827
|
return name;
|
|
4146
4828
|
}
|
|
4147
|
-
function
|
|
4829
|
+
function errorMessage2(err) {
|
|
4148
4830
|
return err instanceof Error ? err.message : String(err);
|
|
4149
4831
|
}
|
|
4150
4832
|
async function writeEdits(mutator, plan, targets, localesDir, localesPath) {
|
|
@@ -4164,12 +4846,12 @@ function exitCodeFor(plan) {
|
|
|
4164
4846
|
async function preparePullRequest(git, projectDir, localesDir) {
|
|
4165
4847
|
const top = (await tryRun(() => git(["rev-parse", "--show-toplevel"], projectDir)))?.trim();
|
|
4166
4848
|
if (!top) throw new ApplyInputError("apply --pr needs a git repository.");
|
|
4167
|
-
const root =
|
|
4849
|
+
const root = path15.resolve(top);
|
|
4168
4850
|
const original = (await tryRun(() => git(["symbolic-ref", "--quiet", "--short", "HEAD"], root)))?.trim();
|
|
4169
4851
|
if (!original) throw new ApplyInputError("Check out a branch first.");
|
|
4170
4852
|
const status = await new GitClient().status(localesDir);
|
|
4171
4853
|
if (status.changes.length > 0) {
|
|
4172
|
-
const rel =
|
|
4854
|
+
const rel = path15.relative(root, localesDir).split(path15.sep).join("/") || ".";
|
|
4173
4855
|
throw new ApplyInputError(
|
|
4174
4856
|
`Commit or stash the changes under ${rel} first; apply --pr commits everything in that folder.`
|
|
4175
4857
|
);
|
|
@@ -4177,10 +4859,10 @@ async function preparePullRequest(git, projectDir, localesDir) {
|
|
|
4177
4859
|
return { root, original };
|
|
4178
4860
|
}
|
|
4179
4861
|
async function createPullRequest(log, gh, root, base, head, body) {
|
|
4180
|
-
const tmpDir =
|
|
4181
|
-
const bodyFile =
|
|
4862
|
+
const tmpDir = fs10.mkdtempSync(path15.join(os2.tmpdir(), "livelocale-pr-"));
|
|
4863
|
+
const bodyFile = path15.join(tmpDir, "body.md");
|
|
4182
4864
|
try {
|
|
4183
|
-
|
|
4865
|
+
fs10.writeFileSync(bodyFile, body, "utf-8");
|
|
4184
4866
|
const out = await gh(
|
|
4185
4867
|
[
|
|
4186
4868
|
"pr",
|
|
@@ -4190,7 +4872,7 @@ async function createPullRequest(log, gh, root, base, head, body) {
|
|
|
4190
4872
|
"--head",
|
|
4191
4873
|
head,
|
|
4192
4874
|
"--title",
|
|
4193
|
-
|
|
4875
|
+
PR_TITLE2,
|
|
4194
4876
|
"--body-file",
|
|
4195
4877
|
bodyFile
|
|
4196
4878
|
],
|
|
@@ -4201,20 +4883,20 @@ async function createPullRequest(log, gh, root, base, head, body) {
|
|
|
4201
4883
|
} catch (err) {
|
|
4202
4884
|
if (err?.code !== "ENOENT") {
|
|
4203
4885
|
const stderr = err?.stderr;
|
|
4204
|
-
const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() :
|
|
4886
|
+
const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() : errorMessage2(err);
|
|
4205
4887
|
log(`gh could not open the pull request: ${reason}`);
|
|
4206
4888
|
}
|
|
4207
4889
|
return void 0;
|
|
4208
4890
|
} finally {
|
|
4209
|
-
|
|
4891
|
+
fs10.rmSync(tmpDir, { recursive: true, force: true });
|
|
4210
4892
|
}
|
|
4211
4893
|
}
|
|
4212
4894
|
async function runLocalApply(options) {
|
|
4213
4895
|
const log = options.log ?? ((line) => console.log(line));
|
|
4214
4896
|
const set = await decodeChangeLink(options.input, nodeInflate);
|
|
4215
4897
|
const localesPath = options.localesPath ?? "locales";
|
|
4216
|
-
const localesDir =
|
|
4217
|
-
if (!
|
|
4898
|
+
const localesDir = path15.resolve(options.projectDir, localesPath);
|
|
4899
|
+
if (!fs10.statSync(localesDir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
4218
4900
|
throw new ApplyInputError(`No locales folder at ${localesPath}.`);
|
|
4219
4901
|
}
|
|
4220
4902
|
const pr = options.pr === true && options.dryRun !== true;
|
|
@@ -4270,7 +4952,7 @@ async function runLocalApply(options) {
|
|
|
4270
4952
|
const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
|
|
4271
4953
|
checkSummary = summarizeCheck(report, edited);
|
|
4272
4954
|
} catch (err) {
|
|
4273
|
-
checkSummary = `Check could not run: ${
|
|
4955
|
+
checkSummary = `Check could not run: ${errorMessage2(err)}`;
|
|
4274
4956
|
}
|
|
4275
4957
|
await new GitClient().commit(localesDir, renderCommitMessage({ plan, by: set.by }));
|
|
4276
4958
|
await git(["push", "-u", "origin", branch], root, { inherit: true });
|
|
@@ -4314,7 +4996,8 @@ var FLAG_OPTIONS = {
|
|
|
4314
4996
|
"--clipboard": "clipboard",
|
|
4315
4997
|
"--pr": "pr",
|
|
4316
4998
|
"--force": "force",
|
|
4317
|
-
"--dry-run": "dryRun"
|
|
4999
|
+
"--dry-run": "dryRun",
|
|
5000
|
+
"--github-event": "githubEvent"
|
|
4318
5001
|
};
|
|
4319
5002
|
function parseApplyArgs(argv) {
|
|
4320
5003
|
const args = {
|
|
@@ -4325,11 +5008,20 @@ function parseApplyArgs(argv) {
|
|
|
4325
5008
|
dryRun: false,
|
|
4326
5009
|
dir: ".",
|
|
4327
5010
|
locales: void 0,
|
|
4328
|
-
source: void 0
|
|
5011
|
+
source: void 0,
|
|
5012
|
+
githubEvent: false,
|
|
5013
|
+
branches: void 0
|
|
4329
5014
|
};
|
|
4330
5015
|
for (let i = 0; i < argv.length; i++) {
|
|
4331
5016
|
const arg = argv[i];
|
|
4332
|
-
if (
|
|
5017
|
+
if (arg === "--branches") {
|
|
5018
|
+
const value = argv[i + 1];
|
|
5019
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
5020
|
+
throw new ApplyInputError(`${arg} needs a value.`);
|
|
5021
|
+
}
|
|
5022
|
+
args.branches = value.split(",").map((branch) => branch.trim()).filter(Boolean);
|
|
5023
|
+
i++;
|
|
5024
|
+
} else if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS2, arg)) {
|
|
4333
5025
|
const value = argv[i + 1];
|
|
4334
5026
|
if (value === void 0 || value.startsWith("--")) {
|
|
4335
5027
|
throw new ApplyInputError(`${arg} needs a value.`);
|
|
@@ -4349,17 +5041,43 @@ function parseApplyArgs(argv) {
|
|
|
4349
5041
|
if (args.clipboard && args.input !== void 0) {
|
|
4350
5042
|
throw new ApplyInputError("Pass the link or --clipboard, not both.");
|
|
4351
5043
|
}
|
|
5044
|
+
if (args.githubEvent) {
|
|
5045
|
+
if (args.force) {
|
|
5046
|
+
throw new ApplyInputError(
|
|
5047
|
+
"--force is not allowed with --github-event; a maintainer comments `/livelocale apply --force`."
|
|
5048
|
+
);
|
|
5049
|
+
}
|
|
5050
|
+
if (args.input !== void 0) {
|
|
5051
|
+
throw new ApplyInputError(
|
|
5052
|
+
"--github-event reads the link from the event file; do not pass one."
|
|
5053
|
+
);
|
|
5054
|
+
}
|
|
5055
|
+
if (args.locales === void 0 || args.source === void 0) {
|
|
5056
|
+
throw new ApplyInputError("--github-event needs --locales and --source.");
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
4352
5059
|
return args;
|
|
4353
5060
|
}
|
|
4354
5061
|
async function runApplyCommand(argv) {
|
|
4355
5062
|
try {
|
|
4356
5063
|
const args = parseApplyArgs(argv);
|
|
5064
|
+
if (args.githubEvent) {
|
|
5065
|
+
const result2 = await runGitHubEventApply({
|
|
5066
|
+
env: process.env,
|
|
5067
|
+
localesPath: args.locales,
|
|
5068
|
+
sourceLocale: args.source,
|
|
5069
|
+
branches: args.branches,
|
|
5070
|
+
dir: args.dir
|
|
5071
|
+
});
|
|
5072
|
+
process.exitCode = result2 === "error" || result2 === "wrong-repo" ? 1 : 0;
|
|
5073
|
+
return;
|
|
5074
|
+
}
|
|
4357
5075
|
const input = await readApplyInput(args.input, {
|
|
4358
5076
|
cwd: process.cwd(),
|
|
4359
5077
|
clipboard: args.clipboard
|
|
4360
5078
|
});
|
|
4361
5079
|
const result = await runLocalApply({
|
|
4362
|
-
projectDir:
|
|
5080
|
+
projectDir: path16.resolve(args.dir),
|
|
4363
5081
|
localesPath: args.locales,
|
|
4364
5082
|
sourceLocale: args.source,
|
|
4365
5083
|
input,
|
|
@@ -4374,6 +5092,327 @@ async function runApplyCommand(argv) {
|
|
|
4374
5092
|
}
|
|
4375
5093
|
}
|
|
4376
5094
|
|
|
5095
|
+
// src/codemod/init-share.ts
|
|
5096
|
+
var fs11 = __toESM(require("fs"), 1);
|
|
5097
|
+
var path17 = __toESM(require("path"), 1);
|
|
5098
|
+
var CHECKOUT_ACTION = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2";
|
|
5099
|
+
var WORKFLOW_PATH = ".github/workflows/livelocale.yml";
|
|
5100
|
+
var LOCALES_CANDIDATES = [
|
|
5101
|
+
"locales",
|
|
5102
|
+
"src/locales",
|
|
5103
|
+
"public/locales",
|
|
5104
|
+
"src/i18n/locales",
|
|
5105
|
+
"messages"
|
|
5106
|
+
];
|
|
5107
|
+
var SAFE_ARG = /^[A-Za-z0-9_./-]+$/;
|
|
5108
|
+
var VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
5109
|
+
var MAX_BRANCHES = 10;
|
|
5110
|
+
var SHARE_GITIGNORE = "public/livelocale/";
|
|
5111
|
+
function readText3(file) {
|
|
5112
|
+
try {
|
|
5113
|
+
return fs11.readFileSync(file, "utf-8");
|
|
5114
|
+
} catch {
|
|
5115
|
+
return null;
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
function parsePackageJson(text) {
|
|
5119
|
+
if (text === null) return null;
|
|
5120
|
+
try {
|
|
5121
|
+
const parsed = JSON.parse(text);
|
|
5122
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
5123
|
+
} catch {
|
|
5124
|
+
return null;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
function hasDependency2(pkg, name) {
|
|
5128
|
+
return Boolean(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]);
|
|
5129
|
+
}
|
|
5130
|
+
function detectShareStack(projectDir) {
|
|
5131
|
+
const target = detectBuildTarget(projectDir);
|
|
5132
|
+
if (target.kind === "next") {
|
|
5133
|
+
return target.staticExport ? { stack: "next-export", evidence: "Next.js with output: 'export'" } : { stack: "next-server", evidence: "Next.js without output: 'export'" };
|
|
5134
|
+
}
|
|
5135
|
+
if (target.kind === "vite") return { stack: "vite", evidence: "Vite" };
|
|
5136
|
+
if (target.kind === "cra") return { stack: "cra", evidence: "react-scripts" };
|
|
5137
|
+
const pkg = parsePackageJson(readText3(path17.join(projectDir, "package.json")));
|
|
5138
|
+
for (const name of ["webpack", "@rsbuild/core"]) {
|
|
5139
|
+
if (hasDependency2(pkg, name)) return { stack: "webpack", evidence: `${name} dependency` };
|
|
5140
|
+
}
|
|
5141
|
+
const config = fs11.readdirSync(projectDir).find((name) => /^webpack\.config\.[A-Za-z]+$/.test(name));
|
|
5142
|
+
if (config) return { stack: "webpack", evidence: config };
|
|
5143
|
+
return { stack: "unknown", evidence: "no Vite, Next.js, CRA, webpack or Rsbuild setup found" };
|
|
5144
|
+
}
|
|
5145
|
+
function checkSafePath(name, value) {
|
|
5146
|
+
const segments = value.split("/");
|
|
5147
|
+
if (!SAFE_ARG.test(value) || value.startsWith("/") || segments.includes("..")) {
|
|
5148
|
+
throw new Error(`\`${name}\` may only contain letters, digits, ".", "_", "-" and "/".`);
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
function validateShareArgs(o) {
|
|
5152
|
+
checkSafePath("--locales", o.localesPath);
|
|
5153
|
+
if (o.dir !== void 0) checkSafePath("--dir", o.dir);
|
|
5154
|
+
if (!LOCALE_RE.test(o.sourceLocale)) {
|
|
5155
|
+
throw new Error("`--source` must be a locale code such as en or pt-BR.");
|
|
5156
|
+
}
|
|
5157
|
+
if (o.branches.length === 0) throw new Error("`--branches` needs at least one branch.");
|
|
5158
|
+
if (o.branches.length > MAX_BRANCHES) {
|
|
5159
|
+
throw new Error(`\`--branches\` takes at most ${MAX_BRANCHES} branches.`);
|
|
5160
|
+
}
|
|
5161
|
+
for (const branch of o.branches) checkSafePath("--branches", branch);
|
|
5162
|
+
}
|
|
5163
|
+
function renderWorkflow(o) {
|
|
5164
|
+
validateShareArgs(o);
|
|
5165
|
+
if (!VERSION_RE.test(o.version)) throw new Error(`Not a livelocale version: ${o.version}`);
|
|
5166
|
+
const dirArg = o.dir === void 0 ? "" : ` --dir ${o.dir}`;
|
|
5167
|
+
return [
|
|
5168
|
+
"# Written by `npx livelocale init --share`. Turns LiveLocale change links into pull requests.",
|
|
5169
|
+
"# livelocale reads the link from the event file; it never reaches a shell command.",
|
|
5170
|
+
"name: LiveLocale",
|
|
5171
|
+
"on:",
|
|
5172
|
+
" issues: { types: [opened] }",
|
|
5173
|
+
" issue_comment: { types: [created] }",
|
|
5174
|
+
" pull_request: { types: [closed] }",
|
|
5175
|
+
" workflow_dispatch: { inputs: { link: { description: 'LiveLocale change link', required: true } } }",
|
|
5176
|
+
"permissions: { contents: write, pull-requests: write, issues: write }",
|
|
5177
|
+
"concurrency: { group: 'livelocale-${{ github.event.issue.number || github.run_id }}', queue: max }",
|
|
5178
|
+
"jobs:",
|
|
5179
|
+
" apply:",
|
|
5180
|
+
" if: >-",
|
|
5181
|
+
" github.event_name == 'workflow_dispatch' ||",
|
|
5182
|
+
" (github.event_name == 'issues' && contains(github.event.issue.body, '```livelocale')) ||",
|
|
5183
|
+
" (github.event_name == 'issue_comment' && !github.event.issue.pull_request &&",
|
|
5184
|
+
" startsWith(github.event.comment.body, '/livelocale')) ||",
|
|
5185
|
+
" (github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'livelocale/issue-') &&",
|
|
5186
|
+
" github.event.pull_request.base.ref != github.event.repository.default_branch)",
|
|
5187
|
+
" runs-on: ubuntu-latest",
|
|
5188
|
+
" steps:",
|
|
5189
|
+
` - uses: ${CHECKOUT_ACTION}`,
|
|
5190
|
+
" with: { persist-credentials: false }",
|
|
5191
|
+
` - run: npx --yes livelocale@${o.version} apply --github-event --locales ${o.localesPath} --source ${o.sourceLocale} --branches ${o.branches.join(",")}${dirArg}`,
|
|
5192
|
+
" env: { GITHUB_TOKEN: '${{ github.token }}' }",
|
|
5193
|
+
""
|
|
5194
|
+
].join("\n");
|
|
5195
|
+
}
|
|
5196
|
+
function toPosix2(p) {
|
|
5197
|
+
return p.split(path17.sep).join("/");
|
|
5198
|
+
}
|
|
5199
|
+
function normalizeArgPath(p) {
|
|
5200
|
+
return p.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
|
|
5201
|
+
}
|
|
5202
|
+
function realDir(dir) {
|
|
5203
|
+
try {
|
|
5204
|
+
return fs11.realpathSync(dir);
|
|
5205
|
+
} catch {
|
|
5206
|
+
return path17.resolve(dir);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
function holdsLocaleJson(dir) {
|
|
5210
|
+
let entries;
|
|
5211
|
+
try {
|
|
5212
|
+
entries = fs11.readdirSync(dir, { withFileTypes: true });
|
|
5213
|
+
} catch {
|
|
5214
|
+
return false;
|
|
5215
|
+
}
|
|
5216
|
+
return entries.some((entry) => {
|
|
5217
|
+
if (entry.isFile())
|
|
5218
|
+
return entry.name.endsWith(".json") && LOCALE_RE.test(entry.name.slice(0, -5));
|
|
5219
|
+
if (!entry.isDirectory() || !LOCALE_RE.test(entry.name)) return false;
|
|
5220
|
+
try {
|
|
5221
|
+
return fs11.readdirSync(path17.join(dir, entry.name)).some((name) => name.endsWith(".json"));
|
|
5222
|
+
} catch {
|
|
5223
|
+
return false;
|
|
5224
|
+
}
|
|
5225
|
+
});
|
|
5226
|
+
}
|
|
5227
|
+
function findLocalesPath(projectDir) {
|
|
5228
|
+
const found = LOCALES_CANDIDATES.find((rel) => holdsLocaleJson(path17.join(projectDir, rel)));
|
|
5229
|
+
if (!found) throw new Error("Pass --locales <path>.");
|
|
5230
|
+
return found;
|
|
5231
|
+
}
|
|
5232
|
+
async function defaultBranchOf(git, cwd) {
|
|
5233
|
+
const ref = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
5234
|
+
if (ref?.startsWith("origin/") && ref.length > "origin/".length) {
|
|
5235
|
+
return ref.slice("origin/".length);
|
|
5236
|
+
}
|
|
5237
|
+
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
5238
|
+
if (head && head !== "HEAD") return head;
|
|
5239
|
+
return "main";
|
|
5240
|
+
}
|
|
5241
|
+
function planWrite(projectDir, rel, content) {
|
|
5242
|
+
const current = readText3(path17.join(projectDir, rel));
|
|
5243
|
+
if (current === null) return { path: rel, action: "create", content };
|
|
5244
|
+
return current === content ? { path: rel, action: "unchanged" } : { path: rel, action: "update", content };
|
|
5245
|
+
}
|
|
5246
|
+
function planViteConfig(c) {
|
|
5247
|
+
const addNote = `Add liveLocaleVite({ localesPath: '${c.localesPath}', share: true }) to the plugins array in vite.config.`;
|
|
5248
|
+
const config = VITE_CONFIG_FILES.find((name) => fs11.existsSync(path17.join(c.projectDir, name)));
|
|
5249
|
+
if (!config) {
|
|
5250
|
+
c.notes.push(addNote);
|
|
5251
|
+
return [];
|
|
5252
|
+
}
|
|
5253
|
+
const source = readText3(path17.join(c.projectDir, config)) ?? "";
|
|
5254
|
+
if (!source.includes("liveLocaleVite(")) {
|
|
5255
|
+
c.notes.push(addNote);
|
|
5256
|
+
return [{ path: config, action: "skipped", reason: "no liveLocaleVite(...) call" }];
|
|
5257
|
+
}
|
|
5258
|
+
if (/\bshare\s*:/.test(source)) return [{ path: config, action: "unchanged" }];
|
|
5259
|
+
const call = /liveLocaleVite\(\{\s*/;
|
|
5260
|
+
if (!call.test(source)) {
|
|
5261
|
+
c.notes.push(addNote);
|
|
5262
|
+
return [{ path: config, action: "skipped", reason: "liveLocaleVite(...) has no { options }" }];
|
|
5263
|
+
}
|
|
5264
|
+
const source2 = c.sourceLocale === "en" ? "" : `sourceLocale: '${c.sourceLocale}', `;
|
|
5265
|
+
const next = source.replace(call, () => `liveLocaleVite({ share: true, ${source2}`);
|
|
5266
|
+
return [{ path: config, action: "update", content: next }];
|
|
5267
|
+
}
|
|
5268
|
+
function planPackageScripts(c) {
|
|
5269
|
+
const text = readText3(path17.join(c.projectDir, "package.json"));
|
|
5270
|
+
const pkg = parsePackageJson(text);
|
|
5271
|
+
const args = `--locales ${c.localesPath} --source ${c.sourceLocale}`;
|
|
5272
|
+
const isNext = c.stack === "next-export" || c.stack === "next-server";
|
|
5273
|
+
const outDir = c.stack === "cra" ? "build" : "dist";
|
|
5274
|
+
const packCommand = `livelocale pack ${outDir} ${args}`;
|
|
5275
|
+
if (!pkg) {
|
|
5276
|
+
c.notes.push(
|
|
5277
|
+
isNext ? `Add "build:preview": "livelocale build --preview ${args} -- next build" to package.json.` : `Run "${packCommand}" after your build.`
|
|
5278
|
+
);
|
|
5279
|
+
return [{ path: "package.json", action: "skipped", reason: "no readable package.json" }];
|
|
5280
|
+
}
|
|
5281
|
+
const scripts = { ...pkg.scripts ?? {} };
|
|
5282
|
+
if (isNext) {
|
|
5283
|
+
const wanted = `livelocale build --preview ${args} -- next build`;
|
|
5284
|
+
const current = scripts["build:preview"];
|
|
5285
|
+
if (current === wanted || typeof current === "string" && current.includes("livelocale build")) {
|
|
5286
|
+
return [{ path: "package.json", action: "unchanged" }];
|
|
5287
|
+
}
|
|
5288
|
+
if (current !== void 0) {
|
|
5289
|
+
c.notes.push(`Set "build:preview" to "${wanted}" in package.json.`);
|
|
5290
|
+
return [
|
|
5291
|
+
{
|
|
5292
|
+
path: "package.json",
|
|
5293
|
+
action: "skipped",
|
|
5294
|
+
reason: 'scripts["build:preview"] already exists'
|
|
5295
|
+
}
|
|
5296
|
+
];
|
|
5297
|
+
}
|
|
5298
|
+
scripts["build:preview"] = wanted;
|
|
5299
|
+
} else {
|
|
5300
|
+
const build = scripts.build;
|
|
5301
|
+
if (typeof build !== "string" || build.trim() === "") {
|
|
5302
|
+
c.notes.push(`Run "${packCommand}" after your build.`);
|
|
5303
|
+
return [{ path: "package.json", action: "skipped", reason: "no scripts.build" }];
|
|
5304
|
+
}
|
|
5305
|
+
if (build.includes("livelocale pack")) return [{ path: "package.json", action: "unchanged" }];
|
|
5306
|
+
scripts.build = `${build} && ${packCommand}`;
|
|
5307
|
+
}
|
|
5308
|
+
return [
|
|
5309
|
+
{
|
|
5310
|
+
path: "package.json",
|
|
5311
|
+
action: "update",
|
|
5312
|
+
content: stringifyLike({ ...pkg, scripts }, text)
|
|
5313
|
+
}
|
|
5314
|
+
];
|
|
5315
|
+
}
|
|
5316
|
+
function planGitignore(projectDir) {
|
|
5317
|
+
const current = readText3(path17.join(projectDir, ".gitignore"));
|
|
5318
|
+
if (current === null) {
|
|
5319
|
+
return { path: ".gitignore", action: "create", content: `${SHARE_GITIGNORE}
|
|
5320
|
+
` };
|
|
5321
|
+
}
|
|
5322
|
+
if (parseGitignore(current).ignores("public/livelocale", true)) {
|
|
5323
|
+
return { path: ".gitignore", action: "unchanged" };
|
|
5324
|
+
}
|
|
5325
|
+
const sep9 = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
5326
|
+
return { path: ".gitignore", action: "update", content: `${current}${sep9}${SHARE_GITIGNORE}
|
|
5327
|
+
` };
|
|
5328
|
+
}
|
|
5329
|
+
function planBuild(c) {
|
|
5330
|
+
switch (c.stack) {
|
|
5331
|
+
case "vite":
|
|
5332
|
+
return planViteConfig(c);
|
|
5333
|
+
case "cra":
|
|
5334
|
+
case "webpack":
|
|
5335
|
+
return planPackageScripts(c);
|
|
5336
|
+
case "next-export":
|
|
5337
|
+
case "next-server": {
|
|
5338
|
+
const files = [...planPackageScripts(c), planGitignore(c.projectDir)];
|
|
5339
|
+
if (c.stack === "next-export") {
|
|
5340
|
+
const route = findExportBlockingRoute(c.projectDir);
|
|
5341
|
+
if (route) {
|
|
5342
|
+
c.notes.push(
|
|
5343
|
+
`Delete ${route}: static export rejects POST routes, and livelocale build stops while it exists.`
|
|
5344
|
+
);
|
|
5345
|
+
}
|
|
5346
|
+
} else {
|
|
5347
|
+
c.notes.push(
|
|
5348
|
+
'Use "npm run build:preview" as the build command of your preview deployments.'
|
|
5349
|
+
);
|
|
5350
|
+
}
|
|
5351
|
+
return files;
|
|
5352
|
+
}
|
|
5353
|
+
case "unknown":
|
|
5354
|
+
c.notes.push(
|
|
5355
|
+
`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}.`
|
|
5356
|
+
);
|
|
5357
|
+
return [];
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
async function planInitShare(o) {
|
|
5361
|
+
const projectDir = realDir(o.projectDir);
|
|
5362
|
+
const git = o.git ?? runGit;
|
|
5363
|
+
const sourceLocale = o.sourceLocale ?? "en";
|
|
5364
|
+
const localesPath = o.localesPath !== void 0 ? normalizeArgPath(o.localesPath) : findLocalesPath(projectDir);
|
|
5365
|
+
validateShareArgs({ localesPath, sourceLocale, branches: o.branches ?? ["main"] });
|
|
5366
|
+
const { stack } = detectShareStack(projectDir);
|
|
5367
|
+
const notes = [];
|
|
5368
|
+
const files = planBuild({ projectDir, stack, localesPath, sourceLocale, notes });
|
|
5369
|
+
const remote = await git(["remote", "get-url", "origin"], projectDir);
|
|
5370
|
+
if (!remote || parseGitHubSlug(remote) === null) {
|
|
5371
|
+
notes.push('One-click pull requests need GitHub. Use: npx livelocale apply "<link>" --pr');
|
|
5372
|
+
return { stack, files, notes };
|
|
5373
|
+
}
|
|
5374
|
+
const top = await git(["rev-parse", "--show-toplevel"], projectDir);
|
|
5375
|
+
const repoRoot = top ? realDir(top) : projectDir;
|
|
5376
|
+
const appRel = toPosix2(path17.relative(repoRoot, projectDir));
|
|
5377
|
+
const dir = appRel === "" ? void 0 : appRel;
|
|
5378
|
+
const repoLocales = dir === void 0 ? localesPath : `${dir}/${localesPath}`;
|
|
5379
|
+
const defaultBranch = await defaultBranchOf(git, projectDir);
|
|
5380
|
+
const branches = o.branches ?? [defaultBranch];
|
|
5381
|
+
const workflow = renderWorkflow({
|
|
5382
|
+
version: o.version,
|
|
5383
|
+
localesPath: repoLocales,
|
|
5384
|
+
sourceLocale,
|
|
5385
|
+
branches,
|
|
5386
|
+
dir
|
|
5387
|
+
});
|
|
5388
|
+
const workflowRel = toPosix2(path17.relative(projectDir, path17.join(repoRoot, WORKFLOW_PATH)));
|
|
5389
|
+
const planned = planWrite(projectDir, workflowRel, workflow);
|
|
5390
|
+
files.push(
|
|
5391
|
+
planned.action === "update" && !o.overwrite ? {
|
|
5392
|
+
path: workflowRel,
|
|
5393
|
+
action: "skipped",
|
|
5394
|
+
reason: "it exists with different content; pass --overwrite to replace it"
|
|
5395
|
+
} : planned
|
|
5396
|
+
);
|
|
5397
|
+
for (const branch of branches) {
|
|
5398
|
+
if (branch === defaultBranch) continue;
|
|
5399
|
+
notes.push(
|
|
5400
|
+
`Also merge ${WORKFLOW_PATH} into \`${branch}\`; pull requests into it run workflows from that branch, so LiveLocale cannot close their issues otherwise.`
|
|
5401
|
+
);
|
|
5402
|
+
}
|
|
5403
|
+
return { stack, files, notes };
|
|
5404
|
+
}
|
|
5405
|
+
function writePlannedFiles(projectDir, files) {
|
|
5406
|
+
for (const file of files) {
|
|
5407
|
+
if (file.action !== "create" && file.action !== "update" || file.content === void 0) {
|
|
5408
|
+
continue;
|
|
5409
|
+
}
|
|
5410
|
+
const target = path17.resolve(projectDir, file.path);
|
|
5411
|
+
fs11.mkdirSync(path17.dirname(target), { recursive: true });
|
|
5412
|
+
fs11.writeFileSync(target, file.content, "utf-8");
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
|
|
4377
5416
|
// src/codemod/index.ts
|
|
4378
5417
|
var DEFAULT_STUDIO_PORT = 3010;
|
|
4379
5418
|
var DEFAULT_APP_ORIGIN = "http://localhost:3000";
|
|
@@ -4401,6 +5440,11 @@ Options for migrate:
|
|
|
4401
5440
|
|
|
4402
5441
|
Options for init:
|
|
4403
5442
|
--dir <path> Target project directory (default: current directory)
|
|
5443
|
+
--share Set up the share link build and the GitHub workflow instead of the API route
|
|
5444
|
+
--locales <path> (--share) Locales directory relative to the project (default: detected)
|
|
5445
|
+
--source <locale> (--share) Source locale (default: en)
|
|
5446
|
+
--branches <a,b> (--share) Branches a change link may target (default: the default branch)
|
|
5447
|
+
--overwrite (--share) Replace an existing .github/workflows/livelocale.yml
|
|
4404
5448
|
|
|
4405
5449
|
Options for studio:
|
|
4406
5450
|
--port <number> Port to run the studio server on (default: ${DEFAULT_STUDIO_PORT})
|
|
@@ -4446,6 +5490,8 @@ Options for apply:
|
|
|
4446
5490
|
--source <locale> Source locale (default: en, else the first locale)
|
|
4447
5491
|
--force Apply edits the translator saved with "Save anyway"
|
|
4448
5492
|
--dry-run Show what would change without writing files
|
|
5493
|
+
--github-event Run inside the generated GitHub workflow; reads the link from the event file
|
|
5494
|
+
--branches <a,b> Branches a link may target (default: the repository's default branch)
|
|
4449
5495
|
|
|
4450
5496
|
Global Options:
|
|
4451
5497
|
-h, --help Show help information
|
|
@@ -4474,11 +5520,11 @@ function normalizeAppArg(value) {
|
|
|
4474
5520
|
}
|
|
4475
5521
|
}
|
|
4476
5522
|
function isPortAnswering(port) {
|
|
4477
|
-
return new Promise((
|
|
5523
|
+
return new Promise((resolve16) => {
|
|
4478
5524
|
const socket = net.connect({ host: "localhost", port });
|
|
4479
5525
|
const done = (answering) => {
|
|
4480
5526
|
socket.destroy();
|
|
4481
|
-
|
|
5527
|
+
resolve16(answering);
|
|
4482
5528
|
};
|
|
4483
5529
|
socket.setTimeout(1e3, () => done(false));
|
|
4484
5530
|
socket.once("connect", () => done(true));
|
|
@@ -4488,8 +5534,8 @@ function isPortAnswering(port) {
|
|
|
4488
5534
|
async function runStudio(port, appOrigin) {
|
|
4489
5535
|
console.log(`
|
|
4490
5536
|
\u{1F680} Starting LiveLocale Studio...`);
|
|
4491
|
-
const studioDir =
|
|
4492
|
-
if (!
|
|
5537
|
+
const studioDir = path18.resolve(moduleDir(), "../studio");
|
|
5538
|
+
if (!fs12.existsSync(path18.join(studioDir, "index.html"))) {
|
|
4493
5539
|
console.error(`Error: Studio assets not found at "${studioDir}".`);
|
|
4494
5540
|
console.error(`Did you build the project? Run "npm run build" first.`);
|
|
4495
5541
|
process.exit(1);
|
|
@@ -4521,48 +5567,48 @@ async function runStudio(port, appOrigin) {
|
|
|
4521
5567
|
function runInit(projectDir) {
|
|
4522
5568
|
console.log(`
|
|
4523
5569
|
\u{1F680} Initializing LiveLocale API handler...`);
|
|
4524
|
-
const absoluteDir =
|
|
4525
|
-
if (!
|
|
5570
|
+
const absoluteDir = path18.resolve(projectDir);
|
|
5571
|
+
if (!fs12.existsSync(absoluteDir)) {
|
|
4526
5572
|
console.error(`Error: Directory "${projectDir}" does not exist.`);
|
|
4527
5573
|
process.exit(1);
|
|
4528
5574
|
}
|
|
4529
|
-
const isTypeScript =
|
|
5575
|
+
const isTypeScript = fs12.existsSync(path18.join(absoluteDir, "tsconfig.json"));
|
|
4530
5576
|
const fileExt = isTypeScript ? "ts" : "js";
|
|
4531
5577
|
console.log(`\u{1F4C1} Target: ${absoluteDir}`);
|
|
4532
5578
|
console.log(`\u{1F527} Language detected: ${isTypeScript ? "TypeScript" : "JavaScript"}`);
|
|
4533
5579
|
let hasNextIntl = false;
|
|
4534
5580
|
let hasReactI18next = false;
|
|
4535
|
-
const targetPkgPath =
|
|
4536
|
-
if (
|
|
5581
|
+
const targetPkgPath = path18.join(absoluteDir, "package.json");
|
|
5582
|
+
if (fs12.existsSync(targetPkgPath)) {
|
|
4537
5583
|
try {
|
|
4538
|
-
const pkg = JSON.parse(
|
|
5584
|
+
const pkg = JSON.parse(fs12.readFileSync(targetPkgPath, "utf8"));
|
|
4539
5585
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4540
5586
|
hasNextIntl = Boolean(allDeps["next-intl"]);
|
|
4541
5587
|
hasReactI18next = Boolean(allDeps["react-i18next"]);
|
|
4542
5588
|
} catch {
|
|
4543
5589
|
}
|
|
4544
5590
|
}
|
|
4545
|
-
const appDirPath =
|
|
4546
|
-
const pagesDirPath =
|
|
4547
|
-
const srcAppDirPath =
|
|
4548
|
-
const srcPagesDirPath =
|
|
5591
|
+
const appDirPath = path18.join(absoluteDir, "app");
|
|
5592
|
+
const pagesDirPath = path18.join(absoluteDir, "pages");
|
|
5593
|
+
const srcAppDirPath = path18.join(absoluteDir, "src", "app");
|
|
5594
|
+
const srcPagesDirPath = path18.join(absoluteDir, "src", "pages");
|
|
4549
5595
|
let targetPath = "";
|
|
4550
5596
|
let routerType = "";
|
|
4551
5597
|
let content = "";
|
|
4552
|
-
if (
|
|
4553
|
-
targetPath =
|
|
5598
|
+
if (fs12.existsSync(appDirPath)) {
|
|
5599
|
+
targetPath = path18.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
|
|
4554
5600
|
routerType = "Next.js App Router (root)";
|
|
4555
|
-
} else if (
|
|
4556
|
-
targetPath =
|
|
5601
|
+
} else if (fs12.existsSync(srcAppDirPath)) {
|
|
5602
|
+
targetPath = path18.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
|
|
4557
5603
|
routerType = "Next.js App Router (src/)";
|
|
4558
|
-
} else if (
|
|
4559
|
-
targetPath =
|
|
5604
|
+
} else if (fs12.existsSync(pagesDirPath)) {
|
|
5605
|
+
targetPath = path18.join(pagesDirPath, "api", `livelocale.${fileExt}`);
|
|
4560
5606
|
routerType = "Next.js Pages Router (root)";
|
|
4561
|
-
} else if (
|
|
4562
|
-
targetPath =
|
|
5607
|
+
} else if (fs12.existsSync(srcPagesDirPath)) {
|
|
5608
|
+
targetPath = path18.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
|
|
4563
5609
|
routerType = "Next.js Pages Router (src/)";
|
|
4564
5610
|
} else {
|
|
4565
|
-
targetPath =
|
|
5611
|
+
targetPath = path18.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
|
|
4566
5612
|
routerType = "Default App Router (app/ not found)";
|
|
4567
5613
|
}
|
|
4568
5614
|
console.log(`\u{1F4CD} Project structure: ${routerType}`);
|
|
@@ -4590,14 +5636,14 @@ const handler = createLiveLocaleHandler({
|
|
|
4590
5636
|
export default handler;
|
|
4591
5637
|
`;
|
|
4592
5638
|
}
|
|
4593
|
-
const targetDir =
|
|
4594
|
-
if (!
|
|
4595
|
-
|
|
5639
|
+
const targetDir = path18.dirname(targetPath);
|
|
5640
|
+
if (!fs12.existsSync(targetDir)) {
|
|
5641
|
+
fs12.mkdirSync(targetDir, { recursive: true });
|
|
4596
5642
|
}
|
|
4597
5643
|
try {
|
|
4598
|
-
|
|
5644
|
+
fs12.writeFileSync(targetPath, content, "utf8");
|
|
4599
5645
|
console.log(`\u2705 Created API Route Handler successfully at:`);
|
|
4600
|
-
console.log(` ${
|
|
5646
|
+
console.log(` ${path18.relative(absoluteDir, targetPath)}`);
|
|
4601
5647
|
if (hasNextIntl) {
|
|
4602
5648
|
console.log(`
|
|
4603
5649
|
\u{1F4A1} Detected 'next-intl' in your project!`);
|
|
@@ -4639,8 +5685,8 @@ async function runMigrate(args) {
|
|
|
4639
5685
|
}
|
|
4640
5686
|
}
|
|
4641
5687
|
}
|
|
4642
|
-
const absoluteDir =
|
|
4643
|
-
if (!
|
|
5688
|
+
const absoluteDir = path18.resolve(dir);
|
|
5689
|
+
if (!fs12.existsSync(absoluteDir)) {
|
|
4644
5690
|
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
4645
5691
|
process.exit(1);
|
|
4646
5692
|
}
|
|
@@ -4664,28 +5710,28 @@ async function runMigrate(args) {
|
|
|
4664
5710
|
let scannedCount = 0;
|
|
4665
5711
|
let migratedCount = 0;
|
|
4666
5712
|
async function walk(currentDir) {
|
|
4667
|
-
const files =
|
|
5713
|
+
const files = fs12.readdirSync(currentDir, { withFileTypes: true });
|
|
4668
5714
|
for (const file of files) {
|
|
4669
|
-
const fullPath =
|
|
5715
|
+
const fullPath = path18.join(currentDir, file.name);
|
|
4670
5716
|
if (file.isDirectory()) {
|
|
4671
5717
|
if (excludes.includes(file.name)) {
|
|
4672
5718
|
continue;
|
|
4673
5719
|
}
|
|
4674
5720
|
await walk(fullPath);
|
|
4675
5721
|
} else if (file.isFile()) {
|
|
4676
|
-
const ext =
|
|
5722
|
+
const ext = path18.extname(file.name);
|
|
4677
5723
|
if ([".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
|
|
4678
5724
|
scannedCount++;
|
|
4679
5725
|
try {
|
|
4680
|
-
const content =
|
|
5726
|
+
const content = fs12.readFileSync(fullPath, "utf8");
|
|
4681
5727
|
const result = await transformReactI18next2(content, fullPath);
|
|
4682
5728
|
if (result.modified) {
|
|
4683
5729
|
migratedCount++;
|
|
4684
|
-
const relativePath =
|
|
5730
|
+
const relativePath = path18.relative(absoluteDir, fullPath);
|
|
4685
5731
|
if (dryRun) {
|
|
4686
5732
|
console.log(`[DRY RUN] Would migrate: ${relativePath}`);
|
|
4687
5733
|
} else {
|
|
4688
|
-
|
|
5734
|
+
fs12.writeFileSync(fullPath, result.code, "utf8");
|
|
4689
5735
|
console.log(`\u2705 Migrated: ${relativePath}`);
|
|
4690
5736
|
}
|
|
4691
5737
|
}
|
|
@@ -4734,8 +5780,8 @@ async function runDoctorCommand(args) {
|
|
|
4734
5780
|
dir = arg;
|
|
4735
5781
|
}
|
|
4736
5782
|
}
|
|
4737
|
-
const projectDir =
|
|
4738
|
-
if (!
|
|
5783
|
+
const projectDir = path18.resolve(dir);
|
|
5784
|
+
if (!fs12.existsSync(projectDir)) {
|
|
4739
5785
|
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
4740
5786
|
process.exit(1);
|
|
4741
5787
|
}
|
|
@@ -4796,8 +5842,8 @@ async function runCheckCommand(args) {
|
|
|
4796
5842
|
dir = arg;
|
|
4797
5843
|
}
|
|
4798
5844
|
}
|
|
4799
|
-
const projectDir =
|
|
4800
|
-
if (!
|
|
5845
|
+
const projectDir = path18.resolve(dir);
|
|
5846
|
+
if (!fs12.existsSync(projectDir)) {
|
|
4801
5847
|
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
4802
5848
|
process.exit(1);
|
|
4803
5849
|
}
|
|
@@ -4838,7 +5884,7 @@ async function runCheckCommand(args) {
|
|
|
4838
5884
|
var PACK_USAGE = "Usage: livelocale pack <outDir> [options]";
|
|
4839
5885
|
function printPackSummary(result, projectDir, warnings) {
|
|
4840
5886
|
for (const warning of warnings) console.log(`\u26A0\uFE0F ${warning}`);
|
|
4841
|
-
const outRel =
|
|
5887
|
+
const outRel = path18.relative(projectDir, path18.dirname(result.shareDir)).split(path18.sep).join("/") || ".";
|
|
4842
5888
|
console.log(
|
|
4843
5889
|
`\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).`
|
|
4844
5890
|
);
|
|
@@ -4850,7 +5896,7 @@ async function runPackCommand(args) {
|
|
|
4850
5896
|
if (options.positional.length !== 1 || options.out !== void 0 || options.preview || options.command.length > 0) {
|
|
4851
5897
|
throw new PackError("USAGE", PACK_USAGE);
|
|
4852
5898
|
}
|
|
4853
|
-
const projectDir =
|
|
5899
|
+
const projectDir = path18.resolve(options.dir);
|
|
4854
5900
|
const result = await packOutput({
|
|
4855
5901
|
projectDir,
|
|
4856
5902
|
outDir: options.positional[0],
|
|
@@ -4873,7 +5919,7 @@ async function runBuildCli(args) {
|
|
|
4873
5919
|
if (options.positional.length > 0 || !options.inject) {
|
|
4874
5920
|
throw new PackError("USAGE", BUILD_USAGE);
|
|
4875
5921
|
}
|
|
4876
|
-
const projectDir =
|
|
5922
|
+
const projectDir = path18.resolve(options.dir);
|
|
4877
5923
|
const result = await runBuildCommand({
|
|
4878
5924
|
projectDir,
|
|
4879
5925
|
command: options.command,
|
|
@@ -4896,6 +5942,55 @@ async function runBuildCli(args) {
|
|
|
4896
5942
|
process.exitCode = 1;
|
|
4897
5943
|
}
|
|
4898
5944
|
}
|
|
5945
|
+
async function runInitShareCommand(args) {
|
|
5946
|
+
let dir = ".";
|
|
5947
|
+
let localesPath;
|
|
5948
|
+
let sourceLocale;
|
|
5949
|
+
let branches;
|
|
5950
|
+
let overwrite = false;
|
|
5951
|
+
for (let i = 1; i < args.length; i++) {
|
|
5952
|
+
const arg = args[i];
|
|
5953
|
+
if (arg === "--dir") {
|
|
5954
|
+
dir = args[++i] || ".";
|
|
5955
|
+
} else if (arg === "--locales") {
|
|
5956
|
+
localesPath = args[++i];
|
|
5957
|
+
} else if (arg === "--source") {
|
|
5958
|
+
sourceLocale = args[++i];
|
|
5959
|
+
} else if (arg === "--branches") {
|
|
5960
|
+
branches = (args[++i] ?? "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
5961
|
+
} else if (arg === "--overwrite") {
|
|
5962
|
+
overwrite = true;
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
const projectDir = path18.resolve(dir);
|
|
5966
|
+
if (!fs12.existsSync(projectDir)) {
|
|
5967
|
+
console.error(`Error: Directory "${dir}" does not exist.`);
|
|
5968
|
+
process.exit(1);
|
|
5969
|
+
}
|
|
5970
|
+
try {
|
|
5971
|
+
const plan = await planInitShare({
|
|
5972
|
+
projectDir,
|
|
5973
|
+
localesPath,
|
|
5974
|
+
sourceLocale,
|
|
5975
|
+
branches,
|
|
5976
|
+
version: readOwnVersion(),
|
|
5977
|
+
overwrite
|
|
5978
|
+
});
|
|
5979
|
+
writePlannedFiles(projectDir, plan.files);
|
|
5980
|
+
console.log(`
|
|
5981
|
+
\u{1F680} LiveLocale share link setup (${plan.stack})`);
|
|
5982
|
+
for (const file of plan.files) {
|
|
5983
|
+
console.log(
|
|
5984
|
+
` ${file.action.padEnd(9)} ${file.path}${file.reason ? ` (${file.reason})` : ""}`
|
|
5985
|
+
);
|
|
5986
|
+
}
|
|
5987
|
+
for (const note of plan.notes) console.log(`\u{1F4A1} ${note}`);
|
|
5988
|
+
console.log("Commit and push: the workflow must be on the default branch.");
|
|
5989
|
+
} catch (err) {
|
|
5990
|
+
console.error(`\u274C ${err.message}`);
|
|
5991
|
+
process.exitCode = 1;
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
4899
5994
|
async function run2() {
|
|
4900
5995
|
const args = process.argv.slice(2);
|
|
4901
5996
|
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
@@ -4928,6 +6023,10 @@ async function run2() {
|
|
|
4928
6023
|
await runApplyCommand(args.slice(1));
|
|
4929
6024
|
return;
|
|
4930
6025
|
}
|
|
6026
|
+
if (args[0] === "init" && args.includes("--share")) {
|
|
6027
|
+
await runInitShareCommand(args);
|
|
6028
|
+
return;
|
|
6029
|
+
}
|
|
4931
6030
|
if (args[0] === "init") {
|
|
4932
6031
|
let dir = ".";
|
|
4933
6032
|
for (let i = 1; i < args.length; i++) {
|