@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.
- package/LICENSE +21 -0
- package/dist/cli/run.d.mts +1 -0
- package/dist/cli/run.mjs +1476 -0
- package/dist/cli/run.mjs.map +1 -0
- package/dist/config.d.mts +3 -0
- package/dist/config.mjs +2 -0
- package/dist/define-config-CroC96-C.mjs +42 -0
- package/dist/define-config-CroC96-C.mjs.map +1 -0
- package/dist/define-config-D-LAhfSJ.d.mts +1731 -0
- package/dist/define-config-D-LAhfSJ.d.mts.map +1 -0
- package/dist/index.d.mts +3449 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +67 -0
- package/dist/index.mjs.map +1 -0
- package/dist/migrate-mantle-state-DqbJ1TLq.mjs +5789 -0
- package/dist/migrate-mantle-state-DqbJ1TLq.mjs.map +1 -0
- package/package.json +80 -0
|
@@ -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
|