@bedrock-rbx/core 0.1.0-beta.1 → 0.1.0-beta.2

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.
@@ -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
  /**
@@ -1357,7 +1357,6 @@ async function publishPlace(deps, desired) {
1357
1357
  * httpClient: universeBodyHttpClient,
1358
1358
  * sleep: async () => {},
1359
1359
  * }),
1360
- * readFile: async () => new Uint8Array(),
1361
1360
  * universes: new UniversesClient({
1362
1361
  * apiKey: "rbx-your-key",
1363
1362
  * httpClient: universeBodyHttpClient,
@@ -1391,29 +1390,22 @@ function createUniverseDriver(deps) {
1391
1390
  return {
1392
1391
  async create(desired) {
1393
1392
  return reconcileUniverse({
1394
- current: void 0,
1395
1393
  deps,
1396
1394
  desired
1397
1395
  });
1398
1396
  },
1399
- async update(current, desired) {
1397
+ async update(_current, desired) {
1400
1398
  return reconcileUniverse({
1401
- current,
1402
1399
  deps,
1403
1400
  desired
1404
1401
  });
1405
1402
  }
1406
1403
  };
1407
1404
  }
1408
- function toCurrentState(inputs) {
1409
- const { desired, iconAssetIds, rootPlaceId } = inputs;
1410
- const baseOutputs = { rootPlaceId: asRobloxAssetId(rootPlaceId) };
1405
+ function toCurrentState(desired, rootPlaceId) {
1411
1406
  return {
1412
1407
  ...desired,
1413
- outputs: iconAssetIds === void 0 ? baseOutputs : {
1414
- ...baseOutputs,
1415
- iconAssetIds
1416
- }
1408
+ outputs: { rootPlaceId: asRobloxAssetId(rootPlaceId) }
1417
1409
  };
1418
1410
  }
1419
1411
  function buildParameters(desired) {
@@ -1457,46 +1449,8 @@ async function resolveUniverse(deps, desired) {
1457
1449
  success: true
1458
1450
  };
1459
1451
  }
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
1452
  async function reconcileUniverse(inputs) {
1499
- const { current, deps, desired } = inputs;
1453
+ const { deps, desired } = inputs;
1500
1454
  const universeResult = await resolveUniverse(deps, desired);
1501
1455
  if (!universeResult.success) return universeResult;
1502
1456
  const { rootPlaceId } = universeResult.data;
@@ -1511,18 +1465,8 @@ async function reconcileUniverse(inputs) {
1511
1465
  success: false
1512
1466
  };
1513
1467
  }
1514
- const iconResult = await reconcileIcon({
1515
- current,
1516
- deps,
1517
- desired
1518
- });
1519
- if (!iconResult.success) return iconResult;
1520
1468
  return {
1521
- data: toCurrentState({
1522
- desired,
1523
- iconAssetIds: iconResult.data,
1524
- rootPlaceId
1525
- }),
1469
+ data: toCurrentState(desired, rootPlaceId),
1526
1470
  success: true
1527
1471
  };
1528
1472
  }
@@ -1656,7 +1600,6 @@ const universeEntry = type({
1656
1600
  "displayName?": OPTIONAL_STRING,
1657
1601
  "facebookSocialLink?": socialLinkOrUndefined$1,
1658
1602
  "guildedSocialLink?": socialLinkOrUndefined$1,
1659
- "icon?": iconMap,
1660
1603
  "mobileEnabled?": OPTIONAL_BOOLEAN$2,
1661
1604
  "privateServerPriceRobux?": OPTIONAL_ROBUX_PRICE,
1662
1605
  "robloxGroupSocialLink?": socialLinkOrUndefined$1,
@@ -2004,7 +1947,6 @@ const entrySchema = type({
2004
1947
  "displayName?": "string | undefined",
2005
1948
  "facebookSocialLink?": socialLinkOrUndefined,
2006
1949
  "guildedSocialLink?": socialLinkOrUndefined,
2007
- "icon?": iconMap,
2008
1950
  "mobileEnabled?": OPTIONAL_BOOLEAN,
2009
1951
  "privateServerPriceRobux?": "number.integer >= 0 | undefined",
2010
1952
  "robloxGroupSocialLink?": socialLinkOrUndefined,
@@ -2032,14 +1974,10 @@ function flatten(config) {
2032
1974
  vrEnabled: entry.vrEnabled,
2033
1975
  ...copyDeclaredSocialLinks(entry)
2034
1976
  };
2035
- const withPrice = "privateServerPriceRobux" in entry ? {
1977
+ return ["privateServerPriceRobux" in entry ? {
2036
1978
  ...base,
2037
1979
  privateServerPriceRobux: entry.privateServerPriceRobux
2038
- } : base;
2039
- return [entry.icon === void 0 ? withPrice : {
2040
- ...withPrice,
2041
- icon: entry.icon
2042
- }];
1980
+ } : base];
2043
1981
  }
2044
1982
  function buildBaseDesired(input) {
2045
1983
  const base = {
@@ -2060,23 +1998,9 @@ function buildBaseDesired(input) {
2060
1998
  privateServerPriceRobux: input.privateServerPriceRobux
2061
1999
  } : base;
2062
2000
  }
2063
- async function normalize(input, io) {
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;
2001
+ async function normalize(input, _io) {
2074
2002
  return {
2075
- data: {
2076
- ...withPrice,
2077
- icon: input.icon,
2078
- iconFileHashes: hashes.data
2079
- },
2003
+ data: buildBaseDesired(input),
2080
2004
  success: true
2081
2005
  };
2082
2006
  }
@@ -2100,7 +2024,6 @@ function fieldsEqual(desired, current) {
2100
2024
  }
2101
2025
  if (desired.displayName !== void 0 && desired.displayName !== current.displayName) return false;
2102
2026
  if ("privateServerPriceRobux" in desired && desired.privateServerPriceRobux !== current.privateServerPriceRobux) return false;
2103
- if (!iconHashesEqual(current.iconFileHashes, desired.iconFileHashes)) return false;
2104
2027
  return declaredSocialLinksEqual(desired, current);
2105
2028
  }
2106
2029
  //#endregion
@@ -2832,7 +2755,7 @@ async function dispatchOp(op, registry) {
2832
2755
  //#region src/shell/build-default-registry.ts
2833
2756
  /**
2834
2757
  * Construct the default `DriverRegistry` from `config.universe.universeId`
2835
- * and `ROBLOX_API_KEY`. Reads the API key via the injected `getEnv` seam
2758
+ * and `BEDROCK_API_KEY`. Reads the API key via the injected `getEnv` seam
2836
2759
  * and surfaces `missingCredential` or `registryConfigMissing` as typed
2837
2760
  * Results instead of throwing.
2838
2761
  *
@@ -2859,7 +2782,7 @@ async function dispatchOp(op, registry) {
2859
2782
  * missing API key or the missing universe declaration.
2860
2783
  */
