livelocale 0.8.0 → 0.9.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 +230 -5
- package/dist/client/index.cjs +178 -70
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +36 -22
- package/dist/client/index.d.ts +36 -22
- package/dist/client/index.js +177 -70
- package/dist/client/index.js.map +1 -1
- package/dist/codemod/index.cjs +3057 -152
- package/dist/codemod/index.cjs.map +1 -1
- package/dist/codemod/index.js +3058 -153
- package/dist/codemod/index.js.map +1 -1
- package/dist/react/index.cjs +185 -77
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +185 -77
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +108 -66
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +107 -66
- package/dist/server/index.js.map +1 -1
- package/dist/studio/assets/index-Bvbtkry-.css +1 -0
- package/dist/studio/assets/index-DC_jRsox.js +13 -0
- package/dist/studio/assets/inter-latin-300-normal-BVlfKGgI.woff2 +0 -0
- package/dist/studio/assets/inter-latin-300-normal-i8F0SvXL.woff +0 -0
- package/dist/studio/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist/studio/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist/studio/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist/studio/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist/studio/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist/studio/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist/studio/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist/studio/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
- package/dist/studio/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
- package/dist/studio/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
- package/dist/studio/assets/outfit-latin-700-normal-Cu9v6i1X.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-700-normal-D4itBLBr.woff +0 -0
- package/dist/studio/index.html +3 -6
- package/dist/vite/index.cjs +739 -77
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.d.cts +22 -2
- package/dist/vite/index.d.ts +22 -2
- package/dist/vite/index.js +738 -77
- package/dist/vite/index.js.map +1 -1
- package/dist/{wrap-CsOtHp8r.d.cts → wrap-2qVxLN9Q.d.cts} +10 -18
- package/dist/{wrap-CsOtHp8r.d.ts → wrap-2qVxLN9Q.d.ts} +10 -18
- package/package.json +4 -1
- package/dist/studio/assets/index-D7ifnqy5.js +0 -10
- package/dist/studio/assets/index-Zh0KtFUH.css +0 -2
package/dist/vite/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/server/vite-plugin.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as path9 from "path";
|
|
3
|
+
import * as fs5 from "fs";
|
|
4
4
|
|
|
5
5
|
// src/server/file-mutator.ts
|
|
6
6
|
import { promises as fs2 } from "fs";
|
|
@@ -526,14 +526,6 @@ var KeyScanner = class {
|
|
|
526
526
|
};
|
|
527
527
|
|
|
528
528
|
// src/server/file-mutator.ts
|
|
529
|
-
function assertEnabled(component) {
|
|
530
|
-
const reason = getDisabledReason();
|
|
531
|
-
if (reason !== null) {
|
|
532
|
-
throw new Error(
|
|
533
|
-
`[livelocale] ${component} can only be executed in development mode (${reason}).`
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
529
|
var FileMutex = class {
|
|
538
530
|
queue = Promise.resolve();
|
|
539
531
|
async acquire(fn) {
|
|
@@ -549,7 +541,23 @@ var FileMutex = class {
|
|
|
549
541
|
};
|
|
550
542
|
var FileMutator = class {
|
|
551
543
|
fileLocks = /* @__PURE__ */ new Map();
|
|
552
|
-
|
|
544
|
+
requireEnabled;
|
|
545
|
+
quiet;
|
|
546
|
+
scanner;
|
|
547
|
+
constructor(options = {}) {
|
|
548
|
+
this.requireEnabled = options.requireEnabled ?? true;
|
|
549
|
+
this.quiet = options.quiet ?? false;
|
|
550
|
+
this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
|
|
551
|
+
}
|
|
552
|
+
assertEnabled() {
|
|
553
|
+
if (!this.requireEnabled) return;
|
|
554
|
+
const reason = getDisabledReason();
|
|
555
|
+
if (reason !== null) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`[livelocale] FileMutator can only be executed in development mode (${reason}).`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
553
561
|
/**
|
|
554
562
|
* Helper to serialize execution per file path
|
|
555
563
|
*/
|
|
@@ -567,7 +575,7 @@ var FileMutator = class {
|
|
|
567
575
|
* structural JSON validation, and concurrency serialization.
|
|
568
576
|
*/
|
|
569
577
|
async updateLocaleKey(basePath, locale, keyPath, newValue) {
|
|
570
|
-
assertEnabled(
|
|
578
|
+
this.assertEnabled();
|
|
571
579
|
if (!isSafeLocaleSegment(locale)) {
|
|
572
580
|
throw new Error(
|
|
573
581
|
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
@@ -670,7 +678,9 @@ var FileMutator = class {
|
|
|
670
678
|
`[livelocale] Failed to rename temp file to target at ${filePath}. Operation: rename. Original error: ${err.message}`
|
|
671
679
|
);
|
|
672
680
|
}
|
|
673
|
-
|
|
681
|
+
if (!this.quiet) {
|
|
682
|
+
console.log(`[livelocale] Updated key '${keyPath}' in locale '${locale}'`);
|
|
683
|
+
}
|
|
674
684
|
});
|
|
675
685
|
}
|
|
676
686
|
/**
|
|
@@ -678,7 +688,7 @@ var FileMutator = class {
|
|
|
678
688
|
* `*.json` inside the `{locale}/` folder merged under its namespace.
|
|
679
689
|
*/
|
|
680
690
|
async readLocale(basePath, locale) {
|
|
681
|
-
assertEnabled(
|
|
691
|
+
this.assertEnabled();
|
|
682
692
|
const localePath = path3.resolve(basePath, locale);
|
|
683
693
|
let isDir = false;
|
|
684
694
|
try {
|
|
@@ -705,7 +715,7 @@ var FileMutator = class {
|
|
|
705
715
|
* Lists all locale codes dynamically scanned from the locales directory.
|
|
706
716
|
*/
|
|
707
717
|
async listLocales(basePath) {
|
|
708
|
-
assertEnabled(
|
|
718
|
+
this.assertEnabled();
|
|
709
719
|
const resolvedBasePath = path3.resolve(basePath);
|
|
710
720
|
const items = await fs2.readdir(resolvedBasePath, { withFileTypes: true });
|
|
711
721
|
const localesSet = /* @__PURE__ */ new Set();
|
|
@@ -727,7 +737,7 @@ var FileMutator = class {
|
|
|
727
737
|
* Adds a new locale directory/file based on current repository structure.
|
|
728
738
|
*/
|
|
729
739
|
async addLocale(basePath, locale) {
|
|
730
|
-
assertEnabled(
|
|
740
|
+
this.assertEnabled();
|
|
731
741
|
if (!isSafeLocaleSegment(locale)) {
|
|
732
742
|
throw new Error(
|
|
733
743
|
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
@@ -783,7 +793,7 @@ var FileMutator = class {
|
|
|
783
793
|
* Renames an existing locale (directory or file) to a new name.
|
|
784
794
|
*/
|
|
785
795
|
async renameLocale(basePath, oldLocale, newLocale) {
|
|
786
|
-
assertEnabled(
|
|
796
|
+
this.assertEnabled();
|
|
787
797
|
if (!isSafeLocaleSegment(oldLocale) || !isSafeLocaleSegment(newLocale)) {
|
|
788
798
|
throw new Error("[livelocale] Path traversal attempt detected");
|
|
789
799
|
}
|
|
@@ -841,7 +851,7 @@ var FileMutator = class {
|
|
|
841
851
|
* Deletes an existing locale (directory or file).
|
|
842
852
|
*/
|
|
843
853
|
async deleteLocale(basePath, locale) {
|
|
844
|
-
assertEnabled(
|
|
854
|
+
this.assertEnabled();
|
|
845
855
|
if (!isSafeLocaleSegment(locale)) {
|
|
846
856
|
throw new Error("[livelocale] Path traversal attempt detected");
|
|
847
857
|
}
|
|
@@ -1204,6 +1214,67 @@ function validateMessageSyntax(message) {
|
|
|
1204
1214
|
return parseIcu(prepared).error;
|
|
1205
1215
|
}
|
|
1206
1216
|
|
|
1217
|
+
// src/core/save-rules.ts
|
|
1218
|
+
var reject = (status, code, error) => ({
|
|
1219
|
+
ok: false,
|
|
1220
|
+
status,
|
|
1221
|
+
code,
|
|
1222
|
+
error
|
|
1223
|
+
});
|
|
1224
|
+
function validateWrite(input, options = {}) {
|
|
1225
|
+
const { locale, key, value } = input;
|
|
1226
|
+
if (!isValidLocaleFormat(locale)) {
|
|
1227
|
+
return reject(400, "INVALID_LOCALE_FORMAT", "Invalid locale string parameter.");
|
|
1228
|
+
}
|
|
1229
|
+
if (!isValidKeyFormat(key)) {
|
|
1230
|
+
return reject(
|
|
1231
|
+
400,
|
|
1232
|
+
"INVALID_KEY_FORMAT",
|
|
1233
|
+
"Invalid key path. Must be 1-200 characters containing alphanumeric, dots, underscores, or hyphens."
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (!isSafeKeyPath(key)) {
|
|
1237
|
+
return reject(
|
|
1238
|
+
400,
|
|
1239
|
+
"INVALID_KEY_FORMAT",
|
|
1240
|
+
"Forbidden key path: segments like __proto__, constructor, or prototype are not allowed."
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
if (!isValidValueFormat(value)) {
|
|
1244
|
+
return reject(
|
|
1245
|
+
400,
|
|
1246
|
+
"INVALID_VALUE_FORMAT",
|
|
1247
|
+
"Invalid value. Must be a string less than 10000 characters."
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
const accepted = {
|
|
1251
|
+
ok: true,
|
|
1252
|
+
locale,
|
|
1253
|
+
key,
|
|
1254
|
+
value
|
|
1255
|
+
};
|
|
1256
|
+
if (options.force === true) return accepted;
|
|
1257
|
+
const syntaxError = validateMessageSyntax(accepted.value);
|
|
1258
|
+
if (syntaxError) {
|
|
1259
|
+
return reject(422, "MESSAGE_SYNTAX", `Message syntax: ${syntaxError}.`);
|
|
1260
|
+
}
|
|
1261
|
+
const comparison = comparePlaceholders(options.reference, accepted.value);
|
|
1262
|
+
if (!comparison.ok) {
|
|
1263
|
+
const parts = [];
|
|
1264
|
+
if (comparison.missing.length) parts.push(`missing ${comparison.missing.join(", ")}`);
|
|
1265
|
+
if (comparison.extra.length) parts.push(`unexpected ${comparison.extra.join(", ")}`);
|
|
1266
|
+
return {
|
|
1267
|
+
...reject(
|
|
1268
|
+
422,
|
|
1269
|
+
"PLACEHOLDER_MISMATCH",
|
|
1270
|
+
`Placeholders differ from the reference: ${parts.join("; ")}.`
|
|
1271
|
+
),
|
|
1272
|
+
details: { missing: comparison.missing, extra: comparison.extra }
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
return accepted;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1207
1278
|
// src/server/router.ts
|
|
1208
1279
|
var DEFAULT_ALLOWED_ORIGINS = ["http://localhost:3010", "http://127.0.0.1:3010"];
|
|
1209
1280
|
var MAX_BODY_BYTES = 1e6;
|
|
@@ -1391,53 +1462,18 @@ function createMutationRouter(mutator, options) {
|
|
|
1391
1462
|
await mutator.deleteLocale(basePath, locale);
|
|
1392
1463
|
return json(200, headers, { success: true, locale });
|
|
1393
1464
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
code: "INVALID_KEY_FORMAT"
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
if (!isSafeKeyPath(key)) {
|
|
1407
|
-
return json(400, headers, {
|
|
1408
|
-
error: "Forbidden key path: segments like __proto__, constructor, or prototype are not allowed.",
|
|
1409
|
-
code: "INVALID_KEY_FORMAT"
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
if (!isValidValueFormat(value)) {
|
|
1413
|
-
return json(400, headers, {
|
|
1414
|
-
error: "Invalid value. Must be a string less than 10000 characters.",
|
|
1415
|
-
code: "INVALID_VALUE_FORMAT"
|
|
1465
|
+
const verdict = validateWrite(
|
|
1466
|
+
{ locale, key, value },
|
|
1467
|
+
{ reference: typeof reference === "string" ? reference : null, force: force === true }
|
|
1468
|
+
);
|
|
1469
|
+
if (!verdict.ok) {
|
|
1470
|
+
return json(verdict.status, headers, {
|
|
1471
|
+
error: verdict.error,
|
|
1472
|
+
code: verdict.code,
|
|
1473
|
+
details: verdict.details
|
|
1416
1474
|
});
|
|
1417
1475
|
}
|
|
1418
|
-
|
|
1419
|
-
const syntaxError = validateMessageSyntax(value);
|
|
1420
|
-
if (syntaxError) {
|
|
1421
|
-
return json(422, headers, {
|
|
1422
|
-
error: `Message syntax: ${syntaxError}.`,
|
|
1423
|
-
code: "MESSAGE_SYNTAX"
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
if (typeof reference === "string") {
|
|
1427
|
-
const comparison = comparePlaceholders(reference, value);
|
|
1428
|
-
if (!comparison.ok) {
|
|
1429
|
-
const parts = [];
|
|
1430
|
-
if (comparison.missing.length) parts.push(`missing ${comparison.missing.join(", ")}`);
|
|
1431
|
-
if (comparison.extra.length) parts.push(`unexpected ${comparison.extra.join(", ")}`);
|
|
1432
|
-
return json(422, headers, {
|
|
1433
|
-
error: `Placeholders differ from the reference: ${parts.join("; ")}.`,
|
|
1434
|
-
code: "PLACEHOLDER_MISMATCH",
|
|
1435
|
-
details: { missing: comparison.missing, extra: comparison.extra }
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
await mutator.updateLocaleKey(basePath, locale, key, value);
|
|
1476
|
+
await mutator.updateLocaleKey(basePath, verdict.locale, verdict.key, verdict.value);
|
|
1441
1477
|
return json(200, headers, { success: true, key, locale });
|
|
1442
1478
|
}
|
|
1443
1479
|
return {
|
|
@@ -1511,7 +1547,7 @@ var BodyTooLargeError = class extends Error {
|
|
|
1511
1547
|
}
|
|
1512
1548
|
};
|
|
1513
1549
|
function readNodeBody(req, maxBytes) {
|
|
1514
|
-
return new Promise((
|
|
1550
|
+
return new Promise((resolve8, reject2) => {
|
|
1515
1551
|
let bodyText = "";
|
|
1516
1552
|
let settled = false;
|
|
1517
1553
|
req.on("data", (chunk) => {
|
|
@@ -1519,19 +1555,19 @@ function readNodeBody(req, maxBytes) {
|
|
|
1519
1555
|
bodyText += chunk.toString();
|
|
1520
1556
|
if (Buffer.byteLength(bodyText, "utf-8") > maxBytes) {
|
|
1521
1557
|
settled = true;
|
|
1522
|
-
|
|
1558
|
+
reject2(new BodyTooLargeError());
|
|
1523
1559
|
req.destroy?.();
|
|
1524
1560
|
}
|
|
1525
1561
|
});
|
|
1526
1562
|
req.on("end", () => {
|
|
1527
1563
|
if (settled) return;
|
|
1528
1564
|
settled = true;
|
|
1529
|
-
|
|
1565
|
+
resolve8(bodyText);
|
|
1530
1566
|
});
|
|
1531
1567
|
req.on("error", (err) => {
|
|
1532
1568
|
if (settled) return;
|
|
1533
1569
|
settled = true;
|
|
1534
|
-
|
|
1570
|
+
reject2(err);
|
|
1535
1571
|
});
|
|
1536
1572
|
});
|
|
1537
1573
|
}
|
|
@@ -1551,6 +1587,590 @@ function deepMerge(target, source) {
|
|
|
1551
1587
|
return result;
|
|
1552
1588
|
}
|
|
1553
1589
|
|
|
1590
|
+
// src/server/pack.ts
|
|
1591
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync, promises as fs4 } from "fs";
|
|
1592
|
+
import * as path8 from "path";
|
|
1593
|
+
|
|
1594
|
+
// src/core/flag-script.ts
|
|
1595
|
+
var FLAG_SCRIPT_SIGNATURE = "/*! livelocale flag v1 */";
|
|
1596
|
+
var FLAG_SCRIPT = `${FLAG_SCRIPT_SIGNATURE}
|
|
1597
|
+
(function () {
|
|
1598
|
+
try {
|
|
1599
|
+
var me = document.currentScript;
|
|
1600
|
+
if (!me || !me.src || window.parent === window || window.__LIVELOCALE_FLAG__ !== undefined)
|
|
1601
|
+
return;
|
|
1602
|
+
var studio = new URL('.', me.src);
|
|
1603
|
+
var host = window.parent.location;
|
|
1604
|
+
if (
|
|
1605
|
+
host.origin === 'null' ||
|
|
1606
|
+
host.origin !== window.location.origin ||
|
|
1607
|
+
host.origin !== studio.origin
|
|
1608
|
+
)
|
|
1609
|
+
return;
|
|
1610
|
+
if (host.pathname.indexOf(studio.pathname) !== 0) return;
|
|
1611
|
+
window.__LIVELOCALE_FLAG__ = 'preview';
|
|
1612
|
+
} catch (e) {}
|
|
1613
|
+
})();
|
|
1614
|
+
`;
|
|
1615
|
+
|
|
1616
|
+
// src/core/change-link.ts
|
|
1617
|
+
function isValidRepoSlug(slug) {
|
|
1618
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/.test(slug)) return false;
|
|
1619
|
+
const repo = slug.slice(slug.indexOf("/") + 1);
|
|
1620
|
+
return repo !== "." && repo !== "..";
|
|
1621
|
+
}
|
|
1622
|
+
var BRANCH_NAME_RE = /^(?!-)(?!.*\.\.)[^\s\x00-\x1f\x7f]{1,255}$/;
|
|
1623
|
+
function isValidBranchName(name) {
|
|
1624
|
+
return BRANCH_NAME_RE.test(name) && !hasLoneSurrogate(name);
|
|
1625
|
+
}
|
|
1626
|
+
var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
1627
|
+
function hasLoneSurrogate(text) {
|
|
1628
|
+
return typeof text === "string" && LONE_SURROGATE_RE.test(text);
|
|
1629
|
+
}
|
|
1630
|
+
var MAX_INFLATED_BYTES = 512 * 1024;
|
|
1631
|
+
var TOO_LARGE_MESSAGE = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
|
|
1632
|
+
|
|
1633
|
+
// src/core/share-manifest.ts
|
|
1634
|
+
var SHARE_DIR = "livelocale";
|
|
1635
|
+
var SHARE_MANIFEST_FILE = "manifest.json";
|
|
1636
|
+
var SHARE_FLAG_FILE = "flag.js";
|
|
1637
|
+
var SHARE_LOCALES_DIR = "locales";
|
|
1638
|
+
var SHARE_MANIFEST_FORMAT = 1;
|
|
1639
|
+
|
|
1640
|
+
// src/server/build-info.ts
|
|
1641
|
+
import { execFile as execFile2 } from "child_process";
|
|
1642
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1643
|
+
import * as path5 from "path";
|
|
1644
|
+
var runGit = (args, cwd) => new Promise((resolve8) => {
|
|
1645
|
+
try {
|
|
1646
|
+
execFile2(
|
|
1647
|
+
"git",
|
|
1648
|
+
args,
|
|
1649
|
+
{ cwd, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
|
|
1650
|
+
(error, stdout) => resolve8(error ? null : String(stdout).trim())
|
|
1651
|
+
);
|
|
1652
|
+
} catch {
|
|
1653
|
+
resolve8(null);
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
var SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
|
|
1657
|
+
async function firstValid(candidates, accept) {
|
|
1658
|
+
for (const candidate of candidates) {
|
|
1659
|
+
const value = typeof candidate === "function" ? await candidate() : candidate;
|
|
1660
|
+
if (typeof value !== "string" || value === "") continue;
|
|
1661
|
+
const accepted = accept(value);
|
|
1662
|
+
if (accepted !== null) return accepted;
|
|
1663
|
+
}
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
var asBranch = (value) => isValidBranchName(value) ? value : null;
|
|
1667
|
+
var asSha = (value) => SHA_RE.test(value) ? value.toLowerCase() : null;
|
|
1668
|
+
var asSlug = (value) => isValidRepoSlug(value) ? value : null;
|
|
1669
|
+
function readEvent(eventPath) {
|
|
1670
|
+
if (!eventPath) return {};
|
|
1671
|
+
try {
|
|
1672
|
+
const parsed = JSON.parse(readFileSync2(eventPath, "utf-8"));
|
|
1673
|
+
return isRecord(parsed) ? parsed : {};
|
|
1674
|
+
} catch {
|
|
1675
|
+
return {};
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function isRecord(value) {
|
|
1679
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1680
|
+
}
|
|
1681
|
+
function pick(obj, ...keys) {
|
|
1682
|
+
let current = obj;
|
|
1683
|
+
for (const key of keys) {
|
|
1684
|
+
if (!isRecord(current)) return void 0;
|
|
1685
|
+
current = current[key];
|
|
1686
|
+
}
|
|
1687
|
+
return typeof current === "string" ? current : void 0;
|
|
1688
|
+
}
|
|
1689
|
+
async function resolveBuildInfo(options) {
|
|
1690
|
+
const { cwd, env = process.env, git = runGit, includeRepo = false } = options;
|
|
1691
|
+
const event = readEvent(env.GITHUB_EVENT_PATH);
|
|
1692
|
+
const isPullRequest = env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target";
|
|
1693
|
+
const netlify = env.NETLIFY === "true";
|
|
1694
|
+
const gitlab = env.GITLAB_CI === "true";
|
|
1695
|
+
const branch = await firstValid(
|
|
1696
|
+
[
|
|
1697
|
+
env.GITHUB_HEAD_REF,
|
|
1698
|
+
env.GITHUB_REF_TYPE === "branch" ? env.GITHUB_REF_NAME : null,
|
|
1699
|
+
env.VERCEL_GIT_COMMIT_REF,
|
|
1700
|
+
env.CF_PAGES_BRANCH,
|
|
1701
|
+
netlify ? env.BRANCH : null,
|
|
1702
|
+
gitlab ? env.CI_COMMIT_REF_NAME : null,
|
|
1703
|
+
async () => {
|
|
1704
|
+
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
1705
|
+
return head === "HEAD" ? null : head;
|
|
1706
|
+
}
|
|
1707
|
+
],
|
|
1708
|
+
asBranch
|
|
1709
|
+
);
|
|
1710
|
+
const base = await firstValid(
|
|
1711
|
+
[
|
|
1712
|
+
isPullRequest ? pick(event, "pull_request", "head", "sha") : null,
|
|
1713
|
+
env.GITHUB_SHA,
|
|
1714
|
+
env.VERCEL_GIT_COMMIT_SHA,
|
|
1715
|
+
env.CF_PAGES_COMMIT_SHA,
|
|
1716
|
+
netlify ? env.COMMIT_REF : null,
|
|
1717
|
+
gitlab ? env.CI_COMMIT_SHA : null,
|
|
1718
|
+
() => git(["rev-parse", "HEAD"], cwd)
|
|
1719
|
+
],
|
|
1720
|
+
asSha
|
|
1721
|
+
);
|
|
1722
|
+
const defaultBranch = await firstValid(
|
|
1723
|
+
[
|
|
1724
|
+
pick(event, "repository", "default_branch"),
|
|
1725
|
+
async () => {
|
|
1726
|
+
const ref = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
1727
|
+
return ref?.startsWith("origin/") ? ref.slice("origin/".length) : null;
|
|
1728
|
+
}
|
|
1729
|
+
],
|
|
1730
|
+
asBranch
|
|
1731
|
+
);
|
|
1732
|
+
const githubCom = env.GITHUB_SERVER_URL === void 0 || env.GITHUB_SERVER_URL === "" || env.GITHUB_SERVER_URL === "https://github.com";
|
|
1733
|
+
const repo = includeRepo ? await firstValid(
|
|
1734
|
+
[
|
|
1735
|
+
githubCom ? env.GITHUB_REPOSITORY : null,
|
|
1736
|
+
env.VERCEL_GIT_PROVIDER === "github" && env.VERCEL_GIT_REPO_OWNER && env.VERCEL_GIT_REPO_SLUG ? `${env.VERCEL_GIT_REPO_OWNER}/${env.VERCEL_GIT_REPO_SLUG}` : null,
|
|
1737
|
+
netlify && env.REPOSITORY_URL ? parseGitHubSlug(env.REPOSITORY_URL) : null,
|
|
1738
|
+
async () => {
|
|
1739
|
+
const remote = await git(["remote", "get-url", "origin"], cwd);
|
|
1740
|
+
return remote ? parseGitHubSlug(remote) : null;
|
|
1741
|
+
}
|
|
1742
|
+
],
|
|
1743
|
+
asSlug
|
|
1744
|
+
) : null;
|
|
1745
|
+
const top = await git(["rev-parse", "--show-toplevel"], cwd);
|
|
1746
|
+
const gitRoot = top ? path5.resolve(top) : null;
|
|
1747
|
+
return { repo, defaultBranch, branch, base, gitRoot };
|
|
1748
|
+
}
|
|
1749
|
+
function parseGitHubSlug(remote) {
|
|
1750
|
+
const text = remote.trim();
|
|
1751
|
+
let repoPath;
|
|
1752
|
+
const scp = /^[A-Za-z0-9._-]+@github\.com:(.*)$/i.exec(text);
|
|
1753
|
+
if (scp) {
|
|
1754
|
+
repoPath = scp[1] ?? "";
|
|
1755
|
+
} else {
|
|
1756
|
+
let url;
|
|
1757
|
+
try {
|
|
1758
|
+
url = new URL(text);
|
|
1759
|
+
} catch {
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
|
|
1763
|
+
if (url.hostname.toLowerCase() !== "github.com") return null;
|
|
1764
|
+
repoPath = url.pathname;
|
|
1765
|
+
}
|
|
1766
|
+
const trimmed = repoPath.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
1767
|
+
return isValidRepoSlug(trimmed) ? trimmed : null;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/server/build-target.ts
|
|
1771
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1772
|
+
import * as path6 from "path";
|
|
1773
|
+
var NEXT_CONFIG_FILES = [
|
|
1774
|
+
"next.config.js",
|
|
1775
|
+
"next.config.mjs",
|
|
1776
|
+
"next.config.cjs",
|
|
1777
|
+
"next.config.ts",
|
|
1778
|
+
"next.config.mts"
|
|
1779
|
+
];
|
|
1780
|
+
var VITE_CONFIG_FILES = [
|
|
1781
|
+
"vite.config.js",
|
|
1782
|
+
"vite.config.mjs",
|
|
1783
|
+
"vite.config.cjs",
|
|
1784
|
+
"vite.config.ts",
|
|
1785
|
+
"vite.config.mts"
|
|
1786
|
+
];
|
|
1787
|
+
var OUTPUT_EXPORT = /\boutput\s*:\s*['"`]export['"`]/;
|
|
1788
|
+
var BASE_PATH = /\bbasePath\s*:\s*['"`](\/[^'"`]*)['"`]/;
|
|
1789
|
+
var VITE_BASE = /\bbase\s*:\s*['"`](\/[^'"`]*|\.\/?|)['"`]/;
|
|
1790
|
+
function readText(file) {
|
|
1791
|
+
try {
|
|
1792
|
+
return readFileSync3(file, "utf-8");
|
|
1793
|
+
} catch {
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function readPackageJson(projectDir) {
|
|
1798
|
+
const text = readText(path6.join(projectDir, "package.json"));
|
|
1799
|
+
if (text === null) return {};
|
|
1800
|
+
try {
|
|
1801
|
+
const parsed = JSON.parse(text);
|
|
1802
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1803
|
+
} catch {
|
|
1804
|
+
return {};
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function hasDependency(pkg, name) {
|
|
1808
|
+
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
1809
|
+
}
|
|
1810
|
+
function firstExisting(projectDir, names) {
|
|
1811
|
+
for (const name of names) {
|
|
1812
|
+
const file = path6.join(projectDir, name);
|
|
1813
|
+
if (existsSync2(file)) return file;
|
|
1814
|
+
}
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
function stripComments(source) {
|
|
1818
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").split("\n").filter((line) => !line.trimStart().startsWith("//")).join("\n");
|
|
1819
|
+
}
|
|
1820
|
+
function craBase(homepage) {
|
|
1821
|
+
if (typeof homepage !== "string") return null;
|
|
1822
|
+
const value = homepage.trim();
|
|
1823
|
+
if (value === "." || value === "./") return "./";
|
|
1824
|
+
if (value.startsWith("/")) return value;
|
|
1825
|
+
try {
|
|
1826
|
+
return new URL(value).pathname;
|
|
1827
|
+
} catch {
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function detectBuildTarget(projectDir) {
|
|
1832
|
+
const pkg = readPackageJson(projectDir);
|
|
1833
|
+
const nextConfig = firstExisting(projectDir, NEXT_CONFIG_FILES);
|
|
1834
|
+
if (nextConfig || hasDependency(pkg, "next")) {
|
|
1835
|
+
const source = stripComments((nextConfig && readText(nextConfig)) ?? "");
|
|
1836
|
+
return {
|
|
1837
|
+
kind: "next",
|
|
1838
|
+
staticExport: OUTPUT_EXPORT.test(source),
|
|
1839
|
+
base: BASE_PATH.exec(source)?.[1] ?? null
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
if (hasDependency(pkg, "react-scripts")) return { kind: "cra", base: craBase(pkg.homepage) };
|
|
1843
|
+
if (hasDependency(pkg, "vite") || firstExisting(projectDir, VITE_CONFIG_FILES)) {
|
|
1844
|
+
const viteConfig = firstExisting(projectDir, VITE_CONFIG_FILES);
|
|
1845
|
+
const source = stripComments((viteConfig && readText(viteConfig)) ?? "");
|
|
1846
|
+
return { kind: "vite", base: VITE_BASE.exec(source)?.[1] ?? null };
|
|
1847
|
+
}
|
|
1848
|
+
return { kind: "other", base: null };
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// src/server/module-dir.ts
|
|
1852
|
+
import * as path7 from "path";
|
|
1853
|
+
import { fileURLToPath } from "url";
|
|
1854
|
+
function moduleDir() {
|
|
1855
|
+
if (typeof __dirname !== "undefined") {
|
|
1856
|
+
return __dirname;
|
|
1857
|
+
}
|
|
1858
|
+
return path7.dirname(fileURLToPath(import.meta.url));
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// src/server/pack-error.ts
|
|
1862
|
+
var PackError = class extends Error {
|
|
1863
|
+
code;
|
|
1864
|
+
constructor(code, message) {
|
|
1865
|
+
super(message);
|
|
1866
|
+
this.name = "PackError";
|
|
1867
|
+
this.code = code;
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
// src/server/share-html.ts
|
|
1872
|
+
var FULL_URL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
1873
|
+
var WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
|
|
1874
|
+
var SEGMENT = /^(?:[A-Za-z0-9._~@+-]|%[0-9A-Fa-f]{2})+$/;
|
|
1875
|
+
function normalizeBase(value) {
|
|
1876
|
+
if (value === "" || value === "." || value === "./") return { kind: "relative" };
|
|
1877
|
+
if (WINDOWS_PATH.test(value)) {
|
|
1878
|
+
throw new PackError(
|
|
1879
|
+
"BAD_BASE",
|
|
1880
|
+
`The app base "${value}" is a Windows path. Your shell (Git Bash/MSYS) probably converted a site path such as /repo/ into it. Run the command with MSYS_NO_PATHCONV=1, or from an npm script, and pass the site path, for example --base /app/.`
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
if (FULL_URL.test(value)) {
|
|
1884
|
+
throw new PackError(
|
|
1885
|
+
"BAD_BASE",
|
|
1886
|
+
`The app base "${value}" is a full URL. Pass the path your HTML is served from, for example --base /app/ (shareBase in the Vite plugin).`
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
const notASitePath = () => new PackError(
|
|
1890
|
+
"BAD_BASE",
|
|
1891
|
+
`The app base "${value}" is not a site path. Use "/", "/app/" or "./".`
|
|
1892
|
+
);
|
|
1893
|
+
if (!value.startsWith("/")) throw notASitePath();
|
|
1894
|
+
const segments = value.slice(1).split("/");
|
|
1895
|
+
if (segments[segments.length - 1] === "") segments.pop();
|
|
1896
|
+
for (const segment of segments) {
|
|
1897
|
+
if (segment === "." || segment === ".." || !SEGMENT.test(segment)) throw notASitePath();
|
|
1898
|
+
}
|
|
1899
|
+
return { kind: "absolute", path: segments.length === 0 ? "/" : `/${segments.join("/")}/` };
|
|
1900
|
+
}
|
|
1901
|
+
function basePathOf(base) {
|
|
1902
|
+
return base.kind === "absolute" ? base.path : null;
|
|
1903
|
+
}
|
|
1904
|
+
function flagSrcFor(base, htmlRelPath) {
|
|
1905
|
+
const file = `${SHARE_DIR}/${SHARE_FLAG_FILE}`;
|
|
1906
|
+
if (base.kind === "absolute") return `${base.path}${file}`;
|
|
1907
|
+
const depth = htmlRelPath.split("/").length - 1;
|
|
1908
|
+
return depth === 0 ? `./${file}` : `${"../".repeat(depth)}${file}`;
|
|
1909
|
+
}
|
|
1910
|
+
function shareUrlPathFor(base) {
|
|
1911
|
+
const page = `${SHARE_DIR}/index.html`;
|
|
1912
|
+
return base.kind === "absolute" ? `${base.path}${page}` : page;
|
|
1913
|
+
}
|
|
1914
|
+
var EXISTING_FLAG_TAG = /<script\b[^>]*\bsrc="[^"]*livelocale\/flag\.js"[^>]*>\s*<\/script>/gi;
|
|
1915
|
+
var HEAD_OPEN = /<head(?=[\s>])[^>]*>/i;
|
|
1916
|
+
var HTML_OPEN = /<html(?=[\s>])[^>]*>/i;
|
|
1917
|
+
var DOCTYPE = /<!doctype\b[^>]*>/i;
|
|
1918
|
+
function escapeAttribute(value) {
|
|
1919
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1920
|
+
}
|
|
1921
|
+
function injectFlagScript(html, src) {
|
|
1922
|
+
const tag = `<script src="${escapeAttribute(src)}"></script>`;
|
|
1923
|
+
const cleaned = html.replace(EXISTING_FLAG_TAG, "");
|
|
1924
|
+
for (const pattern of [HEAD_OPEN, HTML_OPEN, DOCTYPE]) {
|
|
1925
|
+
const match = pattern.exec(cleaned);
|
|
1926
|
+
if (match) {
|
|
1927
|
+
const end = match.index + match[0].length;
|
|
1928
|
+
return cleaned.slice(0, end) + tag + cleaned.slice(end);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return tag + cleaned;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/server/pack.ts
|
|
1935
|
+
var NO_REPO_WARNING = "exposeRepo is on but no GitHub repository was found; the manifest has no repo.";
|
|
1936
|
+
function isPlainObject(value) {
|
|
1937
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1938
|
+
}
|
|
1939
|
+
function toPosix(relative4) {
|
|
1940
|
+
return relative4.split(path8.sep).join("/") || ".";
|
|
1941
|
+
}
|
|
1942
|
+
function isInside(parent, child) {
|
|
1943
|
+
const rel = path8.relative(parent, child);
|
|
1944
|
+
return rel === "" || !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
1945
|
+
}
|
|
1946
|
+
function defineOwn(target, key, value) {
|
|
1947
|
+
Object.defineProperty(target, key, {
|
|
1948
|
+
value,
|
|
1949
|
+
enumerable: true,
|
|
1950
|
+
writable: true,
|
|
1951
|
+
configurable: true
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
async function readJsonObject(file, projectDir) {
|
|
1955
|
+
const shown = toPosix(path8.relative(projectDir, file));
|
|
1956
|
+
let parsed;
|
|
1957
|
+
try {
|
|
1958
|
+
parsed = JSON.parse(await fs4.readFile(file, "utf-8"));
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
throw new PackError(
|
|
1961
|
+
"INVALID_LOCALE_FILE",
|
|
1962
|
+
`${shown} is not a JSON object: ${error instanceof Error ? error.message : String(error)}`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
if (!isPlainObject(parsed)) {
|
|
1966
|
+
const kind = Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed;
|
|
1967
|
+
throw new PackError(
|
|
1968
|
+
"INVALID_LOCALE_FILE",
|
|
1969
|
+
`${shown} is not a JSON object: the root is ${kind}`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
return parsed;
|
|
1973
|
+
}
|
|
1974
|
+
async function collectShareData(options) {
|
|
1975
|
+
const projectDir = path8.resolve(options.projectDir);
|
|
1976
|
+
const localesDir = path8.resolve(projectDir, options.localesPath ?? "locales");
|
|
1977
|
+
const warnings = [];
|
|
1978
|
+
let entries;
|
|
1979
|
+
try {
|
|
1980
|
+
if (!(await fs4.stat(localesDir)).isDirectory()) throw new Error("not a directory");
|
|
1981
|
+
entries = await fs4.readdir(localesDir, { withFileTypes: true });
|
|
1982
|
+
} catch {
|
|
1983
|
+
throw new PackError(
|
|
1984
|
+
"LOCALES_NOT_FOUND",
|
|
1985
|
+
`Locales directory not found: ${localesDir}. Pass --locales <path>.`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
const dirLocales = /* @__PURE__ */ new Set();
|
|
1989
|
+
const fileLocales = /* @__PURE__ */ new Set();
|
|
1990
|
+
for (const entry of entries) {
|
|
1991
|
+
if (entry.isDirectory() && LOCALE_RE.test(entry.name)) {
|
|
1992
|
+
dirLocales.add(entry.name);
|
|
1993
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
1994
|
+
const code = entry.name.slice(0, -".json".length);
|
|
1995
|
+
if (LOCALE_RE.test(code)) fileLocales.add(code);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const locales = [.../* @__PURE__ */ new Set([...dirLocales, ...fileLocales])].sort();
|
|
1999
|
+
if (locales.length === 0) {
|
|
2000
|
+
throw new PackError(
|
|
2001
|
+
"NO_LOCALES",
|
|
2002
|
+
`No locale files in ${localesDir} (expected en.json or en/*.json).`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
const layout = dirLocales.size > 0 ? "namespaced" : "flat";
|
|
2006
|
+
const sourceLocale = options.sourceLocale ?? (locales.includes("en") ? "en" : locales[0]);
|
|
2007
|
+
if (!locales.includes(sourceLocale)) {
|
|
2008
|
+
throw new PackError(
|
|
2009
|
+
"SOURCE_LOCALE_NOT_FOUND",
|
|
2010
|
+
`Source locale "${sourceLocale}" not found in ${localesDir} (found: ${locales.join(", ")}).`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
const dictionaries = {};
|
|
2014
|
+
for (const locale of locales) {
|
|
2015
|
+
let dictionary;
|
|
2016
|
+
if (dirLocales.has(locale)) {
|
|
2017
|
+
dictionary = {};
|
|
2018
|
+
const folder = path8.join(localesDir, locale);
|
|
2019
|
+
const files = (await fs4.readdir(folder, { withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".json")).map((file) => file.name).sort();
|
|
2020
|
+
for (const file of files) {
|
|
2021
|
+
const namespace = file.slice(0, -".json".length);
|
|
2022
|
+
defineOwn(dictionary, namespace, await readJsonObject(path8.join(folder, file), projectDir));
|
|
2023
|
+
}
|
|
2024
|
+
} else {
|
|
2025
|
+
dictionary = await readJsonObject(path8.join(localesDir, `${locale}.json`), projectDir);
|
|
2026
|
+
}
|
|
2027
|
+
defineOwn(dictionaries, locale, dictionary);
|
|
2028
|
+
}
|
|
2029
|
+
const info = await resolveBuildInfo({
|
|
2030
|
+
cwd: projectDir,
|
|
2031
|
+
env: options.env,
|
|
2032
|
+
git: options.git,
|
|
2033
|
+
includeRepo: options.exposeRepo === true
|
|
2034
|
+
});
|
|
2035
|
+
const root = info.gitRoot !== null && isInside(info.gitRoot, localesDir) ? info.gitRoot : projectDir;
|
|
2036
|
+
const codeKeys = await new KeyScanner({ requireEnabled: false }).scanSourceKeys(projectDir);
|
|
2037
|
+
if (options.exposeRepo === true && info.repo === null) warnings.push(NO_REPO_WARNING);
|
|
2038
|
+
const manifest = {
|
|
2039
|
+
format: SHARE_MANIFEST_FORMAT,
|
|
2040
|
+
...options.exposeRepo === true && info.repo !== null ? { repo: info.repo } : {},
|
|
2041
|
+
defaultBranch: info.defaultBranch,
|
|
2042
|
+
branch: info.branch,
|
|
2043
|
+
base: info.base,
|
|
2044
|
+
localesPath: toPosix(path8.relative(root, localesDir)),
|
|
2045
|
+
layout,
|
|
2046
|
+
sourceLocale,
|
|
2047
|
+
locales,
|
|
2048
|
+
basePath: options.basePath,
|
|
2049
|
+
codeKeys: [...codeKeys].sort()
|
|
2050
|
+
};
|
|
2051
|
+
return { manifest, dictionaries, warnings };
|
|
2052
|
+
}
|
|
2053
|
+
var RELATIVE_BASE_WARNING = "The app base is relative (./), so flag.js is linked per HTML file. Pages served by a client-side route fallback cannot load it; build with an absolute base (--base /path/) if the app has such routes.";
|
|
2054
|
+
function defaultStudioCandidates() {
|
|
2055
|
+
const here = moduleDir();
|
|
2056
|
+
return [path8.resolve(here, "../studio"), path8.resolve(here, "../../dist/studio")];
|
|
2057
|
+
}
|
|
2058
|
+
function isDirectory(target) {
|
|
2059
|
+
try {
|
|
2060
|
+
return statSync(target).isDirectory();
|
|
2061
|
+
} catch {
|
|
2062
|
+
return false;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
function resolveStudioDir(candidates = defaultStudioCandidates()) {
|
|
2066
|
+
for (const dir of candidates) {
|
|
2067
|
+
if (existsSync3(path8.join(dir, "index.html")) && isDirectory(path8.join(dir, "assets"))) {
|
|
2068
|
+
return dir;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
async function ownsShareDir(shareDir) {
|
|
2074
|
+
try {
|
|
2075
|
+
await fs4.lstat(shareDir);
|
|
2076
|
+
} catch {
|
|
2077
|
+
return true;
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
const manifest = JSON.parse(
|
|
2081
|
+
await fs4.readFile(path8.join(shareDir, SHARE_MANIFEST_FILE), "utf-8")
|
|
2082
|
+
);
|
|
2083
|
+
return isPlainObject(manifest) && typeof manifest["format"] === "number";
|
|
2084
|
+
} catch {
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
async function findHtmlFiles(outDir, shareDir) {
|
|
2089
|
+
const found = [];
|
|
2090
|
+
const walk = async (dir) => {
|
|
2091
|
+
for (const entry of await fs4.readdir(dir, { withFileTypes: true })) {
|
|
2092
|
+
if (entry.isSymbolicLink()) continue;
|
|
2093
|
+
const full = path8.join(dir, entry.name);
|
|
2094
|
+
if (entry.isDirectory()) {
|
|
2095
|
+
if (full !== shareDir) await walk(full);
|
|
2096
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
2097
|
+
found.push(toPosix(path8.relative(outDir, full)));
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
await walk(outDir);
|
|
2102
|
+
return found.sort();
|
|
2103
|
+
}
|
|
2104
|
+
async function packOutput(options) {
|
|
2105
|
+
const {
|
|
2106
|
+
outDir: outOption,
|
|
2107
|
+
base: baseOption,
|
|
2108
|
+
injectHtml = "auto",
|
|
2109
|
+
studioDir: studioOption,
|
|
2110
|
+
...shareOptions
|
|
2111
|
+
} = options;
|
|
2112
|
+
const projectDir = path8.resolve(options.projectDir);
|
|
2113
|
+
const outDir = path8.resolve(projectDir, outOption);
|
|
2114
|
+
if (!isDirectory(outDir)) {
|
|
2115
|
+
throw new PackError(
|
|
2116
|
+
"OUT_DIR_NOT_FOUND",
|
|
2117
|
+
`Build output not found: ${outDir}. Run your build first.`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
const target = detectBuildTarget(projectDir);
|
|
2121
|
+
const base = normalizeBase(baseOption ?? target.base ?? "/");
|
|
2122
|
+
const candidates = studioOption !== void 0 ? [path8.resolve(projectDir, studioOption)] : defaultStudioCandidates();
|
|
2123
|
+
const studioDir = resolveStudioDir(candidates);
|
|
2124
|
+
if (studioDir === null) {
|
|
2125
|
+
throw new PackError(
|
|
2126
|
+
"STUDIO_NOT_FOUND",
|
|
2127
|
+
`LiveLocale Studio files are missing from the installed package (looked in ${candidates.join(", ")}). Reinstall livelocale.`
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
const data = await collectShareData({ ...shareOptions, projectDir, basePath: basePathOf(base) });
|
|
2131
|
+
const shareDir = path8.join(outDir, SHARE_DIR);
|
|
2132
|
+
if (!await ownsShareDir(shareDir)) {
|
|
2133
|
+
throw new PackError(
|
|
2134
|
+
"SHARE_DIR_TAKEN",
|
|
2135
|
+
`${toPosix(path8.relative(projectDir, outDir))}/${SHARE_DIR}/ already exists and was not written by LiveLocale. Move it away and run again.`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
await fs4.rm(shareDir, { recursive: true, force: true });
|
|
2139
|
+
await fs4.cp(studioDir, shareDir, { recursive: true });
|
|
2140
|
+
await fs4.writeFile(
|
|
2141
|
+
path8.join(shareDir, SHARE_MANIFEST_FILE),
|
|
2142
|
+
JSON.stringify(data.manifest, null, 2) + "\n",
|
|
2143
|
+
"utf-8"
|
|
2144
|
+
);
|
|
2145
|
+
const localesDir = path8.join(shareDir, SHARE_LOCALES_DIR);
|
|
2146
|
+
await fs4.mkdir(localesDir, { recursive: true });
|
|
2147
|
+
for (const locale of data.manifest.locales) {
|
|
2148
|
+
await fs4.writeFile(
|
|
2149
|
+
path8.join(localesDir, `${locale}.json`),
|
|
2150
|
+
JSON.stringify(data.dictionaries[locale]),
|
|
2151
|
+
"utf-8"
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
await fs4.writeFile(path8.join(shareDir, SHARE_FLAG_FILE), FLAG_SCRIPT, "utf-8");
|
|
2155
|
+
const inject = injectHtml === "auto" ? target.kind !== "next" && !existsSync3(path8.join(outDir, "_next")) : injectHtml;
|
|
2156
|
+
const htmlInjected = inject ? await findHtmlFiles(outDir, shareDir) : [];
|
|
2157
|
+
for (const rel of htmlInjected) {
|
|
2158
|
+
const file = path8.join(outDir, ...rel.split("/"));
|
|
2159
|
+
const html = await fs4.readFile(file, "utf-8");
|
|
2160
|
+
const injected = injectFlagScript(html, flagSrcFor(base, rel));
|
|
2161
|
+
if (injected !== html) await fs4.writeFile(file, injected, "utf-8");
|
|
2162
|
+
}
|
|
2163
|
+
const warnings = [...data.warnings];
|
|
2164
|
+
if (base.kind === "relative") warnings.push(RELATIVE_BASE_WARNING);
|
|
2165
|
+
return {
|
|
2166
|
+
shareDir,
|
|
2167
|
+
manifest: data.manifest,
|
|
2168
|
+
htmlInjected,
|
|
2169
|
+
shareUrlPath: shareUrlPathFor(base),
|
|
2170
|
+
warnings
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
|
|
1554
2174
|
// src/server/vite-plugin.ts
|
|
1555
2175
|
var ROUTE = "/api/livelocale";
|
|
1556
2176
|
function liveLocaleVite(config) {
|
|
@@ -1560,10 +2180,47 @@ function liveLocaleVite(config) {
|
|
|
1560
2180
|
git: config.git === false ? void 0 : new GitClient(),
|
|
1561
2181
|
authToken: config.authToken ?? process.env["LIVELOCALE_TOKEN"]
|
|
1562
2182
|
});
|
|
2183
|
+
let shareBuild = null;
|
|
2184
|
+
let packed = false;
|
|
1563
2185
|
return {
|
|
1564
2186
|
name: "livelocale",
|
|
1565
|
-
//
|
|
1566
|
-
apply
|
|
2187
|
+
// Always active in dev; in builds only with `share: true`, and never for the SSR build.
|
|
2188
|
+
apply(_userConfig, env) {
|
|
2189
|
+
if (env.command === "serve") return true;
|
|
2190
|
+
return config.share === true && !env.isSsrBuild && !env.ssrBuild;
|
|
2191
|
+
},
|
|
2192
|
+
configResolved(resolved) {
|
|
2193
|
+
shareBuild = null;
|
|
2194
|
+
if (resolved.command !== "build" || config.share !== true) return;
|
|
2195
|
+
if (resolved.build?.ssr || resolved.build?.lib) return;
|
|
2196
|
+
const base = config.shareBase ?? resolved.base;
|
|
2197
|
+
shareBuild = {
|
|
2198
|
+
root: resolved.root,
|
|
2199
|
+
outDir: path9.resolve(resolved.root, resolved.build.outDir),
|
|
2200
|
+
base,
|
|
2201
|
+
appBase: normalizeBase(base),
|
|
2202
|
+
logger: resolved.logger
|
|
2203
|
+
};
|
|
2204
|
+
},
|
|
2205
|
+
buildStart() {
|
|
2206
|
+
packed = false;
|
|
2207
|
+
},
|
|
2208
|
+
async writeBundle() {
|
|
2209
|
+
if (shareBuild === null || packed) return;
|
|
2210
|
+
packed = true;
|
|
2211
|
+
const result = await packOutput({
|
|
2212
|
+
outDir: shareBuild.outDir,
|
|
2213
|
+
projectDir: shareBuild.root,
|
|
2214
|
+
// Resolved against the working directory, as the serve half does.
|
|
2215
|
+
localesPath: path9.resolve(config.localesPath),
|
|
2216
|
+
sourceLocale: config.sourceLocale,
|
|
2217
|
+
exposeRepo: config.exposeRepo ?? false,
|
|
2218
|
+
base: shareBuild.base,
|
|
2219
|
+
// transformIndexHtml already linked flag.js in every HTML file Vite wrote.
|
|
2220
|
+
injectHtml: false
|
|
2221
|
+
});
|
|
2222
|
+
for (const warning of result.warnings) shareBuild.logger.warn(warning);
|
|
2223
|
+
},
|
|
1567
2224
|
configureServer(server) {
|
|
1568
2225
|
server.middlewares.use(async (req, res, next) => {
|
|
1569
2226
|
const url = req.url ?? "";
|
|
@@ -1600,7 +2257,11 @@ function liveLocaleVite(config) {
|
|
|
1600
2257
|
}
|
|
1601
2258
|
});
|
|
1602
2259
|
},
|
|
1603
|
-
transformIndexHtml(html) {
|
|
2260
|
+
transformIndexHtml(html, ctx) {
|
|
2261
|
+
if (shareBuild !== null) {
|
|
2262
|
+
const htmlRelPath = (ctx?.path ?? "/index.html").replace(/^\/+/, "");
|
|
2263
|
+
return injectFlagScript(html, flagSrcFor(shareBuild.appBase, htmlRelPath));
|
|
2264
|
+
}
|
|
1604
2265
|
const flag = process.env["LIVELOCALE"];
|
|
1605
2266
|
const flagScript = flag ? `<script>window.${BROWSER_FLAG_GLOBAL} = ${JSON.stringify(flag)};</script>
|
|
1606
2267
|
` : "";
|
|
@@ -1608,24 +2269,24 @@ function liveLocaleVite(config) {
|
|
|
1608
2269
|
return flagScript ? html.replace("</head>", `${flagScript}</head>`) : html;
|
|
1609
2270
|
}
|
|
1610
2271
|
try {
|
|
1611
|
-
const resolvedLocalesPath =
|
|
1612
|
-
const entries =
|
|
2272
|
+
const resolvedLocalesPath = path9.resolve(config.localesPath);
|
|
2273
|
+
const entries = fs5.readdirSync(resolvedLocalesPath);
|
|
1613
2274
|
let fallbackData = {};
|
|
1614
2275
|
for (const entry of entries) {
|
|
1615
2276
|
if (entry.startsWith(".")) continue;
|
|
1616
|
-
const entryPath =
|
|
2277
|
+
const entryPath = path9.join(resolvedLocalesPath, entry);
|
|
1617
2278
|
let entryIsDir = false;
|
|
1618
2279
|
try {
|
|
1619
|
-
entryIsDir =
|
|
2280
|
+
entryIsDir = fs5.statSync(entryPath).isDirectory();
|
|
1620
2281
|
} catch {
|
|
1621
2282
|
}
|
|
1622
2283
|
if (entryIsDir) {
|
|
1623
2284
|
try {
|
|
1624
|
-
const files =
|
|
2285
|
+
const files = fs5.readdirSync(entryPath);
|
|
1625
2286
|
for (const file of files) {
|
|
1626
2287
|
if (file.endsWith(".json")) {
|
|
1627
2288
|
const ns = file.slice(0, -5);
|
|
1628
|
-
const raw =
|
|
2289
|
+
const raw = fs5.readFileSync(path9.join(entryPath, file), "utf-8");
|
|
1629
2290
|
try {
|
|
1630
2291
|
const parsed = JSON.parse(raw);
|
|
1631
2292
|
fallbackData[ns] = deepMerge(fallbackData[ns] || {}, parsed);
|
|
@@ -1636,7 +2297,7 @@ function liveLocaleVite(config) {
|
|
|
1636
2297
|
} catch {
|
|
1637
2298
|
}
|
|
1638
2299
|
} else if (entry.endsWith(".json")) {
|
|
1639
|
-
const raw =
|
|
2300
|
+
const raw = fs5.readFileSync(entryPath, "utf-8");
|
|
1640
2301
|
try {
|
|
1641
2302
|
const parsed = JSON.parse(raw);
|
|
1642
2303
|
fallbackData = deepMerge(fallbackData, parsed);
|