@bedrock-rbx/core 0.1.0-beta.1

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.
@@ -0,0 +1,1731 @@
1
+ import { Result } from "@bedrock-rbx/ocale";
2
+ import { SocialLink } from "@bedrock-rbx/ocale/universes";
3
+
4
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/union-to-intersection.d.ts
5
+ /**
6
+ Convert a union type to an intersection type.
7
+
8
+ Inspired by [this Stack Overflow answer](https://stackoverflow.com/a/50375286/2172153).
9
+
10
+ @example
11
+ ```
12
+ import type {UnionToIntersection} from 'type-fest';
13
+
14
+ type Union = {the(): void} | {great(arg: string): void} | {escape: boolean};
15
+
16
+ type Intersection = UnionToIntersection<Union>;
17
+ //=> {the(): void} & {great(arg: string): void} & {escape: boolean}
18
+ ```
19
+
20
+ @category Type
21
+ */
22
+ type UnionToIntersection<Union> = (// `extends unknown` is always going to be the case and is used to convert the
23
+ // `Union` into a [distributive conditional
24
+ // type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types).
25
+ Union extends unknown // The union type is used as the only argument to a function since the union
26
+ // of function arguments is an intersection.
27
+ ? (distributedUnion: Union) => void // This won't happen.
28
+ : never // Infer the `Intersection` type since TypeScript represents the positional
29
+ // arguments of unions of functions as an intersection of the union.
30
+ ) extends ((mergedIntersection: infer Intersection) => void) // The `& Union` is to ensure result of `UnionToIntersection<A | B>` is always assignable to `A | B`
31
+ ? Intersection & Union : never;
32
+ //#endregion
33
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/keys-of-union.d.ts
34
+ /**
35
+ Create a union of all keys from a given type, even those exclusive to specific union members.
36
+
37
+ Unlike the native `keyof` keyword, which returns keys present in **all** union members, this type returns keys from **any** member.
38
+
39
+ @link https://stackoverflow.com/a/49402091
40
+
41
+ @example
42
+ ```
43
+ import type {KeysOfUnion} from 'type-fest';
44
+
45
+ type A = {
46
+ common: string;
47
+ a: number;
48
+ };
49
+
50
+ type B = {
51
+ common: string;
52
+ b: string;
53
+ };
54
+
55
+ type C = {
56
+ common: string;
57
+ c: boolean;
58
+ };
59
+
60
+ type Union = A | B | C;
61
+
62
+ type CommonKeys = keyof Union;
63
+ //=> 'common'
64
+
65
+ type AllKeys = KeysOfUnion<Union>;
66
+ //=> 'common' | 'a' | 'b' | 'c'
67
+ ```
68
+
69
+ @category Object
70
+ */
71
+ type KeysOfUnion<ObjectType> = // Hack to fix https://github.com/sindresorhus/type-fest/issues/1008
72
+ keyof UnionToIntersection<ObjectType extends unknown ? Record<keyof ObjectType, never> : never>;
73
+ //#endregion
74
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/is-any.d.ts
75
+ /**
76
+ Returns a boolean for whether the given type is `any`.
77
+
78
+ @link https://stackoverflow.com/a/49928360/1490091
79
+
80
+ Useful in type utilities, such as disallowing `any`s to be passed to a function.
81
+
82
+ @example
83
+ ```
84
+ import type {IsAny} from 'type-fest';
85
+
86
+ const typedObject = {a: 1, b: 2} as const;
87
+ const anyObject: any = {a: 1, b: 2};
88
+
89
+ function get<O extends (IsAny<O> extends true ? {} : Record<string, number>), K extends keyof O = keyof O>(object: O, key: K) {
90
+ return object[key];
91
+ }
92
+
93
+ const typedA = get(typedObject, 'a');
94
+ //=> 1
95
+
96
+ const anyA = get(anyObject, 'a');
97
+ //=> any
98
+ ```
99
+
100
+ @category Type Guard
101
+ @category Utilities
102
+ */
103
+ type IsAny<T> = 0 extends 1 & NoInfer<T> ? true : false;
104
+ //#endregion
105
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/is-optional-key-of.d.ts
106
+ /**
107
+ Returns a boolean for whether the given key is an optional key of type.
108
+
109
+ This is useful when writing utility types or schema validators that need to differentiate `optional` keys.
110
+
111
+ @example
112
+ ```
113
+ import type {IsOptionalKeyOf} from 'type-fest';
114
+
115
+ type User = {
116
+ name: string;
117
+ surname: string;
118
+
119
+ luckyNumber?: number;
120
+ };
121
+
122
+ type Admin = {
123
+ name: string;
124
+ surname?: string;
125
+ };
126
+
127
+ type T1 = IsOptionalKeyOf<User, 'luckyNumber'>;
128
+ //=> true
129
+
130
+ type T2 = IsOptionalKeyOf<User, 'name'>;
131
+ //=> false
132
+
133
+ type T3 = IsOptionalKeyOf<User, 'name' | 'luckyNumber'>;
134
+ //=> boolean
135
+
136
+ type T4 = IsOptionalKeyOf<User | Admin, 'name'>;
137
+ //=> false
138
+
139
+ type T5 = IsOptionalKeyOf<User | Admin, 'surname'>;
140
+ //=> boolean
141
+ ```
142
+
143
+ @category Type Guard
144
+ @category Utilities
145
+ */
146
+ type IsOptionalKeyOf<Type extends object, Key extends keyof Type> = IsAny<Type | Key> extends true ? never : Key extends keyof Type ? Type extends Record<Key, Type[Key]> ? false : true : false;
147
+ //#endregion
148
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/optional-keys-of.d.ts
149
+ /**
150
+ Extract all optional keys from the given type.
151
+
152
+ This is useful when you want to create a new type that contains different type values for the optional keys only.
153
+
154
+ @example
155
+ ```
156
+ import type {OptionalKeysOf, Except} from 'type-fest';
157
+
158
+ type User = {
159
+ name: string;
160
+ surname: string;
161
+
162
+ luckyNumber?: number;
163
+ };
164
+
165
+ const REMOVE_FIELD = Symbol('remove field symbol');
166
+ type UpdateOperation<Entity extends object> = Except<Partial<Entity>, OptionalKeysOf<Entity>> & {
167
+ [Key in OptionalKeysOf<Entity>]?: Entity[Key] | typeof REMOVE_FIELD;
168
+ };
169
+
170
+ const update1: UpdateOperation<User> = {
171
+ name: 'Alice',
172
+ };
173
+
174
+ const update2: UpdateOperation<User> = {
175
+ name: 'Bob',
176
+ luckyNumber: REMOVE_FIELD,
177
+ };
178
+ ```
179
+
180
+ @category Utilities
181
+ */
182
+ type OptionalKeysOf<Type extends object> = Type extends unknown // For distributing `Type`
183
+ ? (keyof { [Key in keyof Type as IsOptionalKeyOf<Type, Key> extends false ? never : Key]: never }) & keyof Type // Intersect with `keyof Type` to ensure result of `OptionalKeysOf<Type>` is always assignable to `keyof Type`
184
+ : never;
185
+ //#endregion
186
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/required-keys-of.d.ts
187
+ /**
188
+ Extract all required keys from the given type.
189
+
190
+ This is useful when you want to create a new type that contains different type values for the required keys only or use the list of keys for validation purposes, etc...
191
+
192
+ @example
193
+ ```
194
+ import type {RequiredKeysOf} from 'type-fest';
195
+
196
+ declare function createValidation<
197
+ Entity extends object,
198
+ Key extends RequiredKeysOf<Entity> = RequiredKeysOf<Entity>,
199
+ >(field: Key, validator: (value: Entity[Key]) => boolean): (entity: Entity) => boolean;
200
+
201
+ type User = {
202
+ name: string;
203
+ surname: string;
204
+ luckyNumber?: number;
205
+ };
206
+
207
+ const validator1 = createValidation<User>('name', value => value.length < 25);
208
+ const validator2 = createValidation<User>('surname', value => value.length < 25);
209
+
210
+ // @ts-expect-error
211
+ const validator3 = createValidation<User>('luckyNumber', value => value > 0);
212
+ // Error: Argument of type '"luckyNumber"' is not assignable to parameter of type '"name" | "surname"'.
213
+ ```
214
+
215
+ @category Utilities
216
+ */
217
+ type RequiredKeysOf<Type extends object> = Type extends unknown // For distributing `Type`
218
+ ? Exclude<keyof Type, OptionalKeysOf<Type>> : never;
219
+ //#endregion
220
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/is-never.d.ts
221
+ /**
222
+ Returns a boolean for whether the given type is `never`.
223
+
224
+ @link https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919
225
+ @link https://stackoverflow.com/a/53984913/10292952
226
+ @link https://www.zhenghao.io/posts/ts-never
227
+
228
+ Useful in type utilities, such as checking if something does not occur.
229
+
230
+ @example
231
+ ```
232
+ import type {IsNever, And} from 'type-fest';
233
+
234
+ type A = IsNever<never>;
235
+ //=> true
236
+
237
+ type B = IsNever<any>;
238
+ //=> false
239
+
240
+ type C = IsNever<unknown>;
241
+ //=> false
242
+
243
+ type D = IsNever<never[]>;
244
+ //=> false
245
+
246
+ type E = IsNever<object>;
247
+ //=> false
248
+
249
+ type F = IsNever<string>;
250
+ //=> false
251
+ ```
252
+
253
+ @example
254
+ ```
255
+ import type {IsNever} from 'type-fest';
256
+
257
+ type IsTrue<T> = T extends true ? true : false;
258
+
259
+ // When a distributive conditional is instantiated with `never`, the entire conditional results in `never`.
260
+ type A = IsTrue<never>;
261
+ //=> never
262
+
263
+ // If you don't want that behaviour, you can explicitly add an `IsNever` check before the distributive conditional.
264
+ type IsTrueFixed<T> =
265
+ IsNever<T> extends true ? false : T extends true ? true : false;
266
+
267
+ type B = IsTrueFixed<never>;
268
+ //=> false
269
+ ```
270
+
271
+ @category Type Guard
272
+ @category Utilities
273
+ */
274
+ type IsNever<T> = [T] extends [never] ? true : false;
275
+ //#endregion
276
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/if.d.ts
277
+ /**
278
+ An if-else-like type that resolves depending on whether the given `boolean` type is `true` or `false`.
279
+
280
+ Use-cases:
281
+ - You can use this in combination with `Is*` types to create an if-else-like experience. For example, `If<IsAny<any>, 'is any', 'not any'>`.
282
+
283
+ Note:
284
+ - Returns a union of if branch and else branch if the given type is `boolean` or `any`. For example, `If<boolean, 'Y', 'N'>` will return `'Y' | 'N'`.
285
+ - Returns the else branch if the given type is `never`. For example, `If<never, 'Y', 'N'>` will return `'N'`.
286
+
287
+ @example
288
+ ```
289
+ import type {If} from 'type-fest';
290
+
291
+ type A = If<true, 'yes', 'no'>;
292
+ //=> 'yes'
293
+
294
+ type B = If<false, 'yes', 'no'>;
295
+ //=> 'no'
296
+
297
+ type C = If<boolean, 'yes', 'no'>;
298
+ //=> 'yes' | 'no'
299
+
300
+ type D = If<any, 'yes', 'no'>;
301
+ //=> 'yes' | 'no'
302
+
303
+ type E = If<never, 'yes', 'no'>;
304
+ //=> 'no'
305
+ ```
306
+
307
+ @example
308
+ ```
309
+ import type {If, IsAny, IsNever} from 'type-fest';
310
+
311
+ type A = If<IsAny<unknown>, 'is any', 'not any'>;
312
+ //=> 'not any'
313
+
314
+ type B = If<IsNever<never>, 'is never', 'not never'>;
315
+ //=> 'is never'
316
+ ```
317
+
318
+ @example
319
+ ```
320
+ import type {If, IsEqual} from 'type-fest';
321
+
322
+ type IfEqual<T, U, IfBranch, ElseBranch> = If<IsEqual<T, U>, IfBranch, ElseBranch>;
323
+
324
+ type A = IfEqual<string, string, 'equal', 'not equal'>;
325
+ //=> 'equal'
326
+
327
+ type B = IfEqual<string, number, 'equal', 'not equal'>;
328
+ //=> 'not equal'
329
+ ```
330
+
331
+ Note: Sometimes using the `If` type can make an implementation non–tail-recursive, which can impact performance. In such cases, it’s better to use a conditional directly. Refer to the following example:
332
+
333
+ @example
334
+ ```
335
+ import type {If, IsEqual, StringRepeat} from 'type-fest';
336
+
337
+ type HundredZeroes = StringRepeat<'0', 100>;
338
+
339
+ // The following implementation is not tail recursive
340
+ type Includes<S extends string, Char extends string> =
341
+ S extends `${infer First}${infer Rest}`
342
+ ? If<IsEqual<First, Char>,
343
+ 'found',
344
+ Includes<Rest, Char>>
345
+ : 'not found';
346
+
347
+ // Hence, instantiations with long strings will fail
348
+ // @ts-expect-error
349
+ type Fails = Includes<HundredZeroes, '1'>;
350
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
351
+ // Error: Type instantiation is excessively deep and possibly infinite.
352
+
353
+ // However, if we use a simple conditional instead of `If`, the implementation becomes tail-recursive
354
+ type IncludesWithoutIf<S extends string, Char extends string> =
355
+ S extends `${infer First}${infer Rest}`
356
+ ? IsEqual<First, Char> extends true
357
+ ? 'found'
358
+ : IncludesWithoutIf<Rest, Char>
359
+ : 'not found';
360
+
361
+ // Now, instantiations with long strings will work
362
+ type Works = IncludesWithoutIf<HundredZeroes, '1'>;
363
+ //=> 'not found'
364
+ ```
365
+
366
+ @category Type Guard
367
+ @category Utilities
368
+ */
369
+ type If<Type extends boolean, IfBranch, ElseBranch> = IsNever<Type> extends true ? ElseBranch : Type extends true ? IfBranch : ElseBranch;
370
+ //#endregion
371
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/unknown-array.d.ts
372
+ /**
373
+ Represents an array with `unknown` value.
374
+
375
+ Use case: You want a type that all arrays can be assigned to, but you don't care about the value.
376
+
377
+ @example
378
+ ```
379
+ import type {UnknownArray} from 'type-fest';
380
+
381
+ type IsArray<T> = T extends UnknownArray ? true : false;
382
+
383
+ type A = IsArray<['foo']>;
384
+ //=> true
385
+
386
+ type B = IsArray<readonly number[]>;
387
+ //=> true
388
+
389
+ type C = IsArray<string>;
390
+ //=> false
391
+ ```
392
+
393
+ @category Type
394
+ @category Array
395
+ */
396
+ type UnknownArray = readonly unknown[];
397
+ //#endregion
398
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/internal/array.d.ts
399
+ /**
400
+ Returns whether the given array `T` is readonly.
401
+ */
402
+ type IsArrayReadonly<T extends UnknownArray> = If<IsNever<T>, false, T extends unknown[] ? false : true>;
403
+ //#endregion
404
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/simplify.d.ts
405
+ /**
406
+ Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability.
407
+
408
+ @example
409
+ ```
410
+ import type {Simplify} from 'type-fest';
411
+
412
+ type PositionProps = {
413
+ top: number;
414
+ left: number;
415
+ };
416
+
417
+ type SizeProps = {
418
+ width: number;
419
+ height: number;
420
+ };
421
+
422
+ // In your editor, hovering over `Props` will show a flattened object with all the properties.
423
+ type Props = Simplify<PositionProps & SizeProps>;
424
+ ```
425
+
426
+ Sometimes it is desired to pass a value as a function argument that has a different type. At first inspection it may seem assignable, and then you discover it is not because the `value`'s type definition was defined as an interface. In the following example, `fn` requires an argument of type `Record<string, unknown>`. If the value is defined as a literal, then it is assignable. And if the `value` is defined as type using the `Simplify` utility the value is assignable. But if the `value` is defined as an interface, it is not assignable because the interface is not sealed and elsewhere a non-string property could be added to the interface.
427
+
428
+ If the type definition must be an interface (perhaps it was defined in a third-party npm package), then the `value` can be defined as `const value: Simplify<SomeInterface> = ...`. Then `value` will be assignable to the `fn` argument. Or the `value` can be cast as `Simplify<SomeInterface>` if you can't re-declare the `value`.
429
+
430
+ @example
431
+ ```
432
+ import type {Simplify} from 'type-fest';
433
+
434
+ interface SomeInterface {
435
+ foo: number;
436
+ bar?: string;
437
+ baz: number | undefined;
438
+ }
439
+
440
+ type SomeType = {
441
+ foo: number;
442
+ bar?: string;
443
+ baz: number | undefined;
444
+ };
445
+
446
+ const literal = {foo: 123, bar: 'hello', baz: 456};
447
+ const someType: SomeType = literal;
448
+ const someInterface: SomeInterface = literal;
449
+
450
+ declare function fn(object: Record<string, unknown>): void;
451
+
452
+ fn(literal); // Good: literal object type is sealed
453
+ fn(someType); // Good: type is sealed
454
+ // @ts-expect-error
455
+ fn(someInterface); // Error: Index signature for type 'string' is missing in type 'someInterface'. Because `interface` can be re-opened
456
+ fn(someInterface as Simplify<SomeInterface>); // Good: transform an `interface` into a `type`
457
+ ```
458
+
459
+ @link https://github.com/microsoft/TypeScript/issues/15300
460
+ @see {@link SimplifyDeep}
461
+ @category Object
462
+ */
463
+ type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
464
+ //#endregion
465
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/is-equal.d.ts
466
+ /**
467
+ Returns a boolean for whether the two given types are equal.
468
+
469
+ @link https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650
470
+ @link https://stackoverflow.com/questions/68961864/how-does-the-equals-work-in-typescript/68963796#68963796
471
+
472
+ Use-cases:
473
+ - If you want to make a conditional branch based on the result of a comparison of two types.
474
+
475
+ @example
476
+ ```
477
+ import type {IsEqual} from 'type-fest';
478
+
479
+ // This type returns a boolean for whether the given array includes the given item.
480
+ // `IsEqual` is used to compare the given array at position 0 and the given item and then return true if they are equal.
481
+ type Includes<Value extends readonly any[], Item> =
482
+ Value extends readonly [Value[0], ...infer rest]
483
+ ? IsEqual<Value[0], Item> extends true
484
+ ? true
485
+ : Includes<rest, Item>
486
+ : false;
487
+ ```
488
+
489
+ @category Type Guard
490
+ @category Utilities
491
+ */
492
+ type IsEqual<A, B> = [A] extends [B] ? [B] extends [A] ? _IsEqual<A, B> : false : false;
493
+ // This version fails the `equalWrappedTupleIntersectionToBeNeverAndNeverExpanded` test in `test-d/is-equal.ts`.
494
+ type _IsEqual<A, B> = (<G>() => G extends A & G | G ? 1 : 2) extends (<G>() => G extends B & G | G ? 1 : 2) ? true : false;
495
+ //#endregion
496
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/omit-index-signature.d.ts
497
+ /**
498
+ Omit any index signatures from the given object type, leaving only explicitly defined properties.
499
+
500
+ This is the counterpart of `PickIndexSignature`.
501
+
502
+ Use-cases:
503
+ - Remove overly permissive signatures from third-party types.
504
+
505
+ This type was taken from this [StackOverflow answer](https://stackoverflow.com/a/68261113/420747).
506
+
507
+ It relies on the fact that an empty object (`{}`) is assignable to an object with just an index signature, like `Record<string, unknown>`, but not to an object with explicitly defined keys, like `Record<'foo' | 'bar', unknown>`.
508
+
509
+ (The actual value type, `unknown`, is irrelevant and could be any type. Only the key type matters.)
510
+
511
+ ```
512
+ const indexed: Record<string, unknown> = {}; // Allowed
513
+
514
+ // @ts-expect-error
515
+ const keyed: Record<'foo', unknown> = {}; // Error
516
+ // TS2739: Type '{}' is missing the following properties from type 'Record<"foo" | "bar", unknown>': foo, bar
517
+ ```
518
+
519
+ Instead of causing a type error like the above, you can also use a [conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) to test whether a type is assignable to another:
520
+
521
+ ```
522
+ type Indexed = {} extends Record<string, unknown>
523
+ ? '✅ `{}` is assignable to `Record<string, unknown>`'
524
+ : '❌ `{}` is NOT assignable to `Record<string, unknown>`';
525
+
526
+ type IndexedResult = Indexed;
527
+ //=> '✅ `{}` is assignable to `Record<string, unknown>`'
528
+
529
+ type Keyed = {} extends Record<'foo' | 'bar', unknown>
530
+ ? '✅ `{}` is assignable to `Record<\'foo\' | \'bar\', unknown>`'
531
+ : '❌ `{}` is NOT assignable to `Record<\'foo\' | \'bar\', unknown>`';
532
+
533
+ type KeyedResult = Keyed;
534
+ //=> '❌ `{}` is NOT assignable to `Record<\'foo\' | \'bar\', unknown>`'
535
+ ```
536
+
537
+ Using a [mapped type](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#further-exploration), you can then check for each `KeyType` of `ObjectType`...
538
+
539
+ ```
540
+ type OmitIndexSignature<ObjectType> = {
541
+ [KeyType in keyof ObjectType // Map each key of `ObjectType`...
542
+ ]: ObjectType[KeyType]; // ...to its original value, i.e. `OmitIndexSignature<Foo> == Foo`.
543
+ };
544
+ ```
545
+
546
+ ...whether an empty object (`{}`) would be assignable to an object with that `KeyType` (`Record<KeyType, unknown>`)...
547
+
548
+ ```
549
+ type OmitIndexSignature<ObjectType> = {
550
+ [KeyType in keyof ObjectType
551
+ // Is `{}` assignable to `Record<KeyType, unknown>`?
552
+ as {} extends Record<KeyType, unknown>
553
+ ? never // ✅ `{}` is assignable to `Record<KeyType, unknown>`
554
+ : KeyType // ❌ `{}` is NOT assignable to `Record<KeyType, unknown>`
555
+ ]: ObjectType[KeyType];
556
+ };
557
+ ```
558
+
559
+ If `{}` is assignable, it means that `KeyType` is an index signature and we want to remove it. If it is not assignable, `KeyType` is a "real" key and we want to keep it.
560
+
561
+ @example
562
+ ```
563
+ import type {OmitIndexSignature} from 'type-fest';
564
+
565
+ type Example = {
566
+ // These index signatures will be removed.
567
+ [x: string]: any;
568
+ [x: number]: any;
569
+ [x: symbol]: any;
570
+ [x: `head-${string}`]: string;
571
+ [x: `${string}-tail`]: string;
572
+ [x: `head-${string}-tail`]: string;
573
+ [x: `${bigint}`]: string;
574
+ [x: `embedded-${number}`]: string;
575
+
576
+ // These explicitly defined keys will remain.
577
+ foo: 'bar';
578
+ qux?: 'baz';
579
+ };
580
+
581
+ type ExampleWithoutIndexSignatures = OmitIndexSignature<Example>;
582
+ //=> {foo: 'bar'; qux?: 'baz'}
583
+ ```
584
+
585
+ @see {@link PickIndexSignature}
586
+ @category Object
587
+ */
588
+ type OmitIndexSignature<ObjectType> = { [KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType] };
589
+ //#endregion
590
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/pick-index-signature.d.ts
591
+ /**
592
+ Pick only index signatures from the given object type, leaving out all explicitly defined properties.
593
+
594
+ This is the counterpart of `OmitIndexSignature`.
595
+
596
+ @example
597
+ ```
598
+ import type {PickIndexSignature} from 'type-fest';
599
+
600
+ declare const symbolKey: unique symbol;
601
+
602
+ type Example = {
603
+ // These index signatures will remain.
604
+ [x: string]: unknown;
605
+ [x: number]: unknown;
606
+ [x: symbol]: unknown;
607
+ [x: `head-${string}`]: string;
608
+ [x: `${string}-tail`]: string;
609
+ [x: `head-${string}-tail`]: string;
610
+ [x: `${bigint}`]: string;
611
+ [x: `embedded-${number}`]: string;
612
+
613
+ // These explicitly defined keys will be removed.
614
+ ['kebab-case-key']: string;
615
+ [symbolKey]: string;
616
+ foo: 'bar';
617
+ qux?: 'baz';
618
+ };
619
+
620
+ type ExampleIndexSignature = PickIndexSignature<Example>;
621
+ // {
622
+ // [x: string]: unknown;
623
+ // [x: number]: unknown;
624
+ // [x: symbol]: unknown;
625
+ // [x: `head-${string}`]: string;
626
+ // [x: `${string}-tail`]: string;
627
+ // [x: `head-${string}-tail`]: string;
628
+ // [x: `${bigint}`]: string;
629
+ // [x: `embedded-${number}`]: string;
630
+ // }
631
+ ```
632
+
633
+ @see {@link OmitIndexSignature}
634
+ @category Object
635
+ */
636
+ type PickIndexSignature<ObjectType> = { [KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? KeyType : never]: ObjectType[KeyType] };
637
+ //#endregion
638
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/merge.d.ts
639
+ // Merges two objects without worrying about index signatures.
640
+ type SimpleMerge<Destination, Source> = Simplify<{ [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key] } & Source>;
641
+ /**
642
+ Merge two types into a new type. Keys of the second type overrides keys of the first type.
643
+
644
+ This is different from the TypeScript `&` (intersection) operator. With `&`, conflicting property types are intersected, which often results in `never`. For example, `{a: string} & {a: number}` makes `a` become `string & number`, which resolves to `never`. With `Merge`, the second type's keys cleanly override the first, so `Merge<{a: string}, {a: number}>` gives `{a: number}` as expected. `Merge` also produces a flattened type (via `Simplify`), making it more readable in IDE tooltips compared to `A & B`.
645
+
646
+ @example
647
+ ```
648
+ import type {Merge} from 'type-fest';
649
+
650
+ type Foo = {
651
+ a: string;
652
+ b: number;
653
+ };
654
+
655
+ type Bar = {
656
+ a: number; // Conflicts with Foo['a']
657
+ c: boolean;
658
+ };
659
+
660
+ // With `&`, `a` becomes `string & number` which is `never`. Not what you want.
661
+ type WithIntersection = (Foo & Bar)['a'];
662
+ //=> never
663
+
664
+ // With `Merge`, `a` is cleanly overridden to `number`.
665
+ type WithMerge = Merge<Foo, Bar>['a'];
666
+ //=> number
667
+ ```
668
+
669
+ @example
670
+ ```
671
+ import type {Merge} from 'type-fest';
672
+
673
+ type Foo = {
674
+ [x: string]: unknown;
675
+ [x: number]: unknown;
676
+ foo: string;
677
+ bar: symbol;
678
+ };
679
+
680
+ type Bar = {
681
+ [x: number]: number;
682
+ [x: symbol]: unknown;
683
+ bar: Date;
684
+ baz: boolean;
685
+ };
686
+
687
+ export type FooBar = Merge<Foo, Bar>;
688
+ //=> {
689
+ // [x: string]: unknown;
690
+ // [x: number]: number;
691
+ // [x: symbol]: unknown;
692
+ // foo: string;
693
+ // bar: Date;
694
+ // baz: boolean;
695
+ // }
696
+ ```
697
+
698
+ Note: If you want a merge type that more accurately reflects the runtime behavior of object spread or `Object.assign`, refer to the {@link ObjectMerge} type.
699
+
700
+ @see {@link ObjectMerge}
701
+ @category Object
702
+ */
703
+ type Merge<Destination, Source> = Destination extends unknown // For distributing `Destination`
704
+ ? Source extends unknown // For distributing `Source`
705
+ ? If<IsEqual<Destination, Source>, Destination, _Merge<Destination, Source>> : never // Should never happen
706
+ : never;
707
+ // Should never happen
708
+ type _Merge<Destination, Source> = Simplify<SimpleMerge<PickIndexSignature<Destination>, PickIndexSignature<Source>> & SimpleMerge<OmitIndexSignature<Destination>, OmitIndexSignature<Source>>>;
709
+ //#endregion
710
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/internal/object.d.ts
711
+ /**
712
+ Works similar to the built-in `Pick` utility type, except for the following differences:
713
+ - Distributes over union types and allows picking keys from any member of the union type.
714
+ - Primitives types are returned as-is.
715
+ - Picks all keys if `Keys` is `any`.
716
+ - Doesn't pick `number` from a `string` index signature.
717
+
718
+ @example
719
+ ```
720
+ type ImageUpload = {
721
+ url: string;
722
+ size: number;
723
+ thumbnailUrl: string;
724
+ };
725
+
726
+ type VideoUpload = {
727
+ url: string;
728
+ duration: number;
729
+ encodingFormat: string;
730
+ };
731
+
732
+ // Distributes over union types and allows picking keys from any member of the union type
733
+ type MediaDisplay = HomomorphicPick<ImageUpload | VideoUpload, "url" | "size" | "duration">;
734
+ //=> {url: string; size: number} | {url: string; duration: number}
735
+
736
+ // Primitive types are returned as-is
737
+ type Primitive = HomomorphicPick<string | number, 'toUpperCase' | 'toString'>;
738
+ //=> string | number
739
+
740
+ // Picks all keys if `Keys` is `any`
741
+ type Any = HomomorphicPick<{a: 1; b: 2} | {c: 3}, any>;
742
+ //=> {a: 1; b: 2} | {c: 3}
743
+
744
+ // Doesn't pick `number` from a `string` index signature
745
+ type IndexSignature = HomomorphicPick<{[k: string]: unknown}, number>;
746
+ //=> {}
747
+ */
748
+ type HomomorphicPick<T, Keys extends KeysOfUnion<T>> = { [P in keyof T as Extract<P, Keys>]: T[P] };
749
+ /**
750
+ Merges user specified options with default options.
751
+
752
+ @example
753
+ ```
754
+ type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean};
755
+ type DefaultPathsOptions = {maxRecursionDepth: 10; leavesOnly: false};
756
+ type SpecifiedOptions = {leavesOnly: true};
757
+
758
+ type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>;
759
+ //=> {maxRecursionDepth: 10; leavesOnly: true}
760
+ ```
761
+
762
+ @example
763
+ ```
764
+ // Complains if default values are not provided for optional options
765
+
766
+ type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean};
767
+ type DefaultPathsOptions = {maxRecursionDepth: 10};
768
+ type SpecifiedOptions = {};
769
+
770
+ type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>;
771
+ // ~~~~~~~~~~~~~~~~~~~
772
+ // Property 'leavesOnly' is missing in type 'DefaultPathsOptions' but required in type '{ maxRecursionDepth: number; leavesOnly: boolean; }'.
773
+ ```
774
+
775
+ @example
776
+ ```
777
+ // Complains if an option's default type does not conform to the expected type
778
+
779
+ type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean};
780
+ type DefaultPathsOptions = {maxRecursionDepth: 10; leavesOnly: 'no'};
781
+ type SpecifiedOptions = {};
782
+
783
+ type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>;
784
+ // ~~~~~~~~~~~~~~~~~~~
785
+ // Types of property 'leavesOnly' are incompatible. Type 'string' is not assignable to type 'boolean'.
786
+ ```
787
+
788
+ @example
789
+ ```
790
+ // Complains if an option's specified type does not conform to the expected type
791
+
792
+ type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean};
793
+ type DefaultPathsOptions = {maxRecursionDepth: 10; leavesOnly: false};
794
+ type SpecifiedOptions = {leavesOnly: 'yes'};
795
+
796
+ type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>;
797
+ // ~~~~~~~~~~~~~~~~
798
+ // Types of property 'leavesOnly' are incompatible. Type 'string' is not assignable to type 'boolean'.
799
+ ```
800
+ */
801
+ type ApplyDefaultOptions<Options extends object, Defaults extends Simplify<Omit<Required<Options>, RequiredKeysOf<Options>> & Partial<Record<RequiredKeysOf<Options>, never>>>, SpecifiedOptions extends Options> = If<IsAny<SpecifiedOptions>, Defaults, If<IsNever<SpecifiedOptions>, Defaults, Simplify<Merge<Defaults, { [Key in keyof SpecifiedOptions as Key extends OptionalKeysOf<Options> ? undefined extends SpecifiedOptions[Key] ? never : Key : Key]: SpecifiedOptions[Key] }> & Required<Options>>>>;
802
+ //#endregion
803
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/except.d.ts
804
+ /**
805
+ Filter out keys from an object.
806
+
807
+ Returns `never` if `Exclude` is strictly equal to `Key`.
808
+ Returns `never` if `Key` extends `Exclude`.
809
+ Returns `Key` otherwise.
810
+
811
+ @example
812
+ ```
813
+ type Filtered = Filter<'foo', 'foo'>;
814
+ //=> never
815
+ ```
816
+
817
+ @example
818
+ ```
819
+ type Filtered = Filter<'bar', string>;
820
+ //=> never
821
+ ```
822
+
823
+ @example
824
+ ```
825
+ type Filtered = Filter<'bar', 'foo'>;
826
+ //=> 'bar'
827
+ ```
828
+
829
+ @see {Except}
830
+ */
831
+ type Filter<KeyType, ExcludeType> = IsEqual<KeyType, ExcludeType> extends true ? never : (KeyType extends ExcludeType ? never : KeyType);
832
+ type ExceptOptions = {
833
+ /**
834
+ Disallow assigning non-specified properties.
835
+ Note that any omitted properties in the resulting type will be present in autocomplete as `undefined`.
836
+ @default false
837
+ */
838
+ requireExactProps?: boolean;
839
+ };
840
+ type DefaultExceptOptions = {
841
+ requireExactProps: false;
842
+ };
843
+ /**
844
+ Create a type from an object type without certain keys.
845
+
846
+ We recommend setting the `requireExactProps` option to `true`.
847
+
848
+ This type is a stricter version of [`Omit`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#the-omit-helper-type). The `Omit` type does not restrict the omitted keys to be keys present on the given type, while `Except` does. The benefits of a stricter type are avoiding typos and allowing the compiler to pick up on rename refactors automatically.
849
+
850
+ This type was proposed to the TypeScript team, which declined it, saying they prefer that libraries implement stricter versions of the built-in types ([microsoft/TypeScript#30825](https://github.com/microsoft/TypeScript/issues/30825#issuecomment-523668235)).
851
+
852
+ @example
853
+ ```
854
+ import type {Except} from 'type-fest';
855
+
856
+ type Foo = {
857
+ a: number;
858
+ b: string;
859
+ };
860
+
861
+ type FooWithoutA = Except<Foo, 'a'>;
862
+ //=> {b: string}
863
+
864
+ // @ts-expect-error
865
+ const fooWithoutA: FooWithoutA = {a: 1, b: '2'};
866
+ // errors: 'a' does not exist in type '{ b: string; }'
867
+
868
+ type FooWithoutB = Except<Foo, 'b', {requireExactProps: true}>;
869
+ //=> {a: number} & Partial<Record<'b', never>>
870
+
871
+ // @ts-expect-error
872
+ const fooWithoutB: FooWithoutB = {a: 1, b: '2'};
873
+ // errors at 'b': Type 'string' is not assignable to type 'undefined'.
874
+
875
+ // The `Omit` utility type doesn't work when omitting specific keys from objects containing index signatures.
876
+
877
+ // Consider the following example:
878
+
879
+ type UserData = {
880
+ [metadata: string]: string;
881
+ email: string;
882
+ name: string;
883
+ role: 'admin' | 'user';
884
+ };
885
+
886
+ // `Omit` clearly doesn't behave as expected in this case:
887
+ type PostPayload = Omit<UserData, 'email'>;
888
+ //=> {[x: string]: string; [x: number]: string}
889
+
890
+ // In situations like this, `Except` works better.
891
+ // It simply removes the `email` key while preserving all the other keys.
892
+ type PostPayloadFixed = Except<UserData, 'email'>;
893
+ //=> {[x: string]: string; name: string; role: 'admin' | 'user'}
894
+ ```
895
+
896
+ @category Object
897
+ */
898
+ type Except<ObjectType, KeysType extends keyof ObjectType, Options extends ExceptOptions = {}> = _Except<ObjectType, KeysType, ApplyDefaultOptions<ExceptOptions, DefaultExceptOptions, Options>>;
899
+ type _Except<ObjectType, KeysType extends keyof ObjectType, Options extends Required<ExceptOptions>> = { [KeyType in keyof ObjectType as Filter<KeyType, KeysType>]: ObjectType[KeyType] } & (Options['requireExactProps'] extends true ? Partial<Record<KeysType, never>> : {});
900
+ //#endregion
901
+ //#region ../../node_modules/.pnpm/type-fest@5.6.0/node_modules/type-fest/source/set-required.d.ts
902
+ /**
903
+ Create a type that makes the given keys required, while keeping the remaining keys as is.
904
+
905
+ Use-case: You want to define a single model where the only thing that changes is whether or not some of the keys are required.
906
+
907
+ @example
908
+ ```
909
+ import type {SetRequired} from 'type-fest';
910
+
911
+ type Foo = {
912
+ a?: number;
913
+ b: string;
914
+ c?: boolean;
915
+ };
916
+
917
+ type SomeRequired = SetRequired<Foo, 'b' | 'c'>;
918
+ //=> {a?: number; b: string; c: boolean}
919
+
920
+ // Set specific indices in an array to be required.
921
+ type ArrayExample = SetRequired<[number?, number?, number?], 0 | 1>;
922
+ //=> [number, number, number?]
923
+ ```
924
+
925
+ @category Object
926
+ */
927
+ type SetRequired<BaseType, Keys extends keyof BaseType> = (BaseType extends ((...arguments_: never) => any) ? (...arguments_: Parameters<BaseType>) => ReturnType<BaseType> : unknown) & _SetRequired<BaseType, Keys>;
928
+ type _SetRequired<BaseType, Keys extends keyof BaseType> = BaseType extends UnknownArray ? SetArrayRequired<BaseType, Keys> extends infer ResultantArray ? If<IsArrayReadonly<BaseType>, Readonly<ResultantArray>, ResultantArray> : never : Simplify< // Pick just the keys that are optional from the base type.
929
+ Except<BaseType, Keys> // Pick the keys that should be required from the base type and make them required.
930
+ & Required<HomomorphicPick<BaseType, Keys>>>;
931
+ /**
932
+ Remove the optional modifier from the specified keys in an array.
933
+ */
934
+ type SetArrayRequired<TArray extends UnknownArray, Keys, Counter extends any[] = [], Accumulator extends UnknownArray = []> = TArray extends unknown // For distributing `TArray` when it's a union
935
+ ? keyof TArray & `${number}` extends never // Exit if `TArray` is empty (e.g., []), or
936
+ // `TArray` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`).
937
+ ? [...Accumulator, ...TArray] : TArray extends readonly [(infer First)?, ...infer Rest] ? '0' extends OptionalKeysOf<TArray> // If the first element of `TArray` is optional
938
+ ? `${Counter['length']}` extends `${Keys & (string | number)}` // If the current index needs to be required
939
+ ? SetArrayRequired<Rest, Keys, [...Counter, any], [...Accumulator, First]> // If the current element is optional, but it doesn't need to be required,
940
+ // then we can exit early, since no further elements can now be made required.
941
+ : [...Accumulator, ...TArray] : SetArrayRequired<Rest, Keys, [...Counter, any], [...Accumulator, TArray[0]]> : never // Should never happen, since `[(infer F)?, ...infer R]` is a top-type for arrays.
942
+ : never; // Should never happen
943
+ //#endregion
944
+ //#region src/core/config-error.d.ts
945
+ /**
946
+ * Single validation problem reported by the schema validator. `path` is the
947
+ * sequence of keys and indices into the config root; `message` is a
948
+ * human-readable explanation.
949
+ *
950
+ * @example
951
+ *
952
+ * ```ts
953
+ * import type { ConfigValidationIssue } from "@bedrock-rbx/core";
954
+ *
955
+ * const issue: ConfigValidationIssue = {
956
+ * message: "must be a number",
957
+ * path: ["passes", "vip-pass", "price"],
958
+ * };
959
+ *
960
+ * expect(issue.path).toStrictEqual(["passes", "vip-pass", "price"]);
961
+ * ```
962
+ */
963
+ interface ConfigValidationIssue {
964
+ /** Human-readable explanation of why this field failed validation. */
965
+ readonly message: string;
966
+ /** Sequence of keys from the config root to the offending field. */
967
+ readonly path: ReadonlyArray<string>;
968
+ }
969
+ /**
970
+ * Failure surfaced by `loadConfig` when a project config cannot be resolved,
971
+ * parsed, or validated. Plain-data discriminated union; narrow on `kind`
972
+ * rather than using `instanceof`.
973
+ *
974
+ * Current cases:
975
+ * - `fileNotFound` - no `bedrock.config.*` (or other c12-discovered file)
976
+ * was found starting from the working directory.
977
+ * - `parseFailed` - a config file was found but could not be parsed (for
978
+ * example, malformed YAML or JSON). `message` carries the underlying
979
+ * parser message verbatim.
980
+ * - `validationFailed` - a config file was found and parsed, but its content
981
+ * did not satisfy the runtime schema. `issues` attributes each problem to
982
+ * a field path so callers can point at the offending entry.
983
+ * - `configFunctionFailed` - a function-form config threw or its returned
984
+ * promise rejected while being invoked. `message` carries the thrown
985
+ * error's message verbatim.
986
+ * - `luauRuntimeMissing` - a `bedrock.config.luau` file was found but the
987
+ * `lute` runtime needed to evaluate it could not be located on PATH or
988
+ * via the `BEDROCK_LUTE_PATH` environment variable. `hint` carries an
989
+ * actionable install message.
990
+ *
991
+ * @example
992
+ *
993
+ * ```ts
994
+ * import type { ConfigError } from "@bedrock-rbx/core";
995
+ *
996
+ * function describe(err: ConfigError): string {
997
+ * switch (err.kind) {
998
+ * case "fileNotFound": {
999
+ * return `no bedrock config under ${err.searchedFrom}`;
1000
+ * }
1001
+ * case "parseFailed": {
1002
+ * return `${err.sourceFile}: ${err.message}`;
1003
+ * }
1004
+ * case "configFunctionFailed": {
1005
+ * return `${err.sourceFile}: config function threw: ${err.message}`;
1006
+ * }
1007
+ * case "validationFailed": {
1008
+ * const first = err.issues[0];
1009
+ * return first
1010
+ * ? `${err.sourceFile}: ${first.path.join(".")} ${first.message}`
1011
+ * : `${err.sourceFile}: invalid`;
1012
+ * }
1013
+ * case "luauRuntimeMissing": {
1014
+ * return `${err.sourceFile}: ${err.hint}`;
1015
+ * }
1016
+ * }
1017
+ * }
1018
+ *
1019
+ * expect(describe({ kind: "fileNotFound", searchedFrom: "/proj" })).toBe(
1020
+ * "no bedrock config under /proj",
1021
+ * );
1022
+ * expect(
1023
+ * describe({
1024
+ * kind: "parseFailed",
1025
+ * sourceFile: "bedrock.config.yaml",
1026
+ * message: "unexpected end of the stream",
1027
+ * }),
1028
+ * ).toBe("bedrock.config.yaml: unexpected end of the stream");
1029
+ * expect(
1030
+ * describe({
1031
+ * kind: "configFunctionFailed",
1032
+ * sourceFile: "bedrock.config.ts",
1033
+ * message: "boom",
1034
+ * }),
1035
+ * ).toBe("bedrock.config.ts: config function threw: boom");
1036
+ * expect(
1037
+ * describe({
1038
+ * kind: "validationFailed",
1039
+ * sourceFile: "bedrock.config.ts",
1040
+ * issues: [{ path: ["passes", "vip", "price"], message: "must be a number" }],
1041
+ * }),
1042
+ * ).toBe("bedrock.config.ts: passes.vip.price must be a number");
1043
+ * expect(
1044
+ * describe({
1045
+ * kind: "luauRuntimeMissing",
1046
+ * sourceFile: "bedrock.config.luau",
1047
+ * hint: "install lute via mise",
1048
+ * }),
1049
+ * ).toBe("bedrock.config.luau: install lute via mise");
1050
+ * ```
1051
+ */
1052
+ type ConfigError = {
1053
+ readonly hint: string;
1054
+ readonly kind: "luauRuntimeMissing";
1055
+ readonly sourceFile: string;
1056
+ } | {
1057
+ readonly issues: ReadonlyArray<ConfigValidationIssue>;
1058
+ readonly kind: "validationFailed";
1059
+ readonly sourceFile: string;
1060
+ } | {
1061
+ readonly kind: "configFunctionFailed";
1062
+ readonly message: string;
1063
+ readonly sourceFile: string;
1064
+ } | {
1065
+ readonly kind: "fileNotFound";
1066
+ readonly searchedFrom: string;
1067
+ } | {
1068
+ readonly kind: "parseFailed";
1069
+ readonly message: string;
1070
+ readonly sourceFile: string;
1071
+ };
1072
+ //#endregion
1073
+ //#region src/core/schema.d.ts
1074
+ /**
1075
+ * Body of a single entry in the `passes` collection. Keys in the parent
1076
+ * record are `ResourceKey`-shaped strings enforced at schema validation.
1077
+ */
1078
+ interface GamePassEntry {
1079
+ /** Name shown on the Roblox storefront. */
1080
+ name: string;
1081
+ /** Description shown on the game-pass detail page. */
1082
+ description: string;
1083
+ /**
1084
+ * Locale-keyed icon paths. The map shape mirrors `UniverseEntry.icon`
1085
+ * so authors see a single vocabulary across icon-bearing kinds; v1
1086
+ * only accepts the `"en-us"` key. The Roblox game-pass API is
1087
+ * monolingual, so the `"en-us"` icon is the only one ever uploaded.
1088
+ */
1089
+ icon: Record<"en-us", string>;
1090
+ /** Robux price, or omitted / `undefined` for off-sale. */
1091
+ price?: number | undefined;
1092
+ }
1093
+ /**
1094
+ * Body of a single entry in the `products` collection. Keys in the parent
1095
+ * record are `ResourceKey`-shaped strings enforced at schema validation.
1096
+ */
1097
+ interface DeveloperProductEntry {
1098
+ /** Name shown on the Roblox storefront. */
1099
+ name: string;
1100
+ /** Description shown on the developer-product detail page. */
1101
+ description: string;
1102
+ /**
1103
+ * Locale-keyed icon paths. Mirrors `GamePassEntry.icon` and
1104
+ * `UniverseEntry.icon`; the Roblox developer-product API is monolingual,
1105
+ * so the `"en-us"` icon is the only one ever uploaded.
1106
+ */
1107
+ icon?: Record<"en-us", string>;
1108
+ /**
1109
+ * Whether Roblox-managed regional pricing applies to the product.
1110
+ * Tri-state: omit (or set `undefined`) to leave the flag unmanaged;
1111
+ * setting `true` or `false` is propagated to Roblox on every deploy.
1112
+ */
1113
+ isRegionalPricingEnabled?: boolean | undefined;
1114
+ /**
1115
+ * Robux price. Omit (or set `undefined`) for an off-sale product;
1116
+ * re-adding the field puts the product back on sale on the next deploy.
1117
+ */
1118
+ price?: number | undefined;
1119
+ /**
1120
+ * Whether the product appears on the universe's external store page.
1121
+ * Tri-state: omit (or set `undefined`) to leave the flag unmanaged.
1122
+ * The Roblox v2 create endpoint does not accept this field, so the
1123
+ * driver applies it via a follow-up PATCH after the create POST.
1124
+ */
1125
+ storePageEnabled?: boolean | undefined;
1126
+ }
1127
+ /**
1128
+ * Body of a single entry under the root `places` collection. Carries the
1129
+ * file-path environments share plus the optional Open-Cloud-supported
1130
+ * metadata fields. The Roblox `placeId` is environment-specific and lives
1131
+ * on each per-environment overlay so the same `.rbxl` file can publish to
1132
+ * different places across staging, production, and so on.
1133
+ */
1134
+ interface PlaceEntry {
1135
+ /** User-facing description shown on the place's detail page. */
1136
+ description?: string | undefined;
1137
+ /** User-facing place name shown on the Roblox storefront. */
1138
+ displayName?: string | undefined;
1139
+ /** Path to the `.rbxl` or `.rbxlx` file; handed to `readFile` verbatim by `buildDesired`. */
1140
+ filePath: string;
1141
+ /** Maximum players per server; positive integer. */
1142
+ serverSize?: number | undefined;
1143
+ }
1144
+ /**
1145
+ * Body of a places entry after `selectEnvironment` has merged the
1146
+ * matching per-environment overlay onto the root entry. `filePath` flows
1147
+ * from the root (or an overlay override), `placeId` is supplied by the
1148
+ * per-environment overlay, and the optional metadata fields fall through
1149
+ * from the root unless overridden per-environment.
1150
+ *
1151
+ * `placeId` is user-supplied because Open Cloud cannot mint places; the
1152
+ * place must already exist in Roblox before Bedrock can publish versions
1153
+ * to it.
1154
+ */
1155
+ interface ResolvedPlaceEntry {
1156
+ /** User-facing description shown on the place's detail page. */
1157
+ description?: string | undefined;
1158
+ /** User-facing place name shown on the Roblox storefront. */
1159
+ displayName?: string | undefined;
1160
+ /** Path to the `.rbxl` or `.rbxlx` file; handed to `readFile` verbatim by `buildDesired`. */
1161
+ filePath: string;
1162
+ /** Existing Roblox place ID. */
1163
+ placeId: string;
1164
+ /** Maximum players per server; positive integer. */
1165
+ serverSize?: number | undefined;
1166
+ }
1167
+ /**
1168
+ * Body of the singleton `universe` block. Bedrock synthesizes the
1169
+ * `ResourceKey` (`"main"`) in `flattenConfig`, so user config supplies
1170
+ * only the existing `universeId` plus any managed fields they want
1171
+ * bedrock to own. Fields omitted here remain unmanaged (the diff treats
1172
+ * them as non-drift and the driver omits them from the `updateMask`).
1173
+ *
1174
+ * `universeId` is user-supplied because Open Cloud cannot mint universes;
1175
+ * the universe must already exist in Roblox before bedrock can reconcile
1176
+ * its configuration. Declare `universeId` either here at the root (which
1177
+ * applies to every environment) or under each `environments[name].universe`
1178
+ * overlay, but never both: the schema rejects a config that sets it in
1179
+ * both places, and rejects a `universe` block without a resolvable
1180
+ * `universeId`.
1181
+ */
1182
+ interface UniverseEntry {
1183
+ /** Whether console players can join; omit or set `undefined` to leave unmanaged. */
1184
+ consoleEnabled?: boolean | undefined;
1185
+ /** Whether desktop players can join; omit or set `undefined` to leave unmanaged. */
1186
+ desktopEnabled?: boolean | undefined;
1187
+ /**
1188
+ * Discord social link; omit to leave the server value untouched, set to
1189
+ * `undefined` to clear it, or set to a `SocialLink` to update it.
1190
+ */
1191
+ discordSocialLink?: SocialLink | undefined;
1192
+ /**
1193
+ * Display name for the universe. Because Roblox derives this from
1194
+ * the root place's name, the driver routes the update through
1195
+ * `PlacesClient.update`; omit or set `undefined` to leave unmanaged.
1196
+ */
1197
+ displayName?: string | undefined;
1198
+ /**
1199
+ * Facebook social link; omit to leave the server value untouched, set to
1200
+ * `undefined` to clear it, or set to a `SocialLink` to update it.
1201
+ */
1202
+ facebookSocialLink?: SocialLink | undefined;
1203
+ /**
1204
+ * Guilded social link; omit to leave the server value untouched, set to
1205
+ * `undefined` to clear it, or set to a `SocialLink` to update it.
1206
+ */
1207
+ guildedSocialLink?: SocialLink | undefined;
1208
+ /**
1209
+ * Locale-keyed experience-icon paths. The map shape teaches that icons
1210
+ * are per-locale; v1 only accepts the `"en-us"` key. Omit to leave the
1211
+ * server icon unmanaged; remove a previously declared locale to delete
1212
+ * its icon on the next deploy.
1213
+ */
1214
+ icon?: Record<"en-us", string>;
1215
+ /** Whether mobile players can join; omit or set `undefined` to leave unmanaged. */
1216
+ mobileEnabled?: boolean | undefined;
1217
+ /**
1218
+ * Private-server price in Robux. Declare as `undefined` to disable
1219
+ * private servers (cancels active subscriptions); omit to leave the
1220
+ * server value untouched.
1221
+ */
1222
+ privateServerPriceRobux?: number | undefined;
1223
+ /**
1224
+ * Roblox Group social link; omit to leave the server value untouched, set
1225
+ * to `undefined` to clear it, or set to a `SocialLink` to update it.
1226
+ */
1227
+ robloxGroupSocialLink?: SocialLink | undefined;
1228
+ /** Whether tablet players can join; omit or set `undefined` to leave unmanaged. */
1229
+ tabletEnabled?: boolean | undefined;
1230
+ /**
1231
+ * Twitch social link; omit to leave the server value untouched, set to
1232
+ * `undefined` to clear it, or set to a `SocialLink` to update it.
1233
+ */
1234
+ twitchSocialLink?: SocialLink | undefined;
1235
+ /**
1236
+ * Twitter social link; omit to leave the server value untouched, set to
1237
+ * `undefined` to clear it, or set to a `SocialLink` to update it.
1238
+ */
1239
+ twitterSocialLink?: SocialLink | undefined;
1240
+ /**
1241
+ * Existing Roblox universe ID. Optional in this entry shape because
1242
+ * authors may declare it here (root-authoritative, single universe) or
1243
+ * on each `environments[name].universe` overlay (per-environment
1244
+ * universes), but never both.
1245
+ */
1246
+ universeId?: string | undefined;
1247
+ /** Whether voice chat is enabled; omit or set `undefined` to leave unmanaged. */
1248
+ voiceChatEnabled?: boolean | undefined;
1249
+ /** Whether VR players can join; omit or set `undefined` to leave unmanaged. */
1250
+ vrEnabled?: boolean | undefined;
1251
+ /**
1252
+ * YouTube social link; omit to leave the server value untouched, set to
1253
+ * `undefined` to clear it, or set to a `SocialLink` to update it.
1254
+ */
1255
+ youtubeSocialLink?: SocialLink | undefined;
1256
+ }
1257
+ /**
1258
+ * State configuration for the GitHub Gist backend. Holds the public gist
1259
+ * ID; the GitHub token is read from `GITHUB_TOKEN` only when the library
1260
+ * default-constructs the adapter.
1261
+ */
1262
+ interface GistStateConfig {
1263
+ /** Discriminator selecting the gist adapter. */
1264
+ readonly backend: "gist";
1265
+ /** ID of an existing GitHub Gist that holds this project's state files. */
1266
+ readonly gistId: string;
1267
+ }
1268
+ /**
1269
+ * Tagged union describing where Bedrock persists its state. The `backend`
1270
+ * tag is `"gist" | (string & {})` so unknown names autocomplete the
1271
+ * builtins while permitting custom values for plugin scenarios. The
1272
+ * dispatch path inside `deploy()` rejects unknown names with a typed
1273
+ * `unsupportedBackend` error.
1274
+ */
1275
+ type StateConfig = GistStateConfig | {
1276
+ readonly backend: string & {};
1277
+ };
1278
+ /**
1279
+ * Body of a single entry under `environments`. Per-environment overrides
1280
+ * narrow root-level settings for that environment without redefining
1281
+ * unrelated fields. Resource overlays (`passes`, `places`, `universe`)
1282
+ * derive their field shapes from the matching root entry types so adding
1283
+ * a field to a base entry surfaces on the overlay automatically.
1284
+ *
1285
+ * `placeId` stays required when the matching `places` overlay is present
1286
+ * because each environment targets its own Roblox place. `universeId` is
1287
+ * optional on the `universe` overlay because authors may declare it
1288
+ * either at the root (root-authoritative) or per environment, but never
1289
+ * both: the schema enforces this XOR at validation time, attributing the
1290
+ * failure to the offending field's path.
1291
+ */
1292
+ interface EnvironmentEntry {
1293
+ /**
1294
+ * Human-readable label fed to the project-level
1295
+ * {@link DisplayNamePrefixConfig.format | displayNamePrefix.format}
1296
+ * template. An environment without a label (or with an empty string)
1297
+ * is implicitly excluded from prefixing even when the project enables
1298
+ * it.
1299
+ */
1300
+ label?: string | undefined;
1301
+ /**
1302
+ * Per-environment game-pass overlay. Every field is optional; missing
1303
+ * fields fall through to the matching root `passes` entry at merge time.
1304
+ *
1305
+ * Uses `Partial<GamePassEntry>` directly rather than `Overlay<T, K>`
1306
+ * because game passes have no user-supplied identity key (Open Cloud
1307
+ * mints the asset ID). The other overlay fields use `Overlay<T, K>`
1308
+ * to keep their identity-bearing key required.
1309
+ */
1310
+ passes?: Record<string, Partial<GamePassEntry>>;
1311
+ /**
1312
+ * Per-environment places overlay. `placeId` is required on every
1313
+ * declared entry; `filePath` is optional and falls through to the
1314
+ * matching root `places` entry when omitted.
1315
+ */
1316
+ places?: Record<string, Overlay<ResolvedPlaceEntry, "placeId">>;
1317
+ /**
1318
+ * Per-environment developer-product overlay. Every field is optional;
1319
+ * missing fields fall through to the matching root `products` entry at
1320
+ * merge time. Mirrors the `passes` shape because developer products
1321
+ * also have no user-supplied identity key (Open Cloud mints the
1322
+ * `productId`).
1323
+ */
1324
+ products?: Record<string, Partial<DeveloperProductEntry>>;
1325
+ /** Per-environment state override; takes precedence over root `state`. */
1326
+ state?: StateConfig;
1327
+ /**
1328
+ * Per-environment universe overlay. Every field is optional, including
1329
+ * `universeId`: the schema-level XOR rule requires `universeId` here if
1330
+ * and only if the root `universe` block does not declare one. Other
1331
+ * fields fall through to the root `universe` block when omitted.
1332
+ */
1333
+ universe?: Partial<UniverseEntry>;
1334
+ }
1335
+ /**
1336
+ * Per-kind entry registry. Each `ResourceKind` must have a matching entry
1337
+ * type or `ResourceEntryByKind[K]` is a compile error. Modelled as an
1338
+ * interface (not a type alias) so downstream resource kinds can declare
1339
+ * their entry type alongside the kind's other domain types without
1340
+ * touching this module.
1341
+ *
1342
+ * @example
1343
+ *
1344
+ * ```ts
1345
+ * import type { ResourceEntryByKind } from "@bedrock-rbx/core/config";
1346
+ *
1347
+ * const entry: ResourceEntryByKind["gamePass"] = {
1348
+ * description: "Grants VIP perks.",
1349
+ * icon: { "en-us": "assets/vip-icon.png" },
1350
+ * name: "VIP Pass",
1351
+ * price: 500,
1352
+ * };
1353
+ *
1354
+ * expect(entry.name).toBe("VIP Pass");
1355
+ * ```
1356
+ */
1357
+ interface ResourceEntryByKind {
1358
+ /** Authored entry body for a developer-product resource. */
1359
+ developerProduct: DeveloperProductEntry;
1360
+ /** Authored entry body for a game-pass resource. */
1361
+ gamePass: GamePassEntry;
1362
+ /** Post-merge entry body for a place resource (root + env overlay). */
1363
+ place: ResolvedPlaceEntry;
1364
+ /** Authored entry body for a universe resource. */
1365
+ universe: UniverseEntry;
1366
+ }
1367
+ /**
1368
+ * Project-level prefixing policy for universe and place display names.
1369
+ * Each environment's `label` flows through `format` to render a prefix
1370
+ * that `selectEnvironment` prepends to every declared display name.
1371
+ *
1372
+ * Defaults: `enabled` is `true`; `format` is `"[{LABEL}] "`.
1373
+ */
1374
+ interface DisplayNamePrefixConfig {
1375
+ /**
1376
+ * Whether the project applies environment-label prefixing. Treat
1377
+ * `undefined` as enabled; set `false` to opt out across the project.
1378
+ */
1379
+ enabled?: boolean | undefined;
1380
+ /**
1381
+ * Template string applied to each environment's `label`. Placeholders:
1382
+ *
1383
+ * - `{label}`: label as written.
1384
+ * - `{LABEL}`: upper-cased label.
1385
+ * - `{Label}`: capitalized label (first character upper, rest as
1386
+ * written).
1387
+ *
1388
+ * Any other characters in the template flow through verbatim. The
1389
+ * rendered string is prepended to each declared `displayName`.
1390
+ */
1391
+ format?: string | undefined;
1392
+ }
1393
+ /**
1394
+ * Per-environment universe overlay shape that prevents `universeId` from
1395
+ * being redeclared alongside a root-authoritative `universeId`.
1396
+ * Used by {@link ConfigRootUniverseId}: when the root universe block
1397
+ * declares `universeId`, no per-env overlay may redeclare it. Setting
1398
+ * `universeId` here produces a descriptive type error pointing at this
1399
+ * field rather than the opaque `never` message.
1400
+ */
1401
+ type UniverseOverlayWithoutId = Partial<WithoutKey<UniverseEntry, "universeId">> & {
1402
+ universeId?: "universeId is already declared on the root universe block; remove it from this environment overlay, or remove it from root and declare it on every environment overlay instead" & {
1403
+ readonly errorBrand: never;
1404
+ };
1405
+ };
1406
+ /**
1407
+ * Per-environment universe overlay shape that requires `universeId`.
1408
+ * Used by {@link ConfigEnvironmentUniverseId}: when the root universe
1409
+ * block does not declare `universeId`, every env that declares a
1410
+ * `universe` overlay must supply one of its own.
1411
+ */
1412
+ type UniverseOverlayWithId = Partial<WithoutKey<UniverseEntry, "universeId">> & {
1413
+ universeId: string;
1414
+ };
1415
+ /**
1416
+ * Variant of `Config` where the root `universe` block declares
1417
+ * `universeId`. Per-environment universe overlays may carry shared
1418
+ * fields (device flags, social links, display name, icon) but cannot
1419
+ * redeclare `universeId`; the schema rejects any env overlay that
1420
+ * does. The runtime `selectEnvironment` merges shared-field overlays
1421
+ * onto the root and inherits `universeId` from the root unchanged.
1422
+ */
1423
+ type ConfigRootUniverseId = ConfigBase & {
1424
+ /**
1425
+ * Per-environment overrides keyed by environment name. Required and
1426
+ * non-empty; environment names match `[A-Za-z0-9_-]{1,64}`. Each env
1427
+ * entry's `universe` overlay forbids `universeId` because the root
1428
+ * declares it.
1429
+ */
1430
+ environments: Record<string, WithoutKey<EnvironmentEntry, "universe"> & {
1431
+ universe?: UniverseOverlayWithoutId;
1432
+ }>;
1433
+ /**
1434
+ * Singleton universe block declaring the Roblox universe bedrock
1435
+ * manages. `universeId` is required in this variant because no
1436
+ * per-environment overlay may supply one.
1437
+ */
1438
+ universe?: UniverseEntry & {
1439
+ universeId: string;
1440
+ };
1441
+ };
1442
+ /**
1443
+ * Variant of `Config` where the root `universe` block omits
1444
+ * `universeId`. Every env that declares a `universe` overlay must
1445
+ * supply its own `universeId`; envs that omit the overlay deploy no
1446
+ * universe at all. The root may still carry shared fields (device
1447
+ * flags, social links, display name, icon) which `selectEnvironment`
1448
+ * merges onto each env's overlay at resolution time.
1449
+ */
1450
+ type ConfigEnvironmentUniverseId = ConfigBase & {
1451
+ /**
1452
+ * Per-environment overrides keyed by environment name. Required and
1453
+ * non-empty; environment names match `[A-Za-z0-9_-]{1,64}`. Every
1454
+ * env that declares a `universe` overlay must include `universeId`
1455
+ * because the root universe block does not provide one.
1456
+ */
1457
+ environments: Record<string, WithoutKey<EnvironmentEntry, "universe"> & {
1458
+ universe?: UniverseOverlayWithId;
1459
+ }>;
1460
+ /**
1461
+ * Singleton universe block declaring the Roblox universe bedrock
1462
+ * manages. `universeId` is not permitted here in this variant because
1463
+ * every environment supplies its own; setting it produces a descriptive
1464
+ * type error rather than the opaque `never` message.
1465
+ */
1466
+ universe?: WithoutKey<UniverseEntry, "universeId"> & {
1467
+ universeId?: "universeId is already declared per environment; remove it from the root universe block, or remove it from every environment overlay and declare it here instead" & {
1468
+ readonly errorBrand: never;
1469
+ };
1470
+ };
1471
+ };
1472
+ /**
1473
+ * Validated project config as accepted by `loadConfig`. Plain mutable so
1474
+ * users can adjust fields in a long-running script before deploying.
1475
+ *
1476
+ * Discriminated union over the location of `universeId`: it lives at the
1477
+ * root universe block ({@link ConfigRootUniverseId}) or on every
1478
+ * environment universe overlay ({@link ConfigEnvironmentUniverseId}), but never
1479
+ * both. The TypeScript types reject the both-set case at compile time,
1480
+ * and the arktype runtime narrow rejects every offending field path at
1481
+ * `validateConfig` time. State must be configured at the root or under
1482
+ * every entry of `environments`; `resolveStateConfig` surfaces the
1483
+ * missing case at the deploy boundary as `stateNotConfigured`.
1484
+ *
1485
+ * @example
1486
+ *
1487
+ * ```ts
1488
+ * import type { Config } from "@bedrock-rbx/core/config";
1489
+ *
1490
+ * const config: Config = {
1491
+ * environments: { production: {} },
1492
+ * state: { backend: "gist", gistId: "abc123def456" },
1493
+ * passes: {
1494
+ * "vip-pass": {
1495
+ * description: "Grants VIP perks.",
1496
+ * icon: { "en-us": "assets/vip-icon.png" },
1497
+ * name: "VIP Pass",
1498
+ * price: 500,
1499
+ * },
1500
+ * },
1501
+ * };
1502
+ *
1503
+ * expect(config.passes!["vip-pass"]!.name).toBe("VIP Pass");
1504
+ * ```
1505
+ */
1506
+ type Config = ConfigEnvironmentUniverseId | ConfigRootUniverseId;
1507
+ /**
1508
+ * Body of the singleton `universe` block after `selectEnvironment` has
1509
+ * merged a per-environment overlay onto the root. Identical to
1510
+ * {@link UniverseEntry} except `universeId` is required: the schema-level
1511
+ * XOR rule ensures every projected universe carries a resolved
1512
+ * `universeId`. Resource drivers consume this shape rather than
1513
+ * `UniverseEntry` so the post-merge invariant is visible in the type
1514
+ * system.
1515
+ */
1516
+ interface ResolvedUniverseEntry extends Pick<UniverseEntry, Exclude<keyof UniverseEntry, "universeId">> {
1517
+ /** Existing Roblox universe ID, resolved from the root or per-environment overlay. */
1518
+ universeId: string;
1519
+ }
1520
+ /**
1521
+ * Project config after `selectEnvironment` has merged a single
1522
+ * environment's overlays onto the root. The shape mirrors `Config`
1523
+ * except `places` carries `ResolvedPlaceEntry` (both `filePath` and
1524
+ * `placeId`), since the resolver fails before this point if an entry is
1525
+ * missing its environment-supplied `placeId`. Downstream consumers
1526
+ * (`flattenConfig`, `buildDefaultRegistry`, the deploy pipeline) accept
1527
+ * this shape rather than `Config` so the post-merge invariant is visible
1528
+ * in the type system.
1529
+ *
1530
+ * @example
1531
+ *
1532
+ * ```ts
1533
+ * import { selectEnvironment, type ResolvedConfig } from "@bedrock-rbx/core";
1534
+ * import type { Config } from "@bedrock-rbx/core/config";
1535
+ *
1536
+ * const config: Config = {
1537
+ * environments: {
1538
+ * production: { places: { "start-place": { placeId: "4711" } } },
1539
+ * },
1540
+ * places: { "start-place": { filePath: "places/start.rbxl" } },
1541
+ * state: { backend: "gist", gistId: "abc" },
1542
+ * };
1543
+ *
1544
+ * const result = selectEnvironment(config, "production");
1545
+ * expect(result.success).toBeTrue();
1546
+ * if (result.success) {
1547
+ * const resolved: ResolvedConfig = result.data;
1548
+ * expect(resolved.places?.["start-place"]?.placeId).toBe("4711");
1549
+ * }
1550
+ * ```
1551
+ */
1552
+ interface ResolvedConfig extends Pick<ConfigBase, Exclude<keyof ConfigBase, "places">> {
1553
+ /**
1554
+ * Per-environment overrides preserved from the source `Config`.
1555
+ * Carried for downstream context; `selectEnvironment` does not read
1556
+ * other environments after resolving the requested one.
1557
+ */
1558
+ environments: Record<string, EnvironmentEntry>;
1559
+ /** Keyed-map collection of resolved place entries; both `filePath` and `placeId` are present. */
1560
+ places?: Record<string, ResolvedPlaceEntry>;
1561
+ /**
1562
+ * Singleton universe block after `selectEnvironment` has resolved the
1563
+ * XOR between root and per-environment `universeId`. The schema narrow
1564
+ * rejects any config that would leave `universeId` unresolved, so the
1565
+ * post-merge invariant promotes `universeId` from optional to required.
1566
+ */
1567
+ universe?: ResolvedUniverseEntry;
1568
+ }
1569
+ /**
1570
+ * Overlay shape used by per-environment entries: every field of `T`
1571
+ * becomes optional, except `RequiredKey`, which stays required so the
1572
+ * overlay still re-asserts the identity-bearing field of its target
1573
+ * resource.
1574
+ *
1575
+ * @template T - Base entry type whose field shapes the overlay derives from.
1576
+ * @template RequiredKey - Identity-bearing key on `T` that the overlay must
1577
+ * still declare (for example `"placeId"` or `"universeId"`).
1578
+ */
1579
+ type Overlay<T, RequiredKey extends keyof T> = SetRequired<Partial<T>, RequiredKey>;
1580
+ /**
1581
+ * Helper that produces a shallow `Omit<T, K>` without using TypeScript's
1582
+ * built-in `Omit` (deprecated under the project's lint rules because of
1583
+ * its lossy interaction with mapped types).
1584
+ *
1585
+ * @template T - Source type to project keys away from.
1586
+ * @template Key - Key (or union of keys) on `T` to remove.
1587
+ */
1588
+ type WithoutKey<T, Key extends keyof T> = Pick<T, Exclude<keyof T, Key>>;
1589
+ /**
1590
+ * Fields shared by every {@link Config} variant. The discriminated
1591
+ * `Config` union narrows `universe` and `environments` to enforce the
1592
+ * `universeId` XOR rule between the root and per-environment overlays;
1593
+ * everything else lives here.
1594
+ */
1595
+ interface ConfigBase {
1596
+ /**
1597
+ * Project-level prefixing of universe and place display names with the
1598
+ * environment label. Default behaviour (when omitted) is enabled with a
1599
+ * `"[{LABEL}] "` template; set `enabled: false` to opt out, or set
1600
+ * `format` to a custom template.
1601
+ */
1602
+ displayNamePrefix?: DisplayNamePrefixConfig;
1603
+ /** Reserved at the root for c12's config layering / overlay work. */
1604
+ extends?: unknown;
1605
+ /** Keyed-map collection of game-pass entries by user-supplied ResourceKey. */
1606
+ passes?: Record<string, GamePassEntry>;
1607
+ /** Keyed-map collection of place entries by user-supplied ResourceKey. */
1608
+ places?: Record<string, PlaceEntry>;
1609
+ /** Keyed-map collection of developer-product entries by user-supplied ResourceKey. */
1610
+ products?: Record<string, DeveloperProductEntry>;
1611
+ /** Where Bedrock persists state for this project; required at deploy time. */
1612
+ state?: StateConfig;
1613
+ }
1614
+ /**
1615
+ * Narrow a `StateConfig` to the `GistStateConfig` arm. The `(string & {})`
1616
+ * autocomplete idiom prevents TypeScript from narrowing on
1617
+ * `backend === "gist"` alone, so dispatch sites use this guard to
1618
+ * preserve the `gistId` field shape.
1619
+ *
1620
+ * @example
1621
+ *
1622
+ * ```ts
1623
+ * import { isGistStateConfig } from "@bedrock-rbx/core";
1624
+ * import type { StateConfig } from "@bedrock-rbx/core/config";
1625
+ *
1626
+ * const config: StateConfig = { backend: "gist", gistId: "abc" };
1627
+ *
1628
+ * expect(isGistStateConfig(config)).toBeTrue();
1629
+ * if (isGistStateConfig(config)) {
1630
+ * expect(config.gistId).toBe("abc");
1631
+ * }
1632
+ * ```
1633
+ *
1634
+ * @param config - Resolved state config to inspect.
1635
+ * @returns `true` when `config.backend === "gist"`; otherwise `false`.
1636
+ */
1637
+ declare function isGistStateConfig(config: StateConfig): config is GistStateConfig;
1638
+ /**
1639
+ * Validate a parsed config value against the runtime schema. Returns the
1640
+ * validated `Config` on success or a `validationFailed` `ConfigError` with
1641
+ * one issue per problem, each attributed to a field path. `sourceFile`
1642
+ * appears in the error so callers can point a human at the offending file.
1643
+ *
1644
+ * @param input - Parsed value from a config source (object tree from a
1645
+ * config loader, or a hand-built literal). Shape is checked, not assumed.
1646
+ * @param sourceFile - Path or identifier of the source file, used in the
1647
+ * `validationFailed` error.
1648
+ * @returns `Ok` with the validated `Config`, or `Err` with a
1649
+ * `validationFailed` error carrying each issue's field path.
1650
+ * @example
1651
+ *
1652
+ * ```ts
1653
+ * import { validateConfig } from "@bedrock-rbx/core";
1654
+ *
1655
+ * const ok = validateConfig(
1656
+ * {
1657
+ * environments: { production: {} },
1658
+ * passes: {
1659
+ * "vip-pass": {
1660
+ * description: "VIP perks.",
1661
+ * icon: { "en-us": "assets/vip.png" },
1662
+ * name: "VIP Pass",
1663
+ * price: 500,
1664
+ * },
1665
+ * },
1666
+ * },
1667
+ * "bedrock.config.ts",
1668
+ * );
1669
+ * expect(ok.success).toBeTrue();
1670
+ *
1671
+ * const err = validateConfig(
1672
+ * { environments: { production: {} }, passes: { "vip-pass": { name: "VIP" } } },
1673
+ * "bedrock.config.ts",
1674
+ * );
1675
+ * expect(err.success).toBeFalse();
1676
+ * if (!err.success) {
1677
+ * expect(err.err.kind).toBe("validationFailed");
1678
+ * }
1679
+ * ```
1680
+ */
1681
+ declare function validateConfig(input: unknown, sourceFile: string): Result<Config, ConfigError>;
1682
+ //#endregion
1683
+ //#region src/shell/define-config.d.ts
1684
+ /**
1685
+ * Context object passed to a config-function input. Intentionally empty so
1686
+ * future ADRs can add fields without breaking existing user configs.
1687
+ */
1688
+ interface ConfigContext {}
1689
+ /**
1690
+ * Input accepted by `defineConfig`: a plain `Config` object, or a
1691
+ * (sync or async) function that returns one given a `ConfigContext`.
1692
+ */
1693
+ type ConfigInput = ((ctx: ConfigContext) => Config | Promise<Config>) | Config;
1694
+ /**
1695
+ * Identity helper that gives TypeScript users full inference over a config
1696
+ * declared in a `bedrock.config.ts` file. Returns its argument unchanged so
1697
+ * `defineConfig(...)` is free at runtime.
1698
+ *
1699
+ * Accepts a plain `Config` object or a function that produces one. The
1700
+ * function form lets users compute config values from external data at load
1701
+ * time; `loadConfig` awaits the result on call.
1702
+ *
1703
+ * @template T - Narrow `ConfigInput` subtype preserved across the call so
1704
+ * downstream inference does not widen to `Config | (ctx) => Config`.
1705
+ * @param config - Either a `Config` literal or a function returning one.
1706
+ * @returns The same argument, typed as the narrower `T` so downstream
1707
+ * inference does not widen.
1708
+ * @example
1709
+ *
1710
+ * ```ts
1711
+ * import { defineConfig } from "@bedrock-rbx/core/config";
1712
+ *
1713
+ * const config = defineConfig({
1714
+ * environments: { production: {} },
1715
+ * passes: {
1716
+ * "vip-pass": {
1717
+ * description: "Grants VIP perks.",
1718
+ * icon: { "en-us": "assets/vip-icon.png" },
1719
+ * name: "VIP Pass",
1720
+ * price: 500,
1721
+ * },
1722
+ * },
1723
+ * });
1724
+ *
1725
+ * expect(config.passes!["vip-pass"]!.name).toBe("VIP Pass");
1726
+ * ```
1727
+ */
1728
+ declare function defineConfig<T extends ConfigInput>(config: T): T;
1729
+ //#endregion
1730
+ export { ConfigError as C, validateConfig as S, StateConfig as _, ConfigEnvironmentUniverseId as a, UniverseOverlayWithoutId as b, DisplayNamePrefixConfig as c, GistStateConfig as d, PlaceEntry as f, ResourceEntryByKind as g, ResolvedUniverseEntry as h, Config as i, EnvironmentEntry as l, ResolvedPlaceEntry as m, ConfigInput as n, ConfigRootUniverseId as o, ResolvedConfig as p, defineConfig as r, DeveloperProductEntry as s, ConfigContext as t, GamePassEntry as u, UniverseEntry as v, ConfigValidationIssue as w, isGistStateConfig as x, UniverseOverlayWithId as y };
1731
+ //# sourceMappingURL=define-config-D-LAhfSJ.d.mts.map