2861
2784
  function buildDefaultRegistry(deps) {
2862
- const apiKey = deps.getEnv("ROBLOX_API_KEY");
2785
+ const apiKey = deps.getEnv("BEDROCK_API_KEY");
2863
2786
  if (apiKey === void 0) return missingApiKey();
2864
2787
  const rawUniverseId = deps.config.universe?.universeId;
2865
2788
  if (rawUniverseId === void 0) return missingUniverseId();
@@ -2877,7 +2800,7 @@ function missingApiKey() {
2877
2800
  err: {
2878
2801
  kind: "missingCredential",
2879
2802
  purpose: "registry",
2880
- variable: "ROBLOX_API_KEY"
2803
+ variable: "BEDROCK_API_KEY"
2881
2804
  },
2882
2805
  success: false
2883
2806
  };
@@ -2916,7 +2839,6 @@ function assembleRegistry(inputs) {
2916
2839
  }),
2917
2840
  universe: createUniverseDriver({
2918
2841
  places,
2919
- readFile,
2920
2842
  universes
2921
2843
  })
2922
2844
  };
@@ -3057,8 +2979,169 @@ function bootstrapDirectoryPrefix(pid) {
3057
2979
  return `${LUAU_BOOTSTRAP_TEMP_PREFIX}${pid}-`;
3058
2980
  }
3059
2981
  //#endregion
