@bedrock-rbx/core 0.1.0-beta.1 → 0.1.0-beta.10
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/dist/cli/run.mjs +22 -14
- package/dist/cli/run.mjs.map +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/{define-config-D-LAhfSJ.d.mts → define-config-87u2jqjM.d.mts} +8 -16
- package/dist/{define-config-D-LAhfSJ.d.mts.map → define-config-87u2jqjM.d.mts.map} +1 -1
- package/dist/index.d.mts +10 -33
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{migrate-mantle-state-DqbJ1TLq.mjs → migrate-mantle-state-DjAU-I39.mjs} +299 -348
- package/dist/migrate-mantle-state-DjAU-I39.mjs.map +1 -0
- package/package.json +4 -4
- package/dist/migrate-mantle-state-DqbJ1TLq.mjs.map +0 -1
|
@@ -8,10 +8,10 @@ import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
|
8
8
|
import { UniversesClient } from "@bedrock-rbx/ocale/universes";
|
|
9
9
|
import { readFile } from "node:fs/promises";
|
|
10
10
|
import { loadConfig } from "c12";
|
|
11
|
-
import { execFile } from "node:child_process";
|
|
12
11
|
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
12
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
15
|
import { parseYAML, stringifyYAML } from "confbox";
|
|
16
16
|
//#region src/core/derive-price-fields.ts
|
|
17
17
|
/**
|
|
@@ -861,6 +861,8 @@ const RETRYABLE_STATUSES = new Set([
|
|
|
861
861
|
503,
|
|
862
862
|
504
|
|
863
863
|
]);
|
|
864
|
+
const MAX_VISIBILITY_ATTEMPTS = 5;
|
|
865
|
+
const VISIBILITY_BASE_DELAY_MS = 250;
|
|
864
866
|
/**
|
|
865
867
|
* Build a `StatePort` that persists Bedrock state in a GitHub Gist.
|
|
866
868
|
*
|
|
@@ -1066,6 +1068,38 @@ async function sendPatch(ctx, body) {
|
|
|
1066
1068
|
method: "PATCH"
|
|
1067
1069
|
});
|
|
1068
1070
|
}
|
|
1071
|
+
async function isFileVisible(ctx, target) {
|
|
1072
|
+
try {
|
|
1073
|
+
const response = await sendGet(ctx);
|
|
1074
|
+
const body = JSON.parse(await response.text());
|
|
1075
|
+
const files = Reflect.get(body, "files");
|
|
1076
|
+
return typeof files === "object" && files !== null && target in files;
|
|
1077
|
+
} catch {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Polls the gist until the just-written environment file is visible on a
|
|
1083
|
+
* GET, with bounded retries. GitHub's gist API does not guarantee
|
|
1084
|
+
* read-your-write across replicas: a GET issued immediately after a
|
|
1085
|
+
* successful PATCH can return a body that omits the new file. The poll
|
|
1086
|
+
* pre-warms the cache the consumer's next read will hit, so a successful
|
|
1087
|
+
* write honours read-after-write at the port boundary.
|
|
1088
|
+
*
|
|
1089
|
+
* Best-effort: resolves after exhausting the visibility budget regardless
|
|
1090
|
+
* of whether the file became visible. The PATCH already committed; the
|
|
1091
|
+
* poll only narrows the window in which subsequent reads can lag.
|
|
1092
|
+
*
|
|
1093
|
+
* @param ctx - Adapter context carrying the injected fetch and sleep seams.
|
|
1094
|
+
* @param environment - Environment name whose file is being verified.
|
|
1095
|
+
*/
|
|
1096
|
+
async function waitForFileVisibility(ctx, environment) {
|
|
1097
|
+
const target = fileName(environment);
|
|
1098
|
+
for (let attempt = 0; attempt < MAX_VISIBILITY_ATTEMPTS; attempt += 1) {
|
|
1099
|
+
if (await isFileVisible(ctx, target)) return;
|
|
1100
|
+
if (attempt < MAX_VISIBILITY_ATTEMPTS - 1) await ctx.sleep(VISIBILITY_BASE_DELAY_MS * 2 ** attempt);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1069
1103
|
async function writePath(ctx, state) {
|
|
1070
1104
|
const file = fileLabel(ctx.gistId, state.environment);
|
|
1071
1105
|
const body = JSON.stringify({ files: { [fileName(state.environment)]: { content: serializeStateFile(state) } } });
|
|
@@ -1078,10 +1112,15 @@ async function writePath(ctx, state) {
|
|
|
1078
1112
|
success: false
|
|
1079
1113
|
};
|
|
1080
1114
|
}
|
|
1081
|
-
if (response.ok)
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1115
|
+
if (response.ok) {
|
|
1116
|
+
try {
|
|
1117
|
+
await waitForFileVisibility(ctx, state.environment);
|
|
1118
|
+
} catch {}
|
|
1119
|
+
return {
|
|
1120
|
+
data: void 0,
|
|
1121
|
+
success: true
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1085
1124
|
if (response.status === 422) return stateErr(file, "invalid PATCH body sent to github");
|
|
1086
1125
|
return {
|
|
1087
1126
|
err: mapHttpError({
|
|
@@ -1357,7 +1396,6 @@ async function publishPlace(deps, desired) {
|
|
|
1357
1396
|
* httpClient: universeBodyHttpClient,
|
|
1358
1397
|
* sleep: async () => {},
|
|
1359
1398
|
* }),
|
|
1360
|
-
* readFile: async () => new Uint8Array(),
|
|
1361
1399
|
* universes: new UniversesClient({
|
|
1362
1400
|
* apiKey: "rbx-your-key",
|
|
1363
1401
|
* httpClient: universeBodyHttpClient,
|
|
@@ -1391,29 +1429,22 @@ function createUniverseDriver(deps) {
|
|
|
1391
1429
|
return {
|
|
1392
1430
|
async create(desired) {
|
|
1393
1431
|
return reconcileUniverse({
|
|
1394
|
-
current: void 0,
|
|
1395
1432
|
deps,
|
|
1396
1433
|
desired
|
|
1397
1434
|
});
|
|
1398
1435
|
},
|
|
1399
|
-
async update(
|
|
1436
|
+
async update(_current, desired) {
|
|
1400
1437
|
return reconcileUniverse({
|
|
1401
|
-
current,
|
|
1402
1438
|
deps,
|
|
1403
1439
|
desired
|
|
1404
1440
|
});
|
|
1405
1441
|
}
|
|
1406
1442
|
};
|
|
1407
1443
|
}
|
|
1408
|
-
function toCurrentState(
|
|
1409
|
-
const { desired, iconAssetIds, rootPlaceId } = inputs;
|
|
1410
|
-
const baseOutputs = { rootPlaceId: asRobloxAssetId(rootPlaceId) };
|
|
1444
|
+
function toCurrentState(desired, rootPlaceId) {
|
|
1411
1445
|
return {
|
|
1412
1446
|
...desired,
|
|
1413
|
-
outputs:
|
|
1414
|
-
...baseOutputs,
|
|
1415
|
-
iconAssetIds
|
|
1416
|
-
}
|
|
1447
|
+
outputs: { rootPlaceId: asRobloxAssetId(rootPlaceId) }
|
|
1417
1448
|
};
|
|
1418
1449
|
}
|
|
1419
1450
|
function buildParameters(desired) {
|
|
@@ -1457,46 +1488,8 @@ async function resolveUniverse(deps, desired) {
|
|
|
1457
1488
|
success: true
|
|
1458
1489
|
};
|
|
1459
1490
|
}
|
|
1460
|
-
async function captureUploadedIconAssetId(deps, desired) {
|
|
1461
|
-
const listed = await deps.universes.icon.list({ universeId: desired.universeId });
|
|
1462
|
-
if (!listed.success) return listed;
|
|
1463
|
-
const enUs = listed.data.find((entry) => entry.languageCode === "en-us");
|
|
1464
|
-
if (enUs === void 0) return {
|
|
1465
|
-
err: new ApiError(`Malformed experience-icon list for ${desired.universeId}: en-us entry missing after upload`, { statusCode: 200 }),
|
|
1466
|
-
success: false
|
|
1467
|
-
};
|
|
1468
|
-
return {
|
|
1469
|
-
data: { "en-us": asRobloxAssetId(enUs.imageId) },
|
|
1470
|
-
success: true
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
async function deleteRemovedIcon(deps, desired) {
|
|
1474
|
-
return deps.universes.icon.delete({
|
|
1475
|
-
languageCode: "en-us",
|
|
1476
|
-
universeId: desired.universeId
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
async function reconcileIcon(inputs) {
|
|
1480
|
-
const { current, deps, desired } = inputs;
|
|
1481
|
-
if (desired.icon === void 0) return current?.icon === void 0 ? {
|
|
1482
|
-
data: void 0,
|
|
1483
|
-
success: true
|
|
1484
|
-
} : deleteRemovedIcon(deps, desired);
|
|
1485
|
-
if (!shouldReuploadIcon(current?.iconFileHashes, desired.iconFileHashes)) return {
|
|
1486
|
-
data: current?.outputs.iconAssetIds,
|
|
1487
|
-
success: true
|
|
1488
|
-
};
|
|
1489
|
-
const bytes = await deps.readFile(desired.icon["en-us"]);
|
|
1490
|
-
const uploaded = await deps.universes.icon.upload({
|
|
1491
|
-
image: bytes,
|
|
1492
|
-
languageCode: "en-us",
|
|
1493
|
-
universeId: desired.universeId
|
|
1494
|
-
});
|
|
1495
|
-
if (!uploaded.success) return uploaded;
|
|
1496
|
-
return captureUploadedIconAssetId(deps, desired);
|
|
1497
|
-
}
|
|
1498
1491
|
async function reconcileUniverse(inputs) {
|
|
1499
|
-
const {
|
|
1492
|
+
const { deps, desired } = inputs;
|
|
1500
1493
|
const universeResult = await resolveUniverse(deps, desired);
|
|
1501
1494
|
if (!universeResult.success) return universeResult;
|
|
1502
1495
|
const { rootPlaceId } = universeResult.data;
|
|
@@ -1511,18 +1504,8 @@ async function reconcileUniverse(inputs) {
|
|
|
1511
1504
|
success: false
|
|
1512
1505
|
};
|
|
1513
1506
|
}
|
|
1514
|
-
const iconResult = await reconcileIcon({
|
|
1515
|
-
current,
|
|
1516
|
-
deps,
|
|
1517
|
-
desired
|
|
1518
|
-
});
|
|
1519
|
-
if (!iconResult.success) return iconResult;
|
|
1520
1507
|
return {
|
|
1521
|
-
data: toCurrentState(
|
|
1522
|
-
desired,
|
|
1523
|
-
iconAssetIds: iconResult.data,
|
|
1524
|
-
rootPlaceId
|
|
1525
|
-
}),
|
|
1508
|
+
data: toCurrentState(desired, rootPlaceId),
|
|
1526
1509
|
success: true
|
|
1527
1510
|
};
|
|
1528
1511
|
}
|
|
@@ -1656,7 +1639,6 @@ const universeEntry = type({
|
|
|
1656
1639
|
"displayName?": OPTIONAL_STRING,
|
|
1657
1640
|
"facebookSocialLink?": socialLinkOrUndefined$1,
|
|
1658
1641
|
"guildedSocialLink?": socialLinkOrUndefined$1,
|
|
1659
|
-
"icon?": iconMap,
|
|
1660
1642
|
"mobileEnabled?": OPTIONAL_BOOLEAN$2,
|
|
1661
1643
|
"privateServerPriceRobux?": OPTIONAL_ROBUX_PRICE,
|
|
1662
1644
|
"robloxGroupSocialLink?": socialLinkOrUndefined$1,
|
|
@@ -2004,7 +1986,6 @@ const entrySchema = type({
|
|
|
2004
1986
|
"displayName?": "string | undefined",
|
|
2005
1987
|
"facebookSocialLink?": socialLinkOrUndefined,
|
|
2006
1988
|
"guildedSocialLink?": socialLinkOrUndefined,
|
|
2007
|
-
"icon?": iconMap,
|
|
2008
1989
|
"mobileEnabled?": OPTIONAL_BOOLEAN,
|
|
2009
1990
|
"privateServerPriceRobux?": "number.integer >= 0 | undefined",
|
|
2010
1991
|
"robloxGroupSocialLink?": socialLinkOrUndefined,
|
|
@@ -2032,14 +2013,10 @@ function flatten(config) {
|
|
|
2032
2013
|
vrEnabled: entry.vrEnabled,
|
|
2033
2014
|
...copyDeclaredSocialLinks(entry)
|
|
2034
2015
|
};
|
|
2035
|
-
|
|
2016
|
+
return ["privateServerPriceRobux" in entry ? {
|
|
2036
2017
|
...base,
|
|
2037
2018
|
privateServerPriceRobux: entry.privateServerPriceRobux
|
|
2038
|
-
} : base;
|
|
2039
|
-
return [entry.icon === void 0 ? withPrice : {
|
|
2040
|
-
...withPrice,
|
|
2041
|
-
icon: entry.icon
|
|
2042
|
-
}];
|
|
2019
|
+
} : base];
|
|
2043
2020
|
}
|
|
2044
2021
|
function buildBaseDesired(input) {
|
|
2045
2022
|
const base = {
|
|
@@ -2060,23 +2037,9 @@ function buildBaseDesired(input) {
|
|
|
2060
2037
|
privateServerPriceRobux: input.privateServerPriceRobux
|
|
2061
2038
|
} : base;
|
|
2062
2039
|
}
|
|
2063
|
-
async function normalize(input,
|
|
2064
|
-
const withPrice = buildBaseDesired(input);
|
|
2065
|
-
if (input.icon === void 0) return {
|
|
2066
|
-
data: withPrice,
|
|
2067
|
-
success: true
|
|
2068
|
-
};
|
|
2069
|
-
const hashes = await hashIconLocales({
|
|
2070
|
-
key: input.key,
|
|
2071
|
-
icon: input.icon
|
|
2072
|
-
}, io);
|
|
2073
|
-
if (!hashes.success) return hashes;
|
|
2040
|
+
async function normalize(input, _io) {
|
|
2074
2041
|
return {
|
|
2075
|
-
data:
|
|
2076
|
-
...withPrice,
|
|
2077
|
-
icon: input.icon,
|
|
2078
|
-
iconFileHashes: hashes.data
|
|
2079
|
-
},
|
|
2042
|
+
data: buildBaseDesired(input),
|
|
2080
2043
|
success: true
|
|
2081
2044
|
};
|
|
2082
2045
|
}
|
|
@@ -2100,7 +2063,6 @@ function fieldsEqual(desired, current) {
|
|
|
2100
2063
|
}
|
|
2101
2064
|
if (desired.displayName !== void 0 && desired.displayName !== current.displayName) return false;
|
|
2102
2065
|
if ("privateServerPriceRobux" in desired && desired.privateServerPriceRobux !== current.privateServerPriceRobux) return false;
|
|
2103
|
-
if (!iconHashesEqual(current.iconFileHashes, desired.iconFileHashes)) return false;
|
|
2104
2066
|
return declaredSocialLinksEqual(desired, current);
|
|
2105
2067
|
}
|
|
2106
2068
|
//#endregion
|
|
@@ -2832,7 +2794,7 @@ async function dispatchOp(op, registry) {
|
|
|
2832
2794
|
//#region src/shell/build-default-registry.ts
|
|
2833
2795
|
/**
|
|
2834
2796
|
* Construct the default `DriverRegistry` from `config.universe.universeId`
|
|
2835
|
-
* and `
|
|
2797
|
+
* and `BEDROCK_API_KEY`. Reads the API key via the injected `getEnv` seam
|
|
2836
2798
|
* and surfaces `missingCredential` or `registryConfigMissing` as typed
|
|
2837
2799
|
* Results instead of throwing.
|
|
2838
2800
|
*
|
|
@@ -2859,7 +2821,7 @@ async function dispatchOp(op, registry) {
|
|
|
2859
2821
|
* missing API key or the missing universe declaration.
|
|
2860
2822
|
*/
|
|
2861
2823
|
function buildDefaultRegistry(deps) {
|
|
2862
|
-
const apiKey = deps.getEnv("
|
|
2824
|
+
const apiKey = deps.getEnv("BEDROCK_API_KEY");
|
|
2863
2825
|
if (apiKey === void 0) return missingApiKey();
|
|
2864
2826
|
const rawUniverseId = deps.config.universe?.universeId;
|
|
2865
2827
|
if (rawUniverseId === void 0) return missingUniverseId();
|
|
@@ -2877,7 +2839,7 @@ function missingApiKey() {
|
|
|
2877
2839
|
err: {
|
|
2878
2840
|
kind: "missingCredential",
|
|
2879
2841
|
purpose: "registry",
|
|
2880
|
-
variable: "
|
|
2842
|
+
variable: "BEDROCK_API_KEY"
|
|
2881
2843
|
},
|
|
2882
2844
|
success: false
|
|
2883
2845
|
};
|
|
@@ -2916,7 +2878,6 @@ function assembleRegistry(inputs) {
|
|
|
2916
2878
|
}),
|
|
2917
2879
|
universe: createUniverseDriver({
|
|
2918
2880
|
places,
|
|
2919
|
-
readFile,
|
|
2920
2881
|
universes
|
|
2921
2882
|
})
|
|
2922
2883
|
};
|
|
@@ -3057,8 +3018,169 @@ function bootstrapDirectoryPrefix(pid) {
|
|
|
3057
3018
|
return `${LUAU_BOOTSTRAP_TEMP_PREFIX}${pid}-`;
|
|
3058
3019
|
}
|
|
3059
3020
|
//#endregion
|
|
3021
|
+
//#region src/adapters/lute-luau-evaluator.ts
|
|
3022
|
+
const SENTINEL_BASE = "__BEDROCK_LUAU_";
|
|
3023
|
+
const OK_PREFIX = `${SENTINEL_BASE}OK__`;
|
|
3024
|
+
const ERR_PREFIX = `${SENTINEL_BASE}ERR__`;
|
|
3025
|
+
const LUTE_BOOTSTRAP_LUAU = `--!strict
|
|
3026
|
+
local json = require("@std/json")
|
|
3027
|
+
local process = require("@std/process")
|
|
3028
|
+
local io = require("@std/io")
|
|
3029
|
+
|
|
3030
|
+
local function emit(kind, payload)
|
|
3031
|
+
io.write("${SENTINEL_BASE}" .. kind .. "__")
|
|
3032
|
+
io.write(json.serialize(payload))
|
|
3033
|
+
end
|
|
3034
|
+
|
|
3035
|
+
local userBasename = process.args[2]
|
|
3036
|
+
-- The user file lives in a different directory from this bootstrap, so we
|
|
3037
|
+
-- require it via the @user alias defined in the .luaurc written alongside.
|
|
3038
|
+
local req = "@user/" .. string.gsub(userBasename, "%.luau$", "")
|
|
3039
|
+
|
|
3040
|
+
local loadOk, modOrErr = pcall(require, req)
|
|
3041
|
+
if not loadOk then
|
|
3042
|
+
emit("ERR", { kind = "loadFailed", message = tostring(modOrErr) })
|
|
3043
|
+
return
|
|
3044
|
+
end
|
|
3045
|
+
|
|
3046
|
+
local value = if type(modOrErr) == "function" then modOrErr() else modOrErr
|
|
3047
|
+
|
|
3048
|
+
local encOk, encoded = pcall(json.serialize, value)
|
|
3049
|
+
if not encOk then
|
|
3050
|
+
emit("ERR", { kind = "serializeFailed", message = tostring(encoded) })
|
|
3051
|
+
return
|
|
3052
|
+
end
|
|
3053
|
+
|
|
3054
|
+
io.write("${OK_PREFIX}")
|
|
3055
|
+
io.write(encoded)
|
|
3056
|
+
`;
|
|
3057
|
+
const LUAU_RUNTIME_HINT = "install lute (e.g. `mise install` with `github:luau-lang/lute`) or set BEDROCK_LUTE_PATH to the binary.";
|
|
3058
|
+
function isEnoentError(error) {
|
|
3059
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
3060
|
+
}
|
|
3061
|
+
const LUTE_BOOTSTRAP_TIMEOUT_MS = 5e3;
|
|
3062
|
+
/**
|
|
3063
|
+
* Build the default `LuauEvaluator` adapter that shells out to the `lute`
|
|
3064
|
+
* runtime. Reads `BEDROCK_LUTE_PATH` from `process.env` once per call to pick
|
|
3065
|
+
* the binary, so tests can override it via env var without rebuilding the
|
|
3066
|
+
* adapter.
|
|
3067
|
+
* @returns A `LuauEvaluator` that spawns `lute run` per call.
|
|
3068
|
+
*/
|
|
3069
|
+
function createLuteLuauEvaluator() {
|
|
3070
|
+
return evaluateLuauWithLute;
|
|
3071
|
+
}
|
|
3072
|
+
function setupBootstrapDirectory(userCwd) {
|
|
3073
|
+
const bootstrapDirectory = mkdtempSync(join(tmpdir(), bootstrapDirectoryPrefix(process.pid)));
|
|
3074
|
+
writeFileSync(join(bootstrapDirectory, "bootstrap.luau"), LUTE_BOOTSTRAP_LUAU);
|
|
3075
|
+
writeFileSync(join(bootstrapDirectory, ".luaurc"), JSON.stringify({ aliases: { user: userCwd } }));
|
|
3076
|
+
return bootstrapDirectory;
|
|
3077
|
+
}
|
|
3078
|
+
async function runLuteBootstrap(runOptions) {
|
|
3079
|
+
const { bin, bootstrapPath, userBasename } = runOptions;
|
|
3080
|
+
return new Promise((resolve, reject) => {
|
|
3081
|
+
execFile(bin, [
|
|
3082
|
+
"run",
|
|
3083
|
+
bootstrapPath,
|
|
3084
|
+
userBasename
|
|
3085
|
+
], {
|
|
3086
|
+
encoding: "utf8",
|
|
3087
|
+
timeout: LUTE_BOOTSTRAP_TIMEOUT_MS
|
|
3088
|
+
}, (error, stdout) => {
|
|
3089
|
+
if (error instanceof Error) {
|
|
3090
|
+
reject(error);
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
resolve(stdout);
|
|
3094
|
+
});
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
function parseBootstrapOutput(stdout) {
|
|
3098
|
+
if (stdout.startsWith(ERR_PREFIX)) throw new Error(stdout.slice(ERR_PREFIX.length));
|
|
3099
|
+
const parsed = JSON.parse(stdout.slice(OK_PREFIX.length));
|
|
3100
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("Luau config must return a table at the root");
|
|
3101
|
+
return parsed;
|
|
3102
|
+
}
|
|
3103
|
+
async function evaluateLuauWithLute(absPath) {
|
|
3104
|
+
const overridePath = process.env["BEDROCK_LUTE_PATH"];
|
|
3105
|
+
const lute = overridePath !== void 0 && overridePath.length > 0 ? overridePath : "lute";
|
|
3106
|
+
const bootstrapDirectory = setupBootstrapDirectory(dirname(absPath));
|
|
3107
|
+
try {
|
|
3108
|
+
return {
|
|
3109
|
+
data: parseBootstrapOutput(await runLuteBootstrap({
|
|
3110
|
+
bin: lute,
|
|
3111
|
+
bootstrapPath: join(bootstrapDirectory, "bootstrap.luau"),
|
|
3112
|
+
userBasename: basename(absPath)
|
|
3113
|
+
})),
|
|
3114
|
+
success: true
|
|
3115
|
+
};
|
|
3116
|
+
} catch (err) {
|
|
3117
|
+
if (isEnoentError(err)) return {
|
|
3118
|
+
err: {
|
|
3119
|
+
hint: LUAU_RUNTIME_HINT,
|
|
3120
|
+
kind: "missingRuntime"
|
|
3121
|
+
},
|
|
3122
|
+
success: false
|
|
3123
|
+
};
|
|
3124
|
+
return {
|
|
3125
|
+
err: {
|
|
3126
|
+
kind: "evaluationFailed",
|
|
3127
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3128
|
+
},
|
|
3129
|
+
success: false
|
|
3130
|
+
};
|
|
3131
|
+
} finally {
|
|
3132
|
+
rmSync(bootstrapDirectory, { recursive: true });
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
//#endregion
|
|
3060
3136
|
//#region src/shell/load-config.ts
|
|
3061
3137
|
/**
|
|
3138
|
+
* Internal entrypoint that lets tests inject a fake `LuauEvaluator`. The
|
|
3139
|
+
* public {@link loadConfig} wraps this with the real lute adapter; the rest
|
|
3140
|
+
* of the loader pipeline is identical.
|
|
3141
|
+
*
|
|
3142
|
+
* @param deps - Injected dependencies. Only the evaluator is configurable.
|
|
3143
|
+
* @param options - Same loader options accepted by {@link loadConfig}.
|
|
3144
|
+
* @returns Same `Result<Config, ConfigError>` shape as `loadConfig`.
|
|
3145
|
+
*/
|
|
3146
|
+
async function loadConfigWith(deps, options) {
|
|
3147
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
3148
|
+
const configFile = options?.configFile === void 0 ? void 0 : resolveConfigPath(cwd, options.configFile);
|
|
3149
|
+
if (configFile !== void 0 && !isExistingFile(configFile)) return {
|
|
3150
|
+
err: {
|
|
3151
|
+
kind: "fileNotFound",
|
|
3152
|
+
searchedFrom: cwd
|
|
3153
|
+
},
|
|
3154
|
+
success: false
|
|
3155
|
+
};
|
|
3156
|
+
let resolved;
|
|
3157
|
+
try {
|
|
3158
|
+
resolved = await loadConfig({
|
|
3159
|
+
name: "bedrock",
|
|
3160
|
+
cwd,
|
|
3161
|
+
resolve: makeLuauResolver({
|
|
3162
|
+
callerConfigFile: configFile,
|
|
3163
|
+
defaultCwd: cwd,
|
|
3164
|
+
evaluator: deps.evaluator
|
|
3165
|
+
}),
|
|
3166
|
+
...configFile === void 0 ? {} : { configFile }
|
|
3167
|
+
});
|
|
3168
|
+
} catch (err) {
|
|
3169
|
+
return {
|
|
3170
|
+
err: attributeLoadError(err, cwd),
|
|
3171
|
+
success: false
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
if (resolved._configFile === void 0) return {
|
|
3175
|
+
err: {
|
|
3176
|
+
kind: "fileNotFound",
|
|
3177
|
+
searchedFrom: cwd
|
|
3178
|
+
},
|
|
3179
|
+
success: false
|
|
3180
|
+
};
|
|
3181
|
+
return validateConfig(resolved.config, resolved._configFile);
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3062
3184
|
* Discover, parse, and validate the project config.
|
|
3063
3185
|
*
|
|
3064
3186
|
* Looks for `bedrock.config.{ts,js,mjs,cjs,yaml,yml,json}`, `.bedrockrc*`,
|
|
@@ -3098,37 +3220,7 @@ function bootstrapDirectoryPrefix(pid) {
|
|
|
3098
3220
|
* ```
|
|
3099
3221
|
*/
|
|
3100
3222
|
async function loadConfig$1(options) {
|
|
3101
|
-
|
|
3102
|
-
const configFile = options?.configFile === void 0 ? void 0 : resolveConfigPath(cwd, options.configFile);
|
|
3103
|
-
if (configFile !== void 0 && !isExistingFile(configFile)) return {
|
|
3104
|
-
err: {
|
|
3105
|
-
kind: "fileNotFound",
|
|
3106
|
-
searchedFrom: cwd
|
|
3107
|
-
},
|
|
3108
|
-
success: false
|
|
3109
|
-
};
|
|
3110
|
-
let resolved;
|
|
3111
|
-
try {
|
|
3112
|
-
resolved = await loadConfig({
|
|
3113
|
-
name: "bedrock",
|
|
3114
|
-
cwd,
|
|
3115
|
-
resolve: makeLuauResolver(cwd, configFile),
|
|
3116
|
-
...configFile === void 0 ? {} : { configFile }
|
|
3117
|
-
});
|
|
3118
|
-
} catch (err) {
|
|
3119
|
-
return {
|
|
3120
|
-
err: attributeLoadError(err, cwd),
|
|
3121
|
-
success: false
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
if (resolved._configFile === void 0) return {
|
|
3125
|
-
err: {
|
|
3126
|
-
kind: "fileNotFound",
|
|
3127
|
-
searchedFrom: cwd
|
|
3128
|
-
},
|
|
3129
|
-
success: false
|
|
3130
|
-
};
|
|
3131
|
-
return validateConfig(resolved.config, resolved._configFile);
|
|
3223
|
+
return loadConfigWith({ evaluator: createLuteLuauEvaluator() }, options);
|
|
3132
3224
|
}
|
|
3133
3225
|
function resolveConfigPath(cwd, configFile) {
|
|
3134
3226
|
return isAbsolute(configFile) ? configFile : join(cwd, configFile);
|
|
@@ -3181,6 +3273,19 @@ const NATIVE_CONFIG_EXTENSIONS = [
|
|
|
3181
3273
|
"yml"
|
|
3182
3274
|
];
|
|
3183
3275
|
/**
|
|
3276
|
+
* Internal-only wrapper used at the c12 boundary: makeLuauResolver maps an
|
|
3277
|
+
* evaluator `Err` into this throwable, which `attributeLoadError` unwraps
|
|
3278
|
+
* directly. This keeps the port on the `Result` contract per ADR-009 while
|
|
3279
|
+
* still satisfying c12's exception-based `resolve` callback.
|
|
3280
|
+
*/
|
|
3281
|
+
var EvaluatorThrow = class extends Error {
|
|
3282
|
+
configError;
|
|
3283
|
+
constructor(configError) {
|
|
3284
|
+
super();
|
|
3285
|
+
this.configError = configError;
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
/**
|
|
3184
3289
|
* Decide which Luau file the resolver should evaluate for a given c12 source,
|
|
3185
3290
|
* or `undefined` to defer to c12's built-in loaders.
|
|
3186
3291
|
*
|
|
@@ -3199,120 +3304,37 @@ function pickLuauTarget(source, context) {
|
|
|
3199
3304
|
if (source === "." && callerConfigFile !== void 0) return callerConfigFile.endsWith(".luau") ? callerConfigFile : void 0;
|
|
3200
3305
|
return locateLuauConfig(source, cwd);
|
|
3201
3306
|
}
|
|
3202
|
-
function
|
|
3307
|
+
function evaluationErrorToConfigError(err, sourceFile) {
|
|
3308
|
+
if (err.kind === "missingRuntime") return {
|
|
3309
|
+
hint: err.hint,
|
|
3310
|
+
kind: "luauRuntimeMissing",
|
|
3311
|
+
sourceFile
|
|
3312
|
+
};
|
|
3313
|
+
return {
|
|
3314
|
+
kind: "parseFailed",
|
|
3315
|
+
message: err.message,
|
|
3316
|
+
sourceFile
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
function makeLuauResolver(deps) {
|
|
3203
3320
|
return async (source, c12Options) => {
|
|
3204
|
-
const cwd = c12Options.cwd ?? defaultCwd;
|
|
3321
|
+
const cwd = c12Options.cwd ?? deps.defaultCwd;
|
|
3205
3322
|
const luauPath = pickLuauTarget(source, {
|
|
3206
|
-
callerConfigFile,
|
|
3323
|
+
callerConfigFile: deps.callerConfigFile,
|
|
3207
3324
|
cwd
|
|
3208
3325
|
});
|
|
3209
3326
|
if (luauPath === void 0) return;
|
|
3327
|
+
const result = await deps.evaluator(luauPath);
|
|
3328
|
+
if (!result.success) throw new EvaluatorThrow(evaluationErrorToConfigError(result.err, luauPath));
|
|
3210
3329
|
return {
|
|
3211
3330
|
_configFile: luauPath,
|
|
3212
|
-
config:
|
|
3331
|
+
config: result.data,
|
|
3213
3332
|
configFile: luauPath,
|
|
3214
3333
|
cwd
|
|
3215
3334
|
};
|
|
3216
3335
|
};
|
|
3217
3336
|
}
|
|
3218
3337
|
const LUAU_CONFIG_BASENAME = "bedrock.config.luau";
|
|
3219
|
-
const SENTINEL_BASE = "__BEDROCK_LUAU_";
|
|
3220
|
-
const OK_PREFIX = `${SENTINEL_BASE}OK__`;
|
|
3221
|
-
const ERR_PREFIX = `${SENTINEL_BASE}ERR__`;
|
|
3222
|
-
const LUTE_BOOTSTRAP_LUAU = `--!strict
|
|
3223
|
-
local json = require("@std/json")
|
|
3224
|
-
local process = require("@std/process")
|
|
3225
|
-
local io = require("@std/io")
|
|
3226
|
-
|
|
3227
|
-
local function emit(kind, payload)
|
|
3228
|
-
io.write("${SENTINEL_BASE}" .. kind .. "__")
|
|
3229
|
-
io.write(json.serialize(payload))
|
|
3230
|
-
end
|
|
3231
|
-
|
|
3232
|
-
local userBasename = process.args[2]
|
|
3233
|
-
-- The user file lives in a different directory from this bootstrap, so we
|
|
3234
|
-
-- require it via the @user alias defined in the .luaurc written alongside.
|
|
3235
|
-
local req = "@user/" .. string.gsub(userBasename, "%.luau$", "")
|
|
3236
|
-
|
|
3237
|
-
local loadOk, modOrErr = pcall(require, req)
|
|
3238
|
-
if not loadOk then
|
|
3239
|
-
emit("ERR", { kind = "loadFailed", message = tostring(modOrErr) })
|
|
3240
|
-
return
|
|
3241
|
-
end
|
|
3242
|
-
|
|
3243
|
-
local value = if type(modOrErr) == "function" then modOrErr() else modOrErr
|
|
3244
|
-
|
|
3245
|
-
local encOk, encoded = pcall(json.serialize, value)
|
|
3246
|
-
if not encOk then
|
|
3247
|
-
emit("ERR", { kind = "serializeFailed", message = tostring(encoded) })
|
|
3248
|
-
return
|
|
3249
|
-
end
|
|
3250
|
-
|
|
3251
|
-
io.write("${OK_PREFIX}")
|
|
3252
|
-
io.write(encoded)
|
|
3253
|
-
`;
|
|
3254
|
-
var LuauRuntimeMissingError = class extends Error {
|
|
3255
|
-
hint;
|
|
3256
|
-
name = "LuauRuntimeMissingError";
|
|
3257
|
-
sourceFile;
|
|
3258
|
-
constructor(sourceFile, hint) {
|
|
3259
|
-
super();
|
|
3260
|
-
this.hint = hint;
|
|
3261
|
-
this.sourceFile = sourceFile;
|
|
3262
|
-
}
|
|
3263
|
-
};
|
|
3264
|
-
const LUAU_RUNTIME_HINT = "install lute (e.g. `mise install` with `github:luau-lang/lute`) or set BEDROCK_LUTE_PATH to the binary.";
|
|
3265
|
-
function isEnoentError(error) {
|
|
3266
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
3267
|
-
}
|
|
3268
|
-
const LUTE_BOOTSTRAP_TIMEOUT_MS = 1e4;
|
|
3269
|
-
async function runLuteBootstrap(runOptions) {
|
|
3270
|
-
const { bin, bootstrapPath, userBasename } = runOptions;
|
|
3271
|
-
return new Promise((resolve, reject) => {
|
|
3272
|
-
execFile(bin, [
|
|
3273
|
-
"run",
|
|
3274
|
-
bootstrapPath,
|
|
3275
|
-
userBasename
|
|
3276
|
-
], {
|
|
3277
|
-
encoding: "utf8",
|
|
3278
|
-
timeout: LUTE_BOOTSTRAP_TIMEOUT_MS
|
|
3279
|
-
}, (error, stdout) => {
|
|
3280
|
-
if (error instanceof Error) {
|
|
3281
|
-
reject(error);
|
|
3282
|
-
return;
|
|
3283
|
-
}
|
|
3284
|
-
resolve(stdout);
|
|
3285
|
-
});
|
|
3286
|
-
});
|
|
3287
|
-
}
|
|
3288
|
-
function parseBootstrapOutput(stdout) {
|
|
3289
|
-
if (stdout.startsWith(ERR_PREFIX)) throw new Error(stdout.slice(ERR_PREFIX.length));
|
|
3290
|
-
const parsed = JSON.parse(stdout.slice(OK_PREFIX.length));
|
|
3291
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("Luau config must return a table at the root");
|
|
3292
|
-
return parsed;
|
|
3293
|
-
}
|
|
3294
|
-
async function evaluateLuauConfig(absPath) {
|
|
3295
|
-
const overridePath = process.env["BEDROCK_LUTE_PATH"];
|
|
3296
|
-
const lute = overridePath !== void 0 && overridePath.length > 0 ? overridePath : "lute";
|
|
3297
|
-
const cwd = dirname(absPath);
|
|
3298
|
-
const base = basename(absPath);
|
|
3299
|
-
const bootstrapDirectory = mkdtempSync(join(tmpdir(), bootstrapDirectoryPrefix(process.pid)));
|
|
3300
|
-
try {
|
|
3301
|
-
const bootstrapPath = join(bootstrapDirectory, "bootstrap.luau");
|
|
3302
|
-
writeFileSync(bootstrapPath, LUTE_BOOTSTRAP_LUAU);
|
|
3303
|
-
writeFileSync(join(bootstrapDirectory, ".luaurc"), JSON.stringify({ aliases: { user: cwd } }));
|
|
3304
|
-
return parseBootstrapOutput(await runLuteBootstrap({
|
|
3305
|
-
bin: lute,
|
|
3306
|
-
bootstrapPath,
|
|
3307
|
-
userBasename: base
|
|
3308
|
-
}).catch((err) => {
|
|
3309
|
-
if (isEnoentError(err)) throw new LuauRuntimeMissingError(absPath, LUAU_RUNTIME_HINT);
|
|
3310
|
-
throw err;
|
|
3311
|
-
}));
|
|
3312
|
-
} finally {
|
|
3313
|
-
rmSync(bootstrapDirectory, { recursive: true });
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
3338
|
const CONFIG_FILE_IN_FRAME = /[^\s():"']*bedrock\.config\.(?:ts|js|mjs|cjs|yaml|yml|json)/;
|
|
3317
3339
|
function extractConfigFileFromStack(err) {
|
|
3318
3340
|
if (!(err instanceof Error) || err.stack === void 0) return;
|
|
@@ -3334,11 +3356,7 @@ function discoverConfigFile(cwd) {
|
|
|
3334
3356
|
return match === void 0 ? void 0 : join(cwd, match);
|
|
3335
3357
|
}
|
|
3336
3358
|
function attributeLoadError(err, cwd) {
|
|
3337
|
-
if (err instanceof
|
|
3338
|
-
hint: err.hint,
|
|
3339
|
-
kind: "luauRuntimeMissing",
|
|
3340
|
-
sourceFile: err.sourceFile
|
|
3341
|
-
};
|
|
3359
|
+
if (err instanceof EvaluatorThrow) return err.configError;
|
|
3342
3360
|
const message = err instanceof Error ? err.message : String(err);
|
|
3343
3361
|
const frameFile = extractConfigFileFromStack(err);
|
|
3344
3362
|
if (frameFile !== void 0) return {
|
|
@@ -3357,7 +3375,7 @@ function attributeLoadError(err, cwd) {
|
|
|
3357
3375
|
/**
|
|
3358
3376
|
* Run a full reconcile end-to-end. Default-constructs missing deps from
|
|
3359
3377
|
* the project config and the environment variables `GITHUB_TOKEN` and
|
|
3360
|
-
* `
|
|
3378
|
+
* `BEDROCK_API_KEY`; never reads `process.env` when `statePort`,
|
|
3361
3379
|
* `registry`, and `config` are all supplied explicitly.
|
|
3362
3380
|
*
|
|
3363
3381
|
* @param options - Target environment plus optional overrides.
|
|
@@ -3591,14 +3609,9 @@ async function runReconcile(environment, deps) {
|
|
|
3591
3609
|
* the shell from the icon file's bytes) and fall back to the
|
|
3592
3610
|
* Mantle-recorded hashes when the map omits the key. Product resources
|
|
3593
3611
|
* without an icon partner omit `icon` and `iconFileHashes` entirely. The
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
*
|
|
3597
|
-
* be read (the shell emits an `ambiguous` warning), surface a universe
|
|
3598
|
-
* resource without those fields. The `outputs` field carries the
|
|
3599
|
-
* Mantle-recorded identifiers (universe `rootPlaceId` and optional
|
|
3600
|
-
* `iconAssetIds`, place `versionNumber`, pass `assetId` and
|
|
3601
|
-
* `iconAssetIds`, product `productId` and optional `iconImageAssetId`).
|
|
3612
|
+
* `outputs` field carries the Mantle-recorded identifiers (universe
|
|
3613
|
+
* `rootPlaceId`, place `versionNumber`, pass `assetId` and `iconAssetIds`,
|
|
3614
|
+
* product `productId` and optional `iconImageAssetId`).
|
|
3602
3615
|
*
|
|
3603
3616
|
* @param inputs - Folded data plus recomputed hashes for this environment.
|
|
3604
3617
|
* @returns A `BedrockState` populated with one resource per folded kind.
|
|
@@ -3611,8 +3624,8 @@ function buildState(inputs) {
|
|
|
3611
3624
|
};
|
|
3612
3625
|
}
|
|
3613
3626
|
function universeResource(inputs) {
|
|
3614
|
-
const { entry,
|
|
3615
|
-
|
|
3627
|
+
const { entry, outputs } = inputs;
|
|
3628
|
+
return {
|
|
3616
3629
|
key: UNIVERSE_SINGLETON_KEY,
|
|
3617
3630
|
consoleEnabled: entry.consoleEnabled,
|
|
3618
3631
|
desktopEnabled: entry.desktopEnabled,
|
|
@@ -3625,12 +3638,6 @@ function universeResource(inputs) {
|
|
|
3625
3638
|
voiceChatEnabled: entry.voiceChatEnabled,
|
|
3626
3639
|
vrEnabled: entry.vrEnabled
|
|
3627
3640
|
};
|
|
3628
|
-
if (entry.icon === void 0 || iconHashes === void 0) return base;
|
|
3629
|
-
return {
|
|
3630
|
-
...base,
|
|
3631
|
-
icon: entry.icon,
|
|
3632
|
-
iconFileHashes: iconHashes
|
|
3633
|
-
};
|
|
3634
3641
|
}
|
|
3635
3642
|
function placeResource(key, fold) {
|
|
3636
3643
|
return {
|
|
@@ -3676,10 +3683,9 @@ function productResource(fold, productIconHashesByKey) {
|
|
|
3676
3683
|
};
|
|
3677
3684
|
}
|
|
3678
3685
|
function composeResources(inputs) {
|
|
3679
|
-
const { folded, passIconHashesByKey, productIconHashesByKey
|
|
3686
|
+
const { folded, passIconHashesByKey, productIconHashesByKey } = inputs;
|
|
3680
3687
|
const universeResources = folded.universe === void 0 ? [] : [universeResource({
|
|
3681
3688
|
entry: folded.universe.entry,
|
|
3682
|
-
iconHashes: universeIconHashes,
|
|
3683
3689
|
outputs: folded.universe.outputs
|
|
3684
3690
|
})];
|
|
3685
3691
|
const placeResources = [...folded.places.entries()].map(([key, entry]) => placeResource(key, entry));
|
|
@@ -4304,14 +4310,14 @@ function readPassInputs(raw) {
|
|
|
4304
4310
|
price: readPrice$1(raw)
|
|
4305
4311
|
};
|
|
4306
4312
|
}
|
|
4307
|
-
function coerceRobloxId$
|
|
4313
|
+
function coerceRobloxId$3(value) {
|
|
4308
4314
|
if (typeof value === "string") return value;
|
|
4309
4315
|
if (Number.isInteger(value)) return String(value);
|
|
4310
4316
|
}
|
|
4311
4317
|
function readPassOutputs(raw) {
|
|
4312
4318
|
if (!isObjectPayload$2(raw)) return;
|
|
4313
|
-
const assetId = coerceRobloxId$
|
|
4314
|
-
const iconAssetId = coerceRobloxId$
|
|
4319
|
+
const assetId = coerceRobloxId$3(raw["assetId"]);
|
|
4320
|
+
const iconAssetId = coerceRobloxId$3(raw["iconAssetId"]);
|
|
4315
4321
|
if (assetId === void 0 || iconAssetId === void 0) return;
|
|
4316
4322
|
return {
|
|
4317
4323
|
assetId,
|
|
@@ -4358,7 +4364,7 @@ function isObjectPayload$1(value) {
|
|
|
4358
4364
|
* @param value - Raw value pulled from a Mantle resource's outputs.
|
|
4359
4365
|
* @returns The stringified ID, or `undefined` when the value is not a valid wire shape.
|
|
4360
4366
|
*/
|
|
4361
|
-
function coerceRobloxId$
|
|
4367
|
+
function coerceRobloxId$2(value) {
|
|
4362
4368
|
if (typeof value === "string") return value;
|
|
4363
4369
|
if (Number.isInteger(value)) return String(value);
|
|
4364
4370
|
}
|
|
@@ -4632,14 +4638,14 @@ function isStartPlace$1(resource) {
|
|
|
4632
4638
|
if (!isObjectPayload$1(resource.inputs)) return false;
|
|
4633
4639
|
return resource.inputs["isStart"] === true;
|
|
4634
4640
|
}
|
|
4635
|
-
function coerceRobloxId$
|
|
4641
|
+
function coerceRobloxId$1(value) {
|
|
4636
4642
|
if (typeof value === "string") return value;
|
|
4637
4643
|
if (Number.isInteger(value)) return String(value);
|
|
4638
4644
|
}
|
|
4639
4645
|
function readPlaceOutputs(resource) {
|
|
4640
4646
|
const { outputs } = resource;
|
|
4641
4647
|
if (!isObjectPayload$1(outputs)) return;
|
|
4642
|
-
const assetId = coerceRobloxId$
|
|
4648
|
+
const assetId = coerceRobloxId$1(outputs["assetId"]);
|
|
4643
4649
|
if (assetId === void 0) return;
|
|
4644
4650
|
return { assetId };
|
|
4645
4651
|
}
|
|
@@ -4762,7 +4768,7 @@ function readProductInputs(raw) {
|
|
|
4762
4768
|
}
|
|
4763
4769
|
function readProductOutputs(raw) {
|
|
4764
4770
|
if (!isObjectPayload$1(raw)) return;
|
|
4765
|
-
const productId = coerceRobloxId$
|
|
4771
|
+
const productId = coerceRobloxId$2(raw["productId"]);
|
|
4766
4772
|
if (productId === void 0) return;
|
|
4767
4773
|
return { productId };
|
|
4768
4774
|
}
|
|
@@ -4778,7 +4784,7 @@ function readProductIconInputs(raw) {
|
|
|
4778
4784
|
}
|
|
4779
4785
|
function readProductIconOutputs(raw) {
|
|
4780
4786
|
if (!isObjectPayload$1(raw)) return;
|
|
4781
|
-
const assetId = coerceRobloxId$
|
|
4787
|
+
const assetId = coerceRobloxId$2(raw["assetId"]);
|
|
4782
4788
|
if (assetId === void 0) return;
|
|
4783
4789
|
return { assetId };
|
|
4784
4790
|
}
|
|
@@ -4960,51 +4966,36 @@ function foldDisplayName(resources) {
|
|
|
4960
4966
|
//#endregion
|
|
4961
4967
|
//#region src/core/migrate/fold-experience-icon.ts
|
|
4962
4968
|
const EXPERIENCE_ICON_KIND = "experienceIcon";
|
|
4969
|
+
const BLOCKED_REASON = "Open Cloud has no route to set a universe's source-language game icon; configure it via the Roblox creator portal.";
|
|
4963
4970
|
/**
|
|
4964
|
-
*
|
|
4965
|
-
*
|
|
4966
|
-
*
|
|
4967
|
-
*
|
|
4968
|
-
*
|
|
4971
|
+
* Surface every Mantle `experienceIcon_<key>` resource as a `blocked`
|
|
4972
|
+
* migration warning. Bedrock has no `UniverseEntry.icon` field today
|
|
4973
|
+
* because no Open Cloud endpoint accepts a source-language game icon, so
|
|
4974
|
+
* the migrator emits one warning per legacy resource (rather than the
|
|
4975
|
+
* first matching entry only) so the operator can audit each affected
|
|
4976
|
+
* environment before reconfiguring the icon by hand.
|
|
4969
4977
|
*
|
|
4970
4978
|
* Resources whose payload is malformed (non-object inputs/outputs,
|
|
4971
|
-
* non-string `filePath
|
|
4972
|
-
*
|
|
4973
|
-
* multiple `experienceIcon` resources lands in a follow-up slice.
|
|
4979
|
+
* non-string `filePath`) are skipped silently, matching the
|
|
4980
|
+
* malformed-payload behaviour of the other fold rules.
|
|
4974
4981
|
*
|
|
4975
4982
|
* @param resources - Mantle resource list for one environment.
|
|
4976
|
-
* @returns
|
|
4983
|
+
* @returns A fragment whose `warnings` carries one `blocked` entry per
|
|
4984
|
+
* legacy experience-icon resource, or {@link EMPTY_FRAGMENT} when none
|
|
4985
|
+
* are present.
|
|
4977
4986
|
*/
|
|
4978
4987
|
function foldExperienceIcon(resources) {
|
|
4979
|
-
const
|
|
4980
|
-
if (
|
|
4981
|
-
const parts = readParts(first);
|
|
4982
|
-
if (parts === void 0) return EMPTY_FRAGMENT;
|
|
4988
|
+
const warnings = resources.filter((resource) => resource.kind === EXPERIENCE_ICON_KIND && hasReadablePayload(resource)).map((resource) => blockedWarning(`${EXPERIENCE_ICON_KIND}_${resource.key}`, BLOCKED_REASON));
|
|
4989
|
+
if (warnings.length === 0) return EMPTY_FRAGMENT;
|
|
4983
4990
|
return {
|
|
4984
|
-
entryFragment: {
|
|
4985
|
-
|
|
4986
|
-
warnings: [interpretiveWarning({
|
|
4987
|
-
bedrockPath: "universe.icon",
|
|
4988
|
-
mantlePath: `${EXPERIENCE_ICON_KIND}_${first.key}`,
|
|
4989
|
-
rule: "experience-icon-to-en-us-locale"
|
|
4990
|
-
})]
|
|
4991
|
+
entryFragment: {},
|
|
4992
|
+
warnings
|
|
4991
4993
|
};
|
|
4992
4994
|
}
|
|
4993
|
-
function
|
|
4994
|
-
|
|
4995
|
-
return isRobloxAssetId(candidate) ? candidate : void 0;
|
|
4996
|
-
}
|
|
4997
|
-
function readParts(resource) {
|
|
4998
|
-
if (!isObjectPayload$1(resource.inputs)) return;
|
|
4995
|
+
function hasReadablePayload(resource) {
|
|
4996
|
+
if (!isObjectPayload$1(resource.inputs)) return false;
|
|
4999
4997
|
const { filePath } = resource.inputs;
|
|
5000
|
-
|
|
5001
|
-
if (!isObjectPayload$1(resource.outputs)) return;
|
|
5002
|
-
const assetId = coerceRobloxId$1(resource.outputs["assetId"]);
|
|
5003
|
-
if (assetId === void 0) return;
|
|
5004
|
-
return {
|
|
5005
|
-
assetId,
|
|
5006
|
-
filePath
|
|
5007
|
-
};
|
|
4998
|
+
return typeof filePath === "string";
|
|
5008
4999
|
}
|
|
5009
5000
|
//#endregion
|
|
5010
5001
|
//#region src/core/migrate/fold-social-links.ts
|
|
@@ -5480,20 +5471,17 @@ function summarizeWarnings(warnings) {
|
|
|
5480
5471
|
//#endregion
|
|
5481
5472
|
//#region src/shell/recompute-icon-hashes.ts
|
|
5482
5473
|
/**
|
|
5483
|
-
* Walk each environment's folded pass
|
|
5484
|
-
*
|
|
5485
|
-
* the
|
|
5486
|
-
*
|
|
5487
|
-
*
|
|
5488
|
-
*
|
|
5489
|
-
*
|
|
5490
|
-
* and omits the icon entirely on the universe resource. Products without
|
|
5491
|
-
* an icon partner and environments without an experience icon are
|
|
5492
|
-
* silently skipped.
|
|
5474
|
+
* Walk each environment's folded pass and product entries, resolve the
|
|
5475
|
+
* locale-keyed icon paths against `stateFileDirectory`, read the bytes via
|
|
5476
|
+
* the injected `readFile`, and compute the SHA-256 hex digest. Files that
|
|
5477
|
+
* cannot be read surface as `ambiguous` `MigrationWarning`s with the
|
|
5478
|
+
* environment-prefixed `mantlePath` and a hint pointing at the resolved
|
|
5479
|
+
* path; the caller carries the Mantle-recorded hashes forward as a
|
|
5480
|
+
* fallback. Products without an icon partner are silently skipped.
|
|
5493
5481
|
*
|
|
5494
5482
|
* @param inputs - Per-environment fold results plus I/O dependencies.
|
|
5495
|
-
* @returns Per-environment recomputed pass
|
|
5496
|
-
*
|
|
5483
|
+
* @returns Per-environment recomputed pass and product hashes plus
|
|
5484
|
+
* accumulated ambiguous warnings.
|
|
5497
5485
|
*/
|
|
5498
5486
|
async function recomputeIconHashes(inputs) {
|
|
5499
5487
|
return collectRecomputation(await Promise.all([...inputs.folds.entries()].map(async ([environment, folded]) => {
|
|
@@ -5509,9 +5497,6 @@ function collectRecomputation(walked) {
|
|
|
5509
5497
|
return {
|
|
5510
5498
|
passHashesByEnvironment: new Map(walked.map(([environment, walk]) => [environment, walk.passHashes])),
|
|
5511
5499
|
productHashesByEnvironment: new Map(walked.map(([environment, walk]) => [environment, walk.productHashes])),
|
|
5512
|
-
universeHashByEnvironment: new Map(walked.flatMap(([environment, walk]) => {
|
|
5513
|
-
return walk.universeHash === void 0 ? [] : [[environment, walk.universeHash]];
|
|
5514
|
-
})),
|
|
5515
5500
|
warnings: walked.flatMap(([, walk]) => walk.warnings)
|
|
5516
5501
|
};
|
|
5517
5502
|
}
|
|
@@ -5562,27 +5547,6 @@ async function walkIconEntries(inputs) {
|
|
|
5562
5547
|
warnings
|
|
5563
5548
|
};
|
|
5564
5549
|
}
|
|
5565
|
-
async function walkUniverseIcon(inputs) {
|
|
5566
|
-
const iconPath = inputs.folded.universe?.entry.icon?.["en-us"];
|
|
5567
|
-
if (iconPath === void 0) return {
|
|
5568
|
-
hash: void 0,
|
|
5569
|
-
warnings: []
|
|
5570
|
-
};
|
|
5571
|
-
const resolved = join(inputs.stateFileDirectory, iconPath);
|
|
5572
|
-
const recomputed = await tryRecomputeHash(inputs.readFile, resolved);
|
|
5573
|
-
if (recomputed === void 0) return {
|
|
5574
|
-
hash: void 0,
|
|
5575
|
-
warnings: [buildAmbiguousIconWarning({
|
|
5576
|
-
environmentName: inputs.environmentName,
|
|
5577
|
-
mantlePath: "experienceIcon_singleton",
|
|
5578
|
-
resolvedPath: resolved
|
|
5579
|
-
})]
|
|
5580
|
-
};
|
|
5581
|
-
return {
|
|
5582
|
-
hash: { "en-us": recomputed },
|
|
5583
|
-
warnings: []
|
|
5584
|
-
};
|
|
5585
|
-
}
|
|
5586
5550
|
async function walkEnvironment(inputs) {
|
|
5587
5551
|
const passWalk = await walkIconEntries({
|
|
5588
5552
|
entries: inputs.folded.passes.map(passWalkEntry),
|
|
@@ -5596,21 +5560,10 @@ async function walkEnvironment(inputs) {
|
|
|
5596
5560
|
readFile: inputs.readFile,
|
|
5597
5561
|
stateFileDirectory: inputs.stateFileDirectory
|
|
5598
5562
|
});
|
|
5599
|
-
const universeWalk = await walkUniverseIcon({
|
|
5600
|
-
environmentName: inputs.environmentName,
|
|
5601
|
-
folded: inputs.folded,
|
|
5602
|
-
readFile: inputs.readFile,
|
|
5603
|
-
stateFileDirectory: inputs.stateFileDirectory
|
|
5604
|
-
});
|
|
5605
5563
|
return {
|
|
5606
5564
|
passHashes: passWalk.perKey,
|
|
5607
5565
|
productHashes: productWalk.perKey,
|
|
5608
|
-
|
|
5609
|
-
warnings: [
|
|
5610
|
-
...passWalk.warnings,
|
|
5611
|
-
...productWalk.warnings,
|
|
5612
|
-
...universeWalk.warnings
|
|
5613
|
-
]
|
|
5566
|
+
warnings: [...passWalk.warnings, ...productWalk.warnings]
|
|
5614
5567
|
};
|
|
5615
5568
|
}
|
|
5616
5569
|
//#endregion
|
|
@@ -5705,8 +5658,7 @@ function buildStatesByEnvironment(inputs) {
|
|
|
5705
5658
|
environment: name,
|
|
5706
5659
|
folded,
|
|
5707
5660
|
passIconHashesByKey: inputs.passHashesByEnvironment.get(name) ?? EMPTY_HASHES,
|
|
5708
|
-
productIconHashesByKey: inputs.productHashesByEnvironment.get(name) ?? EMPTY_HASHES
|
|
5709
|
-
universeIconHashes: inputs.universeHashByEnvironment.get(name)
|
|
5661
|
+
productIconHashesByKey: inputs.productHashesByEnvironment.get(name) ?? EMPTY_HASHES
|
|
5710
5662
|
})];
|
|
5711
5663
|
}));
|
|
5712
5664
|
}
|
|
@@ -5722,7 +5674,7 @@ function collectFoldWarnings(folds) {
|
|
|
5722
5674
|
});
|
|
5723
5675
|
}
|
|
5724
5676
|
function buildReport(inputs, validated) {
|
|
5725
|
-
const { passHashesByEnvironment, productHashesByEnvironment,
|
|
5677
|
+
const { passHashesByEnvironment, productHashesByEnvironment, warnings: iconWarnings } = inputs.iconRecomputation;
|
|
5726
5678
|
const warnings = [
|
|
5727
5679
|
...collectFoldWarnings(inputs.folds),
|
|
5728
5680
|
...inputs.factorizeWarnings,
|
|
@@ -5737,8 +5689,7 @@ function buildReport(inputs, validated) {
|
|
|
5737
5689
|
statesByEnvironment: buildStatesByEnvironment({
|
|
5738
5690
|
folds: inputs.folds,
|
|
5739
5691
|
passHashesByEnvironment,
|
|
5740
|
-
productHashesByEnvironment
|
|
5741
|
-
universeHashByEnvironment
|
|
5692
|
+
productHashesByEnvironment
|
|
5742
5693
|
}),
|
|
5743
5694
|
summary: summarizeWarnings(warnings),
|
|
5744
5695
|
warnings
|
|
@@ -5786,4 +5737,4 @@ function isFileMissing(err) {
|
|
|
5786
5737
|
//#endregion
|
|
5787
5738
|
export { asResourceKey as A, createGistStateAdapter as C, createGamePassDriver as D, validateEnvironmentName as E, isSha256Hex as F, derivePriceFields as I, asSha256Hex as M, isResourceKey as N, createDeveloperProductDriver as O, isRobloxAssetId as P, UNIVERSE_SINGLETON_KEY as S, serializeStateFile as T, isGistStateConfig as _, buildStatePort as a, createPlaceDriver as b, applyOps as c, resolveStateConfig as d, flattenConfig as f, defaultKindRegistry as g, diff as h, loadConfig$1 as i, asRobloxAssetId as j, shouldReuploadIcon as k, validatePlan as l, renderDisplayNamePrefix as m, serializeConfig as n, buildDesired as o, DEFAULT_PREFIX_FORMAT as p, deploy as r, buildDefaultRegistry as s, migrateMantleState as t, selectEnvironment as u, validateConfig as v, parseStateFile as w, SOCIAL_LINK_FIELDS as x, createUniverseDriver as y };
|
|
5788
5739
|
|
|
5789
|
-
//# sourceMappingURL=migrate-mantle-state-
|
|
5740
|
+
//# sourceMappingURL=migrate-mantle-state-DjAU-I39.mjs.map
|