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.cjs
CHANGED
|
@@ -33,8 +33,8 @@ __export(vite_plugin_exports, {
|
|
|
33
33
|
liveLocaleVite: () => liveLocaleVite
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(vite_plugin_exports);
|
|
36
|
-
var
|
|
37
|
-
var
|
|
36
|
+
var path9 = __toESM(require("path"), 1);
|
|
37
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
38
38
|
|
|
39
39
|
// src/server/file-mutator.ts
|
|
40
40
|
var import_fs2 = require("fs");
|
|
@@ -560,14 +560,6 @@ var KeyScanner = class {
|
|
|
560
560
|
};
|
|
561
561
|
|
|
562
562
|
// src/server/file-mutator.ts
|
|
563
|
-
function assertEnabled(component) {
|
|
564
|
-
const reason = getDisabledReason();
|
|
565
|
-
if (reason !== null) {
|
|
566
|
-
throw new Error(
|
|
567
|
-
`[livelocale] ${component} can only be executed in development mode (${reason}).`
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
563
|
var FileMutex = class {
|
|
572
564
|
queue = Promise.resolve();
|
|
573
565
|
async acquire(fn) {
|
|
@@ -583,7 +575,23 @@ var FileMutex = class {
|
|
|
583
575
|
};
|
|
584
576
|
var FileMutator = class {
|
|
585
577
|
fileLocks = /* @__PURE__ */ new Map();
|
|
586
|
-
|
|
578
|
+
requireEnabled;
|
|
579
|
+
quiet;
|
|
580
|
+
scanner;
|
|
581
|
+
constructor(options = {}) {
|
|
582
|
+
this.requireEnabled = options.requireEnabled ?? true;
|
|
583
|
+
this.quiet = options.quiet ?? false;
|
|
584
|
+
this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
|
|
585
|
+
}
|
|
586
|
+
assertEnabled() {
|
|
587
|
+
if (!this.requireEnabled) return;
|
|
588
|
+
const reason = getDisabledReason();
|
|
589
|
+
if (reason !== null) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`[livelocale] FileMutator can only be executed in development mode (${reason}).`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
587
595
|
/**
|
|
588
596
|
* Helper to serialize execution per file path
|
|
589
597
|
*/
|
|
@@ -601,7 +609,7 @@ var FileMutator = class {
|
|
|
601
609
|
* structural JSON validation, and concurrency serialization.
|
|
602
610
|
*/
|
|
603
611
|
async updateLocaleKey(basePath, locale, keyPath, newValue) {
|
|
604
|
-
assertEnabled(
|
|
612
|
+
this.assertEnabled();
|
|
605
613
|
if (!isSafeLocaleSegment(locale)) {
|
|
606
614
|
throw new Error(
|
|
607
615
|
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
@@ -704,7 +712,9 @@ var FileMutator = class {
|
|
|
704
712
|
`[livelocale] Failed to rename temp file to target at ${filePath}. Operation: rename. Original error: ${err.message}`
|
|
705
713
|
);
|
|
706
714
|
}
|
|
707
|
-
|
|
715
|
+
if (!this.quiet) {
|
|
716
|
+
console.log(`[livelocale] Updated key '${keyPath}' in locale '${locale}'`);
|
|
717
|
+
}
|
|
708
718
|
});
|
|
709
719
|
}
|
|
710
720
|
/**
|
|
@@ -712,7 +722,7 @@ var FileMutator = class {
|
|
|
712
722
|
* `*.json` inside the `{locale}/` folder merged under its namespace.
|
|
713
723
|
*/
|
|
714
724
|
async readLocale(basePath, locale) {
|
|
715
|
-
assertEnabled(
|
|
725
|
+
this.assertEnabled();
|
|
716
726
|
const localePath = path3.resolve(basePath, locale);
|
|
717
727
|
let isDir = false;
|
|
718
728
|
try {
|
|
@@ -739,7 +749,7 @@ var FileMutator = class {
|
|
|
739
749
|
* Lists all locale codes dynamically scanned from the locales directory.
|
|
740
750
|
*/
|
|
741
751
|
async listLocales(basePath) {
|
|
742
|
-
assertEnabled(
|
|
752
|
+
this.assertEnabled();
|
|
743
753
|
const resolvedBasePath = path3.resolve(basePath);
|
|
744
754
|
const items = await import_fs2.promises.readdir(resolvedBasePath, { withFileTypes: true });
|
|
745
755
|
const localesSet = /* @__PURE__ */ new Set();
|
|
@@ -761,7 +771,7 @@ var FileMutator = class {
|
|
|
761
771
|
* Adds a new locale directory/file based on current repository structure.
|
|
762
772
|
*/
|
|
763
773
|
async addLocale(basePath, locale) {
|
|
764
|
-
assertEnabled(
|
|
774
|
+
this.assertEnabled();
|
|
765
775
|
if (!isSafeLocaleSegment(locale)) {
|
|
766
776
|
throw new Error(
|
|
767
777
|
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
@@ -817,7 +827,7 @@ var FileMutator = class {
|
|
|
817
827
|
* Renames an existing locale (directory or file) to a new name.
|
|
818
828
|
*/
|
|
819
829
|
async renameLocale(basePath, oldLocale, newLocale) {
|
|
820
|
-
assertEnabled(
|
|
830
|
+
this.assertEnabled();
|
|
821
831
|
if (!isSafeLocaleSegment(oldLocale) || !isSafeLocaleSegment(newLocale)) {
|
|
822
832
|
throw new Error("[livelocale] Path traversal attempt detected");
|
|
823
833
|
}
|
|
@@ -875,7 +885,7 @@ var FileMutator = class {
|
|
|
875
885
|
* Deletes an existing locale (directory or file).
|
|
876
886
|
*/
|
|
877
887
|
async deleteLocale(basePath, locale) {
|
|
878
|
-
assertEnabled(
|
|
888
|
+
this.assertEnabled();
|
|
879
889
|
if (!isSafeLocaleSegment(locale)) {
|
|
880
890
|
throw new Error("[livelocale] Path traversal attempt detected");
|
|
881
891
|
}
|
|
@@ -1238,6 +1248,67 @@ function validateMessageSyntax(message) {
|
|
|
1238
1248
|
return parseIcu(prepared).error;
|
|
1239
1249
|
}
|
|
1240
1250
|
|
|
1251
|
+
// src/core/save-rules.ts
|
|
1252
|
+
var reject = (status, code, error) => ({
|
|
1253
|
+
ok: false,
|
|
1254
|
+
status,
|
|
1255
|
+
code,
|
|
1256
|
+
error
|
|
1257
|
+
});
|
|
1258
|
+
function validateWrite(input, options = {}) {
|
|
1259
|
+
const { locale, key, value } = input;
|
|
1260
|
+
if (!isValidLocaleFormat(locale)) {
|
|
1261
|
+
return reject(400, "INVALID_LOCALE_FORMAT", "Invalid locale string parameter.");
|
|
1262
|
+
}
|
|
1263
|
+
if (!isValidKeyFormat(key)) {
|
|
1264
|
+
return reject(
|
|
1265
|
+
400,
|
|
1266
|
+
"INVALID_KEY_FORMAT",
|
|
1267
|
+
"Invalid key path. Must be 1-200 characters containing alphanumeric, dots, underscores, or hyphens."
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
if (!isSafeKeyPath(key)) {
|
|
1271
|
+
return reject(
|
|
1272
|
+
400,
|
|
1273
|
+
"INVALID_KEY_FORMAT",
|
|
1274
|
+
"Forbidden key path: segments like __proto__, constructor, or prototype are not allowed."
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (!isValidValueFormat(value)) {
|
|
1278
|
+
return reject(
|
|
1279
|
+
400,
|
|
1280
|
+
"INVALID_VALUE_FORMAT",
|
|
1281
|
+
"Invalid value. Must be a string less than 10000 characters."
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
const accepted = {
|
|
1285
|
+
ok: true,
|
|
1286
|
+
locale,
|
|
1287
|
+
key,
|
|
1288
|
+
value
|
|
1289
|
+
};
|
|
1290
|
+
if (options.force === true) return accepted;
|
|
1291
|
+
const syntaxError = validateMessageSyntax(accepted.value);
|
|
1292
|
+
if (syntaxError) {
|
|
1293
|
+
return reject(422, "MESSAGE_SYNTAX", `Message syntax: ${syntaxError}.`);
|
|
1294
|
+
}
|
|
1295
|
+
const comparison = comparePlaceholders(options.reference, accepted.value);
|
|
1296
|
+
if (!comparison.ok) {
|
|
1297
|
+
const parts = [];
|
|
1298
|
+
if (comparison.missing.length) parts.push(`missing ${comparison.missing.join(", ")}`);
|
|
1299
|
+
if (comparison.extra.length) parts.push(`unexpected ${comparison.extra.join(", ")}`);
|
|
1300
|
+
return {
|
|
1301
|
+
...reject(
|
|
1302
|
+
422,
|
|
1303
|
+
"PLACEHOLDER_MISMATCH",
|
|
1304
|
+
`Placeholders differ from the reference: ${parts.join("; ")}.`
|
|
1305
|
+
),
|
|
1306
|
+
details: { missing: comparison.missing, extra: comparison.extra }
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
return accepted;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1241
1312
|
// src/server/router.ts
|
|
1242
1313
|
var DEFAULT_ALLOWED_ORIGINS = ["http://localhost:3010", "http://127.0.0.1:3010"];
|
|
1243
1314
|
var MAX_BODY_BYTES = 1e6;
|
|
@@ -1425,53 +1496,18 @@ function createMutationRouter(mutator, options) {
|
|
|
1425
1496
|
await mutator.deleteLocale(basePath, locale);
|
|
1426
1497
|
return json(200, headers, { success: true, locale });
|
|
1427
1498
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
code: "INVALID_KEY_FORMAT"
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
if (!isSafeKeyPath(key)) {
|
|
1441
|
-
return json(400, headers, {
|
|
1442
|
-
error: "Forbidden key path: segments like __proto__, constructor, or prototype are not allowed.",
|
|
1443
|
-
code: "INVALID_KEY_FORMAT"
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
if (!isValidValueFormat(value)) {
|
|
1447
|
-
return json(400, headers, {
|
|
1448
|
-
error: "Invalid value. Must be a string less than 10000 characters.",
|
|
1449
|
-
code: "INVALID_VALUE_FORMAT"
|
|
1499
|
+
const verdict = validateWrite(
|
|
1500
|
+
{ locale, key, value },
|
|
1501
|
+
{ reference: typeof reference === "string" ? reference : null, force: force === true }
|
|
1502
|
+
);
|
|
1503
|
+
if (!verdict.ok) {
|
|
1504
|
+
return json(verdict.status, headers, {
|
|
1505
|
+
error: verdict.error,
|
|
1506
|
+
code: verdict.code,
|
|
1507
|
+
details: verdict.details
|
|
1450
1508
|
});
|
|
1451
1509
|
}
|
|
1452
|
-
|
|
1453
|
-
const syntaxError = validateMessageSyntax(value);
|
|
1454
|
-
if (syntaxError) {
|
|
1455
|
-
return json(422, headers, {
|
|
1456
|
-
error: `Message syntax: ${syntaxError}.`,
|
|
1457
|
-
code: "MESSAGE_SYNTAX"
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
if (typeof reference === "string") {
|
|
1461
|
-
const comparison = comparePlaceholders(reference, value);
|
|
1462
|
-
if (!comparison.ok) {
|
|
1463
|
-
const parts = [];
|
|
1464
|
-
if (comparison.missing.length) parts.push(`missing ${comparison.missing.join(", ")}`);
|
|
1465
|
-
if (comparison.extra.length) parts.push(`unexpected ${comparison.extra.join(", ")}`);
|
|
1466
|
-
return json(422, headers, {
|
|
1467
|
-
error: `Placeholders differ from the reference: ${parts.join("; ")}.`,
|
|
1468
|
-
code: "PLACEHOLDER_MISMATCH",
|
|
1469
|
-
details: { missing: comparison.missing, extra: comparison.extra }
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
await mutator.updateLocaleKey(basePath, locale, key, value);
|
|
1510
|
+
await mutator.updateLocaleKey(basePath, verdict.locale, verdict.key, verdict.value);
|
|
1475
1511
|
return json(200, headers, { success: true, key, locale });
|
|
1476
1512
|
}
|
|
1477
1513
|
return {
|
|
@@ -1545,7 +1581,7 @@ var BodyTooLargeError = class extends Error {
|
|
|
1545
1581
|
}
|
|
1546
1582
|
};
|
|
1547
1583
|
function readNodeBody(req, maxBytes) {
|
|
1548
|
-
return new Promise((
|
|
1584
|
+
return new Promise((resolve8, reject2) => {
|
|
1549
1585
|
let bodyText = "";
|
|
1550
1586
|
let settled = false;
|
|
1551
1587
|
req.on("data", (chunk) => {
|
|
@@ -1553,19 +1589,19 @@ function readNodeBody(req, maxBytes) {
|
|
|
1553
1589
|
bodyText += chunk.toString();
|
|
1554
1590
|
if (Buffer.byteLength(bodyText, "utf-8") > maxBytes) {
|
|
1555
1591
|
settled = true;
|
|
1556
|
-
|
|
1592
|
+
reject2(new BodyTooLargeError());
|
|
1557
1593
|
req.destroy?.();
|
|
1558
1594
|
}
|
|
1559
1595
|
});
|
|
1560
1596
|
req.on("end", () => {
|
|
1561
1597
|
if (settled) return;
|
|
1562
1598
|
settled = true;
|
|
1563
|
-
|
|
1599
|
+
resolve8(bodyText);
|
|
1564
1600
|
});
|
|
1565
1601
|
req.on("error", (err) => {
|
|
1566
1602
|
if (settled) return;
|
|
1567
1603
|
settled = true;
|
|
1568
|
-
|
|
1604
|
+
reject2(err);
|
|
1569
1605
|
});
|
|
1570
1606
|
});
|
|
1571
1607
|
}
|
|
@@ -1585,6 +1621,591 @@ function deepMerge(target, source) {
|
|
|
1585
1621
|
return result;
|
|
1586
1622
|
}
|
|
1587
1623
|
|
|
1624
|
+
// src/server/pack.ts
|
|
1625
|
+
var import_fs5 = require("fs");
|
|
1626
|
+
var path8 = __toESM(require("path"), 1);
|
|
1627
|
+
|
|
1628
|
+
// src/core/flag-script.ts
|
|
1629
|
+
var FLAG_SCRIPT_SIGNATURE = "/*! livelocale flag v1 */";
|
|
1630
|
+
var FLAG_SCRIPT = `${FLAG_SCRIPT_SIGNATURE}
|
|
1631
|
+
(function () {
|
|
1632
|
+
try {
|
|
1633
|
+
var me = document.currentScript;
|
|
1634
|
+
if (!me || !me.src || window.parent === window || window.__LIVELOCALE_FLAG__ !== undefined)
|
|
1635
|
+
return;
|
|
1636
|
+
var studio = new URL('.', me.src);
|
|
1637
|
+
var host = window.parent.location;
|
|
1638
|
+
if (
|
|
1639
|
+
host.origin === 'null' ||
|
|
1640
|
+
host.origin !== window.location.origin ||
|
|
1641
|
+
host.origin !== studio.origin
|
|
1642
|
+
)
|
|
1643
|
+
return;
|
|
1644
|
+
if (host.pathname.indexOf(studio.pathname) !== 0) return;
|
|
1645
|
+
window.__LIVELOCALE_FLAG__ = 'preview';
|
|
1646
|
+
} catch (e) {}
|
|
1647
|
+
})();
|
|
1648
|
+
`;
|
|
1649
|
+
|
|
1650
|
+
// src/core/change-link.ts
|
|
1651
|
+
function isValidRepoSlug(slug) {
|
|
1652
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/.test(slug)) return false;
|
|
1653
|
+
const repo = slug.slice(slug.indexOf("/") + 1);
|
|
1654
|
+
return repo !== "." && repo !== "..";
|
|
1655
|
+
}
|
|
1656
|
+
var BRANCH_NAME_RE = /^(?!-)(?!.*\.\.)[^\s\x00-\x1f\x7f]{1,255}$/;
|
|
1657
|
+
function isValidBranchName(name) {
|
|
1658
|
+
return BRANCH_NAME_RE.test(name) && !hasLoneSurrogate(name);
|
|
1659
|
+
}
|
|
1660
|
+
var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
1661
|
+
function hasLoneSurrogate(text) {
|
|
1662
|
+
return typeof text === "string" && LONE_SURROGATE_RE.test(text);
|
|
1663
|
+
}
|
|
1664
|
+
var MAX_INFLATED_BYTES = 512 * 1024;
|
|
1665
|
+
var TOO_LARGE_MESSAGE = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
|
|
1666
|
+
|
|
1667
|
+
// src/core/share-manifest.ts
|
|
1668
|
+
var SHARE_DIR = "livelocale";
|
|
1669
|
+
var SHARE_MANIFEST_FILE = "manifest.json";
|
|
1670
|
+
var SHARE_FLAG_FILE = "flag.js";
|
|
1671
|
+
var SHARE_LOCALES_DIR = "locales";
|
|
1672
|
+
var SHARE_MANIFEST_FORMAT = 1;
|
|
1673
|
+
|
|
1674
|
+
// src/server/build-info.ts
|
|
1675
|
+
var import_child_process2 = require("child_process");
|
|
1676
|
+
var import_fs3 = require("fs");
|
|
1677
|
+
var path5 = __toESM(require("path"), 1);
|
|
1678
|
+
var runGit = (args, cwd) => new Promise((resolve8) => {
|
|
1679
|
+
try {
|
|
1680
|
+
(0, import_child_process2.execFile)(
|
|
1681
|
+
"git",
|
|
1682
|
+
args,
|
|
1683
|
+
{ cwd, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
|
|
1684
|
+
(error, stdout) => resolve8(error ? null : String(stdout).trim())
|
|
1685
|
+
);
|
|
1686
|
+
} catch {
|
|
1687
|
+
resolve8(null);
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
var SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
|
|
1691
|
+
async function firstValid(candidates, accept) {
|
|
1692
|
+
for (const candidate of candidates) {
|
|
1693
|
+
const value = typeof candidate === "function" ? await candidate() : candidate;
|
|
1694
|
+
if (typeof value !== "string" || value === "") continue;
|
|
1695
|
+
const accepted = accept(value);
|
|
1696
|
+
if (accepted !== null) return accepted;
|
|
1697
|
+
}
|
|
1698
|
+
return null;
|
|
1699
|
+
}
|
|
1700
|
+
var asBranch = (value) => isValidBranchName(value) ? value : null;
|
|
1701
|
+
var asSha = (value) => SHA_RE.test(value) ? value.toLowerCase() : null;
|
|
1702
|
+
var asSlug = (value) => isValidRepoSlug(value) ? value : null;
|
|
1703
|
+
function readEvent(eventPath) {
|
|
1704
|
+
if (!eventPath) return {};
|
|
1705
|
+
try {
|
|
1706
|
+
const parsed = JSON.parse((0, import_fs3.readFileSync)(eventPath, "utf-8"));
|
|
1707
|
+
return isRecord(parsed) ? parsed : {};
|
|
1708
|
+
} catch {
|
|
1709
|
+
return {};
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
function isRecord(value) {
|
|
1713
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1714
|
+
}
|
|
1715
|
+
function pick(obj, ...keys) {
|
|
1716
|
+
let current = obj;
|
|
1717
|
+
for (const key of keys) {
|
|
1718
|
+
if (!isRecord(current)) return void 0;
|
|
1719
|
+
current = current[key];
|
|
1720
|
+
}
|
|
1721
|
+
return typeof current === "string" ? current : void 0;
|
|
1722
|
+
}
|
|
1723
|
+
async function resolveBuildInfo(options) {
|
|
1724
|
+
const { cwd, env = process.env, git = runGit, includeRepo = false } = options;
|
|
1725
|
+
const event = readEvent(env.GITHUB_EVENT_PATH);
|
|
1726
|
+
const isPullRequest = env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target";
|
|
1727
|
+
const netlify = env.NETLIFY === "true";
|
|
1728
|
+
const gitlab = env.GITLAB_CI === "true";
|
|
1729
|
+
const branch = await firstValid(
|
|
1730
|
+
[
|
|
1731
|
+
env.GITHUB_HEAD_REF,
|
|
1732
|
+
env.GITHUB_REF_TYPE === "branch" ? env.GITHUB_REF_NAME : null,
|
|
1733
|
+
env.VERCEL_GIT_COMMIT_REF,
|
|
1734
|
+
env.CF_PAGES_BRANCH,
|
|
1735
|
+
netlify ? env.BRANCH : null,
|
|
1736
|
+
gitlab ? env.CI_COMMIT_REF_NAME : null,
|
|
1737
|
+
async () => {
|
|
1738
|
+
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
1739
|
+
return head === "HEAD" ? null : head;
|
|
1740
|
+
}
|
|
1741
|
+
],
|
|
1742
|
+
asBranch
|
|
1743
|
+
);
|
|
1744
|
+
const base = await firstValid(
|
|
1745
|
+
[
|
|
1746
|
+
isPullRequest ? pick(event, "pull_request", "head", "sha") : null,
|
|
1747
|
+
env.GITHUB_SHA,
|
|
1748
|
+
env.VERCEL_GIT_COMMIT_SHA,
|
|
1749
|
+
env.CF_PAGES_COMMIT_SHA,
|
|
1750
|
+
netlify ? env.COMMIT_REF : null,
|
|
1751
|
+
gitlab ? env.CI_COMMIT_SHA : null,
|
|
1752
|
+
() => git(["rev-parse", "HEAD"], cwd)
|
|
1753
|
+
],
|
|
1754
|
+
asSha
|
|
1755
|
+
);
|
|
1756
|
+
const defaultBranch = await firstValid(
|
|
1757
|
+
[
|
|
1758
|
+
pick(event, "repository", "default_branch"),
|
|
1759
|
+
async () => {
|
|
1760
|
+
const ref = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
1761
|
+
return ref?.startsWith("origin/") ? ref.slice("origin/".length) : null;
|
|
1762
|
+
}
|
|
1763
|
+
],
|
|
1764
|
+
asBranch
|
|
1765
|
+
);
|
|
1766
|
+
const githubCom = env.GITHUB_SERVER_URL === void 0 || env.GITHUB_SERVER_URL === "" || env.GITHUB_SERVER_URL === "https://github.com";
|
|
1767
|
+
const repo = includeRepo ? await firstValid(
|
|
1768
|
+
[
|
|
1769
|
+
githubCom ? env.GITHUB_REPOSITORY : null,
|
|
1770
|
+
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,
|
|
1771
|
+
netlify && env.REPOSITORY_URL ? parseGitHubSlug(env.REPOSITORY_URL) : null,
|
|
1772
|
+
async () => {
|
|
1773
|
+
const remote = await git(["remote", "get-url", "origin"], cwd);
|
|
1774
|
+
return remote ? parseGitHubSlug(remote) : null;
|
|
1775
|
+
}
|
|
1776
|
+
],
|
|
1777
|
+
asSlug
|
|
1778
|
+
) : null;
|
|
1779
|
+
const top = await git(["rev-parse", "--show-toplevel"], cwd);
|
|
1780
|
+
const gitRoot = top ? path5.resolve(top) : null;
|
|
1781
|
+
return { repo, defaultBranch, branch, base, gitRoot };
|
|
1782
|
+
}
|
|
1783
|
+
function parseGitHubSlug(remote) {
|
|
1784
|
+
const text = remote.trim();
|
|
1785
|
+
let repoPath;
|
|
1786
|
+
const scp = /^[A-Za-z0-9._-]+@github\.com:(.*)$/i.exec(text);
|
|
1787
|
+
if (scp) {
|
|
1788
|
+
repoPath = scp[1] ?? "";
|
|
1789
|
+
} else {
|
|
1790
|
+
let url;
|
|
1791
|
+
try {
|
|
1792
|
+
url = new URL(text);
|
|
1793
|
+
} catch {
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
|
|
1797
|
+
if (url.hostname.toLowerCase() !== "github.com") return null;
|
|
1798
|
+
repoPath = url.pathname;
|
|
1799
|
+
}
|
|
1800
|
+
const trimmed = repoPath.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
1801
|
+
return isValidRepoSlug(trimmed) ? trimmed : null;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/server/build-target.ts
|
|
1805
|
+
var import_fs4 = require("fs");
|
|
1806
|
+
var path6 = __toESM(require("path"), 1);
|
|
1807
|
+
var NEXT_CONFIG_FILES = [
|
|
1808
|
+
"next.config.js",
|
|
1809
|
+
"next.config.mjs",
|
|
1810
|
+
"next.config.cjs",
|
|
1811
|
+
"next.config.ts",
|
|
1812
|
+
"next.config.mts"
|
|
1813
|
+
];
|
|
1814
|
+
var VITE_CONFIG_FILES = [
|
|
1815
|
+
"vite.config.js",
|
|
1816
|
+
"vite.config.mjs",
|
|
1817
|
+
"vite.config.cjs",
|
|
1818
|
+
"vite.config.ts",
|
|
1819
|
+
"vite.config.mts"
|
|
1820
|
+
];
|
|
1821
|
+
var OUTPUT_EXPORT = /\boutput\s*:\s*['"`]export['"`]/;
|
|
1822
|
+
var BASE_PATH = /\bbasePath\s*:\s*['"`](\/[^'"`]*)['"`]/;
|
|
1823
|
+
var VITE_BASE = /\bbase\s*:\s*['"`](\/[^'"`]*|\.\/?|)['"`]/;
|
|
1824
|
+
function readText(file) {
|
|
1825
|
+
try {
|
|
1826
|
+
return (0, import_fs4.readFileSync)(file, "utf-8");
|
|
1827
|
+
} catch {
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function readPackageJson(projectDir) {
|
|
1832
|
+
const text = readText(path6.join(projectDir, "package.json"));
|
|
1833
|
+
if (text === null) return {};
|
|
1834
|
+
try {
|
|
1835
|
+
const parsed = JSON.parse(text);
|
|
1836
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1837
|
+
} catch {
|
|
1838
|
+
return {};
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function hasDependency(pkg, name) {
|
|
1842
|
+
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
1843
|
+
}
|
|
1844
|
+
function firstExisting(projectDir, names) {
|
|
1845
|
+
for (const name of names) {
|
|
1846
|
+
const file = path6.join(projectDir, name);
|
|
1847
|
+
if ((0, import_fs4.existsSync)(file)) return file;
|
|
1848
|
+
}
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
function stripComments(source) {
|
|
1852
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").split("\n").filter((line) => !line.trimStart().startsWith("//")).join("\n");
|
|
1853
|
+
}
|
|
1854
|
+
function craBase(homepage) {
|
|
1855
|
+
if (typeof homepage !== "string") return null;
|
|
1856
|
+
const value = homepage.trim();
|
|
1857
|
+
if (value === "." || value === "./") return "./";
|
|
1858
|
+
if (value.startsWith("/")) return value;
|
|
1859
|
+
try {
|
|
1860
|
+
return new URL(value).pathname;
|
|
1861
|
+
} catch {
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function detectBuildTarget(projectDir) {
|
|
1866
|
+
const pkg = readPackageJson(projectDir);
|
|
1867
|
+
const nextConfig = firstExisting(projectDir, NEXT_CONFIG_FILES);
|
|
1868
|
+
if (nextConfig || hasDependency(pkg, "next")) {
|
|
1869
|
+
const source = stripComments((nextConfig && readText(nextConfig)) ?? "");
|
|
1870
|
+
return {
|
|
1871
|
+
kind: "next",
|
|
1872
|
+
staticExport: OUTPUT_EXPORT.test(source),
|
|
1873
|
+
base: BASE_PATH.exec(source)?.[1] ?? null
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
if (hasDependency(pkg, "react-scripts")) return { kind: "cra", base: craBase(pkg.homepage) };
|
|
1877
|
+
if (hasDependency(pkg, "vite") || firstExisting(projectDir, VITE_CONFIG_FILES)) {
|
|
1878
|
+
const viteConfig = firstExisting(projectDir, VITE_CONFIG_FILES);
|
|
1879
|
+
const source = stripComments((viteConfig && readText(viteConfig)) ?? "");
|
|
1880
|
+
return { kind: "vite", base: VITE_BASE.exec(source)?.[1] ?? null };
|
|
1881
|
+
}
|
|
1882
|
+
return { kind: "other", base: null };
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// src/server/module-dir.ts
|
|
1886
|
+
var path7 = __toESM(require("path"), 1);
|
|
1887
|
+
var import_url = require("url");
|
|
1888
|
+
var import_meta = {};
|
|
1889
|
+
function moduleDir() {
|
|
1890
|
+
if (typeof __dirname !== "undefined") {
|
|
1891
|
+
return __dirname;
|
|
1892
|
+
}
|
|
1893
|
+
return path7.dirname((0, import_url.fileURLToPath)(import_meta.url));
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/server/pack-error.ts
|
|
1897
|
+
var PackError = class extends Error {
|
|
1898
|
+
code;
|
|
1899
|
+
constructor(code, message) {
|
|
1900
|
+
super(message);
|
|
1901
|
+
this.name = "PackError";
|
|
1902
|
+
this.code = code;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
// src/server/share-html.ts
|
|
1907
|
+
var FULL_URL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
1908
|
+
var WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
|
|
1909
|
+
var SEGMENT = /^(?:[A-Za-z0-9._~@+-]|%[0-9A-Fa-f]{2})+$/;
|
|
1910
|
+
function normalizeBase(value) {
|
|
1911
|
+
if (value === "" || value === "." || value === "./") return { kind: "relative" };
|
|
1912
|
+
if (WINDOWS_PATH.test(value)) {
|
|
1913
|
+
throw new PackError(
|
|
1914
|
+
"BAD_BASE",
|
|
1915
|
+
`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/.`
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
if (FULL_URL.test(value)) {
|
|
1919
|
+
throw new PackError(
|
|
1920
|
+
"BAD_BASE",
|
|
1921
|
+
`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).`
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
const notASitePath = () => new PackError(
|
|
1925
|
+
"BAD_BASE",
|
|
1926
|
+
`The app base "${value}" is not a site path. Use "/", "/app/" or "./".`
|
|
1927
|
+
);
|
|
1928
|
+
if (!value.startsWith("/")) throw notASitePath();
|
|
1929
|
+
const segments = value.slice(1).split("/");
|
|
1930
|
+
if (segments[segments.length - 1] === "") segments.pop();
|
|
1931
|
+
for (const segment of segments) {
|
|
1932
|
+
if (segment === "." || segment === ".." || !SEGMENT.test(segment)) throw notASitePath();
|
|
1933
|
+
}
|
|
1934
|
+
return { kind: "absolute", path: segments.length === 0 ? "/" : `/${segments.join("/")}/` };
|
|
1935
|
+
}
|
|
1936
|
+
function basePathOf(base) {
|
|
1937
|
+
return base.kind === "absolute" ? base.path : null;
|
|
1938
|
+
}
|
|
1939
|
+
function flagSrcFor(base, htmlRelPath) {
|
|
1940
|
+
const file = `${SHARE_DIR}/${SHARE_FLAG_FILE}`;
|
|
1941
|
+
if (base.kind === "absolute") return `${base.path}${file}`;
|
|
1942
|
+
const depth = htmlRelPath.split("/").length - 1;
|
|
1943
|
+
return depth === 0 ? `./${file}` : `${"../".repeat(depth)}${file}`;
|
|
1944
|
+
}
|
|
1945
|
+
function shareUrlPathFor(base) {
|
|
1946
|
+
const page = `${SHARE_DIR}/index.html`;
|
|
1947
|
+
return base.kind === "absolute" ? `${base.path}${page}` : page;
|
|
1948
|
+
}
|
|
1949
|
+
var EXISTING_FLAG_TAG = /<script\b[^>]*\bsrc="[^"]*livelocale\/flag\.js"[^>]*>\s*<\/script>/gi;
|
|
1950
|
+
var HEAD_OPEN = /<head(?=[\s>])[^>]*>/i;
|
|
1951
|
+
var HTML_OPEN = /<html(?=[\s>])[^>]*>/i;
|
|
1952
|
+
var DOCTYPE = /<!doctype\b[^>]*>/i;
|
|
1953
|
+
function escapeAttribute(value) {
|
|
1954
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1955
|
+
}
|
|
1956
|
+
function injectFlagScript(html, src) {
|
|
1957
|
+
const tag = `<script src="${escapeAttribute(src)}"></script>`;
|
|
1958
|
+
const cleaned = html.replace(EXISTING_FLAG_TAG, "");
|
|
1959
|
+
for (const pattern of [HEAD_OPEN, HTML_OPEN, DOCTYPE]) {
|
|
1960
|
+
const match = pattern.exec(cleaned);
|
|
1961
|
+
if (match) {
|
|
1962
|
+
const end = match.index + match[0].length;
|
|
1963
|
+
return cleaned.slice(0, end) + tag + cleaned.slice(end);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return tag + cleaned;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// src/server/pack.ts
|
|
1970
|
+
var NO_REPO_WARNING = "exposeRepo is on but no GitHub repository was found; the manifest has no repo.";
|
|
1971
|
+
function isPlainObject(value) {
|
|
1972
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1973
|
+
}
|
|
1974
|
+
function toPosix(relative4) {
|
|
1975
|
+
return relative4.split(path8.sep).join("/") || ".";
|
|
1976
|
+
}
|
|
1977
|
+
function isInside(parent, child) {
|
|
1978
|
+
const rel = path8.relative(parent, child);
|
|
1979
|
+
return rel === "" || !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
1980
|
+
}
|
|
1981
|
+
function defineOwn(target, key, value) {
|
|
1982
|
+
Object.defineProperty(target, key, {
|
|
1983
|
+
value,
|
|
1984
|
+
enumerable: true,
|
|
1985
|
+
writable: true,
|
|
1986
|
+
configurable: true
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
async function readJsonObject(file, projectDir) {
|
|
1990
|
+
const shown = toPosix(path8.relative(projectDir, file));
|
|
1991
|
+
let parsed;
|
|
1992
|
+
try {
|
|
1993
|
+
parsed = JSON.parse(await import_fs5.promises.readFile(file, "utf-8"));
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
throw new PackError(
|
|
1996
|
+
"INVALID_LOCALE_FILE",
|
|
1997
|
+
`${shown} is not a JSON object: ${error instanceof Error ? error.message : String(error)}`
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
if (!isPlainObject(parsed)) {
|
|
2001
|
+
const kind = Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed;
|
|
2002
|
+
throw new PackError(
|
|
2003
|
+
"INVALID_LOCALE_FILE",
|
|
2004
|
+
`${shown} is not a JSON object: the root is ${kind}`
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
return parsed;
|
|
2008
|
+
}
|
|
2009
|
+
async function collectShareData(options) {
|
|
2010
|
+
const projectDir = path8.resolve(options.projectDir);
|
|
2011
|
+
const localesDir = path8.resolve(projectDir, options.localesPath ?? "locales");
|
|
2012
|
+
const warnings = [];
|
|
2013
|
+
let entries;
|
|
2014
|
+
try {
|
|
2015
|
+
if (!(await import_fs5.promises.stat(localesDir)).isDirectory()) throw new Error("not a directory");
|
|
2016
|
+
entries = await import_fs5.promises.readdir(localesDir, { withFileTypes: true });
|
|
2017
|
+
} catch {
|
|
2018
|
+
throw new PackError(
|
|
2019
|
+
"LOCALES_NOT_FOUND",
|
|
2020
|
+
`Locales directory not found: ${localesDir}. Pass --locales <path>.`
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
const dirLocales = /* @__PURE__ */ new Set();
|
|
2024
|
+
const fileLocales = /* @__PURE__ */ new Set();
|
|
2025
|
+
for (const entry of entries) {
|
|
2026
|
+
if (entry.isDirectory() && LOCALE_RE.test(entry.name)) {
|
|
2027
|
+
dirLocales.add(entry.name);
|
|
2028
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
2029
|
+
const code = entry.name.slice(0, -".json".length);
|
|
2030
|
+
if (LOCALE_RE.test(code)) fileLocales.add(code);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
const locales = [.../* @__PURE__ */ new Set([...dirLocales, ...fileLocales])].sort();
|
|
2034
|
+
if (locales.length === 0) {
|
|
2035
|
+
throw new PackError(
|
|
2036
|
+
"NO_LOCALES",
|
|
2037
|
+
`No locale files in ${localesDir} (expected en.json or en/*.json).`
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
const layout = dirLocales.size > 0 ? "namespaced" : "flat";
|
|
2041
|
+
const sourceLocale = options.sourceLocale ?? (locales.includes("en") ? "en" : locales[0]);
|
|
2042
|
+
if (!locales.includes(sourceLocale)) {
|
|
2043
|
+
throw new PackError(
|
|
2044
|
+
"SOURCE_LOCALE_NOT_FOUND",
|
|
2045
|
+
`Source locale "${sourceLocale}" not found in ${localesDir} (found: ${locales.join(", ")}).`
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
const dictionaries = {};
|
|
2049
|
+
for (const locale of locales) {
|
|
2050
|
+
let dictionary;
|
|
2051
|
+
if (dirLocales.has(locale)) {
|
|
2052
|
+
dictionary = {};
|
|
2053
|
+
const folder = path8.join(localesDir, locale);
|
|
2054
|
+
const files = (await import_fs5.promises.readdir(folder, { withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".json")).map((file) => file.name).sort();
|
|
2055
|
+
for (const file of files) {
|
|
2056
|
+
const namespace = file.slice(0, -".json".length);
|
|
2057
|
+
defineOwn(dictionary, namespace, await readJsonObject(path8.join(folder, file), projectDir));
|
|
2058
|
+
}
|
|
2059
|
+
} else {
|
|
2060
|
+
dictionary = await readJsonObject(path8.join(localesDir, `${locale}.json`), projectDir);
|
|
2061
|
+
}
|
|
2062
|
+
defineOwn(dictionaries, locale, dictionary);
|
|
2063
|
+
}
|
|
2064
|
+
const info = await resolveBuildInfo({
|
|
2065
|
+
cwd: projectDir,
|
|
2066
|
+
env: options.env,
|
|
2067
|
+
git: options.git,
|
|
2068
|
+
includeRepo: options.exposeRepo === true
|
|
2069
|
+
});
|
|
2070
|
+
const root = info.gitRoot !== null && isInside(info.gitRoot, localesDir) ? info.gitRoot : projectDir;
|
|
2071
|
+
const codeKeys = await new KeyScanner({ requireEnabled: false }).scanSourceKeys(projectDir);
|
|
2072
|
+
if (options.exposeRepo === true && info.repo === null) warnings.push(NO_REPO_WARNING);
|
|
2073
|
+
const manifest = {
|
|
2074
|
+
format: SHARE_MANIFEST_FORMAT,
|
|
2075
|
+
...options.exposeRepo === true && info.repo !== null ? { repo: info.repo } : {},
|
|
2076
|
+
defaultBranch: info.defaultBranch,
|
|
2077
|
+
branch: info.branch,
|
|
2078
|
+
base: info.base,
|
|
2079
|
+
localesPath: toPosix(path8.relative(root, localesDir)),
|
|
2080
|
+
layout,
|
|
2081
|
+
sourceLocale,
|
|
2082
|
+
locales,
|
|
2083
|
+
basePath: options.basePath,
|
|
2084
|
+
codeKeys: [...codeKeys].sort()
|
|
2085
|
+
};
|
|
2086
|
+
return { manifest, dictionaries, warnings };
|
|
2087
|
+
}
|
|
2088
|
+
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.";
|
|
2089
|
+
function defaultStudioCandidates() {
|
|
2090
|
+
const here = moduleDir();
|
|
2091
|
+
return [path8.resolve(here, "../studio"), path8.resolve(here, "../../dist/studio")];
|
|
2092
|
+
}
|
|
2093
|
+
function isDirectory(target) {
|
|
2094
|
+
try {
|
|
2095
|
+
return (0, import_fs5.statSync)(target).isDirectory();
|
|
2096
|
+
} catch {
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function resolveStudioDir(candidates = defaultStudioCandidates()) {
|
|
2101
|
+
for (const dir of candidates) {
|
|
2102
|
+
if ((0, import_fs5.existsSync)(path8.join(dir, "index.html")) && isDirectory(path8.join(dir, "assets"))) {
|
|
2103
|
+
return dir;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
async function ownsShareDir(shareDir) {
|
|
2109
|
+
try {
|
|
2110
|
+
await import_fs5.promises.lstat(shareDir);
|
|
2111
|
+
} catch {
|
|
2112
|
+
return true;
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
const manifest = JSON.parse(
|
|
2116
|
+
await import_fs5.promises.readFile(path8.join(shareDir, SHARE_MANIFEST_FILE), "utf-8")
|
|
2117
|
+
);
|
|
2118
|
+
return isPlainObject(manifest) && typeof manifest["format"] === "number";
|
|
2119
|
+
} catch {
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
async function findHtmlFiles(outDir, shareDir) {
|
|
2124
|
+
const found = [];
|
|
2125
|
+
const walk = async (dir) => {
|
|
2126
|
+
for (const entry of await import_fs5.promises.readdir(dir, { withFileTypes: true })) {
|
|
2127
|
+
if (entry.isSymbolicLink()) continue;
|
|
2128
|
+
const full = path8.join(dir, entry.name);
|
|
2129
|
+
if (entry.isDirectory()) {
|
|
2130
|
+
if (full !== shareDir) await walk(full);
|
|
2131
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
2132
|
+
found.push(toPosix(path8.relative(outDir, full)));
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
await walk(outDir);
|
|
2137
|
+
return found.sort();
|
|
2138
|
+
}
|
|
2139
|
+
async function packOutput(options) {
|
|
2140
|
+
const {
|
|
2141
|
+
outDir: outOption,
|
|
2142
|
+
base: baseOption,
|
|
2143
|
+
injectHtml = "auto",
|
|
2144
|
+
studioDir: studioOption,
|
|
2145
|
+
...shareOptions
|
|
2146
|
+
} = options;
|
|
2147
|
+
const projectDir = path8.resolve(options.projectDir);
|
|
2148
|
+
const outDir = path8.resolve(projectDir, outOption);
|
|
2149
|
+
if (!isDirectory(outDir)) {
|
|
2150
|
+
throw new PackError(
|
|
2151
|
+
"OUT_DIR_NOT_FOUND",
|
|
2152
|
+
`Build output not found: ${outDir}. Run your build first.`
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
const target = detectBuildTarget(projectDir);
|
|
2156
|
+
const base = normalizeBase(baseOption ?? target.base ?? "/");
|
|
2157
|
+
const candidates = studioOption !== void 0 ? [path8.resolve(projectDir, studioOption)] : defaultStudioCandidates();
|
|
2158
|
+
const studioDir = resolveStudioDir(candidates);
|
|
2159
|
+
if (studioDir === null) {
|
|
2160
|
+
throw new PackError(
|
|
2161
|
+
"STUDIO_NOT_FOUND",
|
|
2162
|
+
`LiveLocale Studio files are missing from the installed package (looked in ${candidates.join(", ")}). Reinstall livelocale.`
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
const data = await collectShareData({ ...shareOptions, projectDir, basePath: basePathOf(base) });
|
|
2166
|
+
const shareDir = path8.join(outDir, SHARE_DIR);
|
|
2167
|
+
if (!await ownsShareDir(shareDir)) {
|
|
2168
|
+
throw new PackError(
|
|
2169
|
+
"SHARE_DIR_TAKEN",
|
|
2170
|
+
`${toPosix(path8.relative(projectDir, outDir))}/${SHARE_DIR}/ already exists and was not written by LiveLocale. Move it away and run again.`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
await import_fs5.promises.rm(shareDir, { recursive: true, force: true });
|
|
2174
|
+
await import_fs5.promises.cp(studioDir, shareDir, { recursive: true });
|
|
2175
|
+
await import_fs5.promises.writeFile(
|
|
2176
|
+
path8.join(shareDir, SHARE_MANIFEST_FILE),
|
|
2177
|
+
JSON.stringify(data.manifest, null, 2) + "\n",
|
|
2178
|
+
"utf-8"
|
|
2179
|
+
);
|
|
2180
|
+
const localesDir = path8.join(shareDir, SHARE_LOCALES_DIR);
|
|
2181
|
+
await import_fs5.promises.mkdir(localesDir, { recursive: true });
|
|
2182
|
+
for (const locale of data.manifest.locales) {
|
|
2183
|
+
await import_fs5.promises.writeFile(
|
|
2184
|
+
path8.join(localesDir, `${locale}.json`),
|
|
2185
|
+
JSON.stringify(data.dictionaries[locale]),
|
|
2186
|
+
"utf-8"
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
await import_fs5.promises.writeFile(path8.join(shareDir, SHARE_FLAG_FILE), FLAG_SCRIPT, "utf-8");
|
|
2190
|
+
const inject = injectHtml === "auto" ? target.kind !== "next" && !(0, import_fs5.existsSync)(path8.join(outDir, "_next")) : injectHtml;
|
|
2191
|
+
const htmlInjected = inject ? await findHtmlFiles(outDir, shareDir) : [];
|
|
2192
|
+
for (const rel of htmlInjected) {
|
|
2193
|
+
const file = path8.join(outDir, ...rel.split("/"));
|
|
2194
|
+
const html = await import_fs5.promises.readFile(file, "utf-8");
|
|
2195
|
+
const injected = injectFlagScript(html, flagSrcFor(base, rel));
|
|
2196
|
+
if (injected !== html) await import_fs5.promises.writeFile(file, injected, "utf-8");
|
|
2197
|
+
}
|
|
2198
|
+
const warnings = [...data.warnings];
|
|
2199
|
+
if (base.kind === "relative") warnings.push(RELATIVE_BASE_WARNING);
|
|
2200
|
+
return {
|
|
2201
|
+
shareDir,
|
|
2202
|
+
manifest: data.manifest,
|
|
2203
|
+
htmlInjected,
|
|
2204
|
+
shareUrlPath: shareUrlPathFor(base),
|
|
2205
|
+
warnings
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
|
|
1588
2209
|
// src/server/vite-plugin.ts
|
|
1589
2210
|
var ROUTE = "/api/livelocale";
|
|
1590
2211
|
function liveLocaleVite(config) {
|
|
@@ -1594,10 +2215,47 @@ function liveLocaleVite(config) {
|
|
|
1594
2215
|
git: config.git === false ? void 0 : new GitClient(),
|
|
1595
2216
|
authToken: config.authToken ?? process.env["LIVELOCALE_TOKEN"]
|
|
1596
2217
|
});
|
|
2218
|
+
let shareBuild = null;
|
|
2219
|
+
let packed = false;
|
|
1597
2220
|
return {
|
|
1598
2221
|
name: "livelocale",
|
|
1599
|
-
//
|
|
1600
|
-
apply
|
|
2222
|
+
// Always active in dev; in builds only with `share: true`, and never for the SSR build.
|
|
2223
|
+
apply(_userConfig, env) {
|
|
2224
|
+
if (env.command === "serve") return true;
|
|
2225
|
+
return config.share === true && !env.isSsrBuild && !env.ssrBuild;
|
|
2226
|
+
},
|
|
2227
|
+
configResolved(resolved) {
|
|
2228
|
+
shareBuild = null;
|
|
2229
|
+
if (resolved.command !== "build" || config.share !== true) return;
|
|
2230
|
+
if (resolved.build?.ssr || resolved.build?.lib) return;
|
|
2231
|
+
const base = config.shareBase ?? resolved.base;
|
|
2232
|
+
shareBuild = {
|
|
2233
|
+
root: resolved.root,
|
|
2234
|
+
outDir: path9.resolve(resolved.root, resolved.build.outDir),
|
|
2235
|
+
base,
|
|
2236
|
+
appBase: normalizeBase(base),
|
|
2237
|
+
logger: resolved.logger
|
|
2238
|
+
};
|
|
2239
|
+
},
|
|
2240
|
+
buildStart() {
|
|
2241
|
+
packed = false;
|
|
2242
|
+
},
|
|
2243
|
+
async writeBundle() {
|
|
2244
|
+
if (shareBuild === null || packed) return;
|
|
2245
|
+
packed = true;
|
|
2246
|
+
const result = await packOutput({
|
|
2247
|
+
outDir: shareBuild.outDir,
|
|
2248
|
+
projectDir: shareBuild.root,
|
|
2249
|
+
// Resolved against the working directory, as the serve half does.
|
|
2250
|
+
localesPath: path9.resolve(config.localesPath),
|
|
2251
|
+
sourceLocale: config.sourceLocale,
|
|
2252
|
+
exposeRepo: config.exposeRepo ?? false,
|
|
2253
|
+
base: shareBuild.base,
|
|
2254
|
+
// transformIndexHtml already linked flag.js in every HTML file Vite wrote.
|
|
2255
|
+
injectHtml: false
|
|
2256
|
+
});
|
|
2257
|
+
for (const warning of result.warnings) shareBuild.logger.warn(warning);
|
|
2258
|
+
},
|
|
1601
2259
|
configureServer(server) {
|
|
1602
2260
|
server.middlewares.use(async (req, res, next) => {
|
|
1603
2261
|
const url = req.url ?? "";
|
|
@@ -1634,7 +2292,11 @@ function liveLocaleVite(config) {
|
|
|
1634
2292
|
}
|
|
1635
2293
|
});
|
|
1636
2294
|
},
|
|
1637
|
-
transformIndexHtml(html) {
|
|
2295
|
+
transformIndexHtml(html, ctx) {
|
|
2296
|
+
if (shareBuild !== null) {
|
|
2297
|
+
const htmlRelPath = (ctx?.path ?? "/index.html").replace(/^\/+/, "");
|
|
2298
|
+
return injectFlagScript(html, flagSrcFor(shareBuild.appBase, htmlRelPath));
|
|
2299
|
+
}
|
|
1638
2300
|
const flag = process.env["LIVELOCALE"];
|
|
1639
2301
|
const flagScript = flag ? `<script>window.${BROWSER_FLAG_GLOBAL} = ${JSON.stringify(flag)};</script>
|
|
1640
2302
|
` : "";
|
|
@@ -1642,24 +2304,24 @@ function liveLocaleVite(config) {
|
|
|
1642
2304
|
return flagScript ? html.replace("</head>", `${flagScript}</head>`) : html;
|
|
1643
2305
|
}
|
|
1644
2306
|
try {
|
|
1645
|
-
const resolvedLocalesPath =
|
|
1646
|
-
const entries =
|
|
2307
|
+
const resolvedLocalesPath = path9.resolve(config.localesPath);
|
|
2308
|
+
const entries = fs5.readdirSync(resolvedLocalesPath);
|
|
1647
2309
|
let fallbackData = {};
|
|
1648
2310
|
for (const entry of entries) {
|
|
1649
2311
|
if (entry.startsWith(".")) continue;
|
|
1650
|
-
const entryPath =
|
|
2312
|
+
const entryPath = path9.join(resolvedLocalesPath, entry);
|
|
1651
2313
|
let entryIsDir = false;
|
|
1652
2314
|
try {
|
|
1653
|
-
entryIsDir =
|
|
2315
|
+
entryIsDir = fs5.statSync(entryPath).isDirectory();
|
|
1654
2316
|
} catch {
|
|
1655
2317
|
}
|
|
1656
2318
|
if (entryIsDir) {
|
|
1657
2319
|
try {
|
|
1658
|
-
const files =
|
|
2320
|
+
const files = fs5.readdirSync(entryPath);
|
|
1659
2321
|
for (const file of files) {
|
|
1660
2322
|
if (file.endsWith(".json")) {
|
|
1661
2323
|
const ns = file.slice(0, -5);
|
|
1662
|
-
const raw =
|
|
2324
|
+
const raw = fs5.readFileSync(path9.join(entryPath, file), "utf-8");
|
|
1663
2325
|
try {
|
|
1664
2326
|
const parsed = JSON.parse(raw);
|
|
1665
2327
|
fallbackData[ns] = deepMerge(fallbackData[ns] || {}, parsed);
|
|
@@ -1670,7 +2332,7 @@ function liveLocaleVite(config) {
|
|
|
1670
2332
|
} catch {
|
|
1671
2333
|
}
|
|
1672
2334
|
} else if (entry.endsWith(".json")) {
|
|
1673
|
-
const raw =
|
|
2335
|
+
const raw = fs5.readFileSync(entryPath, "utf-8");
|
|
1674
2336
|
try {
|
|
1675
2337
|
const parsed = JSON.parse(raw);
|
|
1676
2338
|
fallbackData = deepMerge(fallbackData, parsed);
|