2982
+ //#region src/adapters/lute-luau-evaluator.ts
2983
+ const SENTINEL_BASE = "__BEDROCK_LUAU_";
2984
+ const OK_PREFIX = `${SENTINEL_BASE}OK__`;
2985
+ const ERR_PREFIX = `${SENTINEL_BASE}ERR__`;
2986
+ const LUTE_BOOTSTRAP_LUAU = `--!strict
2987
+ local json = require("@std/json")
2988
+ local process = require("@std/process")
2989
+ local io = require("@std/io")
2990
+
2991
+ local function emit(kind, payload)
2992
+ io.write("${SENTINEL_BASE}" .. kind .. "__")
2993
+ io.write(json.serialize(payload))
2994
+ end
2995
+
2996
+ local userBasename = process.args[2]
2997
+ -- The user file lives in a different directory from this bootstrap, so we
2998
+ -- require it via the @user alias defined in the .luaurc written alongside.
2999
+ local req = "@user/" .. string.gsub(userBasename, "%.luau$", "")
3000
+
3001
+ local loadOk, modOrErr = pcall(require, req)
3002
+ if not loadOk then
3003
+ emit("ERR", { kind = "loadFailed", message = tostring(modOrErr) })
3004
+ return
3005
+ end
3006
+
3007
+ local value = if type(modOrErr) == "function" then modOrErr() else modOrErr
3008
+
3009
+ local encOk, encoded = pcall(json.serialize, value)
3010
+ if not encOk then
3011
+ emit("ERR", { kind = "serializeFailed", message = tostring(encoded) })
3012
+ return
3013
+ end
3014
+
3015
+ io.write("${OK_PREFIX}")
3016
+ io.write(encoded)
3017
+ `;
3018
+ const LUAU_RUNTIME_HINT = "install lute (e.g. `mise install` with `github:luau-lang/lute`) or set BEDROCK_LUTE_PATH to the binary.";
3019
+ function isEnoentError(error) {
3020
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3021
+ }
3022
+ const LUTE_BOOTSTRAP_TIMEOUT_MS = 5e3;
3023
+ /**
3024
+ * Build the default `LuauEvaluator` adapter that shells out to the `lute`
3025
+ * runtime. Reads `BEDROCK_LUTE_PATH` from `process.env` once per call to pick
3026
+ * the binary, so tests can override it via env var without rebuilding the
3027
+ * adapter.
3028
+ * @returns A `LuauEvaluator` that spawns `lute run` per call.
3029
+ */
3030
+ function createLuteLuauEvaluator() {
3031
+ return evaluateLuauWithLute;
3032
+ }
3033
+ function setupBootstrapDirectory(userCwd) {
3034
+ const bootstrapDirectory = mkdtempSync(join(tmpdir(), bootstrapDirectoryPrefix(process.pid)));
3035
+ writeFileSync(join(bootstrapDirectory, "bootstrap.luau"), LUTE_BOOTSTRAP_LUAU);
3036
+ writeFileSync(join(bootstrapDirectory, ".luaurc"), JSON.stringify({ aliases: { user: userCwd } }));
3037
+ return bootstrapDirectory;
3038
+ }
3039
+ async function runLuteBootstrap(runOptions) {
3040
+ const { bin, bootstrapPath, userBasename } = runOptions;
3041
+ return new Promise((resolve, reject) => {
3042
+ execFile(bin, [
3043
+ "run",
3044
+ bootstrapPath,
3045
+ userBasename
3046
+ ], {
3047
+ encoding: "utf8",
3048
+ timeout: LUTE_BOOTSTRAP_TIMEOUT_MS
3049
+ }, (error, stdout) => {
3050
+ if (error instanceof Error) {
3051
+ reject(error);
3052
+ return;
3053
+ }
3054
+ resolve(stdout);
3055
+ });
3056
+ });
3057
+ }
3058
+ function parseBootstrapOutput(stdout) {
3059
+ if (stdout.startsWith(ERR_PREFIX)) throw new Error(stdout.slice(ERR_PREFIX.length));
3060
+ const parsed = JSON.parse(stdout.slice(OK_PREFIX.length));
3061
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("Luau config must return a table at the root");
3062
+ return parsed;
3063
+ }
3064
+ async function evaluateLuauWithLute(absPath) {
3065
+ const overridePath = process.env["BEDROCK_LUTE_PATH"];
3066
+ const lute = overridePath !== void 0 && overridePath.length > 0 ? overridePath : "lute";
3067
+ const bootstrapDirectory = setupBootstrapDirectory(dirname(absPath));
3068
+ try {
3069
+ return {
3070
+ data: parseBootstrapOutput(await runLuteBootstrap({
3071
+ bin: lute,
3072
+ bootstrapPath: join(bootstrapDirectory, "bootstrap.luau"),
3073
+ userBasename: basename(absPath)
3074
+ })),
3075
+ success: true
3076
+ };
3077
+ } catch (err) {
3078
+ if (isEnoentError(err)) return {
3079
+ err: {
3080
+ hint: LUAU_RUNTIME_HINT,
3081
+ kind: "missingRuntime"
3082
+ },
3083
+ success: false
3084
+ };
3085
+ return {
3086
+ err: {
3087
+ kind: "evaluationFailed",
3088
+ message: err instanceof Error ? err.message : String(err)
3089
+ },
3090
+ success: false
3091
+ };
3092
+ } finally {
3093
+ rmSync(bootstrapDirectory, { recursive: true });
3094
+ }
3095
+ }
3096
+ //#endregion
3060
3097
  //#region src/shell/load-config.ts
3061
3098
  /**
3099
+ * Internal entrypoint that lets tests inject a fake `LuauEvaluator`. The
3100
+ * public {@link loadConfig} wraps this with the real lute adapter; the rest
3101
+ * of the loader pipeline is identical.
3102
+ *
3103
+ * @param deps - Injected dependencies. Only the evaluator is configurable.
3104
+ * @param options - Same loader options accepted by {@link loadConfig}.
3105
+ * @returns Same `Result<Config, ConfigError>` shape as `loadConfig`.
3106
+ */
3107
+ async function loadConfigWith(deps, options) {
3108
+ const cwd = options?.cwd ?? process.cwd();
3109
+ const configFile = options?.configFile === void 0 ? void 0 : resolveConfigPath(cwd, options.configFile);
3110
+ if (configFile !== void 0 && !isExistingFile(configFile)) return {
3111
+ err: {
3112
+ kind: "fileNotFound",
3113
+ searchedFrom: cwd
3114
+ },
3115
+ success: false
3116
+ };
3117
+ let resolved;
3118
+ try {
3119
+ resolved = await loadConfig({
3120
+ name: "bedrock",
3121
+ cwd,
3122
+ resolve: makeLuauResolver({
3123
+ callerConfigFile: configFile,
3124
+ defaultCwd: cwd,
3125
+ evaluator: deps.evaluator
3126
+ }),
3127
+ ...configFile === void 0 ? {} : { configFile }
3128
+ });
3129
+ } catch (err) {
3130
+ return {
3131
+ err: attributeLoadError(err, cwd),
3132
+ success: false
3133
+ };
3134
+ }
3135
+ if (resolved._configFile === void 0) return {
3136
+ err: {
3137
+ kind: "fileNotFound",
3138
+ searchedFrom: cwd
3139
+ },
3140
+ success: false
3141
+ };
3142
+ return validateConfig(resolved.config, resolved._configFile);
3143
+ }
3144
+ /**
3062
3145
  * Discover, parse, and validate the project config.
3063
3146
  *
3064
3147
  * Looks for `bedrock.config.{ts,js,mjs,cjs,yaml,yml,json}`, `.bedrockrc*`,
@@ -3098,37 +3181,7 @@ function bootstrapDirectoryPrefix(pid) {
3098
3181
  * ```
3099
3182
  */
3100
3183
  async function loadConfig$1(options) {
3101
- const cwd = options?.cwd ?? process.cwd();
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);
3184
+ return loadConfigWith({ evaluator: createLuteLuauEvaluator() }, options);
3132
3185
  }
3133
3186
  function resolveConfigPath(cwd, configFile) {
3134
3187
  return isAbsolute(configFile) ? configFile : join(cwd, configFile);
@@ -3181,6 +3234,19 @@ const NATIVE_CONFIG_EXTENSIONS = [
3181
3234
  "yml"
3182
3235
  ];
3183
3236
  /**
3237
+ * Internal-only wrapper used at the c12 boundary: makeLuauResolver maps an
3238
+ * evaluator `Err` into this throwable, which `attributeLoadError` unwraps
3239
+ * directly. This keeps the port on the `Result` contract per ADR-009 while
3240
+ * still satisfying c12's exception-based `resolve` callback.
3241
+ */
3242
+ var EvaluatorThrow = class extends Error {
3243
+ configError;
3244
+ constructor(configError) {
3245
+ super();
3246
+ this.configError = configError;
3247
+ }
3248
+ };
3249
+ /**
3184
3250
  * Decide which Luau file the resolver should evaluate for a given c12 source,
3185
3251
  * or `undefined` to defer to c12's built-in loaders.
3186
3252
  *
@@ -3199,120 +3265,37 @@ function pickLuauTarget(source, context) {
3199
3265
  if (source === "." && callerConfigFile !== void 0) return callerConfigFile.endsWith(".luau") ? callerConfigFile : void 0;
3200
3266
  return locateLuauConfig(source, cwd);
3201
3267
  }
3202
- function makeLuauResolver(defaultCwd, callerConfigFile) {
3268
+ function evaluationErrorToConfigError(err, sourceFile) {
3269
+ if (err.kind === "missingRuntime") return {
3270
+ hint: err.hint,
3271
+ kind: "luauRuntimeMissing",
3272
+ sourceFile
3273
+ };
3274
+ return {
3275
+ kind: "parseFailed",
3276
+ message: err.message,
3277
+ sourceFile
3278
+ };
3279
+ }
3280
+ function makeLuauResolver(deps) {
3203
3281
  return async (source, c12Options) => {
3204
- const cwd = c12Options.cwd ?? defaultCwd;
3282
+ const cwd = c12Options.cwd ?? deps.defaultCwd;
3205
3283
  const luauPath = pickLuauTarget(source, {
3206
- callerConfigFile,
3284
+ callerConfigFile: deps.callerConfigFile,
3207
3285
  cwd
3208
3286
  });
3209
3287
  if (luauPath === void 0) return;
3288
+ const result = await deps.evaluator(luauPath);
3289
+ if (!result.success) throw new EvaluatorThrow(evaluationErrorToConfigError(result.err, luauPath));
3210
3290
  return {
3211
3291
  _configFile: luauPath,
3212
- config: await evaluateLuauConfig(luauPath),
3292
+ config: result.data,
3213
3293
  configFile: luauPath,
3214
3294
  cwd
3215
3295
  };
3216
3296
  };
3217
3297
  }
3218
3298
  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
3299
  const CONFIG_FILE_IN_FRAME = /[^\s():"']*bedrock\.config\.(?:ts|js|mjs|cjs|yaml|yml|json)/;
3317
3300
  function extractConfigFileFromStack(err) {
3318
3301
  if (!(err instanceof Error) || err.stack === void 0) return;
@@ -3334,11 +3317,7 @@ function discoverConfigFile(cwd) {
3334
3317
  return match === void 0 ? void 0 : join(cwd, match);
3335
3318
  }
3336
3319
  function attributeLoadError(err, cwd) {
3337
- if (err instanceof LuauRuntimeMissingError) return {
3338
- hint: err.hint,
3339
- kind: "luauRuntimeMissing",
3340
- sourceFile: err.sourceFile
3341
- };
3320
+ if (err instanceof EvaluatorThrow) return err.configError;
3342
3321
  const message = err instanceof Error ? err.message : String(err);
3343
3322
  const frameFile = extractConfigFileFromStack(err);
3344
3323
  if (frameFile !== void 0) return {
@@ -3357,7 +3336,7 @@ function attributeLoadError(err, cwd) {
3357
3336
  /**
3358
3337
  * Run a full reconcile end-to-end. Default-constructs missing deps from
3359
3338
  * the project config and the environment variables `GITHUB_TOKEN` and
3360
- * `ROBLOX_API_KEY`; never reads `process.env` when `statePort`,
3339
+ * `BEDROCK_API_KEY`; never reads `process.env` when `statePort`,
3361
3340
  * `registry`, and `config` are all supplied explicitly.
3362
3341
  *
3363
3342
  * @param options - Target environment plus optional overrides.
@@ -3591,14 +3570,9 @@ async function runReconcile(environment, deps) {
3591
3570
  * the shell from the icon file's bytes) and fall back to the
3592
3571
  * Mantle-recorded hashes when the map omits the key. Product resources
3593
3572
  * without an icon partner omit `icon` and `iconFileHashes` entirely. The
3594
- * universe resource attaches `icon` and `iconFileHashes` only when the
3595
- * fold produced an icon path and the shell supplied a recomputed hash;
3596
- * folds without an experience icon, and folds whose icon file could not
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`).
3573
+ * `outputs` field carries the Mantle-recorded identifiers (universe
3574
+ * `rootPlaceId`, place `versionNumber`, pass `assetId` and `iconAssetIds`,
3575
+ * product `productId` and optional `iconImageAssetId`).
3602
3576
  *
3603
3577
  * @param inputs - Folded data plus recomputed hashes for this environment.
3604
3578
  * @returns A `BedrockState` populated with one resource per folded kind.
@@ -3611,8 +3585,8 @@ function buildState(inputs) {
3611
3585
  };
3612
3586
  }
3613
3587
  function universeResource(inputs) {
3614
- const { entry, iconHashes, outputs } = inputs;
3615
- const base = {
3588
+ const { entry, outputs } = inputs;
3589
+ return {
3616
3590
  key: UNIVERSE_SINGLETON_KEY,
3617
3591
  consoleEnabled: entry.consoleEnabled,
3618
3592
  desktopEnabled: entry.desktopEnabled,
@@ -3625,12 +3599,6 @@ function universeResource(inputs) {
3625
3599
  voiceChatEnabled: entry.voiceChatEnabled,
3626
3600
  vrEnabled: entry.vrEnabled
3627
3601
  };
3628
- if (entry.icon === void 0 || iconHashes === void 0) return base;
3629
- return {
3630
- ...base,
3631
- icon: entry.icon,
3632
- iconFileHashes: iconHashes
3633
- };
3634
3602
  }
3635
3603
  function placeResource(key, fold) {
3636
3604
  return {
@@ -3676,10 +3644,9 @@ function productResource(fold, productIconHashesByKey) {
3676
3644
  };
3677
3645
  }
3678
3646
  function composeResources(inputs) {
3679
- const { folded, passIconHashesByKey, productIconHashesByKey, universeIconHashes } = inputs;
3647
+ const { folded, passIconHashesByKey, productIconHashesByKey } = inputs;
3680
3648
  const universeResources = folded.universe === void 0 ? [] : [universeResource({
3681
3649
  entry: folded.universe.entry,
3682
- iconHashes: universeIconHashes,
3683
3650
  outputs: folded.universe.outputs
3684
3651
  })];
3685
3652
  const placeResources = [...folded.places.entries()].map(([key, entry]) => placeResource(key, entry));
@@ -4304,14 +4271,14 @@ function readPassInputs(raw) {
4304
4271
  price: readPrice$1(raw)
4305
4272
  };
4306
4273
  }
4307
- function coerceRobloxId$4(value) {
4274
+ function coerceRobloxId$3(value) {
4308
4275
  if (typeof value === "string") return value;
4309
4276
  if (Number.isInteger(value)) return String(value);
4310
4277
  }
4311
4278
  function readPassOutputs(raw) {
4312
4279
  if (!isObjectPayload$2(raw)) return;
4313
- const assetId = coerceRobloxId$4(raw["assetId"]);
4314
- const iconAssetId = coerceRobloxId$4(raw["iconAssetId"]);
4280
+ const assetId = coerceRobloxId$3(raw["assetId"]);
4281
+ const iconAssetId = coerceRobloxId$3(raw["iconAssetId"]);
4315
4282
  if (assetId === void 0 || iconAssetId === void 0) return;
4316
4283
  return {
4317
4284
  assetId,
@@ -4358,7 +4325,7 @@ function isObjectPayload$1(value) {
4358
4325
  * @param value - Raw value pulled from a Mantle resource's outputs.
4359
4326
  * @returns The stringified ID, or `undefined` when the value is not a valid wire shape.
4360
4327
  */
4361
- function coerceRobloxId$3(value) {
4328
+ function coerceRobloxId$2(value) {
4362
4329
  if (typeof value === "string") return value;
4363
4330
  if (Number.isInteger(value)) return String(value);
4364
4331
  }
@@ -4632,14 +4599,14 @@ function isStartPlace$1(resource) {
4632
4599
  if (!isObjectPayload$1(resource.inputs)) return false;
4633
4600
  return resource.inputs["isStart"] === true;
4634
4601
  }
4635
- function coerceRobloxId$2(value) {
4602
+ function coerceRobloxId$1(value) {
4636
4603
  if (typeof value === "string") return value;
4637
4604
  if (Number.isInteger(value)) return String(value);
4638
4605
  }
4639
4606
  function readPlaceOutputs(resource) {
4640
4607
  const { outputs } = resource;
4641
4608
  if (!isObjectPayload$1(outputs)) return;
4642
- const assetId = coerceRobloxId$2(outputs["assetId"]);
4609
+ const assetId = coerceRobloxId$1(outputs["assetId"]);
4643
4610
  if (assetId === void 0) return;
4644
4611
  return { assetId };
4645
4612
  }
@@ -4762,7 +4729,7 @@ function readProductInputs(raw) {
4762
4729
  }
4763
4730
  function readProductOutputs(raw) {
4764
4731
  if (!isObjectPayload$1(raw)) return;
4765
- const productId = coerceRobloxId$3(raw["productId"]);
4732
+ const productId = coerceRobloxId$2(raw["productId"]);
4766
4733
  if (productId === void 0) return;
4767
4734
  return { productId };
4768
4735
  }
@@ -4778,7 +4745,7 @@ function readProductIconInputs(raw) {
4778
4745
  }
4779
4746
  function readProductIconOutputs(raw) {
4780
4747
  if (!isObjectPayload$1(raw)) return;
4781
- const assetId = coerceRobloxId$3(raw["assetId"]);
4748
+ const assetId = coerceRobloxId$2(raw["assetId"]);
4782
4749
  if (assetId === void 0) return;
4783
4750
  return { assetId };
4784
4751
  }
@@ -4960,51 +4927,36 @@ function foldDisplayName(resources) {
4960
4927
  //#endregion
4961
4928
  //#region src/core/migrate/fold-experience-icon.ts
4962
4929
  const EXPERIENCE_ICON_KIND = "experienceIcon";
4930
+ 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
4931
  /**
4964
- * Fold the Mantle `experienceIcon_<key>` resource into the universe's
4965
- * locale-keyed `icon` map and the `iconAssetIds` slot of its outputs.
4966
- * Mantle has no locale concept on `experienceIcon`; the fold assigns the
4967
- * single image to `"en-us"` and emits one `interpretive` warning so the
4968
- * migration report records the implicit locale assignment.
4932
+ * Surface every Mantle `experienceIcon_<key>` resource as a `blocked`
4933
+ * migration warning. Bedrock has no `UniverseEntry.icon` field today
4934
+ * because no Open Cloud endpoint accepts a source-language game icon, so
4935
+ * the migrator emits one warning per legacy resource (rather than the
4936
+ * first matching entry only) so the operator can audit each affected
4937
+ * environment before reconfiguring the icon by hand.
4969
4938
  *
4970
4939
  * Resources whose payload is malformed (non-object inputs/outputs,
4971
- * non-string `filePath`, missing or non-coercible `assetId`) drop
4972
- * silently. The first matching resource wins; ambiguity handling for
4973
- * multiple `experienceIcon` resources lands in a follow-up slice.
4940
+ * non-string `filePath`) are skipped silently, matching the
4941
+ * malformed-payload behaviour of the other fold rules.
4974
4942
  *
4975
4943
  * @param resources - Mantle resource list for one environment.
4976
- * @returns The folded icon entry plus per-rule diagnostics.
4944
+ * @returns A fragment whose `warnings` carries one `blocked` entry per
4945
+ * legacy experience-icon resource, or {@link EMPTY_FRAGMENT} when none
4946
+ * are present.
4977
4947
  */
4978
4948
  function foldExperienceIcon(resources) {
4979
- const [first] = resources.filter((resource) => resource.kind === EXPERIENCE_ICON_KIND);
4980
- if (first === void 0) return EMPTY_FRAGMENT;
4981
- const parts = readParts(first);
4982
- if (parts === void 0) return EMPTY_FRAGMENT;
4949
+ const warnings = resources.filter((resource) => resource.kind === EXPERIENCE_ICON_KIND && hasReadablePayload(resource)).map((resource) => blockedWarning(`${EXPERIENCE_ICON_KIND}_${resource.key}`, BLOCKED_REASON));
4950
+ if (warnings.length === 0) return EMPTY_FRAGMENT;
4983
4951
  return {
4984
- entryFragment: { icon: { "en-us": parts.filePath } },
4985
- outputsFragment: { iconAssetIds: { "en-us": parts.assetId } },
4986
- warnings: [interpretiveWarning({
4987
- bedrockPath: "universe.icon",
4988
- mantlePath: `${EXPERIENCE_ICON_KIND}_${first.key}`,
4989
- rule: "experience-icon-to-en-us-locale"
4990
- })]
4952
+ entryFragment: {},
4953
+ warnings
4991
4954
  };
4992
4955
  }
4993
- function coerceRobloxId$1(value) {
4994
- const candidate = String(value);
4995
- return isRobloxAssetId(candidate) ? candidate : void 0;
4996
- }
4997
- function readParts(resource) {
4998
- if (!isObjectPayload$1(resource.inputs)) return;
4956
+ function hasReadablePayload(resource) {
4957
+ if (!isObjectPayload$1(resource.inputs)) return false;
4999
4958
  const { filePath } = resource.inputs;
5000
- if (typeof filePath !== "string") return;
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
- };
4959
+ return typeof filePath === "string";
5008
4960
  }
5009
4961
  //#endregion
5010
4962
  //#region src/core/migrate/fold-social-links.ts
@@ -5480,20 +5432,17 @@ function summarizeWarnings(warnings) {
5480
5432
  //#endregion
5481
5433
  //#region src/shell/recompute-icon-hashes.ts
5482
5434
  /**
5483
- * Walk each environment's folded pass, product, and universe entries,
5484
- * resolve the locale-keyed icon paths against `stateFileDirectory`, read
5485
- * the bytes via the injected `readFile`, and compute the SHA-256 hex
5486
- * digest. Files that cannot be read surface as `ambiguous`
5487
- * `MigrationWarning`s with the environment-prefixed `mantlePath` and a
5488
- * hint pointing at the resolved path; the caller carries the
5489
- * Mantle-recorded hashes forward as a fallback for passes and products,
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.
5435
+ * Walk each environment's folded pass and product entries, resolve the
5436
+ * locale-keyed icon paths against `stateFileDirectory`, read the bytes via
5437
+ * the injected `readFile`, and compute the SHA-256 hex digest. Files that
5438
+ * cannot be read surface as `ambiguous` `MigrationWarning`s with the
5439
+ * environment-prefixed `mantlePath` and a hint pointing at the resolved
5440
+ * path; the caller carries the Mantle-recorded hashes forward as a
5441
+ * fallback. Products without an icon partner are silently skipped.
5493
5442
  *
5494
5443
  * @param inputs - Per-environment fold results plus I/O dependencies.
5495
- * @returns Per-environment recomputed pass, product, and universe hashes
5496
- * plus accumulated ambiguous warnings.
5444
+ * @returns Per-environment recomputed pass and product hashes plus
5445
+ * accumulated ambiguous warnings.
5497
5446
  */
5498
5447
  async function recomputeIconHashes(inputs) {
5499
5448
  return collectRecomputation(await Promise.all([...inputs.folds.entries()].map(async ([environment, folded]) => {
@@ -5509,9 +5458,6 @@ function collectRecomputation(walked) {
5509
5458
  return {
5510
5459
  passHashesByEnvironment: new Map(walked.map(([environment, walk]) => [environment, walk.passHashes])),
5511
5460
  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
5461
  warnings: walked.flatMap(([, walk]) => walk.warnings)
5516
5462
  };
5517
5463
  }
@@ -5562,27 +5508,6 @@ async function walkIconEntries(inputs) {
5562
5508
  warnings
5563
5509
  };
5564
5510
  }
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
5511
  async function walkEnvironment(inputs) {
5587
5512
  const passWalk = await walkIconEntries({
5588
5513
  entries: inputs.folded.passes.map(passWalkEntry),
@@ -5596,21 +5521,10 @@ async function walkEnvironment(inputs) {
5596
5521
  readFile: inputs.readFile,
5597
5522
  stateFileDirectory: inputs.stateFileDirectory
5598
5523
  });
5599
- const universeWalk = await walkUniverseIcon({
5600
- environmentName: inputs.environmentName,
5601
- folded: inputs.folded,
5602
- readFile: inputs.readFile,
5603
- stateFileDirectory: inputs.stateFileDirectory
5604
- });
5605
5524
  return {
5606
5525
  passHashes: passWalk.perKey,
5607
5526
  productHashes: productWalk.perKey,
5608
- universeHash: universeWalk.hash,
5609
- warnings: [
5610
- ...passWalk.warnings,
5611
- ...productWalk.warnings,
5612
- ...universeWalk.warnings
5613
- ]
5527
+ warnings: [...passWalk.warnings, ...productWalk.warnings]
5614
5528
  };
5615
5529
  }
5616
5530
  //#endregion
@@ -5705,8 +5619,7 @@ function buildStatesByEnvironment(inputs) {
5705
5619
  environment: name,
5706
5620
  folded,
5707
5621
  passIconHashesByKey: inputs.passHashesByEnvironment.get(name) ?? EMPTY_HASHES,
5708
- productIconHashesByKey: inputs.productHashesByEnvironment.get(name) ?? EMPTY_HASHES,
5709
- universeIconHashes: inputs.universeHashByEnvironment.get(name)
5622
+ productIconHashesByKey: inputs.productHashesByEnvironment.get(name) ?? EMPTY_HASHES
5710
5623
  })];
5711
5624
  }));
5712
5625
  }
@@ -5722,7 +5635,7 @@ function collectFoldWarnings(folds) {
5722
5635
  });
5723
5636
  }
5724
5637
  function buildReport(inputs, validated) {
5725
- const { passHashesByEnvironment, productHashesByEnvironment, universeHashByEnvironment, warnings: iconWarnings } = inputs.iconRecomputation;
5638
+ const { passHashesByEnvironment, productHashesByEnvironment, warnings: iconWarnings } = inputs.iconRecomputation;
5726
5639
  const warnings = [
5727
5640
  ...collectFoldWarnings(inputs.folds),
5728
5641
  ...inputs.factorizeWarnings,
@@ -5737,8 +5650,7 @@ function buildReport(inputs, validated) {
5737
5650
  statesByEnvironment: buildStatesByEnvironment({
5738
5651
  folds: inputs.folds,
5739
5652
  passHashesByEnvironment,
5740
- productHashesByEnvironment,
5741
- universeHashByEnvironment
5653
+ productHashesByEnvironment
5742
5654
  }),
5743
5655
  summary: summarizeWarnings(warnings),
5744
5656
  warnings
@@ -5786,4 +5698,4 @@ function isFileMissing(err) {
5786
5698
  //#endregion
5787
5699
  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
5700
 
5789
- //# sourceMappingURL=migrate-mantle-state-DqbJ1TLq.mjs.map
5701
+ //# sourceMappingURL=migrate-mantle-state-_7Tkn0hG.mjs